@maplibre/geojson-vt 6.1.1 → 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.
@@ -7,6 +7,14 @@
7
7
  * @param sqTolerance - square tolerance value
8
8
  */
9
9
  function simplify(coords, first, last, sqTolerance) {
10
+ const stack = [first, last];
11
+ while (stack.length) {
12
+ last = stack.pop();
13
+ first = stack.pop();
14
+ simplifySegment(coords, first, last, sqTolerance, stack);
15
+ }
16
+ }
17
+ function simplifySegment(coords, first, last, sqTolerance, stack) {
10
18
  let maxSqDist = sqTolerance;
11
19
  const mid = first + (last - first >> 1);
12
20
  let minPosToMid = last - first;
@@ -31,9 +39,9 @@ function simplify(coords, first, last, sqTolerance) {
31
39
  }
32
40
  }
33
41
  if (maxSqDist > sqTolerance) {
34
- if (index - first > 3) simplify(coords, first, index, sqTolerance);
35
42
  coords[index + 2] = maxSqDist;
36
- if (last - index > 3) simplify(coords, index, last, sqTolerance);
43
+ if (index - first > 3) stack.push(first, index);
44
+ if (last - index > 3) stack.push(index, last);
37
45
  }
38
46
  }
39
47
  /**
@@ -102,9 +110,7 @@ function createFeature(id, type, geom, tags) {
102
110
  case "MultiLineString":
103
111
  for (const line of data.geom) calcLineBBox(feature, line.points);
104
112
  break;
105
- case "MultiPolygon":
106
- for (const polygon of data.geom) calcLineBBox(feature, polygon[0].points);
107
- break;
113
+ case "MultiPolygon": for (const polygon of data.geom) calcLineBBox(feature, polygon[0].points);
108
114
  }
109
115
  return feature;
110
116
  }
@@ -156,24 +162,25 @@ function featureToInternal(features, geojson, options, index, depth = 0) {
156
162
  if (!geojson.geometry.coordinates?.length) return;
157
163
  const id = getFeatureId(geojson, options, index);
158
164
  const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
165
+ const worldCoordinates = options.worldCoordinates === true;
159
166
  switch (geojson.geometry.type) {
160
167
  case "Point":
161
- convertPointFeature(features, id, geojson.geometry, geojson.properties);
168
+ convertPointFeature(features, id, geojson.geometry, worldCoordinates, geojson.properties);
162
169
  return;
163
170
  case "MultiPoint":
164
- convertMultiPointFeature(features, id, geojson.geometry, geojson.properties);
171
+ convertMultiPointFeature(features, id, geojson.geometry, worldCoordinates, geojson.properties);
165
172
  return;
166
173
  case "LineString":
167
- convertLineStringFeature(features, id, geojson.geometry, tolerance, geojson.properties);
174
+ convertLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
168
175
  return;
169
176
  case "MultiLineString":
170
- convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, options, geojson.properties);
177
+ convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinates, options, geojson.properties);
171
178
  return;
172
179
  case "Polygon":
173
- convertPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
180
+ convertPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
174
181
  return;
175
182
  case "MultiPolygon":
176
- convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
183
+ convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
177
184
  return;
178
185
  default: throw new Error("Input data is not a valid GeoJSON object.");
179
186
  }
@@ -191,56 +198,60 @@ function convertGeometryCollection(features, geojson, geometry, options, index,
191
198
  properties: geojson.properties
192
199
  }, options, index, depth);
193
200
  }
194
- function convertPointFeature(features, id, geom, properties) {
201
+ function convertPointFeature(features, id, geom, worldCoordinates, properties) {
195
202
  const out = [];
196
- 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);
197
204
  features.push(createFeature(id, "Point", out, properties));
198
205
  }
199
- function convertMultiPointFeature(features, id, geom, properties) {
206
+ function convertMultiPointFeature(features, id, geom, worldCoordinates, properties) {
200
207
  const out = [];
201
- 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);
202
209
  features.push(createFeature(id, "MultiPoint", out, properties));
203
210
  }
204
- function convertLineStringFeature(features, id, geom, tolerance, properties) {
211
+ function convertLineStringFeature(features, id, geom, tolerance, worldCoordinates, properties) {
205
212
  const out = { points: [] };
206
- convertLine(geom.coordinates, out, tolerance, false);
213
+ convertLine(geom.coordinates, out, tolerance, false, worldCoordinates);
207
214
  features.push(createFeature(id, "LineString", out, properties));
208
215
  }
209
- function convertMultiLineStringFeature(features, id, geom, tolerance, options, properties) {
216
+ function convertMultiLineStringFeature(features, id, geom, tolerance, worldCoordinates, options, properties) {
210
217
  if (options.lineMetrics) for (const line of geom.coordinates) {
211
218
  const out = { points: [] };
212
- convertLine(line, out, tolerance, false);
219
+ convertLine(line, out, tolerance, false, worldCoordinates);
213
220
  features.push(createFeature(id, "LineString", out, properties));
214
221
  }
215
222
  else {
216
223
  const out = [];
217
- convertLines(geom.coordinates, out, tolerance, false);
224
+ convertLines(geom.coordinates, out, tolerance, false, worldCoordinates);
218
225
  features.push(createFeature(id, "MultiLineString", out, properties));
219
226
  }
220
227
  }
221
- function convertPolygonFeature(features, id, geom, tolerance, properties) {
228
+ function convertPolygonFeature(features, id, geom, tolerance, worldCoordinates, properties) {
222
229
  const out = [];
223
- convertLines(geom.coordinates, out, tolerance, true);
230
+ convertLines(geom.coordinates, out, tolerance, true, worldCoordinates);
224
231
  features.push(createFeature(id, "Polygon", out, properties));
225
232
  }
226
- function convertMultiPolygonFeature(features, id, geom, tolerance, properties) {
233
+ function convertMultiPolygonFeature(features, id, geom, tolerance, worldCoordinates, properties) {
227
234
  const out = [];
228
235
  for (const polygon of geom.coordinates) {
229
236
  const polygonOut = [];
230
- convertLines(polygon, polygonOut, tolerance, true);
237
+ convertLines(polygon, polygonOut, tolerance, true, worldCoordinates);
238
+ if (!polygonOut.length) continue;
231
239
  out.push(polygonOut);
232
240
  }
241
+ if (!out.length) return;
233
242
  features.push(createFeature(id, "MultiPolygon", out, properties));
234
243
  }
235
- function convertLine(ring, out, tolerance, isPolygon) {
244
+ function convertLine(ring, out, tolerance, isPolygon, worldCoordinates) {
236
245
  let x0, y0;
237
246
  let size = 0;
238
247
  for (let j = 0; j < ring.length; j++) {
239
- const x = projectX(ring[j][0]);
240
- const y = projectY(ring[j][1]);
248
+ const x = projectX(ring[j][0], worldCoordinates);
249
+ const y = projectY(ring[j][1], worldCoordinates);
241
250
  out.points.push(x, y, 0);
242
- if (j > 0) if (isPolygon) size += (x0 * y - x * y0) / 2;
243
- else size += Math.sqrt(Math.pow(x - x0, 2) + Math.pow(y - y0, 2));
251
+ if (j > 0) {
252
+ if (isPolygon) size += (x0 * y - x * y0) / 2;
253
+ else size += Math.sqrt(Math.pow(x - x0, 2) + Math.pow(y - y0, 2));
254
+ }
244
255
  x0 = x;
245
256
  y0 = y;
246
257
  }
@@ -253,23 +264,25 @@ function convertLine(ring, out, tolerance, isPolygon) {
253
264
  out.start = 0;
254
265
  out.end = out.size;
255
266
  }
256
- function convertLines(rings, out, tolerance, isPolygon) {
267
+ function convertLines(rings, out, tolerance, isPolygon, worldCoordinates) {
257
268
  for (let i = 0; i < rings.length; i++) {
258
269
  const geom = { points: [] };
259
- convertLine(rings[i], geom, tolerance, isPolygon);
270
+ convertLine(rings[i], geom, tolerance, isPolygon, worldCoordinates);
260
271
  out.push(geom);
261
272
  }
262
273
  }
263
274
  /**
264
- * 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
265
276
  */
