@maplibre/geojson-vt 5.0.3 → 6.0.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.
Files changed (65) hide show
  1. package/README.md +3 -13
  2. package/dist/clip.d.ts +22 -0
  3. package/dist/clip.d.ts.map +1 -0
  4. package/dist/clip.test.d.ts +2 -0
  5. package/dist/clip.test.d.ts.map +1 -0
  6. package/dist/cluster-tile-index.d.ts +76 -0
  7. package/dist/cluster-tile-index.d.ts.map +1 -0
  8. package/dist/cluster-tile-index.test.d.ts +2 -0
  9. package/dist/cluster-tile-index.test.d.ts.map +1 -0
  10. package/dist/convert.d.ts +17 -0
  11. package/dist/convert.d.ts.map +1 -0
  12. package/dist/deconvert.d.ts +19 -0
  13. package/dist/deconvert.d.ts.map +1 -0
  14. package/dist/deconvert.test.d.ts +2 -0
  15. package/dist/deconvert.test.d.ts.map +1 -0
  16. package/dist/definitions.d.ts +241 -0
  17. package/dist/definitions.d.ts.map +1 -0
  18. package/dist/difference.d.ts +67 -0
  19. package/dist/difference.d.ts.map +1 -0
  20. package/dist/difference.test.d.ts +2 -0
  21. package/dist/difference.test.d.ts.map +1 -0
  22. package/dist/feature.d.ts +20 -0
  23. package/dist/feature.d.ts.map +1 -0
  24. package/dist/geojson-to-tile.d.ts +35 -0
  25. package/dist/geojson-to-tile.d.ts.map +1 -0
  26. package/dist/geojson-vt-dev.js +1582 -478
  27. package/dist/geojson-vt.js +1 -1
  28. package/dist/geojson-vt.mjs +1250 -473
  29. package/dist/geojson-vt.mjs.map +1 -1
  30. package/dist/geojsonvt.d.ts +76 -0
  31. package/dist/geojsonvt.d.ts.map +1 -0
  32. package/dist/geojsonvt.test.d.ts +2 -0
  33. package/dist/geojsonvt.test.d.ts.map +1 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/simplify.d.ts +9 -0
  37. package/dist/simplify.d.ts.map +1 -0
  38. package/dist/simplify.test.d.ts +2 -0
  39. package/dist/simplify.test.d.ts.map +1 -0
  40. package/dist/tile-index.d.ts +51 -0
  41. package/dist/tile-index.d.ts.map +1 -0
  42. package/dist/tile.d.ts +12 -0
  43. package/dist/tile.d.ts.map +1 -0
  44. package/dist/transform.d.ts +10 -0
  45. package/dist/transform.d.ts.map +1 -0
  46. package/dist/wrap.d.ts +3 -0
  47. package/dist/wrap.d.ts.map +1 -0
  48. package/package.json +26 -12
  49. package/src/clip.ts +119 -81
  50. package/src/cluster-tile-index.test.ts +205 -0
  51. package/src/cluster-tile-index.ts +513 -0
  52. package/src/convert.ts +97 -75
  53. package/src/deconvert.test.ts +153 -0
  54. package/src/deconvert.ts +92 -0
  55. package/src/definitions.ts +196 -18
  56. package/src/difference.ts +3 -3
  57. package/src/feature.ts +11 -4
  58. package/src/geojson-to-tile.ts +58 -0
  59. package/src/geojsonvt.test.ts +39 -0
  60. package/src/geojsonvt.ts +209 -0
  61. package/src/index.ts +27 -378
  62. package/src/tile-index.ts +310 -0
  63. package/src/tile.ts +92 -103
  64. package/src/transform.ts +41 -39
  65. package/src/wrap.ts +4 -4
