@maplibre/geojson-vt 6.2.0 → 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.
@@ -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,25 +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);
165
- const worldCoordinates = options.worldCoordinates === true;
222
+ const worldCoordinateHelper = getWorldCoordinateHelper(options.coordinates);
166
223
  switch (geojson.geometry.type) {
167
224
  case "Point":
168
- convertPointFeature(features, id, geojson.geometry, worldCoordinates, geojson.properties);
225
+ convertPointFeature(features, id, geojson.geometry, worldCoordinateHelper, geojson.properties);
169
226
  return;
170
227
  case "MultiPoint":
171
- convertMultiPointFeature(features, id, geojson.geometry, worldCoordinates, geojson.properties);
228
+ convertMultiPointFeature(features, id, geojson.geometry, worldCoordinateHelper, geojson.properties);
172
229
  return;
173
230
  case "LineString":
174
- convertLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
231
+ convertLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
175
232
  return;
176
233
  case "MultiLineString":
177
- convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinates, options, geojson.properties);
234
+ convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, options, geojson.properties);
178
235
  return;
179
236
  case "Polygon":
180
- convertPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
237
+ convertPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
181
238
  return;
182
239
  case "MultiPolygon":
183
- convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
240
+ convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
184
241
  return;
185
242
  default: throw new Error("Input data is not a valid GeoJSON object.");
186
243
  }
@@ -198,55 +255,55 @@ function convertGeometryCollection(features, geojson, geometry, options, index,
198
255
  properties: geojson.properties
199
256
  }, options, index, depth);
200
257
  }
