Skip to content

Commit 8582f4c

Browse files
arjxn-pygjmooneymfisher87
authored
Support Graduated, Categorised & Canonical Symbology on VectorTiles (#844)
* trying to get the attribute data of tiles * Make feature reading work --------- Co-authored-by: Greg Mooney <[email protected]> * some logic to sync and get features * lint * Update packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx * not needed * Cleanup * lint * apply graduated symbology to strokes too * Apply suggestions from code review Co-authored-by: Greg Mooney <[email protected]> Co-authored-by: Matt Fisher <[email protected]> * fix brokage * move getproperties logic into methods --------- Co-authored-by: Greg Mooney <[email protected]> Co-authored-by: Matt Fisher <[email protected]>
1 parent b294c7d commit 8582f4c

File tree

6 files changed

+130
-28
lines changed

6 files changed

+130
-28
lines changed

packages/base/src/dialogs/symbology/hooks/useGetProperties.ts

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,64 @@ interface IUseGetPropertiesResult {
1616
error?: Error;
1717
}
1818

19+
async function getGeoJsonProperties({
20+
source,
21+
model,
22+
}: {
23+
source: any;
24+
model: IJupyterGISModel;
25+
}): Promise<Record<string, Set<any>>> {
26+
const result: Record<string, Set<any>> = {};
27+
28+
const data = await loadFile({
29+
filepath: source.parameters?.path,
30+
type: 'GeoJSONSource',
31+
model,
32+
});
33+
34+
if (!data) {
35+
throw new Error('Failed to read GeoJSON data');
36+
}
37+
38+
data.features.forEach((feature: GeoJSONFeature1) => {
39+
if (feature.properties) {
40+
for (const [key, value] of Object.entries(feature.properties)) {
41+
if (!result[key]) {
42+
result[key] = new Set();
43+
}
44+
result[key].add(value);
45+
}
46+
}
47+
});
48+
49+
return result;
50+
}
51+
52+
function getVectorTileProperties({
53+
model,
54+
sourceId,
55+
}: {
56+
model: IJupyterGISModel;
57+
sourceId: string;
58+
}): Record<string, Set<any>> {
59+
const result: Record<string, Set<any>> = {};
60+
const features = model.getFeaturesForCurrentTile({ sourceId });
61+
62+
features.forEach(feature => {
63+
const props = feature.getProperties?.();
64+
if (props) {
65+
for (const [key, value] of Object.entries(props)) {
66+
if (!result[key]) {
67+
result[key] = new Set();
68+
}
69+
result[key].add(value);
70+
}
71+
}
72+
});
73+
74+
return result;
75+
}
76+
1977
export const useGetProperties = ({
2078
layerId,
2179
model,
@@ -39,33 +97,22 @@ export const useGetProperties = ({
3997
throw new Error('Source not found');
4098
}
4199

42-
const data = await loadFile({
43-
filepath: source.parameters?.path,
44-
type: 'GeoJSONSource',
45-
model: model,
46-
});
100+
const sourceType = source?.type;
101+
let result: Record<string, Set<any>> = {};
47102

48-
if (!data) {
49-
throw new Error('Failed to read GeoJSON data');
103+
if (sourceType === 'GeoJSONSource') {
104+
result = await getGeoJsonProperties({ source, model });
105+
} else if (sourceType === 'VectorTileSource') {
106+
const sourceId = layer?.parameters?.source;
107+
result = getVectorTileProperties({ model, sourceId });
108+
} else {
109+
throw new Error(`Unsupported source type: ${sourceType}`);
50110
}
51111

52-
const result: Record<string, Set<any>> = {};
53-
54-
data.features.forEach((feature: GeoJSONFeature1) => {
55-
if (feature.properties) {
56-
Object.entries(feature.properties).forEach(([key, value]) => {
57-
if (!(key in result)) {
58-
result[key] = new Set();
59-
}
60-
result[key].add(value);
61-
});
62-
}
63-
});
64-
65112
setFeatureProperties(result);
66-
setIsLoading(false);
67113
} catch (err) {
68114
setError(err as Error);
115+
} finally {
69116
setIsLoading(false);
70117
}
71118
};

packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,19 @@ const RENDER_TYPE_OPTIONS: RenderTypeOptions = {
4242
Canonical: {
4343
component: Canonical,
4444
attributeChecker: getColorCodeFeatureAttributes,
45-
supportedLayerTypes: ['VectorLayer', 'HeatmapLayer'],
45+
supportedLayerTypes: ['VectorLayer', 'VectorTileLayer', 'HeatmapLayer'],
4646
isTabbed: false,
4747
},
4848
Graduated: {
4949
component: Graduated,
5050
attributeChecker: getNumericFeatureAttributes,
51-
supportedLayerTypes: ['VectorLayer', 'HeatmapLayer'],
51+
supportedLayerTypes: ['VectorLayer', 'VectorTileLayer', 'HeatmapLayer'],
5252
isTabbed: true,
5353
},
5454
Categorized: {
5555
component: Categorized,
5656
attributeChecker: getNumericFeatureAttributes,
57-
supportedLayerTypes: ['VectorLayer', 'HeatmapLayer'],
57+
supportedLayerTypes: ['VectorLayer', 'VectorTileLayer', 'HeatmapLayer'],
5858
isTabbed: true,
5959
},
6060
Heatmap: {

packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,15 @@ const Graduated: React.FC<ISymbologyTabbedDialogWithAttributesProps> = ({
166166
});
167167
newStyle['fill-color'] = colorExpr;
168168
newStyle['circle-fill-color'] = colorExpr;
169+
newStyle['stroke-color'] = colorExpr;
170+
newStyle['circle-stroke-color'] = colorExpr;
169171
} else {
170172
newStyle['fill-color'] = undefined;
171173
newStyle['circle-fill-color'] = undefined;
174+
newStyle['stroke-color'] = colorManualStyleRef.current.strokeColor;
175+
newStyle['circle-stroke-color'] = colorManualStyleRef.current.strokeColor;
172176
}
173177

174-
newStyle['stroke-color'] = colorManualStyleRef.current.strokeColor;
175-
newStyle['circle-stroke-color'] = colorManualStyleRef.current.strokeColor;
176178
newStyle['stroke-width'] = colorManualStyleRef.current.strokeWidth;
177179
newStyle['circle-stroke-width'] = colorManualStyleRef.current.strokeWidth;
178180

packages/base/src/mainview/mainView.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,14 @@ import { IStateDB } from '@jupyterlab/statedb';
4040
import { CommandRegistry } from '@lumino/commands';
4141
import { JSONValue, UUID } from '@lumino/coreutils';
4242
import { ContextMenu } from '@lumino/widgets';
43-
import { Collection, MapBrowserEvent, Map as OlMap, View, getUid } from 'ol';
43+
import {
44+
Collection,
45+
MapBrowserEvent,
46+
Map as OlMap,
47+
VectorTile,
48+
View,
49+
getUid,
50+
} from 'ol';
4451
import Feature, { FeatureLike } from 'ol/Feature';
4552
import { FullScreen, ScaleLine } from 'ol/control';
4653
import { Coordinate } from 'ol/coordinate';
@@ -72,9 +79,10 @@ import {
7279
Vector as VectorSource,
7380
VectorTile as VectorTileSource,
7481
XYZ as XYZSource,
82+
Tile as TileSource,
7583
} from 'ol/source';
7684
import Static from 'ol/source/ImageStatic';
77-
import TileSource from 'ol/source/Tile';
85+
import { TileSourceEvent } from 'ol/source/Tile';
7886
import { Circle, Fill, Stroke, Style } from 'ol/style';
7987
import { Rule } from 'ol/style/flat';
8088
//@ts-expect-error no types for ol-pmtiles
@@ -633,6 +641,15 @@ export class MainView extends React.Component<IProps, IStates> {
633641
});
634642
}
635643

644+
newSource.on('tileloadend', (event: TileSourceEvent) => {
645+
const tile = event.tile as VectorTile<FeatureLike>;
646+
const features = tile.getFeatures();
647+
648+
if (features && features.length > 0) {
649+
this._model.syncTileFeatures({ sourceId: id, features });
650+
}
651+
});
652+
636653
break;
637654
}
638655
case 'GeoJSONSource': {

packages/schema/src/interfaces.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Contents, User } from '@jupyterlab/services';
1313
import { JSONObject } from '@lumino/coreutils';
1414
import { ISignal, Signal } from '@lumino/signaling';
1515
import { SplitPanel } from '@lumino/widgets';
16+
import { FeatureLike } from 'ol/Feature';
1617

1718
import {
1819
IJGISContent,
@@ -186,6 +187,19 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel {
186187

187188
pathChanged: ISignal<IJupyterGISModel, string>;
188189

190+
getFeaturesForCurrentTile: ({
191+
sourceId,
192+
}: {
193+
sourceId: string;
194+
}) => FeatureLike[];
195+
syncTileFeatures: ({
196+
sourceId,
197+
features,
198+
}: {
199+
sourceId: string;
200+
features: FeatureLike[];
201+
}) => void;
202+
189203
getSettings(): IJupyterGISSettings;
190204
getContent(): IJGISContent;
191205
getLayers(): IJGISLayers;

packages/schema/src/model.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry';
66
import { PartialJSONObject } from '@lumino/coreutils';
77
import { ISignal, Signal } from '@lumino/signaling';
88
import Ajv from 'ajv';
9+
import { FeatureLike } from 'ol/Feature';
910

1011
import {
1112
IJGISContent,
@@ -78,6 +79,26 @@ export class JupyterGISModel implements IJupyterGISModel {
7879
return this._settings;
7980
}
8081

82+
getFeaturesForCurrentTile({ sourceId }: { sourceId: string }): FeatureLike[] {
83+
return Array.from(this._tileFeatureCache.get(sourceId) ?? []);
84+
}
85+
86+
syncTileFeatures({
87+
sourceId,
88+
features,
89+
}: {
90+
sourceId: string;
91+
features: FeatureLike[];
92+
}): void {
93+
let featureSet = this._tileFeatureCache.get(sourceId);
94+
95+
if (!featureSet) {
96+
featureSet = new Set();
97+
this._tileFeatureCache.set(sourceId, featureSet);
98+
}
99+
features.forEach(feature => featureSet.add(feature));
100+
}
101+
81102
private _onSharedModelChanged = (sender: any, changes: any): void => {
82103
if (changes && changes?.objectChange?.length) {
83104
this._contentChanged.emit(void 0);
@@ -793,6 +814,7 @@ export class JupyterGISModel implements IJupyterGISModel {
793814

794815
private _geolocation: JgisCoordinates;
795816
private _geolocationChanged = new Signal<this, JgisCoordinates>(this);
817+
private _tileFeatureCache: Map<string, Set<FeatureLike>> = new Map();
796818
}
797819

798820
export namespace JupyterGISModel {

0 commit comments

Comments
 (0)