Skip to content

Commit a8a405b

Browse files
Refactor of processing logic to make it less verbose (#758)
Co-authored-by: Matt Fisher <[email protected]>
1 parent 830d5e3 commit a8a405b

File tree

27 files changed

+568
-260
lines changed

27 files changed

+568
-260
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,7 @@ untitled*
141141

142142
# GeoJSON schema
143143
packages/schema/src/schema/geojson.json
144+
145+
# Automatically generated file for processing
146+
147+
packages/schema/src/processing/_generated/*
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Add a processing operation
2+
3+
:::{admonition} Objectives
4+
:class: seealso
5+
6+
By the end of this tutorial, you will be able to add or modify data processing commands
7+
for JupyterGIS.
8+
:::
9+
10+
:::{admonition} Prerequisites
11+
:class: warning
12+
13+
- Knowledge of geospatial processing operations, for example using
14+
[GDAL/OGR](https://gdal.org/en/stable/).
15+
- Ability to edit JSON and [JSONSchema](https://json-schema.org/).
16+
17+
:::
18+
19+
## Overview
20+
21+
- `packages/schema/src/schema/processing/`: In this directory, we need to create a
22+
file which defines the **UI form structure for each processing operation**.
23+
- `packages/schema/src/processing/config/`: In this directory, we need to create a
24+
file which defines the **processing behavior for each processing operation**.
25+
26+
## Creating a schema
27+
28+
What information is required to execute the processing step?
29+
30+
Most processing operations need an input layer and some other parameters. Here, under
31+
the `properties` key, we define a schema which requires an input layer and a buffer
32+
distance. The required parameters are listed under the `required` key.
33+
34+
There's also an optional parameter to determine whether the output layer should be
35+
embedded in the project file.
36+
37+
:::{admonition} Prerequisites
38+
:class: important
39+
40+
Always include a description for each property!
41+
:::
42+
43+
```json
44+
{
45+
"type": "object",
46+
"description": "Buffer",
47+
"title": "IBuffer",
48+
"required": ["inputLayer", "bufferDistance"],
49+
"additionalProperties": false,
50+
"properties": {
51+
"inputLayer": {
52+
"type": "string",
53+
"description": "The input layer for buffering."
54+
},
55+
"bufferDistance": {
56+
"type": "number",
57+
"default": 10,
58+
"description": "The distance used for buffering the geometry (in projection units)."
59+
},
60+
"embedOutputLayer": {
61+
"type": "boolean",
62+
"title": "Embed output buffered layer in file",
63+
"default": true
64+
}
65+
}
66+
}
67+
```
68+
69+
## Configuring processing behavior
70+
71+
This information is used to generate the code, including a
72+
[JupyterLab command](https://jupyterlab.readthedocs.io/en/stable/user/commands.html),
73+
which will do the command and enable display in the UI.
74+
75+
**No need to edit the UI code!**
76+
77+
The `operation` key contains templates for generating the underlying
78+
processing operation (often using [GDAL/OGR](https://gdal.org/en/stable/)).
79+
Template parameters are set off with braces `{}`.
80+
81+
The `operationParams` key contains the attributes (from the schema defined above) that
82+
will be injected into the processing operation templates.
83+
84+
`type` is a special string that determines how the processing operation is
85+
constructed.
86+
See the next section for more details!
87+
88+
```json
89+
{
90+
"name": "buffer",
91+
"label": "Buffer",
92+
"operationParams": ["bufferDistance"],
93+
"operation": {
94+
"gdalFunction": "ogr2ogr",
95+
"sql": "SELECT ST_Union(ST_Buffer(geometry, {bufferDistance})) AS geometry, * FROM \"{layerName}\""
96+
},
97+
"type": "vector"
98+
}
99+
```
100+
101+
## Processing type
102+
103+
The processing `type` attribute from the config shown above determines which logic will
104+
be used to generate commands.
105+
106+
:::{admonition} Prerequisites
107+
:class: important
108+
109+
The processing type you use **must** be defined in
110+
`packages/schema/src/processing/ProcessingMerge.ts`
111+
and used in
112+
`packages/base/src/processing/processingCommands.ts`.
113+
114+
If no existing types satisfy your needs, then you'll need to add a new case.
115+
:::
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/* This file is not an exhaustive list of commands.
2+
*
3+
* See the documentation for more details.
4+
*/
5+
export const createNew = 'jupytergis:create-new-jGIS-file';
6+
export const redo = 'jupytergis:redo';
7+
export const undo = 'jupytergis:undo';
8+
export const symbology = 'jupytergis:symbology';
9+
export const identify = 'jupytergis:identify';
10+
export const temporalController = 'jupytergis:temporalController';
11+
12+
// geolocation
13+
export const getGeolocation = 'jupytergis:getGeolocation';
14+
15+
// Layers and sources creation commands
16+
export const openLayerBrowser = 'jupytergis:openLayerBrowser';
17+
18+
// Layer and source
19+
export const newRasterEntry = 'jupytergis:newRasterEntry';
20+
export const newVectorTileEntry = 'jupytergis:newVectorTileEntry';
21+
export const newShapefileEntry = 'jupytergis:newShapefileEntry';
22+
export const newGeoJSONEntry = 'jupytergis:newGeoJSONEntry';
23+
export const newHillshadeEntry = 'jupytergis:newHillshadeEntry';
24+
export const newImageEntry = 'jupytergis:newImageEntry';
25+
export const newVideoEntry = 'jupytergis:newVideoEntry';
26+
export const newGeoTiffEntry = 'jupytergis:newGeoTiffEntry';
27+
28+
// Layer and group actions
29+
export const renameLayer = 'jupytergis:renameLayer';
30+
export const removeLayer = 'jupytergis:removeLayer';
31+
export const renameGroup = 'jupytergis:renameGroup';
32+
export const removeGroup = 'jupytergis:removeGroup';
33+
export const moveLayersToGroup = 'jupytergis:moveLayersToGroup';
34+
export const moveLayerToNewGroup = 'jupytergis:moveLayerToNewGroup';
35+
36+
// Source actions
37+
export const renameSource = 'jupytergis:renameSource';
38+
export const removeSource = 'jupytergis:removeSource';
39+
40+
// Console commands
41+
export const toggleConsole = 'jupytergis:toggleConsole';
42+
export const invokeCompleter = 'jupytergis:invokeConsoleCompleter';
43+
export const removeConsole = 'jupytergis:removeConsole';
44+
export const executeConsole = 'jupytergis:executeConsole';
45+
export const selectCompleter = 'jupytergis:selectConsoleCompleter';
46+
47+
// Map Commands
48+
export const addAnnotation = 'jupytergis:addAnnotation';
49+
export const zoomToLayer = 'jupytergis:zoomToLayer';
50+
export const downloadGeoJSON = 'jupytergis:downloadGeoJSON';

packages/base/src/commands.ts renamed to packages/base/src/commands/index.ts

Lines changed: 15 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,18 @@ import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
2020
import { Coordinate } from 'ol/coordinate';
2121
import { fromLonLat } from 'ol/proj';
2222

23-
import { CommandIDs, icons } from './constants';
24-
import { ProcessingFormDialog } from './dialogs/ProcessingFormDialog';
25-
import { LayerBrowserWidget } from './dialogs/layerBrowserDialog';
26-
import { LayerCreationFormDialog } from './dialogs/layerCreationFormDialog';
27-
import { SymbologyWidget } from './dialogs/symbology/symbologyDialog';
28-
import { targetWithCenterIcon } from './icons';
29-
import keybindings from './keybindings.json';
30-
import {
31-
getSingleSelectedLayer,
32-
selectedLayerIsOfType,
33-
processSelectedLayer,
34-
} from './processing';
35-
import { getGeoJSONDataFromLayerSource, downloadFile } from './tools';
36-
import { JupyterGISTracker } from './types';
37-
import { JupyterGISDocumentWidget } from './widget';
23+
import { CommandIDs, icons } from '../constants';
24+
import { ProcessingFormDialog } from '../dialogs/ProcessingFormDialog';
25+
import { LayerBrowserWidget } from '../dialogs/layerBrowserDialog';
26+
import { LayerCreationFormDialog } from '../dialogs/layerCreationFormDialog';
27+
import { SymbologyWidget } from '../dialogs/symbology/symbologyDialog';
28+
import { targetWithCenterIcon } from '../icons';
29+
import keybindings from '../keybindings.json';
30+
import { getSingleSelectedLayer } from '../processing/index';
31+
import { addProcessingCommands } from '../processing/processingCommands';
32+
import { getGeoJSONDataFromLayerSource, downloadFile } from '../tools';
33+
import { JupyterGISTracker } from '../types';
34+
import { JupyterGISDocumentWidget } from '../widget';
3835

3936
interface ICreateEntry {
4037
tracker: JupyterGISTracker;
@@ -323,122 +320,6 @@ export function addCommands(
323320
...icons.get(CommandIDs.newVectorTileEntry),
324321
});
325322

326-
commands.addCommand(CommandIDs.buffer, {
327-
label: trans.__('Buffer'),
328-
isEnabled: () => selectedLayerIsOfType(['VectorLayer'], tracker),
329-
execute: async () => {
330-
await processSelectedLayer(
331-
tracker,
332-
formSchemaRegistry,
333-
'Buffer',
334-
{
335-
sqlQueryFn: (layerName, bufferDistance) => `
336-
SELECT ST_Union(ST_Buffer(geometry, ${bufferDistance})) AS geometry, *
337-
FROM "${layerName}"
338-
`,
339-
gdalFunction: 'ogr2ogr',
340-
options: (sqlQuery: string) => [
341-
'-f',
342-
'GeoJSON',
343-
'-dialect',
344-
'SQLITE',
345-
'-sql',
346-
sqlQuery,
347-
'output.geojson',
348-
],
349-
},
350-
app,
351-
);
352-
},
353-
});
354-
355-
commands.addCommand(CommandIDs.dissolve, {
356-
label: trans.__('Dissolve'),
357-
isEnabled: () => selectedLayerIsOfType(['VectorLayer'], tracker),
358-
execute: async () => {
359-
await processSelectedLayer(
360-
tracker,
361-
formSchemaRegistry,
362-
'Dissolve',
363-
{
364-
sqlQueryFn: (layerName, dissolveField) => `
365-
SELECT ST_Union(geometry) AS geometry, ${dissolveField}
366-
FROM "${layerName}"
367-
GROUP BY ${dissolveField}
368-
`,
369-
gdalFunction: 'ogr2ogr',
370-
options: (sqlQuery: string) => [
371-
'-f',
372-
'GeoJSON',
373-
'-dialect',
374-
'SQLITE',
375-
'-sql',
376-
sqlQuery,
377-
'output.geojson',
378-
],
379-
},
380-
app,
381-
);
382-
},
383-
});
384-
commands.addCommand(CommandIDs.centroids, {
385-
label: trans.__('Centroids'),
386-
isEnabled: () => selectedLayerIsOfType(['VectorLayer'], tracker),
387-
execute: async () => {
388-
await processSelectedLayer(
389-
tracker,
390-
formSchemaRegistry,
391-
'Centroids',
392-
{
393-
sqlQueryFn: (layerName, _) => `
394-
SELECT ST_Centroid(geometry) AS geometry, *
395-
FROM "${layerName}"
396-
`,
397-
gdalFunction: 'ogr2ogr',
398-
options: (sqlQuery: string) => [
399-
'-f',
400-
'GeoJSON',
401-
'-dialect',
402-
'SQLITE',
403-
'-sql',
404-
sqlQuery,
405-
'output.geojson',
406-
],
407-
},
408-
app,
409-
);
410-
},
411-
});
412-
413-
commands.addCommand(CommandIDs.boundingBoxes, {
414-
label: trans.__('Bounding Boxes'),
415-
isEnabled: () => selectedLayerIsOfType(['VectorLayer'], tracker),
416-
execute: async () => {
417-
await processSelectedLayer(
418-
tracker,
419-
formSchemaRegistry,
420-
'BoundingBoxes',
421-
{
422-
sqlQueryFn: (layerName, _) => `
423-
SELECT ST_Envelope(geometry) AS geometry, *
424-
FROM "${layerName}"
425-
`,
426-
gdalFunction: 'ogr2ogr',
427-
options: (sqlQuery: string) => [
428-
'-f',
429-
'GeoJSON',
430-
'-dialect',
431-
'SQLITE',
432-
'-sql',
433-
sqlQuery,
434-
'output.geojson',
435-
],
436-
},
437-
app,
438-
);
439-
},
440-
});
441-
442323
commands.addCommand(CommandIDs.newGeoJSONEntry, {
443324
label: trans.__('New GeoJSON layer'),
444325
isEnabled: () => {
@@ -459,6 +340,9 @@ export function addCommands(
459340
...icons.get(CommandIDs.newGeoJSONEntry),
460341
});
461342

343+
//Add processing commands
344+
addProcessingCommands(app, commands, tracker, trans, formSchemaRegistry);
345+
462346
commands.addCommand(CommandIDs.newHillshadeEntry, {
463347
label: trans.__('New Hillshade layer'),
464348
isEnabled: () => {

0 commit comments

Comments
 (0)