From 2fff268f259ae1a211eebe3188d7baedf1f01e01 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Mon, 12 May 2025 13:16:09 +0200 Subject: [PATCH 01/30] Add a toggle button to enable to add a draw vector layer. Add a isDrawVectorLayerEnabled property and a drawVectorLayerChanged signal to the JupyterGISModel. Add a method _handleVectorLayerChange to update the state of isDrawVectorLayerEnabled. Add VectorLayerDropdown react component to be used in an overlay when the draw vector layer button is toggled. --- packages/base/src/commands.ts | 29 +++++++++++++++- packages/base/src/constants.ts | 11 ++++++ packages/base/src/icons.ts | 6 ++++ .../base/src/mainview/VectorLayerDropdown.tsx | 29 ++++++++++++++++ packages/base/src/mainview/mainView.tsx | 34 +++++++++++++++++++ packages/base/src/toolbar/widget.tsx | 8 +++++ packages/base/style/icons/pencil_solid.svg | 9 +++++ packages/schema/src/interfaces.ts | 3 ++ packages/schema/src/model.ts | 9 +++++ 9 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 packages/base/src/mainview/VectorLayerDropdown.tsx create mode 100644 packages/base/style/icons/pencil_solid.svg diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 8b25b15ea..eae81b17f 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -25,7 +25,6 @@ import { ProcessingFormDialog } from './dialogs/ProcessingFormDialog'; import { LayerBrowserWidget } from './dialogs/layerBrowserDialog'; import { LayerCreationFormDialog } from './dialogs/layerCreationFormDialog'; import { SymbologyWidget } from './dialogs/symbology/symbologyDialog'; -import { targetWithCenterIcon } from './icons'; import keybindings from './keybindings.json'; import { getSingleSelectedLayer, @@ -35,6 +34,7 @@ import { import { getGeoJSONDataFromLayerSource, downloadFile } from './tools'; import { JupyterGISTracker } from './types'; import { JupyterGISDocumentWidget } from './widget'; +import { pencilSolidIcon, targetWithCenterIcon } from './icons'; interface ICreateEntry { tracker: JupyterGISTracker; @@ -938,6 +938,33 @@ export function addCommands( icon: targetWithCenterIcon, }); + commands.addCommand(CommandIDs.newDrawVectorLayer, { + label: trans.__('Create New Draw Vector Layer'), + isToggled: () => { + if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { + const model = tracker.currentWidget?.content.currentViewModel + .jGISModel as IJupyterGISModel; + return model.isDrawVectorLayerEnabled; + } else { + return false; + } + }, + execute: async () => { + if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { + const model = tracker.currentWidget?.content.currentViewModel + .jGISModel as IJupyterGISModel; + if (model.isDrawVectorLayerEnabled === true) { + model.isDrawVectorLayerEnabled = false; + } else { + model.isDrawVectorLayerEnabled = true; + } + model.updateIsDrawVectorLayerEnabled(); + commands.notifyCommandChanged(CommandIDs.newDrawVectorLayer); + } + }, + icon: pencilSolidIcon + }); + loadKeybindings(commands, keybindings); } diff --git a/packages/base/src/constants.ts b/packages/base/src/constants.ts index 608912a96..00bfacd6a 100644 --- a/packages/base/src/constants.ts +++ b/packages/base/src/constants.ts @@ -43,6 +43,17 @@ export namespace CommandIDs { export const centroids = 'jupytergis:centroids'; export const boundingBoxes = 'jupytergis:boundingBoxes'; + // Layers only commands + export const newRasterLayer = 'jupytergis:newRasterLayer'; + export const newVectorLayer = 'jupytergis:newVectorLayer'; + export const newHillshadeLayer = 'jupytergis:newHillshadeLayer'; + export const newImageLayer = 'jupytergis:newImageLayer'; + export const newVideoLayer = 'jupytergis:newVideoLayer'; + export const newShapefileLayer = 'jupytergis:newShapefileLayer'; + export const newWebGlTileLayer = 'jupytergis:newWebGlTileLayer'; + export const newHeatmapLayer = 'jupytergis:newHeatmapLayer'; + export const newDrawVectorLayer = 'jupytergis:newDrawVectorLayer'; + // Layer and group actions export const renameLayer = 'jupytergis:renameLayer'; export const removeLayer = 'jupytergis:removeLayer'; diff --git a/packages/base/src/icons.ts b/packages/base/src/icons.ts index 18f132d83..cb52238af 100644 --- a/packages/base/src/icons.ts +++ b/packages/base/src/icons.ts @@ -24,6 +24,7 @@ import targetWithoutCenterSvgStr from '../style/icons/target_without_center.svg' import terminalToolbarSvgStr from '../style/icons/terminal_toolbar.svg'; import vectorSquareSvgStr from '../style/icons/vector_square.svg'; import visibilitySvgStr from '../style/icons/visibility.svg'; +import pencilSolidSvgStr from '../style/icons/pencil_solid.svg'; export const logoIcon = new LabIcon({ name: 'jupytergis::logo', @@ -109,3 +110,8 @@ export const targetWithCenterIcon = new LabIcon({ name: 'jupytergis::targetWithoutCenter', svgstr: targetWithoutCenterSvgStr, }); + +export const pencilSolidIcon = new LabIcon({ + name: 'jupytergis::pencilSolid', + svgstr: pencilSolidSvgStr +}); diff --git a/packages/base/src/mainview/VectorLayerDropdown.tsx b/packages/base/src/mainview/VectorLayerDropdown.tsx new file mode 100644 index 000000000..0c17ede1e --- /dev/null +++ b/packages/base/src/mainview/VectorLayerDropdown.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; + +export function VectorLayerDropdown() { + const handlegeometryTypeChange = ( + event: React.ChangeEvent + ) => { + console.log('To be implemented'); + }; + const vectorLayers = ['Point', 'LineString', 'Polygon', 'Circle']; + return ( +
+ +
+ ); +} diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 6f56be391..e7494272d 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -87,6 +87,7 @@ import CollaboratorPointers, { ClientPointer } from './CollaboratorPointers'; import { FollowIndicator } from './FollowIndicator'; import TemporalSlider from './TemporalSlider'; import { MainViewModel } from './mainviewmodel'; +import { VectorLayerDropdown } from './VectorLayerDropdown'; interface IProps { viewModel: MainViewModel; @@ -106,6 +107,7 @@ interface IStates { loadingErrors: Array<{ id: string; error: any; index: number }>; displayTemporalController: boolean; filterStates: IDict; + isDrawVectorLayerEnabled: boolean; } export class MainView extends React.Component { @@ -141,6 +143,10 @@ export class MainView extends React.Component { this._handleGeolocationChanged, this, ); + this._model.drawVectorLayerChanged.connect( + this._handleDrawVectorLayerChanged, + this + ); this._model.flyToGeometrySignal.connect(this.flyToGeometry, this); this._model.highlightFeatureSignal.connect( @@ -168,6 +174,7 @@ export class MainView extends React.Component { loadingErrors: [], displayTemporalController: false, filterStates: {}, + isDrawVectorLayerEnabled: false }; this._sources = []; @@ -1997,6 +2004,12 @@ export class MainView extends React.Component { } } + private _handleDrawVectorLayerChanged() { + const isDrawVectorLayerEnabled = this._model.isDrawVectorLayerEnabled; + console.log('isDrawVectorLayerEnabled:', isDrawVectorLayerEnabled); + this.setState(old => ({ ...old, isDrawVectorLayerEnabled })); + } + private _handleThemeChange = (): void => { const lightTheme = isLightTheme(); @@ -2037,6 +2050,27 @@ export class MainView extends React.Component { ); })} + {this.state.isDrawVectorLayerEnabled && ( +
+ {' '} +
+ )} +
{this.state.displayTemporalController && ( +
)} @@ -2125,4 +2170,5 @@ export class MainView extends React.Component { private _loadingLayers: Set; private _originalFeatures: IDict[]> = {}; private _highlightLayer: VectorLayer; + private _currentDrawInteraction: Interaction; } From fec715590b50c0c2eb6e7edd3f47239f1b303fae Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Wed, 14 May 2025 17:59:59 +0200 Subject: [PATCH 03/30] Change _handleDrawVectorLayerChanged method to _updateIsDrawVectorLayerEnabled. --- packages/base/src/mainview/mainView.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 87d43a0be..803acedf8 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -148,7 +148,7 @@ export class MainView extends React.Component { this, ); this._model.drawVectorLayerChanged.connect( - this._handleDrawVectorLayerChanged, + this._updateIsDrawVectorLayerEnabled, this ); @@ -2009,9 +2009,8 @@ export class MainView extends React.Component { } } - private _handleDrawVectorLayerChanged() { + private _updateIsDrawVectorLayerEnabled() { const isDrawVectorLayerEnabled = this._model.isDrawVectorLayerEnabled; - console.log('isDrawVectorLayerEnabled:', isDrawVectorLayerEnabled); this.setState(old => ({ ...old, isDrawVectorLayerEnabled })); } From 0efbcac2bd9766eff02a8c87f9a6c91e34e2b902 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Wed, 14 May 2025 18:14:44 +0200 Subject: [PATCH 04/30] Add logics to create the vector layer and to add it to the map. --- packages/base/src/mainview/mainView.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 803acedf8..55fae7959 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -88,6 +88,8 @@ import { FollowIndicator } from './FollowIndicator'; import TemporalSlider from './TemporalSlider'; import { MainViewModel } from './mainviewmodel'; import Draw from 'ol/interaction/Draw.js'; +import Modify from 'ol/interaction/Modify.js'; +import Snap from 'ol/interaction/Snap.js'; import { Type } from 'ol/geom/Geometry'; const drawGeometries = ['Point', 'LineString', 'Polygon', 'Circle']; @@ -2034,11 +2036,27 @@ export class MainView extends React.Component { this._removeCurrentDrawInteraction(); } const source = new VectorSource(); + const vectorLayer = new VectorLayer({ + source: source, + style: { + 'fill-color': 'rgba(255, 255, 255, 0.2)', + 'stroke-color': '#ffcc33', + 'stroke-width': 2, + 'circle-radius': 7, + 'circle-fill-color': '#ffcc33' + } + }); + this._Map.addLayer(vectorLayer); + const modify = new Modify({ source: source }); + this._Map.addInteraction(modify); const draw = new Draw({ source: source, type: drawGeometryType as Type // Type being a geometry type here }); + const snap = new Snap({ source: source }); + this._Map.addInteraction(draw); + this._Map.addInteraction(snap); this._currentDrawInteraction = draw; this.setState(old => ({ ...old, From 5ca4df41c0b93ad9e4f9cb2de90eeecf650f8f06 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Thu, 15 May 2025 10:04:23 +0200 Subject: [PATCH 05/30] Add logics to remove the draw interaction when isDrawVectorLayerEnabled is false. --- packages/base/src/mainview/mainView.tsx | 64 ++++++++++++++----------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 55fae7959..871129532 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -2014,6 +2014,9 @@ export class MainView extends React.Component { private _updateIsDrawVectorLayerEnabled() { const isDrawVectorLayerEnabled = this._model.isDrawVectorLayerEnabled; this.setState(old => ({ ...old, isDrawVectorLayerEnabled })); + if (isDrawVectorLayerEnabled === false && this._currentDrawInteraction) { + this._removeCurrentDrawInteraction(); + } } private _handleThemeChange = (): void => { @@ -2032,36 +2035,39 @@ export class MainView extends React.Component { event: React.ChangeEvent ) => { const drawGeometryType = event.target.value; - if (this._currentDrawInteraction) { - this._removeCurrentDrawInteraction(); - } - const source = new VectorSource(); - const vectorLayer = new VectorLayer({ - source: source, - style: { - 'fill-color': 'rgba(255, 255, 255, 0.2)', - 'stroke-color': '#ffcc33', - 'stroke-width': 2, - 'circle-radius': 7, - 'circle-fill-color': '#ffcc33' + if (this._model.isDrawVectorLayerEnabled) { + if (this._currentDrawInteraction) { + this._removeCurrentDrawInteraction(); } - }); - this._Map.addLayer(vectorLayer); - const modify = new Modify({ source: source }); - this._Map.addInteraction(modify); - const draw = new Draw({ - source: source, - type: drawGeometryType as Type // Type being a geometry type here - }); - const snap = new Snap({ source: source }); - - this._Map.addInteraction(draw); - this._Map.addInteraction(snap); - this._currentDrawInteraction = draw; - this.setState(old => ({ - ...old, - drawGeometryType - })); + const source = new VectorSource(); + const vectorLayer = new VectorLayer({ + source: source, + style: { + 'fill-color': 'rgba(255, 255, 255, 0.2)', + 'stroke-color': '#ffcc33', + 'stroke-width': 2, + 'circle-radius': 7, + 'circle-fill-color': '#ffcc33' + } + }); + this._Map.addLayer(vectorLayer); + console.log('vector layer:', vectorLayer.getSource()?.getFeatures()); + const modify = new Modify({ source: source }); + this._Map.addInteraction(modify); + const draw = new Draw({ + source: source, + type: drawGeometryType as Type // Type being a geometry type here + }); + const snap = new Snap({ source: source }); + + this._Map.addInteraction(draw); + this._Map.addInteraction(snap); + this._currentDrawInteraction = draw; + this.setState(old => ({ + ...old, + drawGeometryType + })); + } else {return;} }; private _removeCurrentDrawInteraction = () => { From 399b3810218f8b68185b0f8ca85abac0200c8216 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Thu, 15 May 2025 16:11:28 +0200 Subject: [PATCH 06/30] Add an enabled method to the newVDrawVectorLayer, update the toggle method and add the command to the _notifyCommands of the LeftPanelWidget class. --- packages/base/src/commands.ts | 20 +++++++++++++++++++- packages/base/src/mainview/mainView.tsx | 4 +++- packages/base/src/panelview/leftpanel.tsx | 1 + 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index eae81b17f..aaa7990c0 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -174,6 +174,8 @@ export function addCommands( }, isEnabled: () => { const selectedLayer = getSingleSelectedLayer(tracker); + console.log('In Identify, selected Type:', selectedLayer?.type); + if (!selectedLayer) { return false; } @@ -941,7 +943,16 @@ export function addCommands( commands.addCommand(CommandIDs.newDrawVectorLayer, { label: trans.__('Create New Draw Vector Layer'), isToggled: () => { - if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { + const selectedLayer = getSingleSelectedLayer(tracker); + if (!selectedLayer) { + return false; + } + const canDrawVectorLayer = ['VectorLayer'].includes(selectedLayer.type); + + if ( + tracker.currentWidget instanceof JupyterGISDocumentWidget && + canDrawVectorLayer + ) { const model = tracker.currentWidget?.content.currentViewModel .jGISModel as IJupyterGISModel; return model.isDrawVectorLayerEnabled; @@ -949,6 +960,13 @@ export function addCommands( return false; } }, + isEnabled: () => { + const selectedLayer = getSingleSelectedLayer(tracker); + if (!selectedLayer) { + return false; + } + return ['VectorLayer'].includes(selectedLayer.type); + }, execute: async () => { if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { const model = tracker.currentWidget?.content.currentViewModel diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 871129532..fbb253f79 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -2067,7 +2067,9 @@ export class MainView extends React.Component { ...old, drawGeometryType })); - } else {return;} + } else { + return; + } }; private _removeCurrentDrawInteraction = () => { diff --git a/packages/base/src/panelview/leftpanel.tsx b/packages/base/src/panelview/leftpanel.tsx index 81b35e019..a479caece 100644 --- a/packages/base/src/panelview/leftpanel.tsx +++ b/packages/base/src/panelview/leftpanel.tsx @@ -208,6 +208,7 @@ export class LeftPanelWidget extends SidePanel { // Notify commands that need updating this._commands.notifyCommandChanged(CommandIDs.identify); this._commands.notifyCommandChanged(CommandIDs.temporalController); + this._commands.notifyCommandChanged(CommandIDs.newDrawVectorLayer); } private _handleFileChange: () => void; From cc5c66a872a82557babd205bd20f487272bbfeee Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Mon, 19 May 2025 12:49:59 +0200 Subject: [PATCH 07/30] Add logics to use the source of the currently selected layer in the _handleDrawGeometryTypeChange method of the MainView class. --- packages/base/src/commands.ts | 9 +++++++-- packages/base/src/mainview/mainView.tsx | 22 +++++++++++++++++----- packages/schema/src/interfaces.ts | 1 + packages/schema/src/model.ts | 2 +- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index aaa7990c0..ff83b26ab 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -174,8 +174,6 @@ export function addCommands( }, isEnabled: () => { const selectedLayer = getSingleSelectedLayer(tracker); - console.log('In Identify, selected Type:', selectedLayer?.type); - if (!selectedLayer) { return false; } @@ -947,6 +945,7 @@ export function addCommands( if (!selectedLayer) { return false; } + const canDrawVectorLayer = ['VectorLayer'].includes(selectedLayer.type); if ( @@ -955,6 +954,12 @@ export function addCommands( ) { const model = tracker.currentWidget?.content.currentViewModel .jGISModel as IJupyterGISModel; + const parameters = selectedLayer.parameters; + if (parameters) { + const selectedvectorLayerSourceId = parameters?.source; + model.selectedVectorLayerSourceId = selectedvectorLayerSourceId; + } + return model.isDrawVectorLayerEnabled; } else { return false; diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index fbb253f79..57e34c26e 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -2039,19 +2039,31 @@ export class MainView extends React.Component { if (this._currentDrawInteraction) { this._removeCurrentDrawInteraction(); } - const source = new VectorSource(); + + const layers = this._Map.getLayers().getArray(); + const matchingLayer = layers.find(layer => { + const layerSource = layer.get('source'); + return ( + layerSource.get('id') === this._model.selectedVectorLayerSourceId + ); + }); + let source; + if (matchingLayer) { + source = matchingLayer.get('source'); + } else { + source = new VectorSource(); + } const vectorLayer = new VectorLayer({ - source: source, - style: { + source: source + /*style: { 'fill-color': 'rgba(255, 255, 255, 0.2)', 'stroke-color': '#ffcc33', 'stroke-width': 2, 'circle-radius': 7, 'circle-fill-color': '#ffcc33' - } + }*/ }); this._Map.addLayer(vectorLayer); - console.log('vector layer:', vectorLayer.getSource()?.getFeatures()); const modify = new Modify({ source: source }); this._Map.addInteraction(modify); const draw = new Draw({ diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index 161f3f9bf..2f5f9f562 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -237,6 +237,7 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { disposed: ISignal; isDrawVectorLayerEnabled: boolean; updateIsDrawVectorLayerEnabled(): void; + selectedVectorLayerSourceId: string; } export interface IUserData { diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index e3385514a..d1e42f21b 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -742,7 +742,6 @@ export class JupyterGISModel implements IJupyterGISModel { updateIsDrawVectorLayerEnabled() { this.drawVectorLayerChanged.emit(this.isDrawVectorLayerEnabled); - console.log('Signal emitted'); } get geolocation(): JgisCoordinates { @@ -801,6 +800,7 @@ export class JupyterGISModel implements IJupyterGISModel { public isDrawVectorLayerEnabled: boolean; public drawVectorLayerChanged = new Signal(this); + public selectedVectorLayerSourceId = ''; } export namespace JupyterGISModel { From 52a592673d8cad455a395066f1f702ed14b8cb97 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Mon, 19 May 2025 15:24:56 +0200 Subject: [PATCH 08/30] Update the _handleDrawGeometryTypeChange method with removing the creation of a new instance of vectorLayer and the addition of this layer to the map. Add style to the draw interaction. --- packages/base/src/mainview/mainView.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 57e34c26e..098321777 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -2053,21 +2053,17 @@ export class MainView extends React.Component { } else { source = new VectorSource(); } - const vectorLayer = new VectorLayer({ - source: source - /*style: { + const modify = new Modify({ source: source }); + this._Map.addInteraction(modify); + const draw = new Draw({ + source: source, + style: { 'fill-color': 'rgba(255, 255, 255, 0.2)', 'stroke-color': '#ffcc33', 'stroke-width': 2, 'circle-radius': 7, 'circle-fill-color': '#ffcc33' - }*/ - }); - this._Map.addLayer(vectorLayer); - const modify = new Modify({ source: source }); - this._Map.addInteraction(modify); - const draw = new Draw({ - source: source, + }, type: drawGeometryType as Type // Type being a geometry type here }); const snap = new Snap({ source: source }); From 4732246713d22f3eea0555c177debf3090d3599d Mon Sep 17 00:00:00 2001 From: Florence Haudin <99649086+HaudinFlorence@users.noreply.github.com> Date: Tue, 20 May 2025 11:26:48 +0200 Subject: [PATCH 09/30] Update packages/base/src/commands.ts Co-authored-by: martinRenou --- packages/base/src/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index ff83b26ab..b9ba1f9f3 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -946,7 +946,7 @@ export function addCommands( return false; } - const canDrawVectorLayer = ['VectorLayer'].includes(selectedLayer.type); + const canDrawVectorLayer = selectedSource.type === 'GeoJSONSource' && selectedSource.data; if ( tracker.currentWidget instanceof JupyterGISDocumentWidget && From c43d67dc01c62002af6203ef780b76ae907457f3 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Tue, 20 May 2025 13:40:15 +0200 Subject: [PATCH 10/30] Update newDrawVectorLayer command. --- packages/base/src/commands.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index b9ba1f9f3..f074690b0 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -945,19 +945,21 @@ export function addCommands( if (!selectedLayer) { return false; } - - const canDrawVectorLayer = selectedSource.type === 'GeoJSONSource' && selectedSource.data; - - if ( - tracker.currentWidget instanceof JupyterGISDocumentWidget && - canDrawVectorLayer - ) { + if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { const model = tracker.currentWidget?.content.currentViewModel .jGISModel as IJupyterGISModel; const parameters = selectedLayer.parameters; if (parameters) { - const selectedvectorLayerSourceId = parameters?.source; - model.selectedVectorLayerSourceId = selectedvectorLayerSourceId; + const selectedSource = model.getSource( + selectedLayer.parameters?.source + ); + const canDrawVectorLayer = + selectedSource?.type === 'GeoJSONSource' && + selectedSource?.parameters?.data; + if (canDrawVectorLayer) { + const selectedvectorLayerSourceId = parameters.source; + model.selectedVectorLayerSourceId = selectedvectorLayerSourceId; + } } return model.isDrawVectorLayerEnabled; From cc2ec6ccf93bc0aa1366881cbdb6b582954ead71 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Tue, 20 May 2025 13:47:17 +0200 Subject: [PATCH 11/30] Remove test-results/.last-run.json from the git history tracking and add test-results to the .gitignore file. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d265d6396..9cbc67744 100644 --- a/.gitignore +++ b/.gitignore @@ -125,6 +125,9 @@ dmypy.json **/ui-tests/test-results/ **/ui-tests/playwright-report/ +# tests_results +**/test-results/ + examples/Untitled*.ipynb # Hatchling jupytergis/_version.py From b042477ec1fa3d52f609586a1144b913d0d3bd86 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Tue, 20 May 2025 15:35:38 +0200 Subject: [PATCH 12/30] Update the newDrawVectorLayer command and the _handleDrawGeometryTypeChange method to update the source in the JupyterGISDoc. --- examples/editable.jGIS | 102 ++++++++++++++++++++++++ packages/base/src/commands.ts | 25 +++--- packages/base/src/mainview/mainView.tsx | 55 +++++++++---- packages/base/src/tools.ts | 3 + packages/schema/src/interfaces.ts | 1 - packages/schema/src/model.ts | 1 - 6 files changed, 156 insertions(+), 31 deletions(-) create mode 100644 examples/editable.jGIS diff --git a/examples/editable.jGIS b/examples/editable.jGIS new file mode 100644 index 000000000..e8061ba0b --- /dev/null +++ b/examples/editable.jGIS @@ -0,0 +1,102 @@ +{ + "layerTree": [ + "8de7c2c0-6024-4716-b542-031a89fb87f9", + "3e21d680-406f-4099-bd9e-3a4edb9a2c8b" + ], + "layers": { + "3e21d680-406f-4099-bd9e-3a4edb9a2c8b": { + "filters": { + "appliedFilters": [], + "logicalOp": "all" + }, + "name": "Editable GeoJSON Layer", + "parameters": { + "color": { + "circle-fill-color": "#f66151", + "circle-radius": 5.0, + "circle-stroke-color": "#62a0ea", + "circle-stroke-line-cap": "round", + "circle-stroke-line-join": "round", + "circle-stroke-width": 1.25 + }, + "opacity": 1.0, + "source": "348d85fa-3a71-447f-8a64-e283ec47cc7c", + "symbologyState": { + "renderType": "Single Symbol" + }, + "type": "circle" + }, + "type": "VectorLayer", + "visible": true + }, + "8de7c2c0-6024-4716-b542-031a89fb87f9": { + "name": "OpenStreetMap.Mapnik Layer", + "parameters": { + "source": "b2ea427a-a51b-43ad-ae72-02cd900736d5" + }, + "type": "RasterLayer", + "visible": true + } + }, + "metadata": {}, + "options": { + "bearing": 0.0, + "extent": [ + -14181614.437015302, + -5303433.533961326, + -2473763.273952904, + 13774201.834902454 + ], + "latitude": 35.52446437432016, + "longitude": -74.80890180273175, + "pitch": 0.0, + "projection": "EPSG:3857", + "zoom": 2.6670105136699993 + }, + "schemaVersion": "0.5.0", + "sources": { + "348d85fa-3a71-447f-8a64-e283ec47cc7c": { + "name": "Editable GeoJSON Layer Source", + "parameters": { + "data": { + "features": [ + { + "geometry": { + "coordinates": [ + 102.0, + 0.5 + ], + "type": "Point" + }, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + 102.0, + 0.5 + ], + "type": "Point" + }, + "type": "Feature" + } + ], + "type": "FeatureCollection" + } + }, + "type": "GeoJSONSource" + }, + "b2ea427a-a51b-43ad-ae72-02cd900736d5": { + "name": "OpenStreetMap.Mapnik", + "parameters": { + "attribution": "(C) OpenStreetMap contributors", + "maxZoom": 19.0, + "minZoom": 0.0, + "provider": "OpenStreetMap", + "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + "urlParameters": {} + }, + "type": "RasterSource" + } + } +} \ No newline at end of file diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index f074690b0..9f907d615 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -941,34 +941,32 @@ export function addCommands( commands.addCommand(CommandIDs.newDrawVectorLayer, { label: trans.__('Create New Draw Vector Layer'), isToggled: () => { - const selectedLayer = getSingleSelectedLayer(tracker); - if (!selectedLayer) { - return false; - } if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { const model = tracker.currentWidget?.content.currentViewModel .jGISModel as IJupyterGISModel; - const parameters = selectedLayer.parameters; - if (parameters) { + const selectedLayer = getSingleSelectedLayer(tracker); + if (!selectedLayer) { + return false; + } else { const selectedSource = model.getSource( selectedLayer.parameters?.source ); - const canDrawVectorLayer = + if ( selectedSource?.type === 'GeoJSONSource' && - selectedSource?.parameters?.data; - if (canDrawVectorLayer) { - const selectedvectorLayerSourceId = parameters.source; - model.selectedVectorLayerSourceId = selectedvectorLayerSourceId; + selectedSource?.parameters?.data + ) { + return model.isDrawVectorLayerEnabled === true; + } else { + return false; } } - - return model.isDrawVectorLayerEnabled; } else { return false; } }, isEnabled: () => { const selectedLayer = getSingleSelectedLayer(tracker); + if (!selectedLayer) { return false; } @@ -978,6 +976,7 @@ export function addCommands( if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { const model = tracker.currentWidget?.content.currentViewModel .jGISModel as IJupyterGISModel; + if (model.isDrawVectorLayerEnabled === true) { model.isDrawVectorLayerEnabled = false; } else { diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 098321777..556b991bf 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -87,6 +87,7 @@ import CollaboratorPointers, { ClientPointer } from './CollaboratorPointers'; import { FollowIndicator } from './FollowIndicator'; import TemporalSlider from './TemporalSlider'; import { MainViewModel } from './mainviewmodel'; +import BaseLayer from 'ol/layer/Base'; import Draw from 'ol/interaction/Draw.js'; import Modify from 'ol/interaction/Modify.js'; import Snap from 'ol/interaction/Snap.js'; @@ -2035,28 +2036,29 @@ export class MainView extends React.Component { event: React.ChangeEvent ) => { const drawGeometryType = event.target.value; + if (this._model.isDrawVectorLayerEnabled) { if (this._currentDrawInteraction) { this._removeCurrentDrawInteraction(); } + const localState = this._model?.sharedModel.awareness.getLocalState(); + const localStateLayer = localState?.selected?.value; + const localStateLayerID = Object.keys(localStateLayer)[0]; + const jGISLayer = this._model.getLayer(localStateLayerID); + let jGISLayerSource = this._model.getSource( + jGISLayer?.parameters?.source + ); - const layers = this._Map.getLayers().getArray(); - const matchingLayer = layers.find(layer => { - const layerSource = layer.get('source'); - return ( - layerSource.get('id') === this._model.selectedVectorLayerSourceId - ); - }); - let source; - if (matchingLayer) { - source = matchingLayer.get('source'); - } else { - source = new VectorSource(); - } - const modify = new Modify({ source: source }); + const layerSource: VectorSource | undefined = this._Map + .getLayers() + .getArray() + .find((layer: BaseLayer) => layer.get('id') === localStateLayerID) + ?.get('source'); + + const modify = new Modify({ source: layerSource }); this._Map.addInteraction(modify); const draw = new Draw({ - source: source, + source: layerSource, style: { 'fill-color': 'rgba(255, 255, 255, 0.2)', 'stroke-color': '#ffcc33', @@ -2066,7 +2068,7 @@ export class MainView extends React.Component { }, type: drawGeometryType as Type // Type being a geometry type here }); - const snap = new Snap({ source: source }); + const snap = new Snap({ source: layerSource }); this._Map.addInteraction(draw); this._Map.addInteraction(snap); @@ -2075,6 +2077,27 @@ export class MainView extends React.Component { ...old, drawGeometryType })); + + draw.on('drawend', event => { + if (jGISLayerSource) { + const updatedData = { + type: 'FeatureCollection', + features: layerSource?.getFeatures() + }; + const updatedJGISLayerSource: IJGISSource = { + name: jGISLayerSource.name, + type: jGISLayerSource.type, + parameters: { + data: updatedData + } + }; + jGISLayerSource = updatedJGISLayerSource; + this._model.sharedModel.updateSource( + localStateLayerID, + updatedJGISLayerSource + ); + } + }); } else { return; } diff --git a/packages/base/src/tools.ts b/packages/base/src/tools.ts index 77b83ab58..c44cf1cee 100644 --- a/packages/base/src/tools.ts +++ b/packages/base/src/tools.ts @@ -499,6 +499,9 @@ export const loadFile = async (fileInfo: { model: IJupyterGISModel; }) => { const { filepath, type, model } = fileInfo; + if (!filepath) { + return; + } if (filepath.startsWith('http://') || filepath.startsWith('https://')) { switch (type) { diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index 2f5f9f562..161f3f9bf 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -237,7 +237,6 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { disposed: ISignal; isDrawVectorLayerEnabled: boolean; updateIsDrawVectorLayerEnabled(): void; - selectedVectorLayerSourceId: string; } export interface IUserData { diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index d1e42f21b..52b463428 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -800,7 +800,6 @@ export class JupyterGISModel implements IJupyterGISModel { public isDrawVectorLayerEnabled: boolean; public drawVectorLayerChanged = new Signal(this); - public selectedVectorLayerSourceId = ''; } export namespace JupyterGISModel { From c41b05b8b2bbf3128456e0b1de61205dc07f8fc8 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Tue, 27 May 2025 18:21:06 +0200 Subject: [PATCH 13/30] Remove the enabled method in the the newDrawVectorLayer command. Create a new empty layer with a GeoJSONSource when there is no selected layer. Add some logics to ensure that the overlay is properly removed when the selected layer is switched and doesn't have the right source type and some embedded data. --- packages/base/src/commands.ts | 54 ++++++++++++++++++------- packages/base/src/mainview/mainView.tsx | 3 +- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 9f907d615..5ecc40738 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -1,9 +1,11 @@ import { IDict, IJGISFormSchemaRegistry, + IJGISLayer, IJGISLayerBrowserRegistry, IJGISLayerGroup, IJGISLayerItem, + IJGISSource, IJupyterGISModel, JgisCoordinates, LayerType, @@ -16,10 +18,9 @@ import { ICompletionProviderManager } from '@jupyterlab/completer'; import { IStateDB } from '@jupyterlab/statedb'; import { ITranslator } from '@jupyterlab/translation'; import { CommandRegistry } from '@lumino/commands'; -import { ReadonlyPartialJSONObject } from '@lumino/coreutils'; +import { ReadonlyPartialJSONObject, UUID} from '@lumino/coreutils'; import { Coordinate } from 'ol/coordinate'; import { fromLonLat } from 'ol/proj'; - import { CommandIDs, icons } from './constants'; import { ProcessingFormDialog } from './dialogs/ProcessingFormDialog'; import { LayerBrowserWidget } from './dialogs/layerBrowserDialog'; @@ -941,8 +942,11 @@ export function addCommands( commands.addCommand(CommandIDs.newDrawVectorLayer, { label: trans.__('Create New Draw Vector Layer'), isToggled: () => { - if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { - const model = tracker.currentWidget?.content.currentViewModel + const current = tracker.currentWidget; + if (!current || !(current instanceof JupyterGISDocumentWidget)) { + return false; + } else { + const model = current.content.currentViewModel .jGISModel as IJupyterGISModel; const selectedLayer = getSingleSelectedLayer(tracker); if (!selectedLayer) { @@ -955,33 +959,53 @@ export function addCommands( selectedSource?.type === 'GeoJSONSource' && selectedSource?.parameters?.data ) { - return model.isDrawVectorLayerEnabled === true; + return model.isDrawVectorLayerEnabled; } else { + /* The source of this layer is not a GeoJSONSource with embedded data*/ + model.isDrawVectorLayerEnabled = false; + model.updateIsDrawVectorLayerEnabled(); return false; } } - } else { - return false; } }, - isEnabled: () => { - const selectedLayer = getSingleSelectedLayer(tracker); - - if (!selectedLayer) { - return false; - } - return ['VectorLayer'].includes(selectedLayer.type); - }, execute: async () => { if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { const model = tracker.currentWidget?.content.currentViewModel .jGISModel as IJupyterGISModel; + const localState = model?.sharedModel.awareness.getLocalState(); + if (localState && localState['selected'] === undefined) { + const emptySourceID = UUID.uuid4(); + const emptyLayerID = UUID.uuid4(); + const emptyLayer: IJGISLayer = { + name: 'Editable GeoJSON Layer', + type: 'VectorLayer', + visible: true, + parameters: { + source: emptySourceID + } + }; + const emptySource: IJGISSource = { + name: 'Editable GeoJSON Layer Source', + type: 'GeoJSONSource', + parameters: { + data: { + type: 'FeatureCollection', + features: [] + } + } + }; + model.sharedModel.addSource(emptySourceID, emptySource); + model.addLayer(emptyLayerID, emptyLayer); + localState['selected'] = emptyLayer; + } if (model.isDrawVectorLayerEnabled === true) { model.isDrawVectorLayerEnabled = false; } else { model.isDrawVectorLayerEnabled = true; } + model.updateIsDrawVectorLayerEnabled(); commands.notifyCommandChanged(CommandIDs.newDrawVectorLayer); } diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 556b991bf..bf1d33692 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -2013,7 +2013,8 @@ export class MainView extends React.Component { } private _updateIsDrawVectorLayerEnabled() { - const isDrawVectorLayerEnabled = this._model.isDrawVectorLayerEnabled; + const isDrawVectorLayerEnabled: boolean = + this._model.isDrawVectorLayerEnabled; this.setState(old => ({ ...old, isDrawVectorLayerEnabled })); if (isDrawVectorLayerEnabled === false && this._currentDrawInteraction) { this._removeCurrentDrawInteraction(); From 1f4a6377f4a023d815311ff77f12e5154b714fc9 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Tue, 27 May 2025 19:07:26 +0200 Subject: [PATCH 14/30] Update _handleDrawGeometryTypeChange with replacing the drawend event by a on change event on the layer source. Use a GeoJSON instance and its writeFeatureObject method for the features attribute of the updatedData. --- packages/base/src/mainview/mainView.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index bf1d33692..306cbb3ba 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -2046,9 +2046,8 @@ export class MainView extends React.Component { const localStateLayer = localState?.selected?.value; const localStateLayerID = Object.keys(localStateLayer)[0]; const jGISLayer = this._model.getLayer(localStateLayerID); - let jGISLayerSource = this._model.getSource( - jGISLayer?.parameters?.source - ); + const localStateSourceID = jGISLayer?.parameters?.source; + let jGISLayerSource = this._model.getSource(localStateSourceID); const layerSource: VectorSource | undefined = this._Map .getLayers() @@ -2079,22 +2078,28 @@ export class MainView extends React.Component { drawGeometryType })); - draw.on('drawend', event => { + const geojsonWriter = new GeoJSON({ + featureProjection: this._Map.getView().getProjection() + }); + + layerSource?.on('change', () => { if (jGISLayerSource) { const updatedData = { type: 'FeatureCollection', - features: layerSource?.getFeatures() + features: layerSource + ?.getFeatures() + .map(feature => geojsonWriter.writeFeatureObject(feature)) }; const updatedJGISLayerSource: IJGISSource = { name: jGISLayerSource.name, type: jGISLayerSource.type, parameters: { data: updatedData - } + } }; jGISLayerSource = updatedJGISLayerSource; this._model.sharedModel.updateSource( - localStateLayerID, + localStateSourceID, updatedJGISLayerSource ); } From f5db4daf2d2b34da8b556ace42151782f7a6a0b1 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Mon, 2 Jun 2025 15:19:31 +0200 Subject: [PATCH 15/30] Change the ui : use the toolbar button for the creation of a new empty vector layer and add a context menu for an already existing vector layer. Update _handleDrawGeometryTypeChange method of MainView class to properly deal with the case where there is a selected vector layer and the case where there isn't. Add a new command addNewDrawFeatures enabled in the context menu of a Vector Layer. Move the logics for the creation of an empty layer with an empty geojson source into a dedicated method createEmptyVectorLayerWithGeoJSONSource in the JupyterGISModel class. --- packages/base/src/commands.ts | 93 ++++++++------- packages/base/src/constants.ts | 3 + packages/base/src/mainview/mainView.tsx | 150 ++++++++++++++---------- packages/base/src/toolbar/widget.tsx | 2 +- packages/schema/src/interfaces.ts | 1 + packages/schema/src/model.ts | 27 ++++- python/jupytergis_lab/src/index.ts | 12 ++ 7 files changed, 184 insertions(+), 104 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 5ecc40738..3ba4816a1 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -1,11 +1,9 @@ import { IDict, IJGISFormSchemaRegistry, - IJGISLayer, IJGISLayerBrowserRegistry, IJGISLayerGroup, IJGISLayerItem, - IJGISSource, IJupyterGISModel, JgisCoordinates, LayerType, @@ -18,7 +16,7 @@ import { ICompletionProviderManager } from '@jupyterlab/completer'; import { IStateDB } from '@jupyterlab/statedb'; import { ITranslator } from '@jupyterlab/translation'; import { CommandRegistry } from '@lumino/commands'; -import { ReadonlyPartialJSONObject, UUID} from '@lumino/coreutils'; +import { ReadonlyPartialJSONObject } from '@lumino/coreutils'; import { Coordinate } from 'ol/coordinate'; import { fromLonLat } from 'ol/proj'; import { CommandIDs, icons } from './constants'; @@ -939,14 +937,11 @@ export function addCommands( icon: targetWithCenterIcon, }); - commands.addCommand(CommandIDs.newDrawVectorLayer, { - label: trans.__('Create New Draw Vector Layer'), - isToggled: () => { - const current = tracker.currentWidget; - if (!current || !(current instanceof JupyterGISDocumentWidget)) { - return false; - } else { - const model = current.content.currentViewModel + commands.addCommand(CommandIDs.addNewDrawFeatures, { + label: trans.__('Add New Draw Features'), + isEnabled: () => { + if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { + const model = tracker.currentWidget?.content.currentViewModel .jGISModel as IJupyterGISModel; const selectedLayer = getSingleSelectedLayer(tracker); if (!selectedLayer) { @@ -959,46 +954,64 @@ export function addCommands( selectedSource?.type === 'GeoJSONSource' && selectedSource?.parameters?.data ) { - return model.isDrawVectorLayerEnabled; + return true; } else { - /* The source of this layer is not a GeoJSONSource with embedded data*/ - model.isDrawVectorLayerEnabled = false; - model.updateIsDrawVectorLayerEnabled(); return false; } } + } else { + return false; } }, execute: async () => { if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { const model = tracker.currentWidget?.content.currentViewModel .jGISModel as IJupyterGISModel; - const localState = model?.sharedModel.awareness.getLocalState(); - if (localState && localState['selected'] === undefined) { - const emptySourceID = UUID.uuid4(); - const emptyLayerID = UUID.uuid4(); - const emptyLayer: IJGISLayer = { - name: 'Editable GeoJSON Layer', - type: 'VectorLayer', - visible: true, - parameters: { - source: emptySourceID - } - }; - const emptySource: IJGISSource = { - name: 'Editable GeoJSON Layer Source', - type: 'GeoJSONSource', - parameters: { - data: { - type: 'FeatureCollection', - features: [] - } - } - }; - model.sharedModel.addSource(emptySourceID, emptySource); - model.addLayer(emptyLayerID, emptyLayer); - localState['selected'] = emptyLayer; + + if (model.isDrawVectorLayerEnabled === false) { + model.isDrawVectorLayerEnabled = true; } + model.updateIsDrawVectorLayerEnabled(); + commands.notifyCommandChanged(CommandIDs.addNewDrawFeatures); + } + }, + icon: pencilSolidIcon + }); + + commands.addCommand(CommandIDs.newDrawVectorLayer, { + label: trans.__('Create New Draw Vector Layer'), + isToggled: () => { + const current = tracker.currentWidget; + if (!current || !(current instanceof JupyterGISDocumentWidget)) { + return false; + } else { + const model = current.content.currentViewModel + .jGISModel as IJupyterGISModel; + return model.isDrawVectorLayerEnabled; + } + }, + execute: async () => { + if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { + const model = tracker.currentWidget?.content.currentViewModel + .jGISModel as IJupyterGISModel; + const layers = model.getLayers(); + const layersArray = Object.values(layers); + + layersArray.forEach(layer => { + if (layer.type === 'VectorLayer') { + const source = model.getSource(layer.parameters?.source); + if (source?.type === 'GeoJSONSource' && source.parameters?.data) { + console.warn( + 'There is already a Vector Layer with a geoGSON source.' + ); + model.isDrawVectorLayerEnabled = false; + return; + } else { + model.isDrawVectorLayerEnabled = true; + model.createEmptyVectorLayerWithGeoJSONSource(); + } + } + }); if (model.isDrawVectorLayerEnabled === true) { model.isDrawVectorLayerEnabled = false; diff --git a/packages/base/src/constants.ts b/packages/base/src/constants.ts index 00bfacd6a..b89de7dc4 100644 --- a/packages/base/src/constants.ts +++ b/packages/base/src/constants.ts @@ -66,6 +66,9 @@ export namespace CommandIDs { export const renameSource = 'jupytergis:renameSource'; export const removeSource = 'jupytergis:removeSource'; + // Add draw features to a geoGSON source + export const addNewDrawFeatures = 'jupytergis:addNewDrawFeatures'; + // Console commands export const toggleConsole = 'jupytergis:toggleConsole'; export const invokeCompleter = 'jupytergis:invokeConsoleCompleter'; diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 306cbb3ba..42e17ed87 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -89,7 +89,7 @@ import TemporalSlider from './TemporalSlider'; import { MainViewModel } from './mainviewmodel'; import BaseLayer from 'ol/layer/Base'; import Draw from 'ol/interaction/Draw.js'; -import Modify from 'ol/interaction/Modify.js'; +//import Modify from 'ol/interaction/Modify.js'; import Snap from 'ol/interaction/Snap.js'; import { Type } from 'ol/geom/Geometry'; @@ -2037,73 +2037,99 @@ export class MainView extends React.Component { event: React.ChangeEvent ) => { const drawGeometryType = event.target.value; - + //let layerSource: IJGISSource | undefined; if (this._model.isDrawVectorLayerEnabled) { if (this._currentDrawInteraction) { this._removeCurrentDrawInteraction(); } const localState = this._model?.sharedModel.awareness.getLocalState(); - const localStateLayer = localState?.selected?.value; - const localStateLayerID = Object.keys(localStateLayer)[0]; - const jGISLayer = this._model.getLayer(localStateLayerID); - const localStateSourceID = jGISLayer?.parameters?.source; - let jGISLayerSource = this._model.getSource(localStateSourceID); - - const layerSource: VectorSource | undefined = this._Map - .getLayers() - .getArray() - .find((layer: BaseLayer) => layer.get('id') === localStateLayerID) - ?.get('source'); - - const modify = new Modify({ source: layerSource }); - this._Map.addInteraction(modify); - const draw = new Draw({ - source: layerSource, - style: { - 'fill-color': 'rgba(255, 255, 255, 0.2)', - 'stroke-color': '#ffcc33', - 'stroke-width': 2, - 'circle-radius': 7, - 'circle-fill-color': '#ffcc33' - }, - type: drawGeometryType as Type // Type being a geometry type here - }); - const snap = new Snap({ source: layerSource }); - - this._Map.addInteraction(draw); - this._Map.addInteraction(snap); - this._currentDrawInteraction = draw; - this.setState(old => ({ - ...old, - drawGeometryType - })); - - const geojsonWriter = new GeoJSON({ - featureProjection: this._Map.getView().getProjection() - }); + const layers = this._model.getLayers(); + const layersArray = Object.values(layers); + const localStateSelectedLayers = localState?.selected?.value; + + /** case with no vector layer and no selected one */ + if (!localStateSelectedLayers) { + layersArray.forEach(layer => { + if (layer.type === 'VectorLayer') { + const source = this._model.getSource(layer.parameters?.source); + if (source?.type === 'GeoJSONSource' && source.parameters?.data) { + console.warn( + 'There is already a Vector Layer with a geoGSON source.' + ); + this._model.isDrawVectorLayerEnabled = false; + return; + } + } + }); + this._model.createEmptyVectorLayerWithGeoJSONSource(); + } else { + /** case with selected vector layer: add new feature to the already existing geoJSONSource + */ + const localStateSelectedLayerID = Object.keys( + localStateSelectedLayers + )[0]; + const jGISLayer = this._model.getLayer(localStateSelectedLayerID); + const localStateSourceID = jGISLayer?.parameters?.source; + let jGISLayerSource = this._model.getSource(localStateSourceID); + + const layerSource: VectorSource | undefined = this._Map + .getLayers() + .getArray() + .find( + (layer: BaseLayer) => layer.get('id') === localStateSelectedLayerID + ) + ?.get('source'); + + //const modify = new Modify({ source: layerSource }); + //this._Map.addInteraction(modify); + const draw = new Draw({ + source: layerSource, + style: { + 'fill-color': 'rgba(255, 255, 255, 0.2)', + 'stroke-color': '#ffcc33', + 'stroke-width': 2, + 'circle-radius': 7, + 'circle-fill-color': '#ffcc33' + }, + type: drawGeometryType as Type // Type being a geometry type here + }); + const snap = new Snap({ source: layerSource }); + + this._Map.addInteraction(draw); + this._Map.addInteraction(snap); + this._currentDrawInteraction = draw; + this.setState(old => ({ + ...old, + drawGeometryType + })); + + const geojsonWriter = new GeoJSON({ + featureProjection: this._Map.getView().getProjection() + }); - layerSource?.on('change', () => { - if (jGISLayerSource) { - const updatedData = { - type: 'FeatureCollection', - features: layerSource - ?.getFeatures() - .map(feature => geojsonWriter.writeFeatureObject(feature)) - }; - const updatedJGISLayerSource: IJGISSource = { - name: jGISLayerSource.name, - type: jGISLayerSource.type, - parameters: { - data: updatedData - } - }; - jGISLayerSource = updatedJGISLayerSource; - this._model.sharedModel.updateSource( - localStateSourceID, - updatedJGISLayerSource - ); - } - }); + layerSource?.on('change', () => { + if (jGISLayerSource) { + const updatedData = { + type: 'FeatureCollection', + features: layerSource + ?.getFeatures() + .map(feature => geojsonWriter.writeFeatureObject(feature)) + }; + const updatedJGISLayerSource: IJGISSource = { + name: jGISLayerSource.name, + type: jGISLayerSource.type, + parameters: { + data: updatedData + } + }; + jGISLayerSource = updatedJGISLayerSource; + this._model.sharedModel.updateSource( + localStateSourceID, + updatedJGISLayerSource + ); + } + }); + } } else { return; } diff --git a/packages/base/src/toolbar/widget.tsx b/packages/base/src/toolbar/widget.tsx index 4e230a89d..10fb2f04c 100644 --- a/packages/base/src/toolbar/widget.tsx +++ b/packages/base/src/toolbar/widget.tsx @@ -123,7 +123,7 @@ export class ToolbarWidget extends ReactiveToolbar { label: '' }); this.addItem('DrawVector', drawVectoreLayerButton); - geolocationButton.node.dataset.testid = 'draw-vector-layer-button'; + drawVectoreLayerButton.node.dataset.testid = 'draw-vector-layer-button'; const identifyButton = new CommandToolbarButton({ id: CommandIDs.identify, diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index 161f3f9bf..a6e7c83d6 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -237,6 +237,7 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { disposed: ISignal; isDrawVectorLayerEnabled: boolean; updateIsDrawVectorLayerEnabled(): void; + createEmptyVectorLayerWithGeoJSONSource(): void; } export interface IUserData { diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index 52b463428..fbd794346 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -3,7 +3,7 @@ import { IChangedArgs } from '@jupyterlab/coreutils'; import { DocumentRegistry } from '@jupyterlab/docregistry'; import { Contents } from '@jupyterlab/services'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; -import { PartialJSONObject } from '@lumino/coreutils'; +import { PartialJSONObject, UUID } from '@lumino/coreutils'; import { ISignal, Signal } from '@lumino/signaling'; import Ajv from 'ajv'; @@ -757,6 +757,31 @@ export class JupyterGISModel implements IJupyterGISModel { return this._geolocationChanged; } + createEmptyVectorLayerWithGeoJSONSource = () => { + const emptySourceID = UUID.uuid4(); + const emptyLayerID = UUID.uuid4(); + const emptyLayer: IJGISLayer = { + name: 'Editable GeoJSON Layer', + type: 'VectorLayer', + visible: true, + parameters: { + source: emptySourceID + } + }; + const emptySource: IJGISSource = { + name: 'Editable GeoJSON Layer Source', + type: 'GeoJSONSource', + parameters: { + data: { + type: 'FeatureCollection', + features: [] + } + } + }; + this.sharedModel.addSource(emptySourceID, emptySource); + this.addLayer(emptyLayerID, emptyLayer); + }; + readonly defaultKernelName: string = ''; readonly defaultKernelLanguage: string = ''; readonly annotationModel?: IAnnotationModel; diff --git a/python/jupytergis_lab/src/index.ts b/python/jupytergis_lab/src/index.ts index ec34eb6e9..8cd9af40b 100644 --- a/python/jupytergis_lab/src/index.ts +++ b/python/jupytergis_lab/src/index.ts @@ -125,6 +125,18 @@ const plugin: JupyterFrontEndPlugin = { rank: 2, }); + app.contextMenu.addItem({ + command: CommandIDs.zoomToLayer, + selector: '.jp-gis-layerItem', + rank: 2 + }); + + app.contextMenu.addItem({ + command: CommandIDs.addNewDrawFeatures, + selector: '.jp-gis-layerItem', + rank: 2 + }); + // Create the Download submenu const downloadSubmenu = new Menu({ commands: app.commands }); downloadSubmenu.title.label = translator.load('jupyterlab').__('Download'); From 5152236e4d833a81dfd8ad8e24fa159536376dba Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Mon, 2 Jun 2025 23:40:22 +0200 Subject: [PATCH 16/30] Update the GeoJSONSourcePropertiesForm class to enable the possibility to not have a path provided and to enable the creation of an empty layer with geoJSON source with embedded data. --- .../objectform/source/geojsonsource.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/base/src/formbuilder/objectform/source/geojsonsource.ts b/packages/base/src/formbuilder/objectform/source/geojsonsource.ts index 35ef9beca..32f863edf 100644 --- a/packages/base/src/formbuilder/objectform/source/geojsonsource.ts +++ b/packages/base/src/formbuilder/objectform/source/geojsonsource.ts @@ -56,7 +56,27 @@ export class GeoJSONSourcePropertiesForm extends PathBasedSourcePropertiesForm { error = `"${path}" is not a valid GeoJSON file: ${e}`; } } else { - error = 'Path is required'; + //error = 'Path is required'; + const layers = this.props.model.getLayers(); + const layersArray = Object.values(layers); + valid = true; + + layersArray.forEach(layer => { + if (layer.type === 'VectorLayer') { + const source = this.props.model.getSource(layer.parameters?.source); + if (source?.type === 'GeoJSONSource' && source.parameters?.data) { + error = + 'There is already a Vector Layer with a geoGSON source.' + + this.props.model.isDrawVectorLayerEnabled = false; + return; + } else { + console.log('No vector layer with geoJSON source with embedded date') + this.props.model.isDrawVectorLayerEnabled = true; + this.props.model.createEmptyVectorLayerWithGeoJSONSource(); + } + } + }) } if (!valid) { From d234d9c4c0ba3f2135edfc47f77da8059931e5c2 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Wed, 4 Jun 2025 18:57:47 +0200 Subject: [PATCH 17/30] Change the ui to create a new empty vector layer by clicking on the plus toolbar button and using the already existing dialog and form. Modify the logics to enable situations where no path is provided in the dialog. Remove the command addNewDrawVectorLayer and the toolbar button. Remove method createEmptyVectorLayerWithGeoJSONSource from JupyterGISModel class and IJupyterGISModel interface. Update _handleDrawGeometryTypeChange method from the MainView class. Add a condition if GeoJSONSourcePropertiesForm for the case where there is no path. --- packages/base/src/commands.ts | 48 ------ packages/base/src/constants.ts | 1 - .../objectform/source/geojsonsource.ts | 24 +-- .../objectform/source/pathbasedsource.ts | 6 + packages/base/src/mainview/mainView.tsx | 156 ++++++++---------- packages/base/src/panelview/leftpanel.tsx | 1 - packages/base/src/toolbar/widget.tsx | 8 - packages/schema/src/interfaces.ts | 1 - packages/schema/src/model.ts | 27 +-- 9 files changed, 79 insertions(+), 193 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 3ba4816a1..2ff92fef7 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -978,54 +978,6 @@ export function addCommands( icon: pencilSolidIcon }); - commands.addCommand(CommandIDs.newDrawVectorLayer, { - label: trans.__('Create New Draw Vector Layer'), - isToggled: () => { - const current = tracker.currentWidget; - if (!current || !(current instanceof JupyterGISDocumentWidget)) { - return false; - } else { - const model = current.content.currentViewModel - .jGISModel as IJupyterGISModel; - return model.isDrawVectorLayerEnabled; - } - }, - execute: async () => { - if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { - const model = tracker.currentWidget?.content.currentViewModel - .jGISModel as IJupyterGISModel; - const layers = model.getLayers(); - const layersArray = Object.values(layers); - - layersArray.forEach(layer => { - if (layer.type === 'VectorLayer') { - const source = model.getSource(layer.parameters?.source); - if (source?.type === 'GeoJSONSource' && source.parameters?.data) { - console.warn( - 'There is already a Vector Layer with a geoGSON source.' - ); - model.isDrawVectorLayerEnabled = false; - return; - } else { - model.isDrawVectorLayerEnabled = true; - model.createEmptyVectorLayerWithGeoJSONSource(); - } - } - }); - - if (model.isDrawVectorLayerEnabled === true) { - model.isDrawVectorLayerEnabled = false; - } else { - model.isDrawVectorLayerEnabled = true; - } - - model.updateIsDrawVectorLayerEnabled(); - commands.notifyCommandChanged(CommandIDs.newDrawVectorLayer); - } - }, - icon: pencilSolidIcon - }); - loadKeybindings(commands, keybindings); } diff --git a/packages/base/src/constants.ts b/packages/base/src/constants.ts index b89de7dc4..d3a489574 100644 --- a/packages/base/src/constants.ts +++ b/packages/base/src/constants.ts @@ -52,7 +52,6 @@ export namespace CommandIDs { export const newShapefileLayer = 'jupytergis:newShapefileLayer'; export const newWebGlTileLayer = 'jupytergis:newWebGlTileLayer'; export const newHeatmapLayer = 'jupytergis:newHeatmapLayer'; - export const newDrawVectorLayer = 'jupytergis:newDrawVectorLayer'; // Layer and group actions export const renameLayer = 'jupytergis:renameLayer'; diff --git a/packages/base/src/formbuilder/objectform/source/geojsonsource.ts b/packages/base/src/formbuilder/objectform/source/geojsonsource.ts index 32f863edf..623b38aa2 100644 --- a/packages/base/src/formbuilder/objectform/source/geojsonsource.ts +++ b/packages/base/src/formbuilder/objectform/source/geojsonsource.ts @@ -40,7 +40,7 @@ export class GeoJSONSourcePropertiesForm extends PathBasedSourcePropertiesForm { const extraErrors: IDict = this.state.extraErrors; let error = ''; - let valid = false; + let valid = true; if (path) { try { const geoJSONData = await loadFile({ @@ -55,28 +55,6 @@ export class GeoJSONSourcePropertiesForm extends PathBasedSourcePropertiesForm { } catch (e) { error = `"${path}" is not a valid GeoJSON file: ${e}`; } - } else { - //error = 'Path is required'; - const layers = this.props.model.getLayers(); - const layersArray = Object.values(layers); - valid = true; - - layersArray.forEach(layer => { - if (layer.type === 'VectorLayer') { - const source = this.props.model.getSource(layer.parameters?.source); - if (source?.type === 'GeoJSONSource' && source.parameters?.data) { - error = - 'There is already a Vector Layer with a geoGSON source.' - - this.props.model.isDrawVectorLayerEnabled = false; - return; - } else { - console.log('No vector layer with geoJSON source with embedded date') - this.props.model.isDrawVectorLayerEnabled = true; - this.props.model.createEmptyVectorLayerWithGeoJSONSource(); - } - } - }) } if (!valid) { diff --git a/packages/base/src/formbuilder/objectform/source/pathbasedsource.ts b/packages/base/src/formbuilder/objectform/source/pathbasedsource.ts index 5a9203c35..0da707d2a 100644 --- a/packages/base/src/formbuilder/objectform/source/pathbasedsource.ts +++ b/packages/base/src/formbuilder/objectform/source/pathbasedsource.ts @@ -66,6 +66,12 @@ export class PathBasedSourcePropertiesForm extends SourcePropertiesForm { showErrorMessage('Invalid file', this.state.extraErrors.path.__errors[0]); return; } + if (!e.formData.path) { + e.formData.data = { + type: 'FeatureCollection', + features: [] + }; + } super.onFormSubmit(e); } diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 42e17ed87..a6e2716c3 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -90,7 +90,7 @@ import { MainViewModel } from './mainviewmodel'; import BaseLayer from 'ol/layer/Base'; import Draw from 'ol/interaction/Draw.js'; //import Modify from 'ol/interaction/Modify.js'; -import Snap from 'ol/interaction/Snap.js'; +//import Snap from 'ol/interaction/Snap.js'; import { Type } from 'ol/geom/Geometry'; const drawGeometries = ['Point', 'LineString', 'Polygon', 'Circle']; @@ -2043,93 +2043,79 @@ export class MainView extends React.Component { this._removeCurrentDrawInteraction(); } const localState = this._model?.sharedModel.awareness.getLocalState(); - const layers = this._model.getLayers(); - const layersArray = Object.values(layers); const localStateSelectedLayers = localState?.selected?.value; - /** case with no vector layer and no selected one */ - if (!localStateSelectedLayers) { - layersArray.forEach(layer => { - if (layer.type === 'VectorLayer') { - const source = this._model.getSource(layer.parameters?.source); - if (source?.type === 'GeoJSONSource' && source.parameters?.data) { - console.warn( - 'There is already a Vector Layer with a geoGSON source.' - ); - this._model.isDrawVectorLayerEnabled = false; - return; - } - } - }); - this._model.createEmptyVectorLayerWithGeoJSONSource(); - } else { - /** case with selected vector layer: add new feature to the already existing geoJSONSource - */ - const localStateSelectedLayerID = Object.keys( - localStateSelectedLayers - )[0]; - const jGISLayer = this._model.getLayer(localStateSelectedLayerID); - const localStateSourceID = jGISLayer?.parameters?.source; - let jGISLayerSource = this._model.getSource(localStateSourceID); - - const layerSource: VectorSource | undefined = this._Map - .getLayers() - .getArray() - .find( - (layer: BaseLayer) => layer.get('id') === localStateSelectedLayerID - ) - ?.get('source'); - - //const modify = new Modify({ source: layerSource }); - //this._Map.addInteraction(modify); - const draw = new Draw({ - source: layerSource, - style: { - 'fill-color': 'rgba(255, 255, 255, 0.2)', - 'stroke-color': '#ffcc33', - 'stroke-width': 2, - 'circle-radius': 7, - 'circle-fill-color': '#ffcc33' - }, - type: drawGeometryType as Type // Type being a geometry type here - }); - const snap = new Snap({ source: layerSource }); - - this._Map.addInteraction(draw); - this._Map.addInteraction(snap); - this._currentDrawInteraction = draw; - this.setState(old => ({ - ...old, - drawGeometryType - })); - - const geojsonWriter = new GeoJSON({ - featureProjection: this._Map.getView().getProjection() - }); + /** add new feature to the already existing geoJSONSource + */ + const localStateSelectedLayerID = Object.keys( + localStateSelectedLayers + )[0]; + const jGISLayer = this._model.getLayer(localStateSelectedLayerID); + const localStateSourceID = jGISLayer?.parameters?.source; + let jGISLayerSource = this._model.getSource(localStateSourceID); + + const layerSource: VectorSource | undefined = this._Map + .getLayers() + .getArray() + .find( + (layer: BaseLayer) => layer.get('id') === localStateSelectedLayerID + ) + ?.get('source'); + + //const modify = new Modify({ source: layerSource }); + //this._Map.addInteraction(modify); + const draw = new Draw({ + source: layerSource, + style: { + 'fill-color': 'rgba(255, 255, 255, 0.2)', + 'stroke-color': '#ffcc33', + 'stroke-width': 2, + 'circle-radius': 7, + 'circle-fill-color': '#ffcc33' + }, + type: drawGeometryType as Type // Type being a geometry type here, + }); + //const snap = new Snap({ source: layerSource }); - layerSource?.on('change', () => { - if (jGISLayerSource) { - const updatedData = { - type: 'FeatureCollection', - features: layerSource - ?.getFeatures() - .map(feature => geojsonWriter.writeFeatureObject(feature)) - }; - const updatedJGISLayerSource: IJGISSource = { - name: jGISLayerSource.name, - type: jGISLayerSource.type, - parameters: { - data: updatedData - } - }; - jGISLayerSource = updatedJGISLayerSource; - this._model.sharedModel.updateSource( - localStateSourceID, - updatedJGISLayerSource - ); - } - }); - } + this._Map.addInteraction(draw); + //this._Map.addInteraction(snap); + this._currentDrawInteraction = draw; + this.setState(old => ({ + ...old, + drawGeometryType + })); + + const geojsonWriter = new GeoJSON({ + featureProjection: this._Map.getView().getProjection() + }); + + layerSource?.on('change', () => { + if (jGISLayerSource) { + const features = layerSource?.getFeatures().map(feature => { + const geometry = feature.getGeometry(); + console.log(geometry?.getProperties()); + geojsonWriter.writeFeatureObject(feature); + console.log(feature.getGeometry()); + }); + + const updatedData = { + type: 'FeatureCollection', + features: features + }; + const updatedJGISLayerSource: IJGISSource = { + name: jGISLayerSource.name, + type: jGISLayerSource.type, + parameters: { + data: updatedData + } + }; + jGISLayerSource = updatedJGISLayerSource; + this._model.sharedModel.updateSource( + localStateSourceID, + updatedJGISLayerSource + ); + } + }); } else { return; } diff --git a/packages/base/src/panelview/leftpanel.tsx b/packages/base/src/panelview/leftpanel.tsx index a479caece..81b35e019 100644 --- a/packages/base/src/panelview/leftpanel.tsx +++ b/packages/base/src/panelview/leftpanel.tsx @@ -208,7 +208,6 @@ export class LeftPanelWidget extends SidePanel { // Notify commands that need updating this._commands.notifyCommandChanged(CommandIDs.identify); this._commands.notifyCommandChanged(CommandIDs.temporalController); - this._commands.notifyCommandChanged(CommandIDs.newDrawVectorLayer); } private _handleFileChange: () => void; diff --git a/packages/base/src/toolbar/widget.tsx b/packages/base/src/toolbar/widget.tsx index 10fb2f04c..7c03cfa17 100644 --- a/packages/base/src/toolbar/widget.tsx +++ b/packages/base/src/toolbar/widget.tsx @@ -117,14 +117,6 @@ export class ToolbarWidget extends ReactiveToolbar { this.addItem('Geolocation', geolocationButton); geolocationButton.node.dataset.testid = 'geolocation-button'; - const drawVectoreLayerButton = new CommandToolbarButton({ - id: CommandIDs.newDrawVectorLayer, - commands: options.commands, - label: '' - }); - this.addItem('DrawVector', drawVectoreLayerButton); - drawVectoreLayerButton.node.dataset.testid = 'draw-vector-layer-button'; - const identifyButton = new CommandToolbarButton({ id: CommandIDs.identify, label: '', diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index a6e7c83d6..161f3f9bf 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -237,7 +237,6 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { disposed: ISignal; isDrawVectorLayerEnabled: boolean; updateIsDrawVectorLayerEnabled(): void; - createEmptyVectorLayerWithGeoJSONSource(): void; } export interface IUserData { diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index fbd794346..52b463428 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -3,7 +3,7 @@ import { IChangedArgs } from '@jupyterlab/coreutils'; import { DocumentRegistry } from '@jupyterlab/docregistry'; import { Contents } from '@jupyterlab/services'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; -import { PartialJSONObject, UUID } from '@lumino/coreutils'; +import { PartialJSONObject } from '@lumino/coreutils'; import { ISignal, Signal } from '@lumino/signaling'; import Ajv from 'ajv'; @@ -757,31 +757,6 @@ export class JupyterGISModel implements IJupyterGISModel { return this._geolocationChanged; } - createEmptyVectorLayerWithGeoJSONSource = () => { - const emptySourceID = UUID.uuid4(); - const emptyLayerID = UUID.uuid4(); - const emptyLayer: IJGISLayer = { - name: 'Editable GeoJSON Layer', - type: 'VectorLayer', - visible: true, - parameters: { - source: emptySourceID - } - }; - const emptySource: IJGISSource = { - name: 'Editable GeoJSON Layer Source', - type: 'GeoJSONSource', - parameters: { - data: { - type: 'FeatureCollection', - features: [] - } - } - }; - this.sharedModel.addSource(emptySourceID, emptySource); - this.addLayer(emptyLayerID, emptyLayer); - }; - readonly defaultKernelName: string = ''; readonly defaultKernelLanguage: string = ''; readonly annotationModel?: IAnnotationModel; From 0fb6a4d4af5ab8bf360c7f1bc18f8df4ed878491 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Fri, 6 Jun 2025 12:01:34 +0200 Subject: [PATCH 18/30] Provide a onFormSubmit method to the GeoJSONSourcePropertiesForm and remove the condition for no path case that was wrongly in the onFormSubmit method of the parent class PathBasedSourceProperties. Update _handleDrawGeometryTypeChange in MainView class. --- packages/base/src/commands.ts | 7 +-- .../objectform/source/geojsonsource.ts | 16 ++++++- .../objectform/source/pathbasedsource.ts | 6 --- packages/base/src/icons.ts | 4 +- packages/base/src/mainview/mainView.tsx | 45 +++++++++---------- python/jupytergis_lab/src/index.ts | 4 +- 6 files changed, 44 insertions(+), 38 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 2ff92fef7..4ee1a93ca 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -19,11 +19,13 @@ import { CommandRegistry } from '@lumino/commands'; import { ReadonlyPartialJSONObject } from '@lumino/coreutils'; import { Coordinate } from 'ol/coordinate'; import { fromLonLat } from 'ol/proj'; + import { CommandIDs, icons } from './constants'; import { ProcessingFormDialog } from './dialogs/ProcessingFormDialog'; import { LayerBrowserWidget } from './dialogs/layerBrowserDialog'; import { LayerCreationFormDialog } from './dialogs/layerCreationFormDialog'; import { SymbologyWidget } from './dialogs/symbology/symbologyDialog'; +import { pencilSolidIcon, targetWithCenterIcon } from './icons'; import keybindings from './keybindings.json'; import { getSingleSelectedLayer, @@ -33,7 +35,6 @@ import { import { getGeoJSONDataFromLayerSource, downloadFile } from './tools'; import { JupyterGISTracker } from './types'; import { JupyterGISDocumentWidget } from './widget'; -import { pencilSolidIcon, targetWithCenterIcon } from './icons'; interface ICreateEntry { tracker: JupyterGISTracker; @@ -948,7 +949,7 @@ export function addCommands( return false; } else { const selectedSource = model.getSource( - selectedLayer.parameters?.source + selectedLayer.parameters?.source, ); if ( selectedSource?.type === 'GeoJSONSource' && @@ -975,7 +976,7 @@ export function addCommands( commands.notifyCommandChanged(CommandIDs.addNewDrawFeatures); } }, - icon: pencilSolidIcon + icon: pencilSolidIcon, }); loadKeybindings(commands, keybindings); diff --git a/packages/base/src/formbuilder/objectform/source/geojsonsource.ts b/packages/base/src/formbuilder/objectform/source/geojsonsource.ts index 623b38aa2..a1d210cd3 100644 --- a/packages/base/src/formbuilder/objectform/source/geojsonsource.ts +++ b/packages/base/src/formbuilder/objectform/source/geojsonsource.ts @@ -1,5 +1,7 @@ import { IDict } from '@jupytergis/schema'; import * as geojson from '@jupytergis/schema/src/schema/geojson.json'; +import { showErrorMessage } from '@jupyterlab/apputils'; +import { ISubmitEvent } from '@rjsf/core'; import { Ajv, ValidateFunction } from 'ajv'; import { loadFile } from '@/src/tools'; @@ -27,7 +29,6 @@ export class GeoJSONSourcePropertiesForm extends PathBasedSourcePropertiesForm { if (data?.path !== '') { this.removeFormEntry('data', data, schema, uiSchema); } - super.processSchema(data, schema, uiSchema); } @@ -77,4 +78,17 @@ export class GeoJSONSourcePropertiesForm extends PathBasedSourcePropertiesForm { this.props.formErrorSignal.emit(!valid); } } + protected onFormSubmit(e: ISubmitEvent) { + if (this.state.extraErrors?.path?.__errors?.length >= 1) { + showErrorMessage('Invalid file', this.state.extraErrors.path.__errors[0]); + return; + } + if (!e.formData.path) { + e.formData.data = { + type: 'FeatureCollection', + features: [], + }; + } + super.onFormSubmit(e); + } } diff --git a/packages/base/src/formbuilder/objectform/source/pathbasedsource.ts b/packages/base/src/formbuilder/objectform/source/pathbasedsource.ts index 0da707d2a..5a9203c35 100644 --- a/packages/base/src/formbuilder/objectform/source/pathbasedsource.ts +++ b/packages/base/src/formbuilder/objectform/source/pathbasedsource.ts @@ -66,12 +66,6 @@ export class PathBasedSourcePropertiesForm extends SourcePropertiesForm { showErrorMessage('Invalid file', this.state.extraErrors.path.__errors[0]); return; } - if (!e.formData.path) { - e.formData.data = { - type: 'FeatureCollection', - features: [] - }; - } super.onFormSubmit(e); } diff --git a/packages/base/src/icons.ts b/packages/base/src/icons.ts index cb52238af..f7d358d85 100644 --- a/packages/base/src/icons.ts +++ b/packages/base/src/icons.ts @@ -18,13 +18,13 @@ import logoMiniAlternativeSvgStr from '../style/icons/logo_mini_alternative.svg' import logoMiniQGZ from '../style/icons/logo_mini_qgz.svg'; import moundSvgStr from '../style/icons/mound.svg'; import nonVisibilitySvgStr from '../style/icons/nonvisibility.svg'; +import pencilSolidSvgStr from '../style/icons/pencil_solid.svg'; import rasterSvgStr from '../style/icons/raster.svg'; import targetWithCenterSvgStr from '../style/icons/target_with_center.svg'; import targetWithoutCenterSvgStr from '../style/icons/target_without_center.svg'; import terminalToolbarSvgStr from '../style/icons/terminal_toolbar.svg'; import vectorSquareSvgStr from '../style/icons/vector_square.svg'; import visibilitySvgStr from '../style/icons/visibility.svg'; -import pencilSolidSvgStr from '../style/icons/pencil_solid.svg'; export const logoIcon = new LabIcon({ name: 'jupytergis::logo', @@ -113,5 +113,5 @@ export const targetWithCenterIcon = new LabIcon({ export const pencilSolidIcon = new LabIcon({ name: 'jupytergis::pencilSolid', - svgstr: pencilSolidSvgStr + svgstr: pencilSolidSvgStr, }); diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index a6e2716c3..12aae040f 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -42,7 +42,9 @@ import { Coordinate } from 'ol/coordinate'; import { singleClick } from 'ol/events/condition'; import { GeoJSON, MVT } from 'ol/format'; import { Geometry, Point } from 'ol/geom'; +import { Type } from 'ol/geom/Geometry'; import { DragAndDrop, Interaction, Select } from 'ol/interaction'; +import Draw from 'ol/interaction/Draw.js'; import { Heatmap as HeatmapLayer, Image as ImageLayer, @@ -51,6 +53,7 @@ import { VectorTile as VectorTileLayer, WebGLTile as WebGlTileLayer, } from 'ol/layer'; +import BaseLayer from 'ol/layer/Base'; import TileLayer from 'ol/layer/Tile'; import { fromLonLat, @@ -87,13 +90,10 @@ import CollaboratorPointers, { ClientPointer } from './CollaboratorPointers'; import { FollowIndicator } from './FollowIndicator'; import TemporalSlider from './TemporalSlider'; import { MainViewModel } from './mainviewmodel'; -import BaseLayer from 'ol/layer/Base'; -import Draw from 'ol/interaction/Draw.js'; //import Modify from 'ol/interaction/Modify.js'; //import Snap from 'ol/interaction/Snap.js'; -import { Type } from 'ol/geom/Geometry'; -const drawGeometries = ['Point', 'LineString', 'Polygon', 'Circle']; +const drawGeometries = ['Point', 'LineString', 'Polygon']; interface IProps { viewModel: MainViewModel; @@ -152,7 +152,7 @@ export class MainView extends React.Component { ); this._model.drawVectorLayerChanged.connect( this._updateIsDrawVectorLayerEnabled, - this + this, ); this._model.flyToGeometrySignal.connect(this.flyToGeometry, this); @@ -182,7 +182,7 @@ export class MainView extends React.Component { displayTemporalController: false, filterStates: {}, isDrawVectorLayerEnabled: false, - drawGeometryType: '' + drawGeometryType: '', }; this._sources = []; @@ -2034,7 +2034,7 @@ export class MainView extends React.Component { }; private _handleDrawGeometryTypeChange = ( - event: React.ChangeEvent + event: React.ChangeEvent, ) => { const drawGeometryType = event.target.value; //let layerSource: IJGISSource | undefined; @@ -2048,7 +2048,7 @@ export class MainView extends React.Component { /** add new feature to the already existing geoJSONSource */ const localStateSelectedLayerID = Object.keys( - localStateSelectedLayers + localStateSelectedLayers, )[0]; const jGISLayer = this._model.getLayer(localStateSelectedLayerID); const localStateSourceID = jGISLayer?.parameters?.source; @@ -2058,7 +2058,7 @@ export class MainView extends React.Component { .getLayers() .getArray() .find( - (layer: BaseLayer) => layer.get('id') === localStateSelectedLayerID + (layer: BaseLayer) => layer.get('id') === localStateSelectedLayerID, ) ?.get('source'); @@ -2071,9 +2071,9 @@ export class MainView extends React.Component { 'stroke-color': '#ffcc33', 'stroke-width': 2, 'circle-radius': 7, - 'circle-fill-color': '#ffcc33' + 'circle-fill-color': '#ffcc33', }, - type: drawGeometryType as Type // Type being a geometry type here, + type: drawGeometryType as Type, // Type being a geometry type here, }); //const snap = new Snap({ source: layerSource }); @@ -2082,37 +2082,34 @@ export class MainView extends React.Component { this._currentDrawInteraction = draw; this.setState(old => ({ ...old, - drawGeometryType + drawGeometryType, })); const geojsonWriter = new GeoJSON({ - featureProjection: this._Map.getView().getProjection() + featureProjection: this._Map.getView().getProjection(), }); layerSource?.on('change', () => { if (jGISLayerSource) { - const features = layerSource?.getFeatures().map(feature => { - const geometry = feature.getGeometry(); - console.log(geometry?.getProperties()); - geojsonWriter.writeFeatureObject(feature); - console.log(feature.getGeometry()); - }); + const features = layerSource + ?.getFeatures() + .map(feature => geojsonWriter.writeFeatureObject(feature)); const updatedData = { type: 'FeatureCollection', - features: features + features: features, }; const updatedJGISLayerSource: IJGISSource = { name: jGISLayerSource.name, type: jGISLayerSource.type, parameters: { - data: updatedData - } + data: updatedData, + }, }; jGISLayerSource = updatedJGISLayerSource; this._model.sharedModel.updateSource( localStateSourceID, - updatedJGISLayerSource + updatedJGISLayerSource, ); } }); @@ -2167,7 +2164,7 @@ export class MainView extends React.Component { borderRadius: '4px', fontSize: '12px', gap: '8px', - zIndex: '9999' + zIndex: '9999', }} >
diff --git a/python/jupytergis_lab/src/index.ts b/python/jupytergis_lab/src/index.ts index 8cd9af40b..eed948700 100644 --- a/python/jupytergis_lab/src/index.ts +++ b/python/jupytergis_lab/src/index.ts @@ -128,13 +128,13 @@ const plugin: JupyterFrontEndPlugin = { app.contextMenu.addItem({ command: CommandIDs.zoomToLayer, selector: '.jp-gis-layerItem', - rank: 2 + rank: 2, }); app.contextMenu.addItem({ command: CommandIDs.addNewDrawFeatures, selector: '.jp-gis-layerItem', - rank: 2 + rank: 2, }); // Create the Download submenu From c3e28e0854b79144fd026a5f3f128de0a371a451 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Fri, 6 Jun 2025 17:55:12 +0200 Subject: [PATCH 19/30] Try to modify the path description in the processSchema method of GeoJSONSourcePropertiesForm. --- .../base/src/formbuilder/objectform/source/geojsonsource.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/base/src/formbuilder/objectform/source/geojsonsource.ts b/packages/base/src/formbuilder/objectform/source/geojsonsource.ts index a1d210cd3..c3b42f079 100644 --- a/packages/base/src/formbuilder/objectform/source/geojsonsource.ts +++ b/packages/base/src/formbuilder/objectform/source/geojsonsource.ts @@ -29,7 +29,10 @@ export class GeoJSONSourcePropertiesForm extends PathBasedSourcePropertiesForm { if (data?.path !== '') { this.removeFormEntry('data', data, schema, uiSchema); } - super.processSchema(data, schema, uiSchema); + + (schema.properties.path.description = + 'The local path to a GeoJSON file. (If no path/url is provided, an empty GeoJSON is created.)'), + super.processSchema(data, schema, uiSchema); } /** From 0cfe6449522228689a448485b2cc5b83979e4eaf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 09:30:55 +0000 Subject: [PATCH 20/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- examples/editable.jGIS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/editable.jGIS b/examples/editable.jGIS index e8061ba0b..f7d922ca4 100644 --- a/examples/editable.jGIS +++ b/examples/editable.jGIS @@ -99,4 +99,4 @@ "type": "RasterSource" } } -} \ No newline at end of file +} From 8a1cb383c22ac68c759e3f3cad32a538e3957290 Mon Sep 17 00:00:00 2001 From: Florence Haudin <99649086+HaudinFlorence@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:25:38 +0200 Subject: [PATCH 21/30] Apply suggestions from code review Co-authored-by: martinRenou --- packages/base/src/commands.ts | 2 +- packages/base/src/constants.ts | 12 +----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 4ee1a93ca..7a60a349d 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -939,7 +939,7 @@ export function addCommands( }); commands.addCommand(CommandIDs.addNewDrawFeatures, { - label: trans.__('Add New Draw Features'), + label: trans.__('Edit Features'), isEnabled: () => { if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { const model = tracker.currentWidget?.content.currentViewModel diff --git a/packages/base/src/constants.ts b/packages/base/src/constants.ts index d3a489574..4808858b9 100644 --- a/packages/base/src/constants.ts +++ b/packages/base/src/constants.ts @@ -43,16 +43,6 @@ export namespace CommandIDs { export const centroids = 'jupytergis:centroids'; export const boundingBoxes = 'jupytergis:boundingBoxes'; - // Layers only commands - export const newRasterLayer = 'jupytergis:newRasterLayer'; - export const newVectorLayer = 'jupytergis:newVectorLayer'; - export const newHillshadeLayer = 'jupytergis:newHillshadeLayer'; - export const newImageLayer = 'jupytergis:newImageLayer'; - export const newVideoLayer = 'jupytergis:newVideoLayer'; - export const newShapefileLayer = 'jupytergis:newShapefileLayer'; - export const newWebGlTileLayer = 'jupytergis:newWebGlTileLayer'; - export const newHeatmapLayer = 'jupytergis:newHeatmapLayer'; - // Layer and group actions export const renameLayer = 'jupytergis:renameLayer'; export const removeLayer = 'jupytergis:removeLayer'; @@ -66,7 +56,7 @@ export namespace CommandIDs { export const removeSource = 'jupytergis:removeSource'; // Add draw features to a geoGSON source - export const addNewDrawFeatures = 'jupytergis:addNewDrawFeatures'; + export const toggleDrawFeatures = 'jupytergis:toggleDrawFeatures'; // Console commands export const toggleConsole = 'jupytergis:toggleConsole'; From 2107d46a0aa458d56805434c8f2d34454c4a286b Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Tue, 10 Jun 2025 14:53:05 +0200 Subject: [PATCH 22/30] Take review comments into account. --- packages/base/src/commands.ts | 4 ++-- .../src/formbuilder/objectform/source/geojsonsource.ts | 9 +++++---- python/jupytergis_lab/src/index.ts | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 7a60a349d..427e7a1e1 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -938,7 +938,7 @@ export function addCommands( icon: targetWithCenterIcon, }); - commands.addCommand(CommandIDs.addNewDrawFeatures, { + commands.addCommand(CommandIDs.toggleDrawFeatures, { label: trans.__('Edit Features'), isEnabled: () => { if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { @@ -973,7 +973,7 @@ export function addCommands( model.isDrawVectorLayerEnabled = true; } model.updateIsDrawVectorLayerEnabled(); - commands.notifyCommandChanged(CommandIDs.addNewDrawFeatures); + commands.notifyCommandChanged(CommandIDs.toggleDrawFeatures); } }, icon: pencilSolidIcon, diff --git a/packages/base/src/formbuilder/objectform/source/geojsonsource.ts b/packages/base/src/formbuilder/objectform/source/geojsonsource.ts index c3b42f079..988bdfe80 100644 --- a/packages/base/src/formbuilder/objectform/source/geojsonsource.ts +++ b/packages/base/src/formbuilder/objectform/source/geojsonsource.ts @@ -29,10 +29,11 @@ export class GeoJSONSourcePropertiesForm extends PathBasedSourcePropertiesForm { if (data?.path !== '') { this.removeFormEntry('data', data, schema, uiSchema); } - - (schema.properties.path.description = - 'The local path to a GeoJSON file. (If no path/url is provided, an empty GeoJSON is created.)'), - super.processSchema(data, schema, uiSchema); + if (this.props.formContext === 'create') { + (schema.properties.path.description = + 'The local path to a GeoJSON file. (If no path/url is provided, an empty GeoJSON is created.)'), + super.processSchema(data, schema, uiSchema); + } } /** diff --git a/python/jupytergis_lab/src/index.ts b/python/jupytergis_lab/src/index.ts index eed948700..e4f32043d 100644 --- a/python/jupytergis_lab/src/index.ts +++ b/python/jupytergis_lab/src/index.ts @@ -132,7 +132,7 @@ const plugin: JupyterFrontEndPlugin = { }); app.contextMenu.addItem({ - command: CommandIDs.addNewDrawFeatures, + command: CommandIDs.toggleDrawFeatures, selector: '.jp-gis-layerItem', rank: 2, }); From ddaa0888d0bff156ce80e32ba6ad78feb36883eb Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Wed, 11 Jun 2025 19:37:27 +0200 Subject: [PATCH 23/30] Restore toggle toolbar button for the toggleDrawFeatures command. In leftPanel,use notifyCommandChanged for all commands. Fix bug regarding the overlay display when a draw vector layer is removed. Enable the toggleDrawFeature command only when the layer is a draw vector layer one. --- packages/base/src/commands.ts | 47 ++++++++++++++++------- packages/base/src/mainview/mainView.tsx | 34 +++++++++++++--- packages/base/src/panelview/leftpanel.tsx | 9 +++-- packages/base/src/toolbar/widget.tsx | 9 +++++ packages/schema/src/interfaces.ts | 1 + packages/schema/src/model.ts | 12 ++++++ 6 files changed, 90 insertions(+), 22 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 427e7a1e1..b090af11d 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -940,25 +940,36 @@ export function addCommands( commands.addCommand(CommandIDs.toggleDrawFeatures, { label: trans.__('Edit Features'), + isToggled: () => { + if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { + const model = tracker.currentWidget?.content.currentViewModel + .jGISModel as IJupyterGISModel; + const selectedLayer = getSingleSelectedLayer(tracker); + if (!selectedLayer) { + return false; + } else if (model.checkIfIsADrawVectorLayer(selectedLayer) === true) { + return model.isDrawVectorLayerEnabled; + } else { + model.isDrawVectorLayerEnabled === false; + return false; + } + } else { + return false; + } + }, isEnabled: () => { if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { const model = tracker.currentWidget?.content.currentViewModel .jGISModel as IJupyterGISModel; const selectedLayer = getSingleSelectedLayer(tracker); + if (!selectedLayer) { return false; + } + if (model.checkIfIsADrawVectorLayer(selectedLayer) === true) { + return true; } else { - const selectedSource = model.getSource( - selectedLayer.parameters?.source, - ); - if ( - selectedSource?.type === 'GeoJSONSource' && - selectedSource?.parameters?.data - ) { - return true; - } else { - return false; - } + return false; } } else { return false; @@ -966,16 +977,24 @@ export function addCommands( }, execute: async () => { if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { + const selectedLayer = getSingleSelectedLayer(tracker); const model = tracker.currentWidget?.content.currentViewModel .jGISModel as IJupyterGISModel; - - if (model.isDrawVectorLayerEnabled === false) { - model.isDrawVectorLayerEnabled = true; + if (!selectedLayer) { + return false; + } else { + if (model.isDrawVectorLayerEnabled === false) { + model.isDrawVectorLayerEnabled = true; + } else { + model.isDrawVectorLayerEnabled = false; + } } + model.updateIsDrawVectorLayerEnabled(); commands.notifyCommandChanged(CommandIDs.toggleDrawFeatures); } }, + icon: pencilSolidIcon, }); diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 12aae040f..80c68a39a 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -45,6 +45,8 @@ import { Geometry, Point } from 'ol/geom'; import { Type } from 'ol/geom/Geometry'; import { DragAndDrop, Interaction, Select } from 'ol/interaction'; import Draw from 'ol/interaction/Draw.js'; +import Modify from 'ol/interaction/Modify.js'; +import Snap from 'ol/interaction/Snap.js'; import { Heatmap as HeatmapLayer, Image as ImageLayer, @@ -1460,6 +1462,20 @@ export class MainView extends React.Component { return; } + /* check if the currently selected layer is a drawVector layer + and update isDrawVectorLayer to remove the display of the geometry selection overlay if required*/ + const selectedLayers = localState?.selected?.value; + if (selectedLayers) { + const selectedLayerID = Object.keys(selectedLayers)[0]; + const JGISLayer = this._model.getLayer(selectedLayerID); + if (JGISLayer) { + if (this._model.checkIfIsADrawVectorLayer(JGISLayer) === false) { + this._model.isDrawVectorLayerEnabled = false; + this._updateIsDrawVectorLayerEnabled(); + } + } + } + const remoteUser = localState.remoteUser; // If we are in following mode, we update our position and selection if (remoteUser) { @@ -1704,6 +1720,15 @@ export class MainView extends React.Component { if (!newLayer || Object.keys(newLayer).length === 0) { this.removeLayer(id); + if ( + this._model.checkIfIsADrawVectorLayer(oldLayer as IJGISLayer) === true + ) { + this._model.isDrawVectorLayerEnabled = false; + this._updateIsDrawVectorLayerEnabled(); + this._mainViewModel.commands.notifyCommandChanged( + CommandIDs.toggleDrawFeatures, + ); + } return; } @@ -2037,7 +2062,6 @@ export class MainView extends React.Component { event: React.ChangeEvent, ) => { const drawGeometryType = event.target.value; - //let layerSource: IJGISSource | undefined; if (this._model.isDrawVectorLayerEnabled) { if (this._currentDrawInteraction) { this._removeCurrentDrawInteraction(); @@ -2062,8 +2086,8 @@ export class MainView extends React.Component { ) ?.get('source'); - //const modify = new Modify({ source: layerSource }); - //this._Map.addInteraction(modify); + const modify = new Modify({ source: layerSource }); + this._Map.addInteraction(modify); const draw = new Draw({ source: layerSource, style: { @@ -2075,10 +2099,10 @@ export class MainView extends React.Component { }, type: drawGeometryType as Type, // Type being a geometry type here, }); - //const snap = new Snap({ source: layerSource }); + const snap = new Snap({ source: layerSource }); this._Map.addInteraction(draw); - //this._Map.addInteraction(snap); + this._Map.addInteraction(snap); this._currentDrawInteraction = draw; this.setState(old => ({ ...old, diff --git a/packages/base/src/panelview/leftpanel.tsx b/packages/base/src/panelview/leftpanel.tsx index 81b35e019..4791aea87 100644 --- a/packages/base/src/panelview/leftpanel.tsx +++ b/packages/base/src/panelview/leftpanel.tsx @@ -205,9 +205,12 @@ export class LeftPanelWidget extends SidePanel { } private _notifyCommands() { - // Notify commands that need updating - this._commands.notifyCommandChanged(CommandIDs.identify); - this._commands.notifyCommandChanged(CommandIDs.temporalController); + // Notify updating + Object.values(CommandIDs).forEach(id => { + if (this._commands.hasCommand(id)) { + this._commands.notifyCommandChanged(id); + } + }); } private _handleFileChange: () => void; diff --git a/packages/base/src/toolbar/widget.tsx b/packages/base/src/toolbar/widget.tsx index 7c03cfa17..8529e0bb5 100644 --- a/packages/base/src/toolbar/widget.tsx +++ b/packages/base/src/toolbar/widget.tsx @@ -107,6 +107,15 @@ export class ToolbarWidget extends ReactiveToolbar { this.addItem('New', NewEntryButton); + const toggleDrawFeaturesButton = new CommandToolbarButton({ + id: CommandIDs.toggleDrawFeatures, + commands: options.commands, + label: '', + }); + this.addItem('Toggle Draw Features', toggleDrawFeaturesButton); + toggleDrawFeaturesButton.node.dataset.testid = + 'toggle-draw-features-button'; + this.addItem('separator2', new Separator()); const geolocationButton = new CommandToolbarButton({ diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index 161f3f9bf..5cadb0410 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -236,6 +236,7 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { disposed: ISignal; isDrawVectorLayerEnabled: boolean; + checkIfIsADrawVectorLayer(JGISlayer: IJGISLayer): boolean; updateIsDrawVectorLayerEnabled(): void; } diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index 52b463428..b077ed35f 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -744,6 +744,18 @@ export class JupyterGISModel implements IJupyterGISModel { this.drawVectorLayerChanged.emit(this.isDrawVectorLayerEnabled); } + checkIfIsADrawVectorLayer(layer: IJGISLayer): boolean { + const selectedSource = this.getSource(layer.parameters?.source); + if ( + selectedSource?.type === 'GeoJSONSource' && + selectedSource?.parameters?.data + ) { + return true; + } else { + return false; + } + } + get geolocation(): JgisCoordinates { return this._geolocation; } From 43f20b9822e4b86101c2b2f848bb5527d6b55191 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Thu, 12 Jun 2025 20:38:03 +0200 Subject: [PATCH 24/30] Refactor the code. --- examples/earthquakes.jGIS | 56 +++++- packages/base/src/mainview/mainView.tsx | 219 ++++++++++++++---------- yarn.lock | 20 +++ 3 files changed, 199 insertions(+), 96 deletions(-) diff --git a/examples/earthquakes.jGIS b/examples/earthquakes.jGIS index 04c2f0e76..63fdaf781 100644 --- a/examples/earthquakes.jGIS +++ b/examples/earthquakes.jGIS @@ -1,7 +1,9 @@ { "layerTree": [ "8de7c2c0-6024-4716-b542-031a89fb87f9", - "3e21d680-406f-4099-bd9e-3a4edb9a2c8b" + "3e21d680-406f-4099-bd9e-3a4edb9a2c8b", + "5ff49984-5480-4cd4-aabd-3a43186350ba", + "f44d7a8c-2d45-484b-89e3-e59edea8f988" ], "layers": { "3e21d680-406f-4099-bd9e-3a4edb9a2c8b": { @@ -29,6 +31,15 @@ "type": "VectorLayer", "visible": true }, + "5ff49984-5480-4cd4-aabd-3a43186350ba": { + "name": "Editable GeoJSON Layer 1", + "parameters": { + "opacity": 1.0, + "source": "e5b8b9db-22b3-4439-8db3-59da882636ac" + }, + "type": "VectorLayer", + "visible": true + }, "8de7c2c0-6024-4716-b542-031a89fb87f9": { "name": "OpenStreetMap.Mapnik Layer", "parameters": { @@ -36,25 +47,44 @@ }, "type": "RasterLayer", "visible": true + }, + "f44d7a8c-2d45-484b-89e3-e59edea8f988": { + "name": "Editable GeoJSON Layer 2", + "parameters": { + "opacity": 1.0, + "source": "210844ec-68e3-49d5-afd3-bc9bd872c695" + }, + "type": "VectorLayer", + "visible": true } }, "metadata": {}, "options": { "bearing": 0.0, "extent": [ - -15808389.545988183, - -6461894.596411711, - -846988.1649800241, - 14932662.897352839 + -16434055.989266738, + -3411504.4661820857, + -2212097.8396098875, + 14828095.240483545 ], - "latitude": 35.52446437432016, - "longitude": -74.80890180273175, + "latitude": 45.54854082519543, + "longitude": -83.75062487261803, "pitch": 0.0, "projection": "EPSG:3857", "zoom": 2.6670105136699993 }, "schemaVersion": "0.5.0", "sources": { + "210844ec-68e3-49d5-afd3-bc9bd872c695": { + "name": "Editable GeoJSON Layer 2 Source", + "parameters": { + "data": { + "features": [], + "type": "FeatureCollection" + } + }, + "type": "GeoJSONSource" + }, "348d85fa-3a71-447f-8a64-e283ec47cc7c": { "name": "Custom GeoJSON Layer Source", "parameters": { @@ -73,6 +103,16 @@ "urlParameters": {} }, "type": "RasterSource" + }, + "e5b8b9db-22b3-4439-8db3-59da882636ac": { + "name": "Custom GeoJSON Layer Source", + "parameters": { + "data": { + "features": [], + "type": "FeatureCollection" + } + }, + "type": "GeoJSONSource" } } -} +} \ No newline at end of file diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 80c68a39a..9eca4bb00 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -42,11 +42,10 @@ import { Coordinate } from 'ol/coordinate'; import { singleClick } from 'ol/events/condition'; import { GeoJSON, MVT } from 'ol/format'; import { Geometry, Point } from 'ol/geom'; -import { Type } from 'ol/geom/Geometry'; import { DragAndDrop, Interaction, Select } from 'ol/interaction'; import Draw from 'ol/interaction/Draw.js'; -import Modify from 'ol/interaction/Modify.js'; -import Snap from 'ol/interaction/Snap.js'; +//import Modify from 'ol/interaction/Modify.js'; +//import Snap from 'ol/interaction/Snap.js'; import { Heatmap as HeatmapLayer, Image as ImageLayer, @@ -94,6 +93,7 @@ import TemporalSlider from './TemporalSlider'; import { MainViewModel } from './mainviewmodel'; //import Modify from 'ol/interaction/Modify.js'; //import Snap from 'ol/interaction/Snap.js'; +import { Type } from 'ol/geom/Geometry'; const drawGeometries = ['Point', 'LineString', 'Polygon']; @@ -116,7 +116,7 @@ interface IStates { displayTemporalController: boolean; filterStates: IDict; isDrawVectorLayerEnabled: boolean; - drawGeometryType: string | undefined; + drawGeometryLabel: string | undefined; } export class MainView extends React.Component { @@ -184,7 +184,7 @@ export class MainView extends React.Component { displayTemporalController: false, filterStates: {}, isDrawVectorLayerEnabled: false, - drawGeometryType: '', + drawGeometryLabel: '', }; this._sources = []; @@ -385,6 +385,43 @@ export class MainView extends React.Component { units: view.getProjection().getUnits(), }, })); + + /* Snap and Modify interactions */ + //this._snap = new Snap(); + //this._modify = new Modify({}); + //this._Map.addInteraction(this._snap); + //this._Map.addInteraction(this._modify); + + /* Draw interactions */ + this._draw = new Draw({ + style: { + 'fill-color': 'rgba(255, 255, 255, 0.2)', + 'stroke-color': '#ffcc33', + 'stroke-width': 2, + 'circle-radius': 7, + 'circle-fill-color': '#ffcc33', + }, + type: 'Point', + }); + this._Map.addInteraction(this._draw); + + const drawGeometryLabel = 'Point' + this.setState(old => ({ + ...old, + drawGeometryLabel + })); + this._draw.setActive(false); + //this._modify.setActive(false); + //this._snap.setActive(false); + } + /* Listen to the change in selected layer and get the new source */ + this._model.sharedModel.awareness.on('change', this._getDrawSourceFromSelectedLayer) + + if (this._currentDrawSourceID) { + const layerSource = this._getVectorSourceFromSourceID(this._currentDrawSourceID) + if (layerSource) { + this._updateDrawSource(layerSource) /*add new features to the JGIS Document in geoJSON format */ + } } } @@ -1337,8 +1374,8 @@ export class MainView extends React.Component { const parsedGeometry = isOlGeometry ? geometry : new GeoJSON().readGeometry(geometry, { - featureProjection: this._Map.getView().getProjection(), - }); + featureProjection: this._Map.getView().getProjection(), + }); const olFeature = new Feature({ geometry: parsedGeometry, @@ -2041,8 +2078,8 @@ export class MainView extends React.Component { const isDrawVectorLayerEnabled: boolean = this._model.isDrawVectorLayerEnabled; this.setState(old => ({ ...old, isDrawVectorLayerEnabled })); - if (isDrawVectorLayerEnabled === false && this._currentDrawInteraction) { - this._removeCurrentDrawInteraction(); + if (isDrawVectorLayerEnabled === false && this._draw) { + this._removeDrawInteraction(); } } @@ -2058,92 +2095,94 @@ export class MainView extends React.Component { // TODO SOMETHING }; + private _getGeometryType(label: string): Type { + switch (label) { + case 'Point': + return "Point"; + case 'LineString': + return "LineString"; + case 'Polygon': + return "Polygon"; + default: + return "Point"; + } + } + + private _getVectorSourceFromSourceID = (sourceID: string): VectorSource | undefined => { + /* get the OL vectorSource corresponding to the JGIS currentDrawSourceID */ + const layerSource: VectorSource | undefined = this._Map + .getLayers() + .getArray() + .find( + (layer: BaseLayer) => layer.get('id') === sourceID + ) + ?.get('source'); + return layerSource + } + private _handleDrawGeometryTypeChange = ( event: React.ChangeEvent, ) => { - const drawGeometryType = event.target.value; - if (this._model.isDrawVectorLayerEnabled) { - if (this._currentDrawInteraction) { - this._removeCurrentDrawInteraction(); - } - const localState = this._model?.sharedModel.awareness.getLocalState(); - const localStateSelectedLayers = localState?.selected?.value; - - /** add new feature to the already existing geoJSONSource - */ - const localStateSelectedLayerID = Object.keys( - localStateSelectedLayers, - )[0]; - const jGISLayer = this._model.getLayer(localStateSelectedLayerID); - const localStateSourceID = jGISLayer?.parameters?.source; - let jGISLayerSource = this._model.getSource(localStateSourceID); - - const layerSource: VectorSource | undefined = this._Map - .getLayers() - .getArray() - .find( - (layer: BaseLayer) => layer.get('id') === localStateSelectedLayerID, - ) - ?.get('source'); - - const modify = new Modify({ source: layerSource }); - this._Map.addInteraction(modify); - const draw = new Draw({ - source: layerSource, - style: { - 'fill-color': 'rgba(255, 255, 255, 0.2)', - 'stroke-color': '#ffcc33', - 'stroke-width': 2, - 'circle-radius': 7, - 'circle-fill-color': '#ffcc33', - }, - type: drawGeometryType as Type, // Type being a geometry type here, + const drawGeometryLabel = event.target.value; + const drawGeometryType = this._getGeometryType(drawGeometryLabel) + if (this._draw) { + this._Map.removeInteraction(this._draw); + + this._draw = new Draw({ + source: this._getVectorSourceFromSourceID(this._currentDrawSourceID), + type: drawGeometryType, // e.g., 'Point', 'LineString', etc. }); - const snap = new Snap({ source: layerSource }); + this._Map.addInteraction(this._draw) + } - this._Map.addInteraction(draw); - this._Map.addInteraction(snap); - this._currentDrawInteraction = draw; - this.setState(old => ({ - ...old, - drawGeometryType, - })); + this.setState(old => ({ + ...old, + drawGeometryLabel, + })); + }; - const geojsonWriter = new GeoJSON({ - featureProjection: this._Map.getView().getProjection(), - }); + _getDrawSourceFromSelectedLayer = () => { + const localStateSelectedLayers = this._model?.sharedModel.awareness.getLocalState()?.selected?.value; + const selectedLayerID = Object.keys( + localStateSelectedLayers, + )[0]; + console.log('selectedLayerID:', selectedLayerID) + const jGISLayer = this._model.getLayer(selectedLayerID); + this._currentDrawSourceID = jGISLayer?.parameters?.source; + const IJGISSource = this._model.getSource(this._currentDrawSourceID) + if (IJGISSource) { + this._currentDrawSource = IJGISSource; + } + } - layerSource?.on('change', () => { - if (jGISLayerSource) { - const features = layerSource - ?.getFeatures() - .map(feature => geojsonWriter.writeFeatureObject(feature)); + _updateDrawSource = (layerSource: VectorSource) => { + const geojsonWriter = new GeoJSON({ + featureProjection: this._Map.getView().getProjection(), + }); + const features = layerSource + ?.getFeatures() + .map(feature => geojsonWriter.writeFeatureObject(feature)); - const updatedData = { - type: 'FeatureCollection', - features: features, - }; - const updatedJGISLayerSource: IJGISSource = { - name: jGISLayerSource.name, - type: jGISLayerSource.type, - parameters: { - data: updatedData, - }, - }; - jGISLayerSource = updatedJGISLayerSource; - this._model.sharedModel.updateSource( - localStateSourceID, - updatedJGISLayerSource, - ); - } - }); - } else { - return; - } - }; + const updatedData = { + type: 'FeatureCollection', + features: features, + }; + const updatedJGISLayerSource: IJGISSource = { + name: this._currentDrawSource.name, + type: this._currentDrawSource.type, + parameters: { + data: updatedData, + }, + }; + this._currentDrawSource = updatedJGISLayerSource; + this._model.sharedModel.updateSource( + this._currentDrawSourceID, + updatedJGISLayerSource, + ); - private _removeCurrentDrawInteraction = () => { - this._Map.removeInteraction(this._currentDrawInteraction); + } + private _removeDrawInteraction = () => { + this._Map.removeInteraction(this._draw); }; render(): JSX.Element { @@ -2195,7 +2234,7 @@ export class MainView extends React.Component {