201
- function convertPointFeature(features, id, geom, worldCoordinates, properties) {
258
+ function convertPointFeature(features, id, geom, worldCoordinateHelper, properties) {
202
259
  const out = [];
203
- out.push(projectX(geom.coordinates[0], worldCoordinates), projectY(geom.coordinates[1], worldCoordinates), 0);
260
+ out.push(worldCoordinateHelper.projectX(geom.coordinates[0]), worldCoordinateHelper.projectY(geom.coordinates[1]), 0);
204
261
  features.push(createFeature(id, "Point", out, properties));
205
262
  }
206
- function convertMultiPointFeature(features, id, geom, worldCoordinates, properties) {
263
+ function convertMultiPointFeature(features, id, geom, worldCoordinateHelper, properties) {
207
264
  const out = [];
208
- for (const coords of geom.coordinates) out.push(projectX(coords[0], worldCoordinates), projectY(coords[1], worldCoordinates), 0);
265
+ for (const coords of geom.coordinates) out.push(worldCoordinateHelper.projectX(coords[0]), worldCoordinateHelper.projectY(coords[1]), 0);
209
266
  features.push(createFeature(id, "MultiPoint", out, properties));
210
267
  }
211
- function convertLineStringFeature(features, id, geom, tolerance, worldCoordinates, properties) {
268
+ function convertLineStringFeature(features, id, geom, tolerance, worldCoordinateHelper, properties) {
212
269
  const out = { points: [] };
213
- convertLine(geom.coordinates, out, tolerance, false, worldCoordinates);
270
+ convertLine(geom.coordinates, out, tolerance, false, worldCoordinateHelper);
214
271
  features.push(createFeature(id, "LineString", out, properties));
215
272
  }
216
- function convertMultiLineStringFeature(features, id, geom, tolerance, worldCoordinates, options, properties) {
273
+ function convertMultiLineStringFeature(features, id, geom, tolerance, worldCoordinateHelper, options, properties) {
217
274
  if (options.lineMetrics) for (const line of geom.coordinates) {
218
275
  const out = { points: [] };
219
- convertLine(line, out, tolerance, false, worldCoordinates);
276
+ convertLine(line, out, tolerance, false, worldCoordinateHelper);
220
277
  features.push(createFeature(id, "LineString", out, properties));
221
278
  }
222
279
  else {
223
280
  const out = [];
224
- convertLines(geom.coordinates, out, tolerance, false, worldCoordinates);
281
+ convertLines(geom.coordinates, out, tolerance, false, worldCoordinateHelper);
225
282
  features.push(createFeature(id, "MultiLineString", out, properties));
226
283
  }
227
284
  }
228
- function convertPolygonFeature(features, id, geom, tolerance, worldCoordinates, properties) {
285
+ function convertPolygonFeature(features, id, geom, tolerance, worldCoordinateHelper, properties) {
229
286
  const out = [];
230
- convertLines(geom.coordinates, out, tolerance, true, worldCoordinates);
287
+ convertLines(geom.coordinates, out, tolerance, true, worldCoordinateHelper);
231
288
  features.push(createFeature(id, "Polygon", out, properties));
232
289
  }
233
- function convertMultiPolygonFeature(features, id, geom, tolerance, worldCoordinates, properties) {
290
+ function convertMultiPolygonFeature(features, id, geom, tolerance, worldCoordinateHelper, properties) {
234
291
  const out = [];
235
292
  for (const polygon of geom.coordinates) {
236
293
  const polygonOut = [];
237
- convertLines(polygon, polygonOut, tolerance, true, worldCoordinates);
294
+ convertLines(polygon, polygonOut, tolerance, true, worldCoordinateHelper);
238
295
  if (!polygonOut.length) continue;
239
296
  out.push(polygonOut);
240
297
  }
241
298
  if (!out.length) return;
242
299
  features.push(createFeature(id, "MultiPolygon", out, properties));
243
300
  }
244
- function convertLine(ring, out, tolerance, isPolygon, worldCoordinates) {
301
+ function convertLine(ring, out, tolerance, isPolygon, worldCoordinateHelper) {
245
302
  let x0, y0;
246
303
  let size = 0;
247
304
  for (let j = 0; j < ring.length; j++) {
248
- const x = projectX(ring[j][0], worldCoordinates);
249
- const y = projectY(ring[j][1], worldCoordinates);
305
+ const x = worldCoordinateHelper.projectX(ring[j][0]);
306
+ const y = worldCoordinateHelper.projectY(ring[j][1]);
250
307
  out.points.push(x, y, 0);
251
308
  if (j > 0) {
252
309
  if (isPolygon) size += (x0 * y - x * y0) / 2;
@@ -264,47 +321,31 @@ function convertLine(ring, out, tolerance, isPolygon, worldCoordinates) {
264
321
  out.start = 0;
265
322
  out.end = out.size;
266
323
  }
267
- function convertLines(rings, out, tolerance, isPolygon, worldCoordinates) {
324
+ function convertLines(rings, out, tolerance, isPolygon, worldCoordinateHelper) {
268
325
  for (let i = 0; i < rings.length; i++) {
269
326
  const geom = { points: [] };
270
- convertLine(rings[i], geom, tolerance, isPolygon, worldCoordinates);
327
+ convertLine(rings[i], geom, tolerance, isPolygon, worldCoordinateHelper);
271
328
  out.push(geom);
272
329
  }
273
330
  }
274
- /**
275
- * Convert longitude to spherical mercator in [0..1] range, or keep a world coordinate as it is
276
- */
277
- function projectX(x, worldCoordinates = false) {
278
- if (worldCoordinates) return x;
279
- return x / 360 + .5;
280
- }
281
- /**
282
- * Convert latitude to spherical mercator in [0..1] range, or keep a world coordinate as it is
283
- */
284
- function projectY(y, worldCoordinates = false) {
285
- if (worldCoordinates) return y;
286
- const sin = Math.sin(y * Math.PI / 180);
287
- const y2 = .5 - .25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
288
- return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
289
- }
290
331
  //#endregion
291
332
  //#region src/deconvert.ts
292
333
  /**
293
334
  * Converts internal source features back to GeoJSON format.
294
335
  */
295
- function convertToGeoJSON(source, worldCoordinates = false) {
336
+ function convertToGeoJSON(source, worldCoordinateHelper = mercatorWorldCoordinateHelper) {
296
337
  return {
297
338
  type: "FeatureCollection",
298
- features: source.map((feature) => featureToGeoJSON(feature, worldCoordinates))
339
+ features: source.map((feature) => featureToGeoJSON(feature, worldCoordinateHelper))
299
340
  };
300
341
  }
301
342
  /**
302
343
  * Converts a single internal feature to GeoJSON format.
303
344
  */
304
- function featureToGeoJSON(feature, worldCoordinates = false) {
345
+ function featureToGeoJSON(feature, worldCoordinateHelper = mercatorWorldCoordinateHelper) {
305
346
  const geojsonFeature = {
306
347
  type: "Feature",
307
- geometry: geometryToGeoJSON(feature, worldCoordinates),
348
+ geometry: geometryToGeoJSON(feature, worldCoordinateHelper),
308
349
  properties: feature.tags
309
350
  };
310
351
  if (feature.id != null) geojsonFeature.id = feature.id;
@@ -313,54 +354,39 @@ function featureToGeoJSON(feature, worldCoordinates = false) {
313
354
  /**
314
355
  * Converts a single internal feature geometry to GeoJSON format.
315
356
  */
316
- function geometryToGeoJSON(feature, worldCoordinates) {
357
+ function geometryToGeoJSON(feature, worldCoordinateHelper) {
317
358
  const { type, geometry } = feature;
318
359
  switch (type) {
319
360
  case "Point": return {
320
361
  type,
321
- coordinates: unprojectPoint(geometry[0], geometry[1], worldCoordinates)
362
+ coordinates: unprojectPoint(geometry[0], geometry[1], worldCoordinateHelper)
322
363
  };
323
364
  case "MultiPoint": return {
324
365
  type,
325
- coordinates: unprojectPoints(geometry, worldCoordinates)
366
+ coordinates: unprojectPoints(geometry, worldCoordinateHelper)
326
367
  };
327
368
  case "LineString": return {
328
369
  type,
329
- coordinates: unprojectPoints(geometry.points, worldCoordinates)
370
+ coordinates: unprojectPoints(geometry.points, worldCoordinateHelper)
330
371
  };
331
372
  case "MultiLineString":
332
373
  case "Polygon": return {
333
374
  type,
334
- coordinates: geometry.map((ring) => unprojectPoints(ring.points, worldCoordinates))
375
+ coordinates: geometry.map((ring) => unprojectPoints(ring.points, worldCoordinateHelper))
335
376
  };
336
377
  case "MultiPolygon": return {
337
378
  type,
338
- coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points, worldCoordinates)))
379
+ coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points, worldCoordinateHelper)))
339
380
  };
340
381
  }
341
382
  }
342
- function unprojectPoints(coords, worldCoordinates = false) {
383
+ function unprojectPoints(coords, worldCoordinateHelper = mercatorWorldCoordinateHelper) {
343
384
  const result = [];
344
- for (let i = 0; i < coords.length; i += 3) result.push(unprojectPoint(coords[i], coords[i + 1], worldCoordinates));
385
+ for (let i = 0; i < coords.length; i += 3) result.push(unprojectPoint(coords[i], coords[i + 1], worldCoordinateHelper));
345
386
  return result;
346
387
  }
347
- function unprojectPoint(x, y, worldCoordinates) {
348
- return [unprojectX(x, worldCoordinates), unprojectY(y, worldCoordinates)];
349
- }
350
- /**
351
- * Convert spherical mercator in [0..1] range to longitude, or keep a world coordinate as it is
352
- */
353
- function unprojectX(x, worldCoordinates = false) {
354
- if (worldCoordinates) return x;
355
- return (x - .5) * 360;
356
- }
357
- /**
358
- * Convert spherical mercator in [0..1] range to latitude, or keep a world coordinate as it is
359
- */
360
- function unprojectY(y, worldCoordinates = false) {
361
- if (worldCoordinates) return y;
362
- const y2 = (180 - y * 360) * Math.PI / 180;
363
- return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
388
+ function unprojectPoint(x, y, worldCoordinateHelper) {
389
+ return [worldCoordinateHelper.unprojectX(x), worldCoordinateHelper.unprojectY(y)];
364
390
  }
365
391
  //#endregion
366
392
  //#region src/clip.ts
@@ -550,7 +576,7 @@ function intersectY(out, ax, ay, bx, by, y) {
550
576
  //#endregion
551
577
  //#region src/wrap.ts
552
578
  function wrap(features, options) {
553
- if (options.worldCoordinates) return features;
579
+ if (!options.wrap) return features;
554
580
  const buffer = options.buffer / options.extent;
555
581
  let merged = features;
556
582
  const left = clip(features, 1, -1 - buffer, buffer, 0, -1, 2, options);
@@ -620,16 +646,19 @@ function shiftLineCoords(line, offset) {
620
646
  * @param source
621
647
  * @param dataDiff
622
648
  * @param options
649
+ * @param unlocated - the features without coordinates, by id, so that an update can give them some
623
650
  * @returns
624
651
  */
625
- function applySourceDiff(source, dataDiff, options) {
652
+ function applySourceDiff(source, dataDiff, options, unlocated) {
626
653
  const diff = diffToHashed(dataDiff, options);
627
654
  let affected = [];
628
655
  if (diff.removeAll) {
629
656
  affected = source;
630
657
  source = [];
658
+ unlocated?.clear();
631
659
  }
632
660
  if (diff.remove.size || diff.add.size) {
661
+ for (const id of diff.remove) unlocated?.delete(id);
633
662
  const removeFeatures = [];
634
663
  for (const feature of source) if (diff.remove.has(feature.id) || diff.add.has(feature.id)) removeFeatures.push(feature);
635
664
  if (removeFeatures.length) {
@@ -641,7 +670,7 @@ function applySourceDiff(source, dataDiff, options) {
641
670
  let addFeatures = convertToInternal({
642
671
  type: "FeatureCollection",
643
672
  features: Array.from(diff.add.values())
644
- }, options);
673
+ }, options, unlocated);
645
674
  addFeatures = wrap(addFeatures, options);
646
675
  affected = affected.concat(addFeatures);
647
676
  source = source.concat(addFeatures);
@@ -654,8 +683,14 @@ function applySourceDiff(source, dataDiff, options) {
654
683
  else keepFeatures.push(feature);
655
684
  for (const [id, update] of diff.update) {
656
685
  const oldFeatures = oldFeaturesMap.get(id);
657
- if (!oldFeatures || oldFeatures.length === 0) continue;
658
- 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);
659
694
  affected = affected.concat(oldFeatures, updatedFeatures);
660
695
  keepFeatures = keepFeatures.concat(updatedFeatures);
661
696
  }
@@ -671,28 +706,16 @@ function applySourceDiff(source, dataDiff, options) {
671
706
  * @param vtFeatures - the original features
672
707
  * @param update - the update object to apply
673
708
  * @param options - the options to use for the wrap method
709
+ * @param unlocated - the features without coordinates, by id, which a new geometry without coordinates adds the feature to
674
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.
675
711
  */
676
- function getUpdatedFeatures(vtFeatures, update, options) {
712
+ function getUpdatedFeatures(vtFeatures, update, options, unlocated) {
677
713
  const changeGeometry = !!update.newGeometry;
678
- const changeProps = update.removeAllProperties || update.removeProperties?.length > 0 || update.addOrUpdateProperties?.length > 0;
714
+ const changeProps = hasPropertyChanges(update);
679
715
  if (changeGeometry) {
680
716
  const vtFeature = vtFeatures[0];
681
- let features = convertToInternal({
682
- type: "FeatureCollection",
683
- features: [{
684
- type: "Feature",
685
- id: vtFeature.id,
686
- geometry: update.newGeometry,
687
- properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
688
- }]
689
- }, {
690
- ...options,
691
- promoteId: void 0,
692
- generateId: false
693
- });
694
- features = wrap(features, options);
695
- return features;
717
+ const properties = changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags;
718
+ return convertNewGeometry(vtFeature.id, update.newGeometry, properties, options, unlocated);
696
719
  }
697
720
  if (changeProps) {
698
721
  const updated = [];
@@ -706,6 +729,43 @@ function getUpdatedFeatures(vtFeatures, update, options) {
706
729
  return vtFeatures;
707
730
  }
708
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
+ /**
709
769
  * helper to apply property updates from a diff update object to a properties object
710
770
  */
711
771
  function applyPropertyUpdates(tags, update) {
@@ -1028,7 +1088,9 @@ const defaultClusterOptions = {
1028
1088
  log: false,
1029
1089
  generateId: false,
1030
1090
  reduce: null,
1031
- map: (props) => props
1091
+ map: (props) => props,
1092
+ coordinates: "lngLat",
1093
+ wrap: true
1032
1094
  };
1033
1095
  /**
1034
1096
  * The highest zoom level to cluster on. A cluster id keeps its zoom level in five bits.
@@ -1068,9 +1130,9 @@ const OFFSET_PROP = 4;
1068
1130
  * This class allow clustering of geojson points.
1069
1131
  */
1070
1132
  var ClusterTileIndex = class {
1071
- constructor(options, worldCoordinates = false) {
1133
+ constructor(options) {
1072
1134
  this.options = createOptions(options);
1073
- this.worldCoordinates = worldCoordinates;
1135
+ this.worldCoordinateHelper = getWorldCoordinateHelper(this.options.coordinates);
1074
1136
  this.trees = new Array(this.options.maxZoom + 1);
1075
1137
  this.stride = this.options.reduce ? 5 : 4;
1076
1138
  this.clusterProps = [];
@@ -1087,8 +1149,8 @@ var ClusterTileIndex = class {
1087
1149
  if (!point.geometry) continue;
1088
1150
  const positions = point.geometry.type === "MultiPoint" ? point.geometry.coordinates : [point.geometry.coordinates];
1089
1151
  for (const [lng, lat] of positions) {
1090
- const [x, y] = [projectX(lng, this.worldCoordinates), projectY(lat, this.worldCoordinates)];
1091
- if (this.worldCoordinates && !isInWorld([x, y])) continue;
1152
+ const [x, y] = [this.worldCoordinateHelper.projectX(lng), this.worldCoordinateHelper.projectY(lat)];
1153
+ if (!this.options.wrap && !isInWorld([x, y])) continue;
1092
1154
  const feature = {
1093
1155
  id: point.id,
1094
1156
  type: "Point",
@@ -1110,14 +1172,14 @@ var ClusterTileIndex = class {
1110
1172
  const points = [];
1111
1173
  for (const feature of features) {
1112
1174
  if (feature.type === "Point") {
1113
- if (this.worldCoordinates && !isInWorld(feature.geometry)) continue;
1175
+ if (!this.options.wrap && !isInWorld(feature.geometry)) continue;
1114
1176
  points.push(feature);
1115
1177
  continue;
1116
1178
  }
1117
1179
  if (feature.type !== "MultiPoint") continue;
1118
1180
  for (let i = 0; i < feature.geometry.length; i += 3) {
1119
1181
  const geometry = [feature.geometry[i], feature.geometry[i + 1]];
1120
- if (this.worldCoordinates && !isInWorld(geometry)) continue;
1182
+ if (!this.options.wrap && !isInWorld(geometry)) continue;
1121
1183
  points.push({
1122
1184
  id: feature.id,
1123
1185
  type: "Point",
@@ -1134,7 +1196,11 @@ var ClusterTileIndex = class {
1134
1196
  * @param features
1135
1197
  */
1136
1198
  updateIndex(features, _affected, options) {
1137
- this.options = createOptions(options.clusterOptions);
1199
+ this.options = createOptions({
1200
+ ...options.clusterOptions,
1201
+ coordinates: this.options.coordinates,
1202
+ wrap: this.options.wrap
1203
+ });
1138
1204
  this.initialize(features);
1139
1205
  }
1140
1206
  /**
@@ -1182,11 +1248,61 @@ var ClusterTileIndex = class {
1182
1248
  }
1183
1249
  /**
1184
1250
  * Returns clusters and/or points within a bounding box at a given zoom level.
1185
- * @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order, or `[minX, minY, maxX, maxY]` in world coordinates.
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.
1186
1253
  * @param zoom - Zoom level to query.
1187
1254
  */
1188
1255
  getClusters(bbox, zoom) {
1189
- return (this.worldCoordinates ? this.getClustersInRange(bbox, zoom) : this.getClustersInternal(bbox, zoom)).map((f) => featureToGeoJSON(f, this.worldCoordinates));
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));
1190
1306
  }
1191
1307
  getClustersInternal(bbox, zoom) {
1192
1308
  let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
@@ -1212,10 +1328,10 @@ var ClusterTileIndex = class {
1212
1328
  return easternHem.concat(westernHem);
1213
1329
  }
1214
1330
  return this.getClustersInRange([
1215
- projectX(minLng),
1216
- projectY(maxLat),
1217
- projectX(maxLng),
1218
- projectY(minLat)
1331
+ this.worldCoordinateHelper.projectX(minLng),
1332
+ this.worldCoordinateHelper.projectY(maxLat),
1333
+ this.worldCoordinateHelper.projectX(maxLng),
1334
+ this.worldCoordinateHelper.projectY(minLat)
1219
1335
  ], zoom);
1220
1336
  }
1221
1337
  /**
@@ -1253,7 +1369,7 @@ var ClusterTileIndex = class {
1253
1369
  const children = [];
1254
1370
  for (const id of ids) {
1255
1371
  const k = id * this.stride;
1256
- if (data[k + OFFSET_PARENT] === clusterId) children.push(data[k + OFFSET_NUM] > 1 ? getClusterGeoJSON(coords, id, data, k, this.clusterProps, this.worldCoordinates) : featureToGeoJSON(this.points[data[k + OFFSET_ID]], this.worldCoordinates));
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));
1257
1373
  }
1258
1374
  if (children.length === 0) throw clusterError;
1259
1375
  return children;
@@ -1281,7 +1397,7 @@ var ClusterTileIndex = class {
1281
1397
  const tree = this.trees[this.limitZoom(z)];
1282
1398
  if (!tree) return null;
1283
1399
  const z2 = Math.pow(2, z);
1284
- if (this.worldCoordinates && (x < 0 || x >= z2 || y < 0 || y >= z2)) return null;
1400
+ if (!this.options.wrap && (x < 0 || x >= z2 || y < 0 || y >= z2)) return null;
1285
1401
  const { extent, radius } = this.options;
1286
1402
  const p = radius / extent;
1287
1403
  const top = encode((y - p) / z2);
@@ -1295,8 +1411,8 @@ var ClusterTileIndex = class {
1295
1411
  z
1296
1412
  };
1297
1413
  this.addTileFeatures(tree.range(encode((x - p) / z2), top, encode((x + 1 + p) / z2), bottom), tree, x, y, z2, tile);
1298
- if (!this.worldCoordinates && x === 0) this.addTileFeatures(tree.range(encode(1 - p / z2), top, encode(1), bottom), tree, z2, y, z2, tile);
1299
- if (!this.worldCoordinates && x === z2 - 1) this.addTileFeatures(tree.range(encode(0), top, encode(p / z2), bottom), tree, -1, y, z2, tile);
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);
1300
1416
  return tile;
1301
1417
  }
1302
1418
  /**
@@ -1479,14 +1595,14 @@ function getClusterFeature(coords, index, data, i, clusterProps) {
1479
1595
  geometry: [decode(coords[index * 2]), decode(coords[index * 2 + 1])]
1480
1596
  };
1481
1597
  }
1482
- function getClusterGeoJSON(coords, index, data, i, clusterProps, worldCoordinates) {
1598
+ function getClusterGeoJSON(coords, index, data, i, clusterProps, worldCoordinateHelper) {
1483
1599
  return {
1484
1600
  type: "Feature",
1485
1601
  id: data[i + OFFSET_ID],
1486
1602
  properties: getClusterProperties(data, i, clusterProps),
1487
1603
  geometry: {
1488
1604
  type: "Point",
1489
- coordinates: [unprojectX(decode(coords[index * 2]), worldCoordinates), unprojectY(decode(coords[index * 2 + 1]), worldCoordinates)]
1605
+ coordinates: [worldCoordinateHelper.unprojectX(decode(coords[index * 2])), worldCoordinateHelper.unprojectY(decode(coords[index * 2 + 1]))]
1490
1606
  }
1491
1607
  };
1492
1608
  }
@@ -1766,7 +1882,7 @@ var TileIndex = class {
1766
1882
  getTile(z, x, y) {
1767
1883
  const { extent, debug } = this.options;
1768
1884
  const z2 = 1 << z;
1769
- if (this.options.worldCoordinates && (x < 0 || x >= z2)) return null;
1885
+ if (!this.options.wrap && (x < 0 || x >= z2)) return null;
1770
1886
  x = x + z2 & z2 - 1;
1771
1887
  const id = toID(z, x, y);
1772
1888
  if (this.tiles[id]) return transformTile(this.tiles[id], extent);
@@ -1792,14 +1908,16 @@ var TileIndex = class {
1792
1908
  return transformTile(this.tiles[id], extent);
1793
1909
  }
1794
1910
  /**
1795
- * The features the top tile starts from: all of them in a wrapping world, where wrapping already put them inside,
1796
- * and only what lies inside the world square and its buffer with `worldCoordinates`. The index keeps the rest, so
1797
- * an update can still move a feature in from outside.
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.
1798
1915
  */
1799
1916
  clipToWorld(features) {
1800
- if (!this.options.worldCoordinates) return features;
1917
+ const { wrap, coordinates } = this.options;
1918
+ if (wrap && coordinates !== "unitSquare") return features;
1801
1919
  const buffer = this.options.buffer / this.options.extent;
1802
- return clip(clip(features, 1, -buffer, 1 + buffer, 0, -Infinity, Infinity, this.options) || [], 1, -buffer, 1 + buffer, 1, -Infinity, Infinity, this.options) || [];
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) || [];
1803
1921
  }
1804
1922
  /**
1805
1923
  * splits features from a parent tile to sub-tiles.
@@ -1955,7 +2073,8 @@ const defaultOptions = {
1955
2073
  cluster: false,
1956
2074
  clusterOptions: defaultClusterOptions,
1957
2075
  debug: 0,
1958
- worldCoordinates: false
2076
+ coordinates: "lngLat",
2077
+ wrap: true
1959
2078
  };
1960
2079
  /**
1961
2080
  * Main class for creating and managing a vector tile index from GeoJSON data.
@@ -1963,11 +2082,13 @@ const defaultOptions = {
1963
2082
  var GeoJSONVT = class {
1964
2083
  constructor(data, options) {
1965
2084
  options = this.options = Object.assign({}, defaultOptions, options);
2085
+ this.worldCoordinateHelper = getWorldCoordinateHelper(options.coordinates);
1966
2086
  const debug = options.debug;
1967
2087
  if (debug) console.time("preprocess data");
1968
2088
  if (options.maxZoom < 0 || options.maxZoom > 24) throw new Error("maxZoom should be in the 0-24 range");
1969
2089
  if (options.promoteId && options.generateId) throw new Error("promoteId and generateId cannot be used together.");
1970
- let features = convertToInternal(data, options);
2090
+ if (options.updateable) this.unlocated = /* @__PURE__ */ new Map();
2091
+ let features = convertToInternal(data, options, this.unlocated);
1971
2092
  if (debug) {
1972
2093
  console.timeEnd("preprocess data");
1973
2094
  console.log("index: maxZoom: %d, maxPoints: %d", options.indexMaxZoom, options.indexMaxPoints);
@@ -1978,7 +2099,11 @@ var GeoJSONVT = class {
1978
2099
  this.initializeIndex(features, options);
1979
2100
  }
1980
2101
  initializeIndex(features, options) {
1981
- this.tileIndex = options.cluster ? new ClusterTileIndex(options.clusterOptions, options.worldCoordinates) : 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);
1982
2107
  if (!features.length) return;
1983
2108
  this.tileIndex.initialize(features);
1984
2109
  }
@@ -2003,24 +2128,27 @@ var GeoJSONVT = class {
2003
2128
  updateData(diff, filter) {
2004
2129
  const options = this.options;
2005
2130
  if (!options.updateable) throw new Error("to update tile geojson `updateable` option must be set to true");
2006
- let { affected, source } = applySourceDiff(this.source, diff, options);
2131
+ let { affected, source } = applySourceDiff(this.source, diff, options, this.unlocated);
2007
2132
  if (filter) ({affected, source} = this.filterUpdate(source, affected, filter));
2008
2133
  if (!affected.length) return;
2009
2134
  this.source = source;
2010
2135
  this.tileIndex.updateIndex(source, affected, options);
2011
2136
  }
2012
2137
  /**
2013
- * 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.
2014
2141
  */
2015
2142
  filterUpdate(source, affected, predicate) {
2016
2143
  const removeIds = /* @__PURE__ */ new Set();
2017
2144
  for (const feature of source) {
2018
2145
  if (feature.id == void 0) continue;
2019
- if (predicate(featureToGeoJSON(feature, this.options.worldCoordinates))) continue;
2146
+ if (predicate(featureToGeoJSON(feature, this.worldCoordinateHelper))) continue;
2020
2147
  affected.push(feature);
2021
2148
  removeIds.add(feature.id);
2022
2149
  }
2023
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);
2024
2152
  return {
2025
2153
  affected,
2026
2154
  source
@@ -2031,12 +2159,12 @@ var GeoJSONVT = class {
2031
2159
  */
2032
2160
  getData() {
2033
2161
  if (!this.options.updateable) throw new Error("to retrieve data the `updateable` option must be set to true");
2034
- return convertToGeoJSON(this.source, this.options.worldCoordinates);
2162
+ return convertToGeoJSON(this.source, this.worldCoordinateHelper);
2035
2163
  }
2036
2164
  /**
2037
2165
  * Update supercluster options and regenerate the index.
2038
2166
  * @param cluster - whether to enable clustering
2039
- * @param clusterOptions - {@link SuperclusterOptions}
2167
+ * @param clusterOptions - {@link SuperclusterOptions} without `coordinates` and `wrap`, which come from the index options
2040
2168
  */
2041
2169
  updateClusterOptions(cluster, clusterOptions) {
2042
2170
  const wasCluster = this.options.cluster;
@@ -2112,9 +2240,10 @@ var GeoJSONVT = class {
2112
2240
  function geoJSONToTile(data, z, x, y, options = {}) {
2113
2241
  options = {
2114
2242
  ...defaultOptions,
2243
+ wrap: false,
2115
2244
  ...options
2116
2245
  };
2117
- const { wrap: shouldWrap = false, clip: shouldClip = false } = options;
2246
+ const { wrap: shouldWrap, clip: shouldClip = false } = options;
2118
2247
  let features = convertToInternal(data, options);
2119
2248
  if (shouldWrap) features = wrap(features, options);
2120
2249
  if (shouldClip || options.lineMetrics) {