@@ -24,7 +24,8 @@ import {
24
24
GetPickingInfoParams ,
25
25
Layer ,
26
26
LayersList ,
27
- PickingInfo
27
+ PickingInfo ,
28
+ WebMercatorViewport
28
29
} from '@deck.gl/core' ;
29
30
30
31
import {
@@ -34,38 +35,59 @@ import {
34
35
computeAggregationStats ,
35
36
extractAggregationProperties ,
36
37
ParsedQuadbinCell ,
37
- ParsedQuadbinTile
38
+ ParsedQuadbinTile ,
39
+ ParsedH3Cell ,
40
+ ParsedH3Tile
38
41
} from './cluster-utils' ;
39
42
import { DEFAULT_TILE_SIZE } from '../constants' ;
40
43
import QuadbinTileset2D from './quadbin-tileset-2d' ;
44
+ import H3Tileset2D , { getHexagonResolution } from './h3-tileset-2d' ;
41
45
import { getQuadbinPolygon } from './quadbin-utils' ;
46
+ import { getResolution , cellToLatLng } from 'h3-js' ;
42
47
import CartoSpatialTileLoader from './schema/carto-spatial-tile-loader' ;
43
48
import { TilejsonPropType , mergeLoadOptions } from './utils' ;
44
49
import type { TilejsonResult } from '@carto/api-client' ;
45
50
46
51
registerLoaders ( [ CartoSpatialTileLoader ] ) ;
47
52
53
+ function getScheme ( tilesetClass : typeof H3Tileset2D | typeof QuadbinTileset2D ) : 'h3' | 'quadbin' {
54
+ if ( tilesetClass === H3Tileset2D ) return 'h3' ;
55
+ if ( tilesetClass === QuadbinTileset2D ) return 'quadbin' ;
56
+ throw new Error ( 'Invalid tileset class' ) ;
57
+ }
58
+
48
59
const defaultProps : DefaultProps < ClusterTileLayerProps > = {
49
60
data : TilejsonPropType ,
50
61
clusterLevel : { type : 'number' , value : 5 , min : 1 } ,
51
62
getPosition : {
52
63
type : 'accessor' ,
53
- value : ( { id} ) => getQuadbinPolygon ( id , 0.5 ) . slice ( 2 , 4 ) as [ number , number ]
64
+ value : ( { id} ) => {
65
+ // Determine scheme based on ID type: H3 uses string IDs, Quadbin uses bigint IDs
66
+ if ( typeof id === 'string' ) {
67
+ const [ lat , lng ] = cellToLatLng ( id ) ;
68
+ return [ lng , lat ] ;
69
+ }
70
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
71
+ return getQuadbinPolygon ( id as bigint , 0.5 ) . slice ( 2 , 4 ) as [ number , number ] ;
72
+ }
54
73
} ,
55
74
getWeight : { type : 'accessor' , value : 1 } ,
56
75
refinementStrategy : 'no-overlap' ,
57
76
tileSize : DEFAULT_TILE_SIZE
58
77
} ;
59
78
60
79
export type ClusterTileLayerPickingInfo < FeaturePropertiesT = { } > = TileLayerPickingInfo <
61
- ParsedQuadbinTile < FeaturePropertiesT > ,
80
+ ParsedQuadbinTile < FeaturePropertiesT > | ParsedH3Tile < FeaturePropertiesT > ,
62
81
PickingInfo < Feature < Geometry , FeaturePropertiesT > >
63
82
> ;
64
83
65
84
/** All properties supported by ClusterTileLayer. */
66
85
export type ClusterTileLayerProps < FeaturePropertiesT = unknown > =
67
86
_ClusterTileLayerProps < FeaturePropertiesT > &
68
- Omit < TileLayerProps < ParsedQuadbinTile < FeaturePropertiesT > > , 'data' > ;
87
+ Omit <
88
+ TileLayerProps < ParsedQuadbinTile < FeaturePropertiesT > | ParsedH3Tile < FeaturePropertiesT > > ,
89
+ 'data'
90
+ > ;
69
91
70
92
/** Properties added by ClusterTileLayer. */
71
93
type _ClusterTileLayerProps < FeaturePropertiesT > = Omit <
@@ -84,63 +106,84 @@ type _ClusterTileLayerProps<FeaturePropertiesT> = Omit<
84
106
85
107
/**
86
108
* The (average) position of points in a cell used for clustering.
87
- * If not supplied the center of the quadbin cell is used.
109
+ * If not supplied the center of the quadbin cell or H3 cell is used.
88
110
*
89
111
* @default cell center
90
112
*/
91
- getPosition ?: Accessor < ParsedQuadbinCell < FeaturePropertiesT > , [ number , number ] > ;
113
+ getPosition ?: Accessor <
114
+ ParsedQuadbinCell < FeaturePropertiesT > | ParsedH3Cell < FeaturePropertiesT > ,
115
+ [ number , number ]
116
+ > ;
92
117
93
118
/**
94
119
* The weight of each cell used for clustering.
95
120
*
96
121
* @default 1
97
122
*/
98
- getWeight ?: Accessor < ParsedQuadbinCell < FeaturePropertiesT > , number > ;
123
+ getWeight ?: Accessor <
124
+ ParsedQuadbinCell < FeaturePropertiesT > | ParsedH3Cell < FeaturePropertiesT > ,
125
+ number
126
+ > ;
99
127
} ;
100
128
101
129
class ClusterGeoJsonLayer <
102
130
FeaturePropertiesT extends { } = { } ,
103
131
ExtraProps extends { } = { }
104
132
> extends TileLayer <
105
- ParsedQuadbinTile < FeaturePropertiesT > ,
133
+ ParsedQuadbinTile < FeaturePropertiesT > | ParsedH3Tile < FeaturePropertiesT > ,
106
134
ExtraProps & Required < _ClusterTileLayerProps < FeaturePropertiesT > >
107
135
> {
108
136
static layerName = 'ClusterGeoJsonLayer' ;
109
137
static defaultProps = defaultProps ;
110
138
state ! : TileLayer < FeaturePropertiesT > [ 'state' ] & {
111
139
data : BinaryFeatureCollection ;
112
- clusterIds : bigint [ ] ;
113
- hoveredFeatureId : bigint | number | null ;
140
+ clusterIds : ( bigint | string ) [ ] ;
141
+ hoveredFeatureId : bigint | string | number | null ;
114
142
highlightColor : number [ ] ;
115
143
aggregationCache : WeakMap < any , Map < number , ClusteredFeaturePropertiesT < FeaturePropertiesT > [ ] > > ;
144
+ scheme : string | null ;
116
145
} ;
117
146
118
147
initializeState ( ) {
119
148
super . initializeState ( ) ;
120
149
this . state . aggregationCache = new WeakMap ( ) ;
150
+ this . state . scheme = getScheme ( this . props . TilesetClass as any ) ;
151
+ }
152
+
153
+ updateState ( opts ) {
154
+ const { props} = opts ;
155
+ const scheme = getScheme ( props . TilesetClass ) ;
156
+ if ( this . state . scheme !== scheme ) {
157
+ // Clear caches when scheme changes
158
+ this . setState ( { scheme, tileset : null } ) ;
159
+ this . state . aggregationCache = new WeakMap ( ) ;
160
+ }
161
+
162
+ super . updateState ( opts ) ;
121
163
}
122
164
123
165
// eslint-disable-next-line max-statements
124
166
renderLayers ( ) : Layer | null | LayersList {
125
167
const visibleTiles = this . state . tileset ?. tiles . filter ( ( tile : Tile2DHeader ) => {
126
168
return tile . isLoaded && tile . content && this . state . tileset ! . isTileVisible ( tile ) ;
127
- } ) as Tile2DHeader < ParsedQuadbinTile < FeaturePropertiesT > > [ ] ;
128
- if ( ! visibleTiles ?. length ) {
169
+ } ) as Tile2DHeader < ParsedQuadbinTile < FeaturePropertiesT > | ParsedH3Tile < FeaturePropertiesT > > [ ] ;
170
+ if ( ! visibleTiles ?. length || ! this . state . tileset ) {
129
171
return null ;
130
172
}
131
173
visibleTiles . sort ( ( a , b ) => b . zoom - a . zoom ) ;
174
+ const { getPosition, getWeight} = this . props ;
175
+ const { aggregationCache, scheme} = this . state ;
132
176
133
- const { zoom} = this . context . viewport ;
134
- const { clusterLevel, getPosition, getWeight} = this . props ;
135
- const { aggregationCache} = this . state ;
177
+ const isH3 = scheme === 'h3' ;
136
178
137
179
const properties = extractAggregationProperties ( visibleTiles [ 0 ] ) ;
138
180
const data = [ ] as ClusteredFeaturePropertiesT < FeaturePropertiesT > [ ] ;
139
181
let needsUpdate = false ;
182
+
183
+ const aggregationLevels = this . _getAggregationLevels ( visibleTiles ) ;
184
+
140
185
for ( const tile of visibleTiles ) {
141
186
// Calculate aggregation based on viewport zoom
142
- const overZoom = Math . round ( zoom - tile . zoom ) ;
143
- const aggregationLevels = Math . round ( clusterLevel ) - overZoom ;
144
187
let tileAggregationCache = aggregationCache . get ( tile . content ) ;
145
188
if ( ! tileAggregationCache ) {
146
189
tileAggregationCache = new Map ( ) ;
@@ -152,7 +195,8 @@ class ClusterGeoJsonLayer<
152
195
aggregationLevels ,
153
196
properties ,
154
197
getPosition ,
155
- getWeight
198
+ getWeight ,
199
+ isH3 ? 'h3' : 'quadbin'
156
200
) ;
157
201
needsUpdate ||= didAggregate ;
158
202
data . push ( ...tileAggregationCache . get ( aggregationLevels ) ! ) ;
@@ -186,7 +230,9 @@ class ClusterGeoJsonLayer<
186
230
}
187
231
188
232
getPickingInfo ( params : GetPickingInfoParams ) : ClusterTileLayerPickingInfo < FeaturePropertiesT > {
189
- const info = params . info as TileLayerPickingInfo < ParsedQuadbinTile < FeaturePropertiesT > > ;
233
+ const info = params . info as TileLayerPickingInfo <
234
+ ParsedQuadbinTile < FeaturePropertiesT > | ParsedH3Tile < FeaturePropertiesT >
235
+ > ;
190
236
191
237
if ( info . index !== - 1 ) {
192
238
const { data} = params . sourceLayer ! . props ;
@@ -207,6 +253,31 @@ class ClusterGeoJsonLayer<
207
253
filterSubLayer ( ) {
208
254
return true ;
209
255
}
256
+
257
+ private _getAggregationLevels ( visibleTiles : Tile2DHeader [ ] ) : number {
258
+ const isH3 = this . state . scheme === 'h3' ;
259
+ const firstTile = visibleTiles [ 0 ] ;
260
+
261
+ // Resolution of data present in tiles
262
+ let tileResolution ;
263
+
264
+ // Resolution of tiles that should be (eventually) visible in the viewport
265
+ let viewportResolution ;
266
+ if ( isH3 ) {
267
+ tileResolution = getResolution ( firstTile . id ) ;
268
+ viewportResolution = getHexagonResolution (
269
+ this . context . viewport as WebMercatorViewport ,
270
+ ( this . state . tileset as any ) . opts . tileSize
271
+ ) ;
272
+ } else {
273
+ tileResolution = firstTile . zoom ;
274
+ viewportResolution = this . context . viewport . zoom ;
275
+ }
276
+
277
+ const resolutionDiff = Math . round ( viewportResolution - tileResolution ) ;
278
+ const aggregationLevels = Math . round ( this . props . clusterLevel ) - resolutionDiff ;
279
+ return aggregationLevels ;
280
+ }
210
281
}
211
282
212
283
// Adapter layer around ClusterLayer that converts tileJSON into TileLayer API
@@ -219,9 +290,10 @@ export default class ClusterTileLayer<
219
290
220
291
getLoadOptions ( ) : any {
221
292
const tileJSON = this . props . data as TilejsonResult ;
293
+ const scheme = tileJSON && 'scheme' in tileJSON ? tileJSON . scheme : 'quadbin' ;
222
294
return mergeLoadOptions ( super . getLoadOptions ( ) , {
223
295
fetch : { headers : { Authorization : `Bearer ${ tileJSON . accessToken } ` } } ,
224
- cartoSpatialTile : { scheme : 'quadbin' }
296
+ cartoSpatialTile : { scheme}
225
297
} ) ;
226
298
}
227
299
@@ -230,13 +302,16 @@ export default class ClusterTileLayer<
230
302
if ( ! tileJSON ) return null ;
231
303
232
304
const { tiles : data , maxresolution : maxZoom } = tileJSON ;
305
+ const isH3 = tileJSON && 'scheme' in tileJSON && tileJSON . scheme === 'h3' ;
306
+ const TilesetClass = isH3 ? H3Tileset2D : QuadbinTileset2D ;
307
+
233
308
return [
234
309
// @ts -ignore
235
310
new ClusterGeoJsonLayer ( this . props , {
236
311
id : `cluster-geojson-layer-${ this . props . id } ` ,
237
312
data,
238
313
// TODO: Tileset2D should be generic over TileIndex type
239
- TilesetClass : QuadbinTileset2D as any ,
314
+ TilesetClass : TilesetClass as any ,
240
315
maxZoom,
241
316
loadOptions : this . getLoadOptions ( )
242
317
} )
0 commit comments