diff --git a/examples/buildings.jGIS b/examples/buildings.jGIS new file mode 100644 index 00000000..e6aed93d --- /dev/null +++ b/examples/buildings.jGIS @@ -0,0 +1,56 @@ +{ + "layers": { + "148f2fb3-3077-4dcb-8d70-831570d5021f": { + "type": "VectorLayer", + "name": "Vector Tile Source Layer", + "parameters": { + "opacity": 1.0, + "source": "7a7ee6fd-c1e2-4c5d-a4e2-a7974db138a4", + "sourceLayer": "bingmlbuildings", + "color": "green", + "type": "fill" + }, + "visible": true + }, + "f99eb7b0-5e38-4078-b310-36a0746472aa": { + "parameters": { + "source": "ed8628b0-3e0a-45d5-9cd0-65e2a7dd61f5" + }, + "visible": true, + "type": "RasterLayer", + "name": "OpenStreetMap.Mapnik Layer" + } + }, + "sources": { + "7a7ee6fd-c1e2-4c5d-a4e2-a7974db138a4": { + "name": "Vector Tile Source", + "parameters": { + "minZoom": 13.0, + "url": "https://planetarycomputer.microsoft.com/api/data/v1/vector/collections/ms-buildings/tilesets/global-footprints/tiles/{z}/{x}/{y}", + "maxZoom": 13.0 + }, + "type": "VectorTileSource" + }, + "ed8628b0-3e0a-45d5-9cd0-65e2a7dd61f5": { + "parameters": { + "provider": "OpenStreetMap", + "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + "maxZoom": 19.0, + "attribution": "(C) OpenStreetMap contributors", + "minZoom": 0.0, + "urlParameters": {} + }, + "type": "RasterSource", + "name": "OpenStreetMap.Mapnik" + } + }, + "options": { + "longitude": -88.1392955068439, + "zoom": 13.915138763623208, + "latitude": 41.932061631424034 + }, + "layerTree": [ + "f99eb7b0-5e38-4078-b310-36a0746472aa", + "148f2fb3-3077-4dcb-8d70-831570d5021f" + ] +} \ No newline at end of file diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 9cfc3b85..1f333c70 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -37,6 +37,9 @@ export namespace CommandIDs { export const newGeoJSONLayer = 'jupytergis:newGeoJSONLayer'; export const newGeoJSONSource = 'jupytergis:newGeoJSONSource'; + + export const newVectorTileLayer = 'jupytergis:newVectorTileLayer'; + export const newVectorLayer = 'jupytergis:newVectorLayer'; } @@ -125,6 +128,17 @@ export function addCommands( execute: Private.createVectorLayer(tracker) }); + commands.addCommand(CommandIDs.newVectorTileLayer, { + label: trans.__('New vector tile layer'), + isEnabled: () => { + return tracker.currentWidget + ? tracker.currentWidget.context.model.sharedModel.editable + : false; + }, + iconClass: 'fa fa-vector-square', + execute: Private.createVectorTileLayer(tracker) + }); + commands.addCommand(CommandIDs.newGeoJSONSource, { label: trans.__('Add GeoJSON data from file'), isEnabled: () => { @@ -200,6 +214,70 @@ namespace Private { }; } + export function createVectorTileLayer( + tracker: WidgetTracker + ) { + return async (args: any) => { + const current = tracker.currentWidget; + + if (!current) { + return; + } + + const form = { + title: 'Vector Tile Layer parameters', + default: (model: IJupyterGISModel) => { + return { + name: 'Vector Tile Source', + maxZoom: 24, + minZoom: 0 + }; + } + }; + + const dialog = new FormDialog({ + context: current.context, + title: form.title, + sourceData: form.default(current.context.model), + schema: FORM_SCHEMA['VectorTileSource'], + syncData: (props: IDict) => { + const sharedModel = current.context.model.sharedModel; + if (!sharedModel) { + return; + } + + const { name, ...parameters } = props; + + const sourceId = UUID.uuid4(); + + const sourceModel: IJGISSource = { + type: 'VectorTileSource', + name, + parameters: { + url: parameters.url, + minZoom: parameters.minZoom, + maxZoom: parameters.maxZoom + } + }; + + const layerModel: IJGISLayer = { + type: 'VectorLayer', + parameters: { + type: 'line', + source: sourceId + }, + visible: true, + name: name + ' Layer' + }; + + sharedModel.addSource(sourceId, sourceModel); + current.context.model.addLayer(UUID.uuid4(), layerModel); + } + }); + await dialog.launch(); + }; + } + /** * Command to create a GeoJSON source. * diff --git a/packages/base/src/formdialog.tsx b/packages/base/src/formdialog.tsx index 73e555c0..2003228c 100644 --- a/packages/base/src/formdialog.tsx +++ b/packages/base/src/formdialog.tsx @@ -16,7 +16,6 @@ export interface IFormDialogOptions { context: DocumentRegistry.IContext; } -// TODO This is currently not used, shall we remove it or will we need it later? export class FormDialog extends Dialog { constructor(options: IFormDialogOptions) { const filePath = options.context.path; diff --git a/packages/base/src/mainview/mainview.tsx b/packages/base/src/mainview/mainview.tsx index 47424efb..f4d6126d 100644 --- a/packages/base/src/mainview/mainview.tsx +++ b/packages/base/src/mainview/mainview.tsx @@ -10,7 +10,9 @@ import { IJupyterGISDoc, IJupyterGISModel, IRasterSource, - JupyterGISModel + JupyterGISModel, + IVectorLayer, + IVectorTileSource } from '@jupytergis/schema'; import { showErrorMessage } from '@jupyterlab/apputils'; import { IObservableMap, ObservableMap } from '@jupyterlab/observables'; @@ -163,6 +165,20 @@ export class MainView extends React.Component { } break; } + case 'VectorTileSource': { + const mapSource = this._Map.getSource(id) as MapLibre.VectorTileSource; + if (!mapSource) { + const parameters = source.parameters as IVectorTileSource; + this._Map.addSource(id, { + type: 'vector', + minzoom: parameters.minZoom, + maxzoom: parameters.maxZoom, + attribution: parameters.attribution || '', + tiles: [this.computeSourceUrl(source)] + }); + } + break; + } case 'GeoJSONSource': { const mapSource = this._Map.getSource(id) as MapLibre.GeoJSONSource; if (!mapSource) { @@ -217,6 +233,12 @@ export class MainView extends React.Component { ]); break; } + case 'VectorTileSource': { + (mapSource as MapLibre.RasterTileSource).setTiles([ + this.computeSourceUrl(source) + ]); + break; + } case 'GeoJSONSource': { const data = source.parameters?.data || @@ -351,37 +373,29 @@ export class MainView extends React.Component { break; } case 'VectorLayer': { - const vectorLayerType = layer.parameters?.type; - if (!vectorLayerType) { - showErrorMessage( - 'Vector layer error', - 'The vector layer type is undefined' - ); - } - this._Map.addLayer( - { - id: id, - type: vectorLayerType, - layout: { - visibility: layer.visible ? 'visible' : 'none' - }, - source: sourceId, - minzoom: source.parameters?.minZoom || 0, - maxzoom: source.parameters?.maxZoom || 24 + const parameters = layer.parameters as IVectorLayer; + const layerSpecification: MapLibre.AddLayerObject = { + id, + type: parameters.type, + layout: { + visibility: layer.visible ? 'visible' : 'none' }, - beforeId - ); + source: sourceId + }; + + parameters.sourceLayer && + (layerSpecification['source-layer'] = parameters.sourceLayer); + + this._Map.addLayer(layerSpecification, beforeId); this._Map.setPaintProperty( id, - `${vectorLayerType}-color`, - layer.parameters?.color !== undefined - ? layer.parameters.color - : '#FF0000' + `${parameters.type}-color`, + parameters.color !== undefined ? parameters.color : '#FF0000' ); this._Map.setPaintProperty( id, - `${vectorLayerType}-opacity`, - layer.parameters?.opacity !== undefined ? layer.parameters.opacity : 1 + `${parameters.type}-opacity`, + parameters.opacity !== undefined ? parameters.opacity : 1 ); break; } diff --git a/packages/base/src/panelview/objectproperties.tsx b/packages/base/src/panelview/objectproperties.tsx index c9b55301..ce4f5cc9 100644 --- a/packages/base/src/panelview/objectproperties.tsx +++ b/packages/base/src/panelview/objectproperties.tsx @@ -237,11 +237,21 @@ class ObjectPropertiesReact extends React.Component { let LayerForm = LayerPropertiesForm; let SourceForm = ObjectPropertiesForm; - if (selectedObjSource.type === 'RasterSource') { - SourceForm = RasterSourcePropertiesForm; - } else if (selectedObjSource.type === 'GeoJSONSource') { - LayerForm = VectorLayerPropertiesForm; - SourceForm = GeoJSONSourcePropertiesForm; + switch (selectedObj.type) { + case 'VectorLayer': + LayerForm = VectorLayerPropertiesForm; + break; + // ADD MORE FORM TYPES HERE + } + + switch (selectedObjSource.type) { + case 'GeoJSONSource': + SourceForm = GeoJSONSourcePropertiesForm; + break; + case 'RasterSource': + SourceForm = RasterSourcePropertiesForm; + break; + // ADD MORE FORM TYPES HERE } return ( diff --git a/packages/base/src/toolbar/widget.tsx b/packages/base/src/toolbar/widget.tsx index dc685b8c..2f0f4836 100644 --- a/packages/base/src/toolbar/widget.tsx +++ b/packages/base/src/toolbar/widget.tsx @@ -55,6 +55,15 @@ export class ToolbarWidget extends Toolbar { this.addItem('separator1', new Separator()); + this.addItem( + 'newVectorTileLayer', + new CommandToolbarButton({ + id: CommandIDs.newVectorTileLayer, + label: '', + commands: options.commands + }) + ); + this.addItem( 'openLayerBrowser', new CommandToolbarButton({ diff --git a/packages/schema/src/schema/jgis.json b/packages/schema/src/schema/jgis.json index 0db31091..ec2b8f9e 100644 --- a/packages/schema/src/schema/jgis.json +++ b/packages/schema/src/schema/jgis.json @@ -24,7 +24,7 @@ }, "sourceType": { "type": "string", - "enum": ["RasterSource", "GeoJSONSource"] + "enum": ["RasterSource", "VectorTileSource", "GeoJSONSource"] }, "jGISLayer": { "title": "IJGISLayer", diff --git a/packages/schema/src/schema/vectorLayer.json b/packages/schema/src/schema/vectorlayer.json similarity index 81% rename from packages/schema/src/schema/vectorLayer.json rename to packages/schema/src/schema/vectorlayer.json index a3f6ca27..4d178061 100644 --- a/packages/schema/src/schema/vectorLayer.json +++ b/packages/schema/src/schema/vectorlayer.json @@ -2,7 +2,7 @@ "type": "object", "description": "VectorLayer", "title": "IVectorLayer", - "required": ["source"], + "required": ["source", "type"], "additionalProperties": false, "properties": { "source": { @@ -12,8 +12,13 @@ "type": { "type": "string", "enum": ["circle", "fill", "line"], + "default": "line", "description": "The type of vector layer" }, + "sourceLayer": { + "type": "string", + "description": "The source layer to use" + }, "color": { "type": "string", "description": "The color of the the object", diff --git a/packages/schema/src/schema/vectortilesource.json b/packages/schema/src/schema/vectortilesource.json new file mode 100644 index 00000000..469b4b3d --- /dev/null +++ b/packages/schema/src/schema/vectortilesource.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "VectorTileSource", + "title": "IVectorTileSource", + "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 vector source" + }, + "minZoom": { + "type": "number", + "minimum": 0, + "maximum": 24, + "description": "The minimum zoom level for the vector source" + }, + "attribution": { + "type": "string", + "description": "The attribution for the vector source" + }, + "provider": { + "type": "string", + "readOnly": true, + "description": "The map provider" + }, + "urlParameters": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } +} diff --git a/packages/schema/src/types.ts b/packages/schema/src/types.ts index d3ebc8bc..f70e7dbd 100644 --- a/packages/schema/src/types.ts +++ b/packages/schema/src/types.ts @@ -1,6 +1,9 @@ export * from './_interface/jgis'; export * from './_interface/rasterlayer'; +export * from './_interface/vectorlayer'; export * from './_interface/rastersource'; +export * from './_interface/vectortilesource'; +export * from './_interface/geojsonsource'; export * from './interfaces'; export * from './model'; export * from './doc'; diff --git a/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py b/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py index 807f56c3..5ad9ec79 100644 --- a/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py +++ b/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py @@ -20,6 +20,7 @@ SourceType, IRasterLayer, IRasterSource, + IVectorTileSource, IVectorLayer, IGeoJSONSource, ) @@ -35,7 +36,13 @@ class GISDocument(CommWidget): If not provided, a new empty document will be created. """ - def __init__(self, path: Optional[str] = None): + def __init__( + self, + path: Optional[str] = None, + latitude: Optional[number] = None, + longitude: Optional[number] = None, + zoom: Optional[number] = None + ): comm_metadata = GISDocument._path_to_comm(path) ydoc = Doc() @@ -50,6 +57,14 @@ def __init__(self, path: Optional[str] = None): self.ydoc["options"] = self._options = Map() self.ydoc["layerTree"] = self._layerTree = Array() + if path is None: + if latitude is not None: + self._options['latitude'] = latitude + if longitude is not None: + self._options['longitude'] = longitude + if zoom is not None: + self._options['zoom'] = zoom + @property def layers(self) -> Dict: """ @@ -105,10 +120,64 @@ def add_raster_layer( self._add_layer(OBJECT_FACTORY.create_layer(layer, self)) + def add_vectortile_layer( + self, + url: str, + name: str = "Vector Tile Layer", + source_layer: str | None = None, + attribution: str = "", + min_zoom: number = 0, + max_zoom: number = 24, + type: "circle" | "fill" | "line" = "line", + color: str = "#FF0000", + opacity: float = 1, + ): + """ + Add a Vector Tile Layer to the document. + + :param name: The name that will be used for the object in the document. + :param url: The tiles url. + :param source_layer: The source layer to use. + :param attribution: The attribution. + :param opacity: The opacity, between 0 and 1. + """ + source = { + "type": SourceType.VectorTileSource, + "name": f"{name} Source", + "parameters": { + "url": url, + "minZoom": min_zoom, + "maxZoom": max_zoom, + "attribution": attribution, + "htmlAttribution": attribution, + "provider": "", + "bounds": [], + "urlParameters": {}, + }, + } + + source_id = self._add_source(OBJECT_FACTORY.create_source(source, self)) + + layer = { + "type": LayerType.VectorLayer, + "name": name, + "visible": True, + "parameters": { + "source": source_id, + "type": type, + "opacity": opacity, + "sourceLayer": source_layer, + "color": color, + "opacity": opacity, + }, + } + + self._add_layer(OBJECT_FACTORY.create_layer(layer, self)) + def add_geojson_layer( self, - path: str = None, - data: Dict = None, + path: str | None = None, + data: Dict | None = None, name: str = "GeoJSON Layer", type: "circle" | "fill" | "line" = "line", color: str = "#FF0000", @@ -227,6 +296,7 @@ class Config: type: SourceType parameters: Union[ IRasterSource, + IVectorTileSource, IGeoJSONSource, ] _parent = Optional[GISDocument] @@ -301,5 +371,6 @@ def create_source( OBJECT_FACTORY.register_factory(LayerType.RasterLayer, IRasterLayer) OBJECT_FACTORY.register_factory(LayerType.VectorLayer, IVectorLayer) +OBJECT_FACTORY.register_factory(SourceType.VectorTileSource, IVectorTileSource) OBJECT_FACTORY.register_factory(SourceType.RasterSource, IRasterSource) OBJECT_FACTORY.register_factory(SourceType.GeoJSONSource, IGeoJSONSource) diff --git a/python/jupytergis_lab/jupytergis_lab/notebook/objects/__init__.py b/python/jupytergis_lab/jupytergis_lab/notebook/objects/__init__.py index 2d6d1333..2f24068a 100644 --- a/python/jupytergis_lab/jupytergis_lab/notebook/objects/__init__.py +++ b/python/jupytergis_lab/jupytergis_lab/notebook/objects/__init__.py @@ -1,7 +1,8 @@ from ._schema.jgis import * # noqa from ._schema.rasterlayer import IRasterLayer # noqa -from ._schema.vectorLayer import IVectorLayer # noqa +from ._schema.vectorlayer import IVectorLayer # noqa +from ._schema.vectortilesource import IVectorTileSource # noqa from ._schema.rastersource import IRasterSource # noqa from ._schema.geojsonsource import IGeoJSONSource # noqa diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-1-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-1-linux.png new file mode 100644 index 00000000..9ff1381d Binary files /dev/null and b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-1-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-2-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-2-linux.png new file mode 100644 index 00000000..26dac126 Binary files /dev/null and b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-2-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-1-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-1-linux.png new file mode 100644 index 00000000..c24543d0 Binary files /dev/null and b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-1-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-2-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-2-linux.png new file mode 100644 index 00000000..6b2a18bb Binary files /dev/null and b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-2-linux.png differ diff --git a/ui-tests/tests/notebooks/Notebook.ipynb b/ui-tests/tests/notebooks/Notebook.ipynb index 3585d206..0becef71 100644 --- a/ui-tests/tests/notebooks/Notebook.ipynb +++ b/ui-tests/tests/notebooks/Notebook.ipynb @@ -20,6 +20,51 @@ "\n", "doc" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b65dce55-9364-479e-ac7b-384cda9f422e", + "metadata": {}, + "outputs": [], + "source": [ + "doc = GISDocument(latitude=40.775, longitude=-73.973, zoom=13)\n", + "\n", + "doc.add_raster_layer(\n", + " url=\"https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}\",\n", + " name=\"Google Satellite\",\n", + " attribution=\"Google\",\n", + " opacity=0.6\n", + ")\n", + "\n", + "doc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4a2ebd4-3d48-4aa7-8999-6684fdae0812", + "metadata": {}, + "outputs": [], + "source": [ + "doc = GISDocument(latitude=40.775, longitude=-73.973, zoom=13)\n", + "\n", + "doc.add_raster_layer(\n", + " url=\"https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}\",\n", + " name=\"Google Satellite\",\n", + " attribution=\"Google\",\n", + " opacity=0.6\n", + ")\n", + "\n", + "doc.add_vectortile_layer(\n", + " url=\"https://planetarycomputer.microsoft.com/api/data/v1/vector/collections/ms-buildings/tilesets/global-footprints/tiles/{z}/{x}/{y}\",\n", + " source_layer=\"bingmlbuildings\",\n", + " max_zoom=13,\n", + " min_zoom=13\n", + ")\n", + "\n", + "doc" + ] } ], "metadata": {