@maplibre/geojson-vt 6.1.2 → 6.2.0

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