@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.
@@ -170,24 +170,25 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
170
170
  if (!geojson.geometry.coordinates?.length) return;
171
171
  const id = getFeatureId(geojson, options, index);
172
172
  const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
173
+ const worldCoordinates = options.worldCoordinates === true;
173
174
  switch (geojson.geometry.type) {
174
175
  case "Point":
175
- convertPointFeature(features, id, geojson.geometry, geojson.properties);
176
+ convertPointFeature(features, id, geojson.geometry, worldCoordinates, geojson.properties);
176
177
  return;
177
178
  case "MultiPoint":
178
- convertMultiPointFeature(features, id, geojson.geometry, geojson.properties);
179
+ convertMultiPointFeature(features, id, geojson.geometry, worldCoordinates, geojson.properties);
179
180
  return;
180
181
  case "LineString":
181
- convertLineStringFeature(features, id, geojson.geometry, tolerance, geojson.properties);
182
+ convertLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
182
183
  return;
183
184
  case "MultiLineString":
184
- convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, options, geojson.properties);
185
+ convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinates, options, geojson.properties);
185
186
  return;
186
187
  case "Polygon":
187
- convertPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
188
+ convertPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
188
189
  return;
189
190
  case "MultiPolygon":
190
- convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
191
+ convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
191
192
  return;
192
193
  default: throw new Error("Input data is not a valid GeoJSON object.");
193
194
  }
@@ -205,53 +206,55 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
205
206
  properties: geojson.properties
206
207
  }, options, index, depth);
207
208
  }
