@maplibre/geojson-vt 6.1.1 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,14 @@ type ClusterOrPointFeatureInternal = ClusterFeatureInternal | GeoJSONVTInternalP
11
11
 
12
12
  /** @internal */
13
13
  export type KDBushWithData = KDBush & {
14
- flatData: number[];
14
+ /**
15
+ * The projected x and y of each point or cluster, as integers, see `encode`.
16
+ */
17
+ flatCoords: Int32Array;
18
+ /**
19
+ * The other values of each point or cluster, at the `OFFSET_` positions.
20
+ */
21
+ flatData: Int32Array;
15
22
  };
16
23
 
17
24
  export const defaultClusterOptions: Required<SuperclusterOptions> = {
@@ -27,35 +34,82 @@ export const defaultClusterOptions: Required<SuperclusterOptions> = {
27
34
  map: (props) => props as Record<string, unknown>
28
35
  };
29
36
 
30
- const OFFSET_ZOOM = 2;
31
- const OFFSET_ID = 3;
32
- const OFFSET_PARENT = 4;
33
- const OFFSET_NUM = 5;
34
- const OFFSET_PROP = 6;
37
+ /**
38
+ * The highest zoom level to cluster on. A cluster id keeps its zoom level in five bits.
39
+ */
40
+ const MAX_ZOOM = 30;
41
+
42
+ /**
43
+ * Projected coordinates, which are 0 to 1 inside the world, are stored as integers with this many steps per world,
44
+ * so that they take half the memory of doubles. One step is ~4 cm at the equator.
45
+ */
46
+ const SCALE = 0x40000000;
47
+
48
+ /**
49
+ * The largest value a 32-bit integer holds.
50
+ */
51
+ const MAX_INTEGER = 0x7fffffff;
52
+
53
+ /**
54
+ * A projected coordinate in steps from the center of the world, see `SCALE`.
55
+ */
56
+ function encode(coord: number): number {
57
+ return (coord - 0.5) * SCALE;
58
+ }
59
+
60
+ function decode(value: number): number {
61
+ return value / SCALE + 0.5;
62
+ }
63
+
64
+ /**
65
+ * Whether a projected coordinate fits in a 32-bit integer. One that is more than two worlds from the center of the
66
+ * world does not, and neither does one that is not a number.
67
+ */
68
+ function fitsInteger(coord: number): boolean {
69
+ return Math.abs(encode(coord)) < MAX_INTEGER;
70
+ }
71
+
72
+ const OFFSET_ZOOM = 0;
73
+ const OFFSET_ID = 1;
74
+ const OFFSET_PARENT = 2;
75
+ const OFFSET_NUM = 3;
76
+ const OFFSET_PROP = 4;
35
77
 
36
78
  /**
37
79
  * This class allow clustering of geojson points.
38
80
  */
39
81
  export class ClusterTileIndex implements GeoJSONVTTileIndex {
40
82
  options: Required<SuperclusterOptions>;
83
+ /**
84
+ * A tree per zoom level. A zoom level where no clusters form shares the tree of the zoom level above it.
85
+ */
41
86
  trees: KDBushWithData[];
42
87
  stride: number;
43
88
  clusterProps: Record<string, unknown>[];
44
89
  points: GeoJSONVTInternalPointFeature[];
90
+ /**
91
+ * Whether points are in world coordinates, see `worldCoordinates` in {@link GeoJSONVTOptions}. Points outside the
92
+ * world are then left out of the clusters, tiles at an edge get no points from the opposite one, `getTile` returns
93
+ * `null` outside the world, and clusters come back in world coordinates.
94
+ * @experimental
95
+ */
96
+ worldCoordinates: boolean;
45
97
 
46
- constructor(options?: SuperclusterOptions) {
47
- this.options = Object.assign(Object.create(defaultClusterOptions), options) as Required<SuperclusterOptions>;
98
+ constructor(options?: SuperclusterOptions, worldCoordinates = false) {
99
+ this.options = createOptions(options);
100
+ this.worldCoordinates = worldCoordinates;
48
101
  this.trees = new Array(this.options.maxZoom + 1);
49
- this.stride = this.options.reduce ? 7 : 6;
102
+ this.stride = this.options.reduce ? 5 : 4;
50
103
  this.clusterProps = [];
51
104
  this.points = [];
52
105
  }
53
106
 
54
107
  /**
55
- * Loads GeoJSON point features and builds the internal clustering index.
56
- * @param points - GeoJSON point features to cluster.
108
+ * Loads GeoJSON Point and MultiPoint features and builds the internal clustering index.
109
+ * Each position of a MultiPoint is clustered as a point of its own, with the id and properties of its feature.
110
+ * @param points - GeoJSON Point and MultiPoint features to cluster.
57
111
  */
58
- load(points: GeoJSON.Feature<GeoJSON.Point>[]): void {
112
+ load(points: GeoJSON.Feature<GeoJSON.Point | GeoJSON.MultiPoint>[]): void {
59
113
  const features: GeoJSONVTInternalPointFeature[] = [];
60
114
 
61
115
  // Convert GeoJSON point features to GeoJSONVT internal point features
@@ -64,17 +118,20 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
64
118
  continue;
65
119
  }
66
120
 
67
- const [lng, lat] = point.geometry.coordinates;
68
- const [x, y] = [projectX(lng), projectY(lat)];
69
-
70
- const feature: GeoJSONVTInternalPointFeature = {
71
- id: point.id,
72
- type: 'Point',
73
- geometry: [x, y],
74
- tags: point.properties
75
- };
76
-
77
- features.push(feature);
121
+ const positions = point.geometry.type === 'MultiPoint' ? point.geometry.coordinates : [point.geometry.coordinates];
122
+ for (const [lng, lat] of positions) {
123
+ const [x, y] = [projectX(lng, this.worldCoordinates), projectY(lat, this.worldCoordinates)];
124
+ if (this.worldCoordinates && !isInWorld([x, y])) continue;
125
+
126
+ const feature: GeoJSONVTInternalPointFeature = {
127
+ id: point.id,
128
+ type: 'Point',
129
+ geometry: [x, y],
130
+ tags: point.properties
131
+ };
132
+
133
+ features.push(feature);
134
+ }
78
135
  }
79
136
 
80
137
  this.createIndex(features);
@@ -83,14 +140,25 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
83
140
  /**
84
141
  * @internal
85
142
  * Loads internal GeoJSONVT point features from a data source and builds the clustering index.
143
+ * Each position of a MultiPoint feature becomes a point of its own, with the id and properties of its feature.
86
144
  * @param features - {@link GeoJSONVTInternalFeature} data source features to filter and cluster.
87
145
  */
88
146
  initialize(features: GeoJSONVTInternalFeature[]): void {
89
147
  const points: GeoJSONVTInternalPointFeature[] = [];
90
148
 
91
149
  for (const feature of features) {
92
- if (feature.type !== 'Point') continue;
93
- points.push(feature);
150
+ if (feature.type === 'Point') {
151
+ if (this.worldCoordinates && !isInWorld(feature.geometry)) continue;
152
+ points.push(feature);
153
+ continue;
154
+ }
155
+ if (feature.type !== 'MultiPoint') continue;
156
+
157
+ for (let i = 0; i < feature.geometry.length; i += 3) {
158
+ const geometry = [feature.geometry[i], feature.geometry[i + 1]];
159
+ if (this.worldCoordinates && !isInWorld(geometry)) continue;
160
+ points.push({id: feature.id, type: 'Point', geometry, tags: feature.tags});
161
+ }
94
162
  }
95
163
 
96
164
  this.createIndex(points);
@@ -102,13 +170,22 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
102
170
  * @param features
103
171
  */
104
172
  updateIndex(features: GeoJSONVTInternalFeature[], _affected: GeoJSONVTInternalFeature[], options: GeoJSONVTOptions) {
105
- this.options = Object.assign(Object.create(defaultClusterOptions), options.clusterOptions) as Required<SuperclusterOptions>;
173
+ this.options = createOptions(options.clusterOptions);
106
174
  this.initialize(features);
107
175
  }
108
176
 
177
+ /**
178
+ * Builds a tree per zoom level from the points. A point whose position does not fit in an integer is left out,
179
+ * see `fitsInteger`.
180
+ */
109
181
  private createIndex(points: GeoJSONVTInternalPointFeature[]): void {
110
182
  const {log, minZoom, maxZoom} = this.options;
111
183
 
184
+ // start over, since the options can have changed since the last build
185
+ this.trees = new Array(maxZoom + 1);
186
+ this.stride = this.options.reduce ? 5 : 4;
187
+ this.clusterProps = [];
188
+
112
189
  if (log) console.time('total time');
113
190
 
114
191
  const timerId = `prepare ${points.length} points`;
@@ -116,38 +193,46 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
116
193
 
117
194
  this.points = points;
118
195
 
196
+ const stride = this.stride;
197
+ const notProcessed = maxZoom + 1;
198
+
119
199
  // generate a cluster object for each point and index input points into a KD-tree
120
- const data: number[] = [];
200
+ const coords = new Int32Array(points.length * 2);
201
+ const data = new Int32Array(points.length * stride);
202
+ let numItems = 0;
121
203
 
122
204
  for (let i = 0; i < points.length; i++) {
123
205
  const p = points[i];
124
- if (!p?.geometry) continue;
125
-
126
- let [x, y] = p.geometry;
127
- x = Math.fround(x);
128
- y = Math.fround(y);
206
+ if (!p?.geometry || !fitsInteger(p.geometry[0]) || !fitsInteger(p.geometry[1])) continue;
129
207
 
130
208
  // store internal point/cluster data in flat numeric arrays for performance
131
- data.push(
132
- x, y, // projected point coordinates
133
- Infinity, // the last zoom the point was processed at
134
- i, // index of the source feature in the original input array
135
- -1, // parent cluster id
136
- 1 // number of points in a cluster
137
- );
138
- if (this.options.reduce) data.push(0); // noop
209
+ coords[numItems * 2] = Math.round(encode(p.geometry[0])); // projected point coordinates
210
+ coords[numItems * 2 + 1] = Math.round(encode(p.geometry[1]));
211
+ const k = numItems * stride;
212
+ data[k + OFFSET_ZOOM] = notProcessed; // the last zoom the point was processed at
213
+ data[k + OFFSET_ID] = i; // index of the source feature in the original input array
214
+ data[k + OFFSET_PARENT] = -1; // parent cluster id
215
+ data[k + OFFSET_NUM] = 1; // number of points in a cluster
216
+ numItems++;
139
217
  }
140
- let tree = this.trees[maxZoom + 1] = this.createTree(data);
218
+ let tree = this.trees[maxZoom + 1] = this.createTree(coords, data, numItems);
141
219
 
142
220
  if (log) console.timeEnd(timerId);
143
221
 
144
222
  // cluster points on max zoom, then cluster the results on previous zoom, etc.;
145
223
  // results in a cluster hierarchy across zoom levels
224
+ const nextCoords = new Int32Array(numItems * 2);
225
+ const nextData = new Int32Array(numItems * stride);
146
226
  for (let z = maxZoom; z >= minZoom; z--) {
147
227
  const now = Date.now();
148
228
 
149
- // create a new set of clusters for the zoom and index them with a KD-tree
150
- tree = this.trees[z] = this.createTree(this.cluster(tree, z));
229
+ const nextNumItems = this.cluster(tree, z, nextCoords, nextData);
230
+
231
+ if (!this.isSameAsTree(tree, nextData, nextNumItems)) {
232
+ // create a new set of clusters for the zoom and index them with a KD-tree
233
+ tree = this.createTree(nextCoords.slice(0, nextNumItems * 2), nextData.slice(0, nextNumItems * stride), nextNumItems);
234
+ }
235
+ this.trees[z] = tree;
151
236
 
152
237
  if (log) console.log('z%d: %d clusters in %dms', z, tree.numItems, Date.now() - now);
153
238
  }
@@ -157,12 +242,12 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
157
242
 
158
243
  /**
159
244
  * Returns clusters and/or points within a bounding box at a given zoom level.
160
- * @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order.
245
+ * @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order, or `[minX, minY, maxX, maxY]` in world coordinates.
161
246
  * @param zoom - Zoom level to query.
162
247
  */
163
248
  public getClusters(bbox: [number, number, number, number], zoom: number): ClusterOrPointFeature[] {
164
- const clusterInternal = this.getClustersInternal(bbox, zoom);
165
- return clusterInternal.map((f) => featureToGeoJSON(f) as ClusterOrPointFeature);
249
+ const clusterInternal = this.worldCoordinates ? this.getClustersInRange(bbox, zoom) : this.getClustersInternal(bbox, zoom);
250
+ return clusterInternal.map((f) => featureToGeoJSON(f, this.worldCoordinates) as ClusterOrPointFeature);
166
251
  }
167
252
 
168
253
  private getClustersInternal(bbox: [number, number, number, number], zoom: number): ClusterOrPointFeatureInternal[] {
@@ -180,13 +265,21 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
180
265
  return easternHem.concat(westernHem);
181
266
  }
182
267
 
268
+ return this.getClustersInRange([projectX(minLng), projectY(maxLat), projectX(maxLng), projectY(minLat)], zoom);
269
+ }
270
+
271
+ /**
272
+ * The clusters and points in a box of the internal coordinates, `[minX, minY, maxX, maxY]`.
273
+ */
274
+ private getClustersInRange([minX, minY, maxX, maxY]: [number, number, number, number], zoom: number): ClusterOrPointFeatureInternal[] {
183
275
  const tree = this.trees[this.limitZoom(zoom)];
184
- const ids = tree.range(projectX(minLng), projectY(maxLat), projectX(maxLng), projectY(minLat));
276
+ const ids = tree.range(encode(minX), encode(minY), encode(maxX), encode(maxY));
277
+ const coords = tree.flatCoords;
185
278
  const data = tree.flatData;
186
279
  const clusters: ClusterOrPointFeatureInternal[] = [];
187
280
  for (const id of ids) {
188
281
  const k = this.stride * id;
189
- clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
282
+ clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(coords, id, data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
190
283
  }
191
284
  return clusters;
192
285
  }
@@ -203,18 +296,19 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
203
296
  const tree = this.trees[originZoom];
204
297
  if (!tree) throw clusterError;
205
298
 
299
+ const coords = tree.flatCoords;
206
300
  const data = tree.flatData;
207
- if (originId * this.stride >= data.length) throw clusterError;
301
+ if (originId >= tree.numItems) throw clusterError;
208
302
 
209
- const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
210
- const x = data[originId * this.stride];
211
- const y = data[originId * this.stride + 1];
303
+ const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1)) * SCALE;
304
+ const x = coords[originId * 2];
305
+ const y = coords[originId * 2 + 1];
212
306
  const ids = tree.within(x, y, r);
213
307
  const children: ClusterOrPointFeature[] = [];
214
308
  for (const id of ids) {
215
309
  const k = id * this.stride;
216
310
  if (data[k + OFFSET_PARENT] === clusterId) {
217
- children.push(data[k + OFFSET_NUM] > 1 ? getClusterGeoJSON(data, k, this.clusterProps) : featureToGeoJSON(this.points[data[k + OFFSET_ID]]) as GeoJSON.Feature<GeoJSON.Point>);
311
+ children.push(data[k + OFFSET_NUM] > 1 ? getClusterGeoJSON(coords, id, data, k, this.clusterProps, this.worldCoordinates) : featureToGeoJSON(this.points[data[k + OFFSET_ID]], this.worldCoordinates) as GeoJSON.Feature<GeoJSON.Point>);
218
312
  }
219
313
  }
220
314
 
@@ -251,10 +345,11 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
251
345
  return null;
252
346
  }
253
347
  const z2 = Math.pow(2, z);
348
+ if (this.worldCoordinates && (x < 0 || x >= z2 || y < 0 || y >= z2)) return null;
254
349
  const {extent, radius} = this.options;
255
350
  const p = radius / extent;
256
- const top = (y - p) / z2;
257
- const bottom = (y + 1 + p) / z2;
351
+ const top = encode((y - p) / z2);
352
+ const bottom = encode((y + 1 + p) / z2);
258
353
 
259
354
  const tile: GeoJSONVTTile = {
260
355
  transformed: true,
@@ -266,18 +361,18 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
266
361
  };
267
362
 
268
363
  this.addTileFeatures(
269
- tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom),
270
- tree.flatData, x, y, z2, tile);
364
+ tree.range(encode((x - p) / z2), top, encode((x + 1 + p) / z2), bottom),
365
+ tree, x, y, z2, tile);
271
366
 
