@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.
- package/README.md +3 -13
- package/dist/clip.d.ts +22 -0
- package/dist/clip.d.ts.map +1 -0
- package/dist/clip.test.d.ts +2 -0
- package/dist/clip.test.d.ts.map +1 -0
- package/dist/cluster-tile-index.d.ts +76 -0
- package/dist/cluster-tile-index.d.ts.map +1 -0
- package/dist/cluster-tile-index.test.d.ts +2 -0
- package/dist/cluster-tile-index.test.d.ts.map +1 -0
- package/dist/convert.d.ts +17 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/deconvert.d.ts +19 -0
- package/dist/deconvert.d.ts.map +1 -0
- package/dist/deconvert.test.d.ts +2 -0
- package/dist/deconvert.test.d.ts.map +1 -0
- package/dist/definitions.d.ts +241 -0
- package/dist/definitions.d.ts.map +1 -0
- package/dist/difference.d.ts +67 -0
- package/dist/difference.d.ts.map +1 -0
- package/dist/difference.test.d.ts +2 -0
- package/dist/difference.test.d.ts.map +1 -0
- package/dist/feature.d.ts +20 -0
- package/dist/feature.d.ts.map +1 -0
- package/dist/geojson-to-tile.d.ts +35 -0
- package/dist/geojson-to-tile.d.ts.map +1 -0
- package/dist/geojson-vt-dev.js +1582 -478
- package/dist/geojson-vt.js +1 -1
- package/dist/geojson-vt.mjs +1250 -473
- package/dist/geojson-vt.mjs.map +1 -1
- package/dist/geojsonvt.d.ts +76 -0
- package/dist/geojsonvt.d.ts.map +1 -0
- package/dist/geojsonvt.test.d.ts +2 -0
- package/dist/geojsonvt.test.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/simplify.d.ts +9 -0
- package/dist/simplify.d.ts.map +1 -0
- package/dist/simplify.test.d.ts +2 -0
- package/dist/simplify.test.d.ts.map +1 -0
- package/dist/tile-index.d.ts +51 -0
- package/dist/tile-index.d.ts.map +1 -0
- package/dist/tile.d.ts +12 -0
- package/dist/tile.d.ts.map +1 -0
- package/dist/transform.d.ts +10 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/wrap.d.ts +3 -0
- package/dist/wrap.d.ts.map +1 -0
- package/package.json +26 -12
- package/src/clip.ts +119 -81
- package/src/cluster-tile-index.test.ts +205 -0
- package/src/cluster-tile-index.ts +513 -0
- package/src/convert.ts +97 -75
- package/src/deconvert.test.ts +153 -0
- package/src/deconvert.ts +92 -0
- package/src/definitions.ts +196 -18
- package/src/difference.ts +3 -3
- package/src/feature.ts +11 -4
- package/src/geojson-to-tile.ts +58 -0
- package/src/geojsonvt.test.ts +39 -0
- package/src/geojsonvt.ts +209 -0
- package/src/index.ts +27 -378
- package/src/tile-index.ts +310 -0
- package/src/tile.ts +92 -103
- package/src/transform.ts +41 -39
- 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
|
+
}
|