Skip to content

Commit 2b8af82

Browse files
authored
[fix] vector tile layer - use highlightedFeatureId for hover (#3202)
* [fix] vector tile layer - hover - group adjacent Signed-off-by: Ihor Dykhta <[email protected]> * smart outline calculation for features that are part of multiple tiles Signed-off-by: Ihor Dykhta <[email protected]> --------- Signed-off-by: Ihor Dykhta <[email protected]>
1 parent 32fb77f commit 2b8af82

File tree

2 files changed

+115
-5
lines changed

2 files changed

+115
-5
lines changed

src/layers/src/vector-tile/common-tile/tile-dataset.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ export default class TileDataset<T, I extends Iterable<any> = T extends Iterable
7676
this.tileSet = new IterableTileSet(tiles.map(getIterable), getRowCount);
7777
}
7878

79+
/**
80+
* Return current tiles
81+
*/
82+
getTiles(): readonly T[] {
83+
return this.tiles;
84+
}
85+
7986
/**
8087
* Get the min/max domain of a field
8188
*/

src/layers/src/vector-tile/vector-tile-layer.ts

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {GeoJsonLayer, PathLayer} from '@deck.gl/layers/typed';
99
import {MVTSource, MVTTileSource} from '@loaders.gl/mvt';
1010
import {PMTilesSource, PMTilesTileSource} from '@loaders.gl/pmtiles';
1111
import GL from '@luma.gl/constants';
12+
import {ClipExtension} from '@deck.gl/extensions/typed';
1213

