Skip to content

Commit 1f0c03d

Browse files
Processing: Add Dissolve Command & A better Form structure for Processing Commands (#550)
* Processing: Add `Dissolve` Command * not needed * Apply suggestions from code review Co-authored-by: martinRenou <martin.renou@gmail.com> * fix * for convinience * extend dialog * push it * make it work with baseForm * remove `SourceType` from baseform * move `dialogOptions` to layerpropertiesform * using baseform directly * form doesn't show up * rename dissolveform * use `ISourceFormProps` from a common sourceform * sourcepropertiesform * sourceform in editform too * `creationformdialog` -> `layerCreationFormdialog` * `ProcessingFormDialog` * fix filename * fix filename * dynamic form selection * launch dialog * it's working 🎉 * lint * not required * Ok button fixed * lint * Apply suggestions from code review Co-authored-by: martinRenou <martin.renou@gmail.com> --------- Co-authored-by: martinRenou <martin.renou@gmail.com>
1 parent 2ed561a commit 1f0c03d

23 files changed

+553
-455
lines changed

examples/world.jGIS

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{
22
"layerTree": [
3-
"6e55cdae-35b0-4bff-9dd1-c8aa9563b2a6"
3+
"6e55cdae-35b0-4bff-9dd1-c8aa9563b2a6",
4+
"f80d0fa2-3e2b-4922-b7d5-fefd4b085259"
45
],
56
"layers": {
67
"6e55cdae-35b0-4bff-9dd1-c8aa9563b2a6": {
7-
"name": "Custom GeoJSON Layer",
8+
"name": "World",
89
"parameters": {
910
"color": {
1011
"fill-color": [
@@ -95,26 +96,53 @@
9596
},
9697
"type": "VectorLayer",
9798
"visible": true
99+
},
100+
"f80d0fa2-3e2b-4922-b7d5-fefd4b085259": {
101+
"name": "France",
102+
"parameters": {
103+
"color": {
104+
"fill-color": "#ff0000",
105+
"stroke-color": "#3399CC",
106+
"stroke-line-cap": "round",
107+
"stroke-line-join": "round",
108+
"stroke-width": 1.25
109+
},
110+
"opacity": 1.0,
111+
"source": "5970d6c9-26be-4cc6-84c9-16593dd3edfe",
112+
"symbologyState": {
113+
"renderType": "Single Symbol"
114+
},
115+
"type": "line"
116+
},
117+
"type": "VectorLayer",
118+
"visible": true
98119
}
99120
},
100121
"metadata": {},
101122
"options": {
102123
"bearing": 0.0,
103124
"extent": [
104-
-35938860.79774074,
105-
-17466155.24107265,
106-
4136155.887837734,
107-
18813049.299423713
125+
-19065470.770224877,
126+
-18193969.787702393,
127+
21009545.91535361,
128+
20037508.342789244
108129
],
109-
"latitude": 6.038467945870664,
110-
"longitude": -142.8442794845441,
130+
"latitude": 8.251719751227498,
131+
"longitude": 8.731962081730105,
111132
"pitch": 0.0,
112133
"projection": "EPSG:3857",
113-
"zoom": 2.100662339005199
134+
"zoom": 1.834471049984222
114135
},
115136
"sources": {
137+
"5970d6c9-26be-4cc6-84c9-16593dd3edfe": {
138+
"name": "France",
139+
"parameters": {
140+
"path": "https://geodata.ucdavis.edu/gadm/gadm4.1/json/gadm41_FRA_1.json"
141+
},
142+
"type": "GeoJSONSource"
143+
},
116144
"b4287bea-e217-443c-b527-58f7559c824c": {
117-
"name": "Custom GeoJSON Layer Source",
145+
"name": "World",
118146
"parameters": {
119147
"path": "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson"
120148
},

packages/base/src/commands.ts

Lines changed: 168 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { ITranslator } from '@jupyterlab/translation';
1717
import { CommandRegistry } from '@lumino/commands';
1818
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
1919
import { CommandIDs, icons } from './constants';
20-
import { CreationFormDialog } from './dialogs/formdialog';
20+
import { LayerCreationFormDialog } from './dialogs/layerCreationFormDialog';
2121
import { LayerBrowserWidget } from './dialogs/layerBrowserDialog';
2222
import { SymbologyWidget } from './dialogs/symbology/symbologyDialog';
2323
import keybindings from './keybindings.json';
@@ -27,7 +27,7 @@ import { getGdal } from './gdal';
2727
import { getGeoJSONDataFromLayerSource, downloadFile } from './tools';
2828
import { IJGISLayer, IJGISSource } from '@jupytergis/schema';
2929
import { UUID } from '@lumino/coreutils';
30-
import { FormDialog } from './formbuilder/formdialog';
30+
import { ProcessingFormDialog } from './dialogs/ProcessingFormDialog';
3131

3232
interface ICreateEntry {
3333
tracker: JupyterGISTracker;
@@ -376,7 +376,7 @@ export function addCommands(
376376

377377
// Open form and get user input
378378
const formValues = await new Promise<IDict>(resolve => {
379-
const dialog = new FormDialog({
379+
const dialog = new ProcessingFormDialog({
380380
title: 'Buffer',
381381
schema: schema,
382382
model: model,
@@ -385,7 +385,8 @@ export function addCommands(
385385
bufferDistance: 10,
386386
projection: 'EPSG:4326'
387387
},
388-
cancelButton: false,
388+
formContext: 'create',
389+
processingType: 'buffer',
389390
syncData: (props: IDict) => {
390391
resolve(props);
391392
dialog.dispose();
@@ -480,6 +481,165 @@ export function addCommands(
480481
}
481482
});
482483

484+
commands.addCommand(CommandIDs.dissolve, {
485+
label: trans.__('Dissolve'),
486+
isEnabled: () => {
487+
const selectedLayer = getSingleSelectedLayer(tracker);
488+
if (!selectedLayer) {
489+
return false;
490+
}
491+
return ['VectorLayer', 'ShapefileLayer'].includes(selectedLayer.type);
492+
},
493+
execute: async () => {
494+
const selected = getSingleSelectedLayer(tracker);
495+
if (!selected) {
496+
console.error('No valid selected layer.');
497+
return;
498+
}
499+
500+
const sources = tracker.currentWidget?.model.sharedModel.sources ?? {};
501+
const model = tracker.currentWidget?.model;
502+
const localState = model?.sharedModel.awareness.getLocalState();
503+
504+
if (
505+
!model ||
506+
!localState ||
507+
!localState['selected']?.value ||
508+
!selected.parameters
509+
) {
510+
return;
511+
}
512+
513+
const sourceId = selected.parameters.source;
514+
const source = sources[sourceId];
515+
516+
if (!source || !source.parameters) {
517+
console.error(`Source with ID ${sourceId} not found or missing path.`);
518+
return;
519+
}
520+
521+
// Load GeoJSON data
522+
const geojsonString = await getGeoJSONDataFromLayerSource(source, model);
523+
if (!geojsonString) {
524+
return;
525+
}
526+
527+
const geojson = JSON.parse(geojsonString);
528+
if (!geojson.features || geojson.features.length === 0) {
529+
console.error('Invalid GeoJSON: No features found.');
530+
return;
531+
}
532+
533+
// Extract field names from the first feature's properties
534+
const properties = geojson.features[0].properties;
535+
const fieldNames = Object.keys(properties);
536+
537+
if (fieldNames.length === 0) {
538+
console.error('No attribute fields found in GeoJSON.');
539+
return;
540+
}
541+
542+
// Retrieve dissolve schema and update fields dynamically
543+
const schema = {
544+
...(formSchemaRegistry.getSchemas().get('Dissolve') as IDict),
545+
properties: {
546+
...formSchemaRegistry.getSchemas().get('Dissolve')?.properties,
547+
dissolveField: {
548+
type: 'string',
549+
enum: fieldNames, // Populate dropdown with field names
550+
description: 'Select the field for dissolving features.'
551+
}
552+
}
553+
};
554+
555+
const selectedLayer = localState['selected'].value;
556+
const selectedLayerId = Object.keys(selectedLayer)[0];
557+
558+
// Open form and get user input
559+
const formValues = await new Promise<IDict>(resolve => {
560+
const dialog = new ProcessingFormDialog({
561+
title: 'Dissolve',
562+
schema: schema,
563+
model: model,
564+
sourceData: {
565+
inputLayer: selectedLayerId,
566+
dissolveField: fieldNames[0] // Default to the first field
567+
},
568+
formContext: 'create',
569+
processingType: 'dissolve',
570+
syncData: (props: IDict) => {
571+
resolve(props);
572+
dialog.dispose();
573+
}
574+
});
575+
576+
dialog.launch();
577+
});
578+
579+
if (!formValues) {
580+
return;
581+
}
582+
583+
const dissolveField = formValues.dissolveField;
584+
const fileBlob = new Blob([geojsonString], {
585+
type: 'application/geo+json'
586+
});
587+
const geoFile = new File([fileBlob], 'data.geojson', {
588+
type: 'application/geo+json'
589+
});
590+
591+
const Gdal = await getGdal();
592+
const result = await Gdal.open(geoFile);
593+
594+
if (result.datasets.length > 0) {
595+
const dataset = result.datasets[0] as any;
596+
const layerName = dataset.info.layers[0].name;
597+
598+
const sqlQuery = `
599+
SELECT ST_Union(geometry) AS geometry, ${dissolveField}
600+
FROM ${layerName}
601+
GROUP BY ${dissolveField}
602+
`;
603+
604+
const options = [
605+
'-f',
606+
'GeoJSON',
607+
'-nlt',
608+
'PROMOTE_TO_MULTI',
609+
'-dialect',
610+
'sqlite',
611+
'-sql',
612+
sqlQuery,
613+
'output.geojson'
614+
];
615+
616+
const outputFilePath = await Gdal.ogr2ogr(dataset, options);
617+
const dissolvedBytes = await Gdal.getFileBytes(outputFilePath);
618+
const dissolvedGeoJSONString = new TextDecoder().decode(dissolvedBytes);
619+
Gdal.close(dataset);
620+
621+
const dissolvedGeoJSON = JSON.parse(dissolvedGeoJSONString);
622+
623+
const newSourceId = UUID.uuid4();
624+
const sourceModel: IJGISSource = {
625+
type: 'GeoJSONSource',
626+
name: selected.name + ' Dissolved',
627+
parameters: { data: dissolvedGeoJSON }
628+
};
629+
630+
const layerModel: IJGISLayer = {
631+
type: 'VectorLayer',
632+
parameters: { source: newSourceId },
633+
visible: true,
634+
name: selected.name + ' Dissolved'
635+
};
636+
637+
model.sharedModel.addSource(newSourceId, sourceModel);
638+
model.addLayer(UUID.uuid4(), layerModel);
639+
}
640+
}
641+
});
642+
483643
commands.addCommand(CommandIDs.newGeoJSONEntry, {
484644
label: trans.__('New GeoJSON layer'),
485645
isEnabled: () => {
@@ -1206,12 +1366,13 @@ export function addCommands(
12061366
};
12071367

12081368
const formValues = await new Promise<IDict>(resolve => {
1209-
const dialog = new FormDialog({
1369+
const dialog = new ProcessingFormDialog({
12101370
title: 'Download GeoJSON',
12111371
schema: exportSchema,
12121372
model,
12131373
sourceData: { exportFormat: 'GeoJSON' },
1214-
cancelButton: false,
1374+
formContext: 'create',
1375+
processingType: 'export',
12151376
syncData: (props: IDict) => {
12161377
resolve(props);
12171378
dialog.dispose();
@@ -1304,7 +1465,7 @@ namespace Private {
13041465
return;
13051466
}
13061467

1307-
const dialog = new CreationFormDialog({
1468+
const dialog = new LayerCreationFormDialog({
13081469
model: current.model,
13091470
title,
13101471
createLayer,

packages/base/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export namespace CommandIDs {
2626

2727
// Processing commands
2828
export const buffer = 'jupytergis:buffer';
29+
export const dissolve = 'jupytergis:dissolve';
2930

3031
// Sources only commands
3132
export const newRasterSource = 'jupytergis:newRasterSource';

0 commit comments

Comments
 (0)