@maplibre/geojson-vt 5.0.4 → 6.0.1

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 (53) hide show
  1. package/README.md +3 -13
  2. package/dist/clip.d.ts +20 -1
  3. package/dist/clip.d.ts.map +1 -1
  4. package/dist/cluster-tile-index.d.ts +76 -0
  5. package/dist/cluster-tile-index.d.ts.map +1 -0
  6. package/dist/cluster-tile-index.test.d.ts +2 -0
  7. package/dist/cluster-tile-index.test.d.ts.map +1 -0
  8. package/dist/convert.d.ts +10 -2
  9. package/dist/convert.d.ts.map +1 -1
  10. package/dist/deconvert.d.ts +19 -0
  11. package/dist/deconvert.d.ts.map +1 -0
  12. package/dist/deconvert.test.d.ts +2 -0
  13. package/dist/deconvert.test.d.ts.map +1 -0
  14. package/dist/definitions.d.ts +176 -20
  15. package/dist/definitions.d.ts.map +1 -1
  16. package/dist/feature.d.ts +11 -3
  17. package/dist/feature.d.ts.map +1 -1
  18. package/dist/geojson-to-tile.d.ts +35 -0
  19. package/dist/geojson-to-tile.d.ts.map +1 -0
  20. package/dist/geojson-vt-dev.js +1586 -478
  21. package/dist/geojson-vt.js +1 -1
  22. package/dist/geojson-vt.mjs +1252 -473
  23. package/dist/geojson-vt.mjs.map +1 -1
  24. package/dist/geojsonvt.d.ts +76 -0
  25. package/dist/geojsonvt.d.ts.map +1 -0
  26. package/dist/geojsonvt.test.d.ts +2 -0
  27. package/dist/geojsonvt.test.d.ts.map +1 -0
  28. package/dist/index.d.ts +9 -62
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/tile-index.d.ts +51 -0
  31. package/dist/tile-index.d.ts.map +1 -0
  32. package/dist/tile.d.ts +3 -29
  33. package/dist/tile.d.ts.map +1 -1
  34. package/dist/transform.d.ts +1 -18
  35. package/dist/transform.d.ts.map +1 -1
  36. package/package.json +18 -10
  37. package/src/clip.ts +119 -81
  38. package/src/cluster-tile-index.test.ts +205 -0
  39. package/src/cluster-tile-index.ts +513 -0
  40. package/src/convert.ts +97 -75
  41. package/src/deconvert.test.ts +153 -0
  42. package/src/deconvert.ts +92 -0
  43. package/src/definitions.ts +196 -18
  44. package/src/difference.ts +3 -3
  45. package/src/feature.ts +11 -4
  46. package/src/geojson-to-tile.ts +58 -0
  47. package/src/geojsonvt.test.ts +39 -0
  48. package/src/geojsonvt.ts +209 -0
  49. package/src/index.ts +30 -378
  50. package/src/tile-index.ts +310 -0
  51. package/src/tile.ts +94 -104
  52. package/src/transform.ts +41 -39
  53. 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,773 @@ 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
