@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
package/dist/geojson-vt.mjs
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
* @param sqTolerance - square tolerance value
|
|
8
8
|
*/
|
|
9
9
|
function simplify(coords, first, last, sqTolerance) {
|
|
10
|
+
const stack = [first, last];
|
|
11
|
+
while (stack.length) {
|
|
12
|
+
last = stack.pop();
|
|
13
|
+
first = stack.pop();
|
|
14
|
+
simplifySegment(coords, first, last, sqTolerance, stack);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function simplifySegment(coords, first, last, sqTolerance, stack) {
|
|
10
18
|
let maxSqDist = sqTolerance;
|
|
11
19
|
const mid = first + (last - first >> 1);
|
|
12
20
|
let minPosToMid = last - first;
|
|
@@ -31,9 +39,9 @@ function simplify(coords, first, last, sqTolerance) {
|
|
|
31
39
|
}
|
|
32
40
|
}
|
|
33
41
|
if (maxSqDist > sqTolerance) {
|
|
34
|
-
if (index - first > 3) simplify(coords, first, index, sqTolerance);
|
|
35
42
|
coords[index + 2] = maxSqDist;
|
|
36
|
-
if (
|
|
43
|
+
if (index - first > 3) stack.push(first, index);
|
|
44
|
+
if (last - index > 3) stack.push(index, last);
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
/**
|
|
@@ -102,9 +110,7 @@ function createFeature(id, type, geom, tags) {
|
|
|
102
110
|
case "MultiLineString":
|
|
103
111
|
for (const line of data.geom) calcLineBBox(feature, line.points);
|
|
104
112
|
break;
|
|
105
|
-
case "MultiPolygon":
|
|
106
|
-
for (const polygon of data.geom) calcLineBBox(feature, polygon[0].points);
|
|
107
|
-
break;
|
|
113
|
+
case "MultiPolygon": for (const polygon of data.geom) calcLineBBox(feature, polygon[0].points);
|
|
108
114
|
}
|
|
109
115
|
return feature;
|
|
110
116
|
}
|
|
@@ -156,24 +162,25 @@ function featureToInternal(features, geojson, options, index, depth = 0) {
|
|
|
156
162
|
if (!geojson.geometry.coordinates?.length) return;
|
|
157
163
|
const id = getFeatureId(geojson, options, index);
|
|
158
164
|
const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
|
|
165
|
+
const worldCoordinates = options.worldCoordinates === true;
|
|
159
166
|
switch (geojson.geometry.type) {
|
|
160
167
|
case "Point":
|
|
161
|
-
convertPointFeature(features, id, geojson.geometry, geojson.properties);
|
|
168
|
+
convertPointFeature(features, id, geojson.geometry, worldCoordinates, geojson.properties);
|
|
162
169
|
return;
|
|
163
170
|
case "MultiPoint":
|
|
164
|
-
convertMultiPointFeature(features, id, geojson.geometry, geojson.properties);
|
|
171
|
+
convertMultiPointFeature(features, id, geojson.geometry, worldCoordinates, geojson.properties);
|
|
165
172
|
return;
|
|
166
173
|
case "LineString":
|
|
167
|
-
convertLineStringFeature(features, id, geojson.geometry, tolerance, geojson.properties);
|
|
174
|
+
convertLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
|
|
168
175
|
return;
|
|
169
176
|
case "MultiLineString":
|
|
170
|
-
convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, options, geojson.properties);
|
|
177
|
+
convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinates, options, geojson.properties);
|
|
171
178
|
return;
|
|
172
179
|
case "Polygon":
|
|
173
|
-
convertPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
|
|
180
|
+
convertPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
|
|
174
181
|
return;
|
|
175
182
|
case "MultiPolygon":
|
|
176
|
-
convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
|
|
183
|
+
convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
|
|
177
184
|
return;
|
|
178
185
|
default: throw new Error("Input data is not a valid GeoJSON object.");
|
|
179
186
|
}
|
|
@@ -191,56 +198,60 @@ function convertGeometryCollection(features, geojson, geometry, options, index,
|
|
|
191
198
|
properties: geojson.properties
|
|
192
199
|
}, options, index, depth);
|
|
193
200
|
}
|
|
194
|
-
function convertPointFeature(features, id, geom, properties) {
|
|
201
|
+
function convertPointFeature(features, id, geom, worldCoordinates, properties) {
|
|
195
202
|
const out = [];
|
|
196
|
-
out.push(projectX(geom.coordinates[0]), projectY(geom.coordinates[1]), 0);
|
|
203
|
+
out.push(projectX(geom.coordinates[0], worldCoordinates), projectY(geom.coordinates[1], worldCoordinates), 0);
|
|
197
204
|
features.push(createFeature(id, "Point", out, properties));
|
|
198
205
|
}
|
|
199
|
-
function convertMultiPointFeature(features, id, geom, properties) {
|
|
206
|
+
function convertMultiPointFeature(features, id, geom, worldCoordinates, properties) {
|
|
200
207
|
const out = [];
|
|
201
|
-
for (const coords of geom.coordinates) out.push(projectX(coords[0]), projectY(coords[1]), 0);
|
|
208
|
+
for (const coords of geom.coordinates) out.push(projectX(coords[0], worldCoordinates), projectY(coords[1], worldCoordinates), 0);
|
|
202
209
|
features.push(createFeature(id, "MultiPoint", out, properties));
|
|
203
210
|
}
|
|
204
|
-
function convertLineStringFeature(features, id, geom, tolerance, properties) {
|
|
211
|
+
function convertLineStringFeature(features, id, geom, tolerance, worldCoordinates, properties) {
|
|
205
212
|
const out = { points: [] };
|
|
206
|
-
convertLine(geom.coordinates, out, tolerance, false);
|
|
213
|
+
convertLine(geom.coordinates, out, tolerance, false, worldCoordinates);
|
|
207
214
|
features.push(createFeature(id, "LineString", out, properties));
|
|
208
215
|
}
|
|
209
|
-
function convertMultiLineStringFeature(features, id, geom, tolerance, options, properties) {
|
|
216
|
+
function convertMultiLineStringFeature(features, id, geom, tolerance, worldCoordinates, options, properties) {
|
|
210
217
|
if (options.lineMetrics) for (const line of geom.coordinates) {
|
|
211
218
|
const out = { points: [] };
|
|
212
|
-
convertLine(line, out, tolerance, false);
|
|
219
|
+
convertLine(line, out, tolerance, false, worldCoordinates);
|
|
213
220
|
features.push(createFeature(id, "LineString", out, properties));
|
|
214
221
|
}
|
|
215
222
|
else {
|
|
216
223
|
const out = [];
|
|
217
|
-
convertLines(geom.coordinates, out, tolerance, false);
|
|
224
|
+
convertLines(geom.coordinates, out, tolerance, false, worldCoordinates);
|
|
218
225
|
features.push(createFeature(id, "MultiLineString", out, properties));
|
|
219
226
|
}
|
|
220
227
|
}
|
|
221
|
-
function convertPolygonFeature(features, id, geom, tolerance, properties) {
|
|
228
|
+
function convertPolygonFeature(features, id, geom, tolerance, worldCoordinates, properties) {
|
|
222
229
|
const out = [];
|
|
223
|
-
convertLines(geom.coordinates, out, tolerance, true);
|
|
230
|
+
convertLines(geom.coordinates, out, tolerance, true, worldCoordinates);
|
|
224
231
|
features.push(createFeature(id, "Polygon", out, properties));
|
|
225
232
|
}
|
|
226
|
-
function convertMultiPolygonFeature(features, id, geom, tolerance, properties) {
|
|
233
|
+
function convertMultiPolygonFeature(features, id, geom, tolerance, worldCoordinates, properties) {
|
|
227
234
|
const out = [];
|
|
228
235
|
for (const polygon of geom.coordinates) {
|
|
229
236
|
const polygonOut = [];
|
|
230
|
-
convertLines(polygon, polygonOut, tolerance, true);
|
|
237
|
+
convertLines(polygon, polygonOut, tolerance, true, worldCoordinates);
|
|
238
|
+
if (!polygonOut.length) continue;
|
|
231
239
|
out.push(polygonOut);
|
|
232
240
|
}
|
|
241
|
+
if (!out.length) return;
|
|
233
242
|
features.push(createFeature(id, "MultiPolygon", out, properties));
|
|
234
243
|
}
|
|
235
|
-
function convertLine(ring, out, tolerance, isPolygon) {
|
|
244
|
+
function convertLine(ring, out, tolerance, isPolygon, worldCoordinates) {
|
|
236
245
|
let x0, y0;
|
|
237
246
|
let size = 0;
|
|
238
247
|
for (let j = 0; j < ring.length; j++) {
|
|
239
|
-
const x = projectX(ring[j][0]);
|
|
240
|
-
const y = projectY(ring[j][1]);
|
|
248
|
+
const x = projectX(ring[j][0], worldCoordinates);
|
|
249
|
+
const y = projectY(ring[j][1], worldCoordinates);
|
|
241
250
|
out.points.push(x, y, 0);
|
|
242
|
-
if (j > 0)
|
|
243
|
-
|
|
251
|
+
if (j > 0) {
|
|
252
|
+
if (isPolygon) size += (x0 * y - x * y0) / 2;
|
|
253
|
+
else size += Math.sqrt(Math.pow(x - x0, 2) + Math.pow(y - y0, 2));
|
|
254
|
+
}
|
|
244
255
|
x0 = x;
|
|
245
256
|
y0 = y;
|
|
246
257
|
}
|
|
@@ -253,23 +264,25 @@ function convertLine(ring, out, tolerance, isPolygon) {
|
|
|
253
264
|
out.start = 0;
|
|
254
265
|
out.end = out.size;
|
|
255
266
|
}
|
|
256
|
-
function convertLines(rings, out, tolerance, isPolygon) {
|
|
267
|
+
function convertLines(rings, out, tolerance, isPolygon, worldCoordinates) {
|
|
257
268
|
for (let i = 0; i < rings.length; i++) {
|
|
258
269
|
const geom = { points: [] };
|
|
259
|
-
convertLine(rings[i], geom, tolerance, isPolygon);
|
|
270
|
+
convertLine(rings[i], geom, tolerance, isPolygon, worldCoordinates);
|
|
260
271
|
out.push(geom);
|
|
261
272
|
}
|
|
262
273
|
}
|
|
263
274
|
/**
|
|
264
|
-
* Convert longitude to spherical mercator in [0..1] range
|
|
275
|
+
* Convert longitude to spherical mercator in [0..1] range, or keep a world coordinate as it is
|
|
265
276
|
*/
|
|
266
|
-
function projectX(x) {
|
|
277
|
+
function projectX(x, worldCoordinates = false) {
|
|
278
|
+
if (worldCoordinates) return x;
|
|
267
279
|
return x / 360 + .5;
|
|
268
280
|
}
|
|
269
281
|
/**
|
|
270
|
-
* Convert latitude to spherical mercator in [0..1] range
|
|
282
|
+
* Convert latitude to spherical mercator in [0..1] range, or keep a world coordinate as it is
|
|
271
283
|
*/
|
|
272
|
-
function projectY(y) {
|
|
284
|
+
function projectY(y, worldCoordinates = false) {
|
|
285
|
+
if (worldCoordinates) return y;
|
|
273
286
|
const sin = Math.sin(y * Math.PI / 180);
|
|
274
287
|
const y2 = .5 - .25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
|
|
275
288
|
return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
|
|
@@ -279,19 +292,19 @@ function projectY(y) {
|
|
|
279
292
|
/**
|
|
280
293
|
* Converts internal source features back to GeoJSON format.
|
|
281
294
|
*/
|
|
282
|
-
function convertToGeoJSON(source) {
|
|
295
|
+
function convertToGeoJSON(source, worldCoordinates = false) {
|
|
283
296
|
return {
|
|
284
297
|
type: "FeatureCollection",
|
|
285
|
-
features: source.map((feature) => featureToGeoJSON(feature))
|
|
298
|
+
features: source.map((feature) => featureToGeoJSON(feature, worldCoordinates))
|
|
286
299
|
};
|
|
287
300
|
}
|
|
288
301
|
/**
|
|
289
302
|
* Converts a single internal feature to GeoJSON format.
|
|
290
303
|
*/
|
|
291
|
-
function featureToGeoJSON(feature) {
|
|
304
|
+
function featureToGeoJSON(feature, worldCoordinates = false) {
|
|
292
305
|
const geojsonFeature = {
|
|
293
306
|
type: "Feature",
|
|
294
|
-
geometry: geometryToGeoJSON(feature),
|
|
307
|
+
geometry: geometryToGeoJSON(feature, worldCoordinates),
|
|
295
308
|
properties: feature.tags
|
|
296
309
|
};
|
|
297
310
|
if (feature.id != null) geojsonFeature.id = feature.id;
|
|
@@ -300,50 +313,52 @@ function featureToGeoJSON(feature) {
|
|
|
300
313
|
/**
|
|
301
314
|
* Converts a single internal feature geometry to GeoJSON format.
|
|
302
315
|
*/
|
|
303
|
-
function geometryToGeoJSON(feature) {
|
|
316
|
+
function geometryToGeoJSON(feature, worldCoordinates) {
|
|
304
317
|
const { type, geometry } = feature;
|
|
305
318
|
switch (type) {
|
|
306
319
|
case "Point": return {
|
|
307
320
|
type,
|
|
308
|
-
coordinates: unprojectPoint(geometry[0], geometry[1])
|
|
321
|
+
coordinates: unprojectPoint(geometry[0], geometry[1], worldCoordinates)
|
|
309
322
|
};
|
|
310
323
|
case "MultiPoint": return {
|
|
311
324
|
type,
|
|
312
|
-
coordinates: unprojectPoints(geometry)
|
|
325
|
+
coordinates: unprojectPoints(geometry, worldCoordinates)
|
|
313
326
|
};
|
|
314
327
|
case "LineString": return {
|
|
315
328
|
type,
|
|
316
|
-
coordinates: unprojectPoints(geometry.points)
|
|
329
|
+
coordinates: unprojectPoints(geometry.points, worldCoordinates)
|
|
317
330
|
};
|
|
318
331
|
case "MultiLineString":
|
|
319
332
|
case "Polygon": return {
|
|
320
333
|
type,
|
|
321
|
-
coordinates: geometry.map((ring) => unprojectPoints(ring.points))
|
|
334
|
+
coordinates: geometry.map((ring) => unprojectPoints(ring.points, worldCoordinates))
|
|
322
335
|
};
|
|
323
336
|
case "MultiPolygon": return {
|
|
324
337
|
type,
|
|
325
|
-
coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points)))
|
|
338
|
+
coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points, worldCoordinates)))
|
|
326
339
|
};
|
|
327
340
|
}
|
|
328
341
|
}
|
|
329
|
-
function unprojectPoints(coords) {
|
|
342
|
+
function unprojectPoints(coords, worldCoordinates = false) {
|
|
330
343
|
const result = [];
|
|
331
|
-
for (let i = 0; i < coords.length; i += 3) result.push(unprojectPoint(coords[i], coords[i + 1]));
|
|
344
|
+
for (let i = 0; i < coords.length; i += 3) result.push(unprojectPoint(coords[i], coords[i + 1], worldCoordinates));
|
|
332
345
|
return result;
|
|
333
346
|
}
|
|
334
|
-
function unprojectPoint(x, y) {
|
|
335
|
-
return [unprojectX(x), unprojectY(y)];
|
|
347
|
+
function unprojectPoint(x, y, worldCoordinates) {
|
|
348
|
+
return [unprojectX(x, worldCoordinates), unprojectY(y, worldCoordinates)];
|
|
336
349
|
}
|
|
337
350
|
/**
|
|
338
|
-
* Convert spherical mercator in [0..1] range to longitude
|
|
351
|
+
* Convert spherical mercator in [0..1] range to longitude, or keep a world coordinate as it is
|
|
339
352
|
*/
|
|
340
|
-
function unprojectX(x) {
|
|
353
|
+
function unprojectX(x, worldCoordinates = false) {
|
|
354
|
+
if (worldCoordinates) return x;
|
|
341
355
|
return (x - .5) * 360;
|
|
342
356
|
}
|
|
343
357
|
/**
|
|
344
|
-
* Convert spherical mercator in [0..1] range to latitude
|
|
358
|
+
* Convert spherical mercator in [0..1] range to latitude, or keep a world coordinate as it is
|
|
345
359
|
*/
|
|
346
|
-
function unprojectY(y) {
|
|
360
|
+
function unprojectY(y, worldCoordinates = false) {
|
|
361
|
+
if (worldCoordinates) return y;
|
|
347
362
|
const y2 = (180 - y * 360) * Math.PI / 180;
|
|
348
363
|
return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
|
|
349
364
|
}
|
|
@@ -470,12 +485,12 @@ function clipLine(geom, newGeom, start, end, axis, isPolygon, trackMetrics) {
|
|
|
470
485
|
let exited = false;
|
|
471
486
|
if (trackMetrics) segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
|
|
472
487
|
if (a < start) {
|
|
473
|
-
if (b
|
|
488
|
+
if (b >= start) {
|
|
474
489
|
t = intersect(slice, ax, ay, bx, by, start);
|
|
475
490
|
if (trackMetrics) slice.start = len + segLen * t;
|
|
476
491
|
}
|
|
477
492
|
} else if (a > end) {
|
|
478
|
-
if (b
|
|
493
|
+
if (b <= end) {
|
|
479
494
|
t = intersect(slice, ax, ay, bx, by, end);
|
|
480
495
|
if (trackMetrics) slice.start = len + segLen * t;
|
|
481
496
|
}
|
|
@@ -535,6 +550,7 @@ function intersectY(out, ax, ay, bx, by, y) {
|
|
|
535
550
|
//#endregion
|
|
536
551
|
//#region src/wrap.ts
|
|
537
552
|
function wrap(features, options) {
|
|
553
|
+
if (options.worldCoordinates) return features;
|
|
538
554
|
const buffer = options.buffer / options.extent;
|
|
539
555
|
let merged = features;
|
|
540
556
|
const left = clip(features, 1, -1 - buffer, buffer, 0, -1, 2, options);
|
|
@@ -655,7 +671,7 @@ function applySourceDiff(source, dataDiff, options) {
|
|
|
655
671
|
* @param vtFeatures - the original features
|
|
656
672
|
* @param update - the update object to apply
|
|
657
673
|
* @param options - the options to use for the wrap method
|
|
658
|
-
* @returns Updated features. If geometry is updated, returns new feature(s) converted from geojson and wrapped. If only properties are updated, returns feature(s) with tags updated.
|
|
674
|
+
* @returns Updated features. If geometry is updated, returns new feature(s) converted from geojson and wrapped, with the id the feature already has. If only properties are updated, returns feature(s) with tags updated.
|
|
659
675
|
*/
|
|
660
676
|
function getUpdatedFeatures(vtFeatures, update, options) {
|
|
661
677
|
const changeGeometry = !!update.newGeometry;
|
|
@@ -670,7 +686,11 @@ function getUpdatedFeatures(vtFeatures, update, options) {
|
|
|
670
686
|
geometry: update.newGeometry,
|
|
671
687
|
properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
|
|
672
688
|
}]
|
|
673
|
-
},
|
|
689
|
+
}, {
|
|
690
|
+
...options,
|
|
691
|
+
promoteId: void 0,
|
|
692
|
+
generateId: false
|
|
693
|
+
});
|
|
674
694
|
features = wrap(features, options);
|
|
675
695
|
return features;
|
|
676
696
|
}
|
|
@@ -707,7 +727,7 @@ function diffToHashed(diff, options) {
|
|
|
707
727
|
return {
|
|
708
728
|
removeAll: diff.removeAll,
|
|
709
729
|
remove: new Set(diff.remove || []),
|
|
710
|
-
add: new Map(diff.add?.map((feature) => [options.promoteId ? feature.properties[options.promoteId] : feature.id, feature])),
|
|
730
|
+
add: new Map(diff.add?.map((feature) => [options.promoteId ? feature.properties?.[options.promoteId] : feature.id, feature])),
|
|
711
731
|
update: new Map(diff.update?.map((update) => [update.id, update]))
|
|
712
732
|
};
|
|
713
733
|
}
|
|
@@ -777,7 +797,7 @@ var KDBush = class KDBush {
|
|
|
777
797
|
this.coords = new ArrayType(data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
|
|
778
798
|
this._pos = 0;
|
|
779
799
|
this._finished = false;
|
|
780
|
-
new Uint8Array(data, 0, 2).set([219,
|
|
800
|
+
new Uint8Array(data, 0, 2).set([219, 16 + arrayTypeIndex]);
|
|
781
801
|
new Uint16Array(data, 2, 1)[0] = nodeSize;
|
|
782
802
|
new Uint32Array(data, 4, 1)[0] = numItems;
|
|
783
803
|
}
|
|
@@ -1010,52 +1030,101 @@ const defaultClusterOptions = {
|
|
|
1010
1030
|
reduce: null,
|
|
1011
1031
|
map: (props) => props
|
|
1012
1032
|
};
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1033
|
+
/**
|
|
1034
|
+
* The highest zoom level to cluster on. A cluster id keeps its zoom level in five bits.
|
|
1035
|
+
*/
|
|
1036
|
+
const MAX_ZOOM = 30;
|
|
1037
|
+
/**
|
|
1038
|
+
* Projected coordinates, which are 0 to 1 inside the world, are stored as integers with this many steps per world,
|
|
1039
|
+
* so that they take half the memory of doubles. One step is ~4 cm at the equator.
|
|
1040
|
+
*/
|
|
1041
|
+
const SCALE = 1073741824;
|
|
1042
|
+
/**
|
|
1043
|
+
* The largest value a 32-bit integer holds.
|
|
1044
|
+
*/
|
|
1045
|
+
const MAX_INTEGER = 2147483647;
|
|
1046
|
+
/**
|
|
1047
|
+
* A projected coordinate in steps from the center of the world, see `SCALE`.
|
|
1048
|
+
*/
|
|
1049
|
+
function encode(coord) {
|
|
1050
|
+
return (coord - .5) * SCALE;
|
|
1051
|
+
}
|
|
1052
|
+
function decode(value) {
|
|
1053
|
+
return value / SCALE + .5;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Whether a projected coordinate fits in a 32-bit integer. One that is more than two worlds from the center of the
|
|
1057
|
+
* world does not, and neither does one that is not a number.
|
|
1058
|
+
*/
|
|
1059
|
+
function fitsInteger(coord) {
|
|
1060
|
+
return Math.abs(encode(coord)) < MAX_INTEGER;
|
|
1061
|
+
}
|
|
1062
|
+
const OFFSET_ZOOM = 0;
|
|
1063
|
+
const OFFSET_ID = 1;
|
|
1064
|
+
const OFFSET_PARENT = 2;
|
|
1065
|
+
const OFFSET_NUM = 3;
|
|
1066
|
+
const OFFSET_PROP = 4;
|
|
1018
1067
|
/**
|
|
1019
1068
|
* This class allow clustering of geojson points.
|
|
1020
1069
|
*/
|
|
1021
1070
|
var ClusterTileIndex = class {
|
|
1022
|
-
constructor(options) {
|
|
1023
|
-
this.options =
|
|
1071
|
+
constructor(options, worldCoordinates = false) {
|
|
1072
|
+
this.options = createOptions(options);
|
|
1073
|
+
this.worldCoordinates = worldCoordinates;
|
|
1024
1074
|
this.trees = new Array(this.options.maxZoom + 1);
|
|
1025
|
-
this.stride = this.options.reduce ?
|
|
1075
|
+
this.stride = this.options.reduce ? 5 : 4;
|
|
1026
1076
|
this.clusterProps = [];
|
|
1027
1077
|
this.points = [];
|
|
1028
1078
|
}
|
|
1029
1079
|
/**
|
|
1030
|
-
* Loads GeoJSON
|
|
1031
|
-
*
|
|
1080
|
+
* Loads GeoJSON Point and MultiPoint features and builds the internal clustering index.
|
|
1081
|
+
* Each position of a MultiPoint is clustered as a point of its own, with the id and properties of its feature.
|
|
1082
|
+
* @param points - GeoJSON Point and MultiPoint features to cluster.
|
|
1032
1083
|
*/
|
|
1033
1084
|
load(points) {
|
|
1034
1085
|
const features = [];
|
|
1035
1086
|
for (const point of points) {
|
|
1036
1087
|
if (!point.geometry) continue;
|
|
1037
|
-
const
|
|
1038
|
-
const [
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1088
|
+
const positions = point.geometry.type === "MultiPoint" ? point.geometry.coordinates : [point.geometry.coordinates];
|
|
1089
|
+
for (const [lng, lat] of positions) {
|
|
1090
|
+
const [x, y] = [projectX(lng, this.worldCoordinates), projectY(lat, this.worldCoordinates)];
|
|
1091
|
+
if (this.worldCoordinates && !isInWorld([x, y])) continue;
|
|
1092
|
+
const feature = {
|
|
1093
|
+
id: point.id,
|
|
1094
|
+
type: "Point",
|
|
1095
|
+
geometry: [x, y],
|
|
1096
|
+
tags: point.properties
|
|
1097
|
+
};
|
|
1098
|
+
features.push(feature);
|
|
1099
|
+
}
|
|
1046
1100
|
}
|
|
1047
1101
|
this.createIndex(features);
|
|
1048
1102
|
}
|
|
1049
1103
|
/**
|
|
1050
1104
|
* @internal
|
|
1051
1105
|
* Loads internal GeoJSONVT point features from a data source and builds the clustering index.
|
|
1106
|
+
* Each position of a MultiPoint feature becomes a point of its own, with the id and properties of its feature.
|
|
1052
1107
|
* @param features - {@link GeoJSONVTInternalFeature} data source features to filter and cluster.
|
|
1053
1108
|
*/
|
|
1054
1109
|
initialize(features) {
|
|
1055
1110
|
const points = [];
|
|
1056
1111
|
for (const feature of features) {
|
|
1057
|
-
if (feature.type
|
|
1058
|
-
|
|
1112
|
+
if (feature.type === "Point") {
|
|
1113
|
+
if (this.worldCoordinates && !isInWorld(feature.geometry)) continue;
|
|
1114
|
+
points.push(feature);
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (feature.type !== "MultiPoint") continue;
|
|
1118
|
+
for (let i = 0; i < feature.geometry.length; i += 3) {
|
|
1119
|
+
const geometry = [feature.geometry[i], feature.geometry[i + 1]];
|
|
1120
|
+
if (this.worldCoordinates && !isInWorld(geometry)) continue;
|
|
1121
|
+
points.push({
|
|
1122
|
+
id: feature.id,
|
|
1123
|
+
type: "Point",
|
|
1124
|
+
geometry,
|
|
1125
|
+
tags: feature.tags
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1059
1128
|
}
|
|
1060
1129
|
this.createIndex(points);
|
|
1061
1130
|
}
|
|
@@ -1065,41 +1134,59 @@ var ClusterTileIndex = class {
|
|
|
1065
1134
|
* @param features
|
|
1066
1135
|
*/
|
|
1067
1136
|
updateIndex(features, _affected, options) {
|
|
1068
|
-
this.options =
|
|
1137
|
+
this.options = createOptions(options.clusterOptions);
|
|
1069
1138
|
this.initialize(features);
|
|
1070
1139
|
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Builds a tree per zoom level from the points. A point whose position does not fit in an integer is left out,
|
|
1142
|
+
* see `fitsInteger`.
|
|
1143
|
+
*/
|
|
1071
1144
|
createIndex(points) {
|
|
1072
1145
|
const { log, minZoom, maxZoom } = this.options;
|
|
1146
|
+
this.trees = new Array(maxZoom + 1);
|
|
1147
|
+
this.stride = this.options.reduce ? 5 : 4;
|
|
1148
|
+
this.clusterProps = [];
|
|
1073
1149
|
if (log) console.time("total time");
|
|
1074
1150
|
const timerId = `prepare ${points.length} points`;
|
|
1075
1151
|
if (log) console.time(timerId);
|
|
1076
1152
|
this.points = points;
|
|
1077
|
-
const
|
|
1153
|
+
const stride = this.stride;
|
|
1154
|
+
const notProcessed = maxZoom + 1;
|
|
1155
|
+
const coords = new Int32Array(points.length * 2);
|
|
1156
|
+
const data = new Int32Array(points.length * stride);
|
|
1157
|
+
let numItems = 0;
|
|
1078
1158
|
for (let i = 0; i < points.length; i++) {
|
|
1079
1159
|
const p = points[i];
|
|
1080
|
-
if (!p?.geometry) continue;
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
data
|
|
1085
|
-
|
|
1160
|
+
if (!p?.geometry || !fitsInteger(p.geometry[0]) || !fitsInteger(p.geometry[1])) continue;
|
|
1161
|
+
coords[numItems * 2] = Math.round(encode(p.geometry[0]));
|
|
1162
|
+
coords[numItems * 2 + 1] = Math.round(encode(p.geometry[1]));
|
|
1163
|
+
const k = numItems * stride;
|
|
1164
|
+
data[k + OFFSET_ZOOM] = notProcessed;
|
|
1165
|
+
data[k + OFFSET_ID] = i;
|
|
1166
|
+
data[k + OFFSET_PARENT] = -1;
|
|
1167
|
+
data[k + OFFSET_NUM] = 1;
|
|
1168
|
+
numItems++;
|
|
1086
1169
|
}
|
|
1087
|
-
let tree = this.trees[maxZoom + 1] = this.createTree(data);
|
|
1170
|
+
let tree = this.trees[maxZoom + 1] = this.createTree(coords, data, numItems);
|
|
1088
1171
|
if (log) console.timeEnd(timerId);
|
|
1172
|
+
const nextCoords = new Int32Array(numItems * 2);
|
|
1173
|
+
const nextData = new Int32Array(numItems * stride);
|
|
1089
1174
|
for (let z = maxZoom; z >= minZoom; z--) {
|
|
1090
1175
|
const now = Date.now();
|
|
1091
|
-
|
|
1176
|
+
const nextNumItems = this.cluster(tree, z, nextCoords, nextData);
|
|
1177
|
+
if (!this.isSameAsTree(tree, nextData, nextNumItems)) tree = this.createTree(nextCoords.slice(0, nextNumItems * 2), nextData.slice(0, nextNumItems * stride), nextNumItems);
|
|
1178
|
+
this.trees[z] = tree;
|
|
1092
1179
|
if (log) console.log("z%d: %d clusters in %dms", z, tree.numItems, Date.now() - now);
|
|
1093
1180
|
}
|
|
1094
1181
|
if (log) console.timeEnd("total time");
|
|
1095
1182
|
}
|
|
1096
1183
|
/**
|
|
1097
1184
|
* Returns clusters and/or points within a bounding box at a given zoom level.
|
|
1098
|
-
* @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order.
|
|
1185
|
+
* @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order, or `[minX, minY, maxX, maxY]` in world coordinates.
|
|
1099
1186
|
* @param zoom - Zoom level to query.
|
|
1100
1187
|
*/
|
|
1101
1188
|
getClusters(bbox, zoom) {
|
|
1102
|
-
return this.getClustersInternal(bbox, zoom).map((f) => featureToGeoJSON(f));
|
|
1189
|
+
return (this.worldCoordinates ? this.getClustersInRange(bbox, zoom) : this.getClustersInternal(bbox, zoom)).map((f) => featureToGeoJSON(f, this.worldCoordinates));
|
|
1103
1190
|
}
|
|
1104
1191
|
getClustersInternal(bbox, zoom) {
|
|
1105
1192
|
let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
|
|
@@ -1124,13 +1211,25 @@ var ClusterTileIndex = class {
|
|
|
1124
1211
|
], zoom);
|
|
1125
1212
|
return easternHem.concat(westernHem);
|
|
1126
1213
|
}
|
|
1214
|
+
return this.getClustersInRange([
|
|
1215
|
+
projectX(minLng),
|
|
1216
|
+
projectY(maxLat),
|
|
1217
|
+
projectX(maxLng),
|
|
1218
|
+
projectY(minLat)
|
|
1219
|
+
], zoom);
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* The clusters and points in a box of the internal coordinates, `[minX, minY, maxX, maxY]`.
|
|
1223
|
+
*/
|
|
1224
|
+
getClustersInRange([minX, minY, maxX, maxY], zoom) {
|
|
1127
1225
|
const tree = this.trees[this.limitZoom(zoom)];
|
|
1128
|
-
const ids = tree.range(
|
|
1226
|
+
const ids = tree.range(encode(minX), encode(minY), encode(maxX), encode(maxY));
|
|
1227
|
+
const coords = tree.flatCoords;
|
|
1129
1228
|
const data = tree.flatData;
|
|
1130
1229
|
const clusters = [];
|
|
1131
1230
|
for (const id of ids) {
|
|
1132
1231
|
const k = this.stride * id;
|
|
1133
|
-
clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
|
|
1232
|
+
clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(coords, id, data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
|
|
1134
1233
|
}
|
|
1135
1234
|
return clusters;
|
|
1136
1235
|
}
|
|
@@ -1144,16 +1243,17 @@ var ClusterTileIndex = class {
|
|
|
1144
1243
|
const clusterError = /* @__PURE__ */ new Error("No cluster with the specified id: " + clusterId);
|
|
1145
1244
|
const tree = this.trees[originZoom];
|
|
1146
1245
|
if (!tree) throw clusterError;
|
|
1246
|
+
const coords = tree.flatCoords;
|
|
1147
1247
|
const data = tree.flatData;
|
|
1148
|
-
if (originId
|
|
1149
|
-
const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
|
|
1150
|
-
const x =
|
|
1151
|
-
const y =
|
|
1248
|
+
if (originId >= tree.numItems) throw clusterError;
|
|
1249
|
+
const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1)) * SCALE;
|
|
1250
|
+
const x = coords[originId * 2];
|
|
1251
|
+
const y = coords[originId * 2 + 1];
|
|
1152
1252
|
const ids = tree.within(x, y, r);
|
|
1153
1253
|
const children = [];
|
|
1154
1254
|
for (const id of ids) {
|
|
1155
1255
|
const k = id * this.stride;
|
|
1156
|
-
if (data[k + OFFSET_PARENT] === clusterId) children.push(data[k + OFFSET_NUM] > 1 ? getClusterGeoJSON(data, k, this.clusterProps) : featureToGeoJSON(this.points[data[k + OFFSET_ID]]));
|
|
1256
|
+
if (data[k + OFFSET_PARENT] === clusterId) 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));
|
|
1157
1257
|
}
|
|
1158
1258
|
if (children.length === 0) throw clusterError;
|
|
1159
1259
|
return children;
|
|
@@ -1181,10 +1281,11 @@ var ClusterTileIndex = class {
|
|
|
1181
1281
|
const tree = this.trees[this.limitZoom(z)];
|
|
1182
1282
|
if (!tree) return null;
|
|
1183
1283
|
const z2 = Math.pow(2, z);
|
|
1284
|
+
if (this.worldCoordinates && (x < 0 || x >= z2 || y < 0 || y >= z2)) return null;
|
|
1184
1285
|
const { extent, radius } = this.options;
|
|
1185
1286
|
const p = radius / extent;
|
|
1186
|
-
const top = (y - p) / z2;
|
|
1187
|
-
const bottom = (y + 1 + p) / z2;
|
|
1287
|
+
const top = encode((y - p) / z2);
|
|
1288
|
+
const bottom = encode((y + 1 + p) / z2);
|
|
1188
1289
|
const tile = {
|
|
1189
1290
|
transformed: true,
|
|
1190
1291
|
features: [],
|
|
@@ -1193,9 +1294,9 @@ var ClusterTileIndex = class {
|
|
|
1193
1294
|
y,
|
|
1194
1295
|
z
|
|
1195
1296
|
};
|
|
1196
|
-
this.addTileFeatures(tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), tree
|
|
1197
|
-
if (x === 0) this.addTileFeatures(tree.range(1 - p / z2, top, 1, bottom), tree
|
|
1198
|
-
if (x === z2 - 1) this.addTileFeatures(tree.range(0, top, p / z2, bottom), tree
|
|
1297
|
+
this.addTileFeatures(tree.range(encode((x - p) / z2), top, encode((x + 1 + p) / z2), bottom), tree, x, y, z2, tile);
|
|
1298
|
+
if (!this.worldCoordinates && x === 0) this.addTileFeatures(tree.range(encode(1 - p / z2), top, encode(1), bottom), tree, z2, y, z2, tile);
|
|
1299
|
+
if (!this.worldCoordinates && x === z2 - 1) this.addTileFeatures(tree.range(encode(0), top, encode(p / z2), bottom), tree, -1, y, z2, tile);
|
|
1199
1300
|
return tile;
|
|
1200
1301
|
}
|
|
1201
1302
|
/**
|
|
@@ -1209,23 +1310,36 @@ var ClusterTileIndex = class {
|
|
|
1209
1310
|
const children = this.getChildren(clusterId);
|
|
1210
1311
|
for (const child of children) {
|
|
1211
1312
|
const props = child.properties;
|
|
1212
|
-
if (props?.cluster)
|
|
1213
|
-
|
|
1214
|
-
|
|
1313
|
+
if (props?.cluster) {
|
|
1314
|
+
if (skipped + props.point_count <= offset) skipped += props.point_count;
|
|
1315
|
+
else skipped = this.appendLeaves(result, props.cluster_id, limit, offset, skipped);
|
|
1316
|
+
} else if (skipped < offset) skipped++;
|
|
1215
1317
|
else result.push(child);
|
|
1216
1318
|
if (result.length === limit) break;
|
|
1217
1319
|
}
|
|
1218
1320
|
return skipped;
|
|
1219
1321
|
}
|
|
1220
|
-
createTree(data) {
|
|
1221
|
-
const tree = new KDBush(
|
|
1222
|
-
for (let i = 0; i <
|
|
1322
|
+
createTree(coords, data, numItems) {
|
|
1323
|
+
const tree = new KDBush(numItems, this.options.nodeSize, Int32Array);
|
|
1324
|
+
for (let i = 0; i < numItems; i++) tree.add(coords[i * 2], coords[i * 2 + 1]);
|
|
1223
1325
|
tree.finish();
|
|
1326
|
+
tree.flatCoords = coords;
|
|
1224
1327
|
tree.flatData = data;
|
|
1225
1328
|
tree.data = null;
|
|
1226
1329
|
return tree;
|
|
1227
1330
|
}
|
|
1228
|
-
|
|
1331
|
+
/**
|
|
1332
|
+
* Whether clustering a tree left all its points and clusters as they were, in the same order.
|
|
1333
|
+
*/
|
|
1334
|
+
isSameAsTree(tree, nextData, nextNumItems) {
|
|
1335
|
+
if (nextNumItems !== tree.numItems) return false;
|
|
1336
|
+
const data = tree.flatData;
|
|
1337
|
+
for (let k = OFFSET_ID; k < nextNumItems * this.stride; k += this.stride) if (nextData[k] !== data[k]) return false;
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
addTileFeatures(ids, tree, x, y, z2, tile) {
|
|
1341
|
+
const coords = tree.flatCoords;
|
|
1342
|
+
const data = tree.flatData;
|
|
1229
1343
|
for (const i of ids) {
|
|
1230
1344
|
const k = i * this.stride;
|
|
1231
1345
|
const isCluster = data[k + OFFSET_NUM] > 1;
|
|
@@ -1234,8 +1348,8 @@ var ClusterTileIndex = class {
|
|
|
1234
1348
|
let py;
|
|
1235
1349
|
if (isCluster) {
|
|
1236
1350
|
tags = getClusterProperties(data, k, this.clusterProps);
|
|
1237
|
-
px =
|
|
1238
|
-
py =
|
|
1351
|
+
px = decode(coords[i * 2]);
|
|
1352
|
+
py = decode(coords[i * 2 + 1]);
|
|
1239
1353
|
} else {
|
|
1240
1354
|
const p = this.points[data[k + OFFSET_ID]];
|
|
1241
1355
|
tags = p.tags;
|
|
@@ -1256,22 +1370,30 @@ var ClusterTileIndex = class {
|
|
|
1256
1370
|
limitZoom(z) {
|
|
1257
1371
|
return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
|
|
1258
1372
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1373
|
+
/**
|
|
1374
|
+
* Clusters the points of a tree on a zoom level, writing the result to `nextCoords` and `nextData`.
|
|
1375
|
+
* @returns the number of points and clusters written
|
|
1376
|
+
*/
|
|
1377
|
+
cluster(tree, zoom, nextCoords, nextData) {
|
|
1378
|
+
const { radius, extent, reduce, minPoints, maxZoom } = this.options;
|
|
1379
|
+
const r = radius / (extent * Math.pow(2, zoom)) * SCALE;
|
|
1380
|
+
const notProcessed = maxZoom + 1;
|
|
1381
|
+
const coords = tree.flatCoords;
|
|
1262
1382
|
const data = tree.flatData;
|
|
1263
|
-
const nextData = [];
|
|
1264
1383
|
const stride = this.stride;
|
|
1265
|
-
|
|
1384
|
+
const neighborIds = new Uint32Array(tree.numItems);
|
|
1385
|
+
let nextNumItems = 0;
|
|
1386
|
+
for (let index = 0; index < tree.numItems; index++) {
|
|
1387
|
+
const i = index * stride;
|
|
1266
1388
|
if (data[i + OFFSET_ZOOM] <= zoom) continue;
|
|
1267
1389
|
data[i + OFFSET_ZOOM] = zoom;
|
|
1268
|
-
const x =
|
|
1269
|
-
const y =
|
|
1270
|
-
const
|
|
1390
|
+
const x = coords[index * 2];
|
|
1391
|
+
const y = coords[index * 2 + 1];
|
|
1392
|
+
const numNeighbors = tree.withinInto(x, y, r, neighborIds);
|
|
1271
1393
|
const numPointsOrigin = data[i + OFFSET_NUM];
|
|
1272
1394
|
let numPoints = numPointsOrigin;
|
|
1273
|
-
for (
|
|
1274
|
-
const k =
|
|
1395
|
+
for (let n = 0; n < numNeighbors; n++) {
|
|
1396
|
+
const k = neighborIds[n] * stride;
|
|
1275
1397
|
if (data[k + OFFSET_ZOOM] > zoom) numPoints += data[k + OFFSET_NUM];
|
|
1276
1398
|
}
|
|
1277
1399
|
if (numPoints > numPointsOrigin && numPoints >= minPoints) {
|
|
@@ -1279,14 +1401,15 @@ var ClusterTileIndex = class {
|
|
|
1279
1401
|
let wy = y * numPointsOrigin;
|
|
1280
1402
|
let clusterProperties;
|
|
1281
1403
|
let clusterPropIndex = -1;
|
|
1282
|
-
const id = (
|
|
1283
|
-
for (
|
|
1284
|
-
const
|
|
1404
|
+
const id = (index << 5) + (zoom + 1) + this.points.length;
|
|
1405
|
+
for (let n = 0; n < numNeighbors; n++) {
|
|
1406
|
+
const neighborIndex = neighborIds[n];
|
|
1407
|
+
const k = neighborIndex * stride;
|
|
1285
1408
|
if (data[k + OFFSET_ZOOM] <= zoom) continue;
|
|
1286
1409
|
data[k + OFFSET_ZOOM] = zoom;
|
|
1287
1410
|
const numPoints2 = data[k + OFFSET_NUM];
|
|
1288
|
-
wx +=
|
|
1289
|
-
wy +=
|
|
1411
|
+
wx += coords[neighborIndex * 2] * numPoints2;
|
|
1412
|
+
wy += coords[neighborIndex * 2 + 1] * numPoints2;
|
|
1290
1413
|
data[k + OFFSET_PARENT] = id;
|
|
1291
1414
|
if (reduce) {
|
|
1292
1415
|
if (!clusterProperties) {
|
|
@@ -1298,19 +1421,33 @@ var ClusterTileIndex = class {
|
|
|
1298
1421
|
}
|
|
1299
1422
|
}
|
|
1300
1423
|
data[i + OFFSET_PARENT] = id;
|
|
1301
|
-
|
|
1302
|
-
|
|
1424
|
+
nextCoords[nextNumItems * 2] = Math.round(wx / numPoints);
|
|
1425
|
+
nextCoords[nextNumItems * 2 + 1] = Math.round(wy / numPoints);
|
|
1426
|
+
const next = nextNumItems * stride;
|
|
1427
|
+
nextData[next + OFFSET_ZOOM] = notProcessed;
|
|
1428
|
+
nextData[next + OFFSET_ID] = id;
|
|
1429
|
+
nextData[next + OFFSET_PARENT] = -1;
|
|
1430
|
+
nextData[next + OFFSET_NUM] = numPoints;
|
|
1431
|
+
if (reduce) nextData[next + OFFSET_PROP] = clusterPropIndex;
|
|
1432
|
+
nextNumItems++;
|
|
1303
1433
|
} else {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1434
|
+
nextCoords[nextNumItems * 2] = x;
|
|
1435
|
+
nextCoords[nextNumItems * 2 + 1] = y;
|
|
1436
|
+
for (let j = 0; j < stride; j++) nextData[nextNumItems * stride + j] = data[i + j];
|
|
1437
|
+
nextNumItems++;
|
|
1438
|
+
if (numPoints > 1) for (let n = 0; n < numNeighbors; n++) {
|
|
1439
|
+
const neighborIndex = neighborIds[n];
|
|
1440
|
+
const k = neighborIndex * stride;
|
|
1307
1441
|
if (data[k + OFFSET_ZOOM] <= zoom) continue;
|
|
1308
1442
|
data[k + OFFSET_ZOOM] = zoom;
|
|
1309
|
-
|
|
1443
|
+
nextCoords[nextNumItems * 2] = coords[neighborIndex * 2];
|
|
1444
|
+
nextCoords[nextNumItems * 2 + 1] = coords[neighborIndex * 2 + 1];
|
|
1445
|
+
for (let j = 0; j < stride; j++) nextData[nextNumItems * stride + j] = data[k + j];
|
|
1446
|
+
nextNumItems++;
|
|
1310
1447
|
}
|
|
1311
1448
|
}
|
|
1312
1449
|
}
|
|
1313
|
-
return
|
|
1450
|
+
return nextNumItems;
|
|
1314
1451
|
}
|
|
1315
1452
|
getOriginId(clusterId) {
|
|
1316
1453
|
return clusterId - this.points.length >> 5;
|
|
@@ -1328,22 +1465,28 @@ var ClusterTileIndex = class {
|
|
|
1328
1465
|
return clone && result === original ? Object.assign({}, result) : result;
|
|
1329
1466
|
}
|
|
1330
1467
|
};
|
|
1331
|
-
function
|
|
1468
|
+
function createOptions(options) {
|
|
1469
|
+
const result = Object.assign(Object.create(defaultClusterOptions), options);
|
|
1470
|
+
result.minZoom = Math.min(result.minZoom, MAX_ZOOM);
|
|
1471
|
+
result.maxZoom = Math.min(result.maxZoom, MAX_ZOOM);
|
|
1472
|
+
return result;
|
|
1473
|
+
}
|
|
1474
|
+
function getClusterFeature(coords, index, data, i, clusterProps) {
|
|
1332
1475
|
return {
|
|
1333
1476
|
id: data[i + OFFSET_ID],
|
|
1334
1477
|
type: "Point",
|
|
1335
1478
|
tags: getClusterProperties(data, i, clusterProps),
|
|
1336
|
-
geometry: [
|
|
1479
|
+
geometry: [decode(coords[index * 2]), decode(coords[index * 2 + 1])]
|
|
1337
1480
|
};
|
|
1338
1481
|
}
|
|
1339
|
-
function getClusterGeoJSON(data, i, clusterProps) {
|
|
1482
|
+
function getClusterGeoJSON(coords, index, data, i, clusterProps, worldCoordinates) {
|
|
1340
1483
|
return {
|
|
1341
1484
|
type: "Feature",
|
|
1342
1485
|
id: data[i + OFFSET_ID],
|
|
1343
1486
|
properties: getClusterProperties(data, i, clusterProps),
|
|
1344
1487
|
geometry: {
|
|
1345
1488
|
type: "Point",
|
|
1346
|
-
coordinates: [unprojectX(
|
|
1489
|
+
coordinates: [unprojectX(decode(coords[index * 2]), worldCoordinates), unprojectY(decode(coords[index * 2 + 1]), worldCoordinates)]
|
|
1347
1490
|
}
|
|
1348
1491
|
};
|
|
1349
1492
|
}
|
|
@@ -1359,6 +1502,9 @@ function getClusterProperties(data, i, clusterProps) {
|
|
|
1359
1502
|
point_count_abbreviated: abbrev
|
|
1360
1503
|
});
|
|
1361
1504
|
}
|
|
1505
|
+
function isInWorld(geometry) {
|
|
1506
|
+
return geometry[0] >= 0 && geometry[0] <= 1 && geometry[1] >= 0 && geometry[1] <= 1;
|
|
1507
|
+
}
|
|
1362
1508
|
//#endregion
|
|
1363
1509
|
//#region src/tile.ts
|
|
1364
1510
|
const GEOJSONVT_CLIP_START = "geojsonvt_clip_start";
|
|
@@ -1567,7 +1713,7 @@ var TileIndex = class {
|
|
|
1567
1713
|
this.total = 0;
|
|
1568
1714
|
}
|
|
1569
1715
|
initialize(features) {
|
|
1570
|
-
this.splitTile(features, 0, 0, 0);
|
|
1716
|
+
this.splitTile(this.clipToWorld(features), 0, 0, 0);
|
|
1571
1717
|
if (this.options.debug) {
|
|
1572
1718
|
if (features.length) console.log("features: %d, points: %d", this.tiles[0].numFeatures, this.tiles[0].numPoints);
|
|
1573
1719
|
console.timeEnd("generate tiles");
|
|
@@ -1587,8 +1733,9 @@ var TileIndex = class {
|
|
|
1587
1733
|
0,
|
|
1588
1734
|
0
|
|
1589
1735
|
];
|
|
1590
|
-
const
|
|
1591
|
-
rootTile
|
|
1736
|
+
const rootSource = this.clipToWorld(source);
|
|
1737
|
+
const rootTile = createTile(rootSource, z, x, y, options);
|
|
1738
|
+
rootTile.source = rootSource;
|
|
1592
1739
|
const id = toID(z, x, y);
|
|
1593
1740
|
this.tiles[id] = rootTile;
|
|
1594
1741
|
this.tileCoords.push({
|
|
@@ -1619,6 +1766,7 @@ var TileIndex = class {
|
|
|
1619
1766
|
getTile(z, x, y) {
|
|
1620
1767
|
const { extent, debug } = this.options;
|
|
1621
1768
|
const z2 = 1 << z;
|
|
1769
|
+
if (this.options.worldCoordinates && (x < 0 || x >= z2)) return null;
|
|
1622
1770
|
x = x + z2 & z2 - 1;
|
|
1623
1771
|
const id = toID(z, x, y);
|
|
1624
1772
|
if (this.tiles[id]) return transformTile(this.tiles[id], extent);
|
|
@@ -1644,6 +1792,16 @@ var TileIndex = class {
|
|
|
1644
1792
|
return transformTile(this.tiles[id], extent);
|
|
1645
1793
|
}
|
|
1646
1794
|
/**
|
|
1795
|
+
* The features the top tile starts from: all of them in a wrapping world, where wrapping already put them inside,
|
|
1796
|
+
* and only what lies inside the world square and its buffer with `worldCoordinates`. The index keeps the rest, so
|
|
1797
|
+
* an update can still move a feature in from outside.
|
|
1798
|
+
*/
|
|
1799
|
+
clipToWorld(features) {
|
|
1800
|
+
if (!this.options.worldCoordinates) return features;
|
|
1801
|
+
const buffer = this.options.buffer / this.options.extent;
|
|
1802
|
+
return clip(clip(features, 1, -buffer, 1 + buffer, 0, -Infinity, Infinity, this.options) || [], 1, -buffer, 1 + buffer, 1, -Infinity, Infinity, this.options) || [];
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1647
1805
|
* splits features from a parent tile to sub-tiles.
|
|
1648
1806
|
* z, x, and y are the coordinates of the parent tile
|
|
1649
1807
|
* cz, cx, and cy are the coordinates of the target tile
|
|
@@ -1796,7 +1954,8 @@ const defaultOptions = {
|
|
|
1796
1954
|
updateable: false,
|
|
1797
1955
|
cluster: false,
|
|
1798
1956
|
clusterOptions: defaultClusterOptions,
|
|
1799
|
-
debug: 0
|
|
1957
|
+
debug: 0,
|
|
1958
|
+
worldCoordinates: false
|
|
1800
1959
|
};
|
|
1801
1960
|
/**
|
|
1802
1961
|
* Main class for creating and managing a vector tile index from GeoJSON data.
|
|
@@ -1819,7 +1978,7 @@ var GeoJSONVT = class {
|
|
|
1819
1978
|
this.initializeIndex(features, options);
|
|
1820
1979
|
}
|
|
1821
1980
|
initializeIndex(features, options) {
|
|
1822
|
-
this.tileIndex = options.cluster ? new ClusterTileIndex(options.clusterOptions) : new TileIndex(options);
|
|
1981
|
+
this.tileIndex = options.cluster ? new ClusterTileIndex(options.clusterOptions, options.worldCoordinates) : new TileIndex(options);
|
|
1823
1982
|
if (!features.length) return;
|
|
1824
1983
|
this.tileIndex.initialize(features);
|
|
1825
1984
|
}
|
|
@@ -1857,7 +2016,7 @@ var GeoJSONVT = class {
|
|
|
1857
2016
|
const removeIds = /* @__PURE__ */ new Set();
|
|
1858
2017
|
for (const feature of source) {
|
|
1859
2018
|
if (feature.id == void 0) continue;
|
|
1860
|
-
if (predicate(featureToGeoJSON(feature))) continue;
|
|
2019
|
+
if (predicate(featureToGeoJSON(feature, this.options.worldCoordinates))) continue;
|
|
1861
2020
|
affected.push(feature);
|
|
1862
2021
|
removeIds.add(feature.id);
|
|
1863
2022
|
}
|
|
@@ -1872,7 +2031,7 @@ var GeoJSONVT = class {
|
|
|
1872
2031
|
*/
|
|
1873
2032
|
getData() {
|
|
1874
2033
|
if (!this.options.updateable) throw new Error("to retrieve data the `updateable` option must be set to true");
|
|
1875
|
-
return convertToGeoJSON(this.source);
|
|
2034
|
+
return convertToGeoJSON(this.source, this.options.worldCoordinates);
|
|
1876
2035
|
}
|
|
1877
2036
|
/**
|
|
1878
2037
|
* Update supercluster options and regenerate the index.
|