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 diff --git a/examples/editable.jGIS b/examples/editable.jGIS new file mode 100644 index 000000000..f7d922ca4 --- /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" + } + } +} diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 8b25b15ea..73d85ff1b 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -25,7 +25,7 @@ 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 { pencilSolidIcon, targetWithCenterIcon } from './icons'; import keybindings from './keybindings.json'; import { getSingleSelectedLayer, @@ -938,6 +938,66 @@ export function addCommands( icon: targetWithCenterIcon, }); + 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.editingVectorLayer; + } else { + model.editingVectorLayer === 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 { + return false; + } + } else { + return false; + } + }, + execute: async () => { + if (tracker.currentWidget instanceof JupyterGISDocumentWidget) { + const selectedLayer = getSingleSelectedLayer(tracker); + const model = tracker.currentWidget?.content.currentViewModel + .jGISModel as IJupyterGISModel; + if (!selectedLayer) { + return false; + } else { + if (model.editingVectorLayer === false) { + model.editingVectorLayer = true; + } else { + model.editingVectorLayer = false; + } + } + + model.updateEditingVectorLayer(); + commands.notifyCommandChanged(CommandIDs.toggleDrawFeatures); + } + }, + + icon: pencilSolidIcon, + }); + loadKeybindings(commands, keybindings); } diff --git a/packages/base/src/constants.ts b/packages/base/src/constants.ts index 608912a96..4808858b9 100644 --- a/packages/base/src/constants.ts +++ b/packages/base/src/constants.ts @@ -55,6 +55,9 @@ export namespace CommandIDs { export const renameSource = 'jupytergis:renameSource'; export const removeSource = 'jupytergis:removeSource'; + // Add draw features to a geoGSON source + export const toggleDrawFeatures = 'jupytergis:toggleDrawFeatures'; + // Console commands export const toggleConsole = 'jupytergis:toggleConsole'; export const invokeCompleter = 'jupytergis:invokeConsoleCompleter'; diff --git a/packages/base/src/formbuilder/objectform/source/geojsonsource.ts b/packages/base/src/formbuilder/objectform/source/geojsonsource.ts index 35ef9beca..988bdfe80 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,8 +29,11 @@ export class GeoJSONSourcePropertiesForm extends PathBasedSourcePropertiesForm { if (data?.path !== '') { this.removeFormEntry('data', data, schema, uiSchema); } - - 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); + } } /** @@ -40,7 +45,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,8 +60,6 @@ export class GeoJSONSourcePropertiesForm extends PathBasedSourcePropertiesForm { } catch (e) { error = `"${path}" is not a valid GeoJSON file: ${e}`; } - } else { - error = 'Path is required'; } if (!valid) { @@ -79,4 +82,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/icons.ts b/packages/base/src/icons.ts index 18f132d83..f7d358d85 100644 --- a/packages/base/src/icons.ts +++ b/packages/base/src/icons.ts @@ -18,6 +18,7 @@ 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'; @@ -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/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 6f56be391..e4fa61ee0 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -42,7 +42,12 @@ import { Coordinate } from 'ol/coordinate'; import { singleClick } from 'ol/events/condition'; import { GeoJSON, MVT } from 'ol/format'; import { Geometry, Point } from 'ol/geom'; -import { DragAndDrop, Select } from 'ol/interaction'; +import { Type } from 'ol/geom/Geometry'; +import DragAndDrop from 'ol/interaction/DragAndDrop'; +import Draw from 'ol/interaction/Draw'; +import Modify from 'ol/interaction/Modify'; +import Select from 'ol/interaction/Select'; +import Snap from 'ol/interaction/Snap'; import { Heatmap as HeatmapLayer, Image as ImageLayer, @@ -51,6 +56,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, @@ -71,6 +77,7 @@ import { import Static from 'ol/source/ImageStatic'; import TileSource from 'ol/source/Tile'; import { Circle, Fill, Stroke, Style } from 'ol/style'; +import CircleStyle from 'ol/style/Circle'; import { Rule } from 'ol/style/flat'; //@ts-expect-error no types for ol-pmtiles import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles'; @@ -87,6 +94,10 @@ import CollaboratorPointers, { ClientPointer } from './CollaboratorPointers'; import { FollowIndicator } from './FollowIndicator'; import TemporalSlider from './TemporalSlider'; import { MainViewModel } from './mainviewmodel'; +//import Modify from 'ol/interaction/Modify.js'; +//import Snap from 'ol/interaction/Snap.js'; + +const DRAW_GEOMETRIES = ['Point', 'LineString', 'Polygon'] as const; interface IProps { viewModel: MainViewModel; @@ -106,8 +117,26 @@ interface IStates { loadingErrors: Array<{ id: string; error: any; index: number }>; displayTemporalController: boolean; filterStates: IDict; + editingVectorLayer: boolean; + drawGeometryLabel: string | undefined; } +const drawInteractionStyle = new Style({ + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.2)', + }), + stroke: new Stroke({ + color: '#ffcc33', + width: 2, + }), + image: new CircleStyle({ + radius: 7, + fill: new Fill({ + color: '#ffcc33', + }), + }), +}); + export class MainView extends React.Component { constructor(props: IProps) { super(props); @@ -141,6 +170,10 @@ export class MainView extends React.Component { this._handleGeolocationChanged, this, ); + this._model.editingVectorLayerChanged.connect( + this._updateEditingVectorLayer, + this, + ); this._model.flyToGeometrySignal.connect(this.flyToGeometry, this); this._model.highlightFeatureSignal.connect( @@ -168,6 +201,8 @@ export class MainView extends React.Component { loadingErrors: [], displayTemporalController: false, filterStates: {}, + editingVectorLayer: false, + drawGeometryLabel: '', }; this._sources = []; @@ -213,6 +248,10 @@ export class MainView extends React.Component { this, ); + this._model.sharedModel.awareness.off( + 'change', + this._onSelectedLayerChange, + ); this._mainViewModel.dispose(); } @@ -368,6 +407,14 @@ export class MainView extends React.Component { units: view.getProjection().getUnits(), }, })); + + /* Track changes of selected layers + Get the vector source of the selected layer + Edit the vector layer*/ + this._model.sharedModel.awareness.on( + 'change', + this._onSelectedLayerChange, + ); } } @@ -1445,6 +1492,22 @@ 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) { + return; + } + + const selectedLayerID = Object.keys(selectedLayers)[0]; + const JGISLayer = this._model.getLayer(selectedLayerID); + if (JGISLayer) { + if (this._model.checkIfIsADrawVectorLayer(JGISLayer) === false) { + this._model.editingVectorLayer = false; + this._updateEditingVectorLayer(); + } + } + const remoteUser = localState.remoteUser; // If we are in following mode, we update our position and selection if (remoteUser) { @@ -1689,6 +1752,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.editingVectorLayer = false; + this._updateEditingVectorLayer(); + this._mainViewModel.commands.notifyCommandChanged( + CommandIDs.toggleDrawFeatures, + ); + } return; } @@ -2009,6 +2081,183 @@ export class MainView extends React.Component { // TODO SOMETHING }; + private _updateEditingVectorLayer() { + const editingVectorLayer: boolean = this._model.editingVectorLayer; + this.setState(old => ({ ...old, editingVectorLayer })); + if (editingVectorLayer === false && this._draw) { + this._removeDrawInteraction(); + } + } + + private _getVectorSourceFromLayerID = ( + layerID: string, + ): VectorSource | undefined => { + /* get the OpenLayers VectorSource corresponding to the JGIS currentDrawLayerID */ + this._currentVectorSource = this._Map + .getLayers() + .getArray() + .find((layer: BaseLayer) => layer.get('id') === layerID) + ?.get('source'); + return this._currentVectorSource; + }; + + private _handleDrawGeometryTypeChange = ( + /* handle with the change of geometry and instantiate new draw interaction and other ones accordingly*/ + event: React.ChangeEvent, + ) => { + const drawGeometryLabel = event.target.value; + this._currentDrawGeometry = drawGeometryLabel as Type; + this._updateInteractions(); + this._updateDrawSource(); + this.setState(old => ({ + ...old, + drawGeometryLabel, + })); + }; + + _getDrawSourceFromSelectedLayer = () => { + const selectedLayers = + this._model?.sharedModel.awareness.getLocalState()?.selected?.value; + if (!selectedLayers) { + return; + } + const selectedLayerID = Object.keys(selectedLayers)[0]; + this._currentDrawLayerID = selectedLayerID; + const JGISLayer = this._model.getLayer(selectedLayerID); + this._currentDrawSourceID = JGISLayer?.parameters?.source; + if (this._currentDrawSourceID) { + this._currentDrawSource = this._model.getSource( + this._currentDrawSourceID, + ); + } + }; + + _onVectorSourceChange = () => { + const geojsonWriter = new GeoJSON({ + featureProjection: this._Map.getView().getProjection(), + }); + if (this._currentVectorSource) { + const features = this._currentVectorSource + ?.getFeatures() + .map(feature => geojsonWriter.writeFeatureObject(feature)); + + const updatedData = { + type: 'FeatureCollection', + features: features, + }; + + if (this._currentDrawSource) { + const updatedJGISLayerSource: IJGISSource = { + name: this._currentDrawSource.name, + type: this._currentDrawSource.type, + parameters: { + data: updatedData, + }, + }; + + this._currentDrawSource = updatedJGISLayerSource; + if (this._currentDrawSourceID) { + this._model.sharedModel.updateSource( + this._currentDrawSourceID, + updatedJGISLayerSource, + ); + } + } + } + }; + + _updateDrawSource = () => { + if (this._currentVectorSource) { + this._currentVectorSource.on('change', this._onVectorSourceChange); + } + }; + + _updateInteractions = () => { + if (this._draw) { + this._removeDrawInteraction(); + } + if (this._select) { + this._removeSelectInteraction(); + } + if (this._modify) { + this._removeModifyInteraction(); + } + if (this._snap) { + this._removeSnapInteraction(); + } + this._draw = new Draw({ + style: drawInteractionStyle, + type: this._currentDrawGeometry, + source: this._currentVectorSource, + }); + this._select = new Select(); + this._modify = new Modify({ + features: this._select.getFeatures(), + }); + this._snap = new Snap({ + source: this._currentVectorSource, + }); + this._Map.addInteraction(this._draw); + this._Map.addInteraction(this._select); + this._Map.addInteraction(this._modify); + this._Map.addInteraction(this._snap); + this._draw.setActive(true); + this._select.setActive(false); + this._modify.setActive(false); + this._snap.setActive(true); + }; + + _editVectorLayer = () => { + this._getDrawSourceFromSelectedLayer(); + if (this._currentDrawLayerID) { + this._currentVectorSource = this._getVectorSourceFromLayerID( + this._currentDrawLayerID, + ); + if (this._currentVectorSource && this._currentDrawGeometry) { + this._updateInteractions(); /* remove previous interactions and instantiate new ones */ + this._updateDrawSource(); /*add new features, update source and get changes reported to the JGIS Document in geoJSON format */ + } + } + }; + + private _removeDrawInteraction = () => { + this._draw.setActive(false); + this._Map.removeInteraction(this._draw); + }; + + private _removeSelectInteraction = () => { + this._select.setActive(false); + this._Map.removeInteraction(this._select); + }; + + private _removeSnapInteraction = () => { + this._snap.setActive(false); + this._Map.removeInteraction(this._snap); + }; + + private _removeModifyInteraction = () => { + this._modify.setActive(false); + this._Map.removeInteraction(this._modify); + }; + + private _onSelectedLayerChange = () => { + const selectedLayers = + this._model.sharedModel.awareness.getLocalState()?.selected?.value; + const selectedLayerId = selectedLayers + ? Object.keys(selectedLayers)[0] + : undefined; + if (selectedLayerId && selectedLayerId !== this._previousDrawLayerID) { + const selectedLayer = this._model.getLayer(selectedLayerId); + if (selectedLayer) { + if (this._model.checkIfIsADrawVectorLayer(selectedLayer)) { + this._previousDrawLayerID = selectedLayerId; + this._currentDrawLayerID = selectedLayerId; + this._editVectorLayer(); + } + } + } + }; + render(): JSX.Element { return ( <> @@ -2037,6 +2286,43 @@ export class MainView extends React.Component { ); })} + {this.state.editingVectorLayer && ( +
+
+ +
+
+ )} +
{this.state.displayTemporalController && ( { private _loadingLayers: Set; private _originalFeatures: IDict[]> = {}; private _highlightLayer: VectorLayer; + private _draw: Draw; + private _snap: Snap; + private _modify: Modify; + private _select: Select; + private _currentDrawLayerID: string | undefined; + private _previousDrawLayerID: string | undefined; + private _currentDrawSource: IJGISSource | undefined; + private _currentVectorSource: VectorSource | undefined; + private _currentDrawSourceID: string | undefined; + private _currentDrawGeometry: Type; } 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/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/base/style/icons/pencil_solid.svg b/packages/base/style/icons/pencil_solid.svg new file mode 100644 index 000000000..6c83f9005 --- /dev/null +++ b/packages/base/style/icons/pencil_solid.svg @@ -0,0 +1,9 @@ + + + diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index 0f931df73..b98fdbb28 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -179,6 +179,7 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { geolocationChanged: Signal; flyToGeometrySignal: Signal; highlightFeatureSignal: Signal; + editingVectorLayerChanged: Signal; contentsManager: Contents.IManager | undefined; filePath: string; @@ -234,6 +235,9 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { triggerLayerUpdate(layerId: string, layer: IJGISLayer): void; disposed: ISignal; + editingVectorLayer: boolean; + checkIfIsADrawVectorLayer(JGISlayer: IJGISLayer): boolean; + updateEditingVectorLayer(): void; } export interface IUserData { diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index b77086578..86314ce1c 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -55,6 +55,7 @@ export class JupyterGISModel implements IJupyterGISModel { this, ); this.annotationModel = annotationModel; + this._editingVectorLayer = false; this.settingRegistry = settingRegistry; this._pathChanged = new Signal(this); } @@ -739,6 +740,22 @@ export class JupyterGISModel implements IJupyterGISModel { this.updateLayerSignal.emit(JSON.stringify({ layerId, layer })); }; + updateEditingVectorLayer() { + this._editingVectorLayerChanged.emit(this._editingVectorLayer); + } + + 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; } @@ -752,6 +769,19 @@ export class JupyterGISModel implements IJupyterGISModel { return this._geolocationChanged; } + get editingVectorLayer(): boolean { + return this._editingVectorLayer; + } + + set editingVectorLayer(editingVectorLayer: boolean) { + this._editingVectorLayer = editingVectorLayer; + this.editingVectorLayerChanged.emit(this.editingVectorLayer); + } + + get editingVectorLayerChanged() { + return this._editingVectorLayerChanged; + } + readonly defaultKernelName: string = ''; readonly defaultKernelLanguage: string = ''; readonly annotationModel?: IAnnotationModel; @@ -792,6 +822,9 @@ export class JupyterGISModel implements IJupyterGISModel { private _geolocation: JgisCoordinates; private _geolocationChanged = new Signal(this); + + private _editingVectorLayer: boolean; + private _editingVectorLayerChanged = new Signal(this); } export namespace JupyterGISModel { diff --git a/python/jupytergis_lab/src/index.ts b/python/jupytergis_lab/src/index.ts index ec34eb6e9..e4f32043d 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.toggleDrawFeatures, + selector: '.jp-gis-layerItem', + rank: 2, + }); + // Create the Download submenu const downloadSubmenu = new Menu({ commands: app.commands }); downloadSubmenu.title.label = translator.load('jupyterlab').__('Download'); diff --git a/yarn.lock b/yarn.lock index dc04ce465..9b4080186 100644 --- a/yarn.lock +++ b/yarn.lock @@ -853,9 +853,11 @@ __metadata: shpjs: ^6.1.0 styled-components: ^5.3.6 three: ^0.135.0 + three-mesh-bvh: ^0.5.17 ts-patch: ^3.3.0 typescript: ^5 typescript-transform-paths: ^3.5.5 + uuid: ^11.0.3 languageName: unknown linkType: soft @@ -11362,6 +11364,15 @@ __metadata: languageName: node linkType: hard +"three-mesh-bvh@npm:^0.5.17": + version: 0.5.24 + resolution: "three-mesh-bvh@npm:0.5.24" + peerDependencies: + three: ">= 0.123.0" + checksum: 5a5e356111756869cfd00870bca2d881d3019b55e71142b5eee544a2e6bcc3546a340b0defa29c3ebe7ed4cfe99e7ee6addd5671bb89852841107189c0f98157 + languageName: node + linkType: hard + "three@npm:^0.135.0": version: 0.135.0 resolution: "three@npm:0.135.0" @@ -11885,6 +11896,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^11.0.3": + version: 11.1.0 + resolution: "uuid@npm:11.1.0" + bin: + uuid: dist/esm/bin/uuid + checksum: 840f19758543c4631e58a29439e51b5b669d5f34b4dd2700b6a1d15c5708c7a6e0c3e2c8c4a2eae761a3a7caa7e9884d00c86c02622ba91137bd3deade6b4b4a + languageName: node + linkType: hard + "uuid@npm:^9.0.0": version: 9.0.1 resolution: "uuid@npm:9.0.1"