@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.
Files changed (41) hide show
  1. package/README.md +8 -0
  2. package/dist/cluster-tile-index.d.ts +43 -5
  3. package/dist/cluster-tile-index.d.ts.map +1 -1
  4. package/dist/convert.d.ts +2 -9
  5. package/dist/convert.d.ts.map +1 -1
  6. package/dist/deconvert.d.ts +4 -11
  7. package/dist/deconvert.d.ts.map +1 -1
  8. package/dist/definitions.d.ts +47 -7
  9. package/dist/definitions.d.ts.map +1 -1
  10. package/dist/difference.d.ts +2 -1
  11. package/dist/difference.d.ts.map +1 -1
  12. package/dist/geojson-vt-dev.js +462 -181
  13. package/dist/geojson-vt.js +1 -1
  14. package/dist/geojson-vt.mjs +459 -180
  15. package/dist/geojson-vt.mjs.map +1 -1
  16. package/dist/geojsonvt.d.ts +8 -4
  17. package/dist/geojsonvt.d.ts.map +1 -1
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/tile-index.d.ts +7 -0
  21. package/dist/tile-index.d.ts.map +1 -1
  22. package/dist/world-coordinate-helper.d.ts +13 -0
  23. package/dist/world-coordinate-helper.d.ts.map +1 -0
  24. package/dist/wrap.d.ts.map +1 -1
  25. package/package.json +5 -5
  26. package/src/clip.ts +2 -2
  27. package/src/cluster-tile-index.test.ts +123 -0
  28. package/src/cluster-tile-index.ts +270 -99
  29. package/src/convert.ts +49 -44
  30. package/src/deconvert.test.ts +4 -4
  31. package/src/deconvert.ts +15 -29
  32. package/src/definitions.ts +48 -7
  33. package/src/difference.test.ts +71 -0
  34. package/src/difference.ts +50 -21
  35. package/src/geojson-to-tile.ts +2 -2
  36. package/src/geojsonvt.test.ts +387 -1
  37. package/src/geojsonvt.ts +25 -10
  38. package/src/index.ts +2 -1
  39. package/src/tile-index.ts +19 -3
  40. package/src/world-coordinate-helper.ts +63 -0
  41. package/src/wrap.ts +2 -0
@@ -134,6 +134,51 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
134
134
  }
135
135
  }
136
136
 
137
+ //#endregion
138
+ //#region src/world-coordinate-helper.ts
139
+ /**
140
+ * Longitude and latitude, which spherical mercator puts in the world square.
141
+ */
142
+ var MercatorWorldCoordinateHelper = class {
143
+ projectX(lng) {
144
+ return lng / 360 + .5;
145
+ }
146
+ projectY(lat) {
147
+ const sin = Math.sin(lat * Math.PI / 180);
148
+ const y = .5 - .25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
149
+ return y < 0 ? 0 : y > 1 ? 1 : y;
150
+ }
151
+ unprojectX(x) {
152
+ return (x - .5) * 360;
153
+ }
154
+ unprojectY(y) {
155
+ const y2 = (180 - y * 360) * Math.PI / 180;
156
+ return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
157
+ }
158
+ };
159
+ /**
160
+ * Positions in the unit square already, see `coordinates` in {@link GeoJSONVTOptions}.
161
+ */
162
+ var IdentityWorldCoordinateHelper = class {
163
+ projectX(x) {
164
+ return x;
165
+ }
166
+ projectY(y) {
167
+ return y;
168
+ }
169
+ unprojectX(x) {
170
+ return x;
171
+ }
172
+ unprojectY(y) {
173
+ return y;
174
+ }
175
+ };
176
+ const mercatorWorldCoordinateHelper = new MercatorWorldCoordinateHelper();
177
+ const identityWorldCoordinateHelper = new IdentityWorldCoordinateHelper();
178
+ function getWorldCoordinateHelper(coordinates) {
179
+ return coordinates === "unitSquare" ? identityWorldCoordinateHelper : mercatorWorldCoordinateHelper;
180
+ }
181
+
137
182
  //#endregion
138
183
  //#region src/convert.ts
139
184
  const MAX_GEOMETRY_COLLECTION_DEPTH = 1024;
@@ -141,25 +186,38 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
141
186
  * converts GeoJSON to internal source features (an intermediate projected JSON vector format with simplification data)
142
187
  * @param data
143
188
  * @param options
189
+ * @param unlocated - the features without coordinates, by id: a feature with an id that converts to nothing is set in it, and one that converts to something is deleted from it
144
190
  * @returns
145
191
  */
146
- function convertToInternal(data, options) {
192
+ function convertToInternal(data, options, unlocated) {
147
193
  const features = [];
148
194
  switch (data.type) {
149
195
  case "FeatureCollection":
150
- for (let i = 0; i < data.features.length; i++) featureToInternal(features, data.features[i], options, i);
196
+ for (let i = 0; i < data.features.length; i++) convertFeature(features, data.features[i], options, unlocated, i);
151
197
  break;
152
198
  case "Feature":
153
- featureToInternal(features, data, options);
199
+ convertFeature(features, data, options, unlocated);
154
200
  break;
155
- default: featureToInternal(features, {
201
+ default: convertFeature(features, {
156
202
  type: "Feature",
157
203
  geometry: data,
158
204
  properties: void 0
159
- }, options);
205
+ }, options, unlocated);
160
206
  }
161
207
  return features;
162
208
  }
209
+ function convertFeature(features, geojson, options, unlocated, index) {
210
+ const count = features.length;
211
+ featureToInternal(features, geojson, options, index);
212
+ if (!unlocated) return;
213
+ const id = getFeatureId(geojson, options, index);
214
+ if (id == null) return;
215
+ if (features.length > count) unlocated.delete(id);
216
+ else unlocated.set(id, {
217
+ ...geojson,
218
+ id
219
+ });
220
+ }
163
221
  function featureToInternal(features, geojson, options, index, depth = 0) {
164
222
  if (!geojson.geometry) return;
165
223
  if (depth > MAX_GEOMETRY_COLLECTION_DEPTH) throw new Error("GeometryCollection nesting exceeds supported depth: 1024");
@@ -170,24 +228,25 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
170
228
  if (!geojson.geometry.coordinates?.length) return;
171
229
  const id = getFeatureId(geojson, options, index);
172
230
  const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
231
+ const worldCoordinateHelper = getWorldCoordinateHelper(options.coordinates);
173
232
  switch (geojson.geometry.type) {
174
233
  case "Point":
175
- convertPointFeature(features, id, geojson.geometry, geojson.properties);
234
+ convertPointFeature(features, id, geojson.geometry, worldCoordinateHelper, geojson.properties);
176
235
  return;
177
236
  case "MultiPoint":
178
- convertMultiPointFeature(features, id, geojson.geometry, geojson.properties);
237
+ convertMultiPointFeature(features, id, geojson.geometry, worldCoordinateHelper, geojson.properties);
179
238
  return;
180
239
  case "LineString":
181
- convertLineStringFeature(features, id, geojson.geometry, tolerance, geojson.properties);
240
+ convertLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
182
241
  return;
183
242
  case "MultiLineString":
184
- convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, options, geojson.properties);
243
+ convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, options, geojson.properties);
185
244
  return;
186
245
  case "Polygon":
187
- convertPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
246
+ convertPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
188
247
  return;
189
248
  case "MultiPolygon":
190
- convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
249
+ convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
191
250
  return;
192
251
  default: throw new Error("Input data is not a valid GeoJSON object.");
193
252
  }