+ const GEOJSONVT_CLIP_START = 'geojsonvt_clip_start';
1240
+ const GEOJSONVT_CLIP_END = 'geojsonvt_clip_end';
1241
+ /**
1242
+ * Creates a tile object from the given features
1243
+ * @param features - the features to include in the tile
1244
+ * @param z
1245
+ * @param tx
1246
+ * @param ty
1247
+ * @param options - the options object
1248
+ * @returns the created tile
1249
+ */
1250
+ function createTile(features, z, tx, ty, options) {
1251
+ const tolerance = z === options.maxZoom ? 0 : options.tolerance / ((1 << z) * options.extent);
1252
+ const tile = {
1253
+ transformed: false,
1254
+ features: [],
1255
+ source: null,
1256
+ x: tx,
1257
+ y: ty,
1258
+ z: z,
1259
+ minX: 2,
1260
+ minY: 1,
1261
+ maxX: -1,
1262
+ maxY: 0,
1263
+ numPoints: 0,
1264
+ numSimplified: 0,
1265
+ numFeatures: features.length
1266
+ };
1267
+ for (const feature of features) {
1268
+ addFeature(tile, feature, tolerance, options);
1269
+ }
1270
+ return tile;
1271
+ }
1272
+ function addFeature(tile, feature, tolerance, options) {
1273
+ tile.minX = Math.min(tile.minX, feature.minX);
1274
+ tile.minY = Math.min(tile.minY, feature.minY);
1275
+ tile.maxX = Math.max(tile.maxX, feature.maxX);
1276
+ tile.maxY = Math.max(tile.maxY, feature.maxY);
1277
+ switch (feature.type) {
1278
+ case 'Point':
1279
+ case 'MultiPoint':
1280
+ addPointsTileFeature(tile, feature);
1281
+ return;
1282
+ case 'LineString':
1283
+ addLineTileFeautre(tile, feature, tolerance, options);
1284
+ return;
1285
+ case 'MultiLineString':
1286
+ case 'Polygon':
1287
+ addLinesTileFeature(tile, feature, tolerance);
1288
+ return;
1289
+ case 'MultiPolygon':
1290
+ addMultiPolygonTileFeature(tile, feature, tolerance);
1291
+ return;
1292
+ }
1293
+ }
1294
+ function addPointsTileFeature(tile, feature) {
1295
+ const geometry = [];
1296
+ for (let i = 0; i < feature.geometry.length; i += 3) {
1297
+ geometry.push(feature.geometry[i], feature.geometry[i + 1]);
1298
+ tile.numPoints++;
1299
+ tile.numSimplified++;
1300
+ }
1301
+ if (!geometry.length)
1302
+ return;
1303
+ const tileFeature = {
1304
+ type: 1,
1305
+ tags: feature.tags || null,
1306
+ geometry: geometry
1307
+ };
1308
+ if (feature.id !== null) {
1309
+ tileFeature.id = feature.id;
1310
+ }
1311
+ tile.features.push(tileFeature);
1312
+ }
1313
+ function addLineTileFeautre(tile, feature, tolerance, options) {
1314
+ const geometry = [];
1315
+ addLine(geometry, feature.geometry, tile, tolerance, false, false);
1316
+ if (!geometry.length)
1317
+ return;
1318
+ let tags = feature.tags || null;
1319
+ if (options.lineMetrics) {
1320
+ tags = {};
1321
+ for (const key in feature.tags)
1322
+ tags[key] = feature.tags[key];
1323
+ tags[GEOJSONVT_CLIP_START] = feature.geometry.start / feature.geometry.size;
1324
+ tags[GEOJSONVT_CLIP_END] = feature.geometry.end / feature.geometry.size;
1325
+ }
1326
+ const tileFeature = {
1327
+ type: 2,
1328
+ tags: tags,
1329
+ geometry: geometry
1330
+ };
1331
+ if (feature.id !== null) {
1332
+ tileFeature.id = feature.id;
1333
+ }
1334
+ tile.features.push(tileFeature);
1335
+ }
1336
+ function addLinesTileFeature(tile, feature, tolerance) {
1337
+ const geometry = [];
1338
+ for (let i = 0; i < feature.geometry.length; i++) {
1339
+ addLine(geometry, feature.geometry[i], tile, tolerance, feature.type === 'Polygon', i === 0);
1340
+ }
1341
+ if (!geometry.length)
1342
+ return;
1343
+ const tileFeature = {
1344
+ type: feature.type === 'Polygon' ? 3 : 2,
1345
+ tags: feature.tags || null,
1346
+ geometry: geometry
1347
+ };
1348
+ if (feature.id !== null) {
1349
+ tileFeature.id = feature.id;
1350
+ }
1351
+ tile.features.push(tileFeature);
1352
+ }
1353
+ function addMultiPolygonTileFeature(tile, feature, tolerance) {
1354
+ const geometry = [];
1355
+ for (let k = 0; k < feature.geometry.length; k++) {
1356
+ const polygon = feature.geometry[k];
1357
+ for (let i = 0; i < polygon.length; i++) {
1358
+ addLine(geometry, polygon[i], tile, tolerance, true, i === 0);
1359
+ }
1360
+ }
1361
+ if (!geometry.length)
1362
+ return;
1363
+ const tileFeature = {
1364
+ type: 3,
1365
+ tags: feature.tags || null,
1366
+ geometry: geometry
1367
+ };
1368
+ if (feature.id !== null) {
1369
+ tileFeature.id = feature.id;
1370
+ }
1371
+ tile.features.push(tileFeature);
1372
+ }
1373
+ function addLine(result, geom, tile, tolerance, isPolygon, isOuter) {
1374
+ const sqTolerance = tolerance * tolerance;
1375
+ if (tolerance > 0 && (geom.size < (isPolygon ? sqTolerance : tolerance))) {
1376
+ tile.numPoints += geom.length / 3;
1377
+ return;
1378
+ }
1379
+ const ring = [];
1380
+ for (let i = 0; i < geom.length; i += 3) {
1381
+ if (tolerance === 0 || geom[i + 2] > sqTolerance) {
1382
+ tile.numSimplified++;
1383
+ ring.push(geom[i], geom[i + 1]);
1384
+ }
1385
+ tile.numPoints++;
1386
+ }
1387
+ if (isPolygon)
1388
+ rewind(ring, isOuter);
1389
+ result.push(ring);
1390
+ }
1391
+ function rewind(ring, clockwise) {
1392
+ let area = 0;
1393
+ for (let i = 0, len = ring.length, j = len - 2; i < len; j = i, i += 2) {
1394
+ area += (ring[i] - ring[j]) * (ring[i + 1] + ring[j + 1]);
1395
+ }
1396
+ if (area > 0 !== clockwise)
1397
+ return;
1398
+ for (let i = 0, len = ring.length; i < len / 2; i += 2) {
1399
+ const x = ring[i];
1400
+ const y = ring[i + 1];
1401
+ ring[i] = ring[len - 2 - i];
1402
+ ring[i + 1] = ring[len - 1 - i];
1403
+ ring[len - 2 - i] = x;
1404
+ ring[len - 1 - i] = y;
1405
+ }
1406
+ }
1407
+
1408
+ /**
1409
+ * Transforms the coordinates of each feature in the given tile from
1410
+ * mercator-projected space into (extent x extent) tile space.
1411
+ * @param tile - the tile to transform, this gets modified in place
1412
+ * @param extent - the tile extent (usually 4096)
1413
+ * @returns the transformed tile
1414
+ */
1415
+ function transformTile(tile, extent) {
1416
+ if (tile.transformed) {
1417
+ return tile;
1418
+ }
1419
+ const z2 = 1 << tile.z;
1420
+ const tx = tile.x;
1421
+ const ty = tile.y;
1422
+ for (const feature of tile.features) {
1423
+ if (feature.type === 1) {
1424
+ transformPointFeature(feature, extent, z2, tx, ty);
1425
+ }
1426
+ else {
1427
+ transformNonPointFeature(feature, extent, z2, tx, ty);
1428
+ }
1429
+ }
1430
+ tile.transformed = true;
1431
+ return tile;
1432
+ }
1433
+ /**
1434
+ * Transforms a single point feature from mercator-projected space into (extent x extent) tile space.
1435
+ */
1436
+ function transformPointFeature(feature, extent, z2, tx, ty) {
1437
+ const transformed = feature;
1438
+ const geometry = feature.geometry;
1439
+ const point = [];
1440
+ for (let i = 0; i < geometry.length; i += 2) {
1441
+ point.push(transformPoint(geometry[i], geometry[i + 1], extent, z2, tx, ty));
1442
+ }
1443
+ transformed.geometry = point;
1444
+ return transformed;
1445
+ }
1446
+ /**
1447
+ * Transforms a single non-point feature from mercator-projected space into (extent x extent) tile space.
1448
+ */
1449
+ function transformNonPointFeature(feature, extent, z2, tx, ty) {
1450
+ const transformed = feature;
1451
+ const geometry = feature.geometry;
1452
+ const nonPoint = [];
1453
+ for (const geom of geometry) {
1454
+ const ring = [];
1455
+ for (let i = 0; i < geom.length; i += 2) {
1456
+ ring.push(transformPoint(geom[i], geom[i + 1], extent, z2, tx, ty));
1457
+ }
1458
+ nonPoint.push(ring);
1459
+ }
1460
+ transformed.geometry = nonPoint;
1461
+ return transformed;
1462
+ }
1463
+ function transformPoint(x, y, extent, z2, tx, ty) {
1464
+ return [
1465
+ Math.round(extent * (x * z2 - tx)),
1466
+ Math.round(extent * (y * z2 - ty))
1467
+ ];
1468
+ }
1469
+
1470
+ class TileIndex {
1471
+ options;
1472
+ tileCoords;
1473
+ /** @internal */
1474
+ tiles;
1475
+ /** @internal */
1476
+ stats = {};
1477
+ /** @internal */
890
1478
  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
1479
+ constructor(options) {
1480
+ this.options = options;
904
1481
  this.tiles = {};
905
1482
  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);
1483
+ this.stats = {};
1484
+ this.total = 0;
1485
+ }
1486
+ initialize(features) {
915
1487
  // 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) {
1488
+ this.splitTile(features, 0, 0, 0);
1489
+ if (this.options.debug) {
924
1490
  if (features.length)
925
1491
  console.log('features: %d, points: %d', this.tiles[0].numFeatures, this.tiles[0].numPoints);
926
1492
  console.timeEnd('generate tiles');
927
1493
  console.log('tiles generated:', this.total, JSON.stringify(this.stats));
928
1494
  }
929
1495
  }