266
- function projectX(x) {
277
+ function projectX(x, worldCoordinates = false) {
278
+ if (worldCoordinates) return x;
267
279
  return x / 360 + .5;
268
280
  }
269
281
  /**
270
- * 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
271
283
  */
272
- function projectY(y) {
284
+ function projectY(y, worldCoordinates = false) {
285
+ if (worldCoordinates) return y;
273
286
  const sin = Math.sin(y * Math.PI / 180);
274
287
  const y2 = .5 - .25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
275
288
  return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
@@ -279,19 +292,19 @@ function projectY(y) {
279
292
  /**
280
293
  * Converts internal source features back to GeoJSON format.
281
294
  */
282
- function convertToGeoJSON(source) {
295
+ function convertToGeoJSON(source, worldCoordinates = false) {
283
296
  return {
284
297
  type: "FeatureCollection",
285
- features: source.map((feature) => featureToGeoJSON(feature))
298
+ features: source.map((feature) => featureToGeoJSON(feature, worldCoordinates))
286
299
  };
287
300
  }
288
301
  /**
289
302
  * Converts a single internal feature to GeoJSON format.
290
303
  */
291
- function featureToGeoJSON(feature) {
304
+ function featureToGeoJSON(feature, worldCoordinates = false) {
292
305
  const geojsonFeature = {
293
306
  type: "Feature",
294
- geometry: geometryToGeoJSON(feature),
307
+ geometry: geometryToGeoJSON(feature, worldCoordinates),
295
308
  properties: feature.tags
296
309
  };
297
310
  if (feature.id != null) geojsonFeature.id = feature.id;
@@ -300,50 +313,52 @@ function featureToGeoJSON(feature) {
300
313
  /**
301
314
  * Converts a single internal feature geometry to GeoJSON format.
302
315
  */
303
- function geometryToGeoJSON(feature) {
316
+ function geometryToGeoJSON(feature, worldCoordinates) {
304
317
  const { type, geometry } = feature;
305
318
  switch (type) {
306
319
  case "Point": return {
307
320
  type,
308
- coordinates: unprojectPoint(geometry[0], geometry[1])
321
+ coordinates: unprojectPoint(geometry[0], geometry[1], worldCoordinates)
309
322
  };
310
323
  case "MultiPoint": return {
311
324
  type,
312
- coordinates: unprojectPoints(geometry)
325
+ coordinates: unprojectPoints(geometry, worldCoordinates)
313
326
  };
314
327
  case "LineString": return {
315
328
  type,
316
- coordinates: unprojectPoints(geometry.points)
329
+ coordinates: unprojectPoints(geometry.points, worldCoordinates)
317
330
  };
318
331
  case "MultiLineString":
319
332
  case "Polygon": return {
320
333
  type,
321
- coordinates: geometry.map((ring) => unprojectPoints(ring.points))
334
+ coordinates: geometry.map((ring) => unprojectPoints(ring.points, worldCoordinates))
322
335
  };
323
336
  case "MultiPolygon": return {
324
337
  type,
325
- coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points)))
338
+ coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points, worldCoordinates)))
326
339
  };
327
340
  }
328
341
  }
329
- function unprojectPoints(coords) {
342
+ function unprojectPoints(coords, worldCoordinates = false) {
330
343
  const result = [];
331
- 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));
332
345
  return result;
333
346
  }
334
- function unprojectPoint(x, y) {
335
- return [unprojectX(x), unprojectY(y)];
347
+ function unprojectPoint(x, y, worldCoordinates) {
348
+ return [unprojectX(x, worldCoordinates), unprojectY(y, worldCoordinates)];
336
349
  }
337
350
  /**
338
- * 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
339
352
  */
340
- function unprojectX(x) {
353
+ function unprojectX(x, worldCoordinates = false) {
354
+ if (worldCoordinates) return x;
341
355
  return (x - .5) * 360;
342
356
  }
343
357
  /**
344
- * 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
345
359
  */
346
- function unprojectY(y) {
360
+ function unprojectY(y, worldCoordinates = false) {
361
+ if (worldCoordinates) return y;
347
362
  const y2 = (180 - y * 360) * Math.PI / 180;
348
363
  return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
349
364
  }
@@ -470,12 +485,12 @@ function clipLine(geom, newGeom, start, end, axis, isPolygon, trackMetrics) {
470
485
  let exited = false;
471
486
  if (trackMetrics) segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
472
487
  if (a < start) {
473
- if (b > start) {
488
+ if (b >= start) {
474
489
  t = intersect(slice, ax, ay, bx, by, start);
475
490
  if (trackMetrics) slice.start = len + segLen * t;
476
491
  }
477
492
  } else if (a > end) {
478
- if (b < end) {
493
+ if (b <= end) {
479
494
  t = intersect(slice, ax, ay, bx, by, end);
480
495
  if (trackMetrics) slice.start = len + segLen * t;
481
496
  }
@@ -535,6 +550,7 @@ function intersectY(out, ax, ay, bx, by, y) {
535
550
  //#endregion
536
551
  //#region src/wrap.ts
537
552
  function wrap(features, options) {
553
+ if (options.worldCoordinates) return features;
538
554
  const buffer = options.buffer / options.extent;
539
555
  let merged = features;
540
556
  const left = clip(features, 1, -1 - buffer, buffer, 0, -1, 2, options);
@@ -655,7 +671,7 @@ function applySourceDiff(source, dataDiff, options) {
655
671
  * @param vtFeatures - the original features
656
672
  * @param update - the update object to apply
657
673
  * @param options - the options to use for the wrap method
658
- * @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.
659
675
  */
660
676
  function getUpdatedFeatures(vtFeatures, update, options) {
661
677
  const changeGeometry = !!update.newGeometry;
@@ -670,7 +686,11 @@ function getUpdatedFeatures(vtFeatures, update, options) {
670
686
  geometry: update.newGeometry,
671
687
  properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
672
688
  }]
673
- }, options);
689
+ }, {
690
+ ...options,
691
+ promoteId: void 0,
692
+ generateId: false
693
+ });
674
694
  features = wrap(features, options);
675
695
  return features;
676
696
  }
@@ -707,7 +727,7 @@ function diffToHashed(diff, options) {
707
727
  return {
708
728
  removeAll: diff.removeAll,
709
729
  remove: new Set(diff.remove || []),
710
- 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])),
711
731
  update: new Map(diff.update?.map((update) => [update.id, update]))
712
732
  };
713
733
  }
@@ -777,7 +797,7 @@ var KDBush = class KDBush {
777
797
  this.coords = new ArrayType(data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
778
798
  this._pos = 0;
779
799
  this._finished = false;
780
- new Uint8Array(data, 0, 2).set([219, (VERSION << 4) + arrayTypeIndex]);
800
+ new Uint8Array(data, 0, 2).set([219, 16 + arrayTypeIndex]);
781
801
  new Uint16Array(data, 2, 1)[0] = nodeSize;
782
802
  new Uint32Array(data, 4, 1)[0] = numItems;
783
803
  }
@@ -1010,52 +1030,101 @@ const defaultClusterOptions = {
1010
1030
  reduce: null,
1011
1031
  map: (props) => props
1012
1032
  };
1013
- const OFFSET_ZOOM = 2;
1014
- const OFFSET_ID = 3;
1015
- const OFFSET_PARENT = 4;
1016
- const OFFSET_NUM = 5;
1017
- 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;
1018
1067
  /**
1019
1068
  * This class allow clustering of geojson points.
1020
1069
  */
1021
1070
  var ClusterTileIndex = class {
1022
- constructor(options) {
1023
- this.options = Object.assign(Object.create(defaultClusterOptions), options);
1071
+ constructor(options, worldCoordinates = false) {
1072
+ this.options = createOptions(options);
1073
+ this.worldCoordinates = worldCoordinates;
1024
1074
  this.trees = new Array(this.options.maxZoom + 1);
1025
- this.stride = this.options.reduce ? 7 : 6;
1075
+ this.stride = this.options.reduce ? 5 : 4;
1026
1076
  this.clusterProps = [];
1027
1077
  this.points = [];
1028
1078
  }
1029
1079
  /**
1030
- * Loads GeoJSON point features and builds the internal clustering index.
1031
- * @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.
1032
1083
  */
1033
1084
  load(points) {
1034
1085
  const features = [];
1035
1086
  for (const point of points) {
1036
1087
  if (!point.geometry) continue;
1037
- const [lng, lat] = point.geometry.coordinates;
1038
- const [x, y] = [projectX(lng), projectY(lat)];
1039
- const feature = {
1040
- id: point.id,
1041
- type: "Point",
1042
- geometry: [x, y],
1043
- tags: point.properties
1044
- };
1045
- 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
+ }
1046
1100
  }
1047
1101
  this.createIndex(features);
1048
1102
  }
1049
1103
  /**
1050
1104
  * @internal
1051
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.
1052
1107
  * @param features - {@link GeoJSONVTInternalFeature} data source features to filter and cluster.
1053
1108
  */
1054
1109
  initialize(features) {
1055
1110
  const points = [];
1056
1111
  for (const feature of features) {
1057
- if (feature.type !== "Point") continue;
1058
- 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
+ }
1059
1128
  }
1060
1129
  this.createIndex(points);
1061
1130
  }
@@ -1065,41 +1134,59 @@ var ClusterTileIndex = class {
1065
1134
  * @param features
1066
1135
  */
1067
1136
  updateIndex(features, _affected, options) {
1068
- this.options = Object.assign(Object.create(defaultClusterOptions), options.clusterOptions);
1137
+ this.options = createOptions(options.clusterOptions);
1069
1138
  this.initialize(features);
1070
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
+ */
1071
1144
  createIndex(points) {
1072
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 = [];
1073
1149
  if (log) console.time("total time");
1074
1150
  const timerId = `prepare ${points.length} points`;
1075
1151
  if (log) console.time(timerId);
1076
1152
  this.points = points;
1077
- 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;
1078
1158
  for (let i = 0; i < points.length; i++) {
1079
1159
  const p = points[i];
1080
- if (!p?.geometry) continue;
1081
- let [x, y] = p.geometry;
1082
- x = Math.fround(x);
1083
- y = Math.fround(y);
1084
- data.push(x, y, Infinity, i, -1, 1);
1085
- 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++;
1086
1169
  }
1087
- let tree = this.trees[maxZoom + 1] = this.createTree(data);
1170
+ let tree = this.trees[maxZoom + 1] = this.createTree(coords, data, numItems);
1088
1171
  if (log) console.timeEnd(timerId);
1172
+ const nextCoords = new Int32Array(numItems * 2);
1173
+ const nextData = new Int32Array(numItems * stride);
1089
1174
  for (let z = maxZoom; z >= minZoom; z--) {
1090
1175
  const now = Date.now();
1091
- 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;
1092
1179
  if (log) console.log("z%d: %d clusters in %dms", z, tree.numItems, Date.now() - now);
1093
1180
  }
1094
1181
  if (log) console.timeEnd("total time");
1095
1182
  }
1096
1183
  /**
1097
1184
  * Returns clusters and/or points within a bounding box at a given zoom level.
1098
- * @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.
1099
1186
  * @param zoom - Zoom level to query.
1100
1187
  */
1101
1188
  getClusters(bbox, zoom) {
1102
- 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));
1103
1190
  }
1104
1191
  getClustersInternal(bbox, zoom) {
1105
1192
  let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
@@ -1124,13 +1211,25 @@ var ClusterTileIndex = class {
1124
1211
  ], zoom);
1125
1212
  return easternHem.concat(westernHem);
1126
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) {
1127
1225
  const tree = this.trees[this.limitZoom(zoom)];
1128
- 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;
1129
1228
  const data = tree.flatData;
1130
1229
  const clusters = [];
1131
1230
  for (const id of ids) {
1132
1231
  const k = this.stride * id;
1133
- 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]]);
1134
1233
  }
1135
1234
  return clusters;
1136
1235
  }
@@ -1144,16 +1243,17 @@ var ClusterTileIndex = class {
1144
1243
  const clusterError = /* @__PURE__ */ new Error("No cluster with the specified id: " + clusterId);
1145
1244
  const tree = this.trees[originZoom];
1146
1245
  if (!tree) throw clusterError;
1246
+ const coords = tree.flatCoords;
1147
1247
  const data = tree.flatData;
1148
- if (originId * this.stride >= data.length) throw clusterError;
1149
- const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
1150
- const x = data[originId * this.stride];
1151
- 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];
1152
1252
  const ids = tree.within(x, y, r);
1153
1253
  const children = [];
1154
1254
  for (const id of ids) {
1155
1255
  const k = id * this.stride;
1156
- 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));
1157
1257
  }
