From 6de01e0586e47667037b6c0710e7d4eaba1bdecb Mon Sep 17 00:00:00 2001 From: Nicolas Brichet <32258950+brichet@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:13:34 +0200 Subject: [PATCH] Add layer in the layers tree from the GIS model (#22) * Add layer in the layers tree from the GIS model * Docstring and lint * Rename layersTree to layerTree and layersGroup to layerGroup * lint * Fix test after rebase * Correction after rebase --- examples/test.jGIS | 52 +++--- packages/base/src/commands.ts | 2 +- packages/base/src/mainview/mainview.tsx | 6 +- .../base/src/panelview/components/layers.tsx | 50 +++--- packages/base/src/panelview/leftpanel.tsx | 8 +- packages/base/style/leftPanel.css | 10 +- packages/schema/src/doc.ts | 45 +++-- packages/schema/src/interfaces.ts | 32 +++- packages/schema/src/model.ts | 154 ++++++++++++++++-- packages/schema/src/schema/jgis.json | 17 +- .../jupytergis_core/jgis_ydoc.py | 14 +- .../jupytergis_core/src/jgisplugin/plugins.ts | 2 +- .../jupytergis_lab/notebook/gis_document.py | 8 +- ui-tests/tests/left-panel.spec.ts | 62 ++++--- 14 files changed, 309 insertions(+), 153 deletions(-) diff --git a/examples/test.jGIS b/examples/test.jGIS index da295b55..c048d4e9 100644 --- a/examples/test.jGIS +++ b/examples/test.jGIS @@ -1,40 +1,31 @@ { "layers": { - "a5ac7671-74bb-4c99-a494-916348397d01": { - "name": "Open Street Map 2", + "a0044fd7-f167-445f-b3d1-620a8f94b498": { + "name": "Open Topo Map", "type": "RasterLayer", - "visible": true, "parameters": { - "source": "f22850a8-bfd5-4dfb-ba1e-c3f2c3ccf93b" - } + "source": "5fd42e3b-4681-4607-b15d-65c3a3e89b32" + }, + "visible": true }, - "a0044fd7-f167-445f-b3d1-620a8f94b498": { + "a5ac7671-74bb-4c99-a494-916348397d01": { + "name": "Open Street Map 2", "parameters": { - "source": "5fd42e3b-4681-4607-b15d-65c3a3e89b32" + "source": "f22850a8-bfd5-4dfb-ba1e-c3f2c3ccf93b" }, "type": "RasterLayer", - "name": "Open Topo Map", "visible": true }, "2467576f-b527-4cb7-998d-fa1d056fb8a1": { "parameters": { "source": "699facc9-e7c4-4f38-acf1-1fd7f02d9f36" }, - "type": "RasterLayer", + "visible": true, "name": "Open Street Map", - "visible": true + "type": "RasterLayer" } }, "sources": { - "5fd42e3b-4681-4607-b15d-65c3a3e89b32": { - "type": "RasterSource", - "parameters": { - "minZoom": 0.0, - "maxZoom": 24.0, - "url": "https://tile.opentopomap.org/{z}/{x}/{y}.png " - }, - "name": "Open Topo Map" - }, "f22850a8-bfd5-4dfb-ba1e-c3f2c3ccf93b": { "parameters": { "maxZoom": 24.0, @@ -44,21 +35,29 @@ "type": "RasterSource", "name": "Open Street Map 2" }, + "5fd42e3b-4681-4607-b15d-65c3a3e89b32": { + "parameters": { + "url": "https://tile.opentopomap.org/{z}/{x}/{y}.png ", + "maxZoom": 24.0, + "minZoom": 0.0 + }, + "name": "Open Topo Map", + "type": "RasterSource" + }, "699facc9-e7c4-4f38-acf1-1fd7f02d9f36": { + "type": "RasterSource", "name": "Open Street Map", "parameters": { - "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "minZoom": 0.0, - "maxZoom": 24.0 - }, - "type": "RasterSource" + "maxZoom": 24.0, + "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png" + } } }, "options": {}, - "layersTree": [ + "layerTree": [ "2467576f-b527-4cb7-998d-fa1d056fb8a1", { - "name": "level 1 group", "layers": [ "a0044fd7-f167-445f-b3d1-620a8f94b498", { @@ -67,7 +66,8 @@ "a5ac7671-74bb-4c99-a494-916348397d01" ] } - ] + ], + "name": "level 1 group" } ] } \ No newline at end of file diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 41fbb189..4b5939d8 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -181,7 +181,7 @@ namespace Private { }; sharedModel.addSource(sourceId, sourceModel); - sharedModel.addLayer(UUID.uuid4(), layerModel); + current.context.model.addLayer(UUID.uuid4(), layerModel); }, cancelButton: () => { current.context.model.syncFormData(undefined); diff --git a/packages/base/src/mainview/mainview.tsx b/packages/base/src/mainview/mainview.tsx index 026b7fd9..5e9a0734 100644 --- a/packages/base/src/mainview/mainview.tsx +++ b/packages/base/src/mainview/mainview.tsx @@ -2,7 +2,7 @@ import { MapChange } from '@jupyter/ydoc'; import { IJGISLayer, IJGISLayerDocChange, - IJGISLayersTreeDocChange, + IJGISLayerTreeDocChange, IJGISSource, IJGISSourceDocChange, IJupyterGISClientState, @@ -55,7 +55,7 @@ export class MainView extends React.Component { ); this._model.sharedLayersChanged.connect(this._onLayersChanged, this); - this._model.sharedLayersTreeChanged.connect(this._onLayerTreeChange, this); + this._model.sharedLayerTreeChanged.connect(this._onLayerTreeChange, this); this._model.sharedSourcesChanged.connect(this._onSourcesChange, this); this.state = { @@ -380,7 +380,7 @@ export class MainView extends React.Component { private _onLayerTreeChange( sender: IJupyterGISDoc, - change: IJGISLayersTreeDocChange + change: IJGISLayerTreeDocChange ): void { // We can't properly use the change, because of the nested groups in the the shared // document which is flattened for the map tool. diff --git a/packages/base/src/panelview/components/layers.tsx b/packages/base/src/panelview/components/layers.tsx index b0c02f08..46c65651 100644 --- a/packages/base/src/panelview/components/layers.tsx +++ b/packages/base/src/panelview/components/layers.tsx @@ -1,6 +1,6 @@ import { - IJGISLayersGroup, - IJGISLayersTree, + IJGISLayerGroup, + IJGISLayerTree, IJupyterGISClientState, IJupyterGISModel, ISelection @@ -17,9 +17,9 @@ import { nonVisibilityIcon, rasterIcon, visibilityIcon } from '../../icons'; import { IControlPanelModel } from '../../types'; const LAYERS_PANEL_CLASS = 'jp-gis-layerPanel'; -const LAYERS_GROUP_CLASS = 'jp-gis-layersGroup'; -const LAYERS_GROUP_HEADER_CLASS = 'jp-gis-layersGroupHeader'; -const LAYERS_GROUP_COLLAPSER_CLASS = 'jp-gis-layersGroupCollapser'; +const LAYER_GROUP_CLASS = 'jp-gis-layerGroup'; +const LAYER_GROUP_HEADER_CLASS = 'jp-gis-layerGroupHeader'; +const LAYER_GROUP_COLLAPSER_CLASS = 'jp-gis-layerGroupCollapser'; const LAYER_ITEM_CLASS = 'jp-gis-layerItem'; const LAYER_CLASS = 'jp-gis-layer'; const LAYER_TITLE_CLASS = 'jp-gis-layerTitle'; @@ -44,7 +44,7 @@ export class LayersPanel extends Panel { constructor(options: LayersPanel.IOptions) { super(); this._model = options.model; - this.id = 'jupytergis::layersTree'; + this.id = 'jupytergis::layerTree'; this.addClass(LAYERS_PANEL_CLASS); this.addWidget( ReactWidget.create( @@ -91,8 +91,8 @@ function LayersBodyComponent(props: IBodyProps): JSX.Element { const [model, setModel] = useState( props.model?.jGISModel ); - const [layersTree, setLayersTree] = useState( - model?.getLayersTree() || [] + const [layerTree, setLayerTree] = useState( + model?.getLayerTree() || [] ); /** @@ -103,18 +103,18 @@ function LayersBodyComponent(props: IBodyProps): JSX.Element { }; /** - * Listen to the layers and layers tree changes. + * Listen to the layers and layer tree changes. */ useEffect(() => { const updateLayers = () => { - setLayersTree(model?.getLayersTree() || []); + setLayerTree(model?.getLayerTree() || []); }; - model?.sharedModel?.layersChanged.connect(updateLayers); - model?.sharedModel?.layersTreeChanged.connect(updateLayers); + model?.sharedModel.layersChanged.connect(updateLayers); + model?.sharedModel.layerTreeChanged.connect(updateLayers); return () => { - model?.sharedModel?.layersChanged.disconnect(updateLayers); - model?.sharedModel?.layersTreeChanged.disconnect(updateLayers); + model?.sharedModel.layersChanged.disconnect(updateLayers); + model?.sharedModel.layerTreeChanged.disconnect(updateLayers); }; }, [model]); @@ -123,12 +123,12 @@ function LayersBodyComponent(props: IBodyProps): JSX.Element { */ props.model?.documentChanged.connect((_, widget) => { setModel(widget?.context.model); - setLayersTree(widget?.context.model?.getLayersTree() || []); + setLayerTree(widget?.context.model?.getLayerTree() || []); }); return (
- {layersTree.map(layer => + {layerTree.map(layer => typeof layer === 'string' ? ( ) : ( - void; } /** * The component to handle group of layers. */ -function LayersGroupComponent(props: ILayersGroupProps): JSX.Element { +function LayerGroupComponent(props: ILayerGroupProps): JSX.Element { const { group, gisModel } = props; if (group === undefined) { return <>; @@ -169,12 +169,12 @@ function LayersGroupComponent(props: ILayersGroupProps): JSX.Element { const layers = group?.layers ?? []; return ( -
-
setOpen(!open)} className={LAYERS_GROUP_HEADER_CLASS}> +
+
setOpen(!open)} className={LAYER_GROUP_HEADER_CLASS}> @@ -190,7 +190,7 @@ function LayersGroupComponent(props: ILayersGroupProps): JSX.Element { onClick={props.onClick} /> ) : ( - { if (changed) { diff --git a/packages/base/style/leftPanel.css b/packages/base/style/leftPanel.css index c76f7a93..6dd9c599 100644 --- a/packages/base/style/leftPanel.css +++ b/packages/base/style/leftPanel.css @@ -7,28 +7,28 @@ padding: 2px 0 2px 12px; } -.jp-gis-layersGroup { +.jp-gis-layerGroup { display: flex; flex-direction: column; } -.jp-gis-layersGroupHeader { +.jp-gis-layerGroupHeader { display: flex; margin-left: -2px; padding-bottom: 4px; } -.jp-gis-layersGroupHeader:hover { +.jp-gis-layerGroupHeader:hover { cursor: pointer; } -.jp-gis-layersGroupCollapser { +.jp-gis-layerGroupCollapser { transform: rotate(-90deg); margin: auto 0; height: 16px; } -.jp-gis-layersGroupCollapser.jp-mod-expanded { +.jp-gis-layerGroupCollapser.jp-mod-expanded { transform: rotate(0deg); } diff --git a/packages/schema/src/doc.ts b/packages/schema/src/doc.ts index e078e2c2..78c10705 100644 --- a/packages/schema/src/doc.ts +++ b/packages/schema/src/doc.ts @@ -7,7 +7,7 @@ import { IJGISLayer, IJGISLayerItem, IJGISLayers, - IJGISLayersTree, + IJGISLayerTree, IJGISOptions, IJGISSource, IJGISSources @@ -15,7 +15,7 @@ import { import { IDict, IJGISLayerDocChange, - IJGISLayersTreeDocChange, + IJGISLayerTreeDocChange, IJGISSourceDocChange, IJupyterGISDoc, IJupyterGISDocChange @@ -30,13 +30,13 @@ export class JupyterGISDoc this._options = this.ydoc.getMap>('options'); this._layers = this.ydoc.getMap>('layers'); - this._layersTree = this.ydoc.getArray('layersTree'); + this._layerTree = this.ydoc.getArray('layerTree'); this._sources = this.ydoc.getMap>('sources'); this.undoManager.addToScope(this._layers); this.undoManager.addToScope(this._sources); this._layers.observeDeep(this._layersObserver.bind(this)); - this._layersTree.observe(this._layersTreeObserver.bind(this)); + this._layerTree.observe(this._layerTreeObserver.bind(this)); this._sources.observeDeep(this._sourcesObserver.bind(this)); this._options.observe(this._optionsObserver.bind(this)); } @@ -73,14 +73,14 @@ export class JupyterGISDoc return JSONExt.deepCopy(this._sources.toJSON()); } - get layersTree(): IJGISLayersTree { - return this._layersTree.toJSON() as IJGISLayersTree; + get layerTree(): IJGISLayerTree { + return JSONExt.deepCopy(this._layerTree.toJSON()); } - set layersTree(layersTree: IJGISLayersTree) { + set layerTree(layerTree: IJGISLayerTree) { this.transact(() => { - this._layersTree.delete(0, this._layersTree.length); - this._layersTree.push(layersTree); + this._layerTree.delete(0, this._layerTree.length); + this._layerTree.push(layerTree); }); } @@ -114,8 +114,8 @@ export class JupyterGISDoc return this._layersChanged; } - get layersTreeChanged(): ISignal { - return this._layersTreeChanged; + get layerTreeChanged(): ISignal { + return this._layerTreeChanged; } get sourcesChanged(): ISignal { @@ -148,6 +148,19 @@ export class JupyterGISDoc }); } + addLayerTreeItem(index: number, item: IJGISLayerItem) { + this.transact(() => { + this._layerTree.insert(index, [item]); + }); + } + + updateLayerTreeItem(index: number, item: IJGISLayerItem) { + this.transact(() => { + this._layerTree.delete(index); + this._layerTree.insert(index, [item]); + }); + } + getObject(id: string): IJGISLayer | IJGISSource | undefined { const layer = this.getLayer(id); if (layer) { @@ -260,9 +273,9 @@ export class JupyterGISDoc } } - private _layersTreeObserver(event: Y.YArrayEvent): void { + private _layerTreeObserver(event: Y.YArrayEvent): void { const layerTreeChanges = event.delta as Delta; - this._layersTreeChanged.emit({ layersTreeChange: layerTreeChanges }); + this._layerTreeChanged.emit({ layerTreeChange: layerTreeChanges }); } private _sourcesObserver(events: Y.YEvent[]): void { @@ -293,16 +306,16 @@ export class JupyterGISDoc }; private _layers: Y.Map; - private _layersTree: Y.Array; + private _layerTree: Y.Array; private _sources: Y.Map; private _options: Y.Map; private _optionsChanged = new Signal(this); private _layersChanged = new Signal( this ); - private _layersTreeChanged = new Signal< + private _layerTreeChanged = new Signal< IJupyterGISDoc, - IJGISLayersTreeDocChange + IJGISLayerTreeDocChange >(this); private _sourcesChanged = new Signal( this diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index 529dcc3f..15c6e909 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -17,7 +17,7 @@ import { IJGISLayer, IJGISLayerItem, IJGISLayers, - IJGISLayersTree, + IJGISLayerTree, IJGISOptions, IJGISSource, IJGISSources @@ -34,8 +34,8 @@ export interface IJGISLayerDocChange { }>; } -export interface IJGISLayersTreeDocChange { - layersTreeChange?: Delta; +export interface IJGISLayerTreeDocChange { + layerTreeChange?: Delta; } export interface IJGISSourceDocChange { @@ -66,7 +66,7 @@ export interface IJupyterGISDoc extends YDocument { options: IJGISOptions; layers: IJGISLayers; sources: IJGISSources; - layersTree: IJGISLayersTree; + layerTree: IJGISLayerTree; readonly editable: boolean; readonly toJGISEndpoint?: string; @@ -74,7 +74,12 @@ export interface IJupyterGISDoc extends YDocument { layerExists(id: string): boolean; getLayer(id: string): IJGISLayer | undefined; removeLayer(id: string): void; - addLayer(id: string, value: IJGISLayer): void; + addLayer( + id: string, + value: IJGISLayer, + groupName?: string, + position?: number + ): void; updateLayer(id: string, value: IJGISLayer): void; sourceExists(id: string): boolean; @@ -83,6 +88,9 @@ export interface IJupyterGISDoc extends YDocument { addSource(id: string, value: IJGISSource): void; updateSource(id: string, value: IJGISSource): void; + addLayerTreeItem(index: number, item: IJGISLayerItem): void; + updateLayerTreeItem(index: number, item: IJGISLayerItem): void; + updateObjectParameters( id: string, value: IJGISLayer['parameters'] | IJGISSource['parameters'] @@ -95,7 +103,7 @@ export interface IJupyterGISDoc extends YDocument { optionsChanged: ISignal; layersChanged: ISignal; sourcesChanged: ISignal; - layersTreeChanged: ISignal; + layerTreeChanged: ISignal; } export interface IJupyterGISDocChange extends DocumentChange { @@ -106,7 +114,7 @@ export interface IJupyterGISDocChange extends DocumentChange { key: string; newValue: IJGISLayer | undefined; }>; - layersTreeChange?: Delta; + layerTreeChange?: Delta; optionChange?: MapChange; stateChange?: StateChange[]; } @@ -126,7 +134,7 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { >; sharedOptionsChanged: ISignal; sharedLayersChanged: ISignal; - sharedLayersTreeChanged: ISignal; + sharedLayerTreeChanged: ISignal; sharedSourcesChanged: ISignal; getContent(): IJGISContent; @@ -134,7 +142,13 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { getLayer(id: string): IJGISLayer | undefined; getSources(): IJGISSources; getSource(id: string): IJGISSource | undefined; - getLayersTree(): IJGISLayersTree; + getLayerTree(): IJGISLayerTree; + addLayer( + id: string, + layer: IJGISLayer, + groupName?: string, + position?: number + ): void; syncSelected(value: { [key: string]: ISelection }, emitter?: string): void; syncSelectedPropField(data: { diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index 6ef443cf..01bd75ed 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -10,14 +10,15 @@ import { IJGISLayer, IJGISLayerItem, IJGISLayers, - IJGISLayersTree, + IJGISLayerGroup, + IJGISLayerTree, IJGISSource, IJGISSources } from './_interface/jgis'; import { JupyterGISDoc } from './doc'; import { IJGISLayerDocChange, - IJGISLayersTreeDocChange, + IJGISLayerTreeDocChange, IJGISSourceDocChange, IJupyterGISClientState, IJupyterGISDoc, @@ -120,11 +121,11 @@ export class JupyterGISModel implements IJupyterGISModel { return this.sharedModel.layersChanged; } - get sharedLayersTreeChanged(): ISignal< + get sharedLayerTreeChanged(): ISignal< IJupyterGISDoc, - IJGISLayersTreeDocChange + IJGISLayerTreeDocChange > { - return this.sharedModel.layersTreeChanged; + return this.sharedModel.layerTreeChanged; } get sharedSourcesChanged(): ISignal { @@ -162,7 +163,7 @@ export class JupyterGISModel implements IJupyterGISModel { this.sharedModel.transact(() => { this.sharedModel.sources = jsonData.sources ?? {}; this.sharedModel.layers = jsonData.layers ?? {}; - this.sharedModel.layersTree = jsonData.layersTree ?? []; + this.sharedModel.layerTree = jsonData.layerTree ?? []; this.sharedModel.options = jsonData.options ?? {}; }); this.dirty = true; @@ -188,7 +189,7 @@ export class JupyterGISModel implements IJupyterGISModel { return { sources: this.sharedModel.sources, layers: this.sharedModel.layers, - layersTree: this.sharedModel.layersTree, + layerTree: this.sharedModel.layerTree, options: this.sharedModel.options }; } @@ -201,8 +202,8 @@ export class JupyterGISModel implements IJupyterGISModel { return this.sharedModel.sources; } - getLayersTree(): IJGISLayersTree { - return this.sharedModel.layersTree; + getLayerTree(): IJGISLayerTree { + return this.sharedModel.layerTree; } getLayer(id: string): IJGISLayer | undefined { @@ -213,6 +214,51 @@ export class JupyterGISModel implements IJupyterGISModel { return this.sharedModel.getSource(id); } + /** + * Add a layer group in the layer tree. + * + * @param name - the name of the group. + * @param groupName - (optional) the name of the parent group in which to include the + * new group. + * @param position - (optional) the index of the new group in its parent group or + * from root of layer tree. + */ + addGroup(name: string, groupName?: string, position?: number): void { + const indexesPath = Private.findGroupPath(this.getLayerTree(), name); + if (indexesPath.length) { + console.warn(`The group "${groupName}" already exist in the layer tree`); + return; + } + const item: IJGISLayerGroup = { + name, + layers: [] + }; + this._addLayerTreeItem(item, groupName, position); + } + + /** + * Add a layer in the layer tree and the layers list. + * + * @param id - the ID of the layer. + * @param layer - the layer object. + * @param groupName - optional) the name of the group in which to include the new + * layer. + * @param position - (optional) the index of the new layer in its parent group or + * from root of layer tree. + */ + addLayer( + id: string, + layer: IJGISLayer, + groupName?: string, + position?: number + ): void { + if (!this.getLayer(id)) { + this.sharedModel.addLayer(id, layer); + } + + this._addLayerTreeItem(id, groupName, position); + } + syncSelected(value: { [key: string]: ISelection }, emitter?: string): void { this.sharedModel.awareness.setLocalStateField('selected', { value, @@ -244,6 +290,54 @@ export class JupyterGISModel implements IJupyterGISModel { return this.sharedModel.awareness.clientID; } + /** + * Add an item in the layer tree. + * + * @param item - the item to add. + * @param groupName - (optional) the name of the parent group in which to include the + * new item. + * @param index - (optional) the index of the new item in its parent group or + * from root of layer tree. + */ + private _addLayerTreeItem( + item: IJGISLayerItem, + groupName?: string, + index?: number + ): void { + if (groupName) { + const layerTree = this.getLayerTree(); + const indexesPath = Private.findGroupPath(layerTree, groupName); + if (!indexesPath.length) { + console.warn( + `The group "${groupName}" does not exist in the layer tree` + ); + return; + } + + const mainGroupIndex = indexesPath.shift(); + if (mainGroupIndex === undefined) { + return; + } + const mainGroup = layerTree[mainGroupIndex] as IJGISLayerGroup; + let workingGroup = mainGroup; + while (indexesPath.length) { + const groupIndex = indexesPath.shift(); + if (groupIndex === undefined) { + break; + } + workingGroup = workingGroup.layers[groupIndex] as IJGISLayerGroup; + } + workingGroup.layers.splice(index ?? workingGroup.layers.length, 0, item); + + this._sharedModel.updateLayerTreeItem(mainGroupIndex, mainGroup); + } else { + this.sharedModel.addLayerTreeItem( + index ?? this.getLayerTree.length, + item + ); + } + } + private _onClientStateChanged = changed => { const clients = this.sharedModel.awareness.getStates() as Map< number, @@ -288,13 +382,16 @@ export namespace JupyterGISModel { * Function to get the ordered list of layers according to the tree. */ export function getOrderedLayerIds(model: IJupyterGISModel): string[] { - return Private.layerTreeRecursion(model.sharedModel.layersTree); + return Private.layerTreeRecursion(model.sharedModel.layerTree); } } namespace Private { /** - * Recursive function through the layer tree. + * Recursive function through the layer tree to retrieve the flattened layers order. + * + * @param items - the items list being scanned. + * @param current - the current flattened layers. */ export function layerTreeRecursion( items: IJGISLayerItem[], @@ -309,4 +406,39 @@ namespace Private { } return current; } + + /** + * Recursive function through the layer tree to retrieve the indexes path to a group. + * + * @param items - the items list being scanned. + * @param groupName - the target group name. + * @param indexes - the current indexes path to the group + */ + export function findGroupPath( + items: IJGISLayerItem[], + groupName: string, + indexes: number[] = [] + ): number[] { + for (let index = 0; index < items.length; index++) { + const item = items[index]; + if (typeof item === 'string') { + continue; + } else { + const workingIndexes = [...indexes]; + workingIndexes.push(index); + if (item.name === groupName) { + return workingIndexes; + } + const foundIndexes = findGroupPath( + item.layers, + groupName, + workingIndexes + ); + if (foundIndexes.length > workingIndexes.length) { + return foundIndexes; + } + } + } + return indexes; + } } diff --git a/packages/schema/src/schema/jgis.json b/packages/schema/src/schema/jgis.json index 56e75f70..657bd960 100644 --- a/packages/schema/src/schema/jgis.json +++ b/packages/schema/src/schema/jgis.json @@ -10,8 +10,8 @@ "sources": { "$ref": "#/definitions/jGISSources" }, - "layersTree": { - "$ref": "#/definitions/jGISLayersTree" + "layerTree": { + "$ref": "#/definitions/jGISLayerTree" }, "options": { "$ref": "#/definitions/jGISOptions" @@ -63,15 +63,14 @@ } } }, - "jGISLayersGroup": { - "title": "IJGISLayersGroup", + "jGISLayerGroup": { + "title": "IJGISLayerGroup", "type": "object", "additionalProperties": false, "required": ["name", "layers"], "properties": { "name": { - "type": "string", - "default": "" + "type": "string" }, "layers": { "type": "array", @@ -104,8 +103,8 @@ "$ref": "#/definitions/jGISSource" } }, - "jGISLayersTree": { - "title": "IJGISLayersTree", + "jGISLayerTree": { + "title": "IJGISLayerTree", "type": "array", "default": [], "items": { @@ -126,7 +125,7 @@ "type": "string" }, { - "$ref": "#/definitions/jGISLayersGroup" + "$ref": "#/definitions/jGISLayerGroup" } ] } diff --git a/python/jupytergis_core/jupytergis_core/jgis_ydoc.py b/python/jupytergis_core/jupytergis_core/jgis_ydoc.py index 15527e35..ad7c002d 100644 --- a/python/jupytergis_core/jupytergis_core/jgis_ydoc.py +++ b/python/jupytergis_core/jupytergis_core/jgis_ydoc.py @@ -12,7 +12,7 @@ def __init__(self, *args, **kwargs): self._ydoc["layers"] = self._ylayers = Map() self._ydoc["sources"] = self._ysources = Map() self._ydoc["options"] = self._yoptions = Map() - self._ydoc["layersTree"] = self._ylayersTree = Array() + self._ydoc["layerTree"] = self._ylayerTree = Array() def version(self) -> str: return "0.1.0" @@ -26,9 +26,9 @@ def get(self) -> str: layers = self._ylayers.to_py() sources = self._ysources.to_py() options = self._yoptions.to_py() - layers_tree = self._ylayersTree.to_py() + layers_tree = self._ylayerTree.to_py() return json.dumps( - dict(layers=layers, sources=sources, options=options, layersTree=layers_tree), + dict(layers=layers, sources=sources, options=options, layerTree=layers_tree), indent=2, ) @@ -50,8 +50,8 @@ def set(self, value: str) -> None: self._yoptions.clear() self._yoptions.update(valueDict.get("options", {})) - self._ylayersTree.clear() - self._ylayersTree.extend(valueDict.get("layersTree", [])) + self._ylayerTree.clear() + self._ylayerTree.extend(valueDict.get("layerTree", [])) def observe(self, callback: Callable[[str, Any], None]): self.unobserve() @@ -67,6 +67,6 @@ def observe(self, callback: Callable[[str, Any], None]): self._subscriptions[self._yoptions] = self._yoptions.observe_deep( partial(callback, "options") ) - self._subscriptions[self._ylayersTree] = self._yoptions.observe( - partial(callback, "layersTree") + self._subscriptions[self._ylayerTree] = self._yoptions.observe( + partial(callback, "layerTree") ) diff --git a/python/jupytergis_core/src/jgisplugin/plugins.ts b/python/jupytergis_core/src/jgisplugin/plugins.ts index edf659d5..63a5ca51 100644 --- a/python/jupytergis_core/src/jgisplugin/plugins.ts +++ b/python/jupytergis_core/src/jgisplugin/plugins.ts @@ -111,7 +111,7 @@ const activate = ( format: 'text', size: undefined, content: - '{\n\t"layers": {},\n\t"sources": {},\n\t"options": {},\n\t"layersTree": []\n}' + '{\n\t"layers": {},\n\t"sources": {},\n\t"options": {},\n\t"layerTree": []\n}' }); // Open the newly created file with the 'Editor' diff --git a/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py b/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py index 6c5ddec4..3e8c6c5e 100644 --- a/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py +++ b/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py @@ -38,7 +38,7 @@ def __init__(self, path: Optional[str] = None): self.ydoc["layers"] = self._layers = Array() self.ydoc["options"] = self._options = Map() - self.ydoc["layersTree"] = self._layersTree = Array() + self.ydoc["layerTree"] = self._layerTree = Array() @property def layers(self) -> List[str]: @@ -50,12 +50,12 @@ def layers(self) -> List[str]: return [] @property - def layersTree(self) -> List[str | Dict]: + def layerTree(self) -> List[str | Dict]: """ Get the first level layers list of the tree. """ - if self._layersTree: - return self._layersTree + if self._layerTree: + return self._layerTree return [] @classmethod diff --git a/ui-tests/tests/left-panel.spec.ts b/ui-tests/tests/left-panel.spec.ts index 672beb90..ed1ced90 100644 --- a/ui-tests/tests/left-panel.spec.ts +++ b/ui-tests/tests/left-panel.spec.ts @@ -17,15 +17,15 @@ async function openLeftPanel(page: IJupyterLabPageFixture): Promise { return sidePanel; } -async function openLayersTree(page: IJupyterLabPageFixture): Promise { +async function openLayerTree(page: IJupyterLabPageFixture): Promise { const sidePanel = await openLeftPanel(page); - const layersTree = sidePanel.locator('.jp-gis-layerPanel'); - if (!(await layersTree.isVisible())) { + const layerTree = sidePanel.locator('.jp-gis-layerPanel'); + if (!(await layerTree.isVisible())) { const layerTitle = sidePanel.getByTitle('Layer tree'); await layerTitle.click(); - await page.waitForCondition(async () => await layersTree.isVisible()); + await page.waitForCondition(async () => await layerTree.isVisible()); } - return layersTree; + return layerTree; } test.describe('#overview', () => { @@ -45,8 +45,8 @@ test.describe('#overview', () => { test.describe('#layersPanel', () => { test.describe('without GIS document', () => { test('should have empty layer panel', async ({ page }) => { - const layersTree = await openLayersTree(page); - await expect(layersTree).toBeEmpty(); + const layerTree = await openLayerTree(page); + await expect(layerTree).toBeEmpty(); }); }); @@ -68,19 +68,19 @@ test.describe('#layersPanel', () => { }); test('should have layer panel with content', async ({ page }) => { - const layersTree = await openLayersTree(page); - await expect(layersTree).not.toBeEmpty(); + const layerTree = await openLayerTree(page); + await expect(layerTree).not.toBeEmpty(); }); test('should restore empty layer panel', async ({ page }) => { - const layersTree = await openLayersTree(page); + const layerTree = await openLayerTree(page); await page.activity.closeAll(); - await expect(layersTree).toBeEmpty(); + await expect(layerTree).toBeEmpty(); }); test('raster layer should have icons', async ({ page }) => { - const layersTree = await openLayersTree(page); - const layerIcons = layersTree.locator( + const layerTree = await openLayerTree(page); + const layerIcons = layerTree.locator( '.jp-gis-layer .jp-gis-layerIcon svg' ); @@ -90,12 +90,12 @@ test.describe('#layersPanel', () => { }); test('should navigate in nested groups', async ({ page }) => { - const layersTree = await openLayersTree(page); - const layerEntries = layersTree.locator('.jp-gis-layerItem'); + const layerTree = await openLayerTree(page); + const layerEntries = layerTree.locator('.jp-gis-layerItem'); await expect(layerEntries).toHaveCount(2); await expect(layerEntries.first()).toHaveClass(/jp-gis-layer/); - await expect(layerEntries.last()).toHaveClass(/jp-gis-layersGroup/); + await expect(layerEntries.last()).toHaveClass(/jp-gis-layerGroup/); // Open the first level group await layerEntries.last().click(); @@ -107,12 +107,12 @@ test.describe('#layersPanel', () => { }); test('clicking a layer should select it', async ({ page }) => { - const layersTree = await openLayersTree(page); - const layersGroup = layersTree.locator('.jp-gis-layersGroup'); - const layer = layersTree.locator('.jp-gis-layer'); + const layerTree = await openLayerTree(page); + const layerGroup = layerTree.locator('.jp-gis-layerGroup'); + const layer = layerTree.locator('.jp-gis-layer'); // Open the first level group - await layersGroup.last().click(); + await layerGroup.last().click(); await expect(layer.first()).not.toHaveClass(/jp-mod-selected/); expect(await layer.first().screenshot()).toMatchSnapshot( @@ -137,9 +137,9 @@ test.describe('#layersPanel', () => { }); test('should have visibility icon', async ({ page }) => { - const layersTree = await openLayersTree(page); - const hideLayerButton = layersTree.getByTitle('Hide layer'); - const showLayerButton = layersTree.getByTitle('Show layer'); + const layerTree = await openLayerTree(page); + const hideLayerButton = layerTree.getByTitle('Hide layer'); + const showLayerButton = layerTree.getByTitle('Show layer'); await expect(hideLayerButton).toHaveCount(1); await expect(showLayerButton).toHaveCount(0); @@ -163,17 +163,15 @@ test.describe('#layersPanel', () => { test('should hide the top layer', async ({ page }) => { const notHiddenScreenshot = 'top-layer-not-hidden.png'; - const layersTree = await openLayersTree(page); - const layersGroup = layersTree.locator('.jp-gis-layersGroup'); + const layerTree = await openLayerTree(page); + const layerGroup = layerTree.locator('.jp-gis-layerGroup'); const main = page.locator('.jGIS-Mainview'); // Open the first level group - await layersGroup.last().click(); - await page.waitForCondition( - async () => (await layersGroup.count()) === 2 - ); + await layerGroup.last().click(); + await page.waitForCondition(async () => (await layerGroup.count()) === 2); // Open the second level group - await layersGroup.last().click(); + await layerGroup.last().click(); // Wait for the layer to be hidden. expect(await main.screenshot()).toMatchSnapshot({ @@ -181,7 +179,7 @@ test.describe('#layersPanel', () => { maxDiffPixelRatio: 0.01 }); - const hideLayerButton = layersTree.getByTitle('Hide layer'); + const hideLayerButton = layerTree.getByTitle('Hide layer'); // Hide the last layer (top in z-index). await hideLayerButton.last().click(); @@ -205,7 +203,7 @@ test.describe('#layersPanel', () => { }); // Restore the visibility of the layer. - const showLayerButton = layersTree.getByTitle('Show layer'); + const showLayerButton = layerTree.getByTitle('Show layer'); await showLayerButton.last().click(); }); });