1496
+ /** {@inheritdoc} */
1497
+ updateIndex(source, affected, options) {
1498
+ if (options.debug > 1) {
1499
+ console.log('invalidating tiles');
1500
+ console.time('invalidating');
1501
+ }
1502
+ this.invalidateTiles(affected);
1503
+ if (options.debug > 1)
1504
+ console.timeEnd('invalidating');
1505
+ // re-generate root tile with updated feature set
1506
+ const [z, x, y] = [0, 0, 0];
1507
+ const rootTile = createTile(source, z, x, y, options);
1508
+ rootTile.source = source;
1509
+ // update tile index with new root tile - ready for getTile calls
1510
+ const id = toID(z, x, y);
1511
+ this.tiles[id] = rootTile;
1512
+ this.tileCoords.push({ z, x, y, id });
1513
+ if (options.debug) {
1514
+ const key = `z${z}`;
1515
+ this.stats[key] = (this.stats[key] || 0) + 1;
1516
+ this.total++;
1517
+ }
1518
+ }
1519
+ /** {@inheritdoc} */
1520
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1521
+ getClusterExpansionZoom(_clusterId) {
1522
+ return null;
1523
+ }
1524
+ /** {@inheritdoc} */
1525
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1526
+ getChildren(_clusterId) {
1527
+ return null;
1528
+ }
1529
+ /** {@inheritdoc} */
1530
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1531
+ getLeaves(_clusterId, _limit, _offset) {
1532
+ return null;
1533
+ }
1534
+ /** {@inheritdoc} */
1535
+ getTile(z, x, y) {
1536
+ const { extent, debug } = this.options;
1537
+ const z2 = 1 << z;
1538
+ x = (x + z2) & (z2 - 1); // wrap tile x coordinate
1539
+ const id = toID(z, x, y);
1540
+ if (this.tiles[id]) {
1541
+ return transformTile(this.tiles[id], extent);
1542
+ }
1543
+ if (debug > 1)
1544
+ console.log('drilling down to z%d-%d-%d', z, x, y);
1545
+ let z0 = z;
1546
+ let x0 = x;
1547
+ let y0 = y;
1548
+ let parent;
1549
+ while (!parent && z0 > 0) {
1550
+ z0--;
1551
+ x0 = x0 >> 1;
1552
+ y0 = y0 >> 1;
1553
+ parent = this.tiles[toID(z0, x0, y0)];
1554
+ }
1555
+ if (!parent?.source)
1556
+ return null;
1557
+ // if we found a parent tile containing the original geometry, we can drill down from it
1558
+ if (debug > 1) {
1559
+ console.log('found parent tile z%d-%d-%d', z0, x0, y0);
1560
+ console.time('drilling down');
1561
+ }
1562
+ this.splitTile(parent.source, z0, x0, y0, z, x, y);
1563
+ if (debug > 1)
1564
+ console.timeEnd('drilling down');
1565
+ if (!this.tiles[id])
1566
+ return null;
1567
+ return transformTile(this.tiles[id], extent);
1568
+ }
930
1569
  /**
931
1570
  * splits features from a parent tile to sub-tiles.
932
1571
  * z, x, and y are the coordinates of the parent tile
@@ -1005,15 +1644,15 @@ class GeoJSONVT {
1005
1644
  let bl = null;
1006
1645
  let tr = null;
1007
1646
  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);
1647
+ const left = clip(features, z2, x - k1, x + k3, AxisType.X, tile.minX, tile.maxX, options);
1648
+ const right = clip(features, z2, x + k2, x + k4, AxisType.X, tile.minX, tile.maxX, options);
1010
1649
  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);
1650
+ tl = clip(left, z2, y - k1, y + k3, AxisType.Y, tile.minY, tile.maxY, options);
1651
+ bl = clip(left, z2, y + k2, y + k4, AxisType.Y, tile.minY, tile.maxY, options);
1013
1652
  }
1014
1653
  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);
1654
+ tr = clip(right, z2, y - k1, y + k3, AxisType.Y, tile.minY, tile.maxY, options);
1655
+ br = clip(right, z2, y + k2, y + k4, AxisType.Y, tile.minY, tile.maxY, options);
1017
1656
  }
1018
1657
  if (debug > 1)
1019
1658
  console.timeEnd('clipping');
@@ -1023,59 +1662,14 @@ class GeoJSONVT {
1023
1662
  stack.push(br || [], z + 1, x * 2 + 1, y * 2 + 1);
1024
1663
  }
1025
1664
  }
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
1665
  /**
1074
1666
  * Invalidates (removes) tiles affected by the provided features
1075
1667
  * @internal
1076
1668
  * @param features
1077
1669
  */
