diff --git a/packages/base/src/commands/BaseCommandIDs.ts b/packages/base/src/commands/BaseCommandIDs.ts index 95c7491e2..2a511d6d3 100644 --- a/packages/base/src/commands/BaseCommandIDs.ts +++ b/packages/base/src/commands/BaseCommandIDs.ts @@ -2,12 +2,14 @@ * * See the documentation for more details. */ +// Toolbar export const createNew = 'jupytergis:create-new-jGIS-file'; export const redo = 'jupytergis:redo'; export const undo = 'jupytergis:undo'; export const symbology = 'jupytergis:symbology'; export const identify = 'jupytergis:identify'; export const temporalController = 'jupytergis:temporalController'; +export const addMarker = 'jupytergis:addMarker'; // geolocation export const getGeolocation = 'jupytergis:getGeolocation'; diff --git a/packages/base/src/commands/index.ts b/packages/base/src/commands/index.ts index 47ee758b8..029a390a9 100644 --- a/packages/base/src/commands/index.ts +++ b/packages/base/src/commands/index.ts @@ -33,6 +33,8 @@ import { getGeoJSONDataFromLayerSource, downloadFile } from '../tools'; import { JupyterGISTracker } from '../types'; import { JupyterGISDocumentWidget } from '../widget'; +const POINT_SELECTION_TOOL_CLASS = 'jGIS-point-selection-tool'; + interface ICreateEntry { tracker: JupyterGISTracker; formSchemaRegistry: IJGISFormSchemaRegistry; @@ -163,7 +165,7 @@ export function addCommands( if (current.model.currentMode === 'identifying' && !canIdentify) { current.model.currentMode = 'panning'; - current.node.classList.remove('jGIS-identify-tool'); + current.node.classList.remove(POINT_SELECTION_TOOL_CLASS); return false; } @@ -198,14 +200,14 @@ export function addCommands( const keysPressed = luminoEvent.keys as string[] | undefined; if (keysPressed?.includes('Escape')) { current.model.currentMode = 'panning'; - current.node.classList.remove('jGIS-identify-tool'); + current.node.classList.remove(POINT_SELECTION_TOOL_CLASS); commands.notifyCommandChanged(CommandIDs.identify); return; } } - current.node.classList.toggle('jGIS-identify-tool'); - current.model.toggleIdentify(); + current.node.classList.toggle(POINT_SELECTION_TOOL_CLASS); + current.model.toggleMode('identifying'); commands.notifyCommandChanged(CommandIDs.identify); }, @@ -1039,6 +1041,34 @@ export function addCommands( }, }); + commands.addCommand(CommandIDs.addMarker, { + label: trans.__('Add Marker'), + isToggled: () => { + const current = tracker.currentWidget; + if (!current) { + return false; + } + + return current.model.currentMode === 'marking'; + }, + isEnabled: () => { + // TODO should check if at least one layer exists? + return true; + }, + execute: args => { + const current = tracker.currentWidget; + if (!current) { + return; + } + + current.node.classList.toggle(POINT_SELECTION_TOOL_CLASS); + current.model.toggleMode('marking'); + + commands.notifyCommandChanged(CommandIDs.addMarker); + }, + ...icons.get(CommandIDs.addMarker), + }); + loadKeybindings(commands, keybindings); } diff --git a/packages/base/src/constants.ts b/packages/base/src/constants.ts index 9d6bdf0b4..215bf79d4 100644 --- a/packages/base/src/constants.ts +++ b/packages/base/src/constants.ts @@ -10,6 +10,7 @@ import { moundIcon, rasterIcon, vectorSquareIcon, + markerIcon, } from './icons'; /** @@ -56,6 +57,7 @@ const iconObject = { [CommandIDs.symbology]: { iconClass: 'fa fa-brush' }, [CommandIDs.identify]: { icon: infoIcon }, [CommandIDs.temporalController]: { icon: clockIcon }, + [CommandIDs.addMarker]: { icon: markerIcon }, }; /** diff --git a/packages/base/src/icons.ts b/packages/base/src/icons.ts index 18f132d83..b9a6ca61f 100644 --- a/packages/base/src/icons.ts +++ b/packages/base/src/icons.ts @@ -16,6 +16,7 @@ import logoSvgStr from '../style/icons/logo.svg'; import logoMiniSvgStr from '../style/icons/logo_mini.svg'; import logoMiniAlternativeSvgStr from '../style/icons/logo_mini_alternative.svg'; import logoMiniQGZ from '../style/icons/logo_mini_qgz.svg'; +import markerSvgStr from '../style/icons/marker.svg'; import moundSvgStr from '../style/icons/mound.svg'; import nonVisibilitySvgStr from '../style/icons/nonvisibility.svg'; import rasterSvgStr from '../style/icons/raster.svg'; @@ -109,3 +110,8 @@ export const targetWithCenterIcon = new LabIcon({ name: 'jupytergis::targetWithoutCenter', svgstr: targetWithoutCenterSvgStr, }); + +export const markerIcon = new LabIcon({ + name: 'jupytergis::marker', + svgstr: markerSvgStr, +}); diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 0b7359bcc..44ca3a49c 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -32,6 +32,7 @@ import { IWebGlLayer, JgisCoordinates, JupyterGISModel, + IMarkerSource, } from '@jupytergis/schema'; import { showErrorMessage } from '@jupyterlab/apputils'; import { IObservableMap, ObservableMap } from '@jupyterlab/observables'; @@ -411,6 +412,7 @@ export class MainView extends React.Component { }); this._Map.on('click', this._identifyFeature.bind(this)); + this._Map.on('click', this._addMarker.bind(this)); this._Map .getViewport() @@ -825,6 +827,20 @@ export class MainView extends React.Component { }); break; } + + case 'MarkerSource': { + const parameters = source.parameters as IMarkerSource; + + const point = new Point(parameters.feature.coords); + const marker = new Feature({ + type: 'icon', + geometry: point, + }); + + newSource = new VectorSource({ + features: [marker], + }); + } } newSource.set('id', id); @@ -2084,6 +2100,45 @@ export class MainView extends React.Component { this._model.syncPointer(pointer); }); + private _addMarker(e: MapBrowserEvent) { + if (this._model.currentMode !== 'marking') { + return; + } + + const coordinate = this._Map.getCoordinateFromPixel(e.pixel); + const sourceId = UUID.uuid4(); + const layerId = UUID.uuid4(); + + const sourceParameters: IMarkerSource = { + feature: { coords: [coordinate[0], coordinate[1]] }, + }; + + const layerParams: IVectorLayer = { + opacity: 1.0, + source: sourceId, + symbologyState: { renderType: 'Single Symbol' }, + }; + + const sourceModel: IJGISSource = { + type: 'MarkerSource', + name: 'Marker', + parameters: sourceParameters, + }; + + const layerModel: IJGISLayer = { + type: 'VectorLayer', + visible: true, + name: 'Marker', + parameters: layerParams, + }; + + this.addSource(sourceId, sourceModel); + this._model.sharedModel.addSource(sourceId, sourceModel); + + this.addLayer(layerId, layerModel, this.getLayerIDs().length); + this._model.addLayer(layerId, layerModel); + } + private _identifyFeature(e: MapBrowserEvent) { if (this._model.currentMode !== 'identifying') { return; diff --git a/packages/base/src/panelview/components/filter-panel/Filter.tsx b/packages/base/src/panelview/components/filter-panel/Filter.tsx index 76e8c3830..6096bc98e 100644 --- a/packages/base/src/panelview/components/filter-panel/Filter.tsx +++ b/packages/base/src/panelview/components/filter-panel/Filter.tsx @@ -237,6 +237,7 @@ const FilterComponent: React.FC = ({ model }) => { diff --git a/packages/base/src/toolbar/widget.tsx b/packages/base/src/toolbar/widget.tsx index f4eba96bb..08f86b0ef 100644 --- a/packages/base/src/toolbar/widget.tsx +++ b/packages/base/src/toolbar/widget.tsx @@ -170,6 +170,14 @@ export class ToolbarWidget extends ReactiveToolbar { temporalControllerButton.node.dataset.testid = 'temporal-controller-button'; + const addMarkerButton = new CommandToolbarButton({ + id: CommandIDs.addMarker, + label: '', + commands: options.commands, + }); + this.addItem('addMarker', addMarkerButton); + addMarkerButton.node.dataset.testid = 'add-marker-controller-button'; + this.addItem('spacer', ReactiveToolbar.createSpacerItem()); // Users diff --git a/packages/base/style/icons/marker.svg b/packages/base/style/icons/marker.svg new file mode 100644 index 000000000..621846bab --- /dev/null +++ b/packages/base/style/icons/marker.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index 248c090b0..eca7ead7e 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -29,6 +29,7 @@ import { SourceType, } from './_interface/project/jgis'; import { IRasterSource } from './_interface/project/sources/rasterSource'; +import { Modes } from './types'; export { IGeoJSONSource } from './_interface/project/sources/geoJsonSource'; export type JgisCoordinates = { x: number; y: number }; @@ -161,10 +162,7 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { geolocation: JgisCoordinates; localState: IJupyterGISClientState | null; annotationModel?: IAnnotationModel; - - // TODO Add more modes: "annotating" - currentMode: 'panning' | 'identifying'; - + currentMode: Modes; themeChanged: Signal< IJupyterGISModel, IChangedArgs @@ -246,7 +244,7 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { removeMetadata(key: string): void; centerOnPosition(id: string): void; - toggleIdentify(): void; + toggleMode(mode: Modes): void; isTemporalControllerActive: boolean; toggleTemporalController(): void; diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index bd59e726f..242b77efc 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -37,6 +37,7 @@ import { IJupyterGISSettings, } from './interfaces'; import jgisSchema from './schema/project/jgis.json'; +import { Modes } from './types'; const SETTINGS_ID = '@jupytergis/jupytergis-core:jupytergis-settings'; @@ -758,19 +759,20 @@ export class JupyterGISModel implements IJupyterGISModel { } } - toggleIdentify() { - if (this._currentMode === 'identifying') { - this._currentMode = 'panning'; - } else { - this._currentMode = 'identifying'; - } + /** + * Toggle a map interaction mode on or off. + * Toggleing off sets the mode to 'panning'. + * @param mode The mode to be toggled + */ + toggleMode(mode: Modes) { + this._currentMode = this._currentMode === mode ? 'panning' : mode; } - get currentMode(): 'panning' | 'identifying' { + get currentMode(): Modes { return this._currentMode; } - set currentMode(value: 'panning' | 'identifying') { + set currentMode(value: Modes) { this._currentMode = value; } @@ -869,7 +871,7 @@ export class JupyterGISModel implements IJupyterGISModel { private _settingsChanged: Signal; private _jgisSettings: IJupyterGISSettings; - private _currentMode: 'panning' | 'identifying'; + private _currentMode: Modes; private _sharedModel: IJupyterGISDoc; private _filePath: string; diff --git a/packages/schema/src/schema/project/jgis.json b/packages/schema/src/schema/project/jgis.json index b710f415a..9a013d5d6 100644 --- a/packages/schema/src/schema/project/jgis.json +++ b/packages/schema/src/schema/project/jgis.json @@ -55,7 +55,8 @@ "ImageSource", "ShapefileSource", "GeoTiffSource", - "GeoParquetSource" + "GeoParquetSource", + "MarkerSource" ] }, "jGISLayer": { diff --git a/packages/schema/src/schema/project/sources/markerSource.json b/packages/schema/src/schema/project/sources/markerSource.json new file mode 100644 index 000000000..3c8bc1e2b --- /dev/null +++ b/packages/schema/src/schema/project/sources/markerSource.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "description": "MarkerSource", + "title": "IMarkerSource", + "required": ["feature"], + "additionalProperties": false, + "properties": { + "feature": { + "type": "object", + "description": "Info for the marker", + "required": ["coords"], + "properties": { + "coords": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "number" + } + } + } + } + } +} diff --git a/packages/schema/src/types.ts b/packages/schema/src/types.ts index c6e2cd86d..97d58074c 100644 --- a/packages/schema/src/types.ts +++ b/packages/schema/src/types.ts @@ -10,6 +10,7 @@ export * from './_interface/project/sources/shapefileSource'; export * from './_interface/project/sources/vectorTileSource'; export * from './_interface/project/sources/videoSource'; export * from './_interface/project/sources/geoParquetSource'; +export * from './_interface/project/sources/markerSource'; // Layers export * from './_interface/project/layers/heatmapLayer'; @@ -34,3 +35,5 @@ export * from './index'; export * from './interfaces'; export * from './model'; export * from './token'; + +export type Modes = 'panning' | 'identifying' | 'marking'; diff --git a/python/jupytergis_core/jupytergis_core/schema/__init__.py b/python/jupytergis_core/jupytergis_core/schema/__init__.py index 64f867eb5..8e51c202e 100644 --- a/python/jupytergis_core/jupytergis_core/schema/__init__.py +++ b/python/jupytergis_core/jupytergis_core/schema/__init__.py @@ -9,6 +9,7 @@ from .interfaces.project.layers.heatmapLayer import IHeatmapLayer # noqa from .interfaces.project.sources.vectorTileSource import IVectorTileSource # noqa +from .interfaces.project.sources.markerSource import IMarkerSource # noqa from .interfaces.project.sources.rasterSource import IRasterSource # noqa from .interfaces.project.sources.geoJsonSource import IGeoJSONSource # noqa from .interfaces.project.sources.videoSource import IVideoSource # noqa diff --git a/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py b/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py index 73b422331..48947ed64 100644 --- a/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py +++ b/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py @@ -26,6 +26,7 @@ IVectorLayer, IVectorTileLayer, IVectorTileSource, + IMarkerSource, IVideoSource, IWebGlLayer, LayerType, @@ -880,6 +881,7 @@ class Config: parameters: ( IRasterSource | IVectorTileSource + | IMarkerSource | IGeoJSONSource | IImageSource | IVideoSource @@ -967,6 +969,7 @@ def create_source( OBJECT_FACTORY.register_factory(LayerType.HeatmapLayer, IHeatmapLayer) OBJECT_FACTORY.register_factory(SourceType.VectorTileSource, IVectorTileSource) +OBJECT_FACTORY.register_factory(SourceType.MarkerSource, IMarkerSource) OBJECT_FACTORY.register_factory(SourceType.RasterSource, IRasterSource) OBJECT_FACTORY.register_factory(SourceType.GeoJSONSource, IGeoJSONSource) OBJECT_FACTORY.register_factory(SourceType.ImageSource, IImageSource) diff --git a/python/jupytergis_lab/style/base.css b/python/jupytergis_lab/style/base.css index 1c5dbf95b..9a8d65ae4 100644 --- a/python/jupytergis_lab/style/base.css +++ b/python/jupytergis_lab/style/base.css @@ -687,7 +687,7 @@ div.jGIS-toolbar-widget > div.jp-Toolbar-item:last-child { fill: var(--jp-inverse-layout-color3); } -.jGIS-identify-tool { +.jGIS-point-selection-tool { cursor: crosshair; } diff --git a/ui-tests/tests/filters.spec.ts b/ui-tests/tests/filters.spec.ts index 8a4c47d5b..a000ec244 100644 --- a/ui-tests/tests/filters.spec.ts +++ b/ui-tests/tests/filters.spec.ts @@ -28,14 +28,14 @@ test.describe('#filters', () => { await page.getByText('Filters').click(); // Add first filter - await page.getByRole('button', { name: 'Add' }).click(); + await page.getByTestId('add-filter-button').click(); await page.locator('#jp-gis-feature-select-0').selectOption('mag'); await page.locator('#jp-gis-operator-select-0').selectOption('>'); await page.locator('#jp-gis-value-select-0').selectOption('2.73'); await page.getByRole('button', { name: 'Submit' }).click(); // Add second filter - await page.getByRole('button', { name: 'Add' }).click(); + await page.getByTestId('add-filter-button').click(); await page.locator('#jp-gis-feature-select-1').selectOption('felt'); await page.locator('#jp-gis-operator-select-1').selectOption('>'); await page.locator('#jp-gis-value-select-1').selectOption('10'); diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-0-chromium-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-0-chromium-linux.png index fdf6fb68f..85900b302 100644 Binary files a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-0-chromium-linux.png and b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-0-chromium-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-1-chromium-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-1-chromium-linux.png index f2723d5ad..5a008d603 100644 Binary files a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-1-chromium-linux.png and b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-1-chromium-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-3-chromium-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-3-chromium-linux.png index c5427f1aa..a2515c610 100644 Binary files a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-3-chromium-linux.png and b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-3-chromium-linux.png differ