@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,3 +1,5 @@
1
+ import KDBush from 'kdbush';
2
+
1
3
  /**
2
4
  * calculate simplification data using optimized Douglas-Peucker algorithm
3
5
  * @param coords - flat array of coordinates
@@ -124,109 +126,125 @@ function calcLineBBox(feature, geom) {
124
126
  }
125
127
 
126
128
  /**
127
- * converts GeoJSON feature into an intermediate projected JSON vector format with simplification data
129
+ * converts GeoJSON to internal source features (an intermediate projected JSON vector format with simplification data)
128
130
  * @param data
129
131
  * @param options
130
132
  * @returns
131
133
  */
132
- function convert(data, options) {
134
+ function convertToInternal(data, options) {
133
135
  const features = [];
134
136
  switch (data.type) {
135
137
  case 'FeatureCollection':
136
138
  for (let i = 0; i < data.features.length; i++) {
137
- convertFeature(features, data.features[i], options, i);
139
+ featureToInternal(features, data.features[i], options, i);
138
140
  }
139
141
  break;
140
142
  case 'Feature':
141
- convertFeature(features, data, options);
143
+ featureToInternal(features, data, options);
142
144
  break;
143
145
  default:
144
- convertFeature(features, { geometry: data, properties: undefined }, options);
146
+ featureToInternal(features, { geometry: data, properties: undefined }, options);
145
147
  }
146
148
  return features;
147
149
  }
148
- function convertFeature(features, geojson, options, index) {
150
+ function featureToInternal(features, geojson, options, index) {
149
151
  if (!geojson.geometry)
150
152
  return;
151
153
  if (geojson.geometry.type === 'GeometryCollection') {
152
- for (const singleGeometry of geojson.geometry.geometries) {
153
- convertFeature(features, {
154
- id: geojson.id,
155
- geometry: singleGeometry,
156
- properties: geojson.properties
157
- }, options, index);
158
- }
154
+ convertGeometryCollection(features, geojson, geojson.geometry, options, index);
159
155
  return;
160
156
  }
161
157
  const coords = geojson.geometry.coordinates;
162
158
  if (!coords?.length)
163
159
  return;
160
+ const id = getFeatureId(geojson, options, index);
164
161
  const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
165
- let id = geojson.id;
166
- if (options.promoteId) {
167
- id = geojson.properties?.[options.promoteId];
168
- }
169
- else if (options.generateId) {
170
- id = index || 0;
171
- }
172
162
  switch (geojson.geometry.type) {
173
- case 'Point': {
174
- const pointGeometry = [];
175
- convertPoint(geojson.geometry.coordinates, pointGeometry);
176
- features.push(createFeature(id, geojson.geometry.type, pointGeometry, geojson.properties));
163
+ case 'Point':
164
+ convertPointFeature(features, id, geojson.geometry, geojson.properties);
177
165
  return;
178
- }
179
- case 'MultiPoint': {
180
- const multiPointGeometry = [];
181
- for (const p of geojson.geometry.coordinates) {
182
- convertPoint(p, multiPointGeometry);
183
- }
184
- features.push(createFeature(id, geojson.geometry.type, multiPointGeometry, geojson.properties));
166
+ case 'MultiPoint':
167
+ convertMultiPointFeature(features, id, geojson.geometry, geojson.properties);
185
168
  return;
186
- }
187
- case 'LineString': {
188
- const lineGeometry = [];
189
- convertLine(geojson.geometry.coordinates, lineGeometry, tolerance, false);
190
- features.push(createFeature(id, geojson.geometry.type, lineGeometry, geojson.properties));
169
+ case 'LineString':
170
+ convertLineStringFeature(features, id, geojson.geometry, tolerance, geojson.properties);
191
171
  return;
192
- }
193
- case 'MultiLineString': {
194
- if (options.lineMetrics) {
195
- // explode into linestrings in order to track metrics
196
- for (const line of geojson.geometry.coordinates) {
197
- const lineGeometry = [];
198
- convertLine(line, lineGeometry, tolerance, false);
199
- features.push(createFeature(id, 'LineString', lineGeometry, geojson.properties));
200
- }
201
- return;
202
- }
203
- const multiLineGeometry = [];
204
- convertLines(geojson.geometry.coordinates, multiLineGeometry, tolerance, false);
205
- features.push(createFeature(id, geojson.geometry.type, multiLineGeometry, geojson.properties));
172
+ case 'MultiLineString':
173
+ convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, options, geojson.properties);
206
174
  return;
207
- }
208
- case 'Polygon': {
209
- const polygonGeometry = [];
210
- convertLines(geojson.geometry.coordinates, polygonGeometry, tolerance, true);
211
- features.push(createFeature(id, geojson.geometry.type, polygonGeometry, geojson.properties));
175
+ case 'Polygon':
176
+ convertPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
212
177
  return;
213
- }
214
- case 'MultiPolygon': {
215
- const multiPolygonGeometry = [];
216
- for (const polygon of geojson.geometry.coordinates) {
217
- const newPolygon = [];
218
- convertLines(polygon, newPolygon, tolerance, true);
219
- multiPolygonGeometry.push(newPolygon);
220
- }
221
- features.push(createFeature(id, geojson.geometry.type, multiPolygonGeometry, geojson.properties));
178
+ case 'MultiPolygon':
179
+ convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
222
180
  return;
223
- }
224
181
  default:
225
182
  throw new Error('Input data is not a valid GeoJSON object.');
226
183
  }
227
184
  }
228
- function convertPoint(coords, out) {
229
- out.push(projectX(coords[0]), projectY(coords[1]), 0);
185
+ function getFeatureId(geojson, options, index) {
186
+ if (options.promoteId) {
187
+ return geojson.properties?.[options.promoteId];
188
+ }
189
+ if (options.generateId) {
190
+ return index || 0;
191
+ }
192
+ return geojson.id;
193
+ }
194
+ function convertGeometryCollection(features, geojson, geometry, options, index) {
195
+ for (const geom of geometry.geometries) {
196
+ featureToInternal(features, {
197
+ id: geojson.id,
198
+ geometry: geom,
199
+ properties: geojson.properties
200
+ }, options, index);
201
+ }
202
+ }
203
+ function convertPointFeature(features, id, geom, properties) {
204
+ const out = [];
205
+ out.push(projectX(geom.coordinates[0]), projectY(geom.coordinates[1]), 0);
206
+ features.push(createFeature(id, 'Point', out, properties));
207
+ }
208
+ function convertMultiPointFeature(features, id, geom, properties) {
209
+ const out = [];
210
+ for (const coords of geom.coordinates) {
211
+ out.push(projectX(coords[0]), projectY(coords[1]), 0);
212
+ }
213
+ features.push(createFeature(id, 'MultiPoint', out, properties));
214
+ }
215
+ function convertLineStringFeature(features, id, geom, tolerance, properties) {
216
+ const out = [];
217
+ convertLine(geom.coordinates, out, tolerance, false);
218
+ features.push(createFeature(id, 'LineString', out, properties));
219
+ }
220
+ function convertMultiLineStringFeature(features, id, geom, tolerance, options, properties) {
221
+ if (options.lineMetrics) {
222
+ // explode into linestrings to be able to track metrics
223
+ for (const line of geom.coordinates) {
224
+ const out = [];
225
+ convertLine(line, out, tolerance, false);
226
+ features.push(createFeature(id, 'LineString', out, properties));
227
+ }
228
+ }
229
+ else {
230
+ const out = [];
231
+ convertLines(geom.coordinates, out, tolerance, false);
232
+ features.push(createFeature(id, 'MultiLineString', out, properties));
233
+ }
234
+ }
235
+ function convertPolygonFeature(features, id, geom, tolerance, properties) {
236
+ const out = [];
237
+ convertLines(geom.coordinates, out, tolerance, true);
238
+ features.push(createFeature(id, 'Polygon', out, properties));
239
+ }
240
+ function convertMultiPolygonFeature(features, id, geom, tolerance, properties) {
241
+ const out = [];
242
+ for (const polygon of geom.coordinates) {
243
+ const polygonOut = [];
244
+ convertLines(polygon, polygonOut, tolerance, true);
245
+ out.push(polygonOut);
246
+ }
247
+ features.push(createFeature(id, 'MultiPolygon', out, properties));
230
248
  }
231
249
  function convertLine(ring, out, tolerance, isPolygon) {
232
250
  let x0, y0;
@@ -262,105 +280,159 @@ function convertLines(rings, out, tolerance, isPolygon) {
262
280
  out.push(geom);
263
281
  }
264
282
  }
283
+ /**
284
+ * Convert longitude to spherical mercator in [0..1] range
285
+ */
265
286
  function projectX(x) {
266
287
  return x / 360 + 0.5;
267
288
  }
289
+ /**
290
+ * Convert latitude to spherical mercator in [0..1] range
291
+ */
268
292
  function projectY(y) {
269
293
  const sin = Math.sin(y * Math.PI / 180);
270
294
  const y2 = 0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
271
295
  return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
272
296
  }
273
297
 
274
- /* clip features between two vertical or horizontal axis-parallel lines:
298
+ /**
299
+ * Converts internal source features back to GeoJSON format.
300
+ */
301
+ function convertToGeoJSON(source) {
302
+ const geojson = {
303
+ type: 'FeatureCollection',
304
+ features: source.map(feature => featureToGeoJSON(feature))
305
+ };
306
+ return geojson;
307
+ }
308
+ /**
309
+ * Converts a single internal feature to GeoJSON format.
310
+ */
311
+ function featureToGeoJSON(feature) {
312
+ const geojsonFeature = {
313
+ type: 'Feature',
314
+ geometry: geometryToGeoJSON(feature),
315
+ properties: feature.tags
316
+ };
317
+ if (feature.id != null) {
318
+ geojsonFeature.id = feature.id;
319
+ }
320
+ return geojsonFeature;
321
+ }
322
+ /**
323
+ * Converts a single internal feature geometry to GeoJSON format.
324
+ */
325
+ function geometryToGeoJSON(feature) {
326
+ const { type, geometry } = feature;
327
+ switch (type) {
328
+ case 'Point':
329
+ return {
330
+ type: type,
331
+ coordinates: unprojectPoint(geometry[0], geometry[1])
332
+ };
333
+ case 'MultiPoint':
334
+ case 'LineString':
335
+ return {
336
+ type: type,
337
+ coordinates: unprojectPoints(geometry)
338
+ };
339
+ case 'MultiLineString':
340
+ case 'Polygon':
341
+ return {
342
+ type: type,
343
+ coordinates: geometry.map(ring => unprojectPoints(ring))
344
+ };
345
+ case 'MultiPolygon':
346
+ return {
347
+ type: type,
348
+ coordinates: geometry.map(polygon => polygon.map(ring => unprojectPoints(ring)))
349
+ };
350
+ }
351
+ }
352
+ function unprojectPoints(coords) {
353
+ const result = [];
354
+ for (let i = 0; i < coords.length; i += 3) {
355
+ result.push(unprojectPoint(coords[i], coords[i + 1]));
356
+ }
357
+ return result;
358
+ }
359
+ function unprojectPoint(x, y) {
360
+ return [unprojectX(x), unprojectY(y)];
361
+ }
362
+ /**
363
+ * Convert spherical mercator in [0..1] range to longitude
364
+ */
365
+ function unprojectX(x) {
366
+ return (x - 0.5) * 360;
367
+ }
368
+ /**
369
+ * Convert spherical mercator in [0..1] range to latitude
370
+ */
371
+ function unprojectY(y) {
372
+ const y2 = (180 - y * 360) * Math.PI / 180;
373
+ return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
374
+ }
375
+
376
+ var AxisType;
377
+ (function (AxisType) {
378
+ AxisType[AxisType["X"] = 0] = "X";
379
+ AxisType[AxisType["Y"] = 1] = "Y";
380
+ })(AxisType || (AxisType = {}));
381
+ /**
382
+ * clip features between two vertical or horizontal axis-parallel lines:
275
383
  * | |
276
384
  * ___|___ | /
277
385
  * / | \____|____/
278
386
  * | |
279
387
  *
280
- * k1 and k2 are the line coordinates
281
- * axis: 0 for x, 1 for y
282
- * minAll and maxAll: minimum and maximum coordinate value for all features
388
+ * @param features - the features to clip
389
+ * @param scale - the scale to divide start and end inputs
390
+ * @param start - the start of the clip range
391
+ * @param end - the end of the clip range
392
+ * @param axis - which axis to clip against
393
+ * @param minAll - the minimum for all features in the relevant axis
394
+ * @param maxAll - the maximum for all features in the relevant axis
283
395
  */
284
- function clip(features, scale, k1, k2, axis, minAll, maxAll, options) {
285
- k1 /= scale;
286
- k2 /= scale;
287
- if (minAll >= k1 && maxAll < k2) { // trivial accept
396
+ function clip(features, scale, start, end, axis, minAll, maxAll, options) {
397
+ start /= scale;
398
+ end /= scale;
399
+ if (minAll >= start && maxAll < end) { // trivial accept
288
400
  return features;
289
401
  }
290
- if (maxAll < k1 || minAll >= k2) { // trivial reject
402
+ if (maxAll < start || minAll >= end) { // trivial reject
291
403
  return null;
292
404
  }
293
405
  const clipped = [];
294
406
  for (const feature of features) {
295
- const min = axis === 0 ? feature.minX : feature.minY;
296
- const max = axis === 0 ? feature.maxX : feature.maxY;
297
- if (min >= k1 && max < k2) { // trivial accept
407
+ const min = axis === AxisType.X ? feature.minX : feature.minY;
408
+ const max = axis === AxisType.X ? feature.maxX : feature.maxY;
409
+ if (min >= start && max < end) { // trivial accept
298
410
  clipped.push(feature);
299
411
  continue;
300
412
  }
301
- if (max < k1 || min >= k2) { // trivial reject
413
+ if (max < start || min >= end) { // trivial reject
302
414
  continue;
303
415
  }
304
416
  switch (feature.type) {
305
417
  case 'Point':
306
418
  case 'MultiPoint': {
307
- const pointGeometry = [];
308
- clipPoints(feature.geometry, pointGeometry, k1, k2, axis);
309
- if (!pointGeometry.length)
310
- continue;
311
- const type = pointGeometry.length === 3 ? 'Point' : 'MultiPoint';
312
- clipped.push(createFeature(feature.id, type, pointGeometry, feature.tags));
419
+ clipPointFeature(feature, clipped, start, end, axis);
313
420
  continue;
314
421
  }
315
422
  case 'LineString': {
316
- const lineGeometry = [];
317
- clipLine(feature.geometry, lineGeometry, k1, k2, axis, false, options.lineMetrics);
318
- if (!lineGeometry.length)
319
- continue;
320
- if (options.lineMetrics) {
321
- for (const line of lineGeometry) {
322
- clipped.push(createFeature(feature.id, feature.type, line, feature.tags));
323
- }
324
- continue;
325
- }
326
- if (lineGeometry.length > 1) {
327
- clipped.push(createFeature(feature.id, "MultiLineString", lineGeometry, feature.tags));
328
- continue;
329
- }
330
- clipped.push(createFeature(feature.id, feature.type, lineGeometry[0], feature.tags));
423
+ clipLineStringFeature(feature, clipped, start, end, axis, options);
331
424
  continue;
332
425
  }
333
426
  case 'MultiLineString': {
334
- const multiLineGeometry = [];
335
- clipLines(feature.geometry, multiLineGeometry, k1, k2, axis, false);
336
- if (!multiLineGeometry.length)
337
- continue;
338
- if (multiLineGeometry.length === 1) {
339
- clipped.push(createFeature(feature.id, "LineString", multiLineGeometry[0], feature.tags));
340
- continue;
341
- }
342
- clipped.push(createFeature(feature.id, feature.type, multiLineGeometry, feature.tags));
427
+ clipMultiLineStringFeature(feature, clipped, start, end, axis);
343
428
  continue;
344
429
  }
345
430
  case 'Polygon': {
346
- const polygonGeometry = [];
347
- clipLines(feature.geometry, polygonGeometry, k1, k2, axis, true);
348
- if (!polygonGeometry.length)
349
- continue;
350
- clipped.push(createFeature(feature.id, feature.type, polygonGeometry, feature.tags));
431
+ clipPolygonFeature(feature, clipped, start, end, axis);
351
432
  continue;
352
433
  }
353
434
  case 'MultiPolygon': {
354
- const multiPolygonGeometry = [];
355
- for (const polygon of feature.geometry) {
356
- const newPolygon = [];
357
- clipLines(polygon, newPolygon, k1, k2, axis, true);
358
- if (newPolygon.length)
359
- multiPolygonGeometry.push(newPolygon);
360
- }
361
- if (!multiPolygonGeometry.length)
362
- continue;
363
- clipped.push(createFeature(feature.id, feature.type, multiPolygonGeometry, feature.tags));
435
+ clipMultiPolygonFeature(feature, clipped, start, end, axis);
364
436
  continue;
365
437
  }
366
438
  }
@@ -369,17 +441,73 @@ function clip(features, scale, k1, k2, axis, minAll, maxAll, options) {
369
441
  return null;
370
442
  return clipped;
371
443
  }
372
- function clipPoints(geom, newGeom, k1, k2, axis) {
444
+ function clipPointFeature(feature, clipped, start, end, axis) {
445
+ const geom = [];
446
+ clipPoints(feature.geometry, geom, start, end, axis);
447
+ if (!geom.length)
448
+ return;
449
+ const type = geom.length === 3 ? 'Point' : 'MultiPoint';
450
+ clipped.push(createFeature(feature.id, type, geom, feature.tags));
451
+ }
452
+ function clipLineStringFeature(feature, clipped, start, end, axis, options) {
453
+ const geom = [];
454
+ clipLine(feature.geometry, geom, start, end, axis, false, options.lineMetrics);
455
+ if (!geom.length)
456
+ return;
457
+ if (options.lineMetrics) {
458
+ for (const line of geom) {
459
+ clipped.push(createFeature(feature.id, 'LineString', line, feature.tags));
460
+ }
461
+ return;
462
+ }
463
+ if (geom.length > 1) {
464
+ clipped.push(createFeature(feature.id, 'MultiLineString', geom, feature.tags));
465
+ return;
466
+ }
467
+ clipped.push(createFeature(feature.id, 'LineString', geom[0], feature.tags));
468
+ }
469
+ function clipMultiLineStringFeature(feature, clipped, start, end, axis) {
470
+ const geom = [];
471
+ clipLines(feature.geometry, geom, start, end, axis, false);
472
+ if (!geom.length)
473
+ return;
474
+ if (geom.length === 1) {
475
+ clipped.push(createFeature(feature.id, 'LineString', geom[0], feature.tags));
476
+ return;
477
+ }
478
+ clipped.push(createFeature(feature.id, 'MultiLineString', geom, feature.tags));
479
+ }
480
+ function clipPolygonFeature(feature, clipped, start, end, axis) {
481
+ const geom = [];
482
+ clipLines(feature.geometry, geom, start, end, axis, true);
483
+ if (!geom.length)
484
+ return;
485
+ clipped.push(createFeature(feature.id, 'Polygon', geom, feature.tags));
486
+ }
487
+ function clipMultiPolygonFeature(feature, clipped, start, end, axis) {
488
+ const geom = [];
489
+ for (const polygon of feature.geometry) {
490
+ const newPolygon = [];
491
+ clipLines(polygon, newPolygon, start, end, axis, true);
492
+ if (!newPolygon.length)
493
+ continue;
494
+ geom.push(newPolygon);
495
+ }
496
+ if (!geom.length)
497
+ return;
498
+ clipped.push(createFeature(feature.id, 'MultiPolygon', geom, feature.tags));
499
+ }
500
+ function clipPoints(geom, newGeom, start, end, axis) {
373
501
  for (let i = 0; i < geom.length; i += 3) {
374
502
  const a = geom[i + axis];
375
- if (a >= k1 && a <= k2) {
503
+ if (a >= start && a <= end) {
376
504
  addPoint(newGeom, geom[i], geom[i + 1], geom[i + 2]);
377
505
  }
378
506
  }
379
507
  }
380
- function clipLine(geom, newGeom, k1, k2, axis, isPolygon, trackMetrics) {
508
+ function clipLine(geom, newGeom, start, end, axis, isPolygon, trackMetrics) {
381
509
  let slice = newSlice(geom);
382
- const intersect = axis === 0 ? intersectX : intersectY;
510
+ const intersect = axis === AxisType.X ? intersectX : intersectY;
383
511
  let len = geom.start;
384
512
  let segLen, t;
385
513
  for (let i = 0; i < geom.length - 3; i += 3) {
@@ -388,23 +516,23 @@ function clipLine(geom, newGeom, k1, k2, axis, isPolygon, trackMetrics) {
388
516
  const az = geom[i + 2];
389
517
  const bx = geom[i + 3];
390
518
  const by = geom[i + 4];
391
- const a = axis === 0 ? ax : ay;
392
- const b = axis === 0 ? bx : by;
519
+ const a = axis === AxisType.X ? ax : ay;
520
+ const b = axis === AxisType.X ? bx : by;
393
521
  let exited = false;
394
522
  if (trackMetrics)
395
523
  segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
396
- if (a < k1) {
524
+ if (a < start) {
397
525
  // ---|--> | (line enters the clip region from the left)
398
- if (b > k1) {
399
- t = intersect(slice, ax, ay, bx, by, k1);
526
+ if (b > start) {
527
+ t = intersect(slice, ax, ay, bx, by, start);
400
528
  if (trackMetrics)
401
529
  slice.start = len + segLen * t;
402
530
  }
403
531
  }
404
- else if (a > k2) {
532
+ else if (a > end) {
405
533
  // | <--|--- (line enters the clip region from the right)
406
- if (b < k2) {
407
- t = intersect(slice, ax, ay, bx, by, k2);
534
+ if (b < end) {
535
+ t = intersect(slice, ax, ay, bx, by, end);
408
536
  if (trackMetrics)
409
537
  slice.start = len + segLen * t;
410
538
  }
@@ -412,14 +540,14 @@ function clipLine(geom, newGeom, k1, k2, axis, isPolygon, trackMetrics) {
412
540
  else {
413
541
  addPoint(slice, ax, ay, az);
414
542
  }
415
- if (b < k1 && a >= k1) {
543
+ if (b < start && a >= start) {
416
544
  // <--|--- | or <--|-----|--- (line exits the clip region on the left)
417
- t = intersect(slice, ax, ay, bx, by, k1);
545
+ t = intersect(slice, ax, ay, bx, by, start);
418
546
  exited = true;
419
547
  }
420
- if (b > k2 && a <= k2) {
548
+ if (b > end && a <= end) {
421
549
  // | ---|--> or ---|-----|--> (line exits the clip region on the right)
422
- t = intersect(slice, ax, ay, bx, by, k2);
550
+ t = intersect(slice, ax, ay, bx, by, end);
423
551
  exited = true;
424
552
  }
425
553
  if (!isPolygon && exited) {
@@ -436,8 +564,8 @@ function clipLine(geom, newGeom, k1, k2, axis, isPolygon, trackMetrics) {
436
564
  const ax = geom[last];
437
565
  const ay = geom[last + 1];
438
566
  const az = geom[last + 2];
439
- const a = axis === 0 ? ax : ay;
440
- if (a >= k1 && a <= k2)
567
+ const a = axis === AxisType.X ? ax : ay;
568
+ if (a >= start && a <= end)
441
569
  addPoint(slice, ax, ay, az);
442
570
  // close the polygon if its endpoints are not the same after clipping
443
571
  last = slice.length - 3;
@@ -456,9 +584,9 @@ function newSlice(line) {
456
584
  slice.end = line.end;
457
585
  return slice;
458
586
  }
459
- function clipLines(geom, newGeom, k1, k2, axis, isPolygon) {
587
+ function clipLines(geom, newGeom, start, end, axis, isPolygon) {
460
588
  for (const line of geom) {
461
- clipLine(line, newGeom, k1, k2, axis, isPolygon, false);
589
+ clipLine(line, newGeom, start, end, axis, isPolygon, false);
462
590
  }
463
591
  }
464
592
  function addPoint(out, x, y, z) {
@@ -478,11 +606,11 @@ function intersectY(out, ax, ay, bx, by, y) {
478
606
  function wrap(features, options) {
479
607
  const buffer = options.buffer / options.extent;
480
608
  let merged = features;
481
- const left = clip(features, 1, -1 - buffer, buffer, 0, -1, 2, options); // left world copy
482
- const right = clip(features, 1, 1 - buffer, 2 + buffer, 0, -1, 2, options); // right world copy
609
+ const left = clip(features, 1, -1 - buffer, buffer, AxisType.X, -1, 2, options); // left world copy
610
+ const right = clip(features, 1, 1 - buffer, 2 + buffer, AxisType.X, -1, 2, options); // right world copy
483
611
  if (!left && !right)
484
612
  return merged;
485
- merged = clip(features, 1, -buffer, 1 + buffer, 0, -1, 2, options) || []; // center world copy
613
+ merged = clip(features, 1, -buffer, 1 + buffer, AxisType.X, -1, 2, options) || []; // center world copy
486
614
  if (left)
487
615
  merged = shiftFeatureCoords(left, 1).concat(merged); // merge left into center
488
616
  if (right)
@@ -538,198 +666,6 @@ function shiftCoords(points, offset) {
538
666
  return newPoints;
539
667
  }
540
668
 
541
- /**
542
- * Transforms the coordinates of each feature in the given tile from
543
- * mercator-projected space into (extent x extent) tile space.
544
- * @param tile - the tile to transform, this gets modified in place
545
- * @param extent - the tile extent (usually 4096)
546
- * @returns the transformed tile
547
- */
548
- function transformTile(tile, extent) {
549
- if (tile.transformed) {
550
- return tile;
551
- }
552
- const z2 = 1 << tile.z;
553
- const tx = tile.x;
554
- const ty = tile.y;
555
- for (const feature of tile.features) {
556
- if (feature.type === 1) {
557
- const pointGeometry = [];
558
- for (let j = 0; j < feature.geometry.length; j += 2) {
559
- pointGeometry.push(transformPoint(feature.geometry[j], feature.geometry[j + 1], extent, z2, tx, ty));
560
- }
561
- feature.geometry = pointGeometry;
562
- continue;
563
- }
564
- const geometry = [];
565
- for (const singleGeom of feature.geometry) {
566
- const ring = [];
567
- for (let k = 0; k < singleGeom.length; k += 2) {
568
- ring.push(transformPoint(singleGeom[k], singleGeom[k + 1], extent, z2, tx, ty));
569
- }
570
- geometry.push(ring);
571
- }
572
- feature.geometry = geometry;
573
- }
574
- tile.transformed = true;
575
- return tile;
576
- }
577
- function transformPoint(x, y, extent, z2, tx, ty) {
578
- return [
579
- Math.round(extent * (x * z2 - tx)),
580
- Math.round(extent * (y * z2 - ty))
581
- ];
582
- }
583
-
584
- /**
585
- * Creates a tile object from the given features
586
- * @param features - the features to include in the tile
587
- * @param z
588
- * @param tx
589
- * @param ty
590
- * @param options - the options object
591
- * @returns the created tile
592
- */
593
- function createTile(features, z, tx, ty, options) {
594
- const tolerance = z === options.maxZoom ? 0 : options.tolerance / ((1 << z) * options.extent);
595
- const tile = {
596
- features: [],
597
- numPoints: 0,
598
- numSimplified: 0,
599
- numFeatures: features.length,
600
- source: null,
601
- x: tx,
602
- y: ty,
603
- z,
604
- transformed: false,
605
- minX: 2,
606
- minY: 1,
607
- maxX: -1,
608
- maxY: 0
609
- };
610
- for (const feature of features) {
611
- addFeature(tile, feature, tolerance, options);
612
- }
613
- return tile;
614
- }
615
- function addFeature(tile, feature, tolerance, options) {
616
- tile.minX = Math.min(tile.minX, feature.minX);
617
- tile.minY = Math.min(tile.minY, feature.minY);
618
- tile.maxX = Math.max(tile.maxX, feature.maxX);
619
- tile.maxY = Math.max(tile.maxY, feature.maxY);
620
- let tags = feature.tags || null;
621
- let tileFeature;
622
- switch (feature.type) {
623
- case 'Point':
624
- case 'MultiPoint': {
625
- const geometry = [];
626
- for (let i = 0; i < feature.geometry.length; i += 3) {
627
- geometry.push(feature.geometry[i], feature.geometry[i + 1]);
628
- tile.numPoints++;
629
- tile.numSimplified++;
630
- }
631
- if (!geometry.length)
632
- return;
633
- tileFeature = {
634
- type: 1,
635
- tags: tags,
636
- geometry: geometry
637
- };
638
- break;
639
- }
640
- case 'LineString': {
641
- const geometry = [];
642
- addLine(geometry, feature.geometry, tile, tolerance, false, false);
643
- if (!geometry.length)
644
- return;
645
- if (options.lineMetrics) {
646
- tags = {};
647
- for (const key in feature.tags)
648
- tags[key] = feature.tags[key];
649
- // HM TODO: replace with geojsonvt
650
- tags['mapbox_clip_start'] = feature.geometry.start / feature.geometry.size;
651
- tags['mapbox_clip_end'] = feature.geometry.end / feature.geometry.size;
652
- }
653
- tileFeature = {
654
- type: 2,
655
- tags: tags,
656
- geometry: geometry
657
- };
658
- break;
659
- }
660
- case 'MultiLineString':
661
- case 'Polygon': {
662
- const geometry = [];
663
- for (let i = 0; i < feature.geometry.length; i++) {
664
- addLine(geometry, feature.geometry[i], tile, tolerance, feature.type === 'Polygon', i === 0);
665
- }
666
- if (!geometry.length)
667
- return;
668
- tileFeature = {
669
- type: feature.type === 'Polygon' ? 3 : 2,
670
- tags: tags,
671
- geometry: geometry
672
- };
673
- break;
674
- }
675
- case 'MultiPolygon': {
676
- const geometry = [];
677
- for (let k = 0; k < feature.geometry.length; k++) {
678
- const polygon = feature.geometry[k];
679
- for (let i = 0; i < polygon.length; i++) {
680
- addLine(geometry, polygon[i], tile, tolerance, true, i === 0);
681
- }
682
- }
683
- if (!geometry.length)
684
- return;
685
- tileFeature = {
686
- type: 3,
687
- tags: tags,
688
- geometry: geometry
689
- };
690
- break;
691
- }
692
- }
693
- if (feature.id !== null) {
694
- tileFeature.id = feature.id;
695
- }
696
- tile.features.push(tileFeature);
697
- }
698
- function addLine(result, geom, tile, tolerance, isPolygon, isOuter) {
699
- const sqTolerance = tolerance * tolerance;
700
- if (tolerance > 0 && (geom.size < (isPolygon ? sqTolerance : tolerance))) {
701
- tile.numPoints += geom.length / 3;
702
- return;
703
- }
704
- const ring = [];
705
- for (let i = 0; i < geom.length; i += 3) {
706
- if (tolerance === 0 || geom[i + 2] > sqTolerance) {
707
- tile.numSimplified++;
708
- ring.push(geom[i], geom[i + 1]);
709
- }
710
- tile.numPoints++;
711
- }
712
- if (isPolygon)
713
- rewind(ring, isOuter);
714
- result.push(ring);
715
- }
716
- function rewind(ring, clockwise) {
717
- let area = 0;
718
- for (let i = 0, len = ring.length, j = len - 2; i < len; j = i, i += 2) {
719
- area += (ring[i] - ring[j]) * (ring[i + 1] + ring[j + 1]);
720
- }
721
- if (area > 0 !== clockwise)
722
- return;
723
- for (let i = 0, len = ring.length; i < len / 2; i += 2) {
724
- const x = ring[i];
725
- const y = ring[i + 1];
726
- ring[i] = ring[len - 2 - i];
727
- ring[i + 1] = ring[len - 1 - i];
728
- ring[len - 2 - i] = x;
729
- ring[len - 1 - i] = y;
730
- }
731
- }
732
-
733
669
  /**
734
670
  * Applies a GeoJSON Source Diff to an existing set of simplified features
735
671
  * @param source
@@ -771,7 +707,7 @@ function applySourceDiff(source, dataDiff, options) {
771
707
  // convert and add new features
772
708
  if (diff.add.size) {
773
709
  // projects and adds simplification info
774
- let addFeatures = convert({ type: 'FeatureCollection', features: Array.from(diff.add.values()) }, options);
710
+ let addFeatures = convertToInternal({ type: 'FeatureCollection', features: Array.from(diff.add.values()) }, options);
775
711
  // wraps features (ie extreme west and extreme east)
776
712
  addFeatures = wrap(addFeatures, options);
777
713
  affected.push(...addFeatures);
@@ -811,7 +747,7 @@ function getUpdatedFeature(vtFeature, update, options) {
811
747
  properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
812
748
  };
813
749
  // projects and adds simplification info
814
- let features = convert({ type: 'FeatureCollection', features: [geojsonFeature] }, options);
750
+ let features = convertToInternal({ type: 'FeatureCollection', features: [geojsonFeature] }, options);
815
751
  // wraps features (ie extreme west and extreme east)
816
752
  features = wrap(features, options);
817
753
  return features[0];
@@ -863,70 +799,771 @@ function diffToHashed(diff) {
863
799
  return hashed;
864
800
  }
865
801
 
866
- const defaultOptions = {
867
- maxZoom: 14,
868
- indexMaxZoom: 5,
869
- indexMaxPoints: 100000,
870
- tolerance: 3,
871
- extent: 4096,
872
- buffer: 64,
873
- lineMetrics: false,
874
- promoteId: null,
802
+ const defaultClusterOptions = {
803
+ minZoom: 0,
804
+ maxZoom: 16,
805
+ minPoints: 2,
806
+ radius: 40,
807
+ extent: 512,
808
+ nodeSize: 64,
809
+ log: false,
875
810
  generateId: false,
876
- updateable: false,
877
- debug: 0
811
+ reduce: null,
812
+ map: (props) => props
878
813
  };
814
+ const OFFSET_ZOOM = 2;
815
+ const OFFSET_ID = 3;
816
+ const OFFSET_PARENT = 4;
817
+ const OFFSET_NUM = 5;
818
+ const OFFSET_PROP = 6;
879
819
  /**
880
- * Main class for creating and managing a vector tile index from GeoJSON data.
820
+ * This class allow clustering of geojson points.
881
821
  */
882
- class GeoJSONVT {
822
+ class ClusterTileIndex {
883
823
  options;
884
- /** @internal */
885
- tiles;
886
- tileCoords;
887
- /** @internal */
888
- stats = {};
889
- /** @internal */
824
+ trees;
825
+ stride;
826
+ clusterProps;
827
+ points;
828
+ constructor(options) {
829
+ this.options = Object.assign(Object.create(defaultClusterOptions), options);
830
+ this.trees = new Array(this.options.maxZoom + 1);
831
+ this.stride = this.options.reduce ? 7 : 6;
832
+ this.clusterProps = [];
833
+ this.points = [];
834
+ }
835
+ /**
836
+ * Loads GeoJSON point features and builds the internal clustering index.
837
+ * @param points - GeoJSON point features to cluster.
838
+ */
839
+ load(points) {
840
+ const features = [];
841
+ // Convert GeoJSON point features to GeoJSONVT internal point features
842
+ for (const point of points) {
843
+ if (!point.geometry) {
844
+ continue;
845
+ }
846
+ const [lng, lat] = point.geometry.coordinates;
847
+ const [x, y] = [projectX(lng), projectY(lat)];
848
+ const feature = {
849
+ id: point.id,
850
+ type: 'Point',
851
+ geometry: [x, y],
852
+ tags: point.properties
853
+ };
854
+ features.push(feature);
855
+ }
856
+ this.createIndex(features);
857
+ }
858
+ /**
859
+ * @internal
860
+ * Loads internal GeoJSONVT point features from a data source and builds the clustering index.
861
+ * @param features - {@link GeoJSONVTInternalFeature} data source features to filter and cluster.
862
+ */
863
+ initialize(features) {
864
+ const points = [];
865
+ for (const feature of features) {
866
+ if (feature.type !== 'Point')
867
+ continue;
868
+ points.push(feature);
869
+ }
870
+ this.createIndex(points);
871
+ }
872
+ /**
873
+ * @internal
874
+ * Updates the cluster data by rebuilding.
875
+ * @param features
876
+ */
877
+ updateIndex(features, _affected, options) {
878
+ this.options = Object.assign(Object.create(defaultClusterOptions), options.clusterOptions);
879
+ this.initialize(features);
880
+ }
881
+ createIndex(points) {
882
+ const { log, minZoom, maxZoom } = this.options;
883
+ if (log)
884
+ console.time('total time');
885
+ const timerId = `prepare ${points.length} points`;
886
+ if (log)
887
+ console.time(timerId);
888
+ this.points = points;
889
+ // generate a cluster object for each point and index input points into a KD-tree
890
+ const data = [];
891
+ for (let i = 0; i < points.length; i++) {
892
+ const p = points[i];
893
+ if (!p?.geometry)
894
+ continue;
895
+ let [x, y] = p.geometry;
896
+ x = Math.fround(x);
897
+ y = Math.fround(y);
898
+ // store internal point/cluster data in flat numeric arrays for performance
899
+ data.push(x, y, // projected point coordinates
900
+ Infinity, // the last zoom the point was processed at
901
+ i, // index of the source feature in the original input array
902
+ -1, // parent cluster id
903
+ 1 // number of points in a cluster
904
+ );
905
+ if (this.options.reduce)
906
+ data.push(0); // noop
907
+ }
908
+ let tree = this.trees[maxZoom + 1] = this.createTree(data);
909
+ if (log)
910
+ console.timeEnd(timerId);
911
+ // cluster points on max zoom, then cluster the results on previous zoom, etc.;
912
+ // results in a cluster hierarchy across zoom levels
913
+ for (let z = maxZoom; z >= minZoom; z--) {
914
+ const now = Date.now();
915
+ // create a new set of clusters for the zoom and index them with a KD-tree
916
+ tree = this.trees[z] = this.createTree(this.cluster(tree, z));
917
+ if (log)
918
+ console.log('z%d: %d clusters in %dms', z, tree.numItems, Date.now() - now);
919
+ }
920
+ if (log)
921
+ console.timeEnd('total time');
922
+ }
923
+ /**
924
+ * Returns clusters and/or points within a bounding box at a given zoom level.
925
+ * @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order.
926
+ * @param zoom - Zoom level to query.
927
+ */
928
+ getClusters(bbox, zoom) {
929
+ const clusterInternal = this.getClustersInternal(bbox, zoom);
930
+ return clusterInternal.map((f) => featureToGeoJSON(f));
931
+ }
932
+ getClustersInternal(bbox, zoom) {
933
+ let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
934
+ const minLat = Math.max(-90, Math.min(90, bbox[1]));
935
+ let maxLng = bbox[2] === 180 ? 180 : ((bbox[2] + 180) % 360 + 360) % 360 - 180;
936
+ const maxLat = Math.max(-90, Math.min(90, bbox[3]));
937
+ if (bbox[2] - bbox[0] >= 360) {
938
+ minLng = -180;
939
+ maxLng = 180;
940
+ }
941
+ else if (minLng > maxLng) {
942
+ const easternHem = this.getClustersInternal([minLng, minLat, 180, maxLat], zoom);
943
+ const westernHem = this.getClustersInternal([-180, minLat, maxLng, maxLat], zoom);
944
+ return easternHem.concat(westernHem);
945
+ }
946
+ const tree = this.trees[this.limitZoom(zoom)];
947
+ const ids = tree.range(projectX(minLng), projectY(maxLat), projectX(maxLng), projectY(minLat));
948
+ const data = tree.flatData;
949
+ const clusters = [];
950
+ for (const id of ids) {
951
+ const k = this.stride * id;
952
+ clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
953
+ }
954
+ return clusters;
955
+ }
956
+ /**
957
+ * Returns the immediate children (clusters or points) of a cluster as GeoJSON.
958
+ * @param clusterId - The target cluster id.
959
+ */
960
+ getChildren(clusterId) {
961
+ const originId = this.getOriginId(clusterId);
962
+ const originZoom = this.getOriginZoom(clusterId);
963
+ const clusterError = new Error('No cluster with the specified id: ' + clusterId);
964
+ const tree = this.trees[originZoom];
965
+ if (!tree)
966
+ throw clusterError;
967
+ const data = tree.flatData;
968
+ if (originId * this.stride >= data.length)
969
+ throw clusterError;
970
+ const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
971
+ const x = data[originId * this.stride];
972
+ const y = data[originId * this.stride + 1];
973
+ const ids = tree.within(x, y, r);
974
+ const children = [];
975
+ for (const id of ids) {
976
+ const k = id * this.stride;
977
+ if (data[k + OFFSET_PARENT] === clusterId) {
978
+ children.push(data[k + OFFSET_NUM] > 1 ? getClusterGeoJSON(data, k, this.clusterProps) : featureToGeoJSON(this.points[data[k + OFFSET_ID]]));
979
+ }
980
+ }
981
+ if (children.length === 0)
982
+ throw clusterError;
983
+ return children;
984
+ }
985
+ /**
986
+ * Returns leaf point features under a cluster, paginated by `limit` and `offset`.
987
+ * @param clusterId - The target cluster id.
988
+ * @param limit - Maximum number of points to return (defaults to `10`).
989
+ * @param offset - Number of points to skip before collecting results (defaults to `0`).
990
+ */
991
+ getLeaves(clusterId, limit, offset) {
992
+ limit = limit || 10;
993
+ offset = offset || 0;
994
+ const leaves = [];
995
+ this.appendLeaves(leaves, clusterId, limit, offset, 0);
996
+ return leaves;
997
+ }
998
+ /**
999
+ * Generates a vector-tile-like representation of a single tile.
1000
+ * @param z - Tile zoom.
1001
+ * @param x - Tile x coordinate.
1002
+ * @param y - Tile y coordinate.
1003
+ */
1004
+ getTile(z, x, y) {
1005
+ const tree = this.trees[this.limitZoom(z)];
1006
+ const z2 = Math.pow(2, z);
1007
+ const { extent, radius } = this.options;
1008
+ const p = radius / extent;
1009
+ const top = (y - p) / z2;
1010
+ const bottom = (y + 1 + p) / z2;
1011
+ const tile = {
1012
+ transformed: true,
1013
+ features: [],
1014
+ source: null,
1015
+ x: x,
1016
+ y: y,
1017
+ z: z
1018
+ };
1019
+ this.addTileFeatures(tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), tree.flatData, x, y, z2, tile);
1020
+ if (x === 0) {
1021
+ this.addTileFeatures(tree.range(1 - p / z2, top, 1, bottom), tree.flatData, z2, y, z2, tile);
1022
+ }
1023
+ if (x === z2 - 1) {
1024
+ this.addTileFeatures(tree.range(0, top, p / z2, bottom), tree.flatData, -1, y, z2, tile);
1025
+ }
1026
+ return tile;
1027
+ }
1028
+ /**
1029
+ * Returns the zoom level at which a cluster expands into multiple children.
1030
+ * @param clusterId - The target cluster id.
1031
+ */
1032
+ getClusterExpansionZoom(clusterId) {
1033
+ return this.getOriginZoom(clusterId);
1034
+ }
1035
+ appendLeaves(result, clusterId, limit, offset, skipped) {
1036
+ const children = this.getChildren(clusterId);
1037
+ for (const child of children) {
1038
+ const props = child.properties;
1039
+ if (props?.cluster) {
1040
+ if (skipped + props.point_count <= offset) {
1041
+ // skip the whole cluster
1042
+ skipped += props.point_count;
1043
+ }
1044
+ else {
1045
+ // enter the cluster
1046
+ skipped = this.appendLeaves(result, props.cluster_id, limit, offset, skipped);
1047
+ // exit the cluster
1048
+ }
1049
+ }
1050
+ else if (skipped < offset) {
1051
+ // skip a single point
1052
+ skipped++;
1053
+ }
1054
+ else {
1055
+ // add a single point
1056
+ result.push(child);
1057
+ }
1058
+ if (result.length === limit)
1059
+ break;
1060
+ }
1061
+ return skipped;
1062
+ }
1063
+ createTree(data) {
1064
+ const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array);
1065
+ for (let i = 0; i < data.length; i += this.stride)
1066
+ tree.add(data[i], data[i + 1]);
1067
+ tree.finish();
1068
+ tree.flatData = data;
1069
+ tree.data = null; // clear original data to free memory as it isn't used later on.
1070
+ return tree;
1071
+ }
1072
+ addTileFeatures(ids, data, x, y, z2, tile) {
1073
+ for (const i of ids) {
1074
+ const k = i * this.stride;
1075
+ const isCluster = data[k + OFFSET_NUM] > 1;
1076
+ let tags;
1077
+ let px;
1078
+ let py;
1079
+ if (isCluster) {
1080
+ tags = getClusterProperties(data, k, this.clusterProps);
1081
+ px = data[k];
1082
+ py = data[k + 1];
1083
+ }
1084
+ else {
1085
+ const p = this.points[data[k + OFFSET_ID]];
1086
+ tags = p.tags;
1087
+ [px, py] = p.geometry;
1088
+ }
1089
+ const f = {
1090
+ type: 1,
1091
+ geometry: [[
1092
+ Math.round(this.options.extent * (px * z2 - x)),
1093
+ Math.round(this.options.extent * (py * z2 - y))
1094
+ ]],
1095
+ tags
1096
+ };
1097
+ // assign id
1098
+ let id;
1099
+ if (isCluster || this.options.generateId) {
1100
+ // optionally generate id for points
1101
+ id = data[k + OFFSET_ID];
1102
+ }
1103
+ else {
1104
+ // keep id if already assigned
1105
+ id = this.points[data[k + OFFSET_ID]].id;
1106
+ }
1107
+ if (id !== undefined)
1108
+ f.id = id;
1109
+ tile.features.push(f);
1110
+ }
1111
+ }
1112
+ limitZoom(z) {
1113
+ return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
1114
+ }
1115
+ cluster(tree, zoom) {
1116
+ const { radius, extent, reduce, minPoints } = this.options;
1117
+ const r = radius / (extent * Math.pow(2, zoom));
1118
+ const data = tree.flatData;
1119
+ const nextData = [];
1120
+ const stride = this.stride;
1121
+ // loop through each point
1122
+ for (let i = 0; i < data.length; i += stride) {
1123
+ // if we've already visited the point at this zoom level, skip it
1124
+ if (data[i + OFFSET_ZOOM] <= zoom)
1125
+ continue;
1126
+ data[i + OFFSET_ZOOM] = zoom;
1127
+ // find all nearby points
1128
+ const x = data[i];
1129
+ const y = data[i + 1];
1130
+ const neighborIds = tree.within(data[i], data[i + 1], r);
1131
+ const numPointsOrigin = data[i + OFFSET_NUM];
1132
+ let numPoints = numPointsOrigin;
1133
+ // count the number of points in a potential cluster
1134
+ for (const neighborId of neighborIds) {
1135
+ const k = neighborId * stride;
1136
+ // filter out neighbors that are already processed
1137
+ if (data[k + OFFSET_ZOOM] > zoom)
1138
+ numPoints += data[k + OFFSET_NUM];
1139
+ }
1140
+ // if there were neighbors to merge, and there are enough points to form a cluster
1141
+ if (numPoints > numPointsOrigin && numPoints >= minPoints) {
1142
+ let wx = x * numPointsOrigin;
1143
+ let wy = y * numPointsOrigin;
1144
+ let clusterProperties;
1145
+ let clusterPropIndex = -1;
1146
+ // encode both zoom and point index on which the cluster originated -- offset by total length of features
1147
+ const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length;
1148
+ for (const neighborId of neighborIds) {
1149
+ const k = neighborId * stride;
1150
+ if (data[k + OFFSET_ZOOM] <= zoom)
1151
+ continue;
1152
+ data[k + OFFSET_ZOOM] = zoom; // save the zoom (so it doesn't get processed twice)
1153
+ const numPoints2 = data[k + OFFSET_NUM];
1154
+ wx += data[k] * numPoints2; // accumulate coordinates for calculating weighted center
1155
+ wy += data[k + 1] * numPoints2;
1156
+ data[k + OFFSET_PARENT] = id;
1157
+ if (reduce) {
1158
+ if (!clusterProperties) {
1159
+ clusterProperties = this.map(data, i, true);
1160
+ clusterPropIndex = this.clusterProps.length;
1161
+ this.clusterProps.push(clusterProperties);
1162
+ }
1163
+ reduce(clusterProperties, this.map(data, k));
1164
+ }
1165
+ }
1166
+ data[i + OFFSET_PARENT] = id;
1167
+ nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints);
1168
+ if (reduce)
1169
+ nextData.push(clusterPropIndex);
1170
+ }
1171
+ else { // left points as unclustered
1172
+ for (let j = 0; j < stride; j++)
1173
+ nextData.push(data[i + j]);
1174
+ if (numPoints > 1) {
1175
+ for (const neighborId of neighborIds) {
1176
+ const k = neighborId * stride;
1177
+ if (data[k + OFFSET_ZOOM] <= zoom)
1178
+ continue;
1179
+ data[k + OFFSET_ZOOM] = zoom;
1180
+ for (let j = 0; j < stride; j++)
1181
+ nextData.push(data[k + j]);
1182
+ }
1183
+ }
1184
+ }
1185
+ }
1186
+ return nextData;
1187
+ }
1188
+ // get index of the point from which the cluster originated
1189
+ getOriginId(clusterId) {
1190
+ return (clusterId - this.points.length) >> 5;
1191
+ }
1192
+ // get zoom of the point from which the cluster originated
1193
+ getOriginZoom(clusterId) {
1194
+ return (clusterId - this.points.length) % 32;
1195
+ }
1196
+ map(data, i, clone) {
1197
+ if (data[i + OFFSET_NUM] > 1) {
1198
+ const props = this.clusterProps[data[i + OFFSET_PROP]];
1199
+ return clone ? Object.assign({}, props) : props;
1200
+ }
1201
+ const original = this.points[data[i + OFFSET_ID]].tags;
1202
+ const result = this.options.map(original);
1203
+ return clone && result === original ? Object.assign({}, result) : result;
1204
+ }
1205
+ }
1206
+ function getClusterFeature(data, i, clusterProps) {
1207
+ return {
1208
+ id: data[i + OFFSET_ID],
1209
+ type: 'Point',
1210
+ tags: getClusterProperties(data, i, clusterProps),
1211
+ geometry: [data[i], data[i + 1]]
1212
+ };
1213
+ }
1214
+ function getClusterGeoJSON(data, i, clusterProps) {
1215
+ return {
1216
+ type: 'Feature',
1217
+ id: data[i + OFFSET_ID],
1218
+ properties: getClusterProperties(data, i, clusterProps),
1219
+ geometry: {
1220
+ type: 'Point',
1221
+ coordinates: [unprojectX(data[i]), unprojectY(data[i + 1])]
1222
+ }
1223
+ };
1224
+ }
1225
+ function getClusterProperties(data, i, clusterProps) {
1226
+ const count = data[i + OFFSET_NUM];
1227
+ const abbrev = count >= 10000 ? `${Math.round(count / 1000)}k` :
1228
+ count >= 1000 ? `${Math.round(count / 100) / 10}k` : count;
1229
+ const propIndex = data[i + OFFSET_PROP];
1230
+ const properties = propIndex === -1 ? {} : Object.assign({}, clusterProps[propIndex]);
1231
+ return Object.assign(properties, {
1232
+ cluster: true,
1233
+ cluster_id: data[i + OFFSET_ID],
1234
+ point_count: count,
1235
+ point_count_abbreviated: abbrev
1236
+ });
1237
+ }
1238
+
1239
+ /**
1240
+ * Creates a tile object from the given features
1241
+ * @param features - the features to include in the tile
1242
+ * @param z
1243
+ * @param tx
1244
+ * @param ty
1245
+ * @param options - the options object
1246
+ * @returns the created tile
1247
+ */
1248
+ function createTile(features, z, tx, ty, options) {
1249
+ const tolerance = z === options.maxZoom ? 0 : options.tolerance / ((1 << z) * options.extent);
1250
+ const tile = {
1251
+ transformed: false,
1252
+ features: [],
1253
+ source: null,
1254
+ x: tx,
1255
+ y: ty,
1256
+ z: z,
1257
+ minX: 2,
1258
+ minY: 1,
1259
+ maxX: -1,
1260
+ maxY: 0,
1261
+ numPoints: 0,
1262
+ numSimplified: 0,
1263
+ numFeatures: features.length
1264
+ };
1265
+ for (const feature of features) {
1266
+ addFeature(tile, feature, tolerance, options);
1267
+ }
1268
+ return tile;
1269
+ }
1270
+ function addFeature(tile, feature, tolerance, options) {
1271
+ tile.minX = Math.min(tile.minX, feature.minX);
1272
+ tile.minY = Math.min(tile.minY, feature.minY);
1273
+ tile.maxX = Math.max(tile.maxX, feature.maxX);
1274
+ tile.maxY = Math.max(tile.maxY, feature.maxY);
1275
+ switch (feature.type) {
1276
+ case 'Point':
1277
+ case 'MultiPoint':
1278
+ addPointsTileFeature(tile, feature);
1279
+ return;
1280
+ case 'LineString':
1281
+ addLineTileFeautre(tile, feature, tolerance, options);
1282
+ return;
1283
+ case 'MultiLineString':
1284
+ case 'Polygon':
1285
+ addLinesTileFeature(tile, feature, tolerance);
1286
+ return;
1287
+ case 'MultiPolygon':
1288
+ addMultiPolygonTileFeature(tile, feature, tolerance);
1289
+ return;
1290
+ }
1291
+ }
1292
+ function addPointsTileFeature(tile, feature) {
1293
+ const geometry = [];
1294
+ for (let i = 0; i < feature.geometry.length; i += 3) {
1295
+ geometry.push(feature.geometry[i], feature.geometry[i + 1]);
1296
+ tile.numPoints++;
1297
+ tile.numSimplified++;
1298
+ }
1299
+ if (!geometry.length)
1300
+ return;
1301
+ const tileFeature = {
1302
+ type: 1,
1303
+ tags: feature.tags || null,
1304
+ geometry: geometry
1305
+ };
1306
+ if (feature.id !== null) {
1307
+ tileFeature.id = feature.id;
1308
+ }
1309
+ tile.features.push(tileFeature);
1310
+ }
1311
+ function addLineTileFeautre(tile, feature, tolerance, options) {
1312
+ const geometry = [];
1313
+ addLine(geometry, feature.geometry, tile, tolerance, false, false);
1314
+ if (!geometry.length)
1315
+ return;
1316
+ let tags = feature.tags || null;
1317
+ if (options.lineMetrics) {
1318
+ tags = {};
1319
+ for (const key in feature.tags)
1320
+ tags[key] = feature.tags[key];
1321
+ tags['geojsonvt_clip_start'] = feature.geometry.start / feature.geometry.size;
1322
+ tags['geojsonvt_clip_end'] = feature.geometry.end / feature.geometry.size;
1323
+ }
1324
+ const tileFeature = {
1325
+ type: 2,
1326
+ tags: tags,
1327
+ geometry: geometry
1328
+ };
1329
+ if (feature.id !== null) {
1330
+ tileFeature.id = feature.id;
1331
+ }
1332
+ tile.features.push(tileFeature);
1333
+ }
1334
+ function addLinesTileFeature(tile, feature, tolerance) {
1335
+ const geometry = [];
1336
+ for (let i = 0; i < feature.geometry.length; i++) {
1337
+ addLine(geometry, feature.geometry[i], tile, tolerance, feature.type === 'Polygon', i === 0);
1338
+ }
1339
+ if (!geometry.length)
1340
+ return;
1341
+ const tileFeature = {
1342
+ type: feature.type === 'Polygon' ? 3 : 2,
1343
+ tags: feature.tags || null,
1344
+ geometry: geometry
1345
+ };
1346
+ if (feature.id !== null) {
1347
+ tileFeature.id = feature.id;
1348
+ }
1349
+ tile.features.push(tileFeature);
1350
+ }
1351
+ function addMultiPolygonTileFeature(tile, feature, tolerance) {
1352
+ const geometry = [];
1353
+ for (let k = 0; k < feature.geometry.length; k++) {
1354
+ const polygon = feature.geometry[k];
1355
+ for (let i = 0; i < polygon.length; i++) {
1356
+ addLine(geometry, polygon[i], tile, tolerance, true, i === 0);
1357
+ }
1358
+ }
1359
+ if (!geometry.length)
1360
+ return;
1361
+ const tileFeature = {
1362
+ type: 3,
1363
+ tags: feature.tags || null,
1364
+ geometry: geometry
1365
+ };
1366
+ if (feature.id !== null) {
1367
+ tileFeature.id = feature.id;
1368
+ }
1369
+ tile.features.push(tileFeature);
1370
+ }
1371
+ function addLine(result, geom, tile, tolerance, isPolygon, isOuter) {
1372
+ const sqTolerance = tolerance * tolerance;
1373
+ if (tolerance > 0 && (geom.size < (isPolygon ? sqTolerance : tolerance))) {
1374
+ tile.numPoints += geom.length / 3;
1375
+ return;
1376
+ }
1377
+ const ring = [];
1378
+ for (let i = 0; i < geom.length; i += 3) {
1379
+ if (tolerance === 0 || geom[i + 2] > sqTolerance) {
1380
+ tile.numSimplified++;
1381
+ ring.push(geom[i], geom[i + 1]);
1382
+ }
1383
+ tile.numPoints++;
1384
+ }
1385
+ if (isPolygon)
1386
+ rewind(ring, isOuter);
1387
+ result.push(ring);
1388
+ }
1389
+ function rewind(ring, clockwise) {
1390
+ let area = 0;
1391
+ for (let i = 0, len = ring.length, j = len - 2; i < len; j = i, i += 2) {
1392
+ area += (ring[i] - ring[j]) * (ring[i + 1] + ring[j + 1]);
1393
+ }
1394
+ if (area > 0 !== clockwise)
1395
+ return;
1396
+ for (let i = 0, len = ring.length; i < len / 2; i += 2) {
1397
+ const x = ring[i];
1398
+ const y = ring[i + 1];
1399
+ ring[i] = ring[len - 2 - i];
1400
+ ring[i + 1] = ring[len - 1 - i];
1401
+ ring[len - 2 - i] = x;
1402
+ ring[len - 1 - i] = y;
1403
+ }
1404
+ }
1405
+
1406
+ /**
1407
+ * Transforms the coordinates of each feature in the given tile from
1408
+ * mercator-projected space into (extent x extent) tile space.
1409
+ * @param tile - the tile to transform, this gets modified in place
1410
+ * @param extent - the tile extent (usually 4096)
1411
+ * @returns the transformed tile
1412
+ */
1413
+ function transformTile(tile, extent) {
1414
+ if (tile.transformed) {
1415
+ return tile;
1416
+ }
1417
+ const z2 = 1 << tile.z;
1418
+ const tx = tile.x;
1419
+ const ty = tile.y;
1420
+ for (const feature of tile.features) {
1421
+ if (feature.type === 1) {
1422
+ transformPointFeature(feature, extent, z2, tx, ty);
1423
+ }
1424
+ else {
1425
+ transformNonPointFeature(feature, extent, z2, tx, ty);
1426
+ }
1427
+ }
1428
+ tile.transformed = true;
1429
+ return tile;
1430
+ }
1431
+ /**
1432
+ * Transforms a single point feature from mercator-projected space into (extent x extent) tile space.
1433
+ */
1434
+ function transformPointFeature(feature, extent, z2, tx, ty) {
1435
+ const transformed = feature;
1436
+ const geometry = feature.geometry;
1437
+ const point = [];
1438
+ for (let i = 0; i < geometry.length; i += 2) {
1439
+ point.push(transformPoint(geometry[i], geometry[i + 1], extent, z2, tx, ty));
1440
+ }
1441
+ transformed.geometry = point;
1442
+ return transformed;
1443
+ }
1444
+ /**
1445
+ * Transforms a single non-point feature from mercator-projected space into (extent x extent) tile space.
1446
+ */
1447
+ function transformNonPointFeature(feature, extent, z2, tx, ty) {
1448
+ const transformed = feature;
1449
+ const geometry = feature.geometry;
1450
+ const nonPoint = [];
1451
+ for (const geom of geometry) {
1452
+ const ring = [];
1453
+ for (let i = 0; i < geom.length; i += 2) {
1454
+ ring.push(transformPoint(geom[i], geom[i + 1], extent, z2, tx, ty));
1455
+ }
1456
+ nonPoint.push(ring);
1457
+ }
1458
+ transformed.geometry = nonPoint;
1459
+ return transformed;
1460
+ }
1461
+ function transformPoint(x, y, extent, z2, tx, ty) {
1462
+ return [
1463
+ Math.round(extent * (x * z2 - tx)),
1464
+ Math.round(extent * (y * z2 - ty))
1465
+ ];
1466
+ }
1467
+
1468
+ class TileIndex {
1469
+ options;
1470
+ tileCoords;
1471
+ /** @internal */
1472
+ tiles;
1473
+ /** @internal */
1474
+ stats = {};
1475
+ /** @internal */
890
1476
  total = 0;
891
- source;
892
- constructor(data, options) {
893
- options = this.options = Object.assign({}, defaultOptions, options);
894
- const debug = options.debug;
895
- if (debug)
896
- console.time('preprocess data');
897
- if (options.maxZoom < 0 || options.maxZoom > 24)
898
- throw new Error('maxZoom should be in the 0-24 range');
899
- if (options.promoteId && options.generateId)
900
- throw new Error('promoteId and generateId cannot be used together.');
901
- // projects and adds simplification info
902
- let features = convert(data, options);
903
- // tiles and tileCoords are part of the public API
1477
+ constructor(options) {
1478
+ this.options = options;
904
1479
  this.tiles = {};
905
1480
  this.tileCoords = [];
906
- if (debug) {
907
- console.timeEnd('preprocess data');
908
- console.log('index: maxZoom: %d, maxPoints: %d', options.indexMaxZoom, options.indexMaxPoints);
909
- console.time('generate tiles');
910
- this.stats = {};
911
- this.total = 0;
912
- }
913
- // wraps features (ie extreme west and extreme east)
914
- features = wrap(features, options);
1481
+ this.stats = {};
1482
+ this.total = 0;
1483
+ }
1484
+ initialize(features) {
915
1485
  // start slicing from the top tile down
916
- if (features.length) {
917
- this.splitTile(features, 0, 0, 0);
918
- }
919
- // for updateable indexes, store a copy of the original simplified features
920
- if (options.updateable) {
921
- this.source = features;
922
- }
923
- if (debug) {
1486
+ this.splitTile(features, 0, 0, 0);
1487
+ if (this.options.debug) {
924
1488
  if (features.length)
925
1489
  console.log('features: %d, points: %d', this.tiles[0].numFeatures, this.tiles[0].numPoints);
926
1490
  console.timeEnd('generate tiles');
927
1491
  console.log('tiles generated:', this.total, JSON.stringify(this.stats));
928
1492
  }
929
1493
  }
1494
+ /** {@inheritdoc} */
1495
+ updateIndex(source, affected, options) {
1496
+ if (options.debug > 1) {
1497
+ console.log('invalidating tiles');
1498
+ console.time('invalidating');
1499
+ }
1500
+ this.invalidateTiles(affected);
1501
+ if (options.debug > 1)
1502
+ console.timeEnd('invalidating');
1503
+ // re-generate root tile with updated feature set
1504
+ const [z, x, y] = [0, 0, 0];
1505
+ const rootTile = createTile(source, z, x, y, options);
1506
+ rootTile.source = source;
1507
+ // update tile index with new root tile - ready for getTile calls
1508
+ const id = toID(z, x, y);
1509
+ this.tiles[id] = rootTile;
1510
+ this.tileCoords.push({ z, x, y, id });
1511
+ if (options.debug) {
1512
+ const key = `z${z}`;
1513
+ this.stats[key] = (this.stats[key] || 0) + 1;
1514
+ this.total++;
1515
+ }
1516
+ }
1517
+ /** {@inheritdoc} */
1518
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1519
+ getClusterExpansionZoom(_clusterId) {
1520
+ return null;
1521
+ }
1522
+ /** {@inheritdoc} */
1523
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1524
+ getChildren(_clusterId) {
1525
+ return null;
1526
+ }
1527
+ /** {@inheritdoc} */
1528
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1529
+ getLeaves(_clusterId, _limit, _offset) {
1530
+ return null;
1531
+ }
1532
+ /** {@inheritdoc} */
1533
+ getTile(z, x, y) {
1534
+ const { extent, debug } = this.options;
1535
+ const z2 = 1 << z;
1536
+ x = (x + z2) & (z2 - 1); // wrap tile x coordinate
1537
+ const id = toID(z, x, y);
1538
+ if (this.tiles[id]) {
1539
+ return transformTile(this.tiles[id], extent);
1540
+ }
1541
+ if (debug > 1)
1542
+ console.log('drilling down to z%d-%d-%d', z, x, y);
1543
+ let z0 = z;
1544
+ let x0 = x;
1545
+ let y0 = y;
1546
+ let parent;
1547
+ while (!parent && z0 > 0) {
1548
+ z0--;
1549
+ x0 = x0 >> 1;
1550
+ y0 = y0 >> 1;
1551
+ parent = this.tiles[toID(z0, x0, y0)];
1552
+ }
1553
+ if (!parent?.source)
1554
+ return null;
1555
+ // if we found a parent tile containing the original geometry, we can drill down from it
1556
+ if (debug > 1) {
1557
+ console.log('found parent tile z%d-%d-%d', z0, x0, y0);
1558
+ console.time('drilling down');
1559
+ }
1560
+ this.splitTile(parent.source, z0, x0, y0, z, x, y);
1561
+ if (debug > 1)
1562
+ console.timeEnd('drilling down');
1563
+ if (!this.tiles[id])
1564
+ return null;
1565
+ return transformTile(this.tiles[id], extent);
1566
+ }
930
1567
  /**
931
1568
  * splits features from a parent tile to sub-tiles.
932
1569
  * z, x, and y are the coordinates of the parent tile
@@ -1005,15 +1642,15 @@ class GeoJSONVT {
1005
1642
  let bl = null;
1006
1643
  let tr = null;
1007
1644
  let br = null;
1008
- const left = clip(features, z2, x - k1, x + k3, 0, tile.minX, tile.maxX, options);
1009
- const right = clip(features, z2, x + k2, x + k4, 0, tile.minX, tile.maxX, options);
1645
+ const left = clip(features, z2, x - k1, x + k3, AxisType.X, tile.minX, tile.maxX, options);
1646
+ const right = clip(features, z2, x + k2, x + k4, AxisType.X, tile.minX, tile.maxX, options);
1010
1647
  if (left) {
1011
- tl = clip(left, z2, y - k1, y + k3, 1, tile.minY, tile.maxY, options);
1012
- bl = clip(left, z2, y + k2, y + k4, 1, tile.minY, tile.maxY, options);
1648
+ tl = clip(left, z2, y - k1, y + k3, AxisType.Y, tile.minY, tile.maxY, options);
1649
+ bl = clip(left, z2, y + k2, y + k4, AxisType.Y, tile.minY, tile.maxY, options);
1013
1650
  }
1014
1651
  if (right) {
1015
- tr = clip(right, z2, y - k1, y + k3, 1, tile.minY, tile.maxY, options);
1016
- br = clip(right, z2, y + k2, y + k4, 1, tile.minY, tile.maxY, options);
1652
+ tr = clip(right, z2, y - k1, y + k3, AxisType.Y, tile.minY, tile.maxY, options);
1653
+ br = clip(right, z2, y + k2, y + k4, AxisType.Y, tile.minY, tile.maxY, options);
1017
1654
  }
1018
1655
  if (debug > 1)
1019
1656
  console.timeEnd('clipping');
@@ -1023,59 +1660,14 @@ class GeoJSONVT {
1023
1660
  stack.push(br || [], z + 1, x * 2 + 1, y * 2 + 1);
1024
1661
  }
1025
1662
  }
1026
- /**
1027
- * Given z, x, and y tile coordinates, returns the corresponding tile with geometries in tile coordinates, much like MVT data is stored.
1028
- * @param z - tile zoom level
1029
- * @param x - tile x coordinate
1030
- * @param y - tile y coordinate
1031
- * @returns the transformed tile or null if not found
1032
- */
1033
- getTile(z, x, y) {
1034
- z = +z;
1035
- x = +x;
1036
- y = +y;
1037
- const options = this.options;
1038
- const { extent, debug } = options;
1039
- if (z < 0 || z > 24)
1040
- return null;
1041
- const z2 = 1 << z;
1042
- x = (x + z2) & (z2 - 1); // wrap tile x coordinate
1043
- const id = toID(z, x, y);
1044
- if (this.tiles[id]) {
1045
- return transformTile(this.tiles[id], extent);
1046
- }
1047
- if (debug > 1)
1048
- console.log('drilling down to z%d-%d-%d', z, x, y);
1049
- let z0 = z;
1050
- let x0 = x;
1051
- let y0 = y;
1052
- let parent;
1053
- while (!parent && z0 > 0) {
1054
- z0--;
1055
- x0 = x0 >> 1;
1056
- y0 = y0 >> 1;
1057
- parent = this.tiles[toID(z0, x0, y0)];
1058
- }
1059
- if (!parent?.source)
1060
- return null;
1061
- // if we found a parent tile containing the original geometry, we can drill down from it
1062
- if (debug > 1) {
1063
- console.log('found parent tile z%d-%d-%d', z0, x0, y0);
1064
- console.time('drilling down');
1065
- }
1066
- this.splitTile(parent.source, z0, x0, y0, z, x, y);
1067
- if (debug > 1)
1068
- console.timeEnd('drilling down');
1069
- if (!this.tiles[id])
1070
- return null;
1071
- return transformTile(this.tiles[id], extent);
1072
- }
1073
1663
  /**
1074
1664
  * Invalidates (removes) tiles affected by the provided features
1075
1665
  * @internal
1076
1666
  * @param features
1077
1667
  */
1078
1668
  invalidateTiles(features) {
1669
+ if (!features.length)
1670
+ return;
1079
1671
  const options = this.options;
1080
1672
  const { debug } = options;
1081
1673
  // calculate bounding box of all features for trivial reject
@@ -1134,51 +1726,236 @@ class GeoJSONVT {
1134
1726
  this.tileCoords = this.tileCoords.filter(c => !removedLookup.has(c.id));
1135
1727
  }
1136
1728
  }
1729
+ }
1730
+ function toID(z, x, y) {
1731
+ return (((1 << z) * y + x) * 32) + z;
1732
+ }
1733
+
1734
+ const defaultOptions = {
1735
+ maxZoom: 14,
1736
+ indexMaxZoom: 5,
1737
+ indexMaxPoints: 100000,
1738
+ tolerance: 3,
1739
+ extent: 4096,
1740
+ buffer: 64,
1741
+ lineMetrics: false,
1742
+ promoteId: null,
1743
+ generateId: false,
1744
+ updateable: false,
1745
+ cluster: false,
1746
+ clusterOptions: defaultClusterOptions,
1747
+ debug: 0
1748
+ };
1749
+ /**
1750
+ * Main class for creating and managing a vector tile index from GeoJSON data.
1751
+ */
1752
+ class GeoJSONVT {
1753
+ /**
1754
+ * @internal
1755
+ * This is for the tests
1756
+ */
1757
+ get tiles() {
1758
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1759
+ return this.tileIndex?.tiles ?? {};
1760
+ }
1761
+ /**
1762
+ * @internal
1763
+ * This is for the tests
1764
+ */
1765
+ get stats() {
1766
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1767
+ return this.tileIndex.stats;
1768
+ }
1769
+ /**
1770
+ * @internal
1771
+ * This is for the tests
1772
+ */
1773
+ get total() {
1774
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1775
+ return this.tileIndex.total;
1776
+ }
1777
+ options;
1778
+ source;
1779
+ tileIndex;
1780
+ constructor(data, options) {
1781
+ options = this.options = Object.assign({}, defaultOptions, options);
1782
+ const debug = options.debug;
1783
+ if (debug)
1784
+ console.time('preprocess data');
1785
+ if (options.maxZoom < 0 || options.maxZoom > 24)
1786
+ throw new Error('maxZoom should be in the 0-24 range');
1787
+ if (options.promoteId && options.generateId)
1788
+ throw new Error('promoteId and generateId cannot be used together.');
1789
+ // projects and adds simplification info
1790
+ let features = convertToInternal(data, options);
1791
+ if (debug) {
1792
+ console.timeEnd('preprocess data');
1793
+ console.log('index: maxZoom: %d, maxPoints: %d', options.indexMaxZoom, options.indexMaxPoints);
1794
+ console.time('generate tiles');
1795
+ }
1796
+ // wraps features (ie extreme west and extreme east)
1797
+ features = wrap(features, options);
1798
+ // for updateable indexes, store a copy of the original simplified features
1799
+ if (options.updateable) {
1800
+ this.source = features;
1801
+ }
1802
+ this.initializeIndex(features, options);
1803
+ }
1804
+ initializeIndex(features, options) {
1805
+ this.tileIndex = options.cluster ? new ClusterTileIndex(options.clusterOptions) : new TileIndex(options);
1806
+ if (!features.length)
1807
+ return;
1808
+ this.tileIndex.initialize(features);
1809
+ }
1810
+ /**
1811
+ * Given z, x, and y tile coordinates, returns the corresponding tile with geometries in tile coordinates, much like MVT data is stored.
1812
+ * @param z - tile zoom level
1813
+ * @param x - tile x coordinate
1814
+ * @param y - tile y coordinate
1815
+ * @returns the transformed tile or null if not found
1816
+ */
1817
+ getTile(z, x, y) {
1818
+ z = +z;
1819
+ x = +x;
1820
+ y = +y;
1821
+ if (z < 0 || z > 24)
1822
+ return null;
1823
+ return this.tileIndex.getTile(z, x, y);
1824
+ }
1137
1825
  /**
1138
- * Updates the tile index by adding and/or removing geojson features
1139
- * invalidates tiles that are affected by the update for regeneration on next getTile call.
1826
+ * Updates the source data feature set using a {@link GeoJSONVTSourceDiff}
1140
1827
  * @param diff - the source diff object
1141
1828
  */
1142
- updateData(diff) {
1829
+ updateData(diff, filter) {
1143
1830
  const options = this.options;
1144
- const debug = options.debug;
1145
1831
  if (!options.updateable)
1146
1832
  throw new Error('to update tile geojson `updateable` option must be set to true');
1147
1833
  // apply diff and collect affected features and updated source that will be used to invalidate tiles
1148
- const { affected, source } = applySourceDiff(this.source, diff, options);
1834
+ let { affected, source } = applySourceDiff(this.source, diff, options);
1835
+ if (filter) {
1836
+ ({ affected, source } = this.filterUpdate(source, affected, filter));
1837
+ }
1149
1838
  // nothing has changed
1150
1839
  if (!affected.length)
1151
1840
  return;
1152
1841
  // update source with new simplified feature set
1153
1842
  this.source = source;
1154
- if (debug > 1) {
1155
- console.log('invalidating tiles');
1156
- console.time('invalidating');
1843
+ this.tileIndex.updateIndex(source, affected, options);
1844
+ }
1845
+ /**
1846
+ * Filter an update using a predicate function. Returns the affected and updated source features.
1847
+ */
1848
+ filterUpdate(source, affected, predicate) {
1849
+ const removeIds = new Set();
1850
+ for (const feature of source) {
1851
+ if (feature.id == undefined)
1852
+ continue;
1853
+ if (predicate(featureToGeoJSON(feature)))
1854
+ continue;
1855
+ affected.push(feature);
1856
+ removeIds.add(feature.id);
1157
1857
  }
1158
- this.invalidateTiles(affected);
1159
- if (debug > 1)
1160
- console.timeEnd('invalidating');
1161
- // re-generate root tile with updated feature set
1162
- const [z, x, y] = [0, 0, 0];
1163
- const rootTile = createTile(this.source, z, x, y, this.options);
1164
- rootTile.source = this.source;
1165
- // update tile index with new root tile - ready for getTile calls
1166
- const id = toID(z, x, y);
1167
- this.tiles[id] = rootTile;
1168
- this.tileCoords.push({ z, x, y, id });
1169
- if (debug) {
1170
- const key = `z${z}`;
1171
- this.stats[key] = (this.stats[key] || 0) + 1;
1172
- this.total++;
1858
+ source = source.filter(feature => !removeIds.has(feature.id));
1859
+ return { affected, source };
1860
+ }
1861
+ /**
1862
+ * Returns source data as GeoJSON - only available when `updateable` option is set to true.
1863
+ */
1864
+ getData() {
1865
+ if (!this.options.updateable)
1866
+ throw new Error('to retrieve data the `updateable` option must be set to true');
1867
+ return convertToGeoJSON(this.source);
1868
+ }
1869
+ /**
1870
+ * Update supercluster options and regenerate the index.
1871
+ * @param cluster - whether to enable clustering
1872
+ * @param clusterOptions - {@link SuperclusterOptions}
1873
+ */
1874
+ updateClusterOptions(cluster, clusterOptions) {
1875
+ const wasCluster = this.options.cluster;
1876
+ this.options.cluster = cluster;
1877
+ this.options.clusterOptions = clusterOptions;
1878
+ if (wasCluster == cluster) {
1879
+ this.tileIndex.updateIndex(this.source, [], this.options);
1880
+ return;
1173
1881
  }
1882
+ this.initializeIndex(this.source, this.options);
1883
+ }
1884
+ /**
1885
+ * Returns the zoom level at which a cluster expands into multiple children.
1886
+ * @param clusterId - The target cluster id.
1887
+ * @returns the expansion zoom or null in case of non-clustered source
1888
+ */
1889
+ getClusterExpansionZoom(clusterId) {
1890
+ return this.tileIndex.getClusterExpansionZoom(clusterId);
1891
+ }
1892
+ /**
1893
+ * Returns the immediate children (clusters or points) of a cluster as GeoJSON.
1894
+ * @param clusterId - The target cluster id.
1895
+ * @returns the immediate children or null in case of non-clustered source
1896
+ */
1897
+ getClusterChildren(clusterId) {
1898
+ return this.tileIndex.getChildren(clusterId);
1899
+ }
1900
+ /**
1901
+ * Returns leaf point features under a cluster, paginated by `limit` and `offset`.
1902
+ * @param clusterId - The target cluster id.
1903
+ * @param limit - Maximum number of points to return (defaults to `10`).
1904
+ * @param offset - Number of points to skip before collecting results (defaults to `0`).
1905
+ * @returns leaf point features under a cluster or null in case of non-clustered source
1906
+ */
1907
+ getClusterLeaves(clusterId, limit, offset) {
1908
+ return this.tileIndex.getLeaves(clusterId, limit, offset);
1174
1909
  }
1175
1910
  }
1176
- function toID(z, x, y) {
1177
- return (((1 << z) * y + x) * 32) + z;
1178
- }
1179
- function geojsonvt(data, options) {
1180
- return new GeoJSONVT(data, options);
1911
+
1912
+ /**
1913
+ * Converts GeoJSON data directly to a single vector tile without building a tile index.
1914
+ *
1915
+ * Unlike the {@link GeoJSONVT} class which builds a hierarchical tile index for efficient
1916
+ * repeated tile access, this function generates a single tile on-demand. This is useful when:
1917
+ * - You only need one specific tile and don't need to query multiple tiles
1918
+ * - The source data is already spatially filtered to the tile's bounding box
1919
+ * - You want to avoid the overhead of building a full tile index
1920
+ *
1921
+ * @example
1922
+ * ```ts
1923
+ * import {geoJSONToTile} from '@maplibre/geojson-vt';
1924
+ *
1925
+ * const geojson = {
1926
+ * type: 'FeatureCollection',
1927
+ * features: [{
1928
+ * type: 'Feature',
1929
+ * geometry: { type: 'Point', coordinates: [-77.03, 38.90] },
1930
+ * properties: { name: 'Washington, D.C.' }
1931
+ * }]
1932
+ * };
1933
+ *
1934
+ * const tile = geoJSONToTile(geojson, 10, 292, 391, { extent: 4096 });
1935
+ * ```
1936
+ *
1937
+ * @param data - GeoJSON data (Feature, FeatureCollection, or Geometry)
1938
+ * @param z - Tile zoom level
1939
+ * @param x - Tile x coordinate
1940
+ * @param y - Tile y coordinate
1941
+ * @param options - Optional configuration for tile generation
1942
+ * @returns The generated tile with geometries in tile coordinates, or null if no features
1943
+ */
1944
+ function geoJSONToTile(data, z, x, y, options = {}) {
1945
+ options = { ...defaultOptions, ...options };
1946
+ const { wrap: shouldWrap = false, clip: shouldClip = false } = options;
1947
+ let features = convertToInternal(data, options);
1948
+ if (shouldWrap) {
1949
+ features = wrap(features, options);
1950
+ }
1951
+ if (shouldClip || options.lineMetrics) {
1952
+ const pow2 = 1 << z;
1953
+ const buffer = options.buffer / options.extent;
1954
+ const left = clip(features, pow2, (x - buffer), (x + 1 + buffer), AxisType.X, -1, 2, options);
1955
+ features = clip(left || [], pow2, (y - buffer), (y + 1 + buffer), AxisType.Y, -1, 2, options);
1956
+ }
1957
+ return transformTile(createTile(features ?? [], z, x, y, options), options.extent);
1181
1958
  }
1182
1959
 
1183
- export { geojsonvt as default };
1960
+ export { GeoJSONVT, ClusterTileIndex as Supercluster, geoJSONToTile };
1184
1961
  //# sourceMappingURL=geojson-vt.mjs.map