@@ -205,53 +264,55 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
205
264
  properties: geojson.properties
206
265
  }, options, index, depth);
207
266
  }
208
- function convertPointFeature(features, id, geom, properties) {
267
+ function convertPointFeature(features, id, geom, worldCoordinateHelper, properties) {
209
268
  const out = [];
210
- out.push(projectX(geom.coordinates[0]), projectY(geom.coordinates[1]), 0);
269
+ out.push(worldCoordinateHelper.projectX(geom.coordinates[0]), worldCoordinateHelper.projectY(geom.coordinates[1]), 0);
211
270
  features.push(createFeature(id, "Point", out, properties));
212
271
  }
213
- function convertMultiPointFeature(features, id, geom, properties) {
272
+ function convertMultiPointFeature(features, id, geom, worldCoordinateHelper, properties) {
214
273
  const out = [];
215
- for (const coords of geom.coordinates) out.push(projectX(coords[0]), projectY(coords[1]), 0);
274
+ for (const coords of geom.coordinates) out.push(worldCoordinateHelper.projectX(coords[0]), worldCoordinateHelper.projectY(coords[1]), 0);
216
275
  features.push(createFeature(id, "MultiPoint", out, properties));
217
276
  }
218
- function convertLineStringFeature(features, id, geom, tolerance, properties) {
277
+ function convertLineStringFeature(features, id, geom, tolerance, worldCoordinateHelper, properties) {
219
278
  const out = { points: [] };
220
- convertLine(geom.coordinates, out, tolerance, false);
279
+ convertLine(geom.coordinates, out, tolerance, false, worldCoordinateHelper);
221
280
  features.push(createFeature(id, "LineString", out, properties));
222
281
  }
223
- function convertMultiLineStringFeature(features, id, geom, tolerance, options, properties) {
282
+ function convertMultiLineStringFeature(features, id, geom, tolerance, worldCoordinateHelper, options, properties) {
224
283
  if (options.lineMetrics) for (const line of geom.coordinates) {
225
284
  const out = { points: [] };
226
- convertLine(line, out, tolerance, false);
285
+ convertLine(line, out, tolerance, false, worldCoordinateHelper);
227
286
  features.push(createFeature(id, "LineString", out, properties));
228
287
  }
229
288
  else {
230
289
  const out = [];
231
- convertLines(geom.coordinates, out, tolerance, false);
290
+ convertLines(geom.coordinates, out, tolerance, false, worldCoordinateHelper);
232
291
  features.push(createFeature(id, "MultiLineString", out, properties));
233
292
  }
234
293
  }
235
- function convertPolygonFeature(features, id, geom, tolerance, properties) {
294
+ function convertPolygonFeature(features, id, geom, tolerance, worldCoordinateHelper, properties) {
236
295
  const out = [];
237
- convertLines(geom.coordinates, out, tolerance, true);
296
+ convertLines(geom.coordinates, out, tolerance, true, worldCoordinateHelper);
238
297
  features.push(createFeature(id, "Polygon", out, properties));
239
298
  }
240
- function convertMultiPolygonFeature(features, id, geom, tolerance, properties) {
299
+ function convertMultiPolygonFeature(features, id, geom, tolerance, worldCoordinateHelper, properties) {
241
300
  const out = [];
242
301
  for (const polygon of geom.coordinates) {
243
302
  const polygonOut = [];
244
- convertLines(polygon, polygonOut, tolerance, true);
303
+ convertLines(polygon, polygonOut, tolerance, true, worldCoordinateHelper);
304
+ if (!polygonOut.length) continue;
245
305
  out.push(polygonOut);
246
306
  }
307
+ if (!out.length) return;
247
308
  features.push(createFeature(id, "MultiPolygon", out, properties));
248
309
  }
249
- function convertLine(ring, out, tolerance, isPolygon) {
310
+ function convertLine(ring, out, tolerance, isPolygon, worldCoordinateHelper) {
250
311
  let x0, y0;
251
312
  let size = 0;
252
313
  for (let j = 0; j < ring.length; j++) {
253
- const x = projectX(ring[j][0]);
254
- const y = projectY(ring[j][1]);
314
+ const x = worldCoordinateHelper.projectX(ring[j][0]);
315
+ const y = worldCoordinateHelper.projectY(ring[j][1]);
255
316
  out.points.push(x, y, 0);
256
317
  if (j > 0) {
257
318
  if (isPolygon) size += (x0 * y - x * y0) / 2;
@@ -269,46 +330,32 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
269
330
  out.start = 0;
270
331
  out.end = out.size;
271
332
  }
272
- function convertLines(rings, out, tolerance, isPolygon) {
333
+ function convertLines(rings, out, tolerance, isPolygon, worldCoordinateHelper) {
273
334
  for (let i = 0; i < rings.length; i++) {
274
335
  const geom = { points: [] };
275
- convertLine(rings[i], geom, tolerance, isPolygon);
336
+ convertLine(rings[i], geom, tolerance, isPolygon, worldCoordinateHelper);
276
337
  out.push(geom);
277
338
  }
278
339
  }
279
- /**
280
- * Convert longitude to spherical mercator in [0..1] range
281
- */
282
- function projectX(x) {
283
- return x / 360 + .5;
284
- }
285
- /**
286
- * Convert latitude to spherical mercator in [0..1] range
287
- */
288
- function projectY(y) {
289
- const sin = Math.sin(y * Math.PI / 180);
290
- const y2 = .5 - .25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
291
- return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
292
- }
293
340
 
294
341
  //#endregion
295
342
  //#region src/deconvert.ts
296
343
  /**
297
344
  * Converts internal source features back to GeoJSON format.
298
345
  */
299
- function convertToGeoJSON(source) {
346
+ function convertToGeoJSON(source, worldCoordinateHelper = mercatorWorldCoordinateHelper) {
300
347
  return {
301
348
  type: "FeatureCollection",
302
- features: source.map((feature) => featureToGeoJSON(feature))
349
+ features: source.map((feature) => featureToGeoJSON(feature, worldCoordinateHelper))
303
350
  };
304
351
  }
305
352
  /**
306
353
  * Converts a single internal feature to GeoJSON format.
307
354
  */
308
- function featureToGeoJSON(feature) {
355
+ function featureToGeoJSON(feature, worldCoordinateHelper = mercatorWorldCoordinateHelper) {
309
356
  const geojsonFeature = {
310
357
  type: "Feature",
311
- geometry: geometryToGeoJSON(feature),
358
+ geometry: geometryToGeoJSON(feature, worldCoordinateHelper),
312
359
  properties: feature.tags
313
360
  };
314
361
  if (feature.id != null) geojsonFeature.id = feature.id;
@@ -317,52 +364,39 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
317
364
  /**
318
365
  * Converts a single internal feature geometry to GeoJSON format.
319
366
  */
320
- function geometryToGeoJSON(feature) {
367
+ function geometryToGeoJSON(feature, worldCoordinateHelper) {
321
368
  const { type, geometry } = feature;
322
369
  switch (type) {
323
370
  case "Point": return {
324
371
  type,
325
- coordinates: unprojectPoint(geometry[0], geometry[1])
372
+ coordinates: unprojectPoint(geometry[0], geometry[1], worldCoordinateHelper)
326
373
  };
327
374
  case "MultiPoint": return {
328
375
  type,
329
- coordinates: unprojectPoints(geometry)
376
+ coordinates: unprojectPoints(geometry, worldCoordinateHelper)
330
377
  };
331
378
  case "LineString": return {
332
379
  type,
333
- coordinates: unprojectPoints(geometry.points)
380
+ coordinates: unprojectPoints(geometry.points, worldCoordinateHelper)
334
381
  };
335
382
  case "MultiLineString":
336
383
  case "Polygon": return {
337
384
  type,
338
- coordinates: geometry.map((ring) => unprojectPoints(ring.points))
385
+ coordinates: geometry.map((ring) => unprojectPoints(ring.points, worldCoordinateHelper))
339
386
  };
340
387
  case "MultiPolygon": return {
341
388
  type,
342
- coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points)))
389
+ coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points, worldCoordinateHelper)))
343
390
  };
344
391
  }
345
392
  }
346
- function unprojectPoints(coords) {
393
+ function unprojectPoints(coords, worldCoordinateHelper = mercatorWorldCoordinateHelper) {
347
394
  const result = [];
348
- for (let i = 0; i < coords.length; i += 3) result.push(unprojectPoint(coords[i], coords[i + 1]));
395
+ for (let i = 0; i < coords.length; i += 3) result.push(unprojectPoint(coords[i], coords[i + 1], worldCoordinateHelper));
349
396
  return result;
350
397
  }
351
- function unprojectPoint(x, y) {
352
- return [unprojectX(x), unprojectY(y)];
353
- }
354
- /**
355
- * Convert spherical mercator in [0..1] range to longitude
356
- */
357
- function unprojectX(x) {
358
- return (x - .5) * 360;
359
- }
360
- /**
361
- * Convert spherical mercator in [0..1] range to latitude
362
- */
363
- function unprojectY(y) {
364
- const y2 = (180 - y * 360) * Math.PI / 180;
365
- return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
398
+ function unprojectPoint(x, y, worldCoordinateHelper) {
399
+ return [worldCoordinateHelper.unprojectX(x), worldCoordinateHelper.unprojectY(y)];
366
400
  }
367
401
 
368
402
  //#endregion
@@ -488,12 +522,12 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
488
522
  let exited = false;
489
523
  if (trackMetrics) segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
490
524
  if (a < start) {
491
- if (b > start) {
525
+ if (b >= start) {
492
526
  t = intersect(slice, ax, ay, bx, by, start);
493
527
  if (trackMetrics) slice.start = len + segLen * t;
494
528
  }
495
529
  } else if (a > end) {
496
- if (b < end) {
530
+ if (b <= end) {
497
531
  t = intersect(slice, ax, ay, bx, by, end);
498
532
  if (trackMetrics) slice.start = len + segLen * t;
499
533
  }
@@ -554,6 +588,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
554
588
  //#endregion
555
589
  //#region src/wrap.ts
556
590
  function wrap(features, options) {
591
+ if (!options.wrap) return features;
557
592
  const buffer = options.buffer / options.extent;
558
593
  let merged = features;
559
594
  const left = clip(features, 1, -1 - buffer, buffer, 0, -1, 2, options);
@@ -624,16 +659,19 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
624
659
  * @param source
625
660
  * @param dataDiff
626
661
  * @param options
662
+ * @param unlocated - the features without coordinates, by id, so that an update can give them some
627
663
  * @returns
628
664
  */
629
- function applySourceDiff(source, dataDiff, options) {
665
+ function applySourceDiff(source, dataDiff, options, unlocated) {
630
666
  const diff = diffToHashed(dataDiff, options);
631
667
  let affected = [];
632
668
  if (diff.removeAll) {
633
669
  affected = source;
634
670
  source = [];
671
+ unlocated?.clear();
635
672
  }
636
673
  if (diff.remove.size || diff.add.size) {
674
+ for (const id of diff.remove) unlocated?.delete(id);
637
675
  const removeFeatures = [];
638
676
  for (const feature of source) if (diff.remove.has(feature.id) || diff.add.has(feature.id)) removeFeatures.push(feature);
639
677
  if (removeFeatures.length) {
@@ -645,7 +683,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
645
683
  let addFeatures = convertToInternal({
646
684
  type: "FeatureCollection",
647
685
  features: Array.from(diff.add.values())
648
- }, options);
686
+ }, options, unlocated);
649
687
  addFeatures = wrap(addFeatures, options);
650
688
  affected = affected.concat(addFeatures);
651
689
  source = source.concat(addFeatures);
@@ -658,8 +696,14 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
658
696
  else keepFeatures.push(feature);
659
697
  for (const [id, update] of diff.update) {
660
698
  const oldFeatures = oldFeaturesMap.get(id);
661
- if (!oldFeatures || oldFeatures.length === 0) continue;
662
- const updatedFeatures = getUpdatedFeatures(oldFeatures, update, options);
699
+ if (!oldFeatures || oldFeatures.length === 0) {
700
+ if (!unlocated?.has(id)) continue;
701
+ const locatedFeatures = getUpdatedUnlocatedFeatures(id, update, options, unlocated);
702
+ affected = affected.concat(locatedFeatures);
703
+ keepFeatures = keepFeatures.concat(locatedFeatures);
704
+ continue;
705
+ }
706
+ const updatedFeatures = getUpdatedFeatures(oldFeatures, update, options, unlocated);
663
707
  affected = affected.concat(oldFeatures, updatedFeatures);
664
708
  keepFeatures = keepFeatures.concat(updatedFeatures);
665
709
  }
@@ -675,25 +719,16 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
675
719
  * @param vtFeatures - the original features
676
720
  * @param update - the update object to apply
677
721
  * @param options - the options to use for the wrap method
678
- * @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.
722
+ * @param unlocated - the features without coordinates, by id, which a new geometry without coordinates adds the feature to
723
+ * @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.
679
724
  */
680
- function getUpdatedFeatures(vtFeatures, update, options) {
725
+ function getUpdatedFeatures(vtFeatures, update, options, unlocated) {
681
726
  const changeGeometry = !!update.newGeometry;
682
- const changeProps = update.removeAllProperties || update.removeProperties?.length > 0 || update.addOrUpdateProperties?.length > 0;
727
+ const changeProps = hasPropertyChanges(update);
683
728
  if (changeGeometry) {
684
729
  const vtFeature = vtFeatures[0];
685
- const geojsonFeature = {
686
- type: "Feature",
687
- id: vtFeature.id,
688
- geometry: update.newGeometry,
689
- properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
690
- };
691
- let features = convertToInternal({
692
- type: "FeatureCollection",
693
- features: [geojsonFeature]
694
- }, options);
695
- features = wrap(features, options);
696
- return features;
730
+ const properties = changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags;
731
+ return convertNewGeometry(vtFeature.id, update.newGeometry, properties, options, unlocated);
697
732
  }
698
733
  if (changeProps) {
699
734
  const updated = [];
@@ -707,6 +742,44 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
707
742
  return vtFeatures;
708
743
  }
709
744
  /**
745
+ * Gets the features an update gives a feature without coordinates: none, unless its new geometry has some.
746
+ * Its property changes apply either way.
747
+ */
748
+ function getUpdatedUnlocatedFeatures(id, update, options, unlocated) {
749
+ const feature = unlocated.get(id);
750
+ const properties = hasPropertyChanges(update) ? applyPropertyUpdates(feature.properties, update) : feature.properties;
751
+ if (!update.newGeometry) {
752
+ unlocated.set(id, {
753
+ ...feature,
754
+ properties
755
+ });
756
+ return [];
757
+ }
758
+ return convertNewGeometry(id, update.newGeometry, properties, options, unlocated);
759
+ }
760
+ /**
761
+ * Converts the new geometry of a feature to internal format, keeping the id the feature already has
762
+ */
763
+ function convertNewGeometry(id, geometry, properties, options, unlocated) {
764
+ const features = convertToInternal({
765
+ type: "FeatureCollection",
766
+ features: [{
767
+ type: "Feature",
768
+ id,
769
+ geometry,
770
+ properties
771
+ }]
772
+ }, {
773
+ ...options,
774
+ promoteId: void 0,
775
+ generateId: false
776
+ }, unlocated);
777
+ return wrap(features, options);
778
+ }
779
+ function hasPropertyChanges(update) {
780
+ return update.removeAllProperties || update.removeProperties?.length > 0 || update.addOrUpdateProperties?.length > 0;
781
+ }
782
+ /**
710
783
  * helper to apply property updates from a diff update object to a properties object
711
784
  */
712
785
  function applyPropertyUpdates(tags, update) {
@@ -728,7 +801,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
728
801
  return {
729
802
  removeAll: diff.removeAll,
730
803
  remove: new Set(diff.remove || []),
731
- add: new Map(diff.add?.map((feature) => [options.promoteId ? feature.properties[options.promoteId] : feature.id, feature])),
804
+ add: new Map(diff.add?.map((feature) => [options.promoteId ? feature.properties?.[options.promoteId] : feature.id, feature])),
732
805
  update: new Map(diff.update?.map((update) => [update.id, update]))
733
806
  };
734
807
  }
@@ -1031,54 +1104,105 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1031
1104
  log: false,
1032
1105
  generateId: false,
1033
1106
  reduce: null,
1034
- map: (props) => props
1107
+ map: (props) => props,
1108
+ coordinates: "lngLat",
1109
+ wrap: true
1035
1110
  };
1036
- const OFFSET_ZOOM = 2;
1037
- const OFFSET_ID = 3;
1038
- const OFFSET_PARENT = 4;
1039
- const OFFSET_NUM = 5;
1040
- const OFFSET_PROP = 6;
1111
+ /**
1112
+ * The highest zoom level to cluster on. A cluster id keeps its zoom level in five bits.
1113
+ */
1114
+ const MAX_ZOOM = 30;
1115
+ /**
1116
+ * Projected coordinates, which are 0 to 1 inside the world, are stored as integers with this many steps per world,
1117
+ * so that they take half the memory of doubles. One step is ~4 cm at the equator.
1118
+ */
1119
+ const SCALE = 1073741824;
1120
+ /**
1121
+ * The largest value a 32-bit integer holds.
1122
+ */
1123
+ const MAX_INTEGER = 2147483647;
1124
+ /**
1125
+ * A projected coordinate in steps from the center of the world, see `SCALE`.
1126
+ */
1127
+ function encode(coord) {
1128
+ return (coord - .5) * SCALE;
1129
+ }
1130
+ function decode(value) {
1131
+ return value / SCALE + .5;
1132
+ }
1133
+ /**
1134
+ * Whether a projected coordinate fits in a 32-bit integer. One that is more than two worlds from the center of the
1135
+ * world does not, and neither does one that is not a number.
1136
+ */
1137
+ function fitsInteger(coord) {
1138
+ return Math.abs(encode(coord)) < MAX_INTEGER;
1139
+ }
1140
+ const OFFSET_ZOOM = 0;
1141
+ const OFFSET_ID = 1;
1142
+ const OFFSET_PARENT = 2;
1143
+ const OFFSET_NUM = 3;
1144
+ const OFFSET_PROP = 4;
1041
1145
  /**
1042
1146
  * This class allow clustering of geojson points.
1043
1147
  */
1044
1148
  var ClusterTileIndex = class {
1045
1149
  constructor(options) {
1046
- this.options = Object.assign(Object.create(defaultClusterOptions), options);
1150
+ this.options = createOptions(options);
1151
+ this.worldCoordinateHelper = getWorldCoordinateHelper(this.options.coordinates);
1047
1152
  this.trees = new Array(this.options.maxZoom + 1);
1048
- this.stride = this.options.reduce ? 7 : 6;
1153
+ this.stride = this.options.reduce ? 5 : 4;
1049
1154
  this.clusterProps = [];
1050
1155
  this.points = [];
1051
1156
  }
1052
1157
  /**
1053
- * Loads GeoJSON point features and builds the internal clustering index.
1054
- * @param points - GeoJSON point features to cluster.
1158
+ * Loads GeoJSON Point and MultiPoint features and builds the internal clustering index.
1159
+ * Each position of a MultiPoint is clustered as a point of its own, with the id and properties of its feature.
1160
+ * @param points - GeoJSON Point and MultiPoint features to cluster.
1055
1161
  */
1056
1162
  load(points) {
1057
1163
  const features = [];
1058
1164
  for (const point of points) {
1059
1165
  if (!point.geometry) continue;
1060
- const [lng, lat] = point.geometry.coordinates;
1061
- const [x, y] = [projectX(lng), projectY(lat)];
1062
- const feature = {
1063
- id: point.id,
1064
- type: "Point",
1065
- geometry: [x, y],
1066
- tags: point.properties
1067
- };
1068
- features.push(feature);
1166
+ const positions = point.geometry.type === "MultiPoint" ? point.geometry.coordinates : [point.geometry.coordinates];
1167
+ for (const [lng, lat] of positions) {
1168
+ const [x, y] = [this.worldCoordinateHelper.projectX(lng), this.worldCoordinateHelper.projectY(lat)];
1169
+ if (!this.options.wrap && !isInWorld([x, y])) continue;
1170
+ const feature = {
1171
+ id: point.id,
1172
+ type: "Point",
1173
+ geometry: [x, y],
1174
+ tags: point.properties
1175
+ };
1176
+ features.push(feature);
1177
+ }
1069
1178
  }
1070
1179
  this.createIndex(features);
1071
1180
  }
1072
1181
  /**
1073
1182
  * @internal
1074
1183
  * Loads internal GeoJSONVT point features from a data source and builds the clustering index.
1184
+ * Each position of a MultiPoint feature becomes a point of its own, with the id and properties of its feature.
1075
1185
  * @param features - {@link GeoJSONVTInternalFeature} data source features to filter and cluster.
1076
1186
  */
1077
1187
  initialize(features) {
1078
1188
  const points = [];
1079
1189
  for (const feature of features) {
1080
- if (feature.type !== "Point") continue;
1081
- points.push(feature);
1190
+ if (feature.type === "Point") {
1191
+ if (!this.options.wrap && !isInWorld(feature.geometry)) continue;
1192
+ points.push(feature);
1193
+ continue;
1194
+ }
1195
+ if (feature.type !== "MultiPoint") continue;
1196
+ for (let i = 0; i < feature.geometry.length; i += 3) {
1197
+ const geometry = [feature.geometry[i], feature.geometry[i + 1]];
1198
+ if (!this.options.wrap && !isInWorld(geometry)) continue;
1199
+ points.push({
1200
+ id: feature.id,
1201
+ type: "Point",
1202
+ geometry,
1203
+ tags: feature.tags
1204
+ });
1205
+ }
1082
1206
  }
1083
1207
  this.createIndex(points);
1084
1208
  }
@@ -1088,41 +1212,113 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1088
1212
  * @param features
1089
1213
  */
1090
1214
  updateIndex(features, _affected, options) {
1091
- this.options = Object.assign(Object.create(defaultClusterOptions), options.clusterOptions);
1215
+ this.options = createOptions({
1216
+ ...options.clusterOptions,
1217
+ coordinates: this.options.coordinates,
1218
+ wrap: this.options.wrap
1219
+ });
1092
1220
  this.initialize(features);
1093
1221
  }
1222
+ /**
1223
+ * Builds a tree per zoom level from the points. A point whose position does not fit in an integer is left out,
1224
+ * see `fitsInteger`.
1225
+ */
1094
1226
  createIndex(points) {
1095
1227
  const { log, minZoom, maxZoom } = this.options;
1228
+ this.trees = new Array(maxZoom + 1);
1229
+ this.stride = this.options.reduce ? 5 : 4;
1230
+ this.clusterProps = [];
1096
1231
  if (log) console.time("total time");
1097
1232
  const timerId = `prepare ${points.length} points`;
1098
1233
  if (log) console.time(timerId);
1099
1234
  this.points = points;
1100
- const data = [];
1235
+ const stride = this.stride;
1236
+ const notProcessed = maxZoom + 1;
1237
+ const coords = new Int32Array(points.length * 2);
1238
+ const data = new Int32Array(points.length * stride);
1239
+ let numItems = 0;
1101
1240
  for (let i = 0; i < points.length; i++) {
1102
1241
  const p = points[i];
1103
- if (!p?.geometry) continue;
1104
- let [x, y] = p.geometry;
1105
- x = Math.fround(x);
1106
- y = Math.fround(y);
1107
- data.push(x, y, Infinity, i, -1, 1);
1108
- if (this.options.reduce) data.push(0);
1242
+ if (!p?.geometry || !fitsInteger(p.geometry[0]) || !fitsInteger(p.geometry[1])) continue;
1243
+ coords[numItems * 2] = Math.round(encode(p.geometry[0]));
1244
+ coords[numItems * 2 + 1] = Math.round(encode(p.geometry[1]));
1245
+ const k = numItems * stride;
1246
+ data[k + OFFSET_ZOOM] = notProcessed;
1247
+ data[k + OFFSET_ID] = i;
1248
+ data[k + OFFSET_PARENT] = -1;
1249
+ data[k + OFFSET_NUM] = 1;
1250
+ numItems++;
1109
1251
  }
1110
- let tree = this.trees[maxZoom + 1] = this.createTree(data);
1252
+ let tree = this.trees[maxZoom + 1] = this.createTree(coords, data, numItems);
1111
1253
  if (log) console.timeEnd(timerId);
1254
+ const nextCoords = new Int32Array(numItems * 2);
1255
+ const nextData = new Int32Array(numItems * stride);
1112
1256
  for (let z = maxZoom; z >= minZoom; z--) {
1113
1257
  const now = Date.now();
1114
- tree = this.trees[z] = this.createTree(this.cluster(tree, z));
1258
+ const nextNumItems = this.cluster(tree, z, nextCoords, nextData);
1259
+ if (!this.isSameAsTree(tree, nextData, nextNumItems)) tree = this.createTree(nextCoords.slice(0, nextNumItems * 2), nextData.slice(0, nextNumItems * stride), nextNumItems);
1260
+ this.trees[z] = tree;
1115
1261
  if (log) console.log("z%d: %d clusters in %dms", z, tree.numItems, Date.now() - now);
1116
1262
  }
1117
1263
  if (log) console.timeEnd("total time");
1118
1264
  }
1119
1265
  /**
1120
1266
  * Returns clusters and/or points within a bounding box at a given zoom level.
1121
- * @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order.
1267
+ * @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order, or `[minX, minY, maxX, maxY]` in the unit square with `coordinates: 'unitSquare'`.
1268
+ * In a world that wraps, the box can reach across the antimeridian. In one that doesn't, only its part inside the world counts.
1122
1269
  * @param zoom - Zoom level to query.
1123
1270
  */
1124
1271
  getClusters(bbox, zoom) {
1125
- return this.getClustersInternal(bbox, zoom).map((f) => featureToGeoJSON(f));
1272
+ return (!this.options.wrap ? this.getClustersInWorld(bbox, zoom) : this.options.coordinates === "unitSquare" ? this.getClustersAcrossEdge(bbox, zoom) : this.getClustersInternal(bbox, zoom)).map((f) => featureToGeoJSON(f, this.worldCoordinateHelper));
1273
+ }
1274
+ /**
1275
+ * The clusters and points in the part of a box inside a world that ends at its edges.
1276
+ */
1277
+ getClustersInWorld(bbox, zoom) {
1278
+ const lngLat = this.options.coordinates !== "unitSquare";
1279
+ const y1 = this.worldCoordinateHelper.projectY(lngLat ? Math.max(-90, Math.min(90, bbox[1])) : bbox[1]);
1280
+ const y2 = this.worldCoordinateHelper.projectY(lngLat ? Math.max(-90, Math.min(90, bbox[3])) : bbox[3]);
1281
+ const minX = Math.max(0, this.worldCoordinateHelper.projectX(bbox[0]));
1282
+ const maxX = Math.min(1, this.worldCoordinateHelper.projectX(bbox[2]));
1283
+ if (minX > maxX) return [];
1284
+ return this.getClustersInRange([
1285
+ minX,
1286
+ Math.min(y1, y2),
1287
+ maxX,
1288
+ Math.max(y1, y2)
1289
+ ], zoom);
1290
+ }
1291
+ /**
1292
+ * The clusters and points in a box of a unit square that wraps, where the box can reach across its left or right edge.
1293
+ */
1294
+ getClustersAcrossEdge([minX, minY, maxX, maxY], zoom) {
1295
+ if (maxX < minX) maxX += 1;
1296
+ if (maxX - minX >= 1) return this.getClustersInRange([
1297
+ 0,
1298
+ minY,
1299
+ 1,
1300
+ maxY
1301
+ ], zoom);
1302
+ const shift = Math.floor(minX);
1303
+ const west = minX - shift;
1304
+ const east = maxX - shift;
1305
+ if (east <= 1) return this.getClustersInRange([
1306
+ west,
1307
+ minY,
1308
+ east,
1309
+ maxY
1310
+ ], zoom);
1311
+ return this.getClustersInRange([
1312
+ west,
1313
+ minY,
1314
+ 1,
1315
+ maxY
1316
+ ], zoom).concat(this.getClustersInRange([
1317
+ 0,
1318
+ minY,
1319
+ east - 1,
1320
+ maxY
1321
+ ], zoom));
1126
1322
  }
1127
1323
  getClustersInternal(bbox, zoom) {
1128
1324
  let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
@@ -1147,13 +1343,25 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1147
1343
  ], zoom);
1148
1344
  return easternHem.concat(westernHem);
1149
1345
  }
1346
+ return this.getClustersInRange([
1347
+ this.worldCoordinateHelper.projectX(minLng),
1348
+ this.worldCoordinateHelper.projectY(maxLat),
1349
+ this.worldCoordinateHelper.projectX(maxLng),
1350
+ this.worldCoordinateHelper.projectY(minLat)
1351
+ ], zoom);
1352
+ }
1353
+ /**
1354
+ * The clusters and points in a box of the internal coordinates, `[minX, minY, maxX, maxY]`.
1355
+ */
1356
+ getClustersInRange([minX, minY, maxX, maxY], zoom) {
1150
1357
  const tree = this.trees[this.limitZoom(zoom)];
1151
- const ids = tree.range(projectX(minLng), projectY(maxLat), projectX(maxLng), projectY(minLat));
1358
+ const ids = tree.range(encode(minX), encode(minY), encode(maxX), encode(maxY));
1359
+ const coords = tree.flatCoords;
1152
1360
  const data = tree.flatData;
1153
1361
  const clusters = [];
1154
1362
  for (const id of ids) {
1155
1363
  const k = this.stride * id;
1156
- clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
1364
+ clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(coords, id, data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
1157
1365
  }
1158
1366
  return clusters;
1159
1367
  }
@@ -1167,16 +1375,17 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1167
1375
  const clusterError = /* @__PURE__ */ new Error("No cluster with the specified id: " + clusterId);
1168
1376
  const tree = this.trees[originZoom];
1169
1377
  if (!tree) throw clusterError;
1378
+ const coords = tree.flatCoords;
1170
1379
  const data = tree.flatData;
1171
- if (originId * this.stride >= data.length) throw clusterError;
1172
- const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
1173
- const x = data[originId * this.stride];
1174
- const y = data[originId * this.stride + 1];
1380
+ if (originId >= tree.numItems) throw clusterError;
1381
+ const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1)) * SCALE;
1382
+ const x = coords[originId * 2];
1383
+ const y = coords[originId * 2 + 1];
1175
1384
  const ids = tree.within(x, y, r);
1176
1385
  const children = [];
1177
1386
  for (const id of ids) {
1178
1387
  const k = id * this.stride;
1179
- 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]]));
1388
+ if (data[k + OFFSET_PARENT] === clusterId) 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));
1180
1389
  }
1181
1390
  if (children.length === 0) throw clusterError;
1182
1391
  return children;
@@ -1204,10 +1413,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1204
1413
  const tree = this.trees[this.limitZoom(z)];
1205
1414
  if (!tree) return null;
1206
1415
  const z2 = Math.pow(2, z);
1416
+ if (!this.options.wrap && (x < 0 || x >= z2 || y < 0 || y >= z2)) return null;
1207
1417
  const { extent, radius } = this.options;
1208
1418
  const p = radius / extent;
1209
- const top = (y - p) / z2;
1210
- const bottom = (y + 1 + p) / z2;
1419
+ const top = encode((y - p) / z2);
1420
+ const bottom = encode((y + 1 + p) / z2);
1211
1421
  const tile = {
1212
1422
  transformed: true,
1213
1423
  features: [],
@@ -1216,9 +1426,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1216
1426
  y,
1217
1427
  z
1218
1428
  };
1219
- this.addTileFeatures(tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), tree.flatData, x, y, z2, tile);
1220
- if (x === 0) this.addTileFeatures(tree.range(1 - p / z2, top, 1, bottom), tree.flatData, z2, y, z2, tile);
1221
- if (x === z2 - 1) this.addTileFeatures(tree.range(0, top, p / z2, bottom), tree.flatData, -1, y, z2, tile);
1429
+ this.addTileFeatures(tree.range(encode((x - p) / z2), top, encode((x + 1 + p) / z2), bottom), tree, x, y, z2, tile);
1430
+ if (this.options.wrap && x === 0) this.addTileFeatures(tree.range(encode(1 - p / z2), top, encode(1), bottom), tree, z2, y, z2, tile);
1431
+ if (this.options.wrap && x === z2 - 1) this.addTileFeatures(tree.range(encode(0), top, encode(p / z2), bottom), tree, -1, y, z2, tile);
1222
1432
  return tile;
1223
1433
  }
1224
1434
  /**
@@ -1241,15 +1451,27 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1241
1451
  }
1242
1452
  return skipped;
1243
1453
  }
1244
- createTree(data) {
1245
- const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array);
1246
- for (let i = 0; i < data.length; i += this.stride) tree.add(data[i], data[i + 1]);
1454
+ createTree(coords, data, numItems) {
1455
+ const tree = new KDBush(numItems, this.options.nodeSize, Int32Array);
1456
+ for (let i = 0; i < numItems; i++) tree.add(coords[i * 2], coords[i * 2 + 1]);
1247
1457
  tree.finish();
1458
+ tree.flatCoords = coords;
1248
1459
  tree.flatData = data;
1249
1460
  tree.data = null;
1250
1461
  return tree;
1251
1462
  }
1252
- addTileFeatures(ids, data, x, y, z2, tile) {
1463
+ /**
1464
+ * Whether clustering a tree left all its points and clusters as they were, in the same order.
1465
+ */
1466
+ isSameAsTree(tree, nextData, nextNumItems) {
1467
+ if (nextNumItems !== tree.numItems) return false;
1468
+ const data = tree.flatData;
1469
+ for (let k = OFFSET_ID; k < nextNumItems * this.stride; k += this.stride) if (nextData[k] !== data[k]) return false;
1470
+ return true;
1471
+ }
1472
+ addTileFeatures(ids, tree, x, y, z2, tile) {
1473
+ const coords = tree.flatCoords;
1474
+ const data = tree.flatData;
1253
1475
  for (const i of ids) {
1254
1476
  const k = i * this.stride;
1255
1477
  const isCluster = data[k + OFFSET_NUM] > 1;
@@ -1258,8 +1480,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1258
1480
  let py;
1259
1481
  if (isCluster) {
1260
1482
  tags = getClusterProperties(data, k, this.clusterProps);
1261
- px = data[k];
1262
- py = data[k + 1];
1483
+ px = decode(coords[i * 2]);
1484
+ py = decode(coords[i * 2 + 1]);
1263
1485
  } else {
1264
1486
  const p = this.points[data[k + OFFSET_ID]];
1265
1487
  tags = p.tags;
@@ -1280,22 +1502,30 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1280
1502
  limitZoom(z) {
1281
1503
  return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
1282
1504
  }
1283
- cluster(tree, zoom) {
1284
- const { radius, extent, reduce, minPoints } = this.options;
1285
- const r = radius / (extent * Math.pow(2, zoom));
1505
+ /**
1506
+ * Clusters the points of a tree on a zoom level, writing the result to `nextCoords` and `nextData`.
1507
+ * @returns the number of points and clusters written
1508
+ */
1509
+ cluster(tree, zoom, nextCoords, nextData) {
1510
+ const { radius, extent, reduce, minPoints, maxZoom } = this.options;
1511
+ const r = radius / (extent * Math.pow(2, zoom)) * SCALE;
1512
+ const notProcessed = maxZoom + 1;
1513
+ const coords = tree.flatCoords;
1286
1514
  const data = tree.flatData;
1287
- const nextData = [];
1288
1515
  const stride = this.stride;
1289
- for (let i = 0; i < data.length; i += stride) {
1516
+ const neighborIds = new Uint32Array(tree.numItems);
1517
+ let nextNumItems = 0;
1518
+ for (let index = 0; index < tree.numItems; index++) {
1519
+ const i = index * stride;
1290
1520
  if (data[i + OFFSET_ZOOM] <= zoom) continue;
1291
1521
  data[i + OFFSET_ZOOM] = zoom;
1292
- const x = data[i];
1293
- const y = data[i + 1];
1294
- const neighborIds = tree.within(data[i], data[i + 1], r);
1522
+ const x = coords[index * 2];
1523
+ const y = coords[index * 2 + 1];
1524
+ const numNeighbors = tree.withinInto(x, y, r, neighborIds);
1295
1525
  const numPointsOrigin = data[i + OFFSET_NUM];
1296
1526
  let numPoints = numPointsOrigin;
1297
- for (const neighborId of neighborIds) {
1298
- const k = neighborId * stride;
1527
+ for (let n = 0; n < numNeighbors; n++) {
1528
+ const k = neighborIds[n] * stride;
1299
1529
  if (data[k + OFFSET_ZOOM] > zoom) numPoints += data[k + OFFSET_NUM];
1300
1530
  }
1301
1531
  if (numPoints > numPointsOrigin && numPoints >= minPoints) {
@@ -1303,14 +1533,15 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1303
1533
  let wy = y * numPointsOrigin;
1304
1534
  let clusterProperties;
1305
1535
  let clusterPropIndex = -1;
1306
- const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length;
1307
- for (const neighborId of neighborIds) {
1308
- const k = neighborId * stride;
1536
+ const id = (index << 5) + (zoom + 1) + this.points.length;
1537
+ for (let n = 0; n < numNeighbors; n++) {
1538
+ const neighborIndex = neighborIds[n];
1539
+ const k = neighborIndex * stride;
1309
1540
  if (data[k + OFFSET_ZOOM] <= zoom) continue;
1310
1541
  data[k + OFFSET_ZOOM] = zoom;
1311
1542
  const numPoints2 = data[k + OFFSET_NUM];
1312
- wx += data[k] * numPoints2;
1313
- wy += data[k + 1] * numPoints2;
1543
+ wx += coords[neighborIndex * 2] * numPoints2;
1544
+ wy += coords[neighborIndex * 2 + 1] * numPoints2;
1314
1545
  data[k + OFFSET_PARENT] = id;
1315
1546
  if (reduce) {
1316
1547
  if (!clusterProperties) {
@@ -1322,19 +1553,33 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1322
1553
  }
1323
1554
  }
1324
1555
  data[i + OFFSET_PARENT] = id;
1325
- nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints);
1326
- if (reduce) nextData.push(clusterPropIndex);
1556
+ nextCoords[nextNumItems * 2] = Math.round(wx / numPoints);
1557
+ nextCoords[nextNumItems * 2 + 1] = Math.round(wy / numPoints);
1558
+ const next = nextNumItems * stride;
1559
+ nextData[next + OFFSET_ZOOM] = notProcessed;
1560
+ nextData[next + OFFSET_ID] = id;
1561
+ nextData[next + OFFSET_PARENT] = -1;
1562
+ nextData[next + OFFSET_NUM] = numPoints;
1563
+ if (reduce) nextData[next + OFFSET_PROP] = clusterPropIndex;
1564
+ nextNumItems++;
1327
1565
  } else {
1328
- for (let j = 0; j < stride; j++) nextData.push(data[i + j]);
1329
- if (numPoints > 1) for (const neighborId of neighborIds) {
1330
- const k = neighborId * stride;
1566
+ nextCoords[nextNumItems * 2] = x;
1567
+ nextCoords[nextNumItems * 2 + 1] = y;
1568
+ for (let j = 0; j < stride; j++) nextData[nextNumItems * stride + j] = data[i + j];
1569
+ nextNumItems++;
1570
+ if (numPoints > 1) for (let n = 0; n < numNeighbors; n++) {
1571
+ const neighborIndex = neighborIds[n];
1572
+ const k = neighborIndex * stride;
1331
1573
  if (data[k + OFFSET_ZOOM] <= zoom) continue;
1332
1574
  data[k + OFFSET_ZOOM] = zoom;
1333
- for (let j = 0; j < stride; j++) nextData.push(data[k + j]);
1575
+ nextCoords[nextNumItems * 2] = coords[neighborIndex * 2];
1576
+ nextCoords[nextNumItems * 2 + 1] = coords[neighborIndex * 2 + 1];
1577
+ for (let j = 0; j < stride; j++) nextData[nextNumItems * stride + j] = data[k + j];
1578
+ nextNumItems++;
1334
1579
  }
1335
1580
  }
1336
1581
  }
1337
- return nextData;
1582
+ return nextNumItems;
1338
1583
  }
1339
1584
  getOriginId(clusterId) {
1340
1585
  return clusterId - this.points.length >> 5;
@@ -1352,22 +1597,28 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1352
1597
  return clone && result === original ? Object.assign({}, result) : result;
1353
1598
  }
1354
1599
  };
1355
- function getClusterFeature(data, i, clusterProps) {
1600
+ function createOptions(options) {
1601
+ const result = Object.assign(Object.create(defaultClusterOptions), options);
1602
+ result.minZoom = Math.min(result.minZoom, MAX_ZOOM);
1603
+ result.maxZoom = Math.min(result.maxZoom, MAX_ZOOM);
1604
+ return result;
1605
+ }
1606
+ function getClusterFeature(coords, index, data, i, clusterProps) {
1356
1607
  return {
1357
1608
  id: data[i + OFFSET_ID],
1358
1609
  type: "Point",
1359
1610
  tags: getClusterProperties(data, i, clusterProps),
1360
- geometry: [data[i], data[i + 1]]
1611
+ geometry: [decode(coords[index * 2]), decode(coords[index * 2 + 1])]
1361
1612
  };
1362
1613
  }
1363
- function getClusterGeoJSON(data, i, clusterProps) {
1614
+ function getClusterGeoJSON(coords, index, data, i, clusterProps, worldCoordinateHelper) {
1364
1615
  return {
1365
1616
  type: "Feature",
1366
1617
  id: data[i + OFFSET_ID],
1367
1618
  properties: getClusterProperties(data, i, clusterProps),
1368
1619
  geometry: {
1369
1620
  type: "Point",
1370
- coordinates: [unprojectX(data[i]), unprojectY(data[i + 1])]
1621
+ coordinates: [worldCoordinateHelper.unprojectX(decode(coords[index * 2])), worldCoordinateHelper.unprojectY(decode(coords[index * 2 + 1]))]
1371
1622
  }
1372
1623
  };
1373
1624
  }
@@ -1383,6 +1634,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1383
1634
  point_count_abbreviated: abbrev
1384
1635
  });
1385
1636
  }
1637
+ function isInWorld(geometry) {
1638
+ return geometry[0] >= 0 && geometry[0] <= 1 && geometry[1] >= 0 && geometry[1] <= 1;
1639
+ }
1386
1640
 
1387
1641
  //#endregion
1388
1642
  //#region src/tile.ts
@@ -1594,7 +1848,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1594
1848
  this.total = 0;
1595
1849
  }
1596
1850
  initialize(features) {
1597
- this.splitTile(features, 0, 0, 0);
1851
+ this.splitTile(this.clipToWorld(features), 0, 0, 0);
1598
1852
  if (this.options.debug) {
1599
1853
  if (features.length) console.log("features: %d, points: %d", this.tiles[0].numFeatures, this.tiles[0].numPoints);
1600
1854
  console.timeEnd("generate tiles");
@@ -1614,8 +1868,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1614
1868
  0,
1615
1869
  0
1616
1870
  ];
1617
- const rootTile = createTile(source, z, x, y, options);
1618
- rootTile.source = source;
1871
+ const rootSource = this.clipToWorld(source);
1872
+ const rootTile = createTile(rootSource, z, x, y, options);
1873
+ rootTile.source = rootSource;
1619
1874
  const id = toID(z, x, y);
1620
1875
  this.tiles[id] = rootTile;
1621
1876
  this.tileCoords.push({
@@ -1646,6 +1901,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1646
1901
  getTile(z, x, y) {
1647
1902
  const { extent, debug } = this.options;
1648
1903
  const z2 = 1 << z;
1904
+ if (!this.options.wrap && (x < 0 || x >= z2)) return null;
1649
1905
  x = x + z2 & z2 - 1;
1650
1906
  const id = toID(z, x, y);
1651
1907
  if (this.tiles[id]) return transformTile(this.tiles[id], extent);
@@ -1671,6 +1927,19 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1671
1927
  return transformTile(this.tiles[id], extent);
1672
1928
  }
1673
1929
  /**
1930
+ * The features the top tile starts from. A world that wraps keeps them all left and right, where wrapping already
1931
+ * put them inside, and one that doesn't only what lies inside the world square and its buffer. Positions in the
1932
+ * unit square are also cut above and below in a world that wraps, where lng/lat is clamped already. The index keeps
1933
+ * the rest, so an update can still move a feature in from outside.
1934
+ */
1935
+ clipToWorld(features) {
1936
+ const { wrap, coordinates } = this.options;
1937
+ if (wrap && coordinates !== "unitSquare") return features;
1938
+ const buffer = this.options.buffer / this.options.extent;
1939
+ const inX = wrap ? features : clip(features, 1, -buffer, 1 + buffer, 0, -Infinity, Infinity, this.options) || [];
1940
+ return clip(inX, 1, -buffer, 1 + buffer, 1, -Infinity, Infinity, this.options) || [];
1941
+ }
1942
+ /**
1674
1943
  * splits features from a parent tile to sub-tiles.
1675
1944
  * z, x, and y are the coordinates of the parent tile
1676
1945
  * cz, cx, and cy are the coordinates of the target tile
@@ -1824,7 +2093,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1824
2093
  updateable: false,
1825
2094
  cluster: false,
1826
2095
  clusterOptions: defaultClusterOptions,
1827
- debug: 0
2096
+ debug: 0,
2097
+ coordinates: "lngLat",
2098
+ wrap: true
1828
2099
  };
1829
2100
  /**
1830
2101
  * Main class for creating and managing a vector tile index from GeoJSON data.
@@ -1832,11 +2103,13 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1832
2103
  var GeoJSONVT = class {
1833
2104
  constructor(data, options) {
1834
2105
  options = this.options = Object.assign({}, defaultOptions, options);
2106
+ this.worldCoordinateHelper = getWorldCoordinateHelper(options.coordinates);
1835
2107
  const debug = options.debug;
1836
2108
  if (debug) console.time("preprocess data");
1837
2109
  if (options.maxZoom < 0 || options.maxZoom > 24) throw new Error("maxZoom should be in the 0-24 range");
1838
2110
  if (options.promoteId && options.generateId) throw new Error("promoteId and generateId cannot be used together.");
1839
- let features = convertToInternal(data, options);
2111
+ if (options.updateable) this.unlocated = /* @__PURE__ */ new Map();
2112
+ let features = convertToInternal(data, options, this.unlocated);
1840
2113
  if (debug) {
1841
2114
  console.timeEnd("preprocess data");
1842
2115
  console.log("index: maxZoom: %d, maxPoints: %d", options.indexMaxZoom, options.indexMaxPoints);
@@ -1847,7 +2120,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1847
2120
  this.initializeIndex(features, options);
1848
2121
  }
1849
2122
  initializeIndex(features, options) {
1850
- this.tileIndex = options.cluster ? new ClusterTileIndex(options.clusterOptions) : new TileIndex(options);
2123
+ this.tileIndex = options.cluster ? new ClusterTileIndex({
2124
+ ...options.clusterOptions,
2125
+ coordinates: options.coordinates,
2126
+ wrap: options.wrap
2127
+ }) : new TileIndex(options);
1851
2128
  if (!features.length) return;
1852
2129
  this.tileIndex.initialize(features);
1853
2130
  }
@@ -1872,24 +2149,27 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1872
2149
  updateData(diff, filter) {
1873
2150
  const options = this.options;
1874
2151
  if (!options.updateable) throw new Error("to update tile geojson `updateable` option must be set to true");
1875
- let { affected, source } = applySourceDiff(this.source, diff, options);
2152
+ let { affected, source } = applySourceDiff(this.source, diff, options, this.unlocated);
1876
2153
  if (filter) ({affected, source} = this.filterUpdate(source, affected, filter));
1877
2154
  if (!affected.length) return;
1878
2155
  this.source = source;
1879
2156
  this.tileIndex.updateIndex(source, affected, options);
1880
2157
  }
1881
2158
  /**
1882
- * Filter an update using a predicate function. Returns the affected and updated source features.
2159
+ * Filter an update using a predicate function, the features an update emptied included, so that one the filter
2160
+ * rejects can't come back with a later update. A feature without a geometry at all isn't passed to the predicate.
2161
+ * Returns the affected and updated source features.
1883
2162
  */
1884
2163
  filterUpdate(source, affected, predicate) {
1885
2164
  const removeIds = /* @__PURE__ */ new Set();
1886
2165
  for (const feature of source) {
1887
2166
  if (feature.id == void 0) continue;
1888
- if (predicate(featureToGeoJSON(feature))) continue;
2167
+ if (predicate(featureToGeoJSON(feature, this.worldCoordinateHelper))) continue;
1889
2168
  affected.push(feature);
1890
2169
  removeIds.add(feature.id);
1891
2170
  }
1892
2171
  source = source.filter((feature) => !removeIds.has(feature.id));
2172
+ for (const [id, feature] of this.unlocated ?? []) if (feature.geometry && !predicate(feature)) this.unlocated.delete(id);
1893
2173
  return {
1894
2174
  affected,
1895
2175
  source
@@ -1900,12 +2180,12 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1900
2180
  */
1901
2181
  getData() {
1902
2182
  if (!this.options.updateable) throw new Error("to retrieve data the `updateable` option must be set to true");
1903
- return convertToGeoJSON(this.source);
2183
+ return convertToGeoJSON(this.source, this.worldCoordinateHelper);
1904
2184
  }
1905
2185
  /**
1906
2186
  * Update supercluster options and regenerate the index.
1907
2187
  * @param cluster - whether to enable clustering
1908
- * @param clusterOptions - {@link SuperclusterOptions}
2188
+ * @param clusterOptions - {@link SuperclusterOptions} without `coordinates` and `wrap`, which come from the index options
1909
2189
  */
1910
2190
  updateClusterOptions(cluster, clusterOptions) {
1911
2191
  const wasCluster = this.options.cluster;
@@ -1982,9 +2262,10 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1982
2262
  function geoJSONToTile(data, z, x, y, options = {}) {
1983
2263
  options = {
1984
2264
  ...defaultOptions,
2265
+ wrap: false,
1985
2266
  ...options
1986
2267
  };
1987
- const { wrap: shouldWrap = false, clip: shouldClip = false } = options;
2268
+ const { wrap: shouldWrap, clip: shouldClip = false } = options;
1988
2269
  let features = convertToInternal(data, options);
1989
2270
  if (shouldWrap) features = wrap(features, options);
1990
2271
  if (shouldClip || options.lineMetrics) {