diff --git a/packages/base/src/dialogs/symbology/hooks/useGetProperties.ts b/packages/base/src/dialogs/symbology/hooks/useGetProperties.ts index eb1228a85..bd752a5c7 100644 --- a/packages/base/src/dialogs/symbology/hooks/useGetProperties.ts +++ b/packages/base/src/dialogs/symbology/hooks/useGetProperties.ts @@ -16,6 +16,64 @@ interface IUseGetPropertiesResult { error?: Error; } +async function getGeoJsonProperties({ + source, + model, +}: { + source: any; + model: IJupyterGISModel; +}): Promise>> { + const result: Record> = {}; + + const data = await loadFile({ + filepath: source.parameters?.path, + type: 'GeoJSONSource', + model, + }); + + if (!data) { + throw new Error('Failed to read GeoJSON data'); + } + + data.features.forEach((feature: GeoJSONFeature1) => { + if (feature.properties) { + for (const [key, value] of Object.entries(feature.properties)) { + if (!result[key]) { + result[key] = new Set(); + } + result[key].add(value); + } + } + }); + + return result; +} + +function getVectorTileProperties({ + model, + sourceId, +}: { + model: IJupyterGISModel; + sourceId: string; +}): Record> { + const result: Record> = {}; + const features = model.getFeaturesForCurrentTile({ sourceId }); + + features.forEach(feature => { + const props = feature.getProperties?.(); + if (props) { + for (const [key, value] of Object.entries(props)) { + if (!result[key]) { + result[key] = new Set(); + } + result[key].add(value); + } + } + }); + + return result; +} + export const useGetProperties = ({ layerId, model, @@ -39,33 +97,22 @@ export const useGetProperties = ({ throw new Error('Source not found'); } - const data = await loadFile({ - filepath: source.parameters?.path, - type: 'GeoJSONSource', - model: model, - }); + const sourceType = source?.type; + let result: Record> = {}; - if (!data) { - throw new Error('Failed to read GeoJSON data'); + if (sourceType === 'GeoJSONSource') { + result = await getGeoJsonProperties({ source, model }); + } else if (sourceType === 'VectorTileSource') { + const sourceId = layer?.parameters?.source; + result = getVectorTileProperties({ model, sourceId }); + } else { + throw new Error(`Unsupported source type: ${sourceType}`); } - const result: Record> = {}; - - data.features.forEach((feature: GeoJSONFeature1) => { - if (feature.properties) { - Object.entries(feature.properties).forEach(([key, value]) => { - if (!(key in result)) { - result[key] = new Set(); - } - result[key].add(value); - }); - } - }); - setFeatureProperties(result); - setIsLoading(false); } catch (err) { setError(err as Error); + } finally { setIsLoading(false); } }; diff --git a/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx index 666401ec2..26206be37 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx @@ -42,19 +42,19 @@ const RENDER_TYPE_OPTIONS: RenderTypeOptions = { Canonical: { component: Canonical, attributeChecker: getColorCodeFeatureAttributes, - supportedLayerTypes: ['VectorLayer', 'HeatmapLayer'], + supportedLayerTypes: ['VectorLayer', 'VectorTileLayer', 'HeatmapLayer'], isTabbed: false, }, Graduated: { component: Graduated, attributeChecker: getNumericFeatureAttributes, - supportedLayerTypes: ['VectorLayer', 'HeatmapLayer'], + supportedLayerTypes: ['VectorLayer', 'VectorTileLayer', 'HeatmapLayer'], isTabbed: true, }, Categorized: { component: Categorized, attributeChecker: getNumericFeatureAttributes, - supportedLayerTypes: ['VectorLayer', 'HeatmapLayer'], + supportedLayerTypes: ['VectorLayer', 'VectorTileLayer', 'HeatmapLayer'], isTabbed: true, }, Heatmap: { 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 ec18970cc..c4f680da5 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx @@ -166,13 +166,15 @@ const Graduated: React.FC = ({ }); newStyle['fill-color'] = colorExpr; newStyle['circle-fill-color'] = colorExpr; + newStyle['stroke-color'] = colorExpr; + newStyle['circle-stroke-color'] = colorExpr; } else { newStyle['fill-color'] = undefined; newStyle['circle-fill-color'] = undefined; + newStyle['stroke-color'] = colorManualStyleRef.current.strokeColor; + newStyle['circle-stroke-color'] = colorManualStyleRef.current.strokeColor; } - newStyle['stroke-color'] = colorManualStyleRef.current.strokeColor; - newStyle['circle-stroke-color'] = colorManualStyleRef.current.strokeColor; newStyle['stroke-width'] = colorManualStyleRef.current.strokeWidth; newStyle['circle-stroke-width'] = colorManualStyleRef.current.strokeWidth; diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 1f786e6fe..cfa42a40c 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -40,7 +40,14 @@ import { IStateDB } from '@jupyterlab/statedb'; import { CommandRegistry } from '@lumino/commands'; import { JSONValue, UUID } from '@lumino/coreutils'; import { ContextMenu } from '@lumino/widgets'; -import { Collection, MapBrowserEvent, Map as OlMap, View, getUid } from 'ol'; +import { + Collection, + MapBrowserEvent, + Map as OlMap, + VectorTile, + View, + getUid, +} from 'ol'; import Feature, { FeatureLike } from 'ol/Feature'; import { FullScreen, ScaleLine } from 'ol/control'; import { Coordinate } from 'ol/coordinate'; @@ -72,9 +79,10 @@ import { Vector as VectorSource, VectorTile as VectorTileSource, XYZ as XYZSource, + Tile as TileSource, } from 'ol/source'; import Static from 'ol/source/ImageStatic'; -import TileSource from 'ol/source/Tile'; +import { TileSourceEvent } from 'ol/source/Tile'; import { Circle, Fill, Stroke, Style } from 'ol/style'; import { Rule } from 'ol/style/flat'; //@ts-expect-error no types for ol-pmtiles @@ -633,6 +641,15 @@ export class MainView extends React.Component { }); } + newSource.on('tileloadend', (event: TileSourceEvent) => { + const tile = event.tile as VectorTile; + const features = tile.getFeatures(); + + if (features && features.length > 0) { + this._model.syncTileFeatures({ sourceId: id, features }); + } + }); + break; } case 'GeoJSONSource': { diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index d408c3843..e1149c98e 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -13,6 +13,7 @@ import { Contents, User } from '@jupyterlab/services'; import { JSONObject } from '@lumino/coreutils'; import { ISignal, Signal } from '@lumino/signaling'; import { SplitPanel } from '@lumino/widgets'; +import { FeatureLike } from 'ol/Feature'; import { IJGISContent, @@ -186,6 +187,19 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel { pathChanged: ISignal; + getFeaturesForCurrentTile: ({ + sourceId, + }: { + sourceId: string; + }) => FeatureLike[]; + syncTileFeatures: ({ + sourceId, + features, + }: { + sourceId: string; + features: FeatureLike[]; + }) => void; + getSettings(): IJupyterGISSettings; getContent(): IJGISContent; getLayers(): IJGISLayers; diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index 27622a0d9..31e9135b1 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -6,6 +6,7 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { PartialJSONObject } from '@lumino/coreutils'; import { ISignal, Signal } from '@lumino/signaling'; import Ajv from 'ajv'; +import { FeatureLike } from 'ol/Feature'; import { IJGISContent, @@ -78,6 +79,26 @@ export class JupyterGISModel implements IJupyterGISModel { return this._settings; } + getFeaturesForCurrentTile({ sourceId }: { sourceId: string }): FeatureLike[] { + return Array.from(this._tileFeatureCache.get(sourceId) ?? []); + } + + syncTileFeatures({ + sourceId, + features, + }: { + sourceId: string; + features: FeatureLike[]; + }): void { + let featureSet = this._tileFeatureCache.get(sourceId); + + if (!featureSet) { + featureSet = new Set(); + this._tileFeatureCache.set(sourceId, featureSet); + } + features.forEach(feature => featureSet.add(feature)); + } + private _onSharedModelChanged = (sender: any, changes: any): void => { if (changes && changes?.objectChange?.length) { this._contentChanged.emit(void 0); @@ -793,6 +814,7 @@ export class JupyterGISModel implements IJupyterGISModel { private _geolocation: JgisCoordinates; private _geolocationChanged = new Signal(this); + private _tileFeatureCache: Map> = new Map(); } export namespace JupyterGISModel {