@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
@@ -127,31 +127,88 @@ function calcLineBBox(feature, geom) {
127
127
  }
128
128
  }
129
129
  //#endregion
130
+ //#region src/world-coordinate-helper.ts
131
+ /**
132
+ * Longitude and latitude, which spherical mercator puts in the world square.
133
+ */
134
+ var MercatorWorldCoordinateHelper = class {
135
+ projectX(lng) {
136
+ return lng / 360 + .5;
137
+ }
138
+ projectY(lat) {
139
+ const sin = Math.sin(lat * Math.PI / 180);
140
+ const y = .5 - .25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
141
+ return y < 0 ? 0 : y > 1 ? 1 : y;
142
+ }
143
+ unprojectX(x) {
144
+ return (x - .5) * 360;
145
+ }
146
+ unprojectY(y) {
147
+ const y2 = (180 - y * 360) * Math.PI / 180;
148
+ return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
149
+ }
150
+ };
151
+ /**
152
+ * Positions in the unit square already, see `coordinates` in {@link GeoJSONVTOptions}.
153
+ */
154
+ var IdentityWorldCoordinateHelper = class {
155
+ projectX(x) {
156
+ return x;
157
+ }
158
+ projectY(y) {
159
+ return y;
160
+ }
161
+ unprojectX(x) {
162
+ return x;
163
+ }
164
+ unprojectY(y) {
165
+ return y;
166
+ }
167
+ };
168
+ const mercatorWorldCoordinateHelper = new MercatorWorldCoordinateHelper();
169
+ const identityWorldCoordinateHelper = new IdentityWorldCoordinateHelper();
170
+ function getWorldCoordinateHelper(coordinates) {
171
+ return coordinates === "unitSquare" ? identityWorldCoordinateHelper : mercatorWorldCoordinateHelper;
172
+ }
173
+ //#endregion
130
174
  //#region src/convert.ts
131
175
  const MAX_GEOMETRY_COLLECTION_DEPTH = 1024;
132
176
  /**
133
177
  * converts GeoJSON to internal source features (an intermediate projected JSON vector format with simplification data)
134
178
  * @param data
135
179
  * @param options
180
+ * @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
136
181
  * @returns
137
182
  */
138
- function convertToInternal(data, options) {
183
+ function convertToInternal(data, options, unlocated) {
139
184
  const features = [];
140
185
  switch (data.type) {
141
186
  case "FeatureCollection":
142
- for (let i = 0; i < data.features.length; i++) featureToInternal(features, data.features[i], options, i);
187
+ for (let i = 0; i < data.features.length; i++) convertFeature(features, data.features[i], options, unlocated, i);
143
188
  break;
144
189
  case "Feature":
145
- featureToInternal(features, data, options);
190
+ convertFeature(features, data, options, unlocated);
146
191
  break;
147
- default: featureToInternal(features, {
192
+ default: convertFeature(features, {
148
193
  type: "Feature",
149
194
  geometry: data,
150
195
  properties: void 0
151
- }, options);
196
+ }, options, unlocated);
152
197
  }
153
198
  return features;
154
199
  }
200
+ function convertFeature(features, geojson, options, unlocated, index) {
201
+ const count = features.length;
202
+ featureToInternal(features, geojson, options, index);
203
+ if (!unlocated) return;
204
+ const id = getFeatureId(geojson, options, index);
205
+ if (id == null) return;
206
+ if (features.length > count) unlocated.delete(id);
207
+ else unlocated.set(id, {
208
+ ...geojson,
209
+ id
210
+ });
211
+ }
155
212
  function featureToInternal(features, geojson, options, index, depth = 0) {
156
213
  if (!geojson.geometry) return;
157
214
  if (depth > MAX_GEOMETRY_COLLECTION_DEPTH) throw new Error("GeometryCollection nesting exceeds supported depth: 1024");
@@ -162,24 +219,25 @@ function featureToInternal(features, geojson, options, index, depth = 0) {
162
219
  if (!geojson.geometry.coordinates?.length) return;
163
220
  const id = getFeatureId(geojson, options, index);
164
221
  const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
222
+ const worldCoordinateHelper = getWorldCoordinateHelper(options.coordinates);
165
223
  switch (geojson.geometry.type) {
166
224
  case "Point":
167
- convertPointFeature(features, id, geojson.geometry, geojson.properties);
225
+ convertPointFeature(features, id, geojson.geometry, worldCoordinateHelper, geojson.properties);
168
226
  return;
169
227
  case "MultiPoint":
170
- convertMultiPointFeature(features, id, geojson.geometry, geojson.properties);
228
+ convertMultiPointFeature(features, id, geojson.geometry, worldCoordinateHelper, geojson.properties);
171
229
  return;
172
230
  case "LineString":
173
- convertLineStringFeature(features, id, geojson.geometry, tolerance, geojson.properties);
231
+ convertLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
174
232
  return;
175
233
  case "MultiLineString":
176
- convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, options, geojson.properties);
234
+ convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, options, geojson.properties);
177
235
  return;
178
236
  case "Polygon":
179
- convertPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
237
+ convertPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
180
238
  return;
181
239
  case "MultiPolygon":
182
- convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
240
+ convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
183
241
  return;
184
242
  default: throw new Error("Input data is not a valid GeoJSON object.");
185
243
  }
@@ -197,53 +255,55 @@ function convertGeometryCollection(features, geojson, geometry, options, index,
197
255
  properties: geojson.properties
198
256
  }, options, index, depth);
199
257
  }
