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