diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 6f1add0f2..647e1c4a4 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -87,7 +87,8 @@ export function addCommands( const isValidLayer = [ 'VectorLayer', 'VectorTileLayer', - 'WebGlLayer' + 'WebGlLayer', + 'HeatmapLayer' ].includes(layer.type); return isValidLayer; @@ -665,6 +666,29 @@ export function addCommands( ...icons.get(CommandIDs.newShapefileLayer) }); + commands.addCommand(CommandIDs.newHeatmapLayer, { + label: args => + args.from === 'contextMenu' + ? trans.__('Heatmap') + : trans.__('Add HeatmapLayer'), + isEnabled: () => { + return tracker.currentWidget + ? tracker.currentWidget.context.model.sharedModel.editable + : false; + }, + execute: Private.createEntry({ + tracker, + formSchemaRegistry, + title: 'Create Heatmap Layer', + createLayer: true, + createSource: false, + layerData: { name: 'Custom Heatmap Layer' }, + sourceType: 'GeoJSONSource', + layerType: 'HeatmapLayer' + }), + ...icons.get(CommandIDs.newHeatmapLayer) + }); + /** * LAYERS and LAYER GROUP actions. */ diff --git a/packages/base/src/constants.ts b/packages/base/src/constants.ts index 9b8831c45..152e639da 100644 --- a/packages/base/src/constants.ts +++ b/packages/base/src/constants.ts @@ -41,6 +41,7 @@ export namespace CommandIDs { export const newVideoLayer = 'jupytergis:newVideoLayer'; export const newShapefileLayer = 'jupytergis:newShapefileLayer'; export const newWebGlTileLayer = 'jupytergis:newWebGlTileLayer'; + export const newHeatmapLayer = 'jupytergis:newHeatmapLayer'; // Layer and group actions export const renameLayer = 'jupytergis:renameLayer'; diff --git a/packages/base/src/dialogs/symbology/symbologyDialog.tsx b/packages/base/src/dialogs/symbology/symbologyDialog.tsx index 0dac696f7..3e64a6152 100644 --- a/packages/base/src/dialogs/symbology/symbologyDialog.tsx +++ b/packages/base/src/dialogs/symbology/symbologyDialog.tsx @@ -75,6 +75,7 @@ const SymbologyDialog = ({ switch (layer.type) { case 'VectorLayer': case 'VectorTileLayer': + case 'HeatmapLayer': LayerSymbology = ( { - const [selectedRenderType, setSelectedRenderType] = useState('Single Symbol'); + const [selectedRenderType, setSelectedRenderType] = useState(''); const [componentToRender, setComponentToRender] = useState(null); const [renderTypeOptions, setRenderTypeOptions] = useState([ 'Single Symbol' @@ -28,13 +29,14 @@ const VectorRendering = ({ } useEffect(() => { - const renderType = layer.parameters?.symbologyState?.renderType; - setSelectedRenderType(renderType ?? 'Single Symbol'); - - if (layer.type === 'VectorLayer') { - const options = ['Single Symbol', 'Graduated', 'Categorized']; - setRenderTypeOptions(options); + let renderType = layer.parameters?.symbologyState?.renderType; + if (!renderType) { + renderType = layer.type === 'HeatmapLayer' ? 'Heatmap' : 'Single Symbol'; } + setSelectedRenderType(renderType); + + const options = ['Single Symbol', 'Graduated', 'Categorized', 'Heatmap']; + setRenderTypeOptions(options); }, []); useEffect(() => { @@ -72,8 +74,19 @@ const VectorRendering = ({ /> ); break; + case 'Heatmap': + RenderComponent = ( + + ); + break; default: - RenderComponent =
Render Type Not Implemented (yet)
; + RenderComponent =
Select a render type
; } setComponentToRender(RenderComponent); }, [selectedRenderType]); diff --git a/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx index 2da454110..0836859c7 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx @@ -1,13 +1,13 @@ +import { IVectorLayer } from '@jupytergis/schema'; +import { ReadonlyJSONObject } from '@lumino/coreutils'; +import { ExpressionValue } from 'ol/expr/expression'; import React, { useEffect, useRef, useState } from 'react'; -import ValueSelect from '../components/ValueSelect'; -import { IStopRow, ISymbologyDialogProps } from '../../symbologyDialog'; -import { useGetProperties } from '../../hooks/useGetProperties'; +import ColorRamp from '../../components/color_ramp/ColorRamp'; import StopContainer from '../../components/color_stops/StopContainer'; +import { useGetProperties } from '../../hooks/useGetProperties'; +import { IStopRow, ISymbologyDialogProps } from '../../symbologyDialog'; import { Utils, VectorUtils } from '../../symbologyUtils'; -import ColorRamp from '../../components/color_ramp/ColorRamp'; -import { ReadonlyJSONObject } from '@lumino/coreutils'; -import { ExpressionValue } from 'ol/expr/expression'; -import { IVectorLayer } from '@jupytergis/schema'; +import ValueSelect from '../components/ValueSelect'; const Categorized = ({ context, @@ -125,6 +125,7 @@ const Categorized = ({ layer.parameters.symbologyState = symbologyState; layer.parameters.color = newStyle; + layer.type = 'VectorLayer'; context.model.sharedModel.updateLayer(layerId, layer); cancel(); diff --git a/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx index 6d38728b0..9d992dc15 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx @@ -154,6 +154,7 @@ const Graduated = ({ layer.parameters.symbologyState = symbologyState; layer.parameters.color = newStyle; + layer.type = 'VectorLayer'; context.model.sharedModel.updateLayer(layerId, layer); cancel(); diff --git a/packages/base/src/dialogs/symbology/vector_layer/types/Heatmap.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/Heatmap.tsx new file mode 100644 index 000000000..ceb166dde --- /dev/null +++ b/packages/base/src/dialogs/symbology/vector_layer/types/Heatmap.tsx @@ -0,0 +1,128 @@ +import colormap from 'colormap'; +import React, { useEffect, useRef, useState } from 'react'; +import CanvasSelectComponent from '../../components/color_ramp/CanvasSelectComponent'; +import { ISymbologyDialogProps } from '../../symbologyDialog'; + +const Heatmap = ({ + context, + state, + okSignalPromise, + cancel, + layerId +}: ISymbologyDialogProps) => { + if (!layerId) { + return; + } + const layer = context.model.getLayer(layerId); + if (!layer?.parameters) { + return; + } + const [selectedRamp, setSelectedRamp] = useState(''); + const [heatmapOptions, setHetamapOptions] = useState({ + radius: 8, + blur: 15 + }); + const selectedRampRef = useRef('cool'); + const heatmapOptionsRef = useRef({ + radius: 8, + blur: 15 + }); + + useEffect(() => { + populateOptions(); + + okSignalPromise.promise.then(okSignal => { + okSignal.connect(handleOk, this); + }); + + return () => { + okSignalPromise.promise.then(okSignal => { + okSignal.disconnect(handleOk, this); + }); + }; + }, []); + + useEffect(() => { + selectedRampRef.current = selectedRamp; + heatmapOptionsRef.current = heatmapOptions; + }, [selectedRamp, heatmapOptions]); + + const populateOptions = async () => { + let colorRamp; + + if (layer.parameters?.symbologyState) { + colorRamp = layer.parameters.symbologyState.colorRamp; + } + + setSelectedRamp(colorRamp ? colorRamp : 'cool'); + }; + + const handleOk = () => { + if (!layer.parameters) { + return; + } + + const colorMap = colormap({ + colormap: selectedRampRef.current, + nshades: 9, + format: 'hex' + }); + + const symbologyState = { + renderType: 'Heatmap', + colorRamp: selectedRampRef.current + }; + + layer.parameters.symbologyState = symbologyState; + layer.parameters.color = colorMap; + layer.parameters.blur = heatmapOptionsRef.current.blur; + layer.parameters.radius = heatmapOptionsRef.current.radius; + layer.type = 'HeatmapLayer'; + + context.model.sharedModel.updateLayer(layerId, layer); + + cancel(); + }; + + return ( +
+
+ + +
+
+ + + setHetamapOptions(prevState => ({ + ...prevState, + radius: +event.target.value + })) + } + /> +
+
+ + + setHetamapOptions(prevState => ({ + ...prevState, + blur: +event.target.value + })) + } + /> +
+
+ ); +}; + +export default Heatmap; diff --git a/packages/base/src/dialogs/symbology/vector_layer/types/SimpleSymbol.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/SimpleSymbol.tsx index 25b09aafe..5ec457691 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/types/SimpleSymbol.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/types/SimpleSymbol.tsx @@ -103,6 +103,7 @@ const SimpleSymbol = ({ layer.parameters.symbologyState = symbologyState; layer.parameters.color = styleExpr; + layer.type = 'VectorLayer'; context.model.sharedModel.updateLayer(layerId, layer); cancel(); diff --git a/packages/base/src/formbuilder/formselectors.ts b/packages/base/src/formbuilder/formselectors.ts index 27dec5956..f943511e6 100644 --- a/packages/base/src/formbuilder/formselectors.ts +++ b/packages/base/src/formbuilder/formselectors.ts @@ -1,13 +1,14 @@ import { LayerType, SourceType } from '@jupytergis/schema'; import { BaseForm } from './objectform/baseform'; import { GeoJSONSourcePropertiesForm } from './objectform/geojsonsource'; +import { GeoTiffSourcePropertiesForm } from './objectform/geotiffsource'; +import { HeatmapLayerPropertiesForm } from './objectform/heatmapLayerForm'; import { HillshadeLayerPropertiesForm } from './objectform/hillshadeLayerForm'; import { LayerPropertiesForm } from './objectform/layerform'; +import { PathBasedSourcePropertiesForm } from './objectform/pathbasedsource'; import { TileSourcePropertiesForm } from './objectform/tilesourceform'; import { VectorLayerPropertiesForm } from './objectform/vectorlayerform'; import { WebGlLayerPropertiesForm } from './objectform/webGlLayerForm'; -import { GeoTiffSourcePropertiesForm } from './objectform/geotiffsource'; -import { PathBasedSourcePropertiesForm } from './objectform/pathbasedsource'; export function getLayerTypeForm( layerType: LayerType @@ -25,6 +26,8 @@ export function getLayerTypeForm( case 'WebGlLayer': LayerForm = WebGlLayerPropertiesForm; break; + case 'HeatmapLayer': + LayerForm = HeatmapLayerPropertiesForm; // ADD MORE FORM TYPES HERE } diff --git a/packages/base/src/formbuilder/objectform/heatmapLayerForm.ts b/packages/base/src/formbuilder/objectform/heatmapLayerForm.ts new file mode 100644 index 000000000..a46bddd80 --- /dev/null +++ b/packages/base/src/formbuilder/objectform/heatmapLayerForm.ts @@ -0,0 +1,97 @@ +import { IDict, IGeoJSONSource, IHeatmapLayer } from '@jupytergis/schema'; +import { IChangeEvent } from '@rjsf/core'; +import { loadFile } from '../../tools'; +import { ILayerProps, LayerPropertiesForm } from './layerform'; + +export class HeatmapLayerPropertiesForm extends LayerPropertiesForm { + protected currentFormData: IHeatmapLayer; + private features: any = []; + + constructor(props: ILayerProps) { + super(props); + + this.fetchFeatureNames(this.props.sourceData as IHeatmapLayer); + + if (this.sourceFormChangedSignal) { + this.sourceFormChangedSignal.connect((sender, sourceData) => { + if (this.props.sourceType === 'GeoJSONSource') { + this.fetchFeatureNames( + this.currentFormData, + sourceData as IGeoJSONSource + ); + } + }); + } + } + + protected onFormChange(e: IChangeEvent): void { + super.onFormChange(e); + + const source = this.props.model.getSource(e.formData.source); + if (!source || source.type !== 'GeoJSONSource') { + return; + } + + this.fetchFeatureNames( + this.currentFormData, + source.parameters as IGeoJSONSource + ); + } + + protected processSchema( + data: IDict | undefined, + schema: IDict, + uiSchema: IDict + ) { + this.removeFormEntry('color', data, schema, uiSchema); + this.removeFormEntry('symbologyState', data, schema, uiSchema); + this.removeFormEntry('blur', data, schema, uiSchema); + this.removeFormEntry('radius', data, schema, uiSchema); + super.processSchema(data, schema, uiSchema); + + uiSchema['feature'] = { enum: this.features }; + + if (!data) { + return; + } + + if (this.features.length !== 0) { + schema.properties.feature.enum = this.features; + } + } + + private async fetchFeatureNames( + data: IHeatmapLayer, + sourceData?: IGeoJSONSource + ) { + if (data && data.source) { + if (!sourceData) { + const currentSource = this.props.model.getSource(data.source); + + if (!currentSource || currentSource.type !== 'GeoJSONSource') { + this.features = []; + return; + } + + sourceData = currentSource.parameters as IGeoJSONSource; + } + } + + const source = this.props.model.getSource(data.source); + + if (!source?.parameters?.path) { + return; + } + + const jsonData = await loadFile({ + filepath: source.parameters.path, + type: 'GeoJSONSource', + model: this.props.model + }); + + const featureProps = jsonData.features[0].properties; + + this.features = Object.keys(featureProps); + this.forceUpdate(); + } +} diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 617b8cf6b..f03c8bddb 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -3,6 +3,7 @@ import { IAnnotation, IDict, IGeoTiffSource, + IHeatmapLayer, IHillshadeLayer, IImageLayer, IImageSource, @@ -41,6 +42,7 @@ import { singleClick } from 'ol/events/condition'; import { GeoJSON, MVT } from 'ol/format'; import { DragAndDrop, Select } from 'ol/interaction'; import { + Heatmap as HeatmapLayer, Image as ImageLayer, Layer, Vector as VectorLayer, @@ -832,8 +834,6 @@ export class MainView extends React.Component { source: this._sources[layerParameters.source] }); - this.updateLayer(id, layer, newMapLayer); - break; } case 'HillshadeLayer': { @@ -875,6 +875,19 @@ export class MainView extends React.Component { newMapLayer = new WebGlTileLayer(layerOptions); break; } + case 'HeatmapLayer': { + layerParameters = layer.parameters as IHeatmapLayer; + + newMapLayer = new HeatmapLayer({ + opacity: layerParameters.opacity, + source: this._sources[layerParameters.source], + blur: layerParameters.blur, + radius: layerParameters.radius, + weight: layerParameters.feature, + gradient: layerParameters.color + }); + break; + } } await this._waitForSourceReady(newMapLayer); @@ -1070,6 +1083,7 @@ export class MainView extends React.Component { async updateLayer( id: string, layer: IJGISLayer, + oldLayer: IDict, mapLayer: Layer ): Promise { const sourceId = layer.parameters?.source; @@ -1128,6 +1142,23 @@ export class MainView extends React.Component { } break; } + case 'HeatmapLayer': { + const layerParams = layer.parameters as IHeatmapLayer; + const heatmap = mapLayer as HeatmapLayer; + + if (oldLayer.feature !== layerParams.feature) { + // No way to change 'weight' attribute (feature used for heatmap stuff) so need to replace layer + this.replaceLayer(id, layer); + return; + } + + heatmap.setOpacity(layerParams.opacity || 1); + heatmap.setBlur(layerParams.blur); + heatmap.setRadius(layerParams.radius); + heatmap.setGradient( + layerParams.color ?? ['#00f', '#0ff', '#0f0', '#ff0', '#f00'] + ); + } } } @@ -1391,6 +1422,17 @@ export class MainView extends React.Component { this._Map.getLayers().insertAt(nextIndex, layer); } + /** + * Remove and recreate layer + * @param id ID of layer being replaced + * @param layer New layer to replace with + */ + replaceLayer(id: string, layer: IJGISLayer) { + const layerIndex = this.getLayerIndex(id); + this.removeLayer(id); + this.addLayer(id, layer, layerIndex); + } + private _onLayersChanged( _: IJupyterGISDoc, change: IJGISLayerDocChange @@ -1400,20 +1442,31 @@ export class MainView extends React.Component { if (!this._ready) { return; } + change.layerChange?.forEach(change => { - const layer = change.newValue; - if (!layer || Object.keys(layer).length === 0) { - this.removeLayer(change.id); + const { id, oldValue: oldLayer, newValue: newLayer } = change; + + if (!newLayer || Object.keys(newLayer).length === 0) { + this.removeLayer(id); + return; + } + + if (oldLayer && oldLayer.type !== newLayer.type) { + this.replaceLayer(id, newLayer); + return; + } + + const mapLayer = this.getLayer(id); + const layerTree = JupyterGISModel.getOrderedLayerIds(this._model); + + if (!mapLayer) { + return; + } + + if (layerTree.includes(id)) { + this.updateLayer(id, newLayer, oldLayer, mapLayer); } else { - const mapLayer = this.getLayer(change.id); - const layerTree = JupyterGISModel.getOrderedLayerIds(this._model); - if (mapLayer) { - if (layerTree.includes(change.id)) { - this.updateLayer(change.id, layer, mapLayer); - } else { - this.updateLayers(layerTree); - } - } + this.updateLayers(layerTree); } }); } diff --git a/packages/base/style/symbologyDialog.css b/packages/base/style/symbologyDialog.css index bc9d86fb2..1f2de9113 100644 --- a/packages/base/style/symbologyDialog.css +++ b/packages/base/style/symbologyDialog.css @@ -159,7 +159,7 @@ select option { z-index: 40; flex: 1 1 auto; width: 97%; - max-height: 375px; + max-height: 20rem; background: var(--jp-input-background); padding-left: 8px; border: var(--jp-border-width) solid var(--jp-input-border-color); @@ -207,6 +207,15 @@ select option { width: 100%; height: 30px; visibility: initial; + border-radius: var(--jp-border-radius); +} + +.jp-Dialog-content:has(.jp-gis-color-canvas-display) { + overflow: visible; +} + +.jp-Dialog-body:has(.jp-gis-color-canvas-display) { + overflow: visible; } .jp-gis-canvas-button-wrapper { diff --git a/packages/schema/src/doc.ts b/packages/schema/src/doc.ts index 5798a9017..c8911a8a3 100644 --- a/packages/schema/src/doc.ts +++ b/packages/schema/src/doc.ts @@ -336,6 +336,7 @@ export class JupyterGISDoc private _layersObserver(events: Y.YEvent[]): void { const changes: Array<{ id: string; + oldValue: IDict; newValue: IJGISLayer; }> = []; let needEmit = false; @@ -346,6 +347,7 @@ export class JupyterGISDoc } changes.push({ id: key as string, + oldValue: change.oldValue, newValue: JSONExt.deepCopy(event.target.toJSON()[key]) }); }); diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index c92b91c27..6a560bc9d 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -7,11 +7,11 @@ import { } from '@jupyter/ydoc'; import { IWidgetTracker } from '@jupyterlab/apputils'; import { IChangedArgs } from '@jupyterlab/coreutils'; +import { IDocumentManager } from '@jupyterlab/docmanager'; import { DocumentRegistry, IDocumentWidget } from '@jupyterlab/docregistry'; import { Contents, User } from '@jupyterlab/services'; import { ISignal, Signal } from '@lumino/signaling'; import { SplitPanel } from '@lumino/widgets'; -import { IDocumentManager } from '@jupyterlab/docmanager'; import { IJGISContent, @@ -46,6 +46,7 @@ export interface IDict { export interface IJGISLayerDocChange { layerChange?: Array<{ id: string; + oldValue: IDict; newValue: IJGISLayer | undefined; }>; } diff --git a/packages/schema/src/schema/heatmapLayer.json b/packages/schema/src/schema/heatmapLayer.json new file mode 100644 index 000000000..39605fec1 --- /dev/null +++ b/packages/schema/src/schema/heatmapLayer.json @@ -0,0 +1,56 @@ +{ + "type": "object", + "description": "HeatmapLayer", + "title": "IHeatmapLayer", + "required": ["source", "blur", "radius", "feature"], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "description": "The id of the source" + }, + "feature": { + "type": "string", + "description": "The feature to use" + }, + "opacity": { + "type": "number", + "description": "The opacity of the source", + "default": 1, + "multipleOf": 0.1, + "minimum": 0, + "maximum": 1 + }, + "radius": { + "type": "number", + "description": "Radius size in pixels", + "default": 8 + }, + "blur": { + "type": "number", + "description": "Blur size in pixels", + "default": 15 + }, + "color": { + "type": "array", + "items": { + "type": "string" + }, + "default": ["#00f", "#0ff", "#0f0", "#ff0", "#f00"] + }, + "symbologyState": { + "type": "object", + "description": "The state of the symbology panel options", + "required": ["renderType"], + "properties": { + "renderType": { + "type": "string" + }, + "colorRamp": { + "type": "string", + "default": "cool" + } + } + } + } +} diff --git a/packages/schema/src/schema/jgis.json b/packages/schema/src/schema/jgis.json index 7dd98fa6e..50813af40 100644 --- a/packages/schema/src/schema/jgis.json +++ b/packages/schema/src/schema/jgis.json @@ -35,7 +35,8 @@ "VectorTileLayer", "HillshadeLayer", "WebGlLayer", - "ImageLayer" + "ImageLayer", + "HeatmapLayer" ] }, "sourceType": { diff --git a/packages/schema/src/types.ts b/packages/schema/src/types.ts index c7f3c3cde..f11e3fd08 100644 --- a/packages/schema/src/types.ts +++ b/packages/schema/src/types.ts @@ -17,6 +17,7 @@ export * from './_interface/vectorlayer'; export * from './_interface/vectorTileLayer'; export * from './_interface/webGlLayer'; export * from './_interface/imageLayer'; +export * from './_interface/heatmapLayer'; // Other 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 ff918fdd7..e8e672bc9 100644 --- a/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py +++ b/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py @@ -13,6 +13,7 @@ from .objects import ( IGeoJSONSource, IGeoTiffSource, + IHeatmapLayer, IHillshadeLayer, IImageLayer, IImageSource, @@ -459,6 +460,74 @@ def add_hillshade_layer( return self._add_layer(OBJECT_FACTORY.create_layer(layer, self)) + def add_heatmap_layer( + self, + feature: string, + path: str | Path | None = None, + data: Dict | None = None, + name: str = "Heatmap Layer", + opacity: float = 1, + blur: number = 15, + radius: number = 8, + gradient: List[str] = ["#00f", "#0ff", "#0f0", "#ff0", "#f00"], + ): + """ + Add a Heatmap Layer to the document. + + :param name: The name that will be used for the object in the document. + :param path: The path to the JSON file to embed into the jGIS file. + :param data: The raw GeoJSON data to embed into the jGIS file. + :param gradient: The color gradient to apply. + :param opacity: The opacity, between 0 and 1. + :param blur: The blur size in pixels + :param radius: The radius size in pixels + :param feature: The feature to use to heatmap weights + """ + if isinstance(path, Path): + path = str(path) + + if path is None and data is None: + raise ValueError("Cannot create a GeoJSON source without data") + + if path is not None and data is not None: + raise ValueError("Cannot set GeoJSON source data and path at the same time") + + if path is not None: + # We cannot put the path to the file in the model + # We don't know where the kernel runs/live + # The front-end would have no way of finding the file reliably + # TODO Support urls to JSON files, in that case, don't embed the data + with open(path, "r") as fobj: + parameters = {"data": json.loads(fobj.read())} + + if data is not None: + parameters = {"data": data} + + source = { + "type": SourceType.GeoJSONSource, + "name": f"{name} Source", + "parameters": parameters, + } + + source_id = self._add_source(OBJECT_FACTORY.create_source(source, self)) + + layer = { + "type": LayerType.HeatmapLayer, + "name": name, + "visible": True, + "parameters": { + "source": source_id, + "type": type, + "color": gradient, + "opacity": opacity, + "blur": blur, + "radius": radius, + "feature": feature, + }, + } + + return self._add_layer(OBJECT_FACTORY.create_layer(layer, self)) + def create_color_expr( self, color_stops: Dict, @@ -678,6 +747,7 @@ class Config: IHillshadeLayer, IImageLayer, IWebGlLayer, + IHeatmapLayer, ] _parent = Optional[GISDocument] @@ -779,6 +849,7 @@ def create_source( OBJECT_FACTORY.register_factory(LayerType.HillshadeLayer, IHillshadeLayer) OBJECT_FACTORY.register_factory(LayerType.WebGlLayer, IWebGlLayer) OBJECT_FACTORY.register_factory(LayerType.ImageLayer, IImageLayer) +OBJECT_FACTORY.register_factory(LayerType.HeatmapLayer, IHeatmapLayer) OBJECT_FACTORY.register_factory(SourceType.VectorTileSource, IVectorTileSource) OBJECT_FACTORY.register_factory(SourceType.RasterSource, IRasterSource) diff --git a/python/jupytergis_lab/jupytergis_lab/notebook/objects/__init__.py b/python/jupytergis_lab/jupytergis_lab/notebook/objects/__init__.py index aa0823302..989f98c09 100644 --- a/python/jupytergis_lab/jupytergis_lab/notebook/objects/__init__.py +++ b/python/jupytergis_lab/jupytergis_lab/notebook/objects/__init__.py @@ -6,6 +6,7 @@ from ._schema.hillshadeLayer import IHillshadeLayer # noqa from ._schema.imageLayer import IImageLayer # noqa from ._schema.webGlLayer import IWebGlLayer # noqa +from ._schema.heatmapLayer import IHeatmapLayer # noqa from ._schema.vectortilesource import IVectorTileSource # noqa from ._schema.rastersource import IRasterSource # noqa diff --git a/python/jupytergis_lab/src/index.ts b/python/jupytergis_lab/src/index.ts index 25f116477..f12ac9067 100644 --- a/python/jupytergis_lab/src/index.ts +++ b/python/jupytergis_lab/src/index.ts @@ -236,6 +236,11 @@ const plugin: JupyterFrontEndPlugin = { args: { from: 'contextMenu' } }); + newLayerSubMenu.addItem({ + command: CommandIDs.newHeatmapLayer, + args: { from: 'contextMenu' } + }); + if (mainMenu) { populateMenus(mainMenu, isEnabled); } diff --git a/python/jupytergis_lab/style/base.css b/python/jupytergis_lab/style/base.css index 457dc85ac..33f72fcca 100644 --- a/python/jupytergis_lab/style/base.css +++ b/python/jupytergis_lab/style/base.css @@ -523,6 +523,7 @@ div.jGIS-toolbar-widget > div.jp-Toolbar-item:last-child { order: 1; margin-top: 2px; margin-bottom: 2px; + text-transform: capitalize; } .jGIS-property-panel diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-3-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-3-linux.png new file mode 100644 index 000000000..56cc71ccd Binary files /dev/null and b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-3-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-0-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-0-linux.png index c3c2a58f6..b7a22802b 100644 Binary files a/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-0-linux.png and b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-0-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-3-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-3-linux.png new file mode 100644 index 000000000..6b5857ac0 Binary files /dev/null and b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-3-linux.png differ diff --git a/ui-tests/tests/notebooks/Notebook.ipynb b/ui-tests/tests/notebooks/Notebook.ipynb index 7d41f4afe..742801901 100644 --- a/ui-tests/tests/notebooks/Notebook.ipynb +++ b/ui-tests/tests/notebooks/Notebook.ipynb @@ -59,6 +59,20 @@ "\n", "doc" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a5fd3c6-2d81-4e93-be21-533df696d7b0", + "metadata": {}, + "outputs": [], + "source": [ + "doc = GISDocument()\n", + "\n", + "doc.add_heatmap_layer(path=\"./eq.json\", feature=\"mag\")\n", + "\n", + "doc" + ] } ], "metadata": {