1158
1258
  if (children.length === 0) throw clusterError;
1159
1259
  return children;
@@ -1181,10 +1281,11 @@ var ClusterTileIndex = class {
1181
1281
  const tree = this.trees[this.limitZoom(z)];
1182
1282
  if (!tree) return null;
1183
1283
  const z2 = Math.pow(2, z);
1284
+ if (this.worldCoordinates && (x < 0 || x >= z2 || y < 0 || y >= z2)) return null;
1184
1285
  const { extent, radius } = this.options;
1185
1286
  const p = radius / extent;
1186
- const top = (y - p) / z2;
1187
- const bottom = (y + 1 + p) / z2;
1287
+ const top = encode((y - p) / z2);
1288
+ const bottom = encode((y + 1 + p) / z2);
1188
1289
  const tile = {
1189
1290
  transformed: true,
1190
1291
  features: [],
@@ -1193,9 +1294,9 @@ var ClusterTileIndex = class {
1193
1294
  y,
1194
1295
  z
1195
1296
  };
1196
- this.addTileFeatures(tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), tree.flatData, x, y, z2, tile);
1197
- if (x === 0) this.addTileFeatures(tree.range(1 - p / z2, top, 1, bottom), tree.flatData, z2, y, z2, tile);
1198
- 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);
1199
1300
  return tile;
