@maplibre/geojson-vt 6.1.2 → 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 +286 -135
- package/dist/geojson-vt.js +1 -1
- package/dist/geojson-vt.mjs +285 -135
- package/dist/geojson-vt.mjs.map +1 -1
- package/dist/geojsonvt.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 +5 -5
- 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/tile-index.ts +17 -3
- package/src/wrap.ts +2 -0
package/dist/geojson-vt.mjs
CHANGED
|
@@ -162,24 +162,25 @@ function featureToInternal(features, geojson, options, index, depth = 0) {
|
|
|
162
162
|
if (!geojson.geometry.coordinates?.length) return;
|
|
163
163
|
const id = getFeatureId(geojson, options, index);
|
|
164
164
|
const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
|
|
165
|
+
const worldCoordinates = options.worldCoordinates === true;
|
|
165
166
|
switch (geojson.geometry.type) {
|
|
166
167
|
case "Point":
|
|
167
|
-
convertPointFeature(features, id, geojson.geometry, geojson.properties);
|
|
168
|
+
convertPointFeature(features, id, geojson.geometry, worldCoordinates, geojson.properties);
|
|
168
169
|
return;
|
|
169
170
|
case "MultiPoint":
|
|
170
|
-
convertMultiPointFeature(features, id, geojson.geometry, geojson.properties);
|
|
171
|
+
convertMultiPointFeature(features, id, geojson.geometry, worldCoordinates, geojson.properties);
|
|
171
172
|
return;
|
|
172
173
|
case "LineString":
|
|
173
|
-
convertLineStringFeature(features, id, geojson.geometry, tolerance, geojson.properties);
|
|
174
|
+
convertLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
|
|
174
175
|
return;
|
|
175
176
|
case "MultiLineString":
|
|
176
|
-
convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, options, geojson.properties);
|
|
177
|
+
convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinates, options, geojson.properties);
|
|
177
178
|
return;
|
|
178
179
|
case "Polygon":
|
|
179
|
-
convertPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
|
|
180
|
+
convertPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
|
|
180
181
|
return;
|
|
181
182
|
case "MultiPolygon":
|
|
182
|
-
convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
|
|
183
|
+
convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
|
|
183
184
|
return;
|
|
184
185
|
default: throw new Error("Input data is not a valid GeoJSON object.");
|
|
185
186
|
}
|
|
@@ -197,53 +198,55 @@ function convertGeometryCollection(features, geojson, geometry, options, index,
|
|
|
197
198
|
properties: geojson.properties
|
|
198
199
|
}, options, index, depth);
|
|
199
200
|
}
|
|
200
|
-
function convertPointFeature(features, id, geom, properties) {
|
|
201
|
+
function convertPointFeature(features, id, geom, worldCoordinates, properties) {
|
|
201
202
|
const out = [];
|
|
202
|
-
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);
|
|
203
204
|
features.push(createFeature(id, "Point", out, properties));
|
|
204
205
|
}
|
|
205
|
-
function convertMultiPointFeature(features, id, geom, properties) {
|
|
206
|
+
function convertMultiPointFeature(features, id, geom, worldCoordinates, properties) {
|
|
206
207
|
const out = [];
|
|
207
|
-
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);
|
|
208
209
|
features.push(createFeature(id, "MultiPoint", out, properties));
|
|
209
210
|
}
|
|
210
|
-
function convertLineStringFeature(features, id, geom, tolerance, properties) {
|
|
211
|
+
function convertLineStringFeature(features, id, geom, tolerance, worldCoordinates, properties) {
|
|
211
212
|
const out = { points: [] };
|
|
212
|
-
convertLine(geom.coordinates, out, tolerance, false);
|
|
213
|
+
convertLine(geom.coordinates, out, tolerance, false, worldCoordinates);
|
|
213
214
|
features.push(createFeature(id, "LineString", out, properties));
|
|
214
215
|
}
|
|
215
|
-
function convertMultiLineStringFeature(features, id, geom, tolerance, options, properties) {
|
|
216
|
+
function convertMultiLineStringFeature(features, id, geom, tolerance, worldCoordinates, options, properties) {
|
|
216
217
|
if (options.lineMetrics) for (const line of geom.coordinates) {
|
|
217
218
|
const out = { points: [] };
|
|
218
|
-
convertLine(line, out, tolerance, false);
|
|
219
|
+
convertLine(line, out, tolerance, false, worldCoordinates);
|
|
219
220
|
features.push(createFeature(id, "LineString", out, properties));
|
|
220
221
|
}
|
|
221
222
|
else {
|
|
222
223
|
const out = [];
|
|
223
|
-
convertLines(geom.coordinates, out, tolerance, false);
|
|
224
|
+
convertLines(geom.coordinates, out, tolerance, false, worldCoordinates);
|
|
224
225
|
features.push(createFeature(id, "MultiLineString", out, properties));
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
|
-
function convertPolygonFeature(features, id, geom, tolerance, properties) {
|
|
228
|
+
function convertPolygonFeature(features, id, geom, tolerance, worldCoordinates, properties) {
|
|
228
229
|
const out = [];
|
|
229
|
-
convertLines(geom.coordinates, out, tolerance, true);
|
|
230
|
+
convertLines(geom.coordinates, out, tolerance, true, worldCoordinates);
|
|
230
231
|
features.push(createFeature(id, "Polygon", out, properties));
|
|
231
232
|
}
|
|
232
|
-
function convertMultiPolygonFeature(features, id, geom, tolerance, properties) {
|
|
233
|
+
function convertMultiPolygonFeature(features, id, geom, tolerance, worldCoordinates, properties) {
|
|
233
234
|
const out = [];
|
|
234
235
|
for (const polygon of geom.coordinates) {
|
|
235
236
|
const polygonOut = [];
|
|
236
|
-
convertLines(polygon, polygonOut, tolerance, true);
|
|
237
|
+
convertLines(polygon, polygonOut, tolerance, true, worldCoordinates);
|
|
238
|
+
if (!polygonOut.length) continue;
|
|
237
239
|
out.push(polygonOut);
|
|
238
240
|
}
|
|
241
|
+
if (!out.length) return;
|
|
239
242
|
features.push(createFeature(id, "MultiPolygon", out, properties));
|
|
240
243
|
}
|
|
241
|
-
function convertLine(ring, out, tolerance, isPolygon) {
|
|
244
|
+
function convertLine(ring, out, tolerance, isPolygon, worldCoordinates) {
|
|
242
245
|
let x0, y0;
|
|
243
246
|
let size = 0;
|
|
244
247
|
for (let j = 0; j < ring.length; j++) {
|
|
245
|
-
const x = projectX(ring[j][0]);
|
|
246
|
-
const y = projectY(ring[j][1]);
|
|
248
|
+
const x = projectX(ring[j][0], worldCoordinates);
|
|
249
|
+
const y = projectY(ring[j][1], worldCoordinates);
|
|
247
250
|
out.points.push(x, y, 0);
|
|
248
251
|
if (j > 0) {
|
|
249
252
|
if (isPolygon) size += (x0 * y - x * y0) / 2;
|
|
@@ -261,23 +264,25 @@ function convertLine(ring, out, tolerance, isPolygon) {
|
|
|
261
264
|
out.start = 0;
|
|
262
265
|
out.end = out.size;
|
|
263
266
|
}
|
|
264
|
-
function convertLines(rings, out, tolerance, isPolygon) {
|
|
267
|
+
function convertLines(rings, out, tolerance, isPolygon, worldCoordinates) {
|
|
265
268
|
for (let i = 0; i < rings.length; i++) {
|
|
266
269
|
const geom = { points: [] };
|
|
267
|
-
convertLine(rings[i], geom, tolerance, isPolygon);
|
|
270
|
+
convertLine(rings[i], geom, tolerance, isPolygon, worldCoordinates);
|
|
268
271
|
out.push(geom);
|
|
269
272
|
}
|
|
270
273
|
}
|
|
271
274
|
/**
|
|
272
|
-
* 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
|
|
273
276
|
*/
|
|
274
|
-
function projectX(x) {
|
|
277
|
+
function projectX(x, worldCoordinates = false) {
|
|
278
|
+
if (worldCoordinates) return x;
|
|
275
279
|
return x / 360 + .5;
|
|
276
280
|
}
|
|
277
281
|
/**
|
|
278
|
-
* 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
|
|
279
283
|
*/
|
|
280
|
-
function projectY(y) {
|
|
284
|
+
function projectY(y, worldCoordinates = false) {
|
|
285
|
+
if (worldCoordinates) return y;
|
|
281
286
|
const sin = Math.sin(y * Math.PI / 180);
|
|
282
287
|
const y2 = .5 - .25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
|
|
283
288
|
return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
|
|
@@ -287,19 +292,19 @@ function projectY(y) {
|
|
|
287
292
|
/**
|
|
288
293
|
* Converts internal source features back to GeoJSON format.
|
|
289
294
|
*/
|
|
290
|
-
function convertToGeoJSON(source) {
|
|
295
|
+
function convertToGeoJSON(source, worldCoordinates = false) {
|
|
291
296
|
return {
|
|
292
297
|
type: "FeatureCollection",
|
|
293
|
-
features: source.map((feature) => featureToGeoJSON(feature))
|
|
298
|
+
features: source.map((feature) => featureToGeoJSON(feature, worldCoordinates))
|
|
294
299
|
};
|
|
295
300
|
}
|
|
296
301
|
/**
|
|
297
302
|
* Converts a single internal feature to GeoJSON format.
|
|
298
303
|
*/
|
|
299
|
-
function featureToGeoJSON(feature) {
|
|
304
|
+
function featureToGeoJSON(feature, worldCoordinates = false) {
|
|
300
305
|
const geojsonFeature = {
|
|
301
306
|
type: "Feature",
|
|
302
|
-
geometry: geometryToGeoJSON(feature),
|
|
307
|
+
geometry: geometryToGeoJSON(feature, worldCoordinates),
|
|
303
308
|
properties: feature.tags
|
|
304
309
|
};
|
|
305
310
|
if (feature.id != null) geojsonFeature.id = feature.id;
|
|
@@ -308,50 +313,52 @@ function featureToGeoJSON(feature) {
|
|
|
308
313
|
/**
|
|
309
314
|
* Converts a single internal feature geometry to GeoJSON format.
|
|
310
315
|
*/
|
|
311
|
-
function geometryToGeoJSON(feature) {
|
|
316
|
+
function geometryToGeoJSON(feature, worldCoordinates) {
|
|
312
317
|
const { type, geometry } = feature;
|
|
313
318
|
switch (type) {
|
|
314
319
|
case "Point": return {
|
|
315
320
|
type,
|
|
316
|
-
coordinates: unprojectPoint(geometry[0], geometry[1])
|
|
321
|
+
coordinates: unprojectPoint(geometry[0], geometry[1], worldCoordinates)
|
|
317
322
|
};
|
|
318
323
|
case "MultiPoint": return {
|
|
319
324
|
type,
|
|
320
|
-
coordinates: unprojectPoints(geometry)
|
|
325
|
+
coordinates: unprojectPoints(geometry, worldCoordinates)
|
|
321
326
|
};
|
|
322
327
|
case "LineString": return {
|
|
323
328
|
type,
|
|
324
|
-
coordinates: unprojectPoints(geometry.points)
|
|
329
|
+
coordinates: unprojectPoints(geometry.points, worldCoordinates)
|
|
325
330
|
};
|
|
326
331
|
case "MultiLineString":
|
|
327
332
|
case "Polygon": return {
|
|
328
333
|
type,
|
|
329
|
-
coordinates: geometry.map((ring) => unprojectPoints(ring.points))
|
|
334
|
+
coordinates: geometry.map((ring) => unprojectPoints(ring.points, worldCoordinates))
|
|
330
335
|
};
|
|
331
336
|
case "MultiPolygon": return {
|
|
332
337
|
type,
|
|
333
|
-
coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points)))
|
|
338
|
+
coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points, worldCoordinates)))
|
|
334
339
|
};
|
|
335
340
|
}
|
|
336
341
|
}
|
|
337
|
-
function unprojectPoints(coords) {
|
|
342
|
+
function unprojectPoints(coords, worldCoordinates = false) {
|
|
338
343
|
const result = [];
|
|
339
|
-
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));
|
|
340
345
|
return result;
|
|
341
346
|
}
|
|
342
|
-
function unprojectPoint(x, y) {
|
|
343
|
-
return [unprojectX(x), unprojectY(y)];
|
|
347
|
+
function unprojectPoint(x, y, worldCoordinates) {
|
|
348
|
+
return [unprojectX(x, worldCoordinates), unprojectY(y, worldCoordinates)];
|
|
344
349
|
}
|
|
345
350
|
/**
|
|
346
|
-
* 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
|
|
347
352
|
*/
|
|
348
|
-
function unprojectX(x) {
|
|
353
|
+
function unprojectX(x, worldCoordinates = false) {
|
|
354
|
+
if (worldCoordinates) return x;
|
|
349
355
|
return (x - .5) * 360;
|
|
350
356
|
}
|
|
351
357
|
/**
|
|
352
|
-
* 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
|
|
353
359
|
*/
|
|
354
|
-
function unprojectY(y) {
|
|
360
|
+
function unprojectY(y, worldCoordinates = false) {
|
|
361
|
+
if (worldCoordinates) return y;
|
|
355
362
|
const y2 = (180 - y * 360) * Math.PI / 180;
|
|
356
363
|
return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
|
|
357
364
|
}
|
|
@@ -478,12 +485,12 @@ function clipLine(geom, newGeom, start, end, axis, isPolygon, trackMetrics) {
|
|
|
478
485
|
let exited = false;
|
|
479
486
|
if (trackMetrics) segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
|
|
480
487
|
if (a < start) {
|
|
481
|
-
if (b
|
|
488
|
+
if (b >= start) {
|
|
482
489
|
t = intersect(slice, ax, ay, bx, by, start);
|
|
483
490
|
if (trackMetrics) slice.start = len + segLen * t;
|
|
484
491
|
}
|
|
485
492
|
} else if (a > end) {
|
|
486
|
-
if (b
|
|
493
|
+
if (b <= end) {
|
|
487
494
|
t = intersect(slice, ax, ay, bx, by, end);
|
|
488
495
|
if (trackMetrics) slice.start = len + segLen * t;
|
|
489
496
|
}
|
|
@@ -543,6 +550,7 @@ function intersectY(out, ax, ay, bx, by, y) {
|
|
|
543
550
|
//#endregion
|
|
544
551
|
//#region src/wrap.ts
|
|
545
552
|
function wrap(features, options) {
|
|
553
|
+
if (options.worldCoordinates) return features;
|
|
546
554
|
const buffer = options.buffer / options.extent;
|
|
547
555
|
let merged = features;
|
|
548
556
|
const left = clip(features, 1, -1 - buffer, buffer, 0, -1, 2, options);
|
|
@@ -663,7 +671,7 @@ function applySourceDiff(source, dataDiff, options) {
|
|
|
663
671
|
* @param vtFeatures - the original features
|
|
664
672
|
* @param update - the update object to apply
|
|
665
673
|
* @param options - the options to use for the wrap method
|
|
666
|
-
* @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.
|
|
667
675
|
*/
|
|
668
676
|
function getUpdatedFeatures(vtFeatures, update, options) {
|
|
669
677
|
const changeGeometry = !!update.newGeometry;
|
|
@@ -678,7 +686,11 @@ function getUpdatedFeatures(vtFeatures, update, options) {
|
|
|
678
686
|
geometry: update.newGeometry,
|
|
679
687
|
properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
|
|
680
688
|
}]
|
|
681
|
-
},
|
|
689
|
+
}, {
|
|
690
|
+
...options,
|
|
691
|
+
promoteId: void 0,
|
|
692
|
+
generateId: false
|
|
693
|
+
});
|
|
682
694
|
features = wrap(features, options);
|
|
683
695
|
return features;
|
|
684
696
|
}
|
|
@@ -715,7 +727,7 @@ function diffToHashed(diff, options) {
|
|
|
715
727
|
return {
|
|
716
728
|
removeAll: diff.removeAll,
|
|
717
729
|
remove: new Set(diff.remove || []),
|
|
718
|
-
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])),
|
|
719
731
|
update: new Map(diff.update?.map((update) => [update.id, update]))
|
|
720
732
|
};
|
|
721
733
|
}
|
|
@@ -1018,52 +1030,101 @@ const defaultClusterOptions = {
|
|
|
1018
1030
|
reduce: null,
|
|
1019
1031
|
map: (props) => props
|
|
1020
1032
|
};
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const
|
|
1025
|
-
|
|
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;
|
|
1026
1067
|
/**
|
|
1027
1068
|
* This class allow clustering of geojson points.
|
|
1028
1069
|
*/
|
|
1029
1070
|
var ClusterTileIndex = class {
|
|
1030
|
-
constructor(options) {
|
|
1031
|
-
this.options =
|
|
1071
|
+
constructor(options, worldCoordinates = false) {
|
|
1072
|
+
this.options = createOptions(options);
|
|
1073
|
+
this.worldCoordinates = worldCoordinates;
|
|
1032
1074
|
this.trees = new Array(this.options.maxZoom + 1);
|
|
1033
|
-
this.stride = this.options.reduce ?
|
|
1075
|
+
this.stride = this.options.reduce ? 5 : 4;
|
|
1034
1076
|
this.clusterProps = [];
|
|
1035
1077
|
this.points = [];
|
|
1036
1078
|
}
|
|
1037
1079
|
/**
|
|
1038
|
-
* Loads GeoJSON
|
|
1039
|
-
*
|
|
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.
|
|
1040
1083
|
*/
|
|
1041
1084
|
load(points) {
|
|
1042
1085
|
const features = [];
|
|
1043
1086
|
for (const point of points) {
|
|
1044
1087
|
if (!point.geometry) continue;
|
|
1045
|
-
const
|
|
1046
|
-
const [
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
+
}
|
|
1054
1100
|
}
|
|
1055
1101
|
this.createIndex(features);
|
|
1056
1102
|
}
|
|
1057
1103
|
/**
|
|
1058
1104
|
* @internal
|
|
1059
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.
|
|
1060
1107
|
* @param features - {@link GeoJSONVTInternalFeature} data source features to filter and cluster.
|
|
1061
1108
|
*/
|
|
1062
1109
|
initialize(features) {
|
|
1063
1110
|
const points = [];
|
|
1064
1111
|
for (const feature of features) {
|
|
1065
|
-
if (feature.type
|
|
1066
|
-
|
|
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
|
+
}
|
|
1067
1128
|
}
|
|
1068
1129
|
this.createIndex(points);
|
|
1069
1130
|
}
|
|
@@ -1073,41 +1134,59 @@ var ClusterTileIndex = class {
|
|
|
1073
1134
|
* @param features
|
|
1074
1135
|
*/
|
|
1075
1136
|
updateIndex(features, _affected, options) {
|
|
1076
|
-
this.options =
|
|
1137
|
+
this.options = createOptions(options.clusterOptions);
|
|
1077
1138
|
this.initialize(features);
|
|
1078
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
|
+
*/
|
|
1079
1144
|
createIndex(points) {
|
|
1080
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 = [];
|
|
1081
1149
|
if (log) console.time("total time");
|
|
1082
1150
|
const timerId = `prepare ${points.length} points`;
|
|
1083
1151
|
if (log) console.time(timerId);
|
|
1084
1152
|
this.points = points;
|
|
1085
|
-
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;
|
|
1086
1158
|
for (let i = 0; i < points.length; i++) {
|
|
1087
1159
|
const p = points[i];
|
|
1088
|
-
if (!p?.geometry) continue;
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
data
|
|
1093
|
-
|
|
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++;
|
|
1094
1169
|
}
|
|
1095
|
-
let tree = this.trees[maxZoom + 1] = this.createTree(data);
|
|
1170
|
+
let tree = this.trees[maxZoom + 1] = this.createTree(coords, data, numItems);
|
|
1096
1171
|
if (log) console.timeEnd(timerId);
|
|
1172
|
+
const nextCoords = new Int32Array(numItems * 2);
|
|
1173
|
+
const nextData = new Int32Array(numItems * stride);
|
|
1097
1174
|
for (let z = maxZoom; z >= minZoom; z--) {
|
|
1098
1175
|
const now = Date.now();
|
|
1099
|
-
|
|
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;
|
|
1100
1179
|
if (log) console.log("z%d: %d clusters in %dms", z, tree.numItems, Date.now() - now);
|
|
1101
1180
|
}
|
|
1102
1181
|
if (log) console.timeEnd("total time");
|
|
1103
1182
|
}
|
|
1104
1183
|
/**
|
|
1105
1184
|
* Returns clusters and/or points within a bounding box at a given zoom level.
|
|
1106
|
-
* @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.
|
|
1107
1186
|
* @param zoom - Zoom level to query.
|
|
1108
1187
|
*/
|
|
1109
1188
|
getClusters(bbox, zoom) {
|
|
1110
|
-
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));
|
|
1111
1190
|
}
|
|
1112
1191
|
getClustersInternal(bbox, zoom) {
|
|
1113
1192
|
let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
|
|
@@ -1132,13 +1211,25 @@ var ClusterTileIndex = class {
|
|
|
1132
1211
|
], zoom);
|
|
1133
1212
|
return easternHem.concat(westernHem);
|
|
1134
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) {
|
|
1135
1225
|
const tree = this.trees[this.limitZoom(zoom)];
|
|
1136
|
-
const ids = tree.range(
|
|
1226
|
+
const ids = tree.range(encode(minX), encode(minY), encode(maxX), encode(maxY));
|
|
1227
|
+
const coords = tree.flatCoords;
|
|
1137
1228
|
const data = tree.flatData;
|
|
1138
1229
|
const clusters = [];
|
|
1139
1230
|
for (const id of ids) {
|
|
1140
1231
|
const k = this.stride * id;
|
|
1141
|
-
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]]);
|
|
1142
1233
|
}
|
|
1143
1234
|
return clusters;
|
|
1144
1235
|
}
|
|
@@ -1152,16 +1243,17 @@ var ClusterTileIndex = class {
|
|
|
1152
1243
|
const clusterError = /* @__PURE__ */ new Error("No cluster with the specified id: " + clusterId);
|
|
1153
1244
|
const tree = this.trees[originZoom];
|
|
1154
1245
|
if (!tree) throw clusterError;
|
|
1246
|
+
const coords = tree.flatCoords;
|
|
1155
1247
|
const data = tree.flatData;
|
|
1156
|
-
if (originId
|
|
1157
|
-
const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
|
|
1158
|
-
const x =
|
|
1159
|
-
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];
|
|
1160
1252
|
const ids = tree.within(x, y, r);
|
|
1161
1253
|
const children = [];
|
|
1162
1254
|
for (const id of ids) {
|
|
1163
1255
|
const k = id * this.stride;
|
|
1164
|
-
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));
|
|
1165
1257
|
}
|
|
1166
1258
|
if (children.length === 0) throw clusterError;
|
|
1167
1259
|
return children;
|
|
@@ -1189,10 +1281,11 @@ var ClusterTileIndex = class {
|
|
|
1189
1281
|
const tree = this.trees[this.limitZoom(z)];
|
|
1190
1282
|
if (!tree) return null;
|
|
1191
1283
|
const z2 = Math.pow(2, z);
|
|
1284
|
+
if (this.worldCoordinates && (x < 0 || x >= z2 || y < 0 || y >= z2)) return null;
|
|
1192
1285
|
const { extent, radius } = this.options;
|
|
1193
1286
|
const p = radius / extent;
|
|
1194
|
-
const top = (y - p) / z2;
|
|
1195
|
-
const bottom = (y + 1 + p) / z2;
|
|
1287
|
+
const top = encode((y - p) / z2);
|
|
1288
|
+
const bottom = encode((y + 1 + p) / z2);
|
|
1196
1289
|
const tile = {
|
|
1197
1290
|
transformed: true,
|
|
1198
1291
|
features: [],
|
|
@@ -1201,9 +1294,9 @@ var ClusterTileIndex = class {
|
|
|
1201
1294
|
y,
|
|
1202
1295
|
z
|
|
1203
1296
|
};
|
|
1204
|
-
this.addTileFeatures(tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), tree
|
|
1205
|
-
if (x === 0) this.addTileFeatures(tree.range(1 - p / z2, top, 1, bottom), tree
|
|
1206
|
-
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);
|
|
1207
1300
|
return tile;
|
|
1208
1301
|
}
|
|
1209
1302
|
/**
|
|
@@ -1226,15 +1319,27 @@ var ClusterTileIndex = class {
|
|
|
1226
1319
|
}
|
|
1227
1320
|
return skipped;
|
|
1228
1321
|
}
|
|
1229
|
-
createTree(data) {
|
|
1230
|
-
const tree = new KDBush(
|
|
1231
|
-
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]);
|
|
1232
1325
|
tree.finish();
|
|
1326
|
+
tree.flatCoords = coords;
|
|
1233
1327
|
tree.flatData = data;
|
|
1234
1328
|
tree.data = null;
|
|
1235
1329
|
return tree;
|
|
1236
1330
|
}
|
|
1237
|
-
|
|
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;
|
|
1238
1343
|
for (const i of ids) {
|
|
1239
1344
|
const k = i * this.stride;
|
|
1240
1345
|
const isCluster = data[k + OFFSET_NUM] > 1;
|
|
@@ -1243,8 +1348,8 @@ var ClusterTileIndex = class {
|
|
|
1243
1348
|
let py;
|
|
1244
1349
|
if (isCluster) {
|
|
1245
1350
|
tags = getClusterProperties(data, k, this.clusterProps);
|
|
1246
|
-
px =
|
|
1247
|
-
py =
|
|
1351
|
+
px = decode(coords[i * 2]);
|
|
1352
|
+
py = decode(coords[i * 2 + 1]);
|
|
1248
1353
|
} else {
|
|
1249
1354
|
const p = this.points[data[k + OFFSET_ID]];
|
|
1250
1355
|
tags = p.tags;
|
|
@@ -1265,22 +1370,30 @@ var ClusterTileIndex = class {
|
|
|
1265
1370
|
limitZoom(z) {
|
|
1266
1371
|
return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
|
|
1267
1372
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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;
|
|
1271
1382
|
const data = tree.flatData;
|
|
1272
|
-
const nextData = [];
|
|
1273
1383
|
const stride = this.stride;
|
|
1274
|
-
|
|
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;
|
|
1275
1388
|
if (data[i + OFFSET_ZOOM] <= zoom) continue;
|
|
1276
1389
|
data[i + OFFSET_ZOOM] = zoom;
|
|
1277
|
-
const x =
|
|
1278
|
-
const y =
|
|
1279
|
-
const
|
|
1390
|
+
const x = coords[index * 2];
|
|
1391
|
+
const y = coords[index * 2 + 1];
|
|
1392
|
+
const numNeighbors = tree.withinInto(x, y, r, neighborIds);
|
|
1280
1393
|
const numPointsOrigin = data[i + OFFSET_NUM];
|
|
1281
1394
|
let numPoints = numPointsOrigin;
|
|
1282
|
-
for (
|
|
1283
|
-
const k =
|
|
1395
|
+
for (let n = 0; n < numNeighbors; n++) {
|
|
1396
|
+
const k = neighborIds[n] * stride;
|
|
1284
1397
|
if (data[k + OFFSET_ZOOM] > zoom) numPoints += data[k + OFFSET_NUM];
|
|
1285
1398
|
}
|
|
1286
1399
|
if (numPoints > numPointsOrigin && numPoints >= minPoints) {
|
|
@@ -1288,14 +1401,15 @@ var ClusterTileIndex = class {
|
|
|
1288
1401
|
let wy = y * numPointsOrigin;
|
|
1289
1402
|
let clusterProperties;
|
|
1290
1403
|
let clusterPropIndex = -1;
|
|
1291
|
-
const id = (
|
|
1292
|
-
for (
|
|
1293
|
-
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;
|
|
1294
1408
|
if (data[k + OFFSET_ZOOM] <= zoom) continue;
|
|
1295
1409
|
data[k + OFFSET_ZOOM] = zoom;
|
|
1296
1410
|
const numPoints2 = data[k + OFFSET_NUM];
|
|
1297
|
-
wx +=
|
|
1298
|
-
wy +=
|
|
1411
|
+
wx += coords[neighborIndex * 2] * numPoints2;
|
|
1412
|
+
wy += coords[neighborIndex * 2 + 1] * numPoints2;
|
|
1299
1413
|
data[k + OFFSET_PARENT] = id;
|
|
1300
1414
|
if (reduce) {
|
|
1301
1415
|
if (!clusterProperties) {
|
|
@@ -1307,19 +1421,33 @@ var ClusterTileIndex = class {
|
|
|
1307
1421
|
}
|
|
1308
1422
|
}
|
|
1309
1423
|
data[i + OFFSET_PARENT] = id;
|
|
1310
|
-
|
|
1311
|
-
|
|
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++;
|
|
1312
1433
|
} else {
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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;
|
|
1316
1441
|
if (data[k + OFFSET_ZOOM] <= zoom) continue;
|
|
1317
1442
|
data[k + OFFSET_ZOOM] = zoom;
|
|
1318
|
-
|
|
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++;
|
|
1319
1447
|
}
|
|
1320
1448
|
}
|
|
1321
1449
|
}
|
|
1322
|
-
return
|
|
1450
|
+
return nextNumItems;
|
|
1323
1451
|
}
|
|
1324
1452
|
getOriginId(clusterId) {
|
|
1325
1453
|
return clusterId - this.points.length >> 5;
|
|
@@ -1337,22 +1465,28 @@ var ClusterTileIndex = class {
|
|
|
1337
1465
|
return clone && result === original ? Object.assign({}, result) : result;
|
|
1338
1466
|
}
|
|
1339
1467
|
};
|
|
1340
|
-
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) {
|
|
1341
1475
|
return {
|
|
1342
1476
|
id: data[i + OFFSET_ID],
|
|
1343
1477
|
type: "Point",
|
|
1344
1478
|
tags: getClusterProperties(data, i, clusterProps),
|
|
1345
|
-
geometry: [
|
|
1479
|
+
geometry: [decode(coords[index * 2]), decode(coords[index * 2 + 1])]
|
|
1346
1480
|
};
|
|
1347
1481
|
}
|
|
1348
|
-
function getClusterGeoJSON(data, i, clusterProps) {
|
|
1482
|
+
function getClusterGeoJSON(coords, index, data, i, clusterProps, worldCoordinates) {
|
|
1349
1483
|
return {
|
|
1350
1484
|
type: "Feature",
|
|
1351
1485
|
id: data[i + OFFSET_ID],
|
|
1352
1486
|
properties: getClusterProperties(data, i, clusterProps),
|
|
1353
1487
|
geometry: {
|
|
1354
1488
|
type: "Point",
|
|
1355
|
-
coordinates: [unprojectX(
|
|
1489
|
+
coordinates: [unprojectX(decode(coords[index * 2]), worldCoordinates), unprojectY(decode(coords[index * 2 + 1]), worldCoordinates)]
|
|
1356
1490
|
}
|
|
1357
1491
|
};
|
|
1358
1492
|
}
|
|
@@ -1368,6 +1502,9 @@ function getClusterProperties(data, i, clusterProps) {
|
|
|
1368
1502
|
point_count_abbreviated: abbrev
|
|
1369
1503
|
});
|
|
1370
1504
|
}
|
|
1505
|
+
function isInWorld(geometry) {
|
|
1506
|
+
return geometry[0] >= 0 && geometry[0] <= 1 && geometry[1] >= 0 && geometry[1] <= 1;
|
|
1507
|
+
}
|
|
1371
1508
|
//#endregion
|
|
1372
1509
|
//#region src/tile.ts
|
|
1373
1510
|
const GEOJSONVT_CLIP_START = "geojsonvt_clip_start";
|
|
@@ -1576,7 +1713,7 @@ var TileIndex = class {
|
|
|
1576
1713
|
this.total = 0;
|
|
1577
1714
|
}
|
|
1578
1715
|
initialize(features) {
|
|
1579
|
-
this.splitTile(features, 0, 0, 0);
|
|
1716
|
+
this.splitTile(this.clipToWorld(features), 0, 0, 0);
|
|
1580
1717
|
if (this.options.debug) {
|
|
1581
1718
|
if (features.length) console.log("features: %d, points: %d", this.tiles[0].numFeatures, this.tiles[0].numPoints);
|
|
1582
1719
|
console.timeEnd("generate tiles");
|
|
@@ -1596,8 +1733,9 @@ var TileIndex = class {
|
|
|
1596
1733
|
0,
|
|
1597
1734
|
0
|
|
1598
1735
|
];
|
|
1599
|
-
const
|
|
1600
|
-
rootTile
|
|
1736
|
+
const rootSource = this.clipToWorld(source);
|
|
1737
|
+
const rootTile = createTile(rootSource, z, x, y, options);
|
|
1738
|
+
rootTile.source = rootSource;
|
|
1601
1739
|
const id = toID(z, x, y);
|
|
1602
1740
|
this.tiles[id] = rootTile;
|
|
1603
1741
|
this.tileCoords.push({
|
|
@@ -1628,6 +1766,7 @@ var TileIndex = class {
|
|
|
1628
1766
|
getTile(z, x, y) {
|
|
1629
1767
|
const { extent, debug } = this.options;
|
|
1630
1768
|
const z2 = 1 << z;
|
|
1769
|
+
if (this.options.worldCoordinates && (x < 0 || x >= z2)) return null;
|
|
1631
1770
|
x = x + z2 & z2 - 1;
|
|
1632
1771
|
const id = toID(z, x, y);
|
|
1633
1772
|
if (this.tiles[id]) return transformTile(this.tiles[id], extent);
|
|
@@ -1653,6 +1792,16 @@ var TileIndex = class {
|
|
|
1653
1792
|
return transformTile(this.tiles[id], extent);
|
|
1654
1793
|
}
|
|
1655
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
|
+
/**
|
|
1656
1805
|
* splits features from a parent tile to sub-tiles.
|
|
1657
1806
|
* z, x, and y are the coordinates of the parent tile
|
|
1658
1807
|
* cz, cx, and cy are the coordinates of the target tile
|
|
@@ -1805,7 +1954,8 @@ const defaultOptions = {
|
|
|
1805
1954
|
updateable: false,
|
|
1806
1955
|
cluster: false,
|
|
1807
1956
|
clusterOptions: defaultClusterOptions,
|
|
1808
|
-
debug: 0
|
|
1957
|
+
debug: 0,
|
|
1958
|
+
worldCoordinates: false
|
|
1809
1959
|
};
|
|
1810
1960
|
/**
|
|
1811
1961
|
* Main class for creating and managing a vector tile index from GeoJSON data.
|
|
@@ -1828,7 +1978,7 @@ var GeoJSONVT = class {
|
|
|
1828
1978
|
this.initializeIndex(features, options);
|
|
1829
1979
|
}
|
|
1830
1980
|
initializeIndex(features, options) {
|
|
1831
|
-
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);
|
|
1832
1982
|
if (!features.length) return;
|
|
1833
1983
|
this.tileIndex.initialize(features);
|
|
1834
1984
|
}
|
|
@@ -1866,7 +2016,7 @@ var GeoJSONVT = class {
|
|
|
1866
2016
|
const removeIds = /* @__PURE__ */ new Set();
|
|
1867
2017
|
for (const feature of source) {
|
|
1868
2018
|
if (feature.id == void 0) continue;
|
|
1869
|
-
if (predicate(featureToGeoJSON(feature))) continue;
|
|
2019
|
+
if (predicate(featureToGeoJSON(feature, this.options.worldCoordinates))) continue;
|
|
1870
2020
|
affected.push(feature);
|
|
1871
2021
|
removeIds.add(feature.id);
|
|
1872
2022
|
}
|
|
@@ -1881,7 +2031,7 @@ var GeoJSONVT = class {
|
|
|
1881
2031
|
*/
|
|
1882
2032
|
getData() {
|
|
1883
2033
|
if (!this.options.updateable) throw new Error("to retrieve data the `updateable` option must be set to true");
|
|
1884
|
-
return convertToGeoJSON(this.source);
|
|
2034
|
+
return convertToGeoJSON(this.source, this.options.worldCoordinates);
|
|
1885
2035
|
}
|
|
1886
2036
|
/**
|
|
1887
2037
|
* Update supercluster options and regenerate the index.
|