@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.
- package/README.md +3 -0
- package/dist/cluster-tile-index.d.ts +41 -6
- package/dist/cluster-tile-index.d.ts.map +1 -1
- package/dist/convert.d.ts +4 -4
- package/dist/convert.d.ts.map +1 -1
- package/dist/deconvert.d.ts +7 -7
- package/dist/deconvert.d.ts.map +1 -1
- package/dist/definitions.d.ts +17 -5
- package/dist/definitions.d.ts.map +1 -1
- package/dist/geojson-vt-dev.js +315 -154
- package/dist/geojson-vt.js +1 -1
- package/dist/geojson-vt.mjs +305 -146
- package/dist/geojson-vt.mjs.map +1 -1
- package/dist/geojsonvt.d.ts.map +1 -1
- package/dist/simplify.d.ts.map +1 -1
- package/dist/tile-index.d.ts +6 -0
- package/dist/tile-index.d.ts.map +1 -1
- package/dist/wrap.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/clip.ts +2 -2
- package/src/cluster-tile-index.test.ts +103 -0
- package/src/cluster-tile-index.ts +242 -97
- package/src/convert.ts +35 -28
- package/src/deconvert.ts +20 -18
- package/src/definitions.ts +17 -5
- package/src/difference.test.ts +71 -0
- package/src/difference.ts +3 -3
- package/src/geojsonvt.test.ts +236 -1
- package/src/geojsonvt.ts +5 -4
- package/src/simplify.test.ts +18 -0
- package/src/simplify.ts +12 -2
- package/src/tile-index.ts +17 -3
- package/src/wrap.ts +2 -0
|
@@ -11,7 +11,14 @@ type ClusterOrPointFeatureInternal = ClusterFeatureInternal | GeoJSONVTInternalP
|
|
|
11
11
|
|
|
12
12
|
/** @internal */
|
|
13
13
|
export type KDBushWithData = KDBush & {
|
|
14
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
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 =
|
|
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 ?
|
|
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
|
|
56
|
-
*
|
|
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
|
|
68
|
-
const [
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
93
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
211
|
-
const y =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
323
|
-
const tree = new KDBush(
|
|
324
|
-
for (let i = 0; i <
|
|
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
|
-
|
|
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 =
|
|
342
|
-
py =
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
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 =
|
|
393
|
-
const y =
|
|
394
|
-
const
|
|
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 (
|
|
401
|
-
const k =
|
|
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 = (
|
|
534
|
+
const id = (index << 5) + (zoom + 1) + this.points.length;
|
|
416
535
|
|
|
417
|
-
for (
|
|
418
|
-
const
|
|
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 +=
|
|
425
|
-
wy +=
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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 (
|
|
448
|
-
const
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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: [
|
|
627
|
+
geometry: [decode(coords[index * 2]), decode(coords[index * 2 + 1])]
|
|
487
628
|
};
|
|
488
629
|
}
|
|
489
630
|
|
|
490
|
-
function getClusterGeoJSON(
|
|
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(
|
|
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:
|
|
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
|
+
}
|