208
- function convertPointFeature(features, id, geom, properties) {
209
+ function convertPointFeature(features, id, geom, worldCoordinates, properties) {
209
210
  const out = [];
210
- out.push(projectX(geom.coordinates[0]), projectY(geom.coordinates[1]), 0);
211
+ out.push(projectX(geom.coordinates[0], worldCoordinates), projectY(geom.coordinates[1], worldCoordinates), 0);
211
212
  features.push(createFeature(id, "Point", out, properties));
212
213
  }
213
- function convertMultiPointFeature(features, id, geom, properties) {
214
+ function convertMultiPointFeature(features, id, geom, worldCoordinates, properties) {
214
215
  const out = [];
215
- for (const coords of geom.coordinates) out.push(projectX(coords[0]), projectY(coords[1]), 0);
216
+ for (const coords of geom.coordinates) out.push(projectX(coords[0], worldCoordinates), projectY(coords[1], worldCoordinates), 0);
216
217
  features.push(createFeature(id, "MultiPoint", out, properties));
217
218
  }
218
- function convertLineStringFeature(features, id, geom, tolerance, properties) {
219
+ function convertLineStringFeature(features, id, geom, tolerance, worldCoordinates, properties) {
219
220
  const out = { points: [] };
220
- convertLine(geom.coordinates, out, tolerance, false);
221
+ convertLine(geom.coordinates, out, tolerance, false, worldCoordinates);
221
222
  features.push(createFeature(id, "LineString", out, properties));
222
223
  }
223
- function convertMultiLineStringFeature(features, id, geom, tolerance, options, properties) {
224
+ function convertMultiLineStringFeature(features, id, geom, tolerance, worldCoordinates, options, properties) {
224
225
  if (options.lineMetrics) for (const line of geom.coordinates) {
225
226
  const out = { points: [] };
226
- convertLine(line, out, tolerance, false);
227
+ convertLine(line, out, tolerance, false, worldCoordinates);
227
228
  features.push(createFeature(id, "LineString", out, properties));
228
229
  }
229
230
  else {
230
231
  const out = [];
231
- convertLines(geom.coordinates, out, tolerance, false);
232
+ convertLines(geom.coordinates, out, tolerance, false, worldCoordinates);
232
233
  features.push(createFeature(id, "MultiLineString", out, properties));
233
234
  }
234
235
  }
235
- function convertPolygonFeature(features, id, geom, tolerance, properties) {
236
+ function convertPolygonFeature(features, id, geom, tolerance, worldCoordinates, properties) {
236
237
  const out = [];
237
- convertLines(geom.coordinates, out, tolerance, true);
238
+ convertLines(geom.coordinates, out, tolerance, true, worldCoordinates);
238
239
  features.push(createFeature(id, "Polygon", out, properties));
239
240
  }
240
- function convertMultiPolygonFeature(features, id, geom, tolerance, properties) {
241
+ function convertMultiPolygonFeature(features, id, geom, tolerance, worldCoordinates, properties) {
241
242
  const out = [];
242
243
  for (const polygon of geom.coordinates) {
243
244
  const polygonOut = [];
244
- convertLines(polygon, polygonOut, tolerance, true);
245
+ convertLines(polygon, polygonOut, tolerance, true, worldCoordinates);
246
+ if (!polygonOut.length) continue;
245
247
  out.push(polygonOut);
246
248
  }
249
+ if (!out.length) return;
247
250
  features.push(createFeature(id, "MultiPolygon", out, properties));
248
251
  }
249
- function convertLine(ring, out, tolerance, isPolygon) {
252
+ function convertLine(ring, out, tolerance, isPolygon, worldCoordinates) {
250
253
  let x0, y0;
251
254
  let size = 0;
252
255
  for (let j = 0; j < ring.length; j++) {
253
- const x = projectX(ring[j][0]);
254
- const y = projectY(ring[j][1]);
256
+ const x = projectX(ring[j][0], worldCoordinates);
257
+ const y = projectY(ring[j][1], worldCoordinates);
255
258
  out.points.push(x, y, 0);
256
259
  if (j > 0) {
257
260
  if (isPolygon) size += (x0 * y - x * y0) / 2;
@@ -269,23 +272,25 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
269
272
  out.start = 0;
270
273
  out.end = out.size;
271
274
  }
272
- function convertLines(rings, out, tolerance, isPolygon) {
275
+ function convertLines(rings, out, tolerance, isPolygon, worldCoordinates) {
273
276
  for (let i = 0; i < rings.length; i++) {
274
277
  const geom = { points: [] };
275
- convertLine(rings[i], geom, tolerance, isPolygon);
278
+ convertLine(rings[i], geom, tolerance, isPolygon, worldCoordinates);
276
279
  out.push(geom);
277
280
  }
278
281
  }
279
282
  /**
280
- * Convert longitude to spherical mercator in [0..1] range
283
+ * Convert longitude to spherical mercator in [0..1] range, or keep a world coordinate as it is
281
284
  */
282
- function projectX(x) {
285
+ function projectX(x, worldCoordinates = false) {
286
+ if (worldCoordinates) return x;
283
287
  return x / 360 + .5;
284
288
  }
285
289
  /**
286
- * Convert latitude to spherical mercator in [0..1] range
290
+ * Convert latitude to spherical mercator in [0..1] range, or keep a world coordinate as it is
287
291
  */
288
- function projectY(y) {
292
+ function projectY(y, worldCoordinates = false) {
293
+ if (worldCoordinates) return y;
289
294
  const sin = Math.sin(y * Math.PI / 180);
290
295
  const y2 = .5 - .25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
291
296
  return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
@@ -296,19 +301,19 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
296
301
  /**
297
302
  * Converts internal source features back to GeoJSON format.
298
303
  */
299
- function convertToGeoJSON(source) {
304
+ function convertToGeoJSON(source, worldCoordinates = false) {
300
305
  return {
301
306
  type: "FeatureCollection",
302
- features: source.map((feature) => featureToGeoJSON(feature))
307
+ features: source.map((feature) => featureToGeoJSON(feature, worldCoordinates))
303
308
  };
304
309
  }
305
310
  /**
306
311
  * Converts a single internal feature to GeoJSON format.
307
312
  */
308
- function featureToGeoJSON(feature) {
313
+ function featureToGeoJSON(feature, worldCoordinates = false) {
309
314
  const geojsonFeature = {
310
315
  type: "Feature",
311
- geometry: geometryToGeoJSON(feature),
316
+ geometry: geometryToGeoJSON(feature, worldCoordinates),
312
317
  properties: feature.tags
313
318
  };
314
319
  if (feature.id != null) geojsonFeature.id = feature.id;
@@ -317,50 +322,52 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
317
322
  /**
318
323
  * Converts a single internal feature geometry to GeoJSON format.
319
324
  */
320
- function geometryToGeoJSON(feature) {
325
+ function geometryToGeoJSON(feature, worldCoordinates) {
321
326
  const { type, geometry } = feature;
322
327
  switch (type) {
323
328
  case "Point": return {
324
329
  type,
325
- coordinates: unprojectPoint(geometry[0], geometry[1])
330
+ coordinates: unprojectPoint(geometry[0], geometry[1], worldCoordinates)
326
331
  };
327
332
  case "MultiPoint": return {
328
333
  type,
329
- coordinates: unprojectPoints(geometry)
334
+ coordinates: unprojectPoints(geometry, worldCoordinates)
330
335
  };
331
336
  case "LineString": return {
332
337
  type,
333
- coordinates: unprojectPoints(geometry.points)
338
+ coordinates: unprojectPoints(geometry.points, worldCoordinates)
334
339
  };
335
340
  case "MultiLineString":
336
341
  case "Polygon": return {
337
342
  type,
338
- coordinates: geometry.map((ring) => unprojectPoints(ring.points))
343
+ coordinates: geometry.map((ring) => unprojectPoints(ring.points, worldCoordinates))
339
344
  };
340
345
  case "MultiPolygon": return {
341
346
  type,
342
- coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points)))
347
+ coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points, worldCoordinates)))
343
348
  };
344
349
  }
345
350
  }
346
- function unprojectPoints(coords) {
351
+ function unprojectPoints(coords, worldCoordinates = false) {
347
352
  const result = [];
348
- for (let i = 0; i < coords.length; i += 3) result.push(unprojectPoint(coords[i], coords[i + 1]));
353
+ for (let i = 0; i < coords.length; i += 3) result.push(unprojectPoint(coords[i], coords[i + 1], worldCoordinates));
349
354
  return result;
350
355
  }
351
- function unprojectPoint(x, y) {
352
- return [unprojectX(x), unprojectY(y)];
356
+ function unprojectPoint(x, y, worldCoordinates) {
357
+ return [unprojectX(x, worldCoordinates), unprojectY(y, worldCoordinates)];
353
358
  }
354
359
  /**
355
- * Convert spherical mercator in [0..1] range to longitude
360
+ * Convert spherical mercator in [0..1] range to longitude, or keep a world coordinate as it is
356
361
  */
357
- function unprojectX(x) {
362
+ function unprojectX(x, worldCoordinates = false) {
363
+ if (worldCoordinates) return x;
358
364
  return (x - .5) * 360;
359
365
  }
360
366
  /**
361
- * Convert spherical mercator in [0..1] range to latitude
367
+ * Convert spherical mercator in [0..1] range to latitude, or keep a world coordinate as it is
362
368
  */
363
- function unprojectY(y) {
369
+ function unprojectY(y, worldCoordinates = false) {
370
+ if (worldCoordinates) return y;
364
371
  const y2 = (180 - y * 360) * Math.PI / 180;
365
372
  return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
366
373
  }
@@ -488,12 +495,12 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
488
495
  let exited = false;
489
496
  if (trackMetrics) segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
490
497
  if (a < start) {
491
- if (b > start) {
498
+ if (b >= start) {
492
499
  t = intersect(slice, ax, ay, bx, by, start);
493
500
  if (trackMetrics) slice.start = len + segLen * t;
494
501
  }
495
502
  } else if (a > end) {
496
- if (b < end) {
503
+ if (b <= end) {
497
504
  t = intersect(slice, ax, ay, bx, by, end);
498
505
  if (trackMetrics) slice.start = len + segLen * t;
499
506
  }
@@ -554,6 +561,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
554
561
  //#endregion
555
562
  //#region src/wrap.ts
556
563
  function wrap(features, options) {
564
+ if (options.worldCoordinates) return features;
557
565
  const buffer = options.buffer / options.extent;
558
566
  let merged = features;
559
567
  const left = clip(features, 1, -1 - buffer, buffer, 0, -1, 2, options);
@@ -675,7 +683,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
675
683
  * @param vtFeatures - the original features
676
684
  * @param update - the update object to apply
677
685
  * @param options - the options to use for the wrap method
678
- * @returns Updated features. If geometry is updated, returns new feature(s) converted from geojson and wrapped. If only properties are updated, returns feature(s) with tags updated.
686
+ * @returns Updated features. If geometry is updated, returns new feature(s) converted from geojson and wrapped, with the id the feature already has. If only properties are updated, returns feature(s) with tags updated.
679
687
  */
680
688
  function getUpdatedFeatures(vtFeatures, update, options) {
681
689
  const changeGeometry = !!update.newGeometry;
@@ -691,7 +699,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
691
699
  let features = convertToInternal({
692
700
  type: "FeatureCollection",
693
701
  features: [geojsonFeature]
694
- }, options);
702
+ }, {
703
+ ...options,
704
+ promoteId: void 0,
705
+ generateId: false
706
+ });
695
707
  features = wrap(features, options);
696
708
  return features;
697
709
  }
@@ -728,7 +740,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
728
740
  return {
729
741
  removeAll: diff.removeAll,
730
742
  remove: new Set(diff.remove || []),
731
- add: new Map(diff.add?.map((feature) => [options.promoteId ? feature.properties[options.promoteId] : feature.id, feature])),
743
+ add: new Map(diff.add?.map((feature) => [options.promoteId ? feature.properties?.[options.promoteId] : feature.id, feature])),
732
744
  update: new Map(diff.update?.map((update) => [update.id, update]))
733
745
  };
734
746
  }
@@ -1033,52 +1045,101 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1033
1045
  reduce: null,
1034
1046
  map: (props) => props
1035
1047
  };
1036
- const OFFSET_ZOOM = 2;
1037
- const OFFSET_ID = 3;
1038
- const OFFSET_PARENT = 4;
1039
- const OFFSET_NUM = 5;
1040
- const OFFSET_PROP = 6;
1048
+ /**
1049
+ * The highest zoom level to cluster on. A cluster id keeps its zoom level in five bits.
1050
+ */
1051
+ const MAX_ZOOM = 30;
1052
+ /**
1053
+ * Projected coordinates, which are 0 to 1 inside the world, are stored as integers with this many steps per world,
1054
+ * so that they take half the memory of doubles. One step is ~4 cm at the equator.
1055
+ */
1056
+ const SCALE = 1073741824;
1057
+ /**
1058
+ * The largest value a 32-bit integer holds.
1059
+ */
1060
+ const MAX_INTEGER = 2147483647;
1061
+ /**
1062
+ * A projected coordinate in steps from the center of the world, see `SCALE`.
1063
+ */
1064
+ function encode(coord) {
1065
+ return (coord - .5) * SCALE;
1066
+ }
1067
+ function decode(value) {
1068
+ return value / SCALE + .5;
1069
+ }
1070
+ /**
1071
+ * Whether a projected coordinate fits in a 32-bit integer. One that is more than two worlds from the center of the
1072
+ * world does not, and neither does one that is not a number.
1073
+ */
1074
+ function fitsInteger(coord) {
1075
+ return Math.abs(encode(coord)) < MAX_INTEGER;
1076
+ }
1077
+ const OFFSET_ZOOM = 0;
1078
+ const OFFSET_ID = 1;
1079
+ const OFFSET_PARENT = 2;
1080
+ const OFFSET_NUM = 3;
1081
+ const OFFSET_PROP = 4;
1041
1082
  /**
1042
1083
  * This class allow clustering of geojson points.
1043
1084
  */
1044
1085
  var ClusterTileIndex = class {
1045
- constructor(options) {
1046
- this.options = Object.assign(Object.create(defaultClusterOptions), options);
1086
+ constructor(options, worldCoordinates = false) {
1087
+ this.options = createOptions(options);
1088
+ this.worldCoordinates = worldCoordinates;
1047
1089
  this.trees = new Array(this.options.maxZoom + 1);
1048
- this.stride = this.options.reduce ? 7 : 6;
1090
+ this.stride = this.options.reduce ? 5 : 4;
1049
1091
  this.clusterProps = [];
1050
1092
  this.points = [];
1051
1093
  }
1052
1094
  /**
1053
- * Loads GeoJSON point features and builds the internal clustering index.
1054
- * @param points - GeoJSON point features to cluster.
1095
+ * Loads GeoJSON Point and MultiPoint features and builds the internal clustering index.
1096
+ * Each position of a MultiPoint is clustered as a point of its own, with the id and properties of its feature.
1097
+ * @param points - GeoJSON Point and MultiPoint features to cluster.
1055
1098
  */
1056
1099
  load(points) {
1057
1100
  const features = [];
1058
1101
  for (const point of points) {
1059
1102
  if (!point.geometry) continue;
1060
- const [lng, lat] = point.geometry.coordinates;
1061
- const [x, y] = [projectX(lng), projectY(lat)];
1062
- const feature = {
1063
- id: point.id,
1064
- type: "Point",
1065
- geometry: [x, y],
1066
- tags: point.properties
1067
- };
1068
- features.push(feature);
1103
+ const positions = point.geometry.type === "MultiPoint" ? point.geometry.coordinates : [point.geometry.coordinates];
1104
+ for (const [lng, lat] of positions) {
1105
+ const [x, y] = [projectX(lng, this.worldCoordinates), projectY(lat, this.worldCoordinates)];
1106
+ if (this.worldCoordinates && !isInWorld([x, y])) continue;
1107
+ const feature = {
1108
+ id: point.id,
1109
+ type: "Point",
1110
+ geometry: [x, y],
1111
+ tags: point.properties
1112
+ };
1113
+ features.push(feature);
1114
+ }
1069
1115
  }
1070
1116
  this.createIndex(features);
1071
1117
  }
1072
1118
  /**
1073
1119
  * @internal
1074
1120
  * Loads internal GeoJSONVT point features from a data source and builds the clustering index.
1121
+ * Each position of a MultiPoint feature becomes a point of its own, with the id and properties of its feature.
1075
1122
  * @param features - {@link GeoJSONVTInternalFeature} data source features to filter and cluster.
1076
1123
  */
1077
1124
  initialize(features) {
1078
1125
  const points = [];
1079
1126
  for (const feature of features) {
1080
- if (feature.type !== "Point") continue;
1081
- points.push(feature);
1127
+ if (feature.type === "Point") {
1128
+ if (this.worldCoordinates && !isInWorld(feature.geometry)) continue;
1129
+ points.push(feature);
1130
+ continue;
1131
+ }
1132
+ if (feature.type !== "MultiPoint") continue;
1133
+ for (let i = 0; i < feature.geometry.length; i += 3) {
1134
+ const geometry = [feature.geometry[i], feature.geometry[i + 1]];
1135
+ if (this.worldCoordinates && !isInWorld(geometry)) continue;
1136
+ points.push({
1137
+ id: feature.id,
1138
+ type: "Point",
1139
+ geometry,
1140
+ tags: feature.tags
1141
+ });
1142
+ }
1082
1143
  }
1083
1144
  this.createIndex(points);
1084
1145
  }
@@ -1088,41 +1149,59 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1088
1149
  * @param features
1089
1150
  */
1090
1151
  updateIndex(features, _affected, options) {
1091
- this.options = Object.assign(Object.create(defaultClusterOptions), options.clusterOptions);
1152
+ this.options = createOptions(options.clusterOptions);
1092
1153
  this.initialize(features);
1093
1154
  }
1155
+ /**
1156
+ * Builds a tree per zoom level from the points. A point whose position does not fit in an integer is left out,
1157
+ * see `fitsInteger`.
1158
+ */
1094
1159
  createIndex(points) {
1095
1160
  const { log, minZoom, maxZoom } = this.options;
1161
+ this.trees = new Array(maxZoom + 1);
1162
+ this.stride = this.options.reduce ? 5 : 4;
1163
+ this.clusterProps = [];
1096
1164
  if (log) console.time("total time");
1097
1165
  const timerId = `prepare ${points.length} points`;
1098
1166
  if (log) console.time(timerId);
1099
1167
  this.points = points;
1100
- const data = [];
1168
+ const stride = this.stride;
1169
+ const notProcessed = maxZoom + 1;
1170
+ const coords = new Int32Array(points.length * 2);
1171
+ const data = new Int32Array(points.length * stride);
1172
+ let numItems = 0;
1101
1173
  for (let i = 0; i < points.length; i++) {
1102
1174
  const p = points[i];
1103
- if (!p?.geometry) continue;
1104
- let [x, y] = p.geometry;
1105
- x = Math.fround(x);
1106
- y = Math.fround(y);
1107
- data.push(x, y, Infinity, i, -1, 1);
1108
- if (this.options.reduce) data.push(0);
1175
+ if (!p?.geometry || !fitsInteger(p.geometry[0]) || !fitsInteger(p.geometry[1])) continue;
1176
+ coords[numItems * 2] = Math.round(encode(p.geometry[0]));
1177
+ coords[numItems * 2 + 1] = Math.round(encode(p.geometry[1]));
1178
+ const k = numItems * stride;
1179
+ data[k + OFFSET_ZOOM] = notProcessed;
1180
+ data[k + OFFSET_ID] = i;
1181
+ data[k + OFFSET_PARENT] = -1;
1182
+ data[k + OFFSET_NUM] = 1;
1183
+ numItems++;
1109
1184
  }
1110
- let tree = this.trees[maxZoom + 1] = this.createTree(data);
1185
+ let tree = this.trees[maxZoom + 1] = this.createTree(coords, data, numItems);
1111
1186
  if (log) console.timeEnd(timerId);
1187
+ const nextCoords = new Int32Array(numItems * 2);
1188
+ const nextData = new Int32Array(numItems * stride);
1112
1189
  for (let z = maxZoom; z >= minZoom; z--) {
1113
1190
  const now = Date.now();
1114
- tree = this.trees[z] = this.createTree(this.cluster(tree, z));
1191
+ const nextNumItems = this.cluster(tree, z, nextCoords, nextData);
1192
+ if (!this.isSameAsTree(tree, nextData, nextNumItems)) tree = this.createTree(nextCoords.slice(0, nextNumItems * 2), nextData.slice(0, nextNumItems * stride), nextNumItems);
1193
+ this.trees[z] = tree;
1115
1194
  if (log) console.log("z%d: %d clusters in %dms", z, tree.numItems, Date.now() - now);
1116
1195
  }
1117
1196
  if (log) console.timeEnd("total time");
1118
1197
  }
1119
1198
  /**
1120
1199
  * Returns clusters and/or points within a bounding box at a given zoom level.
1121
- * @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order.
1200
+ * @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order, or `[minX, minY, maxX, maxY]` in world coordinates.
1122
1201
  * @param zoom - Zoom level to query.
1123
1202
  */
1124
1203
  getClusters(bbox, zoom) {
1125
- return this.getClustersInternal(bbox, zoom).map((f) => featureToGeoJSON(f));
1204
+ return (this.worldCoordinates ? this.getClustersInRange(bbox, zoom) : this.getClustersInternal(bbox, zoom)).map((f) => featureToGeoJSON(f, this.worldCoordinates));
1126
1205
  }
1127
1206
  getClustersInternal(bbox, zoom) {
1128
1207
  let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
@@ -1147,13 +1226,25 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1147
1226
  ], zoom);
1148
1227
  return easternHem.concat(westernHem);
1149
1228
  }
1229
+ return this.getClustersInRange([
1230
+ projectX(minLng),
1231
+ projectY(maxLat),
1232
+ projectX(maxLng),
1233
+ projectY(minLat)
1234
+ ], zoom);
1235
+ }
1236
+ /**
1237
+ * The clusters and points in a box of the internal coordinates, `[minX, minY, maxX, maxY]`.
1238
+ */
1239
+ getClustersInRange([minX, minY, maxX, maxY], zoom) {
1150
1240
  const tree = this.trees[this.limitZoom(zoom)];
1151
- const ids = tree.range(projectX(minLng), projectY(maxLat), projectX(maxLng), projectY(minLat));
1241
+ const ids = tree.range(encode(minX), encode(minY), encode(maxX), encode(maxY));
1242
+ const coords = tree.flatCoords;
1152
1243
  const data = tree.flatData;
1153
1244
  const clusters = [];
1154
1245
  for (const id of ids) {
1155
1246
  const k = this.stride * id;
1156
- clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
1247
+ clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(coords, id, data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
1157
1248
  }
1158
1249
  return clusters;
1159
1250
  }
@@ -1167,16 +1258,17 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1167
1258
  const clusterError = /* @__PURE__ */ new Error("No cluster with the specified id: " + clusterId);
1168
1259
  const tree = this.trees[originZoom];
1169
1260
  if (!tree) throw clusterError;
1261
+ const coords = tree.flatCoords;
1170
1262
  const data = tree.flatData;
1171
- if (originId * this.stride >= data.length) throw clusterError;
1172
- const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
1173
- const x = data[originId * this.stride];
1174
- const y = data[originId * this.stride + 1];
1263
+ if (originId >= tree.numItems) throw clusterError;
1264
+ const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1)) * SCALE;
1265
+ const x = coords[originId * 2];
1266
+ const y = coords[originId * 2 + 1];
1175
1267
  const ids = tree.within(x, y, r);
1176
1268
  const children = [];
1177
1269
  for (const id of ids) {
1178
1270
  const k = id * this.stride;
1179
- if (data[k + OFFSET_PARENT] === clusterId) children.push(data[k + OFFSET_NUM] > 1 ? getClusterGeoJSON(data, k, this.clusterProps) : featureToGeoJSON(this.points[data[k + OFFSET_ID]]));
1271
+ 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));
1180
1272
  }
1181
1273
  if (children.length === 0) throw clusterError;
1182
1274
  return children;
@@ -1204,10 +1296,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1204
1296
  const tree = this.trees[this.limitZoom(z)];
1205
1297
  if (!tree) return null;
1206
1298
  const z2 = Math.pow(2, z);
1299
+ if (this.worldCoordinates && (x < 0 || x >= z2 || y < 0 || y >= z2)) return null;
1207
1300
  const { extent, radius } = this.options;
1208
1301
  const p = radius / extent;
1209
- const top = (y - p) / z2;
1210
- const bottom = (y + 1 + p) / z2;
1302
+ const top = encode((y - p) / z2);
1303
+ const bottom = encode((y + 1 + p) / z2);
1211
1304
  const tile = {
1212
1305
  transformed: true,
1213
1306
  features: [],
@@ -1216,9 +1309,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1216
1309
  y,
1217
1310
  z
1218
1311
  };
1219
- this.addTileFeatures(tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), tree.flatData, x, y, z2, tile);
1220
- if (x === 0) this.addTileFeatures(tree.range(1 - p / z2, top, 1, bottom), tree.flatData, z2, y, z2, tile);
1221
- if (x === z2 - 1) this.addTileFeatures(tree.range(0, top, p / z2, bottom), tree.flatData, -1, y, z2, tile);
1312
+ this.addTileFeatures(tree.range(encode((x - p) / z2), top, encode((x + 1 + p) / z2), bottom), tree, x, y, z2, tile);
1313
+ if (!this.worldCoordinates && x === 0) this.addTileFeatures(tree.range(encode(1 - p / z2), top, encode(1), bottom), tree, z2, y, z2, tile);
1314
+ if (!this.worldCoordinates && x === z2 - 1) this.addTileFeatures(tree.range(encode(0), top, encode(p / z2), bottom), tree, -1, y, z2, tile);
1222
1315
  return tile;
1223
1316
  }
1224
1317
  /**
@@ -1241,15 +1334,27 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1241
1334
  }
1242
1335
  return skipped;
1243
1336
  }
1244
- createTree(data) {
1245
- const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array);
1246
- for (let i = 0; i < data.length; i += this.stride) tree.add(data[i], data[i + 1]);
1337
+ createTree(coords, data, numItems) {
1338
+ const tree = new KDBush(numItems, this.options.nodeSize, Int32Array);
1339
+ for (let i = 0; i < numItems; i++) tree.add(coords[i * 2], coords[i * 2 + 1]);
1247
1340
  tree.finish();
1341
+ tree.flatCoords = coords;
1248
1342
  tree.flatData = data;
1249
1343
  tree.data = null;
1250
1344
  return tree;
1251
1345
  }
1252
- addTileFeatures(ids, data, x, y, z2, tile) {
1346
+ /**
1347
+ * Whether clustering a tree left all its points and clusters as they were, in the same order.
1348
+ */
1349
+ isSameAsTree(tree, nextData, nextNumItems) {
1350
+ if (nextNumItems !== tree.numItems) return false;
1351
+ const data = tree.flatData;
1352
+ for (let k = OFFSET_ID; k < nextNumItems * this.stride; k += this.stride) if (nextData[k] !== data[k]) return false;
1353
+ return true;
1354
+ }
1355
+ addTileFeatures(ids, tree, x, y, z2, tile) {
1356
+ const coords = tree.flatCoords;
1357
+ const data = tree.flatData;
1253
1358
  for (const i of ids) {
1254
1359
  const k = i * this.stride;
1255
1360
  const isCluster = data[k + OFFSET_NUM] > 1;
@@ -1258,8 +1363,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1258
1363
  let py;
1259
1364
  if (isCluster) {
1260
1365
  tags = getClusterProperties(data, k, this.clusterProps);
1261
- px = data[k];
1262
- py = data[k + 1];
1366
+ px = decode(coords[i * 2]);
1367
+ py = decode(coords[i * 2 + 1]);
1263
1368
  } else {
1264
1369
  const p = this.points[data[k + OFFSET_ID]];
1265
1370
  tags = p.tags;
@@ -1280,22 +1385,30 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1280
1385
  limitZoom(z) {
1281
1386
  return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
1282
1387
  }
1283
- cluster(tree, zoom) {
1284
- const { radius, extent, reduce, minPoints } = this.options;
1285
- const r = radius / (extent * Math.pow(2, zoom));
1388
+ /**
1389
+ * Clusters the points of a tree on a zoom level, writing the result to `nextCoords` and `nextData`.
1390
+ * @returns the number of points and clusters written
1391
+ */
1392
+ cluster(tree, zoom, nextCoords, nextData) {
1393
+ const { radius, extent, reduce, minPoints, maxZoom } = this.options;
1394
+ const r = radius / (extent * Math.pow(2, zoom)) * SCALE;
1395
+ const notProcessed = maxZoom + 1;
1396
+ const coords = tree.flatCoords;
1286
1397
  const data = tree.flatData;
1287
- const nextData = [];
1288
1398
  const stride = this.stride;
1289
- for (let i = 0; i < data.length; i += stride) {
1399
+ const neighborIds = new Uint32Array(tree.numItems);
1400
+ let nextNumItems = 0;
1401
+ for (let index = 0; index < tree.numItems; index++) {
1402
+ const i = index * stride;
1290
1403
  if (data[i + OFFSET_ZOOM] <= zoom) continue;
1291
1404
  data[i + OFFSET_ZOOM] = zoom;
1292
- const x = data[i];
1293
- const y = data[i + 1];
1294
- const neighborIds = tree.within(data[i], data[i + 1], r);
1405
+ const x = coords[index * 2];
1406
+ const y = coords[index * 2 + 1];
1407
+ const numNeighbors = tree.withinInto(x, y, r, neighborIds);
1295
1408
  const numPointsOrigin = data[i + OFFSET_NUM];
1296
1409
  let numPoints = numPointsOrigin;
1297
- for (const neighborId of neighborIds) {
1298
- const k = neighborId * stride;
1410
+ for (let n = 0; n < numNeighbors; n++) {
1411
+ const k = neighborIds[n] * stride;
1299
1412
  if (data[k + OFFSET_ZOOM] > zoom) numPoints += data[k + OFFSET_NUM];
1300
1413
  }
1301
1414
  if (numPoints > numPointsOrigin && numPoints >= minPoints) {
@@ -1303,14 +1416,15 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1303
1416
  let wy = y * numPointsOrigin;
1304
1417
  let clusterProperties;
1305
1418
  let clusterPropIndex = -1;
1306
- const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length;
1307
- for (const neighborId of neighborIds) {
1308
- const k = neighborId * stride;
1419
+ const id = (index << 5) + (zoom + 1) + this.points.length;
1420
+ for (let n = 0; n < numNeighbors; n++) {
1421
+ const neighborIndex = neighborIds[n];
1422
+ const k = neighborIndex * stride;
1309
1423
  if (data[k + OFFSET_ZOOM] <= zoom) continue;
1310
1424
  data[k + OFFSET_ZOOM] = zoom;
1311
1425
  const numPoints2 = data[k + OFFSET_NUM];
1312
- wx += data[k] * numPoints2;
1313
- wy += data[k + 1] * numPoints2;
1426
+ wx += coords[neighborIndex * 2] * numPoints2;
1427
+ wy += coords[neighborIndex * 2 + 1] * numPoints2;
1314
1428
  data[k + OFFSET_PARENT] = id;
1315
1429
  if (reduce) {
1316
1430
  if (!clusterProperties) {
@@ -1322,19 +1436,33 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1322
1436
  }
1323
1437
  }
1324
1438
  data[i + OFFSET_PARENT] = id;
1325
- nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints);
1326
- if (reduce) nextData.push(clusterPropIndex);
1439
+ nextCoords[nextNumItems * 2] = Math.round(wx / numPoints);
1440
+ nextCoords[nextNumItems * 2 + 1] = Math.round(wy / numPoints);
1441
+ const next = nextNumItems * stride;
1442
+ nextData[next + OFFSET_ZOOM] = notProcessed;
1443
+ nextData[next + OFFSET_ID] = id;
1444
+ nextData[next + OFFSET_PARENT] = -1;
1445
+ nextData[next + OFFSET_NUM] = numPoints;
1446
+ if (reduce) nextData[next + OFFSET_PROP] = clusterPropIndex;
1447
+ nextNumItems++;
1327
1448
  } else {
1328
- for (let j = 0; j < stride; j++) nextData.push(data[i + j]);
1329
- if (numPoints > 1) for (const neighborId of neighborIds) {
1330
- const k = neighborId * stride;
1449
+ nextCoords[nextNumItems * 2] = x;
1450
+ nextCoords[nextNumItems * 2 + 1] = y;
1451
+ for (let j = 0; j < stride; j++) nextData[nextNumItems * stride + j] = data[i + j];
1452
+ nextNumItems++;
1453
+ if (numPoints > 1) for (let n = 0; n < numNeighbors; n++) {
1454
+ const neighborIndex = neighborIds[n];
1455
+ const k = neighborIndex * stride;
1331
1456
  if (data[k + OFFSET_ZOOM] <= zoom) continue;
1332
1457
  data[k + OFFSET_ZOOM] = zoom;
1333
- for (let j = 0; j < stride; j++) nextData.push(data[k + j]);
1458
+ nextCoords[nextNumItems * 2] = coords[neighborIndex * 2];
1459
+ nextCoords[nextNumItems * 2 + 1] = coords[neighborIndex * 2 + 1];
1460
+ for (let j = 0; j < stride; j++) nextData[nextNumItems * stride + j] = data[k + j];
1461
+ nextNumItems++;
1334
1462
  }
1335
1463
  }
1336
1464
  }
1337
- return nextData;
1465
+ return nextNumItems;
1338
1466
  }
1339
1467
  getOriginId(clusterId) {
1340
1468
  return clusterId - this.points.length >> 5;
@@ -1352,22 +1480,28 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1352
1480
  return clone && result === original ? Object.assign({}, result) : result;
1353
1481
  }
1354
1482
  };
1355
- function getClusterFeature(data, i, clusterProps) {
1483
+ function createOptions(options) {
1484
+ const result = Object.assign(Object.create(defaultClusterOptions), options);
1485
+ result.minZoom = Math.min(result.minZoom, MAX_ZOOM);
1486
+ result.maxZoom = Math.min(result.maxZoom, MAX_ZOOM);
1487
+ return result;
1488
+ }
1489
+ function getClusterFeature(coords, index, data, i, clusterProps) {
1356
1490
  return {
1357
1491
  id: data[i + OFFSET_ID],
1358
1492
  type: "Point",
1359
1493
  tags: getClusterProperties(data, i, clusterProps),
1360
- geometry: [data[i], data[i + 1]]
1494
+ geometry: [decode(coords[index * 2]), decode(coords[index * 2 + 1])]
1361
1495
  };
1362
1496
  }
1363
- function getClusterGeoJSON(data, i, clusterProps) {
1497
+ function getClusterGeoJSON(coords, index, data, i, clusterProps, worldCoordinates) {
1364
1498
  return {
1365
1499
  type: "Feature",
1366
1500
  id: data[i + OFFSET_ID],
1367
1501
  properties: getClusterProperties(data, i, clusterProps),
1368
1502
  geometry: {
1369
1503
  type: "Point",
1370
- coordinates: [unprojectX(data[i]), unprojectY(data[i + 1])]
1504
+ coordinates: [unprojectX(decode(coords[index * 2]), worldCoordinates), unprojectY(decode(coords[index * 2 + 1]), worldCoordinates)]
1371
1505
  }
1372
1506
  };
1373
1507
  }
@@ -1383,6 +1517,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1383
1517
  point_count_abbreviated: abbrev
1384
1518
  });
1385
1519
  }
1520
+ function isInWorld(geometry) {
1521
+ return geometry[0] >= 0 && geometry[0] <= 1 && geometry[1] >= 0 && geometry[1] <= 1;
1522
+ }
1386
1523
 
1387
1524
  //#endregion
1388
1525
  //#region src/tile.ts
@@ -1594,7 +1731,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1594
1731
  this.total = 0;
1595
1732
  }
1596
1733
  initialize(features) {
1597
- this.splitTile(features, 0, 0, 0);
1734
+ this.splitTile(this.clipToWorld(features), 0, 0, 0);
1598
1735
  if (this.options.debug) {
1599
1736
  if (features.length) console.log("features: %d, points: %d", this.tiles[0].numFeatures, this.tiles[0].numPoints);
1600
1737
  console.timeEnd("generate tiles");
@@ -1614,8 +1751,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1614
1751
  0,
1615
1752
  0
1616
1753
  ];
1617
- const rootTile = createTile(source, z, x, y, options);
1618
- rootTile.source = source;
1754
+ const rootSource = this.clipToWorld(source);
1755
+ const rootTile = createTile(rootSource, z, x, y, options);
1756
+ rootTile.source = rootSource;
1619
1757
  const id = toID(z, x, y);
1620
1758
  this.tiles[id] = rootTile;
1621
1759
  this.tileCoords.push({
@@ -1646,6 +1784,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1646
1784
  getTile(z, x, y) {
1647
1785
  const { extent, debug } = this.options;
1648
1786
  const z2 = 1 << z;
1787
+ if (this.options.worldCoordinates && (x < 0 || x >= z2)) return null;
1649
1788
  x = x + z2 & z2 - 1;
1650
1789
  const id = toID(z, x, y);
1651
1790
  if (this.tiles[id]) return transformTile(this.tiles[id], extent);
@@ -1671,6 +1810,17 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1671
1810
  return transformTile(this.tiles[id], extent);
1672
1811
  }
1673
1812
  /**
1813
+ * The features the top tile starts from: all of them in a wrapping world, where wrapping already put them inside,
1814
+ * and only what lies inside the world square and its buffer with `worldCoordinates`. The index keeps the rest, so
1815
+ * an update can still move a feature in from outside.
1816
+ */
1817
+ clipToWorld(features) {
1818
+ if (!this.options.worldCoordinates) return features;
1819
+ const buffer = this.options.buffer / this.options.extent;
1820
+ const inX = clip(features, 1, -buffer, 1 + buffer, 0, -Infinity, Infinity, this.options) || [];
1821
+ return clip(inX, 1, -buffer, 1 + buffer, 1, -Infinity, Infinity, this.options) || [];
1822
+ }
1823
+ /**
1674
1824
  * splits features from a parent tile to sub-tiles.
1675
1825
  * z, x, and y are the coordinates of the parent tile
1676
1826
  * cz, cx, and cy are the coordinates of the target tile
@@ -1824,7 +1974,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1824
1974
  updateable: false,
1825
1975
  cluster: false,
1826
1976
  clusterOptions: defaultClusterOptions,
1827
- debug: 0
1977
+ debug: 0,
1978
+ worldCoordinates: false
1828
1979
  };
1829
1980
  /**
1830
1981
  * Main class for creating and managing a vector tile index from GeoJSON data.
@@ -1847,7 +1998,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1847
1998
  this.initializeIndex(features, options);
1848
1999
  }
1849
2000
  initializeIndex(features, options) {
1850
- this.tileIndex = options.cluster ? new ClusterTileIndex(options.clusterOptions) : new TileIndex(options);
2001
+ this.tileIndex = options.cluster ? new ClusterTileIndex(options.clusterOptions, options.worldCoordinates) : new TileIndex(options);
1851
2002
  if (!features.length) return;
1852
2003
  this.tileIndex.initialize(features);
1853
2004
  }
@@ -1885,7 +2036,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1885
2036
  const removeIds = /* @__PURE__ */ new Set();
1886
2037
  for (const feature of source) {
1887
2038
  if (feature.id == void 0) continue;
1888
- if (predicate(featureToGeoJSON(feature))) continue;
2039
+ if (predicate(featureToGeoJSON(feature, this.options.worldCoordinates))) continue;
1889
2040
  affected.push(feature);
1890
2041
  removeIds.add(feature.id);
1891
2042
  }
@@ -1900,7 +2051,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1900
2051
  */
1901
2052
  getData() {
1902
2053
  if (!this.options.updateable) throw new Error("to retrieve data the `updateable` option must be set to true");
1903
- return convertToGeoJSON(this.source);
2054
+ return convertToGeoJSON(this.source, this.options.worldCoordinates);
1904
2055
  }
1905
2056
  /**
1906
2057
  * Update supercluster options and regenerate the index.