272
- if (x === 0) {
367
+ if (!this.worldCoordinates && x === 0) {
273
368
  this.addTileFeatures(
274
- tree.range(1 - p / z2, top, 1, bottom),
275
- tree.flatData, z2, y, z2, tile);
369
+ tree.range(encode(1 - p / z2), top, encode(1), bottom),
370
+ tree, z2, y, z2, tile);
276
371
  }
277
- if (x === z2 - 1) {
372
+ if (!this.worldCoordinates && x === z2 - 1) {
278
373
  this.addTileFeatures(
279
- tree.range(0, top, p / z2, bottom),
280
- tree.flatData, -1, y, z2, tile);
374
+ tree.range(encode(0), top, encode(p / z2), bottom),
375
+ tree, -1, y, z2, tile);
281
376
  }
282
377
 
283
378
  return tile;
@@ -319,16 +414,32 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
319
414
  return skipped;
320
415
  }
321
416
 
322
- private createTree(data: number[]): KDBushWithData {
323
- const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array) as unknown as KDBushWithData;
324
- for (let i = 0; i < data.length; i += this.stride) tree.add(data[i], data[i + 1]);
417
+ private createTree(coords: Int32Array, data: Int32Array, numItems: number): KDBushWithData {
418
+ const tree = new KDBush(numItems, this.options.nodeSize, Int32Array) as unknown as KDBushWithData;
419
+ for (let i = 0; i < numItems; i++) tree.add(coords[i * 2], coords[i * 2 + 1]);
325
420
  tree.finish();
421
+ tree.flatCoords = coords;
326
422
  tree.flatData = data;
327
423
  tree.data = null; // clear original data to free memory as it isn't used later on.
328
424
  return tree;
329
425
  }
