@maplibre/geojson-vt 5.0.3 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +3 -13
  2. package/dist/clip.d.ts +22 -0
  3. package/dist/clip.d.ts.map +1 -0
  4. package/dist/clip.test.d.ts +2 -0
  5. package/dist/clip.test.d.ts.map +1 -0
  6. package/dist/cluster-tile-index.d.ts +76 -0
  7. package/dist/cluster-tile-index.d.ts.map +1 -0
  8. package/dist/cluster-tile-index.test.d.ts +2 -0
  9. package/dist/cluster-tile-index.test.d.ts.map +1 -0
  10. package/dist/convert.d.ts +17 -0
  11. package/dist/convert.d.ts.map +1 -0
  12. package/dist/deconvert.d.ts +19 -0
  13. package/dist/deconvert.d.ts.map +1 -0
  14. package/dist/deconvert.test.d.ts +2 -0
  15. package/dist/deconvert.test.d.ts.map +1 -0
  16. package/dist/definitions.d.ts +241 -0
  17. package/dist/definitions.d.ts.map +1 -0
  18. package/dist/difference.d.ts +67 -0
  19. package/dist/difference.d.ts.map +1 -0
  20. package/dist/difference.test.d.ts +2 -0
  21. package/dist/difference.test.d.ts.map +1 -0
  22. package/dist/feature.d.ts +20 -0
  23. package/dist/feature.d.ts.map +1 -0
  24. package/dist/geojson-to-tile.d.ts +35 -0
  25. package/dist/geojson-to-tile.d.ts.map +1 -0
  26. package/dist/geojson-vt-dev.js +1582 -478
  27. package/dist/geojson-vt.js +1 -1
  28. package/dist/geojson-vt.mjs +1250 -473
  29. package/dist/geojson-vt.mjs.map +1 -1
  30. package/dist/geojsonvt.d.ts +76 -0
  31. package/dist/geojsonvt.d.ts.map +1 -0
  32. package/dist/geojsonvt.test.d.ts +2 -0
  33. package/dist/geojsonvt.test.d.ts.map +1 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/simplify.d.ts +9 -0
  37. package/dist/simplify.d.ts.map +1 -0
  38. package/dist/simplify.test.d.ts +2 -0
  39. package/dist/simplify.test.d.ts.map +1 -0
  40. package/dist/tile-index.d.ts +51 -0
  41. package/dist/tile-index.d.ts.map +1 -0
  42. package/dist/tile.d.ts +12 -0
  43. package/dist/tile.d.ts.map +1 -0
  44. package/dist/transform.d.ts +10 -0
  45. package/dist/transform.d.ts.map +1 -0
  46. package/dist/wrap.d.ts +3 -0
  47. package/dist/wrap.d.ts.map +1 -0
  48. package/package.json +26 -12
  49. package/src/clip.ts +119 -81
  50. package/src/cluster-tile-index.test.ts +205 -0
  51. package/src/cluster-tile-index.ts +513 -0
  52. package/src/convert.ts +97 -75
  53. package/src/deconvert.test.ts +153 -0
  54. package/src/deconvert.ts +92 -0
  55. package/src/definitions.ts +196 -18
  56. package/src/difference.ts +3 -3
  57. package/src/feature.ts +11 -4
  58. package/src/geojson-to-tile.ts +58 -0
  59. package/src/geojsonvt.test.ts +39 -0
  60. package/src/geojsonvt.ts +209 -0
  61. package/src/index.ts +27 -378
  62. package/src/tile-index.ts +310 -0
  63. package/src/tile.ts +92 -103
  64. package/src/transform.ts +41 -39
  65. package/src/wrap.ts +4 -4
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3
- typeof define === 'function' && define.amd ? define(factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.geojsonvt = factory());
5
- })(this, (function () { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.geojsonvt = {}));
5
+ })(this, (function (exports) { 'use strict';
6
6
 
7
7
  /**
8
8
  * calculate simplification data using optimized Douglas-Peucker algorithm
@@ -130,109 +130,125 @@ function calcLineBBox(feature, geom) {
130
130
  }
131
131
 
132
132
  /**
133
- * converts GeoJSON feature into an intermediate projected JSON vector format with simplification data
133
+ * converts GeoJSON to internal source features (an intermediate projected JSON vector format with simplification data)
134
134
  * @param data
135
135
  * @param options
136
136
  * @returns
137
137
  */
138
- function convert(data, options) {
138
+ function convertToInternal(data, options) {
139
139
  const features = [];
140
140
  switch (data.type) {
141
141
  case 'FeatureCollection':
142
142
  for (let i = 0; i < data.features.length; i++) {
143
- convertFeature(features, data.features[i], options, i);
143
+ featureToInternal(features, data.features[i], options, i);
144
144
  }
145
145
  break;
146
146
  case 'Feature':
147
- convertFeature(features, data, options);
147
+ featureToInternal(features, data, options);
148
148
  break;
149
149
  default:
150
- convertFeature(features, { geometry: data, properties: undefined }, options);
150
+ featureToInternal(features, { geometry: data, properties: undefined }, options);
151
151
  }
152
152
  return features;
153
153
  }
154
- function convertFeature(features, geojson, options, index) {
154
+ function featureToInternal(features, geojson, options, index) {
155
155
  if (!geojson.geometry)
156
156
  return;
157
157
  if (geojson.geometry.type === 'GeometryCollection') {
158
- for (const singleGeometry of geojson.geometry.geometries) {
159
- convertFeature(features, {
160
- id: geojson.id,
161
- geometry: singleGeometry,
162
- properties: geojson.properties
163
- }, options, index);
164
- }
158
+ convertGeometryCollection(features, geojson, geojson.geometry, options, index);
165
159
  return;
166
160
  }
167
161
  const coords = geojson.geometry.coordinates;
168
162
  if (!coords?.length)
169
163
  return;
164
+ const id = getFeatureId(geojson, options, index);
170
165
  const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
171
- let id = geojson.id;
172
- if (options.promoteId) {
173
- id = geojson.properties?.[options.promoteId];
174
- }
175
- else if (options.generateId) {
176
- id = index || 0;
177
- }
178
166
  switch (geojson.geometry.type) {
179
- case 'Point': {
180
- const pointGeometry = [];
181
- convertPoint(geojson.geometry.coordinates, pointGeometry);
182
- features.push(createFeature(id, geojson.geometry.type, pointGeometry, geojson.properties));
167
+ case 'Point':
168
+ convertPointFeature(features, id, geojson.geometry, geojson.properties);
183
169
  return;
184
- }
185
- case 'MultiPoint': {
186
- const multiPointGeometry = [];
187
- for (const p of geojson.geometry.coordinates) {
188
- convertPoint(p, multiPointGeometry);
189
- }
190
- features.push(createFeature(id, geojson.geometry.type, multiPointGeometry, geojson.properties));
170
+ case 'MultiPoint':
171
+ convertMultiPointFeature(features, id, geojson.geometry, geojson.properties);
191
172
  return;
192
- }
193
- case 'LineString': {
194
- const lineGeometry = [];
195
- convertLine(geojson.geometry.coordinates, lineGeometry, tolerance, false);
196
- features.push(createFeature(id, geojson.geometry.type, lineGeometry, geojson.properties));
173
+ case 'LineString':
174
+ convertLineStringFeature(features, id, geojson.geometry, tolerance, geojson.properties);
197
175
  return;
198
- }
199
- case 'MultiLineString': {
200
- if (options.lineMetrics) {
201
- // explode into linestrings in order to track metrics
202
- for (const line of geojson.geometry.coordinates) {
203
- const lineGeometry = [];
204
- convertLine(line, lineGeometry, tolerance, false);
205
- features.push(createFeature(id, 'LineString', lineGeometry, geojson.properties));
206
- }
207
- return;
208
- }
209
- const multiLineGeometry = [];
210
- convertLines(geojson.geometry.coordinates, multiLineGeometry, tolerance, false);
211
- features.push(createFeature(id, geojson.geometry.type, multiLineGeometry, geojson.properties));
176
+ case 'MultiLineString':
177
+ convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, options, geojson.properties);
212
178
  return;
213
- }
214
- case 'Polygon': {
215
- const polygonGeometry = [];
216
- convertLines(geojson.geometry.coordinates, polygonGeometry, tolerance, true);
217
- features.push(createFeature(id, geojson.geometry.type, polygonGeometry, geojson.properties));
179
+ case 'Polygon':
180
+ convertPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
218
181
  return;
219
- }
220
- case 'MultiPolygon': {
221
- const multiPolygonGeometry = [];
222
- for (const polygon of geojson.geometry.coordinates) {
223
- const newPolygon = [];
224
- convertLines(polygon, newPolygon, tolerance, true);
225
- multiPolygonGeometry.push(newPolygon);
226
- }
227
- features.push(createFeature(id, geojson.geometry.type, multiPolygonGeometry, geojson.properties));
182
+ case 'MultiPolygon':
183
+ convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
228
184
  return;
229
- }
230
185
  default:
231
186
  throw new Error('Input data is not a valid GeoJSON object.');
232
187
  }
233
188
  }
234
- function convertPoint(coords, out) {
235
- out.push(projectX(coords[0]), projectY(coords[1]), 0);
189
+ function getFeatureId(geojson, options, index) {
190
+ if (options.promoteId) {
191
+ return geojson.properties?.[options.promoteId];
192
+ }
193
+ if (options.generateId) {
194
+ return index || 0;
195
+ }
196
+ return geojson.id;
197
+ }
198
+ function convertGeometryCollection(features, geojson, geometry, options, index) {
199
+ for (const geom of geometry.geometries) {
200
+ featureToInternal(features, {
201
+ id: geojson.id,
202
+ geometry: geom,
203
+ properties: geojson.properties
204
+ }, options, index);
205
+ }
206
+ }
207
+ function convertPointFeature(features, id, geom, properties) {
208
+ const out = [];
209
+ out.push(projectX(geom.coordinates[0]), projectY(geom.coordinates[1]), 0);
210
+ features.push(createFeature(id, 'Point', out, properties));
211
+ }
212
+ function convertMultiPointFeature(features, id, geom, properties) {
213
+ const out = [];
214
+ for (const coords of geom.coordinates) {
215
+ out.push(projectX(coords[0]), projectY(coords[1]), 0);
216
+ }
217
+ features.push(createFeature(id, 'MultiPoint', out, properties));
218
+ }
219
+ function convertLineStringFeature(features, id, geom, tolerance, properties) {
220
+ const out = [];
221
+ convertLine(geom.coordinates, out, tolerance, false);
222
+ features.push(createFeature(id, 'LineString', out, properties));
223
+ }
224
+ function convertMultiLineStringFeature(features, id, geom, tolerance, options, properties) {
225
+ if (options.lineMetrics) {
226
+ // explode into linestrings to be able to track metrics
227
+ for (const line of geom.coordinates) {
228
+ const out = [];
229
+ convertLine(line, out, tolerance, false);
230
+ features.push(createFeature(id, 'LineString', out, properties));
231
+ }
232
+ }
233
+ else {
234
+ const out = [];
235
+ convertLines(geom.coordinates, out, tolerance, false);
236
+ features.push(createFeature(id, 'MultiLineString', out, properties));
237
+ }
238
+ }
239
+ function convertPolygonFeature(features, id, geom, tolerance, properties) {
240
+ const out = [];
241
+ convertLines(geom.coordinates, out, tolerance, true);
242
+ features.push(createFeature(id, 'Polygon', out, properties));
243
+ }
244
+ function convertMultiPolygonFeature(features, id, geom, tolerance, properties) {
245
+ const out = [];
246
+ for (const polygon of geom.coordinates) {
247
+ const polygonOut = [];
248
+ convertLines(polygon, polygonOut, tolerance, true);
249
+ out.push(polygonOut);
250
+ }
251
+ features.push(createFeature(id, 'MultiPolygon', out, properties));
236
252
  }
237
253
  function convertLine(ring, out, tolerance, isPolygon) {
238
254
  let x0, y0;
@@ -268,105 +284,159 @@ function convertLines(rings, out, tolerance, isPolygon) {
268
284
  out.push(geom);
269
285
  }
270
286
  }
287
+ /**
288
+ * Convert longitude to spherical mercator in [0..1] range
289
+ */
271
290
  function projectX(x) {
272
291
  return x / 360 + 0.5;
273
292
  }
293
+ /**
294
+ * Convert latitude to spherical mercator in [0..1] range
295
+ */
274
296
  function projectY(y) {
275
297
  const sin = Math.sin(y * Math.PI / 180);
276
298
  const y2 = 0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
277
299
  return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
278
300
  }
279
301
 
280
- /* clip features between two vertical or horizontal axis-parallel lines:
302
+ /**
303
+ * Converts internal source features back to GeoJSON format.
304
+ */
305
+ function convertToGeoJSON(source) {
306
+ const geojson = {
307
+ type: 'FeatureCollection',
308
+ features: source.map(feature => featureToGeoJSON(feature))
309
+ };
310
+ return geojson;
311
+ }
312
+ /**
313
+ * Converts a single internal feature to GeoJSON format.
314
+ */
315
+ function featureToGeoJSON(feature) {
316
+ const geojsonFeature = {
317
+ type: 'Feature',
318
+ geometry: geometryToGeoJSON(feature),
319
+ properties: feature.tags
320
+ };
321
+ if (feature.id != null) {
322
+ geojsonFeature.id = feature.id;
323
+ }
324
+ return geojsonFeature;
325
+ }
326
+ /**
327
+ * Converts a single internal feature geometry to GeoJSON format.
328
+ */
329
+ function geometryToGeoJSON(feature) {
330
+ const { type, geometry } = feature;
331
+ switch (type) {
332
+ case 'Point':
333
+ return {
334
+ type: type,
335
+ coordinates: unprojectPoint(geometry[0], geometry[1])
336
+ };
337
+ case 'MultiPoint':
338
+ case 'LineString':
339
+ return {
340
+ type: type,
341
+ coordinates: unprojectPoints(geometry)
342
+ };
343
+ case 'MultiLineString':
344
+ case 'Polygon':
345
+ return {
346
+ type: type,
347
+ coordinates: geometry.map(ring => unprojectPoints(ring))
348
+ };
349
+ case 'MultiPolygon':
350
+ return {
351
+ type: type,
352
+ coordinates: geometry.map(polygon => polygon.map(ring => unprojectPoints(ring)))
353
+ };
354
+ }
355
+ }
356
+ function unprojectPoints(coords) {
357
+ const result = [];
358
+ for (let i = 0; i < coords.length; i += 3) {
359
+ result.push(unprojectPoint(coords[i], coords[i + 1]));
360
+ }
361
+ return result;
362
+ }
363
+ function unprojectPoint(x, y) {
364
+ return [unprojectX(x), unprojectY(y)];
365
+ }
366
+ /**
367
+ * Convert spherical mercator in [0..1] range to longitude
368
+ */
369
+ function unprojectX(x) {
370
+ return (x - 0.5) * 360;
371
+ }
372
+ /**
373
+ * Convert spherical mercator in [0..1] range to latitude
374
+ */
375
+ function unprojectY(y) {
376
+ const y2 = (180 - y * 360) * Math.PI / 180;
377
+ return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
378
+ }
379
+
380
+ var AxisType;
381
+ (function (AxisType) {
382
+ AxisType[AxisType["X"] = 0] = "X";
383
+ AxisType[AxisType["Y"] = 1] = "Y";
384
+ })(AxisType || (AxisType = {}));
385
+ /**
386
+ * clip features between two vertical or horizontal axis-parallel lines:
281
387
  * | |
282
388
  * ___|___ | /
283
389
  * / | \____|____/
284
390
  * | |
285
391
  *
286
- * k1 and k2 are the line coordinates
287
- * axis: 0 for x, 1 for y
288
- * minAll and maxAll: minimum and maximum coordinate value for all features
392
+ * @param features - the features to clip
393
+ * @param scale - the scale to divide start and end inputs
394
+ * @param start - the start of the clip range
395
+ * @param end - the end of the clip range
396
+ * @param axis - which axis to clip against
397
+ * @param minAll - the minimum for all features in the relevant axis
398
+ * @param maxAll - the maximum for all features in the relevant axis
289
399
  */
290
- function clip(features, scale, k1, k2, axis, minAll, maxAll, options) {
291
- k1 /= scale;
292
- k2 /= scale;
293
- if (minAll >= k1 && maxAll < k2) { // trivial accept
400
+ function clip(features, scale, start, end, axis, minAll, maxAll, options) {
401
+ start /= scale;
402
+ end /= scale;
403
+ if (minAll >= start && maxAll < end) { // trivial accept
294
404
  return features;
295
405
  }
296
- if (maxAll < k1 || minAll >= k2) { // trivial reject
406
+ if (maxAll < start || minAll >= end) { // trivial reject
297
407
  return null;
298
408
  }
299
409
  const clipped = [];
300
410
  for (const feature of features) {
301
- const min = axis === 0 ? feature.minX : feature.minY;
302
- const max = axis === 0 ? feature.maxX : feature.maxY;
303
- if (min >= k1 && max < k2) { // trivial accept
411
+ const min = axis === AxisType.X ? feature.minX : feature.minY;
412
+ const max = axis === AxisType.X ? feature.maxX : feature.maxY;
413
+ if (min >= start && max < end) { // trivial accept
304
414
  clipped.push(feature);
305
415
  continue;
306
416
  }
307
- if (max < k1 || min >= k2) { // trivial reject
417
+ if (max < start || min >= end) { // trivial reject
308
418
  continue;
309
419
  }
310
420
  switch (feature.type) {
311
421
  case 'Point':
312
422
  case 'MultiPoint': {
313
- const pointGeometry = [];
314
- clipPoints(feature.geometry, pointGeometry, k1, k2, axis);
315
- if (!pointGeometry.length)
316
- continue;
317
- const type = pointGeometry.length === 3 ? 'Point' : 'MultiPoint';
318
- clipped.push(createFeature(feature.id, type, pointGeometry, feature.tags));
423
+ clipPointFeature(feature, clipped, start, end, axis);
319
424
  continue;
320
425
  }
321
426
  case 'LineString': {
322
- const lineGeometry = [];
323
- clipLine(feature.geometry, lineGeometry, k1, k2, axis, false, options.lineMetrics);
324
- if (!lineGeometry.length)
325
- continue;
326
- if (options.lineMetrics) {
327
- for (const line of lineGeometry) {
328
- clipped.push(createFeature(feature.id, feature.type, line, feature.tags));
329
- }
330
- continue;
331
- }
332
- if (lineGeometry.length > 1) {
333
- clipped.push(createFeature(feature.id, "MultiLineString", lineGeometry, feature.tags));
334
- continue;
335
- }
336
- clipped.push(createFeature(feature.id, feature.type, lineGeometry[0], feature.tags));
427
+ clipLineStringFeature(feature, clipped, start, end, axis, options);
337
428
  continue;
338
429
  }
339
430
  case 'MultiLineString': {
340
- const multiLineGeometry = [];
341
- clipLines(feature.geometry, multiLineGeometry, k1, k2, axis, false);
342
- if (!multiLineGeometry.length)
343
- continue;
344
- if (multiLineGeometry.length === 1) {
345
- clipped.push(createFeature(feature.id, "LineString", multiLineGeometry[0], feature.tags));
346
- continue;
347
- }
348
- clipped.push(createFeature(feature.id, feature.type, multiLineGeometry, feature.tags));
431
+ clipMultiLineStringFeature(feature, clipped, start, end, axis);
349
432
  continue;
350
433
  }
351
434
  case 'Polygon': {
352
- const polygonGeometry = [];
353
- clipLines(feature.geometry, polygonGeometry, k1, k2, axis, true);
354
- if (!polygonGeometry.length)
355
- continue;
356
- clipped.push(createFeature(feature.id, feature.type, polygonGeometry, feature.tags));
435
+ clipPolygonFeature(feature, clipped, start, end, axis);
357
436
  continue;
358
437
  }
359
438
  case 'MultiPolygon': {
360
- const multiPolygonGeometry = [];
361
- for (const polygon of feature.geometry) {
362
- const newPolygon = [];
363
- clipLines(polygon, newPolygon, k1, k2, axis, true);
364
- if (newPolygon.length)
365
- multiPolygonGeometry.push(newPolygon);
366
- }
367
- if (!multiPolygonGeometry.length)
368
- continue;
369
- clipped.push(createFeature(feature.id, feature.type, multiPolygonGeometry, feature.tags));
439
+ clipMultiPolygonFeature(feature, clipped, start, end, axis);
370
440
  continue;
371
441
  }
372
442
  }
@@ -375,17 +445,73 @@ function clip(features, scale, k1, k2, axis, minAll, maxAll, options) {
375
445
  return null;
376
446
  return clipped;
377
447
  }
378
- function clipPoints(geom, newGeom, k1, k2, axis) {
448
+ function clipPointFeature(feature, clipped, start, end, axis) {
449
+ const geom = [];
450
+ clipPoints(feature.geometry, geom, start, end, axis);
451
+ if (!geom.length)
452
+ return;
453
+ const type = geom.length === 3 ? 'Point' : 'MultiPoint';
454
+ clipped.push(createFeature(feature.id, type, geom, feature.tags));
455
+ }
456
+ function clipLineStringFeature(feature, clipped, start, end, axis, options) {
457
+ const geom = [];
458
+ clipLine(feature.geometry, geom, start, end, axis, false, options.lineMetrics);
459
+ if (!geom.length)
460
+ return;
461
+ if (options.lineMetrics) {
462
+ for (const line of geom) {
463
+ clipped.push(createFeature(feature.id, 'LineString', line, feature.tags));
464
+ }
465
+ return;
466
+ }
467
+ if (geom.length > 1) {
468
+ clipped.push(createFeature(feature.id, 'MultiLineString', geom, feature.tags));
469
+ return;
470
+ }
471
+ clipped.push(createFeature(feature.id, 'LineString', geom[0], feature.tags));
472
+ }
473
+ function clipMultiLineStringFeature(feature, clipped, start, end, axis) {
474
+ const geom = [];
475
+ clipLines(feature.geometry, geom, start, end, axis, false);
476
+ if (!geom.length)
477
+ return;
478
+ if (geom.length === 1) {
479
+ clipped.push(createFeature(feature.id, 'LineString', geom[0], feature.tags));
480
+ return;
481
+ }
482
+ clipped.push(createFeature(feature.id, 'MultiLineString', geom, feature.tags));
483
+ }
484
+ function clipPolygonFeature(feature, clipped, start, end, axis) {
485
+ const geom = [];
486
+ clipLines(feature.geometry, geom, start, end, axis, true);
487
+ if (!geom.length)
488
+ return;
489
+ clipped.push(createFeature(feature.id, 'Polygon', geom, feature.tags));
490
+ }
491
+ function clipMultiPolygonFeature(feature, clipped, start, end, axis) {
492
+ const geom = [];
493
+ for (const polygon of feature.geometry) {
494
+ const newPolygon = [];
495
+ clipLines(polygon, newPolygon, start, end, axis, true);
496
+ if (!newPolygon.length)
497
+ continue;
498
+ geom.push(newPolygon);
499
+ }
500
+ if (!geom.length)
501
+ return;
502
+ clipped.push(createFeature(feature.id, 'MultiPolygon', geom, feature.tags));
503
+ }
504
+ function clipPoints(geom, newGeom, start, end, axis) {
379
505
  for (let i = 0; i < geom.length; i += 3) {
380
506
  const a = geom[i + axis];
381
- if (a >= k1 && a <= k2) {
507
+ if (a >= start && a <= end) {
382
508
  addPoint(newGeom, geom[i], geom[i + 1], geom[i + 2]);
383
509
  }
384
510
  }
385
511
  }
386
- function clipLine(geom, newGeom, k1, k2, axis, isPolygon, trackMetrics) {
512
+ function clipLine(geom, newGeom, start, end, axis, isPolygon, trackMetrics) {
387
513
  let slice = newSlice(geom);
388
- const intersect = axis === 0 ? intersectX : intersectY;
514
+ const intersect = axis === AxisType.X ? intersectX : intersectY;
389
515
  let len = geom.start;
390
516
  let segLen, t;
391
517
  for (let i = 0; i < geom.length - 3; i += 3) {
@@ -394,23 +520,23 @@ function clipLine(geom, newGeom, k1, k2, axis, isPolygon, trackMetrics) {
394
520
  const az = geom[i + 2];
395
521
  const bx = geom[i + 3];
396
522
  const by = geom[i + 4];
397
- const a = axis === 0 ? ax : ay;
398
- const b = axis === 0 ? bx : by;
523
+ const a = axis === AxisType.X ? ax : ay;
524
+ const b = axis === AxisType.X ? bx : by;
399
525
  let exited = false;
400
526
  if (trackMetrics)
401
527
  segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
402
- if (a < k1) {
528
+ if (a < start) {
403
529
  // ---|--> | (line enters the clip region from the left)
404
- if (b > k1) {
405
- t = intersect(slice, ax, ay, bx, by, k1);
530
+ if (b > start) {
531
+ t = intersect(slice, ax, ay, bx, by, start);
406
532
  if (trackMetrics)
407
533
  slice.start = len + segLen * t;
408
534
  }
409
535
  }
410
- else if (a > k2) {
536
+ else if (a > end) {
411
537
  // | <--|--- (line enters the clip region from the right)
412
- if (b < k2) {
413
- t = intersect(slice, ax, ay, bx, by, k2);
538
+ if (b < end) {
539
+ t = intersect(slice, ax, ay, bx, by, end);
414
540
  if (trackMetrics)
415
541
  slice.start = len + segLen * t;
416
542
  }
@@ -418,14 +544,14 @@ function clipLine(geom, newGeom, k1, k2, axis, isPolygon, trackMetrics) {
418
544
  else {
419
545
  addPoint(slice, ax, ay, az);
420
546
  }
421
- if (b < k1 && a >= k1) {
547
+ if (b < start && a >= start) {
422
548
  // <--|--- | or <--|-----|--- (line exits the clip region on the left)
423
- t = intersect(slice, ax, ay, bx, by, k1);
549
+ t = intersect(slice, ax, ay, bx, by, start);
424
550
  exited = true;
425
551
  }
426
- if (b > k2 && a <= k2) {
552
+ if (b > end && a <= end) {
427
553
  // | ---|--> or ---|-----|--> (line exits the clip region on the right)
428
- t = intersect(slice, ax, ay, bx, by, k2);
554
+ t = intersect(slice, ax, ay, bx, by, end);
429
555
  exited = true;
430
556
  }
431
557
  if (!isPolygon && exited) {
@@ -442,8 +568,8 @@ function clipLine(geom, newGeom, k1, k2, axis, isPolygon, trackMetrics) {
442
568
  const ax = geom[last];
443
569
  const ay = geom[last + 1];
444
570
  const az = geom[last + 2];
445
- const a = axis === 0 ? ax : ay;
446
- if (a >= k1 && a <= k2)
571
+ const a = axis === AxisType.X ? ax : ay;
572
+ if (a >= start && a <= end)
447
573
  addPoint(slice, ax, ay, az);
448
574
  // close the polygon if its endpoints are not the same after clipping
449
575
  last = slice.length - 3;
@@ -462,9 +588,9 @@ function newSlice(line) {
462
588
  slice.end = line.end;
463
589
  return slice;
464
590
  }
465
- function clipLines(geom, newGeom, k1, k2, axis, isPolygon) {
591
+ function clipLines(geom, newGeom, start, end, axis, isPolygon) {
466
592
  for (const line of geom) {
467
- clipLine(line, newGeom, k1, k2, axis, isPolygon, false);
593
+ clipLine(line, newGeom, start, end, axis, isPolygon, false);
468
594
  }
469
595
  }
470
596
  function addPoint(out, x, y, z) {
@@ -484,11 +610,11 @@ function intersectY(out, ax, ay, bx, by, y) {
484
610
  function wrap(features, options) {
485
611
  const buffer = options.buffer / options.extent;
486
612
  let merged = features;
487
- const left = clip(features, 1, -1 - buffer, buffer, 0, -1, 2, options); // left world copy
488
- const right = clip(features, 1, 1 - buffer, 2 + buffer, 0, -1, 2, options); // right world copy
613
+ const left = clip(features, 1, -1 - buffer, buffer, AxisType.X, -1, 2, options); // left world copy
614
+ const right = clip(features, 1, 1 - buffer, 2 + buffer, AxisType.X, -1, 2, options); // right world copy
489
615
  if (!left && !right)
490
616
  return merged;
491
- merged = clip(features, 1, -buffer, 1 + buffer, 0, -1, 2, options) || []; // center world copy
617
+ merged = clip(features, 1, -buffer, 1 + buffer, AxisType.X, -1, 2, options) || []; // center world copy
492
618
  if (left)
493
619
  merged = shiftFeatureCoords(left, 1).concat(merged); // merge left into center
494
620
  if (right)
@@ -544,198 +670,6 @@ function shiftCoords(points, offset) {
544
670
  return newPoints;
545
671
  }
546
672
 
547
- /**
548
- * Transforms the coordinates of each feature in the given tile from
549
- * mercator-projected space into (extent x extent) tile space.
550
- * @param tile - the tile to transform, this gets modified in place
551
- * @param extent - the tile extent (usually 4096)
552
- * @returns the transformed tile
553
- */
554
- function transformTile(tile, extent) {
555
- if (tile.transformed) {
556
- return tile;
557
- }
558
- const z2 = 1 << tile.z;
559
- const tx = tile.x;
560
- const ty = tile.y;
561
- for (const feature of tile.features) {
562
- if (feature.type === 1) {
563
- const pointGeometry = [];
564
- for (let j = 0; j < feature.geometry.length; j += 2) {
565
- pointGeometry.push(transformPoint(feature.geometry[j], feature.geometry[j + 1], extent, z2, tx, ty));
566
- }
567
- feature.geometry = pointGeometry;
568
- continue;
569
- }
570
- const geometry = [];
571
- for (const singleGeom of feature.geometry) {
572
- const ring = [];
573
- for (let k = 0; k < singleGeom.length; k += 2) {
574
- ring.push(transformPoint(singleGeom[k], singleGeom[k + 1], extent, z2, tx, ty));
575
- }
576
- geometry.push(ring);
577
- }
578
- feature.geometry = geometry;
579
- }
580
- tile.transformed = true;
581
- return tile;
582
- }
583
- function transformPoint(x, y, extent, z2, tx, ty) {
584
- return [
585
- Math.round(extent * (x * z2 - tx)),
586
- Math.round(extent * (y * z2 - ty))
587
- ];
588
- }
589
-
590
- /**
591
- * Creates a tile object from the given features
592
- * @param features - the features to include in the tile
593
- * @param z
594
- * @param tx
595
- * @param ty
596
- * @param options - the options object
597
- * @returns the created tile
598
- */
599
- function createTile(features, z, tx, ty, options) {
600
- const tolerance = z === options.maxZoom ? 0 : options.tolerance / ((1 << z) * options.extent);
601
- const tile = {
602
- features: [],
603
- numPoints: 0,
604
- numSimplified: 0,
605
- numFeatures: features.length,
606
- source: null,
607
- x: tx,
608
- y: ty,
609
- z,
610
- transformed: false,
611
- minX: 2,
612
- minY: 1,
613
- maxX: -1,
614
- maxY: 0
615
- };
616
- for (const feature of features) {
617
- addFeature(tile, feature, tolerance, options);
618
- }
619
- return tile;
620
- }
621
- function addFeature(tile, feature, tolerance, options) {
622
- tile.minX = Math.min(tile.minX, feature.minX);
623
- tile.minY = Math.min(tile.minY, feature.minY);
624
- tile.maxX = Math.max(tile.maxX, feature.maxX);
625
- tile.maxY = Math.max(tile.maxY, feature.maxY);
626
- let tags = feature.tags || null;
627
- let tileFeature;
628
- switch (feature.type) {
629
- case 'Point':
630
- case 'MultiPoint': {
631
- const geometry = [];
632
- for (let i = 0; i < feature.geometry.length; i += 3) {
633
- geometry.push(feature.geometry[i], feature.geometry[i + 1]);
634
- tile.numPoints++;
635
- tile.numSimplified++;
636
- }
637
- if (!geometry.length)
638
- return;
639
- tileFeature = {
640
- type: 1,
641
- tags: tags,
642
- geometry: geometry
643
- };
644
- break;
645
- }
646
- case 'LineString': {
647
- const geometry = [];
648
- addLine(geometry, feature.geometry, tile, tolerance, false, false);
649
- if (!geometry.length)
650
- return;
651
- if (options.lineMetrics) {
652
- tags = {};
653
- for (const key in feature.tags)
654
- tags[key] = feature.tags[key];
655
- // HM TODO: replace with geojsonvt
656
- tags['mapbox_clip_start'] = feature.geometry.start / feature.geometry.size;
657
- tags['mapbox_clip_end'] = feature.geometry.end / feature.geometry.size;
658
- }
659
- tileFeature = {
660
- type: 2,
661
- tags: tags,
662
- geometry: geometry
663
- };
664
- break;
665
- }
666
- case 'MultiLineString':
667
- case 'Polygon': {
668
- const geometry = [];
669
- for (let i = 0; i < feature.geometry.length; i++) {
670
- addLine(geometry, feature.geometry[i], tile, tolerance, feature.type === 'Polygon', i === 0);
671
- }
672
- if (!geometry.length)
673
- return;
674
- tileFeature = {
675
- type: feature.type === 'Polygon' ? 3 : 2,
676
- tags: tags,
677
- geometry: geometry
678
- };
679
- break;
680
- }
681
- case 'MultiPolygon': {
682
- const geometry = [];
683
- for (let k = 0; k < feature.geometry.length; k++) {
684
- const polygon = feature.geometry[k];
685
- for (let i = 0; i < polygon.length; i++) {
686
- addLine(geometry, polygon[i], tile, tolerance, true, i === 0);
687
- }
688
- }
689
- if (!geometry.length)
690
- return;
691
- tileFeature = {
692
- type: 3,
693
- tags: tags,
694
- geometry: geometry
695
- };
696
- break;
697
- }
698
- }
699
- if (feature.id !== null) {
700
- tileFeature.id = feature.id;
701
- }
702
- tile.features.push(tileFeature);
703
- }
704
- function addLine(result, geom, tile, tolerance, isPolygon, isOuter) {
705
- const sqTolerance = tolerance * tolerance;
706
- if (tolerance > 0 && (geom.size < (isPolygon ? sqTolerance : tolerance))) {
707
- tile.numPoints += geom.length / 3;
708
- return;
709
- }
710
- const ring = [];
711
- for (let i = 0; i < geom.length; i += 3) {
712
- if (tolerance === 0 || geom[i + 2] > sqTolerance) {
713
- tile.numSimplified++;
714
- ring.push(geom[i], geom[i + 1]);
715
- }
716
- tile.numPoints++;
717
- }
718
- if (isPolygon)
719
- rewind(ring, isOuter);
720
- result.push(ring);
721
- }
722
- function rewind(ring, clockwise) {
723
- let area = 0;
724
- for (let i = 0, len = ring.length, j = len - 2; i < len; j = i, i += 2) {
725
- area += (ring[i] - ring[j]) * (ring[i + 1] + ring[j + 1]);
726
- }
727
- if (area > 0 !== clockwise)
728
- return;
729
- for (let i = 0, len = ring.length; i < len / 2; i += 2) {
730
- const x = ring[i];
731
- const y = ring[i + 1];
732
- ring[i] = ring[len - 2 - i];
733
- ring[i + 1] = ring[len - 1 - i];
734
- ring[len - 2 - i] = x;
735
- ring[len - 1 - i] = y;
736
- }
737
- }
738
-
739
673
  /**
740
674
  * Applies a GeoJSON Source Diff to an existing set of simplified features
741
675
  * @param source
@@ -777,7 +711,7 @@ function applySourceDiff(source, dataDiff, options) {
777
711
  // convert and add new features
778
712
  if (diff.add.size) {
779
713
  // projects and adds simplification info
780
- let addFeatures = convert({ type: 'FeatureCollection', features: Array.from(diff.add.values()) }, options);
714
+ let addFeatures = convertToInternal({ type: 'FeatureCollection', features: Array.from(diff.add.values()) }, options);
781
715
  // wraps features (ie extreme west and extreme east)
782
716
  addFeatures = wrap(addFeatures, options);
783
717
  affected.push(...addFeatures);
@@ -817,7 +751,7 @@ function getUpdatedFeature(vtFeature, update, options) {
817
751
  properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
818
752
  };
819
753
  // projects and adds simplification info
820
- let features = convert({ type: 'FeatureCollection', features: [geojsonFeature] }, options);
754
+ let features = convertToInternal({ type: 'FeatureCollection', features: [geojsonFeature] }, options);
821
755
  // wraps features (ie extreme west and extreme east)
822
756
  features = wrap(features, options);
823
757
  return features[0];
@@ -869,70 +803,1098 @@ function diffToHashed(diff) {
869
803
  return hashed;
870
804
  }
871
805
 
872
- const defaultOptions = {
873
- maxZoom: 14,
874
- indexMaxZoom: 5,
875
- indexMaxPoints: 100000,
876
- tolerance: 3,
877
- extent: 4096,
878
- buffer: 64,
879
- lineMetrics: false,
880
- promoteId: null,
881
- generateId: false,
882
- updateable: false,
883
- debug: 0
884
- };
885
- /**
886
- * Main class for creating and managing a vector tile index from GeoJSON data.
887
- */
888
- class GeoJSONVT {
889
- options;
890
- /** @internal */
891
- tiles;
806
+ const ARRAY_TYPES = [
807
+ Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
808
+ Int32Array, Uint32Array, Float32Array, Float64Array
809
+ ];
810
+
811
+ /** @typedef {Int8ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor} TypedArrayConstructor */
812
+
813
+ const VERSION = 1; // serialized format version
814
+ const HEADER_SIZE = 8;
815
+
816
+ class KDBush {
817
+
818
+ /**
819
+ * Creates an index from raw `ArrayBuffer` data.
820
+ * @param {ArrayBuffer} data
821
+ */
822
+ static from(data) {
823
+ if (!(data instanceof ArrayBuffer)) {
824
+ throw new Error('Data must be an instance of ArrayBuffer.');
825
+ }
826
+ const [magic, versionAndType] = new Uint8Array(data, 0, 2);
827
+ if (magic !== 0xdb) {
828
+ throw new Error('Data does not appear to be in a KDBush format.');
829
+ }
830
+ const version = versionAndType >> 4;
831
+ if (version !== VERSION) {
832
+ throw new Error(`Got v${version} data when expected v${VERSION}.`);
833
+ }
834
+ const ArrayType = ARRAY_TYPES[versionAndType & 0x0f];
835
+ if (!ArrayType) {
836
+ throw new Error('Unrecognized array type.');
837
+ }
838
+ const [nodeSize] = new Uint16Array(data, 2, 1);
839
+ const [numItems] = new Uint32Array(data, 4, 1);
840
+
841
+ return new KDBush(numItems, nodeSize, ArrayType, data);
842
+ }
843
+
844
+ /**
845
+ * Creates an index that will hold a given number of items.
846
+ * @param {number} numItems
847
+ * @param {number} [nodeSize=64] Size of the KD-tree node (64 by default).
848
+ * @param {TypedArrayConstructor} [ArrayType=Float64Array] The array type used for coordinates storage (`Float64Array` by default).
849
+ * @param {ArrayBuffer} [data] (For internal use only)
850
+ */
851
+ constructor(numItems, nodeSize = 64, ArrayType = Float64Array, data) {
852
+ if (isNaN(numItems) || numItems < 0) throw new Error(`Unpexpected numItems value: ${numItems}.`);
853
+
854
+ this.numItems = +numItems;
855
+ this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535);
856
+ this.ArrayType = ArrayType;
857
+ this.IndexArrayType = numItems < 65536 ? Uint16Array : Uint32Array;
858
+
859
+ const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType);
860
+ const coordsByteSize = numItems * 2 * this.ArrayType.BYTES_PER_ELEMENT;
861
+ const idsByteSize = numItems * this.IndexArrayType.BYTES_PER_ELEMENT;
862
+ const padCoords = (8 - idsByteSize % 8) % 8;
863
+
864
+ if (arrayTypeIndex < 0) {
865
+ throw new Error(`Unexpected typed array class: ${ArrayType}.`);
866
+ }
867
+
868
+ if (data && (data instanceof ArrayBuffer)) { // reconstruct an index from a buffer
869
+ this.data = data;
870
+ this.ids = new this.IndexArrayType(this.data, HEADER_SIZE, numItems);
871
+ this.coords = new this.ArrayType(this.data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
872
+ this._pos = numItems * 2;
873
+ this._finished = true;
874
+ } else { // initialize a new index
875
+ this.data = new ArrayBuffer(HEADER_SIZE + coordsByteSize + idsByteSize + padCoords);
876
+ this.ids = new this.IndexArrayType(this.data, HEADER_SIZE, numItems);
877
+ this.coords = new this.ArrayType(this.data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
878
+ this._pos = 0;
879
+ this._finished = false;
880
+
881
+ // set header
882
+ new Uint8Array(this.data, 0, 2).set([0xdb, (VERSION << 4) + arrayTypeIndex]);
883
+ new Uint16Array(this.data, 2, 1)[0] = nodeSize;
884
+ new Uint32Array(this.data, 4, 1)[0] = numItems;
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Add a point to the index.
890
+ * @param {number} x
891
+ * @param {number} y
892
+ * @returns {number} An incremental index associated with the added item (starting from `0`).
893
+ */
894
+ add(x, y) {
895
+ const index = this._pos >> 1;
896
+ this.ids[index] = index;
897
+ this.coords[this._pos++] = x;
898
+ this.coords[this._pos++] = y;
899
+ return index;
900
+ }
901
+
902
+ /**
903
+ * Perform indexing of the added points.
904
+ */
905
+ finish() {
906
+ const numAdded = this._pos >> 1;
907
+ if (numAdded !== this.numItems) {
908
+ throw new Error(`Added ${numAdded} items when expected ${this.numItems}.`);
909
+ }
910
+ // kd-sort both arrays for efficient search
911
+ sort(this.ids, this.coords, this.nodeSize, 0, this.numItems - 1, 0);
912
+
913
+ this._finished = true;
914
+ return this;
915
+ }
916
+
917
+ /**
918
+ * Search the index for items within a given bounding box.
919
+ * @param {number} minX
920
+ * @param {number} minY
921
+ * @param {number} maxX
922
+ * @param {number} maxY
923
+ * @returns {number[]} An array of indices correponding to the found items.
924
+ */
925
+ range(minX, minY, maxX, maxY) {
926
+ if (!this._finished) throw new Error('Data not yet indexed - call index.finish().');
927
+
928
+ const {ids, coords, nodeSize} = this;
929
+ const stack = [0, ids.length - 1, 0];
930
+ const result = [];
931
+
932
+ // recursively search for items in range in the kd-sorted arrays
933
+ while (stack.length) {
934
+ const axis = stack.pop() || 0;
935
+ const right = stack.pop() || 0;
936
+ const left = stack.pop() || 0;
937
+
938
+ // if we reached "tree node", search linearly
939
+ if (right - left <= nodeSize) {
940
+ for (let i = left; i <= right; i++) {
941
+ const x = coords[2 * i];
942
+ const y = coords[2 * i + 1];
943
+ if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[i]);
944
+ }
945
+ continue;
946
+ }
947
+
948
+ // otherwise find the middle index
949
+ const m = (left + right) >> 1;
950
+
951
+ // include the middle item if it's in range
952
+ const x = coords[2 * m];
953
+ const y = coords[2 * m + 1];
954
+ if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[m]);
955
+
956
+ // queue search in halves that intersect the query
957
+ if (axis === 0 ? minX <= x : minY <= y) {
958
+ stack.push(left);
959
+ stack.push(m - 1);
960
+ stack.push(1 - axis);
961
+ }
962
+ if (axis === 0 ? maxX >= x : maxY >= y) {
963
+ stack.push(m + 1);
964
+ stack.push(right);
965
+ stack.push(1 - axis);
966
+ }
967
+ }
968
+
969
+ return result;
970
+ }
971
+
972
+ /**
973
+ * Search the index for items within a given radius.
974
+ * @param {number} qx
975
+ * @param {number} qy
976
+ * @param {number} r Query radius.
977
+ * @returns {number[]} An array of indices correponding to the found items.
978
+ */
979
+ within(qx, qy, r) {
980
+ if (!this._finished) throw new Error('Data not yet indexed - call index.finish().');
981
+
982
+ const {ids, coords, nodeSize} = this;
983
+ const stack = [0, ids.length - 1, 0];
984
+ const result = [];
985
+ const r2 = r * r;
986
+
987
+ // recursively search for items within radius in the kd-sorted arrays
988
+ while (stack.length) {
989
+ const axis = stack.pop() || 0;
990
+ const right = stack.pop() || 0;
991
+ const left = stack.pop() || 0;
992
+
993
+ // if we reached "tree node", search linearly
994
+ if (right - left <= nodeSize) {
995
+ for (let i = left; i <= right; i++) {
996
+ if (sqDist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2) result.push(ids[i]);
997
+ }
998
+ continue;
999
+ }
1000
+
1001
+ // otherwise find the middle index
1002
+ const m = (left + right) >> 1;
1003
+
1004
+ // include the middle item if it's in range
1005
+ const x = coords[2 * m];
1006
+ const y = coords[2 * m + 1];
1007
+ if (sqDist(x, y, qx, qy) <= r2) result.push(ids[m]);
1008
+
1009
+ // queue search in halves that intersect the query
1010
+ if (axis === 0 ? qx - r <= x : qy - r <= y) {
1011
+ stack.push(left);
1012
+ stack.push(m - 1);
1013
+ stack.push(1 - axis);
1014
+ }
1015
+ if (axis === 0 ? qx + r >= x : qy + r >= y) {
1016
+ stack.push(m + 1);
1017
+ stack.push(right);
1018
+ stack.push(1 - axis);
1019
+ }
1020
+ }
1021
+
1022
+ return result;
1023
+ }
1024
+ }
1025
+
1026
+ /**
1027
+ * @param {Uint16Array | Uint32Array} ids
1028
+ * @param {InstanceType<TypedArrayConstructor>} coords
1029
+ * @param {number} nodeSize
1030
+ * @param {number} left
1031
+ * @param {number} right
1032
+ * @param {number} axis
1033
+ */
1034
+ function sort(ids, coords, nodeSize, left, right, axis) {
1035
+ if (right - left <= nodeSize) return;
1036
+
1037
+ const m = (left + right) >> 1; // middle index
1038
+
1039
+ // sort ids and coords around the middle index so that the halves lie
1040
+ // either left/right or top/bottom correspondingly (taking turns)
1041
+ select(ids, coords, m, left, right, axis);
1042
+
1043
+ // recursively kd-sort first half and second half on the opposite axis
1044
+ sort(ids, coords, nodeSize, left, m - 1, 1 - axis);
1045
+ sort(ids, coords, nodeSize, m + 1, right, 1 - axis);
1046
+ }
1047
+
1048
+ /**
1049
+ * Custom Floyd-Rivest selection algorithm: sort ids and coords so that
1050
+ * [left..k-1] items are smaller than k-th item (on either x or y axis)
1051
+ * @param {Uint16Array | Uint32Array} ids
1052
+ * @param {InstanceType<TypedArrayConstructor>} coords
1053
+ * @param {number} k
1054
+ * @param {number} left
1055
+ * @param {number} right
1056
+ * @param {number} axis
1057
+ */
1058
+ function select(ids, coords, k, left, right, axis) {
1059
+
1060
+ while (right > left) {
1061
+ if (right - left > 600) {
1062
+ const n = right - left + 1;
1063
+ const m = k - left + 1;
1064
+ const z = Math.log(n);
1065
+ const s = 0.5 * Math.exp(2 * z / 3);
1066
+ const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
1067
+ const newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
1068
+ const newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
1069
+ select(ids, coords, k, newLeft, newRight, axis);
1070
+ }
1071
+
1072
+ const t = coords[2 * k + axis];
1073
+ let i = left;
1074
+ let j = right;
1075
+
1076
+ swapItem(ids, coords, left, k);
1077
+ if (coords[2 * right + axis] > t) swapItem(ids, coords, left, right);
1078
+
1079
+ while (i < j) {
1080
+ swapItem(ids, coords, i, j);
1081
+ i++;
1082
+ j--;
1083
+ while (coords[2 * i + axis] < t) i++;
1084
+ while (coords[2 * j + axis] > t) j--;
1085
+ }
1086
+
1087
+ if (coords[2 * left + axis] === t) swapItem(ids, coords, left, j);
1088
+ else {
1089
+ j++;
1090
+ swapItem(ids, coords, j, right);
1091
+ }
1092
+
1093
+ if (j <= k) left = j + 1;
1094
+ if (k <= j) right = j - 1;
1095
+ }
1096
+ }
1097
+
1098
+ /**
1099
+ * @param {Uint16Array | Uint32Array} ids
1100
+ * @param {InstanceType<TypedArrayConstructor>} coords
1101
+ * @param {number} i
1102
+ * @param {number} j
1103
+ */
1104
+ function swapItem(ids, coords, i, j) {
1105
+ swap(ids, i, j);
1106
+ swap(coords, 2 * i, 2 * j);
1107
+ swap(coords, 2 * i + 1, 2 * j + 1);
1108
+ }
1109
+
1110
+ /**
1111
+ * @param {InstanceType<TypedArrayConstructor>} arr
1112
+ * @param {number} i
1113
+ * @param {number} j
1114
+ */
1115
+ function swap(arr, i, j) {
1116
+ const tmp = arr[i];
1117
+ arr[i] = arr[j];
1118
+ arr[j] = tmp;
1119
+ }
1120
+
1121
+ /**
1122
+ * @param {number} ax
1123
+ * @param {number} ay
1124
+ * @param {number} bx
1125
+ * @param {number} by
1126
+ */
1127
+ function sqDist(ax, ay, bx, by) {
1128
+ const dx = ax - bx;
1129
+ const dy = ay - by;
1130
+ return dx * dx + dy * dy;
1131
+ }
1132
+
1133
+ const defaultClusterOptions = {
1134
+ minZoom: 0,
1135
+ maxZoom: 16,
1136
+ minPoints: 2,
1137
+ radius: 40,
1138
+ extent: 512,
1139
+ nodeSize: 64,
1140
+ log: false,
1141
+ generateId: false,
1142
+ reduce: null,
1143
+ map: (props) => props
1144
+ };
1145
+ const OFFSET_ZOOM = 2;
1146
+ const OFFSET_ID = 3;
1147
+ const OFFSET_PARENT = 4;
1148
+ const OFFSET_NUM = 5;
1149
+ const OFFSET_PROP = 6;
1150
+ /**
1151
+ * This class allow clustering of geojson points.
1152
+ */
1153
+ class ClusterTileIndex {
1154
+ options;
1155
+ trees;
1156
+ stride;
1157
+ clusterProps;
1158
+ points;
1159
+ constructor(options) {
1160
+ this.options = Object.assign(Object.create(defaultClusterOptions), options);
1161
+ this.trees = new Array(this.options.maxZoom + 1);
1162
+ this.stride = this.options.reduce ? 7 : 6;
1163
+ this.clusterProps = [];
1164
+ this.points = [];
1165
+ }
1166
+ /**
1167
+ * Loads GeoJSON point features and builds the internal clustering index.
1168
+ * @param points - GeoJSON point features to cluster.
1169
+ */
1170
+ load(points) {
1171
+ const features = [];
1172
+ // Convert GeoJSON point features to GeoJSONVT internal point features
1173
+ for (const point of points) {
1174
+ if (!point.geometry) {
1175
+ continue;
1176
+ }
1177
+ const [lng, lat] = point.geometry.coordinates;
1178
+ const [x, y] = [projectX(lng), projectY(lat)];
1179
+ const feature = {
1180
+ id: point.id,
1181
+ type: 'Point',
1182
+ geometry: [x, y],
1183
+ tags: point.properties
1184
+ };
1185
+ features.push(feature);
1186
+ }
1187
+ this.createIndex(features);
1188
+ }
1189
+ /**
1190
+ * @internal
1191
+ * Loads internal GeoJSONVT point features from a data source and builds the clustering index.
1192
+ * @param features - {@link GeoJSONVTInternalFeature} data source features to filter and cluster.
1193
+ */
1194
+ initialize(features) {
1195
+ const points = [];
1196
+ for (const feature of features) {
1197
+ if (feature.type !== 'Point')
1198
+ continue;
1199
+ points.push(feature);
1200
+ }
1201
+ this.createIndex(points);
1202
+ }
1203
+ /**
1204
+ * @internal
1205
+ * Updates the cluster data by rebuilding.
1206
+ * @param features
1207
+ */
1208
+ updateIndex(features, _affected, options) {
1209
+ this.options = Object.assign(Object.create(defaultClusterOptions), options.clusterOptions);
1210
+ this.initialize(features);
1211
+ }
1212
+ createIndex(points) {
1213
+ const { log, minZoom, maxZoom } = this.options;
1214
+ if (log)
1215
+ console.time('total time');
1216
+ const timerId = `prepare ${points.length} points`;
1217
+ if (log)
1218
+ console.time(timerId);
1219
+ this.points = points;
1220
+ // generate a cluster object for each point and index input points into a KD-tree
1221
+ const data = [];
1222
+ for (let i = 0; i < points.length; i++) {
1223
+ const p = points[i];
1224
+ if (!p?.geometry)
1225
+ continue;
1226
+ let [x, y] = p.geometry;
1227
+ x = Math.fround(x);
1228
+ y = Math.fround(y);
1229
+ // store internal point/cluster data in flat numeric arrays for performance
1230
+ data.push(x, y, // projected point coordinates
1231
+ Infinity, // the last zoom the point was processed at
1232
+ i, // index of the source feature in the original input array
1233
+ -1, // parent cluster id
1234
+ 1 // number of points in a cluster
1235
+ );
1236
+ if (this.options.reduce)
1237
+ data.push(0); // noop
1238
+ }
1239
+ let tree = this.trees[maxZoom + 1] = this.createTree(data);
1240
+ if (log)
1241
+ console.timeEnd(timerId);
1242
+ // cluster points on max zoom, then cluster the results on previous zoom, etc.;
1243
+ // results in a cluster hierarchy across zoom levels
1244
+ for (let z = maxZoom; z >= minZoom; z--) {
1245
+ const now = Date.now();
1246
+ // create a new set of clusters for the zoom and index them with a KD-tree
1247
+ tree = this.trees[z] = this.createTree(this.cluster(tree, z));
1248
+ if (log)
1249
+ console.log('z%d: %d clusters in %dms', z, tree.numItems, Date.now() - now);
1250
+ }
1251
+ if (log)
1252
+ console.timeEnd('total time');
1253
+ }
1254
+ /**
1255
+ * Returns clusters and/or points within a bounding box at a given zoom level.
1256
+ * @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order.
1257
+ * @param zoom - Zoom level to query.
1258
+ */
1259
+ getClusters(bbox, zoom) {
1260
+ const clusterInternal = this.getClustersInternal(bbox, zoom);
1261
+ return clusterInternal.map((f) => featureToGeoJSON(f));
1262
+ }
1263
+ getClustersInternal(bbox, zoom) {
1264
+ let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
1265
+ const minLat = Math.max(-90, Math.min(90, bbox[1]));
1266
+ let maxLng = bbox[2] === 180 ? 180 : ((bbox[2] + 180) % 360 + 360) % 360 - 180;
1267
+ const maxLat = Math.max(-90, Math.min(90, bbox[3]));
1268
+ if (bbox[2] - bbox[0] >= 360) {
1269
+ minLng = -180;
1270
+ maxLng = 180;
1271
+ }
1272
+ else if (minLng > maxLng) {
1273
+ const easternHem = this.getClustersInternal([minLng, minLat, 180, maxLat], zoom);
1274
+ const westernHem = this.getClustersInternal([-180, minLat, maxLng, maxLat], zoom);
1275
+ return easternHem.concat(westernHem);
1276
+ }
1277
+ const tree = this.trees[this.limitZoom(zoom)];
1278
+ const ids = tree.range(projectX(minLng), projectY(maxLat), projectX(maxLng), projectY(minLat));
1279
+ const data = tree.flatData;
1280
+ const clusters = [];
1281
+ for (const id of ids) {
1282
+ const k = this.stride * id;
1283
+ clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
1284
+ }
1285
+ return clusters;
1286
+ }
1287
+ /**
1288
+ * Returns the immediate children (clusters or points) of a cluster as GeoJSON.
1289
+ * @param clusterId - The target cluster id.
1290
+ */
1291
+ getChildren(clusterId) {
1292
+ const originId = this.getOriginId(clusterId);
1293
+ const originZoom = this.getOriginZoom(clusterId);
1294
+ const clusterError = new Error('No cluster with the specified id: ' + clusterId);
1295
+ const tree = this.trees[originZoom];
1296
+ if (!tree)
1297
+ throw clusterError;
1298
+ const data = tree.flatData;
1299
+ if (originId * this.stride >= data.length)
1300
+ throw clusterError;
1301
+ const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
1302
+ const x = data[originId * this.stride];
1303
+ const y = data[originId * this.stride + 1];
1304
+ const ids = tree.within(x, y, r);
1305
+ const children = [];
1306
+ for (const id of ids) {
1307
+ const k = id * this.stride;
1308
+ if (data[k + OFFSET_PARENT] === clusterId) {
1309
+ children.push(data[k + OFFSET_NUM] > 1 ? getClusterGeoJSON(data, k, this.clusterProps) : featureToGeoJSON(this.points[data[k + OFFSET_ID]]));
1310
+ }
1311
+ }
1312
+ if (children.length === 0)
1313
+ throw clusterError;
1314
+ return children;
1315
+ }
1316
+ /**
1317
+ * Returns leaf point features under a cluster, paginated by `limit` and `offset`.
1318
+ * @param clusterId - The target cluster id.
1319
+ * @param limit - Maximum number of points to return (defaults to `10`).
1320
+ * @param offset - Number of points to skip before collecting results (defaults to `0`).
1321
+ */
1322
+ getLeaves(clusterId, limit, offset) {
1323
+ limit = limit || 10;
1324
+ offset = offset || 0;
1325
+ const leaves = [];
1326
+ this.appendLeaves(leaves, clusterId, limit, offset, 0);
1327
+ return leaves;
1328
+ }
1329
+ /**
1330
+ * Generates a vector-tile-like representation of a single tile.
1331
+ * @param z - Tile zoom.
1332
+ * @param x - Tile x coordinate.
1333
+ * @param y - Tile y coordinate.
1334
+ */
1335
+ getTile(z, x, y) {
1336
+ const tree = this.trees[this.limitZoom(z)];
1337
+ const z2 = Math.pow(2, z);
1338
+ const { extent, radius } = this.options;
1339
+ const p = radius / extent;
1340
+ const top = (y - p) / z2;
1341
+ const bottom = (y + 1 + p) / z2;
1342
+ const tile = {
1343
+ transformed: true,
1344
+ features: [],
1345
+ source: null,
1346
+ x: x,
1347
+ y: y,
1348
+ z: z
1349
+ };
1350
+ this.addTileFeatures(tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), tree.flatData, x, y, z2, tile);
1351
+ if (x === 0) {
1352
+ this.addTileFeatures(tree.range(1 - p / z2, top, 1, bottom), tree.flatData, z2, y, z2, tile);
1353
+ }
1354
+ if (x === z2 - 1) {
1355
+ this.addTileFeatures(tree.range(0, top, p / z2, bottom), tree.flatData, -1, y, z2, tile);
1356
+ }
1357
+ return tile;
1358
+ }
1359
+ /**
1360
+ * Returns the zoom level at which a cluster expands into multiple children.
1361
+ * @param clusterId - The target cluster id.
1362
+ */
1363
+ getClusterExpansionZoom(clusterId) {
1364
+ return this.getOriginZoom(clusterId);
1365
+ }
1366
+ appendLeaves(result, clusterId, limit, offset, skipped) {
1367
+ const children = this.getChildren(clusterId);
1368
+ for (const child of children) {
1369
+ const props = child.properties;
1370
+ if (props?.cluster) {
1371
+ if (skipped + props.point_count <= offset) {
1372
+ // skip the whole cluster
1373
+ skipped += props.point_count;
1374
+ }
1375
+ else {
1376
+ // enter the cluster
1377
+ skipped = this.appendLeaves(result, props.cluster_id, limit, offset, skipped);
1378
+ // exit the cluster
1379
+ }
1380
+ }
1381
+ else if (skipped < offset) {
1382
+ // skip a single point
1383
+ skipped++;
1384
+ }
1385
+ else {
1386
+ // add a single point
1387
+ result.push(child);
1388
+ }
1389
+ if (result.length === limit)
1390
+ break;
1391
+ }
1392
+ return skipped;
1393
+ }
1394
+ createTree(data) {
1395
+ const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array);
1396
+ for (let i = 0; i < data.length; i += this.stride)
1397
+ tree.add(data[i], data[i + 1]);
1398
+ tree.finish();
1399
+ tree.flatData = data;
1400
+ tree.data = null; // clear original data to free memory as it isn't used later on.
1401
+ return tree;
1402
+ }
1403
+ addTileFeatures(ids, data, x, y, z2, tile) {
1404
+ for (const i of ids) {
1405
+ const k = i * this.stride;
1406
+ const isCluster = data[k + OFFSET_NUM] > 1;
1407
+ let tags;
1408
+ let px;
1409
+ let py;
1410
+ if (isCluster) {
1411
+ tags = getClusterProperties(data, k, this.clusterProps);
1412
+ px = data[k];
1413
+ py = data[k + 1];
1414
+ }
1415
+ else {
1416
+ const p = this.points[data[k + OFFSET_ID]];
1417
+ tags = p.tags;
1418
+ [px, py] = p.geometry;
1419
+ }
1420
+ const f = {
1421
+ type: 1,
1422
+ geometry: [[
1423
+ Math.round(this.options.extent * (px * z2 - x)),
1424
+ Math.round(this.options.extent * (py * z2 - y))
1425
+ ]],
1426
+ tags
1427
+ };
1428
+ // assign id
1429
+ let id;
1430
+ if (isCluster || this.options.generateId) {
1431
+ // optionally generate id for points
1432
+ id = data[k + OFFSET_ID];
1433
+ }
1434
+ else {
1435
+ // keep id if already assigned
1436
+ id = this.points[data[k + OFFSET_ID]].id;
1437
+ }
1438
+ if (id !== undefined)
1439
+ f.id = id;
1440
+ tile.features.push(f);
1441
+ }
1442
+ }
1443
+ limitZoom(z) {
1444
+ return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
1445
+ }
1446
+ cluster(tree, zoom) {
1447
+ const { radius, extent, reduce, minPoints } = this.options;
1448
+ const r = radius / (extent * Math.pow(2, zoom));
1449
+ const data = tree.flatData;
1450
+ const nextData = [];
1451
+ const stride = this.stride;
1452
+ // loop through each point
1453
+ for (let i = 0; i < data.length; i += stride) {
1454
+ // if we've already visited the point at this zoom level, skip it
1455
+ if (data[i + OFFSET_ZOOM] <= zoom)
1456
+ continue;
1457
+ data[i + OFFSET_ZOOM] = zoom;
1458
+ // find all nearby points
1459
+ const x = data[i];
1460
+ const y = data[i + 1];
1461
+ const neighborIds = tree.within(data[i], data[i + 1], r);
1462
+ const numPointsOrigin = data[i + OFFSET_NUM];
1463
+ let numPoints = numPointsOrigin;
1464
+ // count the number of points in a potential cluster
1465
+ for (const neighborId of neighborIds) {
1466
+ const k = neighborId * stride;
1467
+ // filter out neighbors that are already processed
1468
+ if (data[k + OFFSET_ZOOM] > zoom)
1469
+ numPoints += data[k + OFFSET_NUM];
1470
+ }
1471
+ // if there were neighbors to merge, and there are enough points to form a cluster
1472
+ if (numPoints > numPointsOrigin && numPoints >= minPoints) {
1473
+ let wx = x * numPointsOrigin;
1474
+ let wy = y * numPointsOrigin;
1475
+ let clusterProperties;
1476
+ let clusterPropIndex = -1;
1477
+ // encode both zoom and point index on which the cluster originated -- offset by total length of features
1478
+ const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length;
1479
+ for (const neighborId of neighborIds) {
1480
+ const k = neighborId * stride;
1481
+ if (data[k + OFFSET_ZOOM] <= zoom)
1482
+ continue;
1483
+ data[k + OFFSET_ZOOM] = zoom; // save the zoom (so it doesn't get processed twice)
1484
+ const numPoints2 = data[k + OFFSET_NUM];
1485
+ wx += data[k] * numPoints2; // accumulate coordinates for calculating weighted center
1486
+ wy += data[k + 1] * numPoints2;
1487
+ data[k + OFFSET_PARENT] = id;
1488
+ if (reduce) {
1489
+ if (!clusterProperties) {
1490
+ clusterProperties = this.map(data, i, true);
1491
+ clusterPropIndex = this.clusterProps.length;
1492
+ this.clusterProps.push(clusterProperties);
1493
+ }
1494
+ reduce(clusterProperties, this.map(data, k));
1495
+ }
1496
+ }
1497
+ data[i + OFFSET_PARENT] = id;
1498
+ nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints);
1499
+ if (reduce)
1500
+ nextData.push(clusterPropIndex);
1501
+ }
1502
+ else { // left points as unclustered
1503
+ for (let j = 0; j < stride; j++)
1504
+ nextData.push(data[i + j]);
1505
+ if (numPoints > 1) {
1506
+ for (const neighborId of neighborIds) {
1507
+ const k = neighborId * stride;
1508
+ if (data[k + OFFSET_ZOOM] <= zoom)
1509
+ continue;
1510
+ data[k + OFFSET_ZOOM] = zoom;
1511
+ for (let j = 0; j < stride; j++)
1512
+ nextData.push(data[k + j]);
1513
+ }
1514
+ }
1515
+ }
1516
+ }
1517
+ return nextData;
1518
+ }
1519
+ // get index of the point from which the cluster originated
1520
+ getOriginId(clusterId) {
1521
+ return (clusterId - this.points.length) >> 5;
1522
+ }
1523
+ // get zoom of the point from which the cluster originated
1524
+ getOriginZoom(clusterId) {
1525
+ return (clusterId - this.points.length) % 32;
1526
+ }
1527
+ map(data, i, clone) {
1528
+ if (data[i + OFFSET_NUM] > 1) {
1529
+ const props = this.clusterProps[data[i + OFFSET_PROP]];
1530
+ return clone ? Object.assign({}, props) : props;
1531
+ }
1532
+ const original = this.points[data[i + OFFSET_ID]].tags;
1533
+ const result = this.options.map(original);
1534
+ return clone && result === original ? Object.assign({}, result) : result;
1535
+ }
1536
+ }
1537
+ function getClusterFeature(data, i, clusterProps) {
1538
+ return {
1539
+ id: data[i + OFFSET_ID],
1540
+ type: 'Point',
1541
+ tags: getClusterProperties(data, i, clusterProps),
1542
+ geometry: [data[i], data[i + 1]]
1543
+ };
1544
+ }
1545
+ function getClusterGeoJSON(data, i, clusterProps) {
1546
+ return {
1547
+ type: 'Feature',
1548
+ id: data[i + OFFSET_ID],
1549
+ properties: getClusterProperties(data, i, clusterProps),
1550
+ geometry: {
1551
+ type: 'Point',
1552
+ coordinates: [unprojectX(data[i]), unprojectY(data[i + 1])]
1553
+ }
1554
+ };
1555
+ }
1556
+ function getClusterProperties(data, i, clusterProps) {
1557
+ const count = data[i + OFFSET_NUM];
1558
+ const abbrev = count >= 10000 ? `${Math.round(count / 1000)}k` :
1559
+ count >= 1000 ? `${Math.round(count / 100) / 10}k` : count;
1560
+ const propIndex = data[i + OFFSET_PROP];
1561
+ const properties = propIndex === -1 ? {} : Object.assign({}, clusterProps[propIndex]);
1562
+ return Object.assign(properties, {
1563
+ cluster: true,
1564
+ cluster_id: data[i + OFFSET_ID],
1565
+ point_count: count,
1566
+ point_count_abbreviated: abbrev
1567
+ });
1568
+ }
1569
+
1570
+ /**
1571
+ * Creates a tile object from the given features
1572
+ * @param features - the features to include in the tile
1573
+ * @param z
1574
+ * @param tx
1575
+ * @param ty
1576
+ * @param options - the options object
1577
+ * @returns the created tile
1578
+ */
1579
+ function createTile(features, z, tx, ty, options) {
1580
+ const tolerance = z === options.maxZoom ? 0 : options.tolerance / ((1 << z) * options.extent);
1581
+ const tile = {
1582
+ transformed: false,
1583
+ features: [],
1584
+ source: null,
1585
+ x: tx,
1586
+ y: ty,
1587
+ z: z,
1588
+ minX: 2,
1589
+ minY: 1,
1590
+ maxX: -1,
1591
+ maxY: 0,
1592
+ numPoints: 0,
1593
+ numSimplified: 0,
1594
+ numFeatures: features.length
1595
+ };
1596
+ for (const feature of features) {
1597
+ addFeature(tile, feature, tolerance, options);
1598
+ }
1599
+ return tile;
1600
+ }
1601
+ function addFeature(tile, feature, tolerance, options) {
1602
+ tile.minX = Math.min(tile.minX, feature.minX);
1603
+ tile.minY = Math.min(tile.minY, feature.minY);
1604
+ tile.maxX = Math.max(tile.maxX, feature.maxX);
1605
+ tile.maxY = Math.max(tile.maxY, feature.maxY);
1606
+ switch (feature.type) {
1607
+ case 'Point':
1608
+ case 'MultiPoint':
1609
+ addPointsTileFeature(tile, feature);
1610
+ return;
1611
+ case 'LineString':
1612
+ addLineTileFeautre(tile, feature, tolerance, options);
1613
+ return;
1614
+ case 'MultiLineString':
1615
+ case 'Polygon':
1616
+ addLinesTileFeature(tile, feature, tolerance);
1617
+ return;
1618
+ case 'MultiPolygon':
1619
+ addMultiPolygonTileFeature(tile, feature, tolerance);
1620
+ return;
1621
+ }
1622
+ }
1623
+ function addPointsTileFeature(tile, feature) {
1624
+ const geometry = [];
1625
+ for (let i = 0; i < feature.geometry.length; i += 3) {
1626
+ geometry.push(feature.geometry[i], feature.geometry[i + 1]);
1627
+ tile.numPoints++;
1628
+ tile.numSimplified++;
1629
+ }
1630
+ if (!geometry.length)
1631
+ return;
1632
+ const tileFeature = {
1633
+ type: 1,
1634
+ tags: feature.tags || null,
1635
+ geometry: geometry
1636
+ };
1637
+ if (feature.id !== null) {
1638
+ tileFeature.id = feature.id;
1639
+ }
1640
+ tile.features.push(tileFeature);
1641
+ }
1642
+ function addLineTileFeautre(tile, feature, tolerance, options) {
1643
+ const geometry = [];
1644
+ addLine(geometry, feature.geometry, tile, tolerance, false, false);
1645
+ if (!geometry.length)
1646
+ return;
1647
+ let tags = feature.tags || null;
1648
+ if (options.lineMetrics) {
1649
+ tags = {};
1650
+ for (const key in feature.tags)
1651
+ tags[key] = feature.tags[key];
1652
+ tags['geojsonvt_clip_start'] = feature.geometry.start / feature.geometry.size;
1653
+ tags['geojsonvt_clip_end'] = feature.geometry.end / feature.geometry.size;
1654
+ }
1655
+ const tileFeature = {
1656
+ type: 2,
1657
+ tags: tags,
1658
+ geometry: geometry
1659
+ };
1660
+ if (feature.id !== null) {
1661
+ tileFeature.id = feature.id;
1662
+ }
1663
+ tile.features.push(tileFeature);
1664
+ }
1665
+ function addLinesTileFeature(tile, feature, tolerance) {
1666
+ const geometry = [];
1667
+ for (let i = 0; i < feature.geometry.length; i++) {
1668
+ addLine(geometry, feature.geometry[i], tile, tolerance, feature.type === 'Polygon', i === 0);
1669
+ }
1670
+ if (!geometry.length)
1671
+ return;
1672
+ const tileFeature = {
1673
+ type: feature.type === 'Polygon' ? 3 : 2,
1674
+ tags: feature.tags || null,
1675
+ geometry: geometry
1676
+ };
1677
+ if (feature.id !== null) {
1678
+ tileFeature.id = feature.id;
1679
+ }
1680
+ tile.features.push(tileFeature);
1681
+ }
1682
+ function addMultiPolygonTileFeature(tile, feature, tolerance) {
1683
+ const geometry = [];
1684
+ for (let k = 0; k < feature.geometry.length; k++) {
1685
+ const polygon = feature.geometry[k];
1686
+ for (let i = 0; i < polygon.length; i++) {
1687
+ addLine(geometry, polygon[i], tile, tolerance, true, i === 0);
1688
+ }
1689
+ }
1690
+ if (!geometry.length)
1691
+ return;
1692
+ const tileFeature = {
1693
+ type: 3,
1694
+ tags: feature.tags || null,
1695
+ geometry: geometry
1696
+ };
1697
+ if (feature.id !== null) {
1698
+ tileFeature.id = feature.id;
1699
+ }
1700
+ tile.features.push(tileFeature);
1701
+ }
1702
+ function addLine(result, geom, tile, tolerance, isPolygon, isOuter) {
1703
+ const sqTolerance = tolerance * tolerance;
1704
+ if (tolerance > 0 && (geom.size < (isPolygon ? sqTolerance : tolerance))) {
1705
+ tile.numPoints += geom.length / 3;
1706
+ return;
1707
+ }
1708
+ const ring = [];
1709
+ for (let i = 0; i < geom.length; i += 3) {
1710
+ if (tolerance === 0 || geom[i + 2] > sqTolerance) {
1711
+ tile.numSimplified++;
1712
+ ring.push(geom[i], geom[i + 1]);
1713
+ }
1714
+ tile.numPoints++;
1715
+ }
1716
+ if (isPolygon)
1717
+ rewind(ring, isOuter);
1718
+ result.push(ring);
1719
+ }
1720
+ function rewind(ring, clockwise) {
1721
+ let area = 0;
1722
+ for (let i = 0, len = ring.length, j = len - 2; i < len; j = i, i += 2) {
1723
+ area += (ring[i] - ring[j]) * (ring[i + 1] + ring[j + 1]);
1724
+ }
1725
+ if (area > 0 !== clockwise)
1726
+ return;
1727
+ for (let i = 0, len = ring.length; i < len / 2; i += 2) {
1728
+ const x = ring[i];
1729
+ const y = ring[i + 1];
1730
+ ring[i] = ring[len - 2 - i];
1731
+ ring[i + 1] = ring[len - 1 - i];
1732
+ ring[len - 2 - i] = x;
1733
+ ring[len - 1 - i] = y;
1734
+ }
1735
+ }
1736
+
1737
+ /**
1738
+ * Transforms the coordinates of each feature in the given tile from
1739
+ * mercator-projected space into (extent x extent) tile space.
1740
+ * @param tile - the tile to transform, this gets modified in place
1741
+ * @param extent - the tile extent (usually 4096)
1742
+ * @returns the transformed tile
1743
+ */
1744
+ function transformTile(tile, extent) {
1745
+ if (tile.transformed) {
1746
+ return tile;
1747
+ }
1748
+ const z2 = 1 << tile.z;
1749
+ const tx = tile.x;
1750
+ const ty = tile.y;
1751
+ for (const feature of tile.features) {
1752
+ if (feature.type === 1) {
1753
+ transformPointFeature(feature, extent, z2, tx, ty);
1754
+ }
1755
+ else {
1756
+ transformNonPointFeature(feature, extent, z2, tx, ty);
1757
+ }
1758
+ }
1759
+ tile.transformed = true;
1760
+ return tile;
1761
+ }
1762
+ /**
1763
+ * Transforms a single point feature from mercator-projected space into (extent x extent) tile space.
1764
+ */
1765
+ function transformPointFeature(feature, extent, z2, tx, ty) {
1766
+ const transformed = feature;
1767
+ const geometry = feature.geometry;
1768
+ const point = [];
1769
+ for (let i = 0; i < geometry.length; i += 2) {
1770
+ point.push(transformPoint(geometry[i], geometry[i + 1], extent, z2, tx, ty));
1771
+ }
1772
+ transformed.geometry = point;
1773
+ return transformed;
1774
+ }
1775
+ /**
1776
+ * Transforms a single non-point feature from mercator-projected space into (extent x extent) tile space.
1777
+ */
1778
+ function transformNonPointFeature(feature, extent, z2, tx, ty) {
1779
+ const transformed = feature;
1780
+ const geometry = feature.geometry;
1781
+ const nonPoint = [];
1782
+ for (const geom of geometry) {
1783
+ const ring = [];
1784
+ for (let i = 0; i < geom.length; i += 2) {
1785
+ ring.push(transformPoint(geom[i], geom[i + 1], extent, z2, tx, ty));
1786
+ }
1787
+ nonPoint.push(ring);
1788
+ }
1789
+ transformed.geometry = nonPoint;
1790
+ return transformed;
1791
+ }
1792
+ function transformPoint(x, y, extent, z2, tx, ty) {
1793
+ return [
1794
+ Math.round(extent * (x * z2 - tx)),
1795
+ Math.round(extent * (y * z2 - ty))
1796
+ ];
1797
+ }
1798
+
1799
+ class TileIndex {
1800
+ options;
892
1801
  tileCoords;
893
1802
  /** @internal */
1803
+ tiles;
1804
+ /** @internal */
894
1805
  stats = {};
895
1806
  /** @internal */
896
1807
  total = 0;
897
- source;
898
- constructor(data, options) {
899
- options = this.options = Object.assign({}, defaultOptions, options);
900
- const debug = options.debug;
901
- if (debug)
902
- console.time('preprocess data');
903
- if (options.maxZoom < 0 || options.maxZoom > 24)
904
- throw new Error('maxZoom should be in the 0-24 range');
905
- if (options.promoteId && options.generateId)
906
- throw new Error('promoteId and generateId cannot be used together.');
907
- // projects and adds simplification info
908
- let features = convert(data, options);
909
- // tiles and tileCoords are part of the public API
1808
+ constructor(options) {
1809
+ this.options = options;
910
1810
  this.tiles = {};
911
1811
  this.tileCoords = [];
912
- if (debug) {
913
- console.timeEnd('preprocess data');
914
- console.log('index: maxZoom: %d, maxPoints: %d', options.indexMaxZoom, options.indexMaxPoints);
915
- console.time('generate tiles');
916
- this.stats = {};
917
- this.total = 0;
918
- }
919
- // wraps features (ie extreme west and extreme east)
920
- features = wrap(features, options);
1812
+ this.stats = {};
1813
+ this.total = 0;
1814
+ }
1815
+ initialize(features) {
921
1816
  // start slicing from the top tile down
922
- if (features.length) {
923
- this.splitTile(features, 0, 0, 0);
924
- }
925
- // for updateable indexes, store a copy of the original simplified features
926
- if (options.updateable) {
927
- this.source = features;
928
- }
929
- if (debug) {
1817
+ this.splitTile(features, 0, 0, 0);
1818
+ if (this.options.debug) {
930
1819
  if (features.length)
931
1820
  console.log('features: %d, points: %d', this.tiles[0].numFeatures, this.tiles[0].numPoints);
932
1821
  console.timeEnd('generate tiles');
933
1822
  console.log('tiles generated:', this.total, JSON.stringify(this.stats));
934
1823
  }
935
1824
  }
1825
+ /** {@inheritdoc} */
1826
+ updateIndex(source, affected, options) {
1827
+ if (options.debug > 1) {
1828
+ console.log('invalidating tiles');
1829
+ console.time('invalidating');
1830
+ }
1831
+ this.invalidateTiles(affected);
1832
+ if (options.debug > 1)
1833
+ console.timeEnd('invalidating');
1834
+ // re-generate root tile with updated feature set
1835
+ const [z, x, y] = [0, 0, 0];
1836
+ const rootTile = createTile(source, z, x, y, options);
1837
+ rootTile.source = source;
1838
+ // update tile index with new root tile - ready for getTile calls
1839
+ const id = toID(z, x, y);
1840
+ this.tiles[id] = rootTile;
1841
+ this.tileCoords.push({ z, x, y, id });
1842
+ if (options.debug) {
1843
+ const key = `z${z}`;
1844
+ this.stats[key] = (this.stats[key] || 0) + 1;
1845
+ this.total++;
1846
+ }
1847
+ }
1848
+ /** {@inheritdoc} */
1849
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1850
+ getClusterExpansionZoom(_clusterId) {
1851
+ return null;
1852
+ }
1853
+ /** {@inheritdoc} */
1854
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1855
+ getChildren(_clusterId) {
1856
+ return null;
1857
+ }
1858
+ /** {@inheritdoc} */
1859
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1860
+ getLeaves(_clusterId, _limit, _offset) {
1861
+ return null;
1862
+ }
1863
+ /** {@inheritdoc} */
1864
+ getTile(z, x, y) {
1865
+ const { extent, debug } = this.options;
1866
+ const z2 = 1 << z;
1867
+ x = (x + z2) & (z2 - 1); // wrap tile x coordinate
1868
+ const id = toID(z, x, y);
1869
+ if (this.tiles[id]) {
1870
+ return transformTile(this.tiles[id], extent);
1871
+ }
1872
+ if (debug > 1)
1873
+ console.log('drilling down to z%d-%d-%d', z, x, y);
1874
+ let z0 = z;
1875
+ let x0 = x;
1876
+ let y0 = y;
1877
+ let parent;
1878
+ while (!parent && z0 > 0) {
1879
+ z0--;
1880
+ x0 = x0 >> 1;
1881
+ y0 = y0 >> 1;
1882
+ parent = this.tiles[toID(z0, x0, y0)];
1883
+ }
1884
+ if (!parent?.source)
1885
+ return null;
1886
+ // if we found a parent tile containing the original geometry, we can drill down from it
1887
+ if (debug > 1) {
1888
+ console.log('found parent tile z%d-%d-%d', z0, x0, y0);
1889
+ console.time('drilling down');
1890
+ }
1891
+ this.splitTile(parent.source, z0, x0, y0, z, x, y);
1892
+ if (debug > 1)
1893
+ console.timeEnd('drilling down');
1894
+ if (!this.tiles[id])
1895
+ return null;
1896
+ return transformTile(this.tiles[id], extent);
1897
+ }
936
1898
  /**
937
1899
  * splits features from a parent tile to sub-tiles.
938
1900
  * z, x, and y are the coordinates of the parent tile
@@ -1011,15 +1973,15 @@ class GeoJSONVT {
1011
1973
  let bl = null;
1012
1974
  let tr = null;
1013
1975
  let br = null;
1014
- const left = clip(features, z2, x - k1, x + k3, 0, tile.minX, tile.maxX, options);
1015
- const right = clip(features, z2, x + k2, x + k4, 0, tile.minX, tile.maxX, options);
1976
+ const left = clip(features, z2, x - k1, x + k3, AxisType.X, tile.minX, tile.maxX, options);
1977
+ const right = clip(features, z2, x + k2, x + k4, AxisType.X, tile.minX, tile.maxX, options);
1016
1978
  if (left) {
1017
- tl = clip(left, z2, y - k1, y + k3, 1, tile.minY, tile.maxY, options);
1018
- bl = clip(left, z2, y + k2, y + k4, 1, tile.minY, tile.maxY, options);
1979
+ tl = clip(left, z2, y - k1, y + k3, AxisType.Y, tile.minY, tile.maxY, options);
1980
+ bl = clip(left, z2, y + k2, y + k4, AxisType.Y, tile.minY, tile.maxY, options);
1019
1981
  }
1020
1982
  if (right) {
1021
- tr = clip(right, z2, y - k1, y + k3, 1, tile.minY, tile.maxY, options);
1022
- br = clip(right, z2, y + k2, y + k4, 1, tile.minY, tile.maxY, options);
1983
+ tr = clip(right, z2, y - k1, y + k3, AxisType.Y, tile.minY, tile.maxY, options);
1984
+ br = clip(right, z2, y + k2, y + k4, AxisType.Y, tile.minY, tile.maxY, options);
1023
1985
  }
1024
1986
  if (debug > 1)
1025
1987
  console.timeEnd('clipping');
@@ -1029,59 +1991,14 @@ class GeoJSONVT {
1029
1991
  stack.push(br || [], z + 1, x * 2 + 1, y * 2 + 1);
1030
1992
  }
1031
1993
  }
1032
- /**
1033
- * Given z, x, and y tile coordinates, returns the corresponding tile with geometries in tile coordinates, much like MVT data is stored.
1034
- * @param z - tile zoom level
1035
- * @param x - tile x coordinate
1036
- * @param y - tile y coordinate
1037
- * @returns the transformed tile or null if not found
1038
- */
1039
- getTile(z, x, y) {
1040
- z = +z;
1041
- x = +x;
1042
- y = +y;
1043
- const options = this.options;
1044
- const { extent, debug } = options;
1045
- if (z < 0 || z > 24)
1046
- return null;
1047
- const z2 = 1 << z;
1048
- x = (x + z2) & (z2 - 1); // wrap tile x coordinate
1049
- const id = toID(z, x, y);
1050
- if (this.tiles[id]) {
1051
- return transformTile(this.tiles[id], extent);
1052
- }
1053
- if (debug > 1)
1054
- console.log('drilling down to z%d-%d-%d', z, x, y);
1055
- let z0 = z;
1056
- let x0 = x;
1057
- let y0 = y;
1058
- let parent;
1059
- while (!parent && z0 > 0) {
1060
- z0--;
1061
- x0 = x0 >> 1;
1062
- y0 = y0 >> 1;
1063
- parent = this.tiles[toID(z0, x0, y0)];
1064
- }
1065
- if (!parent?.source)
1066
- return null;
1067
- // if we found a parent tile containing the original geometry, we can drill down from it
1068
- if (debug > 1) {
1069
- console.log('found parent tile z%d-%d-%d', z0, x0, y0);
1070
- console.time('drilling down');
1071
- }
1072
- this.splitTile(parent.source, z0, x0, y0, z, x, y);
1073
- if (debug > 1)
1074
- console.timeEnd('drilling down');
1075
- if (!this.tiles[id])
1076
- return null;
1077
- return transformTile(this.tiles[id], extent);
1078
- }
1079
1994
  /**
1080
1995
  * Invalidates (removes) tiles affected by the provided features
1081
1996
  * @internal
1082
1997
  * @param features
1083
1998
  */
1084
1999
  invalidateTiles(features) {
2000
+ if (!features.length)
2001
+ return;
1085
2002
  const options = this.options;
1086
2003
  const { debug } = options;
1087
2004
  // calculate bounding box of all features for trivial reject
@@ -1140,52 +2057,239 @@ class GeoJSONVT {
1140
2057
  this.tileCoords = this.tileCoords.filter(c => !removedLookup.has(c.id));
1141
2058
  }
1142
2059
  }
2060
+ }
2061
+ function toID(z, x, y) {
2062
+ return (((1 << z) * y + x) * 32) + z;
2063
+ }
2064
+
2065
+ const defaultOptions = {
2066
+ maxZoom: 14,
2067
+ indexMaxZoom: 5,
2068
+ indexMaxPoints: 100000,
2069
+ tolerance: 3,
2070
+ extent: 4096,
2071
+ buffer: 64,
2072
+ lineMetrics: false,
2073
+ promoteId: null,
2074
+ generateId: false,
2075
+ updateable: false,
2076
+ cluster: false,
2077
+ clusterOptions: defaultClusterOptions,
2078
+ debug: 0
2079
+ };
2080
+ /**
2081
+ * Main class for creating and managing a vector tile index from GeoJSON data.
2082
+ */
2083
+ class GeoJSONVT {
2084
+ /**
2085
+ * @internal
2086
+ * This is for the tests
2087
+ */
2088
+ get tiles() {
2089
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2090
+ return this.tileIndex?.tiles ?? {};
2091
+ }
2092
+ /**
2093
+ * @internal
2094
+ * This is for the tests
2095
+ */
2096
+ get stats() {
2097
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2098
+ return this.tileIndex.stats;
2099
+ }
2100
+ /**
2101
+ * @internal
2102
+ * This is for the tests
2103
+ */
2104
+ get total() {
2105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2106
+ return this.tileIndex.total;
2107
+ }
2108
+ options;
2109
+ source;
2110
+ tileIndex;
2111
+ constructor(data, options) {
2112
+ options = this.options = Object.assign({}, defaultOptions, options);
2113
+ const debug = options.debug;
2114
+ if (debug)
2115
+ console.time('preprocess data');
2116
+ if (options.maxZoom < 0 || options.maxZoom > 24)
2117
+ throw new Error('maxZoom should be in the 0-24 range');
2118
+ if (options.promoteId && options.generateId)
2119
+ throw new Error('promoteId and generateId cannot be used together.');
2120
+ // projects and adds simplification info
2121
+ let features = convertToInternal(data, options);
2122
+ if (debug) {
2123
+ console.timeEnd('preprocess data');
2124
+ console.log('index: maxZoom: %d, maxPoints: %d', options.indexMaxZoom, options.indexMaxPoints);
2125
+ console.time('generate tiles');
2126
+ }
2127
+ // wraps features (ie extreme west and extreme east)
2128
+ features = wrap(features, options);
2129
+ // for updateable indexes, store a copy of the original simplified features
2130
+ if (options.updateable) {
2131
+ this.source = features;
2132
+ }
2133
+ this.initializeIndex(features, options);
2134
+ }
2135
+ initializeIndex(features, options) {
2136
+ this.tileIndex = options.cluster ? new ClusterTileIndex(options.clusterOptions) : new TileIndex(options);
2137
+ if (!features.length)
2138
+ return;
2139
+ this.tileIndex.initialize(features);
2140
+ }
2141
+ /**
2142
+ * Given z, x, and y tile coordinates, returns the corresponding tile with geometries in tile coordinates, much like MVT data is stored.
2143
+ * @param z - tile zoom level
2144
+ * @param x - tile x coordinate
2145
+ * @param y - tile y coordinate
2146
+ * @returns the transformed tile or null if not found
2147
+ */
2148
+ getTile(z, x, y) {
2149
+ z = +z;
2150
+ x = +x;
2151
+ y = +y;
2152
+ if (z < 0 || z > 24)
2153
+ return null;
2154
+ return this.tileIndex.getTile(z, x, y);
2155
+ }
1143
2156
  /**
1144
- * Updates the tile index by adding and/or removing geojson features
1145
- * invalidates tiles that are affected by the update for regeneration on next getTile call.
2157
+ * Updates the source data feature set using a {@link GeoJSONVTSourceDiff}
1146
2158
  * @param diff - the source diff object
1147
2159
  */
1148
- updateData(diff) {
2160
+ updateData(diff, filter) {
1149
2161
  const options = this.options;
1150
- const debug = options.debug;
1151
2162
  if (!options.updateable)
1152
2163
  throw new Error('to update tile geojson `updateable` option must be set to true');
1153
2164
  // apply diff and collect affected features and updated source that will be used to invalidate tiles
1154
- const { affected, source } = applySourceDiff(this.source, diff, options);
2165
+ let { affected, source } = applySourceDiff(this.source, diff, options);
2166
+ if (filter) {
2167
+ ({ affected, source } = this.filterUpdate(source, affected, filter));
2168
+ }
1155
2169
  // nothing has changed
1156
2170
  if (!affected.length)
1157
2171
  return;
1158
2172
  // update source with new simplified feature set
1159
2173
  this.source = source;
1160
- if (debug > 1) {
1161
- console.log('invalidating tiles');
1162
- console.time('invalidating');
2174
+ this.tileIndex.updateIndex(source, affected, options);
2175
+ }
2176
+ /**
2177
+ * Filter an update using a predicate function. Returns the affected and updated source features.
2178
+ */
2179
+ filterUpdate(source, affected, predicate) {
2180
+ const removeIds = new Set();
2181
+ for (const feature of source) {
2182
+ if (feature.id == undefined)
2183
+ continue;
2184
+ if (predicate(featureToGeoJSON(feature)))
2185
+ continue;
2186
+ affected.push(feature);
2187
+ removeIds.add(feature.id);
1163
2188
  }
1164
- this.invalidateTiles(affected);
1165
- if (debug > 1)
1166
- console.timeEnd('invalidating');
1167
- // re-generate root tile with updated feature set
1168
- const [z, x, y] = [0, 0, 0];
1169
- const rootTile = createTile(this.source, z, x, y, this.options);
1170
- rootTile.source = this.source;
1171
- // update tile index with new root tile - ready for getTile calls
1172
- const id = toID(z, x, y);
1173
- this.tiles[id] = rootTile;
1174
- this.tileCoords.push({ z, x, y, id });
1175
- if (debug) {
1176
- const key = `z${z}`;
1177
- this.stats[key] = (this.stats[key] || 0) + 1;
1178
- this.total++;
2189
+ source = source.filter(feature => !removeIds.has(feature.id));
2190
+ return { affected, source };
2191
+ }
2192
+ /**
2193
+ * Returns source data as GeoJSON - only available when `updateable` option is set to true.
2194
+ */
2195
+ getData() {
2196
+ if (!this.options.updateable)
2197
+ throw new Error('to retrieve data the `updateable` option must be set to true');
2198
+ return convertToGeoJSON(this.source);
2199
+ }
2200
+ /**
2201
+ * Update supercluster options and regenerate the index.
2202
+ * @param cluster - whether to enable clustering
2203
+ * @param clusterOptions - {@link SuperclusterOptions}
2204
+ */
2205
+ updateClusterOptions(cluster, clusterOptions) {
2206
+ const wasCluster = this.options.cluster;
2207
+ this.options.cluster = cluster;
2208
+ this.options.clusterOptions = clusterOptions;
2209
+ if (wasCluster == cluster) {
2210
+ this.tileIndex.updateIndex(this.source, [], this.options);
2211
+ return;
1179
2212
  }
2213
+ this.initializeIndex(this.source, this.options);
2214
+ }
2215
+ /**
2216
+ * Returns the zoom level at which a cluster expands into multiple children.
2217
+ * @param clusterId - The target cluster id.
2218
+ * @returns the expansion zoom or null in case of non-clustered source
2219
+ */
2220
+ getClusterExpansionZoom(clusterId) {
2221
+ return this.tileIndex.getClusterExpansionZoom(clusterId);
2222
+ }
2223
+ /**
2224
+ * Returns the immediate children (clusters or points) of a cluster as GeoJSON.
2225
+ * @param clusterId - The target cluster id.
2226
+ * @returns the immediate children or null in case of non-clustered source
2227
+ */
2228
+ getClusterChildren(clusterId) {
2229
+ return this.tileIndex.getChildren(clusterId);
2230
+ }
2231
+ /**
2232
+ * Returns leaf point features under a cluster, paginated by `limit` and `offset`.
2233
+ * @param clusterId - The target cluster id.
2234
+ * @param limit - Maximum number of points to return (defaults to `10`).
2235
+ * @param offset - Number of points to skip before collecting results (defaults to `0`).
2236
+ * @returns leaf point features under a cluster or null in case of non-clustered source
2237
+ */
2238
+ getClusterLeaves(clusterId, limit, offset) {
2239
+ return this.tileIndex.getLeaves(clusterId, limit, offset);
1180
2240
  }
1181
2241
  }
1182
- function toID(z, x, y) {
1183
- return (((1 << z) * y + x) * 32) + z;
1184
- }
1185
- function geojsonvt(data, options) {
1186
- return new GeoJSONVT(data, options);
2242
+
2243
+ /**
2244
+ * Converts GeoJSON data directly to a single vector tile without building a tile index.
2245
+ *
2246
+ * Unlike the {@link GeoJSONVT} class which builds a hierarchical tile index for efficient
2247
+ * repeated tile access, this function generates a single tile on-demand. This is useful when:
2248
+ * - You only need one specific tile and don't need to query multiple tiles
2249
+ * - The source data is already spatially filtered to the tile's bounding box
2250
+ * - You want to avoid the overhead of building a full tile index
2251
+ *
2252
+ * @example
2253
+ * ```ts
2254
+ * import {geoJSONToTile} from '@maplibre/geojson-vt';
2255
+ *
2256
+ * const geojson = {
2257
+ * type: 'FeatureCollection',
2258
+ * features: [{
2259
+ * type: 'Feature',
2260
+ * geometry: { type: 'Point', coordinates: [-77.03, 38.90] },
2261
+ * properties: { name: 'Washington, D.C.' }
2262
+ * }]
2263
+ * };
2264
+ *
2265
+ * const tile = geoJSONToTile(geojson, 10, 292, 391, { extent: 4096 });
2266
+ * ```
2267
+ *
2268
+ * @param data - GeoJSON data (Feature, FeatureCollection, or Geometry)
2269
+ * @param z - Tile zoom level
2270
+ * @param x - Tile x coordinate
2271
+ * @param y - Tile y coordinate
2272
+ * @param options - Optional configuration for tile generation
2273
+ * @returns The generated tile with geometries in tile coordinates, or null if no features
2274
+ */
2275
+ function geoJSONToTile(data, z, x, y, options = {}) {
2276
+ options = { ...defaultOptions, ...options };
2277
+ const { wrap: shouldWrap = false, clip: shouldClip = false } = options;
2278
+ let features = convertToInternal(data, options);
2279
+ if (shouldWrap) {
2280
+ features = wrap(features, options);
2281
+ }
2282
+ if (shouldClip || options.lineMetrics) {
2283
+ const pow2 = 1 << z;
2284
+ const buffer = options.buffer / options.extent;
2285
+ const left = clip(features, pow2, (x - buffer), (x + 1 + buffer), AxisType.X, -1, 2, options);
2286
+ features = clip(left || [], pow2, (y - buffer), (y + 1 + buffer), AxisType.Y, -1, 2, options);
2287
+ }
2288
+ return transformTile(createTile(features ?? [], z, x, y, options), options.extent);
1187
2289
  }
1188
2290
 
1189
- return geojsonvt;
2291
+ exports.GeoJSONVT = GeoJSONVT;
2292
+ exports.Supercluster = ClusterTileIndex;
2293
+ exports.geoJSONToTile = geoJSONToTile;
1190
2294
 
1191
2295
  }));