1314
import {notNullorUndefined} from '@kepler.gl/common-utils';
1415
import {
@@ -73,7 +74,21 @@ export const DEFAULT_HIGHLIGHT_FILL_COLOR = [252, 242, 26, 150];
7374
export const DEFAULT_HIGHLIGHT_STROKE_COLOR = [252, 242, 26, 255];
7475
export const MAX_CACHE_SIZE_MOBILE = 1; // Minimize caching, visible tiles will always be loaded
7576
export const DEFAULT_STROKE_WIDTH = 1;
76-
77+
export const UUID_CANDIDATES = [
78+
'ufid',
79+
'UFID',
80+
'id',
81+
'ID',
82+
'fid',
83+
'FID',
84+
'objectid',
85+
'OBJECTID',
86+
'gid',
87+
'GID',
88+
'feature_id',
89+
'FEATURE_ID',
90+
'_id'
91+
];
7792
/**
7893
* Type for transformRequest returned parameters.
7994
*/
@@ -538,6 +553,26 @@ export default class VectorTileLayer extends AbstractTileLayer<VectorTile, Featu
538553
if (data.tileSource) {
539554
const hoveredObject = this.hasHoveredObject(objectHovered);
540555

556+
// Try to infer a stable unique id property from the hovered feature so we can
557+
// highlight the same feature across adjacent tiles. If none is found, rely on
558+
// feature.id when available.
559+
let uniqueIdProperty: string | undefined;
560+
let highlightedFeatureId: string | number | undefined;
561+
if (hoveredObject && hoveredObject.properties) {
562+
uniqueIdProperty = UUID_CANDIDATES.find(
563+
k => hoveredObject.properties && k in hoveredObject.properties
564+
);
565+
highlightedFeatureId = uniqueIdProperty
566+
? hoveredObject.properties[uniqueIdProperty]
567+
: (hoveredObject as any).id;
568+
}
569+
570+
// Build per-tile clipped overlay to draw only the outer stroke of highlighted feature per tile
571+
const perTileOverlays = this._getPerTileOverlays(hoveredObject, {
572+
defaultLayerProps,
573+
visConfig
574+
});
575+
541576
const layers = [
542577
new CustomMVTLayer({
543578
...defaultLayerProps,
@@ -556,7 +591,8 @@ export default class VectorTileLayer extends AbstractTileLayer<VectorTile, Featu
556591
stroked: visConfig.stroked,
557592

558593
// TODO: this is hard coded, design a UI to allow user assigned unique property id
559-
// uniqueIdProperty: 'ufid',
594+
uniqueIdProperty,
595+
highlightedFeatureId,
560596
renderSubLayers: this.renderSubLayers,
561597
// when radiusUnits is meter
562598
getPointRadiusScaleByZoom: getPropertyByZoom(visConfig.radiusByZoom, visConfig.radius),
@@ -635,8 +671,8 @@ export default class VectorTileLayer extends AbstractTileLayer<VectorTile, Featu
635671
mvt: getLoaderOptions().mvt
636672
}
637673
}),
638-
// hover layer
639-
...(hoveredObject
674+
// render hover layer for features with no unique id property and no highlighted feature id
675+
...(hoveredObject && !uniqueIdProperty && !highlightedFeatureId
640676
? [
641677
new GeoJsonLayer({
642678
// @ts-expect-error props not typed?
@@ -653,12 +689,79 @@ export default class VectorTileLayer extends AbstractTileLayer<VectorTile, Featu
653689
filled: true
654690
})
655691
]
656-
: [])
692+
: []),
693+
...perTileOverlays
657694
// ...tileLayerBoundsLayer(defaultLayerProps.id, data),
658695
];
659696

660697
return layers;
661698
}
662699
return [];
663700
}
701+
702+
/**
703+
* Build per-tile clipped overlay to draw only the outer stroke of highlighted feature per tile
704+
* @param hoveredObject
705+
*/
706+
_getPerTileOverlays(
707+
hoveredObject: Feature,
708+
options: {defaultLayerProps: any; visConfig: any}
709+
): DeckLayer[] {
710+
let perTileOverlays: DeckLayer[] = [];
711+
if (hoveredObject) {
712+
try {
713+
const tiles = this.tileDataset?.getTiles?.() || [];
714+
// Derive hovered id from hoveredObject
715+
const hoveredPid = UUID_CANDIDATES.find(
716+
k => hoveredObject?.properties && k in hoveredObject.properties
717+
);
718+
const hoveredId = hoveredPid
719+
? String(hoveredObject?.properties?.[hoveredPid])
720+
: String((hoveredObject as any)?.id);
721+
722+
// Group matched fragments by tile id
723+
const byTile: Record<string, Feature[]> = {};
724+
for (const tile of tiles) {
725+
const content = (tile as any)?.content;
726+
const features = content?.shape === 'geojson-table' ? content.features : content;
727+
if (!Array.isArray(features)) continue;
728+
const tileId = (tile as any).id;
729+
for (const f of features) {
730+
const pid = UUID_CANDIDATES.find(k => f.properties && k in f.properties);
731+
const fid = pid ? f.properties?.[pid] : (f as any).id;
732+
if (fid !== undefined && String(fid) === hoveredId) {
733+
(byTile[tileId] = byTile[tileId] || []).push(f as Feature);
734+
}
735+
}
736+
}
737+
738+
perTileOverlays = Object.entries(byTile).map(([tileId, feats]) => {
739+
const tile = tiles.find((t: any) => String(t.id) === String(tileId));
740+
const bounds = tile?.boundingBox
741+
? [...tile.boundingBox[0], ...tile.boundingBox[1]]
742+
: undefined;
743+
return new GeoJsonLayer({
744+
...(this.getDefaultHoverLayerProps() as any),
745+
id: `${options.defaultLayerProps.id}-hover-outline-${tileId}`,
746+
visible: true,
747+
wrapLongitude: false,
748+
data: feats,
749+
getLineColor: DEFAULT_HIGHLIGHT_STROKE_COLOR,
750+
getFillColor: [0, 0, 0, 0],
751+
getLineWidth: options.visConfig.strokeWidth + 1,
752+
lineWidthUnits: 'pixels',
753+
lineJointRounded: true,
754+
lineCapRounded: true,
755+
stroked: true,
756+
filled: false,
757+
clipBounds: bounds,
758+
extensions: bounds ? [new ClipExtension()] : []
759+
});
760+
});
761+
} catch {
762+
perTileOverlays = [];
763+
}
764+
}
765+
return perTileOverlays;
766+
}
664767
}

0 commit comments

Comments
 (0)