330
426
 
331
- private addTileFeatures(ids: number[], data: number[], x: number, y: number, z2: number, tile: GeoJSONVTTile): void {
427
+ /**
428
+ * Whether clustering a tree left all its points and clusters as they were, in the same order.
429
+ */
430
+ private isSameAsTree(tree: KDBushWithData, nextData: Int32Array, nextNumItems: number): boolean {
431
+ if (nextNumItems !== tree.numItems) return false;
432
+
433
+ const data = tree.flatData;
434
+ for (let k = OFFSET_ID; k < nextNumItems * this.stride; k += this.stride) {
435
+ if (nextData[k] !== data[k]) return false;
436
+ }
437
+ return true;
438
+ }
439
+
440
+ private addTileFeatures(ids: number[], tree: KDBushWithData, x: number, y: number, z2: number, tile: GeoJSONVTTile): void {
441
+ const coords = tree.flatCoords;
442
+ const data = tree.flatData;
332
443
  for (const i of ids) {
333
444
  const k = i * this.stride;
334
445
  const isCluster = data[k + OFFSET_NUM] > 1;
@@ -338,8 +449,8 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
338
449
  let py: number;
339
450
  if (isCluster) {
340
451
  tags = getClusterProperties(data, k, this.clusterProps);
341
- px = data[k];
342
- py = data[k + 1];
452
+ px = decode(coords[i * 2]);
453
+ py = decode(coords[i * 2 + 1]);
343
454
  } else {
344
455
  const p = this.points[data[k + OFFSET_ID]];
345
456
  tags = p.tags;
@@ -375,30 +486,38 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
375
486
  return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
376
487
  }
377
488
 
378
- private cluster(tree: KDBushWithData, zoom: number): number[] {
379
- const {radius, extent, reduce, minPoints} = this.options;
380
- const r = radius / (extent * Math.pow(2, zoom));
489
+ /**
490
+ * Clusters the points of a tree on a zoom level, writing the result to `nextCoords` and `nextData`.
491
+ * @returns the number of points and clusters written
492
+ */
493
+ private cluster(tree: KDBushWithData, zoom: number, nextCoords: Int32Array, nextData: Int32Array): number {
494
+ const {radius, extent, reduce, minPoints, maxZoom} = this.options;
495
+ const r = radius / (extent * Math.pow(2, zoom)) * SCALE;
496
+ const notProcessed = maxZoom + 1;
497
+ const coords = tree.flatCoords;
381
498
  const data = tree.flatData;
382
- const nextData: number[] = [];
383
499
  const stride = this.stride;
500
+ const neighborIds = new Uint32Array(tree.numItems);
501
+ let nextNumItems = 0;
384
502
 
385
503
  // loop through each point
386
- for (let i = 0; i < data.length; i += stride) {
504
+ for (let index = 0; index < tree.numItems; index++) {
505
+ const i = index * stride;
387
506
  // if we've already visited the point at this zoom level, skip it
388
507
  if (data[i + OFFSET_ZOOM] <= zoom) continue;
389
508
  data[i + OFFSET_ZOOM] = zoom;
390
509
 
391
510
  // find all nearby points
392
- const x = data[i];
393
- const y = data[i + 1];
394
- const neighborIds = tree.within(data[i], data[i + 1], r);
511
+ const x = coords[index * 2];
512
+ const y = coords[index * 2 + 1];
513
+ const numNeighbors = tree.withinInto(x, y, r, neighborIds);
395
514
 
396
515
  const numPointsOrigin = data[i + OFFSET_NUM];
397
516
  let numPoints = numPointsOrigin;
398
517
 
399
518
  // count the number of points in a potential cluster
400
- for (const neighborId of neighborIds) {
401
- const k = neighborId * stride;
519
+ for (let n = 0; n < numNeighbors; n++) {
520
+ const k = neighborIds[n] * stride;
402
521
  // filter out neighbors that are already processed
403
522
  if (data[k + OFFSET_ZOOM] > zoom) numPoints += data[k + OFFSET_NUM];
404
523
  }
@@ -412,17 +531,18 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
412
531
  let clusterPropIndex = -1;
413
532
 
414
533
  // encode both zoom and point index on which the cluster originated -- offset by total length of features
415
- const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length;
534
+ const id = (index << 5) + (zoom + 1) + this.points.length;
416
535
 
417
- for (const neighborId of neighborIds) {
418
- const k = neighborId * stride;
536
+ for (let n = 0; n < numNeighbors; n++) {
537
+ const neighborIndex = neighborIds[n];
538
+ const k = neighborIndex * stride;
419
539
 
420
540
  if (data[k + OFFSET_ZOOM] <= zoom) continue;
421
541
  data[k + OFFSET_ZOOM] = zoom; // save the zoom (so it doesn't get processed twice)
422
542
 
423
543
  const numPoints2 = data[k + OFFSET_NUM];
424
- wx += data[k] * numPoints2; // accumulate coordinates for calculating weighted center
425
- wy += data[k + 1] * numPoints2;
544
+ wx += coords[neighborIndex * 2] * numPoints2; // accumulate coordinates for calculating weighted center
545
+ wy += coords[neighborIndex * 2 + 1] * numPoints2;
426
546
 
427
547
  data[k + OFFSET_PARENT] = id;
428
548
 
@@ -437,24 +557,38 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
437
557
  }
438
558
 
439
559
  data[i + OFFSET_PARENT] = id;
440
- nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints);
441
- if (reduce) nextData.push(clusterPropIndex);
560
+ nextCoords[nextNumItems * 2] = Math.round(wx / numPoints);
561
+ nextCoords[nextNumItems * 2 + 1] = Math.round(wy / numPoints);
562
+ const next = nextNumItems * stride;
563
+ nextData[next + OFFSET_ZOOM] = notProcessed;
564
+ nextData[next + OFFSET_ID] = id;
565
+ nextData[next + OFFSET_PARENT] = -1;
566
+ nextData[next + OFFSET_NUM] = numPoints;
567
+ if (reduce) nextData[next + OFFSET_PROP] = clusterPropIndex;
568
+ nextNumItems++;
442
569
 
443
570
  } else { // left points as unclustered
444
- for (let j = 0; j < stride; j++) nextData.push(data[i + j]);
571
+ nextCoords[nextNumItems * 2] = x;
572
+ nextCoords[nextNumItems * 2 + 1] = y;
573
+ for (let j = 0; j < stride; j++) nextData[nextNumItems * stride + j] = data[i + j];
574
+ nextNumItems++;
445
575
 
446
576
  if (numPoints > 1) {
447
- for (const neighborId of neighborIds) {
448
- const k = neighborId * stride;
577
+ for (let n = 0; n < numNeighbors; n++) {
578
+ const neighborIndex = neighborIds[n];
579
+ const k = neighborIndex * stride;
449
580
  if (data[k + OFFSET_ZOOM] <= zoom) continue;
450
581
  data[k + OFFSET_ZOOM] = zoom;
451
- for (let j = 0; j < stride; j++) nextData.push(data[k + j]);
582
+ nextCoords[nextNumItems * 2] = coords[neighborIndex * 2];
583
+ nextCoords[nextNumItems * 2 + 1] = coords[neighborIndex * 2 + 1];
584
+ for (let j = 0; j < stride; j++) nextData[nextNumItems * stride + j] = data[k + j];
585
+ nextNumItems++;
452
586
  }
453
587
  }
454
588
  }
455
589
  }
456
590
 
457
- return nextData;
591
+ return nextNumItems;
458
592
  }
459
593
 
460
594
  // get index of the point from which the cluster originated
@@ -467,7 +601,7 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
467
601
  return (clusterId - this.points.length) % 32;
468
602
  }
469
603
 
470
- private map(data: number[], i: number, clone?: boolean): Record<string, unknown> {
604
+ private map(data: Int32Array, i: number, clone?: boolean): Record<string, unknown> {
471
605
  if (data[i + OFFSET_NUM] > 1) {
472
606
  const props = this.clusterProps[data[i + OFFSET_PROP]];
473
607
  return clone ? Object.assign({}, props) : props;
@@ -478,28 +612,35 @@ export class ClusterTileIndex implements GeoJSONVTTileIndex {
478
612
  }
479
613
  }
480
614
 
481
- function getClusterFeature(data: number[], i: number, clusterProps: Record<string, unknown>[]): ClusterFeatureInternal {
615
+ function createOptions(options?: SuperclusterOptions): Required<SuperclusterOptions> {
616
+ const result = Object.assign(Object.create(defaultClusterOptions), options) as Required<SuperclusterOptions>;
617
+ result.minZoom = Math.min(result.minZoom, MAX_ZOOM);
618
+ result.maxZoom = Math.min(result.maxZoom, MAX_ZOOM);
619
+ return result;
620
+ }
621
+
622
+ function getClusterFeature(coords: Int32Array, index: number, data: Int32Array, i: number, clusterProps: Record<string, unknown>[]): ClusterFeatureInternal {
482
623
  return {
483
624
  id: data[i + OFFSET_ID],
484
625
  type: 'Point',
485
626
  tags: getClusterProperties(data, i, clusterProps),
486
- geometry: [data[i], data[i + 1]]
627
+ geometry: [decode(coords[index * 2]), decode(coords[index * 2 + 1])]
487
628
  };
488
629
  }
489
630
 
490
- function getClusterGeoJSON(data: number[], i: number, clusterProps: Record<string, unknown>[]): ClusterFeature {
631
+ function getClusterGeoJSON(coords: Int32Array, index: number, data: Int32Array, i: number, clusterProps: Record<string, unknown>[], worldCoordinates: boolean): ClusterFeature {
491
632
  return {
492
633
  type: 'Feature',
493
634
  id: data[i + OFFSET_ID],
494
635
  properties: getClusterProperties(data, i, clusterProps),
495
636
  geometry: {
496
637
  type: 'Point',
497
- coordinates: [unprojectX(data[i]), unprojectY(data[i + 1])]
638
+ coordinates: [unprojectX(decode(coords[index * 2]), worldCoordinates), unprojectY(decode(coords[index * 2 + 1]), worldCoordinates)]
498
639
  }
499
640
  };
500
641
  }
501
642
 
502
- function getClusterProperties(data: number[], i: number, clusterProps: Record<string, unknown>[]): ClusterProperties {
643
+ function getClusterProperties(data: Int32Array, i: number, clusterProps: Record<string, unknown>[]): ClusterProperties {
503
644
  const count = data[i + OFFSET_NUM];
504
645
  const abbrev =
505
646
  count >= 10000 ? `${Math.round(count / 1000) }k` :
@@ -514,3 +655,7 @@ function getClusterProperties(data: number[], i: number, clusterProps: Record<st
514
655
  point_count_abbreviated: abbrev
515
656
  });
516
657
  }
658
+
659
+ function isInWorld(geometry: number[]): boolean {
660
+ return geometry[0] >= 0 && geometry[0] <= 1 && geometry[1] >= 0 && geometry[1] <= 1;
661
+ }