1200
1301
  }
1201
1302
  /**
@@ -1209,23 +1310,36 @@ var ClusterTileIndex = class {
1209
1310
  const children = this.getChildren(clusterId);
1210
1311
  for (const child of children) {
1211
1312
  const props = child.properties;
1212
- if (props?.cluster) if (skipped + props.point_count <= offset) skipped += props.point_count;
1213
- else skipped = this.appendLeaves(result, props.cluster_id, limit, offset, skipped);
1214
- else if (skipped < offset) skipped++;
1313
+ if (props?.cluster) {
1314
+ if (skipped + props.point_count <= offset) skipped += props.point_count;
1315
+ else skipped = this.appendLeaves(result, props.cluster_id, limit, offset, skipped);
1316
+ } else if (skipped < offset) skipped++;
1215
1317
  else result.push(child);
1216
1318
  if (result.length === limit) break;
1217
1319
  }
1218
1320
  return skipped;
1219
1321
  }
1220
- createTree(data) {
1221
- const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array);
1222
- 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]);
1223
1325
  tree.finish();
1326
+ tree.flatCoords = coords;
1224
1327
  tree.flatData = data;
1225
1328
  tree.data = null;
1226
1329
  return tree;
1227
1330
  }
1228
- 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;
1229
1343
  for (const i of ids) {
1230
1344
  const k = i * this.stride;
1231
1345
  const isCluster = data[k + OFFSET_NUM] > 1;
@@ -1234,8 +1348,8 @@ var ClusterTileIndex = class {
1234
1348
  let py;
1235
1349
  if (isCluster) {
1236
1350
  tags = getClusterProperties(data, k, this.clusterProps);
1237
- px = data[k];
1238
- py = data[k + 1];
1351
+ px = decode(coords[i * 2]);
1352
+ py = decode(coords[i * 2 + 1]);
1239
1353
  } else {
1240
1354
  const p = this.points[data[k + OFFSET_ID]];
1241
1355
  tags = p.tags;
@@ -1256,22 +1370,30 @@ var ClusterTileIndex = class {
1256
1370
  limitZoom(z) {
1257
1371
  return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
1258
1372
  }
1259
- cluster(tree, zoom) {
1260
- const { radius, extent, reduce, minPoints } = this.options;
1261
- 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;
1262
1382
  const data = tree.flatData;
1263
- const nextData = [];
1264
1383
  const stride = this.stride;
1265
- 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;
1266
1388
  if (data[i + OFFSET_ZOOM] <= zoom) continue;
1267
1389
  data[i + OFFSET_ZOOM] = zoom;
1268
- const x = data[i];
1269
- const y = data[i + 1];
1270
- 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);
1271
1393
  const numPointsOrigin = data[i + OFFSET_NUM];
1272
1394
  let numPoints = numPointsOrigin;
1273
- for (const neighborId of neighborIds) {
1274
- const k = neighborId * stride;
1395
+ for (let n = 0; n < numNeighbors; n++) {
1396
+ const k = neighborIds[n] * stride;
1275
1397
  if (data[k + OFFSET_ZOOM] > zoom) numPoints += data[k + OFFSET_NUM];
1276
1398
  }
1277
1399
  if (numPoints > numPointsOrigin && numPoints >= minPoints) {
@@ -1279,14 +1401,15 @@ var ClusterTileIndex = class {
1279
1401
  let wy = y * numPointsOrigin;
1280
1402
  let clusterProperties;
1281
1403
  let clusterPropIndex = -1;
1282
- const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length;
1283
- for (const neighborId of neighborIds) {
1284
- 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;
1285
1408
  if (data[k + OFFSET_ZOOM] <= zoom) continue;
1286
1409
  data[k + OFFSET_ZOOM] = zoom;
1287
1410
  const numPoints2 = data[k + OFFSET_NUM];
1288
- wx += data[k] * numPoints2;
1289
- wy += data[k + 1] * numPoints2;
1411
+ wx += coords[neighborIndex * 2] * numPoints2;
1412
+ wy += coords[neighborIndex * 2 + 1] * numPoints2;
1290
1413
  data[k + OFFSET_PARENT] = id;
1291
1414
  if (reduce) {
1292
1415
  if (!clusterProperties) {
@@ -1298,19 +1421,33 @@ var ClusterTileIndex = class {
1298
1421
  }
1299
1422
  }
1300
1423
  data[i + OFFSET_PARENT] = id;
1301
- nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints);
1302
- 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++;
1303
1433
  } else {
1304
- for (let j = 0; j < stride; j++) nextData.push(data[i + j]);
1305
- if (numPoints > 1) for (const neighborId of neighborIds) {
1306
- 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;
1307
1441
  if (data[k + OFFSET_ZOOM] <= zoom) continue;
1308
1442
  data[k + OFFSET_ZOOM] = zoom;
1309
- 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++;
1310
1447
  }
1311
1448
  }
1312
1449
  }
1313
- return nextData;
1450
+ return nextNumItems;
1314
1451
  }
1315
1452
  getOriginId(clusterId) {
1316
1453
  return clusterId - this.points.length >> 5;
@@ -1328,22 +1465,28 @@ var ClusterTileIndex = class {
1328
1465
  return clone && result === original ? Object.assign({}, result) : result;
1329
1466
  }
1330
1467
  };
1331
- 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) {
1332
1475
  return {
1333
1476
  id: data[i + OFFSET_ID],
1334
1477
  type: "Point",
1335
1478
  tags: getClusterProperties(data, i, clusterProps),
1336
- geometry: [data[i], data[i + 1]]
1479
+ geometry: [decode(coords[index * 2]), decode(coords[index * 2 + 1])]
1337
1480
  };
1338
1481
  }
1339
- function getClusterGeoJSON(data, i, clusterProps) {
1482
+ function getClusterGeoJSON(coords, index, data, i, clusterProps, worldCoordinates) {
1340
1483
  return {
1341
1484
  type: "Feature",
1342
1485
  id: data[i + OFFSET_ID],
1343
1486
  properties: getClusterProperties(data, i, clusterProps),
1344
1487
  geometry: {
1345
1488
  type: "Point",
1346
- coordinates: [unprojectX(data[i]), unprojectY(data[i + 1])]
1489
+ coordinates: [unprojectX(decode(coords[index * 2]), worldCoordinates), unprojectY(decode(coords[index * 2 + 1]), worldCoordinates)]
1347
1490
  }
1348
1491
  };
1349
1492
  }
@@ -1359,6 +1502,9 @@ function getClusterProperties(data, i, clusterProps) {
1359
1502
  point_count_abbreviated: abbrev
1360
1503
  });
1361
1504
  }
1505
+ function isInWorld(geometry) {
1506
+ return geometry[0] >= 0 && geometry[0] <= 1 && geometry[1] >= 0 && geometry[1] <= 1;
1507
+ }
1362
1508
  //#endregion
1363
1509
  //#region src/tile.ts
1364
1510
  const GEOJSONVT_CLIP_START = "geojsonvt_clip_start";
@@ -1567,7 +1713,7 @@ var TileIndex = class {
1567
1713
  this.total = 0;
1568
1714
  }
1569
1715
  initialize(features) {
1570
- this.splitTile(features, 0, 0, 0);
1716
+ this.splitTile(this.clipToWorld(features), 0, 0, 0);
1571
1717
  if (this.options.debug) {
1572
1718
  if (features.length) console.log("features: %d, points: %d", this.tiles[0].numFeatures, this.tiles[0].numPoints);
1573
1719
  console.timeEnd("generate tiles");
@@ -1587,8 +1733,9 @@ var TileIndex = class {
1587
1733
  0,
1588
1734
  0
1589
1735
  ];
1590
- const rootTile = createTile(source, z, x, y, options);
1591
- rootTile.source = source;
1736
+ const rootSource = this.clipToWorld(source);
1737
+ const rootTile = createTile(rootSource, z, x, y, options);
1738
+ rootTile.source = rootSource;
1592
1739
  const id = toID(z, x, y);
1593
1740
  this.tiles[id] = rootTile;
1594
1741
  this.tileCoords.push({
@@ -1619,6 +1766,7 @@ var TileIndex = class {
1619
1766
  getTile(z, x, y) {
1620
1767
  const { extent, debug } = this.options;
1621
1768
  const z2 = 1 << z;
1769
+ if (this.options.worldCoordinates && (x < 0 || x >= z2)) return null;
1622
1770
  x = x + z2 & z2 - 1;
1623
1771
  const id = toID(z, x, y);
1624
1772
  if (this.tiles[id]) return transformTile(this.tiles[id], extent);
@@ -1644,6 +1792,16 @@ var TileIndex = class {
1644
1792
  return transformTile(this.tiles[id], extent);
1645
1793
  }
1646
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
+ /**
1647
1805
  * splits features from a parent tile to sub-tiles.
1648
1806
  * z, x, and y are the coordinates of the parent tile
1649
1807
  * cz, cx, and cy are the coordinates of the target tile
@@ -1796,7 +1954,8 @@ const defaultOptions = {
1796
1954
  updateable: false,
1797
1955
  cluster: false,
1798
1956
  clusterOptions: defaultClusterOptions,
1799
- debug: 0
1957
+ debug: 0,
1958
+ worldCoordinates: false
1800
1959
  };
1801
1960
  /**
1802
1961
  * Main class for creating and managing a vector tile index from GeoJSON data.
@@ -1819,7 +1978,7 @@ var GeoJSONVT = class {
1819
1978
  this.initializeIndex(features, options);
1820
1979
  }
1821
1980
  initializeIndex(features, options) {
1822
- 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);
1823
1982
  if (!features.length) return;
1824
1983
  this.tileIndex.initialize(features);
1825
1984
  }
@@ -1857,7 +2016,7 @@ var GeoJSONVT = class {
1857
2016
  const removeIds = /* @__PURE__ */ new Set();
1858
2017
  for (const feature of source) {
1859
2018
  if (feature.id == void 0) continue;
1860
- if (predicate(featureToGeoJSON(feature))) continue;
2019
+ if (predicate(featureToGeoJSON(feature, this.options.worldCoordinates))) continue;
1861
2020
  affected.push(feature);
1862
2021
  removeIds.add(feature.id);
1863
2022
  }
@@ -1872,7 +2031,7 @@ var GeoJSONVT = class {
1872
2031
  */
1873
2032
  getData() {
1874
2033
  if (!this.options.updateable) throw new Error("to retrieve data the `updateable` option must be set to true");
1875
- return convertToGeoJSON(this.source);
2034
+ return convertToGeoJSON(this.source, this.options.worldCoordinates);
1876
2035
  }
1877
2036
  /**
1878
2037
  * Update supercluster options and regenerate the index.