diff --git a/examples/test.jGIS b/examples/test.jGIS index 9a33d4ff6..4c8096df5 100644 --- a/examples/test.jGIS +++ b/examples/test.jGIS @@ -1,4 +1,5 @@ { - "layers": [], + "layers": {}, + "sources": {}, "options": {} -} \ No newline at end of file +} diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index fabe28aa7..9a6ace866 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -1,41 +1,14 @@ import { JupyterFrontEnd } from '@jupyterlab/application'; -import { WidgetTracker, showErrorMessage } from '@jupyterlab/apputils'; +import { WidgetTracker } from '@jupyterlab/apputils'; import { ITranslator } from '@jupyterlab/translation'; import { redoIcon, undoIcon } from '@jupyterlab/ui-components'; import { JupyterGISWidget } from './widget'; -import { IDict, IJGISFormSchemaRegistry, IJGISLayer, IJupyterGISModel, LayerType } from '@jupytergis/schema'; +import { IDict, IJGISFormSchemaRegistry, IJGISLayer, IJGISSource, IJupyterGISModel } from '@jupytergis/schema'; import { FormDialog } from './formdialog'; +import { UUID } from '@lumino/coreutils'; -export function newName(type: string, model: IJupyterGISModel): string { - const sharedModel = model.sharedModel; - - let n = 1; - let name = `${type} 1`; - while (sharedModel.layerExists(name)) { - name = `${type} ${++n}`; - } - - return name; -} - - -const LAYERS = { - TileLayer: { - title: 'Tile Layer parameters', - layerType: 'TileLayer', - default: (model: IJupyterGISModel) => { - return { - name: newName('TileLayer', model), - url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", - maxZoom: 24, - minZoom: 0 - }; - } - }, -}; - /** * Add the commands to the application's command registry. */ @@ -82,7 +55,7 @@ export function addCommands( icon: undoIcon }); - commands.addCommand(CommandIDs.newTileLayer, { + commands.addCommand(CommandIDs.newRasterLayer, { label: trans.__('New Tile Layer'), isEnabled: () => { return tracker.currentWidget @@ -90,7 +63,7 @@ export function addCommands( : false; }, iconClass: 'fa fa-map', - execute: Private.createLayer('TileLayer', tracker) + execute: Private.createRasterSourceAndLayer(tracker) }); } @@ -101,7 +74,7 @@ export namespace CommandIDs { export const redo = 'jupytergis:redo'; export const undo = 'jupytergis:undo'; - export const newTileLayer = 'jupytergis:newTileLayer'; + export const newRasterLayer = 'jupytergis:newRasterLayer'; } @@ -119,14 +92,16 @@ namespace Private { const value = (FORM_SCHEMA[key] = JSON.parse(JSON.stringify(val))); value['required'] = ['name', ...value['required']]; value['properties'] = { - name: { type: 'string', description: 'The name of the layer' }, + name: { type: 'string', description: 'The name of the layer/source' }, ...value['properties'] }; }); } - export function createLayer( - layer: keyof typeof LAYERS, + // TODO Allow for creating only a source (e.g. loading a CSV file) + // TODO Allow for creating only a layer (e.g. creating a vector layer given a source selected from a dropdown) + + export function createRasterSourceAndLayer( tracker: WidgetTracker ) { return async (args: any) => { @@ -136,9 +111,19 @@ namespace Private { return; } - const value = LAYERS[layer]; + const form = { + title: 'Raster Layer parameters', + default: (model: IJupyterGISModel) => { + return { + name: 'RasterSource', + url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + maxZoom: 24, + minZoom: 0 + }; + } + }; - current.context.model.syncFormData(value); + current.context.model.syncFormData(form); const syncSelectedField = ( id: string | null, @@ -159,29 +144,40 @@ namespace Private { const dialog = new FormDialog({ context: current.context, - title: value.title, - sourceData: value.default(current.context.model), - schema: FORM_SCHEMA[value.layerType], + title: form.title, + sourceData: form.default(current.context.model), + schema: FORM_SCHEMA["RasterSource"], syncData: (props: IDict) => { + const sharedModel = current.context.model.sharedModel; + if (!sharedModel) { + return; + } + const { name, ...parameters } = props; + + const sourceId = UUID.uuid4(); + + const sourceModel: IJGISSource = { + type: "RasterSource", + name, + parameters: { + url: parameters.url, + minZoom: parameters.minZoom, + maxZoom: parameters.maxZoom + } + }; + const layerModel: IJGISLayer = { - layerType: value.layerType as LayerType, - parameters, + type: "RasterLayer", + parameters: { + source: sourceId + }, visible: true, - name: name + name: name + " Layer" }; - const sharedModel = current.context.model.sharedModel; - if (sharedModel) { - if (!sharedModel.layerExists(layerModel.name)) { - sharedModel.addLayer(layerModel); - } else { - showErrorMessage( - 'The object already exists', - 'There is an existing object with the same name.' - ); - } - } + sharedModel.addSource(sourceId, sourceModel) + sharedModel.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 e570d702e..0b3e86141 100644 --- a/packages/base/src/mainview/mainview.tsx +++ b/packages/base/src/mainview/mainview.tsx @@ -4,7 +4,7 @@ import { IJupyterGISClientState, IJupyterGISDoc, IJupyterGISModel, - ITileLayer, + IRasterSource, } from '@jupytergis/schema'; import { IObservableMap, ObservableMap } from '@jupyterlab/observables'; import { User } from '@jupyterlab/services'; @@ -131,26 +131,53 @@ export class MainView extends React.Component { // console.log('new change', change); // }) - for (const layer of this._model.sharedModel.layers) { + for (const layerId of Object.keys(this._model.sharedModel.layers)) { + const layer = this._model.sharedModel.getLayer(layerId); + + if (!layer) { + console.log(`Layer id ${layerId} does not exist`); + continue; + } + switch(layer.type) { case 'RasterLayer': - const tileLayerParameters = layer.parameters as ITileLayer; - this._Map.addSource('raster-source', { - type: 'raster', - tiles: [tileLayerParameters.url], - tileSize: 256, - }); + const sourceId = layer.parameters?.source; + const source = this.getSource(sourceId); + + if (!source) { + continue; + } + + if (!this._Map.getSource(sourceId)) { + this._Map.addSource(sourceId, { + type: 'raster', + tiles: [source.url], + tileSize: 256, + }); + } + this._Map.addLayer({ - id: 'simple-tiles', + id: layerId, type: 'raster', - source: 'raster-source', - minzoom: tileLayerParameters.minZoom, - maxzoom: tileLayerParameters.maxZoom, + source: sourceId, + minzoom: source.minZoom, + maxzoom: source.maxZoom, }); } } } + private getSource(id: string): T | undefined { + const source = this._model.sharedModel.getSource(id); + + if (source) { + console.log(`Source id ${id} does not exist`); + return; + } + + return source as T; + } + private _handleThemeChange = (): void => { const lightTheme = isLightTheme(); diff --git a/packages/base/src/panelview/objectproperties.tsx b/packages/base/src/panelview/objectproperties.tsx index e05e57bce..85a0a13c8 100644 --- a/packages/base/src/panelview/objectproperties.tsx +++ b/packages/base/src/panelview/objectproperties.tsx @@ -1,7 +1,6 @@ import { IDict, IJGISFormSchemaRegistry, - IJGISLayers, IJGISLayerDocChange, IJupyterGISClientState, IJupyterGISDoc, @@ -16,7 +15,6 @@ import { v4 as uuid } from 'uuid'; import { focusInputField, - itemFromName, removeStyleFromProperty } from '../tools'; import { IControlPanelModel } from '../types'; @@ -42,7 +40,6 @@ export class ObjectProperties extends PanelWithToolbar { interface IStates { jGISOption?: IDict; filePath?: string; - jGISLayers?: IJGISLayers; selectedObjectData?: IDict; selectedObject?: string; schema?: IDict; @@ -62,7 +59,6 @@ class ObjectPropertiesReact extends React.Component { super(props); this.state = { filePath: this.props.cpModel.filePath, - jGISLayers: this.props.cpModel.jGISModel?.getLayers(), clientId: null, id: uuid() }; @@ -84,14 +80,12 @@ class ObjectPropertiesReact extends React.Component { this.setState(old => ({ ...old, filePath: changed.context.localPath, - jGISLayers: this.props.cpModel.jGISModel?.getLayers(), clientId: changed.context.model.getClientId() })); } else { this.setState({ jGISOption: undefined, filePath: undefined, - jGISLayers: undefined, selectedObjectData: undefined, selectedObject: undefined, schema: undefined @@ -100,11 +94,11 @@ class ObjectPropertiesReact extends React.Component { }); } - async syncLayerProperties( - objectName: string | undefined, + async syncObjectProperties( + id: string | undefined, properties: { [key: string]: any } ) { - if (!this.state.jGISLayers || !objectName) { + if (!id) { return; } @@ -119,24 +113,7 @@ class ObjectPropertiesReact extends React.Component { return; } - // getContent already returns a deep copy of the content, we can change it safely here - const updatedContent = model.getContent(); - for (const object of updatedContent.layers) { - if (object.name === objectName) { - object.parameters = { - ...object.parameters, - ...properties - }; - } - } - - const obj = model.sharedModel.getLayerByName(objectName); - if (obj) { - model.sharedModel.updateLayerByName(objectName, 'parameters', { - ...obj['parameters'], - ...properties - }); - } + model.sharedModel.updateObjectParameters(id, properties); } syncSelectedField = ( @@ -162,26 +139,18 @@ class ObjectPropertiesReact extends React.Component { ): void => { this.setState(old => { if (old.selectedObject) { - const jGISLayers = this.props.cpModel.jGISModel?.getLayers(); - if (jGISLayers) { - const selectedObj = itemFromName(old.selectedObject, jGISLayers); - if (!selectedObj) { - return old; - } - const selectedObjectData = selectedObj['parameters']; + const selectedObject = this.props.cpModel.jGISModel?.sharedModel.getObject(old.selectedObject); + if (selectedObject) { + const selectedObjectData = selectedObject.parameters; return { ...old, - jGISLayers: jGISLayers, selectedObjectData }; } else { return old; } } else { - return { - ...old, - jGISLayers: this.props.cpModel.jGISModel?.getLayers() - }; + return old; } }); }; @@ -228,7 +197,7 @@ class ObjectPropertiesReact extends React.Component { schema={this.state.schema} sourceData={this.state.selectedObjectData} syncData={(properties: { [key: string]: any }) => { - this.syncLayerProperties(this.state.selectedObject, properties); + this.syncObjectProperties(this.state.selectedObject, properties); }} syncSelectedField={this.syncSelectedField} /> diff --git a/packages/base/src/toolbar/widget.tsx b/packages/base/src/toolbar/widget.tsx index 20f136654..4e6329250 100644 --- a/packages/base/src/toolbar/widget.tsx +++ b/packages/base/src/toolbar/widget.tsx @@ -55,9 +55,9 @@ export class ToolbarWidget extends Toolbar { this.addItem('separator1', new Separator()); this.addItem( - 'newTileLayer', + 'newRasterLayer', new CommandToolbarButton({ - id: CommandIDs.newTileLayer, + id: CommandIDs.newRasterLayer, label: '', commands: options.commands }) diff --git a/packages/base/src/tools.ts b/packages/base/src/tools.ts index 505d2fad8..7616990a7 100644 --- a/packages/base/src/tools.ts +++ b/packages/base/src/tools.ts @@ -37,18 +37,6 @@ export function throttle void>( } as T; } -export function itemFromName( - name: string, - arr: T[] -): T | undefined { - for (const it of arr) { - if (it.name === name) { - return it; - } - } - return undefined; -} - export function focusInputField( filePath?: string, fieldId?: string | null, diff --git a/packages/schema/src/doc.ts b/packages/schema/src/doc.ts index 8c8931fbf..fb2188b83 100644 --- a/packages/schema/src/doc.ts +++ b/packages/schema/src/doc.ts @@ -118,6 +118,40 @@ export class JupyterGISDoc this.transact(() => obj.set(id, value)); } + getObject(id: string): IJGISLayer | IJGISSource | undefined { + const layer = this.getLayer(id); + if (layer) { + return layer; + } + + const source = this.getSource(id); + if (source) { + return source; + } + } + + updateObjectParameters(id: string, value: IJGISLayer['parameters'] | IJGISSource['parameters']) { + const layer = this.getLayer(id); + if (layer) { + layer.parameters = { + ...layer.parameters, + ...value + }; + + this.updateLayer(id, layer); + } + + const source = this.getSource(id); + if (source) { + source.parameters = { + ...source.parameters, + ...value + }; + + this.updateSource(id, source); + } + } + sourceExists(id: string): boolean { return Boolean(this._getSourceAsYMap(id)); } diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index e3e6f6894..df710c289 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -69,6 +69,9 @@ export interface IJupyterGISDoc extends YDocument { addSource(id: string, value: IJGISSource): void; updateSource(id: string, value: IJGISSource): void; + updateObjectParameters(id: string, value: IJGISLayer['parameters'] | IJGISSource['parameters']): void; + getObject(id: string): IJGISLayer | IJGISSource | undefined; + getOption(key: keyof IJGISOptions): IDict | undefined; setOption(key: keyof IJGISOptions, value: IDict): void; @@ -107,6 +110,9 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { getContent(): IJGISContent; getLayers(): IJGISLayers; + getLayer(id: string): IJGISLayer | undefined; + getSources(): IJGISSources; + getSource(id: string): IJGISSource | undefined; syncSelectedPropField(data: { id: string | null; diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index 3c216f866..4c416a641 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -5,7 +5,7 @@ import { PartialJSONObject } from '@lumino/coreutils'; import { ISignal, Signal } from '@lumino/signaling'; import Ajv from 'ajv'; -import { IJGISContent, IJGISLayers } from './_interface/jgis'; +import { IJGISContent, IJGISLayer, IJGISLayers, IJGISSource, IJGISSources } from './_interface/jgis'; import { JupyterGISDoc } from './doc'; import { IJGISLayerDocChange, @@ -174,6 +174,18 @@ export class JupyterGISModel implements IJupyterGISModel { return this.sharedModel.layers; } + getSources(): IJGISSources { + return this.sharedModel.sources; + } + + getLayer(id: string): IJGISLayer | undefined { + return this.sharedModel.getLayer(id); + } + + getSource(id: string): IJGISSource | undefined { + return this.sharedModel.getSource(id); + } + syncSelectedPropField(data: { id: string | null; value: any; diff --git a/packages/schema/src/schema/rasterlayer.json b/packages/schema/src/schema/rasterlayer.json index 3d08c97c4..515157a50 100644 --- a/packages/schema/src/schema/rasterlayer.json +++ b/packages/schema/src/schema/rasterlayer.json @@ -2,24 +2,12 @@ "type": "object", "description": "RasterLayer", "title": "IRasterLayer", - "required": ["source", "maxZoom", "minZoom"], + "required": ["source"], "additionalProperties": false, "properties": { "source": { "type": "string", "description": "The id of the source" - }, - "maxZoom": { - "type": "number", - "minimum": 0, - "maximum": 24, - "description": "The maximum zoom level for the tile layer" - }, - "minZoom": { - "type": "number", - "minimum": 0, - "maximum": 24, - "description": "The minimum zoom level for the tile layer" } } } diff --git a/packages/schema/src/schema/rastersource.json b/packages/schema/src/schema/rastersource.json index 69d3f634f..918535821 100644 --- a/packages/schema/src/schema/rastersource.json +++ b/packages/schema/src/schema/rastersource.json @@ -2,12 +2,24 @@ "type": "object", "description": "RasterSource", "title": "IRasterSource", - "required": ["url"], + "required": ["url", "maxZoom", "minZoom"], "additionalProperties": false, "properties": { "url": { "type": "string", "description": "The url to the tile provider" + }, + "maxZoom": { + "type": "number", + "minimum": 0, + "maximum": 24, + "description": "The maximum zoom level for the raster source" + }, + "minZoom": { + "type": "number", + "minimum": 0, + "maximum": 24, + "description": "The minimum zoom level for the raster source" } } } diff --git a/python/jupytergis_core/src/jgisplugin/plugins.ts b/python/jupytergis_core/src/jgisplugin/plugins.ts index 782733916..c96c1b778 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"options": {}\n}' + '{\n\t"layers": {},\n\t"sources": {},\n\t"options": {}\n}' }); // Open the newly created file with the 'Editor'