Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/layers/src/vector-tile/common-tile/tile-dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ export default class TileDataset<T, I extends Iterable<any> = T extends Iterable
this.tileSet = new IterableTileSet(tiles.map(getIterable), getRowCount);
}

/**
* Return current tiles
*/
getTiles(): readonly T[] {
return this.tiles;
}

/**
* Get the min/max domain of a field
*/
Expand Down
113 changes: 108 additions & 5 deletions src/layers/src/vector-tile/vector-tile-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {GeoJsonLayer, PathLayer} from '@deck.gl/layers/typed';
import {MVTSource, MVTTileSource} from '@loaders.gl/mvt';
import {PMTilesSource, PMTilesTileSource} from '@loaders.gl/pmtiles';
import GL from '@luma.gl/constants';
import {ClipExtension} from '@deck.gl/extensions/typed';

import {notNullorUndefined} from '@kepler.gl/common-utils';
import {
Expand Down Expand Up @@ -73,7 +74,21 @@ export const DEFAULT_HIGHLIGHT_FILL_COLOR = [252, 242, 26, 150];
export const DEFAULT_HIGHLIGHT_STROKE_COLOR = [252, 242, 26, 255];
export const MAX_CACHE_SIZE_MOBILE = 1; // Minimize caching, visible tiles will always be loaded
export const DEFAULT_STROKE_WIDTH = 1;

export const UUID_CANDIDATES = [
'ufid',
'UFID',
'id',
'ID',
'fid',
'FID',
'objectid',
'OBJECTID',
'gid',
'GID',
'feature_id',
'FEATURE_ID',
'_id'
];
Comment thread
igorDykhta marked this conversation as resolved.
/**
* Type for transformRequest returned parameters.
*/
Expand Down Expand Up @@ -538,6 +553,26 @@ export default class VectorTileLayer extends AbstractTileLayer<VectorTile, Featu
if (data.tileSource) {
const hoveredObject = this.hasHoveredObject(objectHovered);

// Try to infer a stable unique id property from the hovered feature so we can
// highlight the same feature across adjacent tiles. If none is found, rely on
// feature.id when available.
let uniqueIdProperty: string | undefined;
let highlightedFeatureId: string | number | undefined;
if (hoveredObject && hoveredObject.properties) {
uniqueIdProperty = UUID_CANDIDATES.find(
k => hoveredObject.properties && k in hoveredObject.properties
);
highlightedFeatureId = uniqueIdProperty
? hoveredObject.properties[uniqueIdProperty]
: (hoveredObject as any).id;
Comment thread
igorDykhta marked this conversation as resolved.
}

// Build per-tile clipped overlay to draw only the outer stroke of highlighted feature per tile
const perTileOverlays = this._getPerTileOverlays(hoveredObject, {
defaultLayerProps,
visConfig
});

const layers = [
new CustomMVTLayer({
...defaultLayerProps,
Expand All @@ -556,7 +591,8 @@ export default class VectorTileLayer extends AbstractTileLayer<VectorTile, Featu
stroked: visConfig.stroked,

// TODO: this is hard coded, design a UI to allow user assigned unique property id
// uniqueIdProperty: 'ufid',
uniqueIdProperty,
highlightedFeatureId,
renderSubLayers: this.renderSubLayers,
// when radiusUnits is meter
getPointRadiusScaleByZoom: getPropertyByZoom(visConfig.radiusByZoom, visConfig.radius),
Expand Down Expand Up @@ -635,8 +671,8 @@ export default class VectorTileLayer extends AbstractTileLayer<VectorTile, Featu
mvt: getLoaderOptions().mvt
}
}),
// hover layer
...(hoveredObject
// render hover layer for features with no unique id property and no highlighted feature id
...(hoveredObject && !uniqueIdProperty && !highlightedFeatureId
? [
new GeoJsonLayer({
// @ts-expect-error props not typed?
Expand All @@ -653,12 +689,79 @@ export default class VectorTileLayer extends AbstractTileLayer<VectorTile, Featu
filled: true
})
]
: [])
: []),
...perTileOverlays
// ...tileLayerBoundsLayer(defaultLayerProps.id, data),
];

return layers;
}
return [];
}

/**
* Build per-tile clipped overlay to draw only the outer stroke of highlighted feature per tile
* @param hoveredObject
*/
_getPerTileOverlays(
hoveredObject: Feature,
options: {defaultLayerProps: any; visConfig: any}
): DeckLayer[] {
let perTileOverlays: DeckLayer[] = [];
if (hoveredObject) {
try {
const tiles = this.tileDataset?.getTiles?.() || [];
// Derive hovered id from hoveredObject
const hoveredPid = UUID_CANDIDATES.find(
k => hoveredObject?.properties && k in hoveredObject.properties
);
const hoveredId = hoveredPid
? String(hoveredObject?.properties?.[hoveredPid])
: String((hoveredObject as any)?.id);

// Group matched fragments by tile id
const byTile: Record<string, Feature[]> = {};
for (const tile of tiles) {
const content = (tile as any)?.content;
const features = content?.shape === 'geojson-table' ? content.features : content;
if (!Array.isArray(features)) continue;
const tileId = (tile as any).id;
for (const f of features) {
const pid = UUID_CANDIDATES.find(k => f.properties && k in f.properties);
const fid = pid ? f.properties?.[pid] : (f as any).id;
if (fid !== undefined && String(fid) === hoveredId) {
(byTile[tileId] = byTile[tileId] || []).push(f as Feature);
}
}
}

perTileOverlays = Object.entries(byTile).map(([tileId, feats]) => {
const tile = tiles.find((t: any) => String(t.id) === String(tileId));
const bounds = tile?.boundingBox
? [...tile.boundingBox[0], ...tile.boundingBox[1]]
: undefined;
return new GeoJsonLayer({
...(this.getDefaultHoverLayerProps() as any),
id: `${options.defaultLayerProps.id}-hover-outline-${tileId}`,
visible: true,
wrapLongitude: false,
data: feats,
getLineColor: DEFAULT_HIGHLIGHT_STROKE_COLOR,
getFillColor: [0, 0, 0, 0],
getLineWidth: options.visConfig.strokeWidth + 1,
lineWidthUnits: 'pixels',
lineJointRounded: true,
lineCapRounded: true,
stroked: true,
filled: false,
clipBounds: bounds,
extensions: bounds ? [new ClipExtension()] : []
});
});
} catch {
perTileOverlays = [];
}
}
return perTileOverlays;
}
}