200
- function convertPointFeature(features, id, geom, properties) {
258
+ function convertPointFeature(features, id, geom, worldCoordinateHelper, properties) {
201
259
  const out = [];
202
- out.push(projectX(geom.coordinates[0]), projectY(geom.coordinates[1]), 0);
260
+ out.push(worldCoordinateHelper.projectX(geom.coordinates[0]), worldCoordinateHelper.projectY(geom.coordinates[1]), 0);
203
261
  features.push(createFeature(id, "Point", out, properties));
204
262
  }
205
- function convertMultiPointFeature(features, id, geom, properties) {
263
+ function convertMultiPointFeature(features, id, geom, worldCoordinateHelper, properties) {
206
264
  const out = [];
207
- for (const coords of geom.coordinates) out.push(projectX(coords[0]), projectY(coords[1]), 0);
265
+ for (const coords of geom.coordinates) out.push(worldCoordinateHelper.projectX(coords[0]), worldCoordinateHelper.projectY(coords[1]), 0);
208
266
  features.push(createFeature(id, "MultiPoint", out, properties));
209
267
  }
210
- function convertLineStringFeature(features, id, geom, tolerance, properties) {
268
+ function convertLineStringFeature(features, id, geom, tolerance, worldCoordinateHelper, properties) {
211
269
  const out = { points: [] };
212
- convertLine(geom.coordinates, out, tolerance, false);
270
+ convertLine(geom.coordinates, out, tolerance, false, worldCoordinateHelper);
213
271
  features.push(createFeature(id, "LineString", out, properties));
214
272
  }
215
- function convertMultiLineStringFeature(features, id, geom, tolerance, options, properties) {
273
+ function convertMultiLineStringFeature(features, id, geom, tolerance, worldCoordinateHelper, options, properties) {
216
274
  if (options.lineMetrics) for (const line of geom.coordinates) {
217
275
  const out = { points: [] };
218
- convertLine(line, out, tolerance, false);
276
+ convertLine(line, out, tolerance, false, worldCoordinateHelper);
219
277
  features.push(createFeature(id, "LineString", out, properties));
220
278
  }
221
279
  else {
222
280
  const out = [];
223
- convertLines(geom.coordinates, out, tolerance, false);
281
+ convertLines(geom.coordinates, out, tolerance, false, worldCoordinateHelper);
224
282
  features.push(createFeature(id, "MultiLineString", out, properties));
225
283
  }
226
284
  }
227
- function convertPolygonFeature(features, id, geom, tolerance, properties) {
285
+ function convertPolygonFeature(features, id, geom, tolerance, worldCoordinateHelper, properties) {
228
286
  const out = [];
229
- convertLines(geom.coordinates, out, tolerance, true);
287
+ convertLines(geom.coordinates, out, tolerance, true, worldCoordinateHelper);
230
288
  features.push(createFeature(id, "Polygon", out, properties));
231
289
  }
232
- function convertMultiPolygonFeature(features, id, geom, tolerance, properties) {
290
+ function convertMultiPolygonFeature(features, id, geom, tolerance, worldCoordinateHelper, properties) {
233
291
  const out = [];
234
292
  for (const polygon of geom.coordinates) {
235
293
  const polygonOut = [];
236
- convertLines(polygon, polygonOut, tolerance, true);
294
+ convertLines(polygon, polygonOut, tolerance, true, worldCoordinateHelper);
295
+ if (!polygonOut.length) continue;
237
296
  out.push(polygonOut);
238
297
  }
298
+ if (!out.length) return;
239
299
  features.push(createFeature(id, "MultiPolygon", out, properties));
240
300
  }
241
- function convertLine(ring, out, tolerance, isPolygon) {
301
+ function convertLine(ring, out, tolerance, isPolygon, worldCoordinateHelper) {
242
302
  let x0, y0;
243
303
  let size = 0;
244
304
  for (let j = 0; j < ring.length; j++) {
245
- const x = projectX(ring[j][0]);
246
- const y = projectY(ring[j][1]);
305
+ const x = worldCoordinateHelper.projectX(ring[j][0]);
306
+ const y = worldCoordinateHelper.projectY(ring[j][1]);
247
307
  out.points.push(x, y, 0);
248
308
  if (j > 0) {
249
309
  if (isPolygon) size += (x0 * y - x * y0) / 2;
@@ -261,45 +321,31 @@ function convertLine(ring, out, tolerance, isPolygon) {
261
321
  out.start = 0;
262
322
  out.end = out.size;
263
323
  }
264
- function convertLines(rings, out, tolerance, isPolygon) {
324
+ function convertLines(rings, out, tolerance, isPolygon, worldCoordinateHelper) {
265
325
  for (let i = 0; i < rings.length; i++) {
266
326
  const geom = { points: [] };
267
- convertLine(rings[i], geom, tolerance, isPolygon);
327
+ convertLine(rings[i], geom, tolerance, isPolygon, worldCoordinateHelper);
268
328
  out.push(geom);
269
329
  }
270
330
  }
271
- /**
272
- * Convert longitude to spherical mercator in [0..1] range
273
- */
274
- function projectX(x) {
275
- return x / 360 + .5;
276
- }
277
- /**
278
- * Convert latitude to spherical mercator in [0..1] range
279
- */
280
- function projectY(y) {
281
- const sin = Math.sin(y * Math.PI / 180);
282
- const y2 = .5 - .25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
283
- return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
284
- }
285
331
  //#endregion
286
332
  //#region src/deconvert.ts
287
333
  /**
288
334
  * Converts internal source features back to GeoJSON format.
289
335
  */
290
- function convertToGeoJSON(source) {
336
+ function convertToGeoJSON(source, worldCoordinateHelper = mercatorWorldCoordinateHelper) {
291
337
  return {
292
338
  type: "FeatureCollection",
293
- features: source.map((feature) => featureToGeoJSON(feature))
339
+ features: source.map((feature) => featureToGeoJSON(feature, worldCoordinateHelper))
294
340
  };
295
341
  }
296
342
  /**
297
343
  * Converts a single internal feature to GeoJSON format.
298
344
  */
299
- function featureToGeoJSON(feature) {
345
+ function featureToGeoJSON(feature, worldCoordinateHelper = mercatorWorldCoordinateHelper) {
300
346
  const geojsonFeature = {
301
347
  type: "Feature",
302
- geometry: geometryToGeoJSON(feature),
348
+ geometry: geometryToGeoJSON(feature, worldCoordinateHelper),
303
349
  properties: feature.tags
304
350
  };
305
351
  if (feature.id != null) geojsonFeature.id = feature.id;
@@ -308,52 +354,39 @@ function featureToGeoJSON(feature) {
308
354
  /**
309
355
  * Converts a single internal feature geometry to GeoJSON format.
310
356
  */
311
- function geometryToGeoJSON(feature) {
357
+ function geometryToGeoJSON(feature, worldCoordinateHelper) {
312
358
  const { type, geometry } = feature;
313
359
  switch (type) {
314
360
  case "Point": return {
315
361
  type,
316
- coordinates: unprojectPoint(geometry[0], geometry[1])
362
+ coordinates: unprojectPoint(geometry[0], geometry[1], worldCoordinateHelper)
317
363
  };
318
364
  case "MultiPoint": return {
319
365
  type,
320
- coordinates: unprojectPoints(geometry)
366
+ coordinates: unprojectPoints(geometry, worldCoordinateHelper)
321
367
  };
322
368
  case "LineString": return {
323
369
  type,
324
- coordinates: unprojectPoints(geometry.points)
370
+ coordinates: unprojectPoints(geometry.points, worldCoordinateHelper)
325
371
  };
326
372
  case "MultiLineString":
327
373
  case "Polygon": return {
328
374
  type,
329
- coordinates: geometry.map((ring) => unprojectPoints(ring.points))
375
+ coordinates: geometry.map((ring) => unprojectPoints(ring.points, worldCoordinateHelper))
330
376
  };
331
377
  case "MultiPolygon": return {
332
378
  type,
333
- coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points)))
379
+ coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points, worldCoordinateHelper)))
334
380
  };
335
381
  }
336
382
  }
337
- function unprojectPoints(coords) {
383
+ function unprojectPoints(coords, worldCoordinateHelper = mercatorWorldCoordinateHelper) {
338
384
  const result = [];
339
- for (let i = 0; i < coords.length; i += 3) result.push(unprojectPoint(coords[i], coords[i + 1]));
385
+ for (let i = 0; i < coords.length; i += 3) result.push(unprojectPoint(coords[i], coords[i + 1], worldCoordinateHelper));
340
386
  return result;
341
387
  }
342
- function unprojectPoint(x, y) {
343
- return [unprojectX(x), unprojectY(y)];
344
- }
345
- /**
346
- * Convert spherical mercator in [0..1] range to longitude
347
- */
348
- function unprojectX(x) {
349
- return (x - .5) * 360;
350
- }
351
- /**
352
- * Convert spherical mercator in [0..1] range to latitude
353
- */
354
- function unprojectY(y) {
355
- const y2 = (180 - y * 360) * Math.PI / 180;
356
- return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
388
+ function unprojectPoint(x, y, worldCoordinateHelper) {
389
+ return [worldCoordinateHelper.unprojectX(x), worldCoordinateHelper.unprojectY(y)];
357
390
  }
358
391
  //#endregion
359
392
  //#region src/clip.ts
@@ -478,12 +511,12 @@ function clipLine(geom, newGeom, start, end, axis, isPolygon, trackMetrics) {
478
511
  let exited = false;
479
512
  if (trackMetrics) segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
480
513
  if (a < start) {
481
- if (b > start) {
514
+ if (b >= start) {
482
515
  t = intersect(slice, ax, ay, bx, by, start);
483
516
  if (trackMetrics) slice.start = len + segLen * t;
484
517
  }
485
518
  } else if (a > end) {
486
- if (b < end) {
519
+ if (b <= end) {
487
520
  t = intersect(slice, ax, ay, bx, by, end);
488
521
  if (trackMetrics) slice.start = len + segLen * t;
489
522
  }
@@ -543,6 +576,7 @@ function intersectY(out, ax, ay, bx, by, y) {
543
576
  //#endregion
544
577
  //#region src/wrap.ts
545
578
  function wrap(features, options) {
579
+ if (!options.wrap) return features;
546
580
  const buffer = options.buffer / options.extent;
547
581
  let merged = features;
548
582
  const left = clip(features, 1, -1 - buffer, buffer, 0, -1, 2, options);
@@ -612,16 +646,19 @@ function shiftLineCoords(line, offset) {
612
646
  * @param source
613
647
  * @param dataDiff
614
648
  * @param options
649
+ * @param unlocated - the features without coordinates, by id, so that an update can give them some
615
650
  * @returns
616
651
  */
617
- function applySourceDiff(source, dataDiff, options) {
652
+ function applySourceDiff(source, dataDiff, options, unlocated) {
618
653
  const diff = diffToHashed(dataDiff, options);
619
654
  let affected = [];
620
655
  if (diff.removeAll) {
621
656
  affected = source;
622
657
  source = [];
658
+ unlocated?.clear();
623
659
  }
624
660
  if (diff.remove.size || diff.add.size) {
661
+ for (const id of diff.remove) unlocated?.delete(id);
625
662
  const removeFeatures = [];
626
663
  for (const feature of source) if (diff.remove.has(feature.id) || diff.add.has(feature.id)) removeFeatures.push(feature);
627
664
  if (removeFeatures.length) {
@@ -633,7 +670,7 @@ function applySourceDiff(source, dataDiff, options) {
633
670
  let addFeatures = convertToInternal({
634
671
  type: "FeatureCollection",
635
672
  features: Array.from(diff.add.values())
636
- }, options);
673
+ }, options, unlocated);
637
674
  addFeatures = wrap(addFeatures, options);
638
675
  affected = affected.concat(addFeatures);
639
676
  source = source.concat(addFeatures);
@@ -646,8 +683,14 @@ function applySourceDiff(source, dataDiff, options) {
646
683
  else keepFeatures.push(feature);
647
684
  for (const [id, update] of diff.update) {
648
685
  const oldFeatures = oldFeaturesMap.get(id);
649
- if (!oldFeatures || oldFeatures.length === 0) continue;
650
- const updatedFeatures = getUpdatedFeatures(oldFeatures, update, options);
686
+ if (!oldFeatures || oldFeatures.length === 0) {
687
+ if (!unlocated?.has(id)) continue;
688
+ const locatedFeatures = getUpdatedUnlocatedFeatures(id, update, options, unlocated);
689
+ affected = affected.concat(locatedFeatures);
690
+ keepFeatures = keepFeatures.concat(locatedFeatures);
691
+ continue;
692
+ }
693
+ const updatedFeatures = getUpdatedFeatures(oldFeatures, update, options, unlocated);
651
694
  affected = affected.concat(oldFeatures, updatedFeatures);
652
695
  keepFeatures = keepFeatures.concat(updatedFeatures);
653
696
  }
@@ -663,24 +706,16 @@ function applySourceDiff(source, dataDiff, options) {
663
706
  * @param vtFeatures - the original features
664
707
  * @param update - the update object to apply
665
708
  * @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.
709
+ * @param unlocated - the features without coordinates, by id, which a new geometry without coordinates adds the feature to
710
+ * @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
711
  */
668
- function getUpdatedFeatures(vtFeatures, update, options) {
712
+ function getUpdatedFeatures(vtFeatures, update, options, unlocated) {
669
713
  const changeGeometry = !!update.newGeometry;
670
- const changeProps = update.removeAllProperties || update.removeProperties?.length > 0 || update.addOrUpdateProperties?.length > 0;
714
+ const changeProps = hasPropertyChanges(update);
671
715
  if (changeGeometry) {
672
716
  const vtFeature = vtFeatures[0];
673
- let features = convertToInternal({
674
- type: "FeatureCollection",
675
- features: [{
676
- type: "Feature",
677
- id: vtFeature.id,
678
- geometry: update.newGeometry,
679
- properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
680
- }]
681
- }, options);
682
- features = wrap(features, options);
683
- return features;
717
+ const properties = changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags;
718
+ return convertNewGeometry(vtFeature.id, update.newGeometry, properties, options, unlocated);
684
719
  }
685
720
  if (changeProps) {
686
721
  const updated = [];
@@ -694,6 +729,43 @@ function getUpdatedFeatures(vtFeatures, update, options) {
694
729
  return vtFeatures;
695
730
  }
696
731
  /**
732
+ * Gets the features an update gives a feature without coordinates: none, unless its new geometry has some.
733
+ * Its property changes apply either way.
734
+ */
735
+ function getUpdatedUnlocatedFeatures(id, update, options, unlocated) {
736
+ const feature = unlocated.get(id);
737
+ const properties = hasPropertyChanges(update) ? applyPropertyUpdates(feature.properties, update) : feature.properties;
738
+ if (!update.newGeometry) {
739
+ unlocated.set(id, {
740
+ ...feature,
741
+ properties
742
+ });
743
+ return [];
744
+ }
745
+ return convertNewGeometry(id, update.newGeometry, properties, options, unlocated);
746
+ }
747
+ /**
748
+ * Converts the new geometry of a feature to internal format, keeping the id the feature already has
749
+ */
750
+ function convertNewGeometry(id, geometry, properties, options, unlocated) {
751
+ return wrap(convertToInternal({
752
+ type: "FeatureCollection",
753
+ features: [{
754
+ type: "Feature",
755
+ id,
756
+ geometry,
757
+ properties
758
+ }]
759
+ }, {
760
+ ...options,
761
+ promoteId: void 0,
762
+ generateId: false
763
+ }, unlocated), options);
764
+ }
765
+ function hasPropertyChanges(update) {
766
+ return update.removeAllProperties || update.removeProperties?.length > 0 || update.addOrUpdateProperties?.length > 0;
767
+ }
768
+ /**
697
769
  * helper to apply property updates from a diff update object to a properties object
698
770
  */
699
771
  function applyPropertyUpdates(tags, update) {
@@ -715,7 +787,7 @@ function diffToHashed(diff, options) {
715
787
  return {
716
788
  removeAll: diff.removeAll,
717
789
  remove: new Set(diff.remove || []),
718
- add: new Map(diff.add?.map((feature) => [options.promoteId ? feature.properties[options.promoteId] : feature.id, feature])),
790
+ add: new Map(diff.add?.map((feature) => [options.promoteId ? feature.properties?.[options.promoteId] : feature.id, feature])),
719
791
  update: new Map(diff.update?.map((update) => [update.id, update]))
720
792
  };
721
793
  }
@@ -1016,54 +1088,105 @@ const defaultClusterOptions = {
1016
1088
  log: false,
1017
1089
  generateId: false,
1018
1090
  reduce: null,
1019
- map: (props) => props
1091
+ map: (props) => props,
1092
+ coordinates: "lngLat",
1093
+ wrap: true
1020
1094
  };
1021
- const OFFSET_ZOOM = 2;
1022
- const OFFSET_ID = 3;
1023
- const OFFSET_PARENT = 4;
1024
- const OFFSET_NUM = 5;
1025
- const OFFSET_PROP = 6;
1095
+ /**
1096
+ * The highest zoom level to cluster on. A cluster id keeps its zoom level in five bits.
1097
+ */
1098
+ const MAX_ZOOM = 30;
1099
+ /**
1100
+ * Projected coordinates, which are 0 to 1 inside the world, are stored as integers with this many steps per world,
1101
+ * so that they take half the memory of doubles. One step is ~4 cm at the equator.
1102
+ */
1103
+ const SCALE = 1073741824;
1104
+ /**
1105
+ * The largest value a 32-bit integer holds.
1106
+ */
1107
+ const MAX_INTEGER = 2147483647;
1108
+ /**
1109
+ * A projected coordinate in steps from the center of the world, see `SCALE`.
1110
+ */
1111
+ function encode(coord) {
1112
+ return (coord - .5) * SCALE;
1113
+ }
1114
+ function decode(value) {
1115
+ return value / SCALE + .5;
1116
+ }
1117
+ /**
1118
+ * Whether a projected coordinate fits in a 32-bit integer. One that is more than two worlds from the center of the
1119
+ * world does not, and neither does one that is not a number.
1120
+ */
1121
+ function fitsInteger(coord) {
1122
+ return Math.abs(encode(coord)) < MAX_INTEGER;
1123
+ }
1124
+ const OFFSET_ZOOM = 0;
1125
+ const OFFSET_ID = 1;
1126
+ const OFFSET_PARENT = 2;
1127
+ const OFFSET_NUM = 3;
1128
+ const OFFSET_PROP = 4;
1026
1129
  /**
1027
1130
  * This class allow clustering of geojson points.
1028
1131
  */
1029
1132
  var ClusterTileIndex = class {
1030
1133
  constructor(options) {
1031
- this.options = Object.assign(Object.create(defaultClusterOptions), options);
1134
+ this.options = createOptions(options);
1135
+ this.worldCoordinateHelper = getWorldCoordinateHelper(this.options.coordinates);
1032
1136
  this.trees = new Array(this.options.maxZoom + 1);
1033
- this.stride = this.options.reduce ? 7 : 6;
1137
+ this.stride = this.options.reduce ? 5 : 4;
1034
1138
  this.clusterProps = [];
1035
1139
  this.points = [];
1036
1140
  }
1037
1141
  /**
1038
- * Loads GeoJSON point features and builds the internal clustering index.
1039
- * @param points - GeoJSON point features to cluster.
1142
+ * Loads GeoJSON Point and MultiPoint features and builds the internal clustering index.
1143
+ * Each position of a MultiPoint is clustered as a point of its own, with the id and properties of its feature.
1144
+ * @param points - GeoJSON Point and MultiPoint features to cluster.
1040
1145
  */
1041
1146
  load(points) {
1042
1147
  const features = [];
1043
1148
  for (const point of points) {
1044
1149
  if (!point.geometry) continue;
1045
- const [lng, lat] = point.geometry.coordinates;
1046
- const [x, y] = [projectX(lng), projectY(lat)];
1047
- const feature = {
1048
- id: point.id,
1049
- type: "Point",
1050
- geometry: [x, y],
1051
- tags: point.properties
1052
- };
1053
- features.push(feature);
1150
+ const positions = point.geometry.type === "MultiPoint" ? point.geometry.coordinates : [point.geometry.coordinates];
1151
+ for (const [lng, lat] of positions) {
1152
+ const [x, y] = [this.worldCoordinateHelper.projectX(lng), this.worldCoordinateHelper.projectY(lat)];
1153
+ if (!this.options.wrap && !isInWorld([x, y])) continue;
1154
+ const feature = {
1155
+ id: point.id,
1156
+ type: "Point",
1157
+ geometry: [x, y],
1158
+ tags: point.properties
1159
+ };
1160
+ features.push(feature);
1161
+ }
1054
1162
  }
1055
1163
  this.createIndex(features);
1056
1164
  }
1057
1165
  /**
1058
1166
  * @internal
1059
1167
  * Loads internal GeoJSONVT point features from a data source and builds the clustering index.
1168
+ * Each position of a MultiPoint feature becomes a point of its own, with the id and properties of its feature.
1060
1169
  * @param features - {@link GeoJSONVTInternalFeature} data source features to filter and cluster.
1061
1170
  */
1062
1171
  initialize(features) {
1063
1172
  const points = [];
1064
1173
  for (const feature of features) {
1065
- if (feature.type !== "Point") continue;
1066
- points.push(feature);
1174
+ if (feature.type === "Point") {
1175
+ if (!this.options.wrap && !isInWorld(feature.geometry)) continue;
1176
+ points.push(feature);
1177
+ continue;
1178
+ }
1179
+ if (feature.type !== "MultiPoint") continue;
1180
+ for (let i = 0; i < feature.geometry.length; i += 3) {
1181
+ const geometry = [feature.geometry[i], feature.geometry[i + 1]];
1182
+ if (!this.options.wrap && !isInWorld(geometry)) continue;
1183
+ points.push({
1184
+ id: feature.id,
1185
+ type: "Point",
1186
+ geometry,
1187
+ tags: feature.tags
1188
+ });
1189
+ }
1067
1190
  }
1068
1191
  this.createIndex(points);
1069
1192
  }
@@ -1073,41 +1196,113 @@ var ClusterTileIndex = class {
1073
1196
  * @param features
1074
1197
  */
1075
1198
  updateIndex(features, _affected, options) {
1076
- this.options = Object.assign(Object.create(defaultClusterOptions), options.clusterOptions);
1199
+ this.options = createOptions({
1200
+ ...options.clusterOptions,
1201
+ coordinates: this.options.coordinates,
1202
+ wrap: this.options.wrap
1203
+ });
1077
1204
  this.initialize(features);
1078
1205
  }
1206
+ /**
1207
+ * Builds a tree per zoom level from the points. A point whose position does not fit in an integer is left out,
1208
+ * see `fitsInteger`.
1209
+ */
1079
1210
  createIndex(points) {
1080
1211
  const { log, minZoom, maxZoom } = this.options;
1212
+ this.trees = new Array(maxZoom + 1);
1213
+ this.stride = this.options.reduce ? 5 : 4;
1214
+ this.clusterProps = [];
1081
1215
  if (log) console.time("total time");
1082
1216
  const timerId = `prepare ${points.length} points`;
1083
1217
  if (log) console.time(timerId);
1084
1218
  this.points = points;
1085
- const data = [];
1219
+ const stride = this.stride;
1220
+ const notProcessed = maxZoom + 1;
1221
+ const coords = new Int32Array(points.length * 2);
1222
+ const data = new Int32Array(points.length * stride);
1223
+ let numItems = 0;
1086
1224
  for (let i = 0; i < points.length; i++) {
1087
1225
  const p = points[i];
1088
- if (!p?.geometry) continue;
1089
- let [x, y] = p.geometry;
1090
- x = Math.fround(x);
1091
- y = Math.fround(y);
1092
- data.push(x, y, Infinity, i, -1, 1);
1093
- if (this.options.reduce) data.push(0);
1226
+ if (!p?.geometry || !fitsInteger(p.geometry[0]) || !fitsInteger(p.geometry[1])) continue;
1227
+ coords[numItems * 2] = Math.round(encode(p.geometry[0]));
1228
+ coords[numItems * 2 + 1] = Math.round(encode(p.geometry[1]));
1229
+ const k = numItems * stride;
1230
+ data[k + OFFSET_ZOOM] = notProcessed;
1231
+ data[k + OFFSET_ID] = i;
1232
+ data[k + OFFSET_PARENT] = -1;
1233
+ data[k + OFFSET_NUM] = 1;
1234
+ numItems++;
1094
1235
  }
1095
- let tree = this.trees[maxZoom + 1] = this.createTree(data);
1236
+ let tree = this.trees[maxZoom + 1] = this.createTree(coords, data, numItems);
1096
1237
  if (log) console.timeEnd(timerId);
1238
+ const nextCoords = new Int32Array(numItems * 2);
1239
+ const nextData = new Int32Array(numItems * stride);
1097
1240
  for (let z = maxZoom; z >= minZoom; z--) {
1098
1241
  const now = Date.now();
1099
- tree = this.trees[z] = this.createTree(this.cluster(tree, z));
1242
+ const nextNumItems = this.cluster(tree, z, nextCoords, nextData);
1243
+ if (!this.isSameAsTree(tree, nextData, nextNumItems)) tree = this.createTree(nextCoords.slice(0, nextNumItems * 2), nextData.slice(0, nextNumItems * stride), nextNumItems);
1244
+ this.trees[z] = tree;
1100
1245
  if (log) console.log("z%d: %d clusters in %dms", z, tree.numItems, Date.now() - now);
1101
1246
  }
1102
1247
  if (log) console.timeEnd("total time");
1103
1248
  }
1104
1249
  /**
1105
1250
  * 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.
1251
+ * @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order, or `[minX, minY, maxX, maxY]` in the unit square with `coordinates: 'unitSquare'`.
1252
+ * In a world that wraps, the box can reach across the antimeridian. In one that doesn't, only its part inside the world counts.
1107
1253
  * @param zoom - Zoom level to query.
1108
1254
  */
1109
1255
  getClusters(bbox, zoom) {
1110
- return this.getClustersInternal(bbox, zoom).map((f) => featureToGeoJSON(f));
1256
+ 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));
1257
+ }
1258
+ /**
1259
+ * The clusters and points in the part of a box inside a world that ends at its edges.
1260
+ */
1261
+ getClustersInWorld(bbox, zoom) {
1262
+ const lngLat = this.options.coordinates !== "unitSquare";
1263
+ const y1 = this.worldCoordinateHelper.projectY(lngLat ? Math.max(-90, Math.min(90, bbox[1])) : bbox[1]);
1264
+ const y2 = this.worldCoordinateHelper.projectY(lngLat ? Math.max(-90, Math.min(90, bbox[3])) : bbox[3]);
1265
+ const minX = Math.max(0, this.worldCoordinateHelper.projectX(bbox[0]));
1266
+ const maxX = Math.min(1, this.worldCoordinateHelper.projectX(bbox[2]));
1267
+ if (minX > maxX) return [];
1268
+ return this.getClustersInRange([
1269
+ minX,
1270
+ Math.min(y1, y2),
1271
+ maxX,
1272
+ Math.max(y1, y2)
1273
+ ], zoom);
1274
+ }
1275
+ /**
1276
+ * The clusters and points in a box of a unit square that wraps, where the box can reach across its left or right edge.
1277
+ */
1278
+ getClustersAcrossEdge([minX, minY, maxX, maxY], zoom) {
1279
+ if (maxX < minX) maxX += 1;
1280
+ if (maxX - minX >= 1) return this.getClustersInRange([
1281
+ 0,
1282
+ minY,
1283
+ 1,
1284
+ maxY
1285
+ ], zoom);
1286
+ const shift = Math.floor(minX);
1287
+ const west = minX - shift;
1288
+ const east = maxX - shift;
1289
+ if (east <= 1) return this.getClustersInRange([
1290
+ west,
1291
+ minY,
1292
+ east,
1293
+ maxY
1294
+ ], zoom);
1295
+ return this.getClustersInRange([
1296
+ west,
1297
+ minY,
1298
+ 1,
1299
+ maxY
1300
+ ], zoom).concat(this.getClustersInRange([
1301
+ 0,
1302
+ minY,
1303
+ east - 1,
1304
+ maxY
1305
+ ], zoom));
1111
1306
  }
1112
1307
  getClustersInternal(bbox, zoom) {
1113
1308
  let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
@@ -1132,13 +1327,25 @@ var ClusterTileIndex = class {
1132
1327
  ], zoom);
1133
1328
  return easternHem.concat(westernHem);
1134
1329
  }
1330
+ return this.getClustersInRange([
1331
+ this.worldCoordinateHelper.projectX(minLng),
1332
+ this.worldCoordinateHelper.projectY(maxLat),
1333
+ this.worldCoordinateHelper.projectX(maxLng),
1334
+ this.worldCoordinateHelper.projectY(minLat)
1335
+ ], zoom);
1336
+ }
1337
+ /**
1338
+ * The clusters and points in a box of the internal coordinates, `[minX, minY, maxX, maxY]`.
1339
+ */
1340
+ getClustersInRange([minX, minY, maxX, maxY], zoom) {
1135
1341
  const tree = this.trees[this.limitZoom(zoom)];
1136
- const ids = tree.range(projectX(minLng), projectY(maxLat), projectX(maxLng), projectY(minLat));
1342
+ const ids = tree.range(encode(minX), encode(minY), encode(maxX), encode(maxY));
1343
+ const coords = tree.flatCoords;
1137
1344
  const data = tree.flatData;
1138
1345
  const clusters = [];
1139
1346
  for (const id of ids) {
1140
1347
  const k = this.stride * id;
1141
- clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
1348
+ clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(coords, id, data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
1142
1349
  }
1143
1350
  return clusters;
1144
1351
  }
@@ -1152,16 +1359,17 @@ var ClusterTileIndex = class {
1152
1359
  const clusterError = /* @__PURE__ */ new Error("No cluster with the specified id: " + clusterId);
1153
1360
  const tree = this.trees[originZoom];
1154
1361
  if (!tree) throw clusterError;
1362
+ const coords = tree.flatCoords;
1155
1363
  const data = tree.flatData;
1156
- if (originId * this.stride >= data.length) throw clusterError;
1157
- const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
1158
- const x = data[originId * this.stride];
1159
- const y = data[originId * this.stride + 1];
1364
+ if (originId >= tree.numItems) throw clusterError;
1365
+ const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1)) * SCALE;
1366
+ const x = coords[originId * 2];
1367
+ const y = coords[originId * 2 + 1];
1160
1368
  const ids = tree.within(x, y, r);
1161
1369
  const children = [];
1162
1370
  for (const id of ids) {
1163
1371
  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]]));
1372
+ 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));
1165
1373
  }
1166
1374
  if (children.length === 0) throw clusterError;
1167
1375
  return children;
@@ -1189,10 +1397,11 @@ var ClusterTileIndex = class {
1189
1397
  const tree = this.trees[this.limitZoom(z)];
1190
1398
  if (!tree) return null;
1191
1399
  const z2 = Math.pow(2, z);
1400
+ if (!this.options.wrap && (x < 0 || x >= z2 || y < 0 || y >= z2)) return null;
1192
1401
  const { extent, radius } = this.options;
1193
1402
  const p = radius / extent;
1194
- const top = (y - p) / z2;
1195
- const bottom = (y + 1 + p) / z2;
1403
+ const top = encode((y - p) / z2);
1404
+ const bottom = encode((y + 1 + p) / z2);
1196
1405
  const tile = {
1197
1406
  transformed: true,
1198
1407
  features: [],
@@ -1201,9 +1410,9 @@ var ClusterTileIndex = class {
1201
1410
  y,
1202
1411
  z
1203
1412
  };
1204
- this.addTileFeatures(tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), tree.flatData, x, y, z2, tile);
1205
- if (x === 0) this.addTileFeatures(tree.range(1 - p / z2, top, 1, bottom), tree.flatData, z2, y, z2, tile);
1206
- if (x === z2 - 1) this.addTileFeatures(tree.range(0, top, p / z2, bottom), tree.flatData, -1, y, z2, tile);
1413
+ this.addTileFeatures(tree.range(encode((x - p) / z2), top, encode((x + 1 + p) / z2), bottom), tree, x, y, z2, tile);
1414
+ if (this.options.wrap && x === 0) this.addTileFeatures(tree.range(encode(1 - p / z2), top, encode(1), bottom), tree, z2, y, z2, tile);
1415
+ if (this.options.wrap && x === z2 - 1) this.addTileFeatures(tree.range(encode(0), top, encode(p / z2), bottom), tree, -1, y, z2, tile);
1207
1416
  return tile;
1208
1417
  }
1209
1418
  /**
@@ -1226,15 +1435,27 @@ var ClusterTileIndex = class {
1226
1435
  }
1227
1436
  return skipped;
1228
1437
  }
1229
- createTree(data) {
1230
- const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array);
1231
- for (let i = 0; i < data.length; i += this.stride) tree.add(data[i], data[i + 1]);
1438
+ createTree(coords, data, numItems) {
1439
+ const tree = new KDBush(numItems, this.options.nodeSize, Int32Array);
1440
+ for (let i = 0; i < numItems; i++) tree.add(coords[i * 2], coords[i * 2 + 1]);
1232
1441
  tree.finish();
1442
+ tree.flatCoords = coords;
1233
1443
  tree.flatData = data;
1234
1444
  tree.data = null;
1235
1445
  return tree;
1236
1446
  }
1237
- addTileFeatures(ids, data, x, y, z2, tile) {
1447
+ /**
1448
+ * Whether clustering a tree left all its points and clusters as they were, in the same order.
1449
+ */
1450
+ isSameAsTree(tree, nextData, nextNumItems) {
1451
+ if (nextNumItems !== tree.numItems) return false;
1452
+ const data = tree.flatData;
1453
+ for (let k = OFFSET_ID; k < nextNumItems * this.stride; k += this.stride) if (nextData[k] !== data[k]) return false;
1454
+ return true;
1455
+ }
1456
+ addTileFeatures(ids, tree, x, y, z2, tile) {
1457
+ const coords = tree.flatCoords;
1458
+ const data = tree.flatData;
1238
1459
  for (const i of ids) {
1239
1460
  const k = i * this.stride;
1240
1461
  const isCluster = data[k + OFFSET_NUM] > 1;
@@ -1243,8 +1464,8 @@ var ClusterTileIndex = class {
1243
1464
  let py;
1244
1465
  if (isCluster) {
1245
1466
  tags = getClusterProperties(data, k, this.clusterProps);
1246
- px = data[k];
1247
- py = data[k + 1];
1467
+ px = decode(coords[i * 2]);
1468
+ py = decode(coords[i * 2 + 1]);
1248
1469
  } else {
1249
1470
  const p = this.points[data[k + OFFSET_ID]];
1250
1471
  tags = p.tags;
@@ -1265,22 +1486,30 @@ var ClusterTileIndex = class {
1265
1486
  limitZoom(z) {
1266
1487
  return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
1267
1488
  }
1268
- cluster(tree, zoom) {
1269
- const { radius, extent, reduce, minPoints } = this.options;
1270
- const r = radius / (extent * Math.pow(2, zoom));
1489
+ /**
1490
+ * Clusters the points of a tree on a zoom level, writing the result to `nextCoords` and `nextData`.
1491
+ * @returns the number of points and clusters written
1492
+ */
1493
+ cluster(tree, zoom, nextCoords, nextData) {
1494
+ const { radius, extent, reduce, minPoints, maxZoom } = this.options;
1495
+ const r = radius / (extent * Math.pow(2, zoom)) * SCALE;
1496
+ const notProcessed = maxZoom + 1;
1497
+ const coords = tree.flatCoords;
1271
1498
  const data = tree.flatData;
1272
- const nextData = [];
1273
1499
  const stride = this.stride;
1274
- for (let i = 0; i < data.length; i += stride) {
1500
+ const neighborIds = new Uint32Array(tree.numItems);
1501
+ let nextNumItems = 0;
1502
+ for (let index = 0; index < tree.numItems; index++) {
1503
+ const i = index * stride;
1275
1504
  if (data[i + OFFSET_ZOOM] <= zoom) continue;
1276
1505
  data[i + OFFSET_ZOOM] = zoom;
1277
- const x = data[i];
1278
- const y = data[i + 1];
1279
- const neighborIds = tree.within(data[i], data[i + 1], r);
1506
+ const x = coords[index * 2];
1507
+ const y = coords[index * 2 + 1];
1508
+ const numNeighbors = tree.withinInto(x, y, r, neighborIds);
1280
1509
  const numPointsOrigin = data[i + OFFSET_NUM];
1281
1510
  let numPoints = numPointsOrigin;
1282
- for (const neighborId of neighborIds) {
1283
- const k = neighborId * stride;
1511
+ for (let n = 0; n < numNeighbors; n++) {
1512
+ const k = neighborIds[n] * stride;
1284
1513
  if (data[k + OFFSET_ZOOM] > zoom) numPoints += data[k + OFFSET_NUM];
1285
1514
  }
1286
1515
  if (numPoints > numPointsOrigin && numPoints >= minPoints) {
@@ -1288,14 +1517,15 @@ var ClusterTileIndex = class {
1288
1517
  let wy = y * numPointsOrigin;
1289
1518
  let clusterProperties;
1290
1519
  let clusterPropIndex = -1;
1291
- const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length;
1292
- for (const neighborId of neighborIds) {
1293
- const k = neighborId * stride;
1520
+ const id = (index << 5) + (zoom + 1) + this.points.length;
1521
+ for (let n = 0; n < numNeighbors; n++) {
1522
+ const neighborIndex = neighborIds[n];
1523
+ const k = neighborIndex * stride;
1294
1524
  if (data[k + OFFSET_ZOOM] <= zoom) continue;
1295
1525
  data[k + OFFSET_ZOOM] = zoom;
1296
1526
  const numPoints2 = data[k + OFFSET_NUM];
1297
- wx += data[k] * numPoints2;
1298
- wy += data[k + 1] * numPoints2;
1527
+ wx += coords[neighborIndex * 2] * numPoints2;
1528
+ wy += coords[neighborIndex * 2 + 1] * numPoints2;
1299
1529
  data[k + OFFSET_PARENT] = id;
1300
1530
  if (reduce) {
1301
1531
  if (!clusterProperties) {
@@ -1307,19 +1537,33 @@ var ClusterTileIndex = class {
1307
1537
  }
1308
1538
  }
1309
1539
  data[i + OFFSET_PARENT] = id;
1310
- nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints);
1311
- if (reduce) nextData.push(clusterPropIndex);
1540
+ nextCoords[nextNumItems * 2] = Math.round(wx / numPoints);
1541
+ nextCoords[nextNumItems * 2 + 1] = Math.round(wy / numPoints);
1542
+ const next = nextNumItems * stride;
1543
+ nextData[next + OFFSET_ZOOM] = notProcessed;
1544
+ nextData[next + OFFSET_ID] = id;
1545
+ nextData[next + OFFSET_PARENT] = -1;
1546
+ nextData[next + OFFSET_NUM] = numPoints;
1547
+ if (reduce) nextData[next + OFFSET_PROP] = clusterPropIndex;
1548
+ nextNumItems++;
1312
1549
  } else {
1313
- for (let j = 0; j < stride; j++) nextData.push(data[i + j]);
1314
- if (numPoints > 1) for (const neighborId of neighborIds) {
1315
- const k = neighborId * stride;
1550
+ nextCoords[nextNumItems * 2] = x;
1551
+ nextCoords[nextNumItems * 2 + 1] = y;
1552
+ for (let j = 0; j < stride; j++) nextData[nextNumItems * stride + j] = data[i + j];
1553
+ nextNumItems++;
1554
+ if (numPoints > 1) for (let n = 0; n < numNeighbors; n++) {
1555
+ const neighborIndex = neighborIds[n];
1556
+ const k = neighborIndex * stride;
1316
1557
  if (data[k + OFFSET_ZOOM] <= zoom) continue;
1317
1558
  data[k + OFFSET_ZOOM] = zoom;
1318
- for (let j = 0; j < stride; j++) nextData.push(data[k + j]);
1559
+ nextCoords[nextNumItems * 2] = coords[neighborIndex * 2];
1560
+ nextCoords[nextNumItems * 2 + 1] = coords[neighborIndex * 2 + 1];
1561
+ for (let j = 0; j < stride; j++) nextData[nextNumItems * stride + j] = data[k + j];
1562
+ nextNumItems++;
1319
1563
  }
1320
1564
  }
1321
1565
  }
1322
- return nextData;
1566
+ return nextNumItems;
1323
1567
  }
1324
1568
  getOriginId(clusterId) {
1325
1569
  return clusterId - this.points.length >> 5;
@@ -1337,22 +1581,28 @@ var ClusterTileIndex = class {
1337
1581
  return clone && result === original ? Object.assign({}, result) : result;
1338
1582
  }
1339
1583
  };
1340
- function getClusterFeature(data, i, clusterProps) {
1584
+ function createOptions(options) {
1585
+ const result = Object.assign(Object.create(defaultClusterOptions), options);
1586
+ result.minZoom = Math.min(result.minZoom, MAX_ZOOM);
1587
+ result.maxZoom = Math.min(result.maxZoom, MAX_ZOOM);
1588
+ return result;
1589
+ }
1590
+ function getClusterFeature(coords, index, data, i, clusterProps) {
1341
1591
  return {
1342
1592
  id: data[i + OFFSET_ID],
1343
1593
  type: "Point",
1344
1594
  tags: getClusterProperties(data, i, clusterProps),
1345
- geometry: [data[i], data[i + 1]]
1595
+ geometry: [decode(coords[index * 2]), decode(coords[index * 2 + 1])]
1346
1596
  };
1347
1597
  }
1348
- function getClusterGeoJSON(data, i, clusterProps) {
1598
+ function getClusterGeoJSON(coords, index, data, i, clusterProps, worldCoordinateHelper) {
1349
1599
  return {
1350
1600
  type: "Feature",
1351
1601
  id: data[i + OFFSET_ID],
1352
1602
  properties: getClusterProperties(data, i, clusterProps),
1353
1603
  geometry: {
1354
1604
  type: "Point",
1355
- coordinates: [unprojectX(data[i]), unprojectY(data[i + 1])]
1605
+ coordinates: [worldCoordinateHelper.unprojectX(decode(coords[index * 2])), worldCoordinateHelper.unprojectY(decode(coords[index * 2 + 1]))]
1356
1606
  }
1357
1607
  };
1358
1608
  }
@@ -1368,6 +1618,9 @@ function getClusterProperties(data, i, clusterProps) {
1368
1618
  point_count_abbreviated: abbrev
1369
1619
  });
1370
1620
  }
1621
+ function isInWorld(geometry) {
1622
+ return geometry[0] >= 0 && geometry[0] <= 1 && geometry[1] >= 0 && geometry[1] <= 1;
1623
+ }
1371
1624
  //#endregion
1372
1625
  //#region src/tile.ts
1373
1626
  const GEOJSONVT_CLIP_START = "geojsonvt_clip_start";
@@ -1576,7 +1829,7 @@ var TileIndex = class {
1576
1829
  this.total = 0;
1577
1830
  }
1578
1831
  initialize(features) {
1579
- this.splitTile(features, 0, 0, 0);
1832
+ this.splitTile(this.clipToWorld(features), 0, 0, 0);
1580
1833
  if (this.options.debug) {
1581
1834
  if (features.length) console.log("features: %d, points: %d", this.tiles[0].numFeatures, this.tiles[0].numPoints);
1582
1835
  console.timeEnd("generate tiles");
@@ -1596,8 +1849,9 @@ var TileIndex = class {
1596
1849
  0,
1597
1850
  0
1598
1851
  ];
1599
- const rootTile = createTile(source, z, x, y, options);
1600
- rootTile.source = source;
1852
+ const rootSource = this.clipToWorld(source);
1853
+ const rootTile = createTile(rootSource, z, x, y, options);
1854
+ rootTile.source = rootSource;
1601
1855
  const id = toID(z, x, y);
1602
1856
  this.tiles[id] = rootTile;
1603
1857
  this.tileCoords.push({
@@ -1628,6 +1882,7 @@ var TileIndex = class {
1628
1882
  getTile(z, x, y) {
1629
1883
  const { extent, debug } = this.options;
1630
1884
  const z2 = 1 << z;
1885
+ if (!this.options.wrap && (x < 0 || x >= z2)) return null;
1631
1886
  x = x + z2 & z2 - 1;
1632
1887
  const id = toID(z, x, y);
1633
1888
  if (this.tiles[id]) return transformTile(this.tiles[id], extent);
@@ -1653,6 +1908,18 @@ var TileIndex = class {
1653
1908
  return transformTile(this.tiles[id], extent);
1654
1909
  }
1655
1910
  /**
1911
+ * The features the top tile starts from. A world that wraps keeps them all left and right, where wrapping already
1912
+ * put them inside, and one that doesn't only what lies inside the world square and its buffer. Positions in the
1913
+ * unit square are also cut above and below in a world that wraps, where lng/lat is clamped already. The index keeps
1914
+ * the rest, so an update can still move a feature in from outside.
1915
+ */
1916
+ clipToWorld(features) {
1917
+ const { wrap, coordinates } = this.options;
1918
+ if (wrap && coordinates !== "unitSquare") return features;
1919
+ const buffer = this.options.buffer / this.options.extent;
1920
+ return clip(wrap ? features : clip(features, 1, -buffer, 1 + buffer, 0, -Infinity, Infinity, this.options) || [], 1, -buffer, 1 + buffer, 1, -Infinity, Infinity, this.options) || [];
1921
+ }
1922
+ /**
1656
1923
  * splits features from a parent tile to sub-tiles.
1657
1924
  * z, x, and y are the coordinates of the parent tile
1658
1925
  * cz, cx, and cy are the coordinates of the target tile
@@ -1805,7 +2072,9 @@ const defaultOptions = {
1805
2072
  updateable: false,
1806
2073
  cluster: false,
1807
2074
  clusterOptions: defaultClusterOptions,
1808
- debug: 0
2075
+ debug: 0,
2076
+ coordinates: "lngLat",
2077
+ wrap: true
1809
2078
  };
1810
2079
  /**
1811
2080
  * Main class for creating and managing a vector tile index from GeoJSON data.
@@ -1813,11 +2082,13 @@ const defaultOptions = {
1813
2082
  var GeoJSONVT = class {
1814
2083
  constructor(data, options) {
1815
2084
  options = this.options = Object.assign({}, defaultOptions, options);
2085
+ this.worldCoordinateHelper = getWorldCoordinateHelper(options.coordinates);
1816
2086
  const debug = options.debug;
1817
2087
  if (debug) console.time("preprocess data");
1818
2088
  if (options.maxZoom < 0 || options.maxZoom > 24) throw new Error("maxZoom should be in the 0-24 range");
1819
2089
  if (options.promoteId && options.generateId) throw new Error("promoteId and generateId cannot be used together.");
1820
- let features = convertToInternal(data, options);
2090
+ if (options.updateable) this.unlocated = /* @__PURE__ */ new Map();
2091
+ let features = convertToInternal(data, options, this.unlocated);
1821
2092
  if (debug) {
1822
2093
  console.timeEnd("preprocess data");
1823
2094
  console.log("index: maxZoom: %d, maxPoints: %d", options.indexMaxZoom, options.indexMaxPoints);
@@ -1828,7 +2099,11 @@ var GeoJSONVT = class {
1828
2099
  this.initializeIndex(features, options);
1829
2100
  }
1830
2101
  initializeIndex(features, options) {
1831
- this.tileIndex = options.cluster ? new ClusterTileIndex(options.clusterOptions) : new TileIndex(options);
2102
+ this.tileIndex = options.cluster ? new ClusterTileIndex({
2103
+ ...options.clusterOptions,
2104
+ coordinates: options.coordinates,
2105
+ wrap: options.wrap
2106
+ }) : new TileIndex(options);
1832
2107
  if (!features.length) return;
1833
2108
  this.tileIndex.initialize(features);
1834
2109
  }
@@ -1853,24 +2128,27 @@ var GeoJSONVT = class {
1853
2128
  updateData(diff, filter) {
1854
2129
  const options = this.options;
1855
2130
  if (!options.updateable) throw new Error("to update tile geojson `updateable` option must be set to true");
1856
- let { affected, source } = applySourceDiff(this.source, diff, options);
2131
+ let { affected, source } = applySourceDiff(this.source, diff, options, this.unlocated);
1857
2132
  if (filter) ({affected, source} = this.filterUpdate(source, affected, filter));
1858
2133
  if (!affected.length) return;
1859
2134
  this.source = source;
1860
2135
  this.tileIndex.updateIndex(source, affected, options);
1861
2136
  }
1862
2137
  /**
1863
- * Filter an update using a predicate function. Returns the affected and updated source features.
2138
+ * Filter an update using a predicate function, the features an update emptied included, so that one the filter
2139
+ * rejects can't come back with a later update. A feature without a geometry at all isn't passed to the predicate.
2140
+ * Returns the affected and updated source features.
1864
2141
  */
1865
2142
  filterUpdate(source, affected, predicate) {
1866
2143
  const removeIds = /* @__PURE__ */ new Set();
1867
2144
  for (const feature of source) {
1868
2145
  if (feature.id == void 0) continue;
1869
- if (predicate(featureToGeoJSON(feature))) continue;
2146
+ if (predicate(featureToGeoJSON(feature, this.worldCoordinateHelper))) continue;
1870
2147
  affected.push(feature);
1871
2148
  removeIds.add(feature.id);
1872
2149
  }
1873
2150
  source = source.filter((feature) => !removeIds.has(feature.id));
2151
+ for (const [id, feature] of this.unlocated ?? []) if (feature.geometry && !predicate(feature)) this.unlocated.delete(id);
1874
2152
  return {
1875
2153
  affected,
1876
2154
  source
@@ -1881,12 +2159,12 @@ var GeoJSONVT = class {
1881
2159
  */
1882
2160
  getData() {
1883
2161
  if (!this.options.updateable) throw new Error("to retrieve data the `updateable` option must be set to true");
1884
- return convertToGeoJSON(this.source);
2162
+ return convertToGeoJSON(this.source, this.worldCoordinateHelper);
1885
2163
  }
1886
2164
  /**
1887
2165
  * Update supercluster options and regenerate the index.
1888
2166
  * @param cluster - whether to enable clustering
1889
- * @param clusterOptions - {@link SuperclusterOptions}
2167
+ * @param clusterOptions - {@link SuperclusterOptions} without `coordinates` and `wrap`, which come from the index options
1890
2168
  */
1891
2169
  updateClusterOptions(cluster, clusterOptions) {
1892
2170
  const wasCluster = this.options.cluster;
@@ -1962,9 +2240,10 @@ var GeoJSONVT = class {
1962
2240
  function geoJSONToTile(data, z, x, y, options = {}) {
1963
2241
  options = {
1964
2242
  ...defaultOptions,
2243
+ wrap: false,
1965
2244
  ...options
1966
2245
  };
1967
- const { wrap: shouldWrap = false, clip: shouldClip = false } = options;
2246
+ const { wrap: shouldWrap, clip: shouldClip = false } = options;
1968
2247
  let features = convertToInternal(data, options);
1969
2248
  if (shouldWrap) features = wrap(features, options);
1970
2249
  if (shouldClip || options.lineMetrics) {