1078
1670
  invalidateTiles(features) {
1671
+ if (!features.length)
1672
+ return;
1079
1673
  const options = this.options;
1080
1674
  const { debug } = options;
1081
1675
  // calculate bounding box of all features for trivial reject
@@ -1134,51 +1728,236 @@ class GeoJSONVT {
1134
1728
  this.tileCoords = this.tileCoords.filter(c => !removedLookup.has(c.id));
1135
1729
  }
1136
1730
  }
1731
+ }
1732
+ function toID(z, x, y) {
1733
+ return (((1 << z) * y + x) * 32) + z;
1734
+ }
1735
+
1736
+ const defaultOptions = {
1737
+ maxZoom: 14,
1738
+ indexMaxZoom: 5,
1739
+ indexMaxPoints: 100000,
1740
+ tolerance: 3,
1741
+ extent: 4096,
1742
+ buffer: 64,
1743
+ lineMetrics: false,
1744
+ promoteId: null,
1745
+ generateId: false,
1746
+ updateable: false,
1747
+ cluster: false,
1748
+ clusterOptions: defaultClusterOptions,
1749
+ debug: 0
1750
+ };
1751
+ /**
1752
+ * Main class for creating and managing a vector tile index from GeoJSON data.
1753
+ */
1754
+ class GeoJSONVT {
1755
+ /**
1756
+ * @internal
1757
+ * This is for the tests
1758
+ */
1759
+ get tiles() {
1760
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1761
+ return this.tileIndex?.tiles ?? {};
1762
+ }
1763
+ /**
1764
+ * @internal
1765
+ * This is for the tests
1766
+ */
1767
+ get stats() {
1768
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1769
+ return this.tileIndex.stats;
1770
+ }
1771
+ /**
1772
+ * @internal
1773
+ * This is for the tests
1774
+ */
1775
+ get total() {
1776
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1777
+ return this.tileIndex.total;
1778
+ }
1779
+ options;
1780
+ source;
1781
+ tileIndex;
1782
+ constructor(data, options) {
1783
+ options = this.options = Object.assign({}, defaultOptions, options);
1784
+ const debug = options.debug;
1785
+ if (debug)
1786
+ console.time('preprocess data');
1787
+ if (options.maxZoom < 0 || options.maxZoom > 24)
1788
+ throw new Error('maxZoom should be in the 0-24 range');
1789
+ if (options.promoteId && options.generateId)
1790
+ throw new Error('promoteId and generateId cannot be used together.');
1791
+ // projects and adds simplification info
1792
+ let features = convertToInternal(data, options);
1793
+ if (debug) {
1794
+ console.timeEnd('preprocess data');
1795
+ console.log('index: maxZoom: %d, maxPoints: %d', options.indexMaxZoom, options.indexMaxPoints);
1796
+ console.time('generate tiles');
1797
+ }
1798
+ // wraps features (ie extreme west and extreme east)
1799
+ features = wrap(features, options);
1800
+ // for updateable indexes, store a copy of the original simplified features
1801
+ if (options.updateable) {
1802
+ this.source = features;
1803
+ }
1804
+ this.initializeIndex(features, options);
1805
+ }
1806
+ initializeIndex(features, options) {
1807
+ this.tileIndex = options.cluster ? new ClusterTileIndex(options.clusterOptions) : new TileIndex(options);
1808
+ if (!features.length)
1809
+ return;
1810
+ this.tileIndex.initialize(features);
1811
+ }
1812
+ /**
1813
+ * Given z, x, and y tile coordinates, returns the corresponding tile with geometries in tile coordinates, much like MVT data is stored.
1814
+ * @param z - tile zoom level
1815
+ * @param x - tile x coordinate
1816
+ * @param y - tile y coordinate
1817
+ * @returns the transformed tile or null if not found
1818
+ */
1819
+ getTile(z, x, y) {
1820
+ z = +z;
1821
+ x = +x;
1822
+ y = +y;
1823
+ if (z < 0 || z > 24)
1824
+ return null;
1825
+ return this.tileIndex.getTile(z, x, y);
1826
+ }
1137
1827
  /**
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.
1828
+ * Updates the source data feature set using a {@link GeoJSONVTSourceDiff}
1140
1829
  * @param diff - the source diff object
1141
1830
  */
1142
- updateData(diff) {
1831
+ updateData(diff, filter) {
1143
1832
  const options = this.options;
1144
- const debug = options.debug;
1145
1833
  if (!options.updateable)
1146
1834
  throw new Error('to update tile geojson `updateable` option must be set to true');
1147
1835
  // 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);
1836
+ let { affected, source } = applySourceDiff(this.source, diff, options);
1837
+ if (filter) {
1838
+ ({ affected, source } = this.filterUpdate(source, affected, filter));
1839
+ }
1149
1840
  // nothing has changed
1150
1841
  if (!affected.length)
1151
1842
  return;
1152
1843
  // update source with new simplified feature set
1153
1844
  this.source = source;
1154
- if (debug > 1) {
1155
- console.log('invalidating tiles');
1156
- console.time('invalidating');
1845
+ this.tileIndex.updateIndex(source, affected, options);
1846
+ }
1847
+ /**
1848
+ * Filter an update using a predicate function. Returns the affected and updated source features.
1849
+ */
1850
+ filterUpdate(source, affected, predicate) {
1851
+ const removeIds = new Set();
1852
+ for (const feature of source) {
1853
+ if (feature.id == undefined)
1854
+ continue;
1855
+ if (predicate(featureToGeoJSON(feature)))
1856
+ continue;
1857
+ affected.push(feature);
1858
+ removeIds.add(feature.id);
1157
1859
  }
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++;
1860
+ source = source.filter(feature => !removeIds.has(feature.id));
1861
+ return { affected, source };
1862
+ }
1863
+ /**
1864
+ * Returns source data as GeoJSON - only available when `updateable` option is set to true.
1865
+ */
1866
+ getData() {
1867
+ if (!this.options.updateable)
1868
+ throw new Error('to retrieve data the `updateable` option must be set to true');
1869
+ return convertToGeoJSON(this.source);
1870
+ }
1871
+ /**
1872
+ * Update supercluster options and regenerate the index.
1873
+ * @param cluster - whether to enable clustering
1874
+ * @param clusterOptions - {@link SuperclusterOptions}
1875
+ */
1876
+ updateClusterOptions(cluster, clusterOptions) {
1877
+ const wasCluster = this.options.cluster;
1878
+ this.options.cluster = cluster;
1879
+ this.options.clusterOptions = clusterOptions;
1880
+ if (wasCluster == cluster) {
1881
+ this.tileIndex.updateIndex(this.source, [], this.options);
1882
+ return;
1173
1883
  }
1884
+ this.initializeIndex(this.source, this.options);
1885
+ }
1886
+ /**
1887
+ * Returns the zoom level at which a cluster expands into multiple children.
1888
+ * @param clusterId - The target cluster id.
1889
+ * @returns the expansion zoom or null in case of non-clustered source
1890
+ */
1891
+ getClusterExpansionZoom(clusterId) {
1892
+ return this.tileIndex.getClusterExpansionZoom(clusterId);
1893
+ }
1894
+ /**
1895
+ * Returns the immediate children (clusters or points) of a cluster as GeoJSON.
1896
+ * @param clusterId - The target cluster id.
1897
+ * @returns the immediate children or null in case of non-clustered source
1898
+ */
1899
+ getClusterChildren(clusterId) {
1900
+ return this.tileIndex.getChildren(clusterId);
1901
+ }
1902
+ /**
1903
+ * Returns leaf point features under a cluster, paginated by `limit` and `offset`.
1904
+ * @param clusterId - The target cluster id.
1905
+ * @param limit - Maximum number of points to return (defaults to `10`).
1906
+ * @param offset - Number of points to skip before collecting results (defaults to `0`).
1907
+ * @returns leaf point features under a cluster or null in case of non-clustered source
1908
+ */
1909
+ getClusterLeaves(clusterId, limit, offset) {
1910
+ return this.tileIndex.getLeaves(clusterId, limit, offset);
1174
1911
  }
1175
1912
  }
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);
1913
+
1914
+ /**
1915
+ * Converts GeoJSON data directly to a single vector tile without building a tile index.
1916
+ *
1917
+ * Unlike the {@link GeoJSONVT} class which builds a hierarchical tile index for efficient
1918
+ * repeated tile access, this function generates a single tile on-demand. This is useful when:
1919
+ * - You only need one specific tile and don't need to query multiple tiles
1920
+ * - The source data is already spatially filtered to the tile's bounding box
1921
+ * - You want to avoid the overhead of building a full tile index
1922
+ *
1923
+ * @example
1924
+ * ```ts
1925
+ * import {geoJSONToTile} from '@maplibre/geojson-vt';
1926
+ *
1927
+ * const geojson = {
1928
+ * type: 'FeatureCollection',
1929
+ * features: [{
1930
+ * type: 'Feature',
1931
+ * geometry: { type: 'Point', coordinates: [-77.03, 38.90] },
1932
+ * properties: { name: 'Washington, D.C.' }
1933
+ * }]
1934
+ * };
1935
+ *
1936
+ * const tile = geoJSONToTile(geojson, 10, 292, 391, { extent: 4096 });
1937
+ * ```
1938
+ *
1939
+ * @param data - GeoJSON data (Feature, FeatureCollection, or Geometry)
1940
+ * @param z - Tile zoom level
1941
+ * @param x - Tile x coordinate
1942
+ * @param y - Tile y coordinate
1943
+ * @param options - Optional configuration for tile generation
1944
+ * @returns The generated tile with geometries in tile coordinates, or null if no features
1945
+ */
1946
+ function geoJSONToTile(data, z, x, y, options = {}) {
1947
+ options = { ...defaultOptions, ...options };
1948
+ const { wrap: shouldWrap = false, clip: shouldClip = false } = options;
1949
+ let features = convertToInternal(data, options);
1950
+ if (shouldWrap) {
1951
+ features = wrap(features, options);
1952
+ }
1953
+ if (shouldClip || options.lineMetrics) {
1954
+ const pow2 = 1 << z;
1955
+ const buffer = options.buffer / options.extent;
1956
+ const left = clip(features, pow2, (x - buffer), (x + 1 + buffer), AxisType.X, -1, 2, options);
1957
+ features = clip(left || [], pow2, (y - buffer), (y + 1 + buffer), AxisType.Y, -1, 2, options);
1958
+ }
1959
+ return transformTile(createTile(features ?? [], z, x, y, options), options.extent);
1181
1960
  }
1182
1961
 
1183
- export { geojsonvt as default };
1962
+ export { GEOJSONVT_CLIP_END, GEOJSONVT_CLIP_START, GeoJSONVT, ClusterTileIndex as Supercluster, geoJSONToTile };
1184
1963
  //# sourceMappingURL=geojson-vt.mjs.map