@@ -0,0 +1,513 @@
1
+ import KDBush from 'kdbush';
2
+ import {projectX, projectY} from './convert';
3
+ import {unprojectX, unprojectY, featureToGeoJSON} from './deconvert';
4
+ import type {ClusterFeature, ClusterOrPointFeature, ClusterProperties, GeoJSONVTTileIndex, GeoJSONVTFeature, GeoJSONVTInternalFeature, GeoJSONVTInternalPointFeature, GeoJSONVTOptions, GeoJSONVTTile, SuperclusterOptions} from './definitions';
5
+
6
+ type ClusterFeatureInternal = GeoJSONVTInternalPointFeature & {
7
+ tags: ClusterProperties;
8
+ };
9
+
10
+ type ClusterOrPointFeatureInternal = ClusterFeatureInternal | GeoJSONVTInternalPointFeature;
11
+
12
+ /** @internal */
13
+ export type KDBushWithData = KDBush & {
14
+ flatData: number[];
15
+ };
16
+
17
+ export const defaultClusterOptions: Required<SuperclusterOptions> = {
18
+ minZoom: 0,
19
+ maxZoom: 16,
20
+ minPoints: 2,
21
+ radius: 40,
22
+ extent: 512,
23
+ nodeSize: 64,
24
+ log: false,
25
+ generateId: false,
26
+ reduce: null,
27
+ map: (props) => props as Record<string, unknown>
28
+ };
29
+
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;
35
+
36
+ /**
37
+ * This class allow clustering of geojson points.
38
+ */
39
+ export class ClusterTileIndex implements GeoJSONVTTileIndex {
40
+ options: Required<SuperclusterOptions>;
41
+ trees: KDBushWithData[];
42
+ stride: number;
43
+ clusterProps: Record<string, unknown>[];
44
+ points: GeoJSONVTInternalPointFeature[];
45
+
46
+ constructor(options?: SuperclusterOptions) {
47
+ this.options = Object.assign(Object.create(defaultClusterOptions), options) as Required<SuperclusterOptions>;
48
+ this.trees = new Array(this.options.maxZoom + 1);
49
+ this.stride = this.options.reduce ? 7 : 6;
50
+ this.clusterProps = [];
51
+ this.points = [];
52
+ }
53
+
54
+ /**
55
+ * Loads GeoJSON point features and builds the internal clustering index.
56
+ * @param points - GeoJSON point features to cluster.
57
+ */
58
+ load(points: GeoJSON.Feature<GeoJSON.Point>[]): void {
59
+ const features: GeoJSONVTInternalPointFeature[] = [];
60
+
61
+ // Convert GeoJSON point features to GeoJSONVT internal point features
62
+ for (const point of points) {
63
+ if (!point.geometry) {
64
+ continue;
65
+ }
66
+
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);
78
+ }
79
+
80
+ this.createIndex(features);
81
+ }
82
+
83
+ /**
84
+ * @internal
85
+ * Loads internal GeoJSONVT point features from a data source and builds the clustering index.
86
+ * @param features - {@link GeoJSONVTInternalFeature} data source features to filter and cluster.
87
+ */
88
+ initialize(features: GeoJSONVTInternalFeature[]): void {
89
+ const points: GeoJSONVTInternalPointFeature[] = [];
90
+
91
+ for (const feature of features) {
92
+ if (feature.type !== 'Point') continue;
93
+ points.push(feature);
94
+ }
95
+
96
+ this.createIndex(points);
97
+ }
98
+
99
+ /**
100
+ * @internal
101
+ * Updates the cluster data by rebuilding.
102
+ * @param features
103
+ */
104
+ updateIndex(features: GeoJSONVTInternalFeature[], _affected: GeoJSONVTInternalFeature[], options: GeoJSONVTOptions) {
105
+ this.options = Object.assign(Object.create(defaultClusterOptions), options.clusterOptions) as Required<SuperclusterOptions>;
106
+ this.initialize(features);
107
+ }
108
+
109
+ private createIndex(points: GeoJSONVTInternalPointFeature[]): void {
110
+ const {log, minZoom, maxZoom} = this.options;
111
+
112
+ if (log) console.time('total time');
113
+
114
+ const timerId = `prepare ${points.length} points`;
115
+ if (log) console.time(timerId);
116
+
117
+ this.points = points;
118
+
119
+ // generate a cluster object for each point and index input points into a KD-tree
120
+ const data: number[] = [];
121
+
122
+ for (let i = 0; i < points.length; i++) {
123
+ 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);
129
+
130
+ // 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
139
+ }
140
+ let tree = this.trees[maxZoom + 1] = this.createTree(data);
141
+
142
+ if (log) console.timeEnd(timerId);
143
+
144
+ // cluster points on max zoom, then cluster the results on previous zoom, etc.;
145
+ // results in a cluster hierarchy across zoom levels
146
+ for (let z = maxZoom; z >= minZoom; z--) {
147
+ const now = Date.now();
148
+
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));
151
+
152
+ if (log) console.log('z%d: %d clusters in %dms', z, tree.numItems, Date.now() - now);
153
+ }
154
+
155
+ if (log) console.timeEnd('total time');
156
+ }
157
+
158
+ /**
159
+ * 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.
161
+ * @param zoom - Zoom level to query.
162
+ */
163
+ 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);
166
+ }
167
+
168
+ private getClustersInternal(bbox: [number, number, number, number], zoom: number): ClusterOrPointFeatureInternal[] {
169
+ let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
170
+ const minLat = Math.max(-90, Math.min(90, bbox[1]));
171
+ let maxLng = bbox[2] === 180 ? 180 : ((bbox[2] + 180) % 360 + 360) % 360 - 180;
172
+ const maxLat = Math.max(-90, Math.min(90, bbox[3]));
173
+
174
+ if (bbox[2] - bbox[0] >= 360) {
175
+ minLng = -180;
176
+ maxLng = 180;
177
+ } else if (minLng > maxLng) {
178
+ const easternHem = this.getClustersInternal([minLng, minLat, 180, maxLat], zoom);
179
+ const westernHem = this.getClustersInternal([-180, minLat, maxLng, maxLat], zoom);
180
+ return easternHem.concat(westernHem);
181
+ }
182
+
183
+ const tree = this.trees[this.limitZoom(zoom)];
184
+ const ids = tree.range(projectX(minLng), projectY(maxLat), projectX(maxLng), projectY(minLat));
185
+ const data = tree.flatData;
186
+ const clusters: ClusterOrPointFeatureInternal[] = [];
187
+ for (const id of ids) {
188
+ const k = this.stride * id;
189
+ clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
190
+ }
191
+ return clusters;
192
+ }
193
+
194
+ /**
195
+ * Returns the immediate children (clusters or points) of a cluster as GeoJSON.
196
+ * @param clusterId - The target cluster id.
197
+ */
198
+ getChildren(clusterId: number): ClusterOrPointFeature[] {
199
+ const originId = this.getOriginId(clusterId);
200
+ const originZoom = this.getOriginZoom(clusterId);
201
+ const clusterError = new Error('No cluster with the specified id: ' + clusterId);
202
+
203
+ const tree = this.trees[originZoom];
204
+ if (!tree) throw clusterError;
205
+
206
+ const data = tree.flatData;
207
+ if (originId * this.stride >= data.length) throw clusterError;
208
+
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];
212
+ const ids = tree.within(x, y, r);
213
+ const children: ClusterOrPointFeature[] = [];
214
+ for (const id of ids) {
215
+ const k = id * this.stride;
216
+ 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>);
218
+ }
219
+ }
220
+
221
+ if (children.length === 0) throw clusterError;
222
+
223
+ return children;
224
+ }
225
+
226
+ /**
227
+ * Returns leaf point features under a cluster, paginated by `limit` and `offset`.
228
+ * @param clusterId - The target cluster id.
229
+ * @param limit - Maximum number of points to return (defaults to `10`).
230
+ * @param offset - Number of points to skip before collecting results (defaults to `0`).
231
+ */
232
+ getLeaves(clusterId: number, limit?: number, offset?: number): GeoJSON.Feature<GeoJSON.Point>[] {
233
+ limit = limit || 10;
234
+ offset = offset || 0;
235
+
236
+ const leaves: GeoJSON.Feature<GeoJSON.Point>[] = [];
237
+ this.appendLeaves(leaves, clusterId, limit, offset, 0);
238
+
239
+ return leaves;
240
+ }
241
+
242
+ /**
243
+ * Generates a vector-tile-like representation of a single tile.
244
+ * @param z - Tile zoom.
245
+ * @param x - Tile x coordinate.
246
+ * @param y - Tile y coordinate.
247
+ */
248
+ getTile(z: number, x: number, y: number): GeoJSONVTTile | null {
249
+ const tree = this.trees[this.limitZoom(z)];
250
+ const z2 = Math.pow(2, z);
251
+ const {extent, radius} = this.options;
252
+ const p = radius / extent;
253
+ const top = (y - p) / z2;
254
+ const bottom = (y + 1 + p) / z2;
255
+
256
+ const tile: GeoJSONVTTile = {
257
+ transformed: true,
258
+ features: [],
259
+ source: null,
260
+ x: x,
261
+ y: y,
262
+ z: z
263
+ };
264
+
265
+ this.addTileFeatures(
266
+ tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom),
267
+ tree.flatData, x, y, z2, tile);
268
+
269
+ if (x === 0) {
270
+ this.addTileFeatures(
271
+ tree.range(1 - p / z2, top, 1, bottom),
272
+ tree.flatData, z2, y, z2, tile);
273
+ }
274
+ if (x === z2 - 1) {
275
+ this.addTileFeatures(
276
+ tree.range(0, top, p / z2, bottom),
277
+ tree.flatData, -1, y, z2, tile);
278
+ }
279
+
280
+ return tile;
281
+ }
282
+
283
+ /**
284
+ * Returns the zoom level at which a cluster expands into multiple children.
285
+ * @param clusterId - The target cluster id.
286
+ */
287
+ getClusterExpansionZoom(clusterId: number): number {
288
+ return this.getOriginZoom(clusterId);
289
+ }
290
+
291
+ private appendLeaves(result: GeoJSON.Feature<GeoJSON.Point>[], clusterId: number, limit: number, offset: number, skipped: number): number {
292
+ const children = this.getChildren(clusterId);
293
+
294
+ for (const child of children) {
295
+ const props = child.properties as ClusterProperties | null;
296
+
297
+ if (props?.cluster) {
298
+ if (skipped + props.point_count <= offset) {
299
+ // skip the whole cluster
300
+ skipped += props.point_count;
301
+ } else {
302
+ // enter the cluster
303
+ skipped = this.appendLeaves(result, props.cluster_id, limit, offset, skipped);
304
+ // exit the cluster
305
+ }
306
+ } else if (skipped < offset) {
307
+ // skip a single point
308
+ skipped++;
309
+ } else {
310
+ // add a single point
311
+ result.push(child as GeoJSON.Feature<GeoJSON.Point>);
312
+ }
313
+ if (result.length === limit) break;
314
+ }
315
+
316
+ return skipped;
317
+ }
318
+
319
+ private createTree(data: number[]): KDBushWithData {
320
+ const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array) as unknown as KDBushWithData;
321
+ for (let i = 0; i < data.length; i += this.stride) tree.add(data[i], data[i + 1]);
322
+ tree.finish();
323
+ tree.flatData = data;
324
+ tree.data = null; // clear original data to free memory as it isn't used later on.
325
+ return tree;
326
+ }
327
+
328
+ private addTileFeatures(ids: number[], data: number[], x: number, y: number, z2: number, tile: GeoJSONVTTile): void {
329
+ for (const i of ids) {
330
+ const k = i * this.stride;
331
+ const isCluster = data[k + OFFSET_NUM] > 1;
332
+
333
+ let tags: GeoJSON.GeoJsonProperties | ClusterProperties;
334
+ let px: number;
335
+ let py: number;
336
+ if (isCluster) {
337
+ tags = getClusterProperties(data, k, this.clusterProps);
338
+ px = data[k];
339
+ py = data[k + 1];
340
+ } else {
341
+ const p = this.points[data[k + OFFSET_ID]];
342
+ tags = p.tags;
343
+ [px, py] = p.geometry;
344
+ }
345
+
346
+ const f: GeoJSONVTFeature = {
347
+ type: 1,
348
+ geometry: [[
349
+ Math.round(this.options.extent * (px * z2 - x)),
350
+ Math.round(this.options.extent * (py * z2 - y))
351
+ ]],
352
+ tags
353
+ };
354
+
355
+ // assign id
356
+ let id: number | string | undefined;
357
+ if (isCluster || this.options.generateId) {
358
+ // optionally generate id for points
359
+ id = data[k + OFFSET_ID];
360
+ } else {
361
+ // keep id if already assigned
362
+ id = this.points[data[k + OFFSET_ID]].id as number | string | undefined;
363
+ }
364
+
365
+ if (id !== undefined) f.id = id;
366
+
367
+ tile.features.push(f);
368
+ }
369
+ }
370
+
371
+ private limitZoom(z: number): number {
372
+ return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
373
+ }
374
+
375
+ private cluster(tree: KDBushWithData, zoom: number): number[] {
376
+ const {radius, extent, reduce, minPoints} = this.options;
377
+ const r = radius / (extent * Math.pow(2, zoom));
378
+ const data = tree.flatData;
379
+ const nextData: number[] = [];
380
+ const stride = this.stride;
381
+
382
+ // loop through each point
383
+ for (let i = 0; i < data.length; i += stride) {
384
+ // if we've already visited the point at this zoom level, skip it
385
+ if (data[i + OFFSET_ZOOM] <= zoom) continue;
386
+ data[i + OFFSET_ZOOM] = zoom;
387
+
388
+ // find all nearby points
389
+ const x = data[i];
390
+ const y = data[i + 1];
391
+ const neighborIds = tree.within(data[i], data[i + 1], r);
392
+
393
+ const numPointsOrigin = data[i + OFFSET_NUM];
394
+ let numPoints = numPointsOrigin;
395
+
396
+ // count the number of points in a potential cluster
397
+ for (const neighborId of neighborIds) {
398
+ const k = neighborId * stride;
399
+ // filter out neighbors that are already processed
400
+ if (data[k + OFFSET_ZOOM] > zoom) numPoints += data[k + OFFSET_NUM];
401
+ }
402
+
403
+ // if there were neighbors to merge, and there are enough points to form a cluster
404
+ if (numPoints > numPointsOrigin && numPoints >= minPoints) {
405
+ let wx = x * numPointsOrigin;
406
+ let wy = y * numPointsOrigin;
407
+
408
+ let clusterProperties: Record<string, unknown> | undefined;
409
+ let clusterPropIndex = -1;
410
+
411
+ // encode both zoom and point index on which the cluster originated -- offset by total length of features
412
+ const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length;
413
+
414
+ for (const neighborId of neighborIds) {
415
+ const k = neighborId * stride;
416
+
417
+ if (data[k + OFFSET_ZOOM] <= zoom) continue;
418
+ data[k + OFFSET_ZOOM] = zoom; // save the zoom (so it doesn't get processed twice)
419
+
420
+ const numPoints2 = data[k + OFFSET_NUM];
421
+ wx += data[k] * numPoints2; // accumulate coordinates for calculating weighted center
422
+ wy += data[k + 1] * numPoints2;
423
+
424
+ data[k + OFFSET_PARENT] = id;
425
+
426
+ if (reduce) {
427
+ if (!clusterProperties) {
428
+ clusterProperties = this.map(data, i, true);
429
+ clusterPropIndex = this.clusterProps.length;
430
+ this.clusterProps.push(clusterProperties);
431
+ }
432
+ reduce(clusterProperties, this.map(data, k));
433
+ }
434
+ }
435
+
436
+ data[i + OFFSET_PARENT] = id;
437
+ nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints);
438
+ if (reduce) nextData.push(clusterPropIndex);
439
+
440
+ } else { // left points as unclustered
441
+ for (let j = 0; j < stride; j++) nextData.push(data[i + j]);
442
+
443
+ if (numPoints > 1) {
444
+ for (const neighborId of neighborIds) {
445
+ const k = neighborId * stride;
446
+ if (data[k + OFFSET_ZOOM] <= zoom) continue;
447
+ data[k + OFFSET_ZOOM] = zoom;
448
+ for (let j = 0; j < stride; j++) nextData.push(data[k + j]);
449
+ }
450
+ }
451
+ }
452
+ }
453
+
454
+ return nextData;
455
+ }
456
+
457
+ // get index of the point from which the cluster originated
458
+ private getOriginId(clusterId: number): number {
459
+ return (clusterId - this.points.length) >> 5;
460
+ }
461
+
462
+ // get zoom of the point from which the cluster originated
463
+ private getOriginZoom(clusterId: number): number {
464
+ return (clusterId - this.points.length) % 32;
465
+ }
466
+
467
+ private map(data: number[], i: number, clone?: boolean): Record<string, unknown> {
468
+ if (data[i + OFFSET_NUM] > 1) {
469
+ const props = this.clusterProps[data[i + OFFSET_PROP]];
470
+ return clone ? Object.assign({}, props) : props;
471
+ }
472
+ const original = this.points[data[i + OFFSET_ID]].tags;
473
+ const result = this.options.map(original);
474
+ return clone && result === original ? Object.assign({}, result) : result;
475
+ }
476
+ }
477
+
478
+ function getClusterFeature(data: number[], i: number, clusterProps: Record<string, unknown>[]): ClusterFeatureInternal {
479
+ return {
480
+ id: data[i + OFFSET_ID],
481
+ type: 'Point',
482
+ tags: getClusterProperties(data, i, clusterProps),
483
+ geometry: [data[i], data[i + 1]]
484
+ };
485
+ }
486
+
487
+ function getClusterGeoJSON(data: number[], i: number, clusterProps: Record<string, unknown>[]): ClusterFeature {
488
+ return {
489
+ type: 'Feature',
490
+ id: data[i + OFFSET_ID],
491
+ properties: getClusterProperties(data, i, clusterProps),
492
+ geometry: {
493
+ type: 'Point',
494
+ coordinates: [unprojectX(data[i]), unprojectY(data[i + 1])]
495
+ }
496
+ };
497
+ }
498
+
499
+ function getClusterProperties(data: number[], i: number, clusterProps: Record<string, unknown>[]): ClusterProperties {
500
+ const count = data[i + OFFSET_NUM];
501
+ const abbrev =
502
+ count >= 10000 ? `${Math.round(count / 1000) }k` :
503
+ count >= 1000 ? `${Math.round(count / 100) / 10 }k` : count;
504
+ const propIndex = data[i + OFFSET_PROP];
505
+ const properties = propIndex === -1 ? {} : Object.assign({}, clusterProps[propIndex]);
506
+
507
+ return Object.assign(properties, {
508
+ cluster: true as const,
509
+ cluster_id: data[i + OFFSET_ID],
510
+ point_count: count,
511
+ point_count_abbreviated: abbrev
512
+ });
513
+ }