@@ -9,6 +9,7 @@ import {GeoJsonLayer, PathLayer} from '@deck.gl/layers/typed';
99import { MVTSource , MVTTileSource } from '@loaders.gl/mvt' ;
1010import { PMTilesSource , PMTilesTileSource } from '@loaders.gl/pmtiles' ;
1111import GL from '@luma.gl/constants' ;
12+ import { ClipExtension } from '@deck.gl/extensions/typed' ;
1213
1314import { notNullorUndefined } from '@kepler.gl/common-utils' ;
1415import {
@@ -73,7 +74,21 @@ export const DEFAULT_HIGHLIGHT_FILL_COLOR = [252, 242, 26, 150];
7374export const DEFAULT_HIGHLIGHT_STROKE_COLOR = [ 252 , 242 , 26 , 255 ] ;
7475export const MAX_CACHE_SIZE_MOBILE = 1 ; // Minimize caching, visible tiles will always be loaded
7576export 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