@maplibre/geojson-vt 5.0.0 → 5.0.2

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.
@@ -4,27 +4,30 @@ typeof define === 'function' && define.amd ? define(factory) :
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.geojsonvt = factory());
5
5
  })(this, (function () { 'use strict';
6
6
 
7
- // calculate simplification data using optimized Douglas-Peucker algorithm
8
-
7
+ /**
8
+ * calculate simplification data using optimized Douglas-Peucker algorithm
9
+ * @param coords - flat array of coordinates
10
+ * @param first - index of the first coordinate in the segment
11
+ * @param last - index of the last coordinate in the segment
12
+ * @param sqTolerance - square tolerance value
13
+ */
9
14
  function simplify(coords, first, last, sqTolerance) {
10
15
  let maxSqDist = sqTolerance;
11
16
  const mid = first + ((last - first) >> 1);
12
17
  let minPosToMid = last - first;
13
18
  let index;
14
-
15
19
  const ax = coords[first];
16
20
  const ay = coords[first + 1];
17
21
  const bx = coords[last];
18
22
  const by = coords[last + 1];
19
-
20
23
  for (let i = first + 3; i < last; i += 3) {
21
24
  const d = getSqSegDist(coords[i], coords[i + 1], ax, ay, bx, by);
22
-
23
25
  if (d > maxSqDist) {
24
26
  index = i;
25
27
  maxSqDist = d;
26
-
27
- } else if (d === maxSqDist) {
28
+ continue;
29
+ }
30
+ if (d === maxSqDist) {
28
31
  // a workaround to ensure we choose a pivot close to the middle of the list,
29
32
  // reducing recursion depth, for certain degenerate inputs
30
33
  // https://github.com/mapbox/geojson-vt/issues/104
@@ -35,74 +38,88 @@ function simplify(coords, first, last, sqTolerance) {
35
38
  }
36
39
  }
37
40
  }
38
-
39
41
  if (maxSqDist > sqTolerance) {
40
- if (index - first > 3) simplify(coords, first, index, sqTolerance);
42
+ if (index - first > 3)
43
+ simplify(coords, first, index, sqTolerance);
41
44
  coords[index + 2] = maxSqDist;
42
- if (last - index > 3) simplify(coords, index, last, sqTolerance);
45
+ if (last - index > 3)
46
+ simplify(coords, index, last, sqTolerance);
43
47
  }
44
48
  }
45
-
46
- // square distance from a point to a segment
49
+ /**
50
+ * Claculates the square distance from a point to a segment
51
+ * @param px - x coordinate of the point
52
+ * @param py - y coordinate of the point
53
+ * @param x - x coordinate of the first segment endpoint
54
+ * @param y - y coordinate of the first segment endpoint
55
+ * @param bx - x coordinate of the second segment endpoint
56
+ * @param by - y coordinate of the second segment endpoint
57
+ * @returns square distance from a point to a segment
58
+ */
47
59
  function getSqSegDist(px, py, x, y, bx, by) {
48
-
49
60
  let dx = bx - x;
50
61
  let dy = by - y;
51
-
52
62
  if (dx !== 0 || dy !== 0) {
53
-
54
63
  const t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);
55
-
56
64
  if (t > 1) {
57
65
  x = bx;
58
66
  y = by;
59
-
60
- } else if (t > 0) {
67
+ }
68
+ else if (t > 0) {
61
69
  x += dx * t;
62
70
  y += dy * t;
63
71
  }
64
72
  }
65
-
66
73
  dx = px - x;
67
74
  dy = py - y;
68
-
69
75
  return dx * dx + dy * dy;
70
76
  }
71
77
 
78
+ /**
79
+ *
80
+ * @param id - the feature's ID
81
+ * @param type - the feature's type
82
+ * @param geom - the feature's geometry
83
+ * @param tags - the feature's properties
84
+ * @returns the created feature
85
+ */
72
86
  function createFeature(id, type, geom, tags) {
87
+ // This is mostly for TypeScript type narrowing
88
+ const data = { type, geom };
73
89
  const feature = {
74
90
  id: id == null ? null : id,
75
- type,
76
- geometry: geom,
91
+ type: data.type,
92
+ geometry: data.geom,
77
93
  tags,
78
94
  minX: Infinity,
79
95
  minY: Infinity,
80
96
  maxX: -Infinity,
81
97
  maxY: -Infinity
82
98
  };
83
-
84
- if (type === 'Point' || type === 'MultiPoint' || type === 'LineString') {
85
- calcLineBBox(feature, geom);
86
-
87
- } else if (type === 'Polygon') {
88
- // the outer ring (ie [0]) contains all inner rings
89
- calcLineBBox(feature, geom[0]);
90
-
91
- } else if (type === 'MultiLineString') {
92
- for (const line of geom) {
93
- calcLineBBox(feature, line);
94
- }
95
-
96
- } else if (type === 'MultiPolygon') {
97
- for (const polygon of geom) {
99
+ switch (data.type) {
100
+ case 'Point':
101
+ case 'MultiPoint':
102
+ case 'LineString':
103
+ calcLineBBox(feature, data.geom);
104
+ break;
105
+ case 'Polygon':
98
106
  // the outer ring (ie [0]) contains all inner rings
99
- calcLineBBox(feature, polygon[0]);
100
- }
107
+ calcLineBBox(feature, data.geom[0]);
108
+ break;
109
+ case 'MultiLineString':
110
+ for (const line of data.geom) {
111
+ calcLineBBox(feature, line);
112
+ }
113
+ break;
114
+ case 'MultiPolygon':
115
+ for (const polygon of data.geom) {
116
+ // the outer ring (ie [0]) contains all inner rings
117
+ calcLineBBox(feature, polygon[0]);
118
+ }
119
+ break;
101
120
  }
102
-
103
121
  return feature;
104
122
  }
105
-
106
123
  function calcLineBBox(feature, geom) {
107
124
  for (let i = 0; i < geom.length; i += 3) {
108
125
  feature.minX = Math.min(feature.minX, geom[i]);
@@ -112,124 +129,138 @@ function calcLineBBox(feature, geom) {
112
129
  }
113
130
  }
114
131
 
115
- // converts GeoJSON feature into an intermediate projected JSON vector format with simplification data
116
-
132
+ /**
133
+ * converts GeoJSON feature into an intermediate projected JSON vector format with simplification data
134
+ * @param data
135
+ * @param options
136
+ * @returns
137
+ */
117
138
  function convert(data, options) {
118
139
  const features = [];
119
- if (data.type === 'FeatureCollection') {
120
- for (let i = 0; i < data.features.length; i++) {
121
- convertFeature(features, data.features[i], options, i);
122
- }
123
-
124
- } else if (data.type === 'Feature') {
125
- convertFeature(features, data, options);
126
-
127
- } else {
128
- // single geometry or a geometry collection
129
- convertFeature(features, {geometry: data}, options);
140
+ switch (data.type) {
141
+ case 'FeatureCollection':
142
+ for (let i = 0; i < data.features.length; i++) {
143
+ convertFeature(features, data.features[i], options, i);
144
+ }
145
+ break;
146
+ case 'Feature':
147
+ convertFeature(features, data, options);
148
+ break;
149
+ default:
150
+ convertFeature(features, { geometry: data, properties: undefined }, options);
130
151
  }
131
-
132
152
  return features;
133
153
  }
134
-
135
154
  function convertFeature(features, geojson, options, index) {
136
- if (!geojson.geometry) return;
137
-
155
+ if (!geojson.geometry)
156
+ return;
157
+ if (geojson.geometry.type === 'GeometryCollection') {
158
+ for (const singleGeometry of geojson.geometry.geometries) {
159
+ convertFeature(features, {
160
+ id: geojson.id,
161
+ geometry: singleGeometry,
162
+ properties: geojson.properties
163
+ }, options, index);
164
+ }
165
+ return;
166
+ }
138
167
  const coords = geojson.geometry.coordinates;
139
- if (coords && coords.length === 0) return;
140
-
141
- const type = geojson.geometry.type;
168
+ if (!coords?.length)
169
+ return;
142
170
  const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
143
- let geometry = [];
144
171
  let id = geojson.id;
145
172
  if (options.promoteId) {
146
- id = geojson.properties[options.promoteId];
147
- } else if (options.generateId) {
173
+ id = geojson.properties?.[options.promoteId];
174
+ }
175
+ else if (options.generateId) {
148
176
  id = index || 0;
149
177
  }
150
- if (type === 'Point') {
151
- convertPoint(coords, geometry);
152
-
153
- } else if (type === 'MultiPoint') {
154
- for (const p of coords) {
155
- convertPoint(p, geometry);
178
+ switch (geojson.geometry.type) {
179
+ case 'Point': {
180
+ const pointGeometry = [];
181
+ convertPoint(geojson.geometry.coordinates, pointGeometry);
182
+ features.push(createFeature(id, geojson.geometry.type, pointGeometry, geojson.properties));
183
+ return;
156
184
  }
157
-
158
- } else if (type === 'LineString') {
159
- convertLine(coords, geometry, tolerance, false);
160
-
161
- } else if (type === 'MultiLineString') {
162
- if (options.lineMetrics) {
163
- // explode into linestrings to be able to track metrics
164
- for (const line of coords) {
165
- geometry = [];
166
- convertLine(line, geometry, tolerance, false);
167
- features.push(createFeature(id, 'LineString', geometry, geojson.properties));
185
+ case 'MultiPoint': {
186
+ const multiPointGeometry = [];
187
+ for (const p of geojson.geometry.coordinates) {
188
+ convertPoint(p, multiPointGeometry);
168
189
  }
190
+ features.push(createFeature(id, geojson.geometry.type, multiPointGeometry, geojson.properties));
169
191
  return;
170
192
  }
171
- convertLines(coords, geometry, tolerance, false);
172
-
173
- } else if (type === 'Polygon') {
174
- convertLines(coords, geometry, tolerance, true);
175
-
176
- } else if (type === 'MultiPolygon') {
177
- for (const polygon of coords) {
178
- const newPolygon = [];
179
- convertLines(polygon, newPolygon, tolerance, true);
180
- geometry.push(newPolygon);
193
+ case 'LineString': {
194
+ const lineGeometry = [];
195
+ convertLine(geojson.geometry.coordinates, lineGeometry, tolerance, false);
196
+ features.push(createFeature(id, geojson.geometry.type, lineGeometry, geojson.properties));
197
+ return;
181
198
  }
182
- } else if (type === 'GeometryCollection') {
183
- for (const singleGeometry of geojson.geometry.geometries) {
184
- convertFeature(features, {
185
- id,
186
- geometry: singleGeometry,
187
- properties: geojson.properties
188
- }, options, index);
199
+ case 'MultiLineString': {
200
+ if (options.lineMetrics) {
201
+ // explode into linestrings in order to track metrics
202
+ for (const line of geojson.geometry.coordinates) {
203
+ const lineGeometry = [];
204
+ convertLine(line, lineGeometry, tolerance, false);
205
+ features.push(createFeature(id, 'LineString', lineGeometry, geojson.properties));
206
+ }
207
+ return;
208
+ }
209
+ const multiLineGeometry = [];
210
+ convertLines(geojson.geometry.coordinates, multiLineGeometry, tolerance, false);
211
+ features.push(createFeature(id, geojson.geometry.type, multiLineGeometry, geojson.properties));
212
+ return;
189
213
  }
190
- return;
191
- } else {
192
- throw new Error('Input data is not a valid GeoJSON object.');
214
+ case 'Polygon': {
215
+ const polygonGeometry = [];
216
+ convertLines(geojson.geometry.coordinates, polygonGeometry, tolerance, true);
217
+ features.push(createFeature(id, geojson.geometry.type, polygonGeometry, geojson.properties));
218
+ return;
219
+ }
220
+ case 'MultiPolygon': {
221
+ const multiPolygonGeometry = [];
222
+ for (const polygon of geojson.geometry.coordinates) {
223
+ const newPolygon = [];
224
+ convertLines(polygon, newPolygon, tolerance, true);
225
+ multiPolygonGeometry.push(newPolygon);
226
+ }
227
+ features.push(createFeature(id, geojson.geometry.type, multiPolygonGeometry, geojson.properties));
228
+ return;
229
+ }
230
+ default:
231
+ throw new Error('Input data is not a valid GeoJSON object.');
193
232
  }
194
-
195
- features.push(createFeature(id, type, geometry, geojson.properties));
196
233
  }
197
-
198
234
  function convertPoint(coords, out) {
199
235
  out.push(projectX(coords[0]), projectY(coords[1]), 0);
200
236
  }
201
-
202
237
  function convertLine(ring, out, tolerance, isPolygon) {
203
238
  let x0, y0;
204
239
  let size = 0;
205
-
206
240
  for (let j = 0; j < ring.length; j++) {
207
241
  const x = projectX(ring[j][0]);
208
242
  const y = projectY(ring[j][1]);
209
-
210
243
  out.push(x, y, 0);
211
-
212
244
  if (j > 0) {
213
245
  if (isPolygon) {
214
246
  size += (x0 * y - x * y0) / 2; // area
215
- } else {
247
+ }
248
+ else {
216
249
  size += Math.sqrt(Math.pow(x - x0, 2) + Math.pow(y - y0, 2)); // length
217
250
  }
218
251
  }
219
252
  x0 = x;
220
253
  y0 = y;
221
254
  }
222
-
223
255
  const last = out.length - 3;
224
256
  out[2] = 1;
225
- if (tolerance > 0) simplify(out, 0, last, tolerance);
257
+ if (tolerance > 0)
258
+ simplify(out, 0, last, tolerance);
226
259
  out[last + 2] = 1;
227
-
228
260
  out.size = Math.abs(size);
229
261
  out.start = 0;
230
262
  out.end = out.size;
231
263
  }
232
-
233
264
  function convertLines(rings, out, tolerance, isPolygon) {
234
265
  for (let i = 0; i < rings.length; i++) {
235
266
  const geom = [];
@@ -237,11 +268,9 @@ function convertLines(rings, out, tolerance, isPolygon) {
237
268
  out.push(geom);
238
269
  }
239
270
  }
240
-
241
271
  function projectX(x) {
242
272
  return x / 360 + 0.5;
243
273
  }
244
-
245
274
  function projectY(y) {
246
275
  const sin = Math.sin(y * Math.PI / 180);
247
276
  const y2 = 0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
@@ -261,94 +290,104 @@ function projectY(y) {
261
290
  function clip(features, scale, k1, k2, axis, minAll, maxAll, options) {
262
291
  k1 /= scale;
263
292
  k2 /= scale;
264
-
265
- if (minAll >= k1 && maxAll < k2) return features; // trivial accept
266
- else if (maxAll < k1 || minAll >= k2) return null; // trivial reject
267
-
293
+ if (minAll >= k1 && maxAll < k2) { // trivial accept
294
+ return features;
295
+ }
296
+ if (maxAll < k1 || minAll >= k2) { // trivial reject
297
+ return null;
298
+ }
268
299
  const clipped = [];
269
-
270
300
  for (const feature of features) {
271
- const geometry = feature.geometry;
272
- let type = feature.type;
273
-
274
301
  const min = axis === 0 ? feature.minX : feature.minY;
275
302
  const max = axis === 0 ? feature.maxX : feature.maxY;
276
-
277
303
  if (min >= k1 && max < k2) { // trivial accept
278
304
  clipped.push(feature);
279
305
  continue;
280
- } else if (max < k1 || min >= k2) { // trivial reject
306
+ }
307
+ if (max < k1 || min >= k2) { // trivial reject
281
308
  continue;
282
309
  }
283
-
284
- let newGeometry = [];
285
-
286
- if (type === 'Point' || type === 'MultiPoint') {
287
- clipPoints(geometry, newGeometry, k1, k2, axis);
288
-
289
- } else if (type === 'LineString') {
290
- clipLine(geometry, newGeometry, k1, k2, axis, false, options.lineMetrics);
291
-
292
- } else if (type === 'MultiLineString') {
293
- clipLines(geometry, newGeometry, k1, k2, axis, false);
294
-
295
- } else if (type === 'Polygon') {
296
- clipLines(geometry, newGeometry, k1, k2, axis, true);
297
-
298
- } else if (type === 'MultiPolygon') {
299
- for (const polygon of geometry) {
300
- const newPolygon = [];
301
- clipLines(polygon, newPolygon, k1, k2, axis, true);
302
- if (newPolygon.length) {
303
- newGeometry.push(newPolygon);
304
- }
310
+ switch (feature.type) {
311
+ case 'Point':
312
+ case 'MultiPoint': {
313
+ const pointGeometry = [];
314
+ clipPoints(feature.geometry, pointGeometry, k1, k2, axis);
315
+ if (!pointGeometry.length)
316
+ continue;
317
+ const type = pointGeometry.length === 3 ? 'Point' : 'MultiPoint';
318
+ clipped.push(createFeature(feature.id, type, pointGeometry, feature.tags));
319
+ continue;
305
320
  }
306
- }
307
-
308
- if (newGeometry.length) {
309
- if (options.lineMetrics && type === 'LineString') {
310
- for (const line of newGeometry) {
311
- clipped.push(createFeature(feature.id, type, line, feature.tags));
321
+ case 'LineString': {
322
+ const lineGeometry = [];
323
+ clipLine(feature.geometry, lineGeometry, k1, k2, axis, false, options.lineMetrics);
324
+ if (!lineGeometry.length)
325
+ continue;
326
+ if (options.lineMetrics) {
327
+ for (const line of lineGeometry) {
328
+ clipped.push(createFeature(feature.id, feature.type, line, feature.tags));
329
+ }
330
+ continue;
331
+ }
332
+ if (lineGeometry.length > 1) {
333
+ clipped.push(createFeature(feature.id, "MultiLineString", lineGeometry, feature.tags));
334
+ continue;
312
335
  }
336
+ clipped.push(createFeature(feature.id, feature.type, lineGeometry[0], feature.tags));
313
337
  continue;
314
338
  }
315
-
316
- if (type === 'LineString' || type === 'MultiLineString') {
317
- if (newGeometry.length === 1) {
318
- type = 'LineString';
319
- newGeometry = newGeometry[0];
320
- } else {
321
- type = 'MultiLineString';
339
+ case 'MultiLineString': {
340
+ const multiLineGeometry = [];
341
+ clipLines(feature.geometry, multiLineGeometry, k1, k2, axis, false);
342
+ if (!multiLineGeometry.length)
343
+ continue;
344
+ if (multiLineGeometry.length === 1) {
345
+ clipped.push(createFeature(feature.id, "LineString", multiLineGeometry[0], feature.tags));
346
+ continue;
322
347
  }
348
+ clipped.push(createFeature(feature.id, feature.type, multiLineGeometry, feature.tags));
349
+ continue;
323
350
  }
324
- if (type === 'Point' || type === 'MultiPoint') {
325
- type = newGeometry.length === 3 ? 'Point' : 'MultiPoint';
351
+ case 'Polygon': {
352
+ const polygonGeometry = [];
353
+ clipLines(feature.geometry, polygonGeometry, k1, k2, axis, true);
354
+ if (!polygonGeometry.length)
355
+ continue;
356
+ clipped.push(createFeature(feature.id, feature.type, polygonGeometry, feature.tags));
357
+ continue;
358
+ }
359
+ case 'MultiPolygon': {
360
+ const multiPolygonGeometry = [];
361
+ for (const polygon of feature.geometry) {
362
+ const newPolygon = [];
363
+ clipLines(polygon, newPolygon, k1, k2, axis, true);
364
+ if (newPolygon.length)
365
+ multiPolygonGeometry.push(newPolygon);
366
+ }
367
+ if (!multiPolygonGeometry.length)
368
+ continue;
369
+ clipped.push(createFeature(feature.id, feature.type, multiPolygonGeometry, feature.tags));
370
+ continue;
326
371
  }
327
-
328
- clipped.push(createFeature(feature.id, type, newGeometry, feature.tags));
329
372
  }
330
373
  }
331
-
332
- return clipped.length ? clipped : null;
374
+ if (!clipped.length)
375
+ return null;
376
+ return clipped;
333
377
  }
334
-
335
378
  function clipPoints(geom, newGeom, k1, k2, axis) {
336
379
  for (let i = 0; i < geom.length; i += 3) {
337
380
  const a = geom[i + axis];
338
-
339
381
  if (a >= k1 && a <= k2) {
340
382
  addPoint(newGeom, geom[i], geom[i + 1], geom[i + 2]);
341
383
  }
342
384
  }
343
385
  }
344
-
345
386
  function clipLine(geom, newGeom, k1, k2, axis, isPolygon, trackMetrics) {
346
-
347
387
  let slice = newSlice(geom);
348
388
  const intersect = axis === 0 ? intersectX : intersectY;
349
389
  let len = geom.start;
350
390
  let segLen, t;
351
-
352
391
  for (let i = 0; i < geom.length - 3; i += 3) {
353
392
  const ax = geom[i];
354
393
  const ay = geom[i + 1];
@@ -358,22 +397,25 @@ function clipLine(geom, newGeom, k1, k2, axis, isPolygon, trackMetrics) {
358
397
  const a = axis === 0 ? ax : ay;
359
398
  const b = axis === 0 ? bx : by;
360
399
  let exited = false;
361
-
362
- if (trackMetrics) segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
363
-
400
+ if (trackMetrics)
401
+ segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
364
402
  if (a < k1) {
365
403
  // ---|--> | (line enters the clip region from the left)
366
404
  if (b > k1) {
367
405
  t = intersect(slice, ax, ay, bx, by, k1);
368
- if (trackMetrics) slice.start = len + segLen * t;
406
+ if (trackMetrics)
407
+ slice.start = len + segLen * t;
369
408
  }
370
- } else if (a > k2) {
409
+ }
410
+ else if (a > k2) {
371
411
  // | <--|--- (line enters the clip region from the right)
372
412
  if (b < k2) {
373
413
  t = intersect(slice, ax, ay, bx, by, k2);
374
- if (trackMetrics) slice.start = len + segLen * t;
414
+ if (trackMetrics)
415
+ slice.start = len + segLen * t;
375
416
  }
376
- } else {
417
+ }
418
+ else {
377
419
  addPoint(slice, ax, ay, az);
378
420
  }
379
421
  if (b < k1 && a >= k1) {
@@ -386,36 +428,33 @@ function clipLine(geom, newGeom, k1, k2, axis, isPolygon, trackMetrics) {
386
428
  t = intersect(slice, ax, ay, bx, by, k2);
387
429
  exited = true;
388
430
  }
389
-
390
431
  if (!isPolygon && exited) {
391
- if (trackMetrics) slice.end = len + segLen * t;
432
+ if (trackMetrics)
433
+ slice.end = len + segLen * t;
392
434
  newGeom.push(slice);
393
435
  slice = newSlice(geom);
394
436
  }
395
-
396
- if (trackMetrics) len += segLen;
437
+ if (trackMetrics)
438
+ len += segLen;
397
439
  }
398
-
399
440
  // add the last point
400
441
  let last = geom.length - 3;
401
442
  const ax = geom[last];
402
443
  const ay = geom[last + 1];
403
444
  const az = geom[last + 2];
404
445
  const a = axis === 0 ? ax : ay;
405
- if (a >= k1 && a <= k2) addPoint(slice, ax, ay, az);
406
-
446
+ if (a >= k1 && a <= k2)
447
+ addPoint(slice, ax, ay, az);
407
448
  // close the polygon if its endpoints are not the same after clipping
408
449
  last = slice.length - 3;
409
450
  if (isPolygon && last >= 3 && (slice[last] !== slice[0] || slice[last + 1] !== slice[1])) {
410
451
  addPoint(slice, slice[0], slice[1], slice[2]);
411
452
  }
412
-
413
453
  // add the final slice
414
454
  if (slice.length) {
415
455
  newGeom.push(slice);
416
456
  }
417
457
  }
418
-
419
458
  function newSlice(line) {
420
459
  const slice = [];
421
460
  slice.size = line.size;
@@ -423,23 +462,19 @@ function newSlice(line) {
423
462
  slice.end = line.end;
424
463
  return slice;
425
464
  }
426
-
427
465
  function clipLines(geom, newGeom, k1, k2, axis, isPolygon) {
428
466
  for (const line of geom) {
429
467
  clipLine(line, newGeom, k1, k2, axis, isPolygon, false);
430
468
  }
431
469
  }
432
-
433
470
  function addPoint(out, x, y, z) {
434
471
  out.push(x, y, z);
435
472
  }
436
-
437
473
  function intersectX(out, ax, ay, bx, by, x) {
438
474
  const t = (x - ax) / (bx - ax);
439
475
  addPoint(out, x, ay + (by - ay) * t, 1);
440
476
  return t;
441
477
  }
442
-
443
478
  function intersectY(out, ax, ay, bx, by, y) {
444
479
  const t = (y - ay) / (by - ay);
445
480
  addPoint(out, ax + (bx - ax) * t, y, 1);
@@ -449,109 +484,117 @@ function intersectY(out, ax, ay, bx, by, y) {
449
484
  function wrap(features, options) {
450
485
  const buffer = options.buffer / options.extent;
451
486
  let merged = features;
452
- const left = clip(features, 1, -1 - buffer, buffer, 0, -1, 2, options); // left world copy
453
- const right = clip(features, 1, 1 - buffer, 2 + buffer, 0, -1, 2, options); // right world copy
454
-
455
- if (left || right) {
456
- merged = clip(features, 1, -buffer, 1 + buffer, 0, -1, 2, options) || []; // center world copy
457
-
458
- if (left) merged = shiftFeatureCoords(left, 1).concat(merged); // merge left into center
459
- if (right) merged = merged.concat(shiftFeatureCoords(right, -1)); // merge right into center
460
- }
461
-
487
+ const left = clip(features, 1, -1 - buffer, buffer, 0, -1, 2, options); // left world copy
488
+ const right = clip(features, 1, 1 - buffer, 2 + buffer, 0, -1, 2, options); // right world copy
489
+ if (!left && !right)
490
+ return merged;
491
+ merged = clip(features, 1, -buffer, 1 + buffer, 0, -1, 2, options) || []; // center world copy
492
+ if (left)
493
+ merged = shiftFeatureCoords(left, 1).concat(merged); // merge left into center
494
+ if (right)
495
+ merged = merged.concat(shiftFeatureCoords(right, -1)); // merge right into center
462
496
  return merged;
463
497
  }
464
-
465
498
  function shiftFeatureCoords(features, offset) {
466
499
  const newFeatures = [];
467
-
468
- for (let i = 0; i < features.length; i++) {
469
- const feature = features[i];
470
- const type = feature.type;
471
-
472
- let newGeometry;
473
-
474
- if (type === 'Point' || type === 'MultiPoint' || type === 'LineString') {
475
- newGeometry = shiftCoords(feature.geometry, offset);
476
-
477
- } else if (type === 'MultiLineString' || type === 'Polygon') {
478
- newGeometry = [];
479
- for (const line of feature.geometry) {
480
- newGeometry.push(shiftCoords(line, offset));
500
+ for (const feature of features) {
501
+ switch (feature.type) {
502
+ case 'Point':
503
+ case 'MultiPoint':
504
+ case 'LineString': {
505
+ const newGeometry = shiftCoords(feature.geometry, offset);
506
+ newFeatures.push(createFeature(feature.id, feature.type, newGeometry, feature.tags));
507
+ continue;
481
508
  }
482
- } else if (type === 'MultiPolygon') {
483
- newGeometry = [];
484
- for (const polygon of feature.geometry) {
485
- const newPolygon = [];
486
- for (const line of polygon) {
487
- newPolygon.push(shiftCoords(line, offset));
509
+ case 'MultiLineString':
510
+ case 'Polygon': {
511
+ const newGeometry = [];
512
+ for (const line of feature.geometry) {
513
+ newGeometry.push(shiftCoords(line, offset));
514
+ }
515
+ newFeatures.push(createFeature(feature.id, feature.type, newGeometry, feature.tags));
516
+ continue;
517
+ }
518
+ case 'MultiPolygon': {
519
+ const newGeometry = [];
520
+ for (const polygon of feature.geometry) {
521
+ const newPolygon = [];
522
+ for (const line of polygon) {
523
+ newPolygon.push(shiftCoords(line, offset));
524
+ }
525
+ newGeometry.push(newPolygon);
488
526
  }
489
- newGeometry.push(newPolygon);
527
+ newFeatures.push(createFeature(feature.id, feature.type, newGeometry, feature.tags));
528
+ continue;
490
529
  }
491
530
  }
492
-
493
- newFeatures.push(createFeature(feature.id, type, newGeometry, feature.tags));
494
531
  }
495
-
496
532
  return newFeatures;
497
533
  }
498
-
499
534
  function shiftCoords(points, offset) {
500
535
  const newPoints = [];
501
536
  newPoints.size = points.size;
502
-
503
537
  if (points.start !== undefined) {
504
538
  newPoints.start = points.start;
505
539
  newPoints.end = points.end;
506
540
  }
507
-
508
541
  for (let i = 0; i < points.length; i += 3) {
509
542
  newPoints.push(points[i] + offset, points[i + 1], points[i + 2]);
510
543
  }
511
544
  return newPoints;
512
545
  }
513
546
 
514
- // Transforms the coordinates of each feature in the given tile from
515
- // mercator-projected space into (extent x extent) tile space.
547
+ /**
548
+ * Transforms the coordinates of each feature in the given tile from
549
+ * mercator-projected space into (extent x extent) tile space.
550
+ * @param tile - the tile to transform, this gets modified in place
551
+ * @param extent - the tile extent (usually 4096)
552
+ * @returns the transformed tile
553
+ */
516
554
  function transformTile(tile, extent) {
517
- if (tile.transformed) return tile;
518
-
555
+ if (tile.transformed) {
556
+ return tile;
557
+ }
519
558
  const z2 = 1 << tile.z;
520
559
  const tx = tile.x;
521
560
  const ty = tile.y;
522
-
523
561
  for (const feature of tile.features) {
524
562
  const geom = feature.geometry;
525
563
  const type = feature.type;
526
-
527
564
  feature.geometry = [];
528
-
529
565
  if (type === 1) {
530
566
  for (let j = 0; j < geom.length; j += 2) {
531
567
  feature.geometry.push(transformPoint(geom[j], geom[j + 1], extent, z2, tx, ty));
532
568
  }
533
- } else {
534
- for (let j = 0; j < geom.length; j++) {
535
- const ring = [];
536
- for (let k = 0; k < geom[j].length; k += 2) {
537
- ring.push(transformPoint(geom[j][k], geom[j][k + 1], extent, z2, tx, ty));
538
- }
539
- feature.geometry.push(ring);
569
+ continue;
570
+ }
571
+ for (const singleGeom of geom) {
572
+ const ring = [];
573
+ for (let k = 0; k < singleGeom.length; k += 2) {
574
+ ring.push(transformPoint(singleGeom[k], singleGeom[k + 1], extent, z2, tx, ty));
540
575
  }
576
+ feature.geometry.push(ring);
541
577
  }
542
578
  }
543
-
544
579
  tile.transformed = true;
545
-
546
580
  return tile;
547
581
  }
548
-
549
582
  function transformPoint(x, y, extent, z2, tx, ty) {
550
583
  return [
551
584
  Math.round(extent * (x * z2 - tx)),
552
- Math.round(extent * (y * z2 - ty))];
585
+ Math.round(extent * (y * z2 - ty))
586
+ ];
553
587
  }
554
588
 
589
+ /**
590
+ * Creates a tile object from the given features
591
+ * @param features - the features to include in the tile
592
+ * @param z
593
+ * @param tx
594
+ * @param ty
595
+ * @param options - the options object
596
+ * @returns the created tile
597
+ */
555
598
  function createTile(features, z, tx, ty, options) {
556
599
  const tolerance = z === options.maxZoom ? 0 : options.tolerance / ((1 << z) * options.extent);
557
600
  const tile = {
@@ -574,77 +617,68 @@ function createTile(features, z, tx, ty, options) {
574
617
  }
575
618
  return tile;
576
619
  }
577
-
578
620
  function addFeature(tile, feature, tolerance, options) {
579
- const geom = feature.geometry;
580
- const type = feature.type;
581
621
  const simplified = [];
582
-
583
622
  tile.minX = Math.min(tile.minX, feature.minX);
584
623
  tile.minY = Math.min(tile.minY, feature.minY);
585
624
  tile.maxX = Math.max(tile.maxX, feature.maxX);
586
625
  tile.maxY = Math.max(tile.maxY, feature.maxY);
587
-
588
- if (type === 'Point' || type === 'MultiPoint') {
589
- for (let i = 0; i < geom.length; i += 3) {
590
- simplified.push(geom[i], geom[i + 1]);
591
- tile.numPoints++;
592
- tile.numSimplified++;
593
- }
594
-
595
- } else if (type === 'LineString') {
596
- addLine(simplified, geom, tile, tolerance, false, false);
597
-
598
- } else if (type === 'MultiLineString' || type === 'Polygon') {
599
- for (let i = 0; i < geom.length; i++) {
600
- addLine(simplified, geom[i], tile, tolerance, type === 'Polygon', i === 0);
601
- }
602
-
603
- } else if (type === 'MultiPolygon') {
604
-
605
- for (let k = 0; k < geom.length; k++) {
606
- const polygon = geom[k];
607
- for (let i = 0; i < polygon.length; i++) {
608
- addLine(simplified, polygon[i], tile, tolerance, true, i === 0);
626
+ switch (feature.type) {
627
+ case 'Point':
628
+ case 'MultiPoint':
629
+ for (let i = 0; i < feature.geometry.length; i += 3) {
630
+ simplified.push(feature.geometry[i], feature.geometry[i + 1]);
631
+ tile.numPoints++;
632
+ tile.numSimplified++;
609
633
  }
610
- }
634
+ break;
635
+ case 'LineString':
636
+ addLine(simplified, feature.geometry, tile, tolerance, false, false);
637
+ break;
638
+ case 'MultiLineString':
639
+ case 'Polygon':
640
+ for (let i = 0; i < feature.geometry.length; i++) {
641
+ addLine(simplified, feature.geometry[i], tile, tolerance, feature.type === 'Polygon', i === 0);
642
+ }
643
+ break;
644
+ case 'MultiPolygon':
645
+ for (let k = 0; k < feature.geometry.length; k++) {
646
+ const polygon = feature.geometry[k];
647
+ for (let i = 0; i < polygon.length; i++) {
648
+ addLine(simplified, polygon[i], tile, tolerance, true, i === 0);
649
+ }
650
+ }
651
+ break;
611
652
  }
612
-
613
- if (simplified.length) {
614
- let tags = feature.tags || null;
615
-
616
- if (type === 'LineString' && options.lineMetrics) {
617
- tags = {};
618
- for (const key in feature.tags) tags[key] = feature.tags[key];
619
- /* eslint-disable dot-notation */
620
- tags['mapbox_clip_start'] = geom.start / geom.size;
621
- tags['mapbox_clip_end'] = geom.end / geom.size;
622
- /* eslint-enable dot-notation */
623
- }
624
-
625
- const tileFeature = {
626
- geometry: simplified,
627
- type: type === 'Polygon' || type === 'MultiPolygon' ? 3 :
628
- (type === 'LineString' || type === 'MultiLineString' ? 2 : 1),
629
- tags
630
- };
631
- if (feature.id !== null) {
632
- tileFeature.id = feature.id;
633
- }
634
- tile.features.push(tileFeature);
653
+ if (!simplified.length)
654
+ return;
655
+ let tags = feature.tags || null;
656
+ if (feature.type === 'LineString' && options.lineMetrics) {
657
+ tags = {};
658
+ for (const key in feature.tags)
659
+ tags[key] = feature.tags[key];
660
+ // HM TODO: replace with geojsonvt
661
+ tags['mapbox_clip_start'] = feature.geometry.start / feature.geometry.size;
662
+ tags['mapbox_clip_end'] = feature.geometry.end / feature.geometry.size;
663
+ }
664
+ const tileFeature = {
665
+ geometry: simplified,
666
+ type: feature.type === 'Polygon' || feature.type === 'MultiPolygon' ? 3 :
667
+ (feature.type === 'LineString' || feature.type === 'MultiLineString' ? 2 : 1),
668
+ tags
669
+ };
670
+ if (feature.id !== null) {
671
+ tileFeature.id = feature.id;
635
672
  }
673
+ tile.features.push(tileFeature);
636
674
  }
637
-
638
675
  function addLine(result, geom, tile, tolerance, isPolygon, isOuter) {
639
676
  const sqTolerance = tolerance * tolerance;
640
-
641
677
  if (tolerance > 0 && (geom.size < (isPolygon ? sqTolerance : tolerance))) {
642
678
  tile.numPoints += geom.length / 3;
643
679
  return;
644
680
  }
645
-
646
681
  const ring = [];
647
-
648
682
  for (let i = 0; i < geom.length; i += 3) {
649
683
  if (tolerance === 0 || geom[i + 2] > sqTolerance) {
650
684
  tile.numSimplified++;
@@ -652,142 +686,99 @@ function addLine(result, geom, tile, tolerance, isPolygon, isOuter) {
652
686
  }
653
687
  tile.numPoints++;
654
688
  }
655
-
656
- if (isPolygon) rewind(ring, isOuter);
657
-
689
+ if (isPolygon)
690
+ rewind(ring, isOuter);
658
691
  result.push(ring);
659
692
  }
660
-
661
693
  function rewind(ring, clockwise) {
662
694
  let area = 0;
663
695
  for (let i = 0, len = ring.length, j = len - 2; i < len; j = i, i += 2) {
664
696
  area += (ring[i] - ring[j]) * (ring[i + 1] + ring[j + 1]);
665
697
  }
666
- if (area > 0 === clockwise) {
667
- for (let i = 0, len = ring.length; i < len / 2; i += 2) {
668
- const x = ring[i];
669
- const y = ring[i + 1];
670
- ring[i] = ring[len - 2 - i];
671
- ring[i + 1] = ring[len - 1 - i];
672
- ring[len - 2 - i] = x;
673
- ring[len - 1 - i] = y;
674
- }
698
+ if (area > 0 !== clockwise)
699
+ return;
700
+ for (let i = 0, len = ring.length; i < len / 2; i += 2) {
701
+ const x = ring[i];
702
+ const y = ring[i + 1];
703
+ ring[i] = ring[len - 2 - i];
704
+ ring[i + 1] = ring[len - 1 - i];
705
+ ring[len - 2 - i] = x;
706
+ ring[len - 1 - i] = y;
675
707
  }
676
708
  }
677
709
 
678
- // This file provides a set of helper functions for managing "diffs" (changes)
679
- // to GeoJSON data structures. These diffs describe additions, removals,
680
- // and updates of features in a GeoJSON source in an efficient way.
681
-
682
- // GeoJSON Source Diff:
683
- // {
684
- // removeAll: true, // If true, clear all existing features
685
- // remove: [featureId, ...], // Array of feature IDs to remove
686
- // add: [feature, ...], // Array of GeoJSON features to add
687
- // update: [GeoJSON Feature Diff, ...] // Array of per-feature updates
688
- // }
689
-
690
- // GeoJSON Feature Diff:
691
- // {
692
- // id: featureId, // ID of the feature being updated
693
- // newGeometry: GeoJSON.Geometry, // Optional new geometry
694
- // removeAllProperties: true, // Remove all properties if true
695
- // removeProperties: [key, ...], // Specific properties to delete
696
- // addOrUpdateProperties: [ // Properties to add or update
697
- // { key: "name", value: "New name" }
698
- // ]
699
- // }
700
-
701
- /* eslint @stylistic/comma-spacing: 0, no-shadow: 0 */
702
-
703
- // applies a diff to the geojsonvt source simplified features array
704
- // returns an object with the affected features and new source array for invalidation
710
+ /**
711
+ * Applies a GeoJSON Source Diff to an existing set of simplified features
712
+ * @param source
713
+ * @param dataDiff
714
+ * @param options
715
+ * @returns
716
+ */
705
717
  function applySourceDiff(source, dataDiff, options) {
706
-
707
718
  // convert diff to sets/maps for o(1) lookups
708
719
  const diff = diffToHashed(dataDiff);
709
-
710
720
  // collection for features that will be affected by this update
711
721
  let affected = [];
712
-
713
722
  // full removal - clear everything before applying diff
714
723
  if (diff.removeAll) {
715
724
  affected = source;
716
725
  source = [];
717
726
  }
718
-
719
727
  // remove/add features and collect affected ones
720
728
  if (diff.remove.size || diff.add.size) {
721
729
  const removeFeatures = [];
722
-
723
730
  // collect source features to be removed
724
731
  for (const feature of source) {
725
- const {id} = feature;
726
-
732
+ const { id } = feature;
727
733
  // explicit feature removal
728
734
  if (diff.remove.has(id)) {
729
735
  removeFeatures.push(feature);
730
- // feature with duplicate id being added
731
- } else if (diff.add.has(id)) {
736
+ // feature with duplicate id being added
737
+ }
738
+ else if (diff.add.has(id)) {
732
739
  removeFeatures.push(feature);
733
740
  }
734
741
  }
735
-
736
742
  // collect affected and remove from source
737
743
  if (removeFeatures.length) {
738
744
  affected.push(...removeFeatures);
739
-
740
745
  const removeIds = new Set(removeFeatures.map(f => f.id));
741
746
  source = source.filter(f => !removeIds.has(f.id));
742
747
  }
743
-
744
748
  // convert and add new features
745
749
  if (diff.add.size) {
746
750
  // projects and adds simplification info
747
- let addFeatures = convert({type: 'FeatureCollection', features: Array.from(diff.add.values())}, options);
748
-
751
+ let addFeatures = convert({ type: 'FeatureCollection', features: Array.from(diff.add.values()) }, options);
749
752
  // wraps features (ie extreme west and extreme east)
750
753
  addFeatures = wrap(addFeatures, options);
751
-
752
754
  affected.push(...addFeatures);
753
755
  source.push(...addFeatures);
754
756
  }
755
757
  }
756
-
757
758
  if (diff.update.size) {
758
759
  for (const [id, update] of diff.update) {
759
760
  const featureIndex = source.findIndex(f => f.id === id);
760
- if (featureIndex === -1) continue;
761
-
761
+ if (featureIndex === -1)
762
+ continue;
762
763
  const feature = source[featureIndex];
763
-
764
764
  // get updated geojsonvt simplified feature
765
765
  const updatedFeature = getUpdatedFeature(feature, update, options);
766
- if (!updatedFeature) continue;
767
-
766
+ if (!updatedFeature)
767
+ continue;
768
768
  // track both features for invalidation
769
769
  affected.push(feature, updatedFeature);
770
-
771
770
  // replace old feature with updated feature
772
771
  source[featureIndex] = updatedFeature;
773
772
  }
774
773
  }
775
-
776
- return {affected, source};
774
+ return { affected, source };
777
775
  }
778
-
779
776
  // return an updated geojsonvt simplified feature
780
777
  function getUpdatedFeature(vtFeature, update, options) {
781
778
  const changeGeometry = !!update.newGeometry;
782
-
783
- const changeProps =
784
- update.removeAllProperties ||
779
+ const changeProps = update.removeAllProperties ||
785
780
  update.removeProperties?.length > 0 ||
786
781
  update.addOrUpdateProperties?.length > 0;
787
-
788
- // nothing to do
789
- if (!changeGeometry && !changeProps) return null;
790
-
791
782
  // if geometry changed, need to create new geojson feature and convert to simplified format
792
783
  if (changeGeometry) {
793
784
  const geojsonFeature = {
@@ -796,95 +787,99 @@ function getUpdatedFeature(vtFeature, update, options) {
796
787
  geometry: update.newGeometry,
797
788
  properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
798
789
  };
799
-
800
790
  // projects and adds simplification info
801
- let features = convert({type: 'FeatureCollection', features: [geojsonFeature]}, options);
802
-
791
+ let features = convert({ type: 'FeatureCollection', features: [geojsonFeature] }, options);
803
792
  // wraps features (ie extreme west and extreme east)
804
793
  features = wrap(features, options);
805
-
806
794
  return features[0];
807
795
  }
808
-
809
796
  // only properties changed - update tags directly
810
797
  if (changeProps) {
811
- const feature = {...vtFeature};
798
+ const feature = { ...vtFeature };
812
799
  feature.tags = applyPropertyUpdates(feature.tags, update);
813
800
  return feature;
814
801
  }
815
-
816
802
  return null;
817
803
  }
818
-
819
- // helper to apply property updates from a diff update object to a properties object
804
+ /**
805
+ * helper to apply property updates from a diff update object to a properties object
806
+ */
820
807
  function applyPropertyUpdates(tags, update) {
821
808
  if (update.removeAllProperties) {
822
809
  return {};
823
810
  }
824
-
825
- const properties = {...tags || {}};
826
-
811
+ const properties = { ...tags || {} };
827
812
  if (update.removeProperties) {
828
813
  for (const key of update.removeProperties) {
829
814
  delete properties[key];
830
815
  }
831
816
  }
832
-
833
817
  if (update.addOrUpdateProperties) {
834
- for (const {key, value} of update.addOrUpdateProperties) {
818
+ for (const { key, value } of update.addOrUpdateProperties) {
835
819
  properties[key] = value;
836
820
  }
837
821
  }
838
-
839
822
  return properties;
840
823
  }
841
-
842
- // Convert a GeoJSON Source Diff to an idempotent hashed representation using Sets and Maps
824
+ /**
825
+ * Convert a GeoJSON Source Diff to an idempotent hashed representation using Sets and Maps
826
+ */
843
827
  function diffToHashed(diff) {
844
- if (!diff) return {};
845
-
846
- const hashed = {};
847
-
848
- hashed.removeAll = diff.removeAll;
849
- hashed.remove = new Set(diff.remove || []);
850
- hashed.add = new Map(diff.add?.map(feature => [feature.id, feature]));
851
- hashed.update = new Map(diff.update?.map(update => [update.id, update]));
852
-
828
+ if (!diff)
829
+ return {
830
+ remove: new Set(),
831
+ add: new Map(),
832
+ update: new Map()
833
+ };
834
+ const hashed = {
835
+ removeAll: diff.removeAll,
836
+ remove: new Set(diff.remove || []),
837
+ add: new Map(diff.add?.map(feature => [feature.id, feature])),
838
+ update: new Map(diff.update?.map(update => [update.id, update]))
839
+ };
853
840
  return hashed;
854
841
  }
855
842
 
856
843
  const defaultOptions = {
857
- maxZoom: 14, // max zoom to preserve detail on
858
- indexMaxZoom: 5, // max zoom in the tile index
859
- indexMaxPoints: 100000, // max number of points per tile in the tile index
860
- tolerance: 3, // simplification tolerance (higher means simpler)
861
- extent: 4096, // tile extent
862
- buffer: 64, // tile buffer on each side
863
- lineMetrics: false, // whether to calculate line metrics
864
- promoteId: null, // name of a feature property to be promoted to feature.id
865
- generateId: false, // whether to generate feature ids. Cannot be used with promoteId
866
- updateable: false, // whether geojson can be updated (with caveat of a stored simplified copy)
867
- debug: 0 // logging level (0, 1 or 2)
844
+ maxZoom: 14,
845
+ indexMaxZoom: 5,
846
+ indexMaxPoints: 100000,
847
+ tolerance: 3,
848
+ extent: 4096,
849
+ buffer: 64,
850
+ lineMetrics: false,
851
+ promoteId: null,
852
+ generateId: false,
853
+ updateable: false,
854
+ debug: 0
868
855
  };
869
-
856
+ /**
857
+ * Main class for creating and managing a vector tile index from GeoJSON data.
858
+ */
870
859
  class GeoJSONVT {
860
+ options;
861
+ /** @internal */
862
+ tiles;
863
+ tileCoords;
864
+ /** @internal */
865
+ stats = {};
866
+ /** @internal */
867
+ total = 0;
868
+ source;
871
869
  constructor(data, options) {
872
- options = this.options = extend(Object.create(defaultOptions), options);
873
-
870
+ options = this.options = Object.assign({}, defaultOptions, options);
874
871
  const debug = options.debug;
875
-
876
- if (debug) console.time('preprocess data');
877
-
878
- if (options.maxZoom < 0 || options.maxZoom > 24) throw new Error('maxZoom should be in the 0-24 range');
879
- if (options.promoteId && options.generateId) throw new Error('promoteId and generateId cannot be used together.');
880
-
872
+ if (debug)
873
+ console.time('preprocess data');
874
+ if (options.maxZoom < 0 || options.maxZoom > 24)
875
+ throw new Error('maxZoom should be in the 0-24 range');
876
+ if (options.promoteId && options.generateId)
877
+ throw new Error('promoteId and generateId cannot be used together.');
881
878
  // projects and adds simplification info
882
879
  let features = convert(data, options);
883
-
884
880
  // tiles and tileCoords are part of the public API
885
881
  this.tiles = {};
886
882
  this.tileCoords = [];
887
-
888
883
  if (debug) {
889
884
  console.timeEnd('preprocess data');
890
885
  console.log('index: maxZoom: %d, maxPoints: %d', options.indexMaxZoom, options.indexMaxPoints);
@@ -892,207 +887,203 @@ class GeoJSONVT {
892
887
  this.stats = {};
893
888
  this.total = 0;
894
889
  }
895
-
896
890
  // wraps features (ie extreme west and extreme east)
897
891
  features = wrap(features, options);
898
-
899
892
  // start slicing from the top tile down
900
- if (features.length) this.splitTile(features, 0, 0, 0);
901
-
893
+ if (features.length) {
894
+ this.splitTile(features, 0, 0, 0);
895
+ }
902
896
  // for updateable indexes, store a copy of the original simplified features
903
897
  if (options.updateable) {
904
898
  this.source = features;
905
899
  }
906
-
907
900
  if (debug) {
908
- if (features.length) console.log('features: %d, points: %d', this.tiles[0].numFeatures, this.tiles[0].numPoints);
901
+ if (features.length)
902
+ console.log('features: %d, points: %d', this.tiles[0].numFeatures, this.tiles[0].numPoints);
909
903
  console.timeEnd('generate tiles');
910
904
  console.log('tiles generated:', this.total, JSON.stringify(this.stats));
911
905
  }
912
906
  }
913
-
914
- // splits features from a parent tile to sub-tiles.
915
- // z, x, and y are the coordinates of the parent tile
916
- // cz, cx, and cy are the coordinates of the target tile
917
- //
918
- // If no target tile is specified, splitting stops when we reach the maximum
919
- // zoom or the number of points is low as specified in the options.
907
+ /**
908
+ * splits features from a parent tile to sub-tiles.
909
+ * z, x, and y are the coordinates of the parent tile
910
+ * cz, cx, and cy are the coordinates of the target tile
911
+ *
912
+ * If no target tile is specified, splitting stops when we reach the maximum
913
+ * zoom or the number of points is low as specified in the options.
914
+ * @internal
915
+ * @param features - features to split
916
+ * @param z - tile zoom level
917
+ * @param x - tile x coordinate
918
+ * @param y - tile y coordinate
919
+ * @param cz - target tile zoom level
920
+ * @param cx - target tile x coordinate
921
+ * @param cy - target tile y coordinate
922
+ */
920
923
  splitTile(features, z, x, y, cz, cx, cy) {
921
-
922
924
  const stack = [features, z, x, y];
923
925
  const options = this.options;
924
926
  const debug = options.debug;
925
-
926
927
  // avoid recursion by using a processing queue
927
928
  while (stack.length) {
928
929
  y = stack.pop();
929
930
  x = stack.pop();
930
931
  z = stack.pop();
931
932
  features = stack.pop();
932
-
933
933
  const z2 = 1 << z;
934
934
  const id = toID(z, x, y);
935
935
  let tile = this.tiles[id];
936
-
937
936
  if (!tile) {
938
- if (debug > 1) console.time('creation');
939
-
937
+ if (debug > 1)
938
+ console.time('creation');
940
939
  tile = this.tiles[id] = createTile(features, z, x, y, options);
941
- this.tileCoords.push({z, x, y, id});
942
-
940
+ this.tileCoords.push({ z, x, y, id });
943
941
  if (debug) {
944
942
  if (debug > 1) {
945
- console.log('tile z%d-%d-%d (features: %d, points: %d, simplified: %d)',
946
- z, x, y, tile.numFeatures, tile.numPoints, tile.numSimplified);
943
+ console.log('tile z%d-%d-%d (features: %d, points: %d, simplified: %d)', z, x, y, tile.numFeatures, tile.numPoints, tile.numSimplified);
947
944
  console.timeEnd('creation');
948
945
  }
949
- const key = `z${ z}`;
946
+ const key = `z${z}`;
950
947
  this.stats[key] = (this.stats[key] || 0) + 1;
951
948
  this.total++;
952
949
  }
953
950
  }
954
-
955
951
  // save reference to original geometry in tile so that we can drill down later if we stop now
956
952
  tile.source = features;
957
-
958
953
  // if it's the first-pass tiling
959
954
  if (cz == null) {
960
955
  // stop tiling if we reached max zoom, or if the tile is too simple
961
- if (z === options.indexMaxZoom || tile.numPoints <= options.indexMaxPoints) continue;
962
- // if a drilldown to a specific tile
963
- } else if (z === options.maxZoom || z === cz) {
956
+ if (z === options.indexMaxZoom || tile.numPoints <= options.indexMaxPoints)
957
+ continue;
958
+ // if a drilldown to a specific tile
959
+ }
960
+ else if (z === options.maxZoom || z === cz) {
964
961
  // stop tiling if we reached base zoom or our target tile zoom
965
962
  continue;
966
- } else if (cz != null) {
963
+ }
964
+ else if (cz != null) {
967
965
  // stop tiling if it's not an ancestor of the target tile
968
966
  const zoomSteps = cz - z;
969
- if (x !== cx >> zoomSteps || y !== cy >> zoomSteps) continue;
967
+ if (x !== cx >> zoomSteps || y !== cy >> zoomSteps)
968
+ continue;
970
969
  }
971
-
972
970
  // if we slice further down, no need to keep source geometry
973
971
  tile.source = null;
974
-
975
- if (features.length === 0) continue;
976
-
977
- if (debug > 1) console.time('clipping');
978
-
972
+ if (!features.length)
973
+ continue;
974
+ if (debug > 1)
975
+ console.time('clipping');
979
976
  // values we'll use for clipping
980
977
  const k1 = 0.5 * options.buffer / options.extent;
981
978
  const k2 = 0.5 - k1;
982
979
  const k3 = 0.5 + k1;
983
980
  const k4 = 1 + k1;
984
-
985
981
  let tl = null;
986
982
  let bl = null;
987
983
  let tr = null;
988
984
  let br = null;
989
-
990
- const left = clip(features, z2, x - k1, x + k3, 0, tile.minX, tile.maxX, options);
985
+ const left = clip(features, z2, x - k1, x + k3, 0, tile.minX, tile.maxX, options);
991
986
  const right = clip(features, z2, x + k2, x + k4, 0, tile.minX, tile.maxX, options);
992
-
993
987
  if (left) {
994
988
  tl = clip(left, z2, y - k1, y + k3, 1, tile.minY, tile.maxY, options);
995
989
  bl = clip(left, z2, y + k2, y + k4, 1, tile.minY, tile.maxY, options);
996
990
  }
997
-
998
991
  if (right) {
999
992
  tr = clip(right, z2, y - k1, y + k3, 1, tile.minY, tile.maxY, options);
1000
993
  br = clip(right, z2, y + k2, y + k4, 1, tile.minY, tile.maxY, options);
1001
994
  }
1002
-
1003
- if (debug > 1) console.timeEnd('clipping');
1004
-
1005
- stack.push(tl || [], z + 1, x * 2, y * 2);
1006
- stack.push(bl || [], z + 1, x * 2, y * 2 + 1);
995
+ if (debug > 1)
996
+ console.timeEnd('clipping');
997
+ stack.push(tl || [], z + 1, x * 2, y * 2);
998
+ stack.push(bl || [], z + 1, x * 2, y * 2 + 1);
1007
999
  stack.push(tr || [], z + 1, x * 2 + 1, y * 2);
1008
1000
  stack.push(br || [], z + 1, x * 2 + 1, y * 2 + 1);
1009
1001
  }
1010
1002
  }
1011
-
1003
+ /**
1004
+ * Given z, x, and y tile coordinates, returns the corresponding tile with geometries in tile coordinates, much like MVT data is stored.
1005
+ * @param z - tile zoom level
1006
+ * @param x - tile x coordinate
1007
+ * @param y - tile y coordinate
1008
+ * @returns the transformed tile or null if not found
1009
+ */
1012
1010
  getTile(z, x, y) {
1013
1011
  z = +z;
1014
1012
  x = +x;
1015
1013
  y = +y;
1016
-
1017
1014
  const options = this.options;
1018
- const {extent, debug} = options;
1019
-
1020
- if (z < 0 || z > 24) return null;
1021
-
1015
+ const { extent, debug } = options;
1016
+ if (z < 0 || z > 24)
1017
+ return null;
1022
1018
  const z2 = 1 << z;
1023
1019
  x = (x + z2) & (z2 - 1); // wrap tile x coordinate
1024
-
1025
1020
  const id = toID(z, x, y);
1026
- if (this.tiles[id]) return transformTile(this.tiles[id], extent);
1027
-
1028
- if (debug > 1) console.log('drilling down to z%d-%d-%d', z, x, y);
1029
-
1021
+ if (this.tiles[id]) {
1022
+ return transformTile(this.tiles[id], extent);
1023
+ }
1024
+ if (debug > 1)
1025
+ console.log('drilling down to z%d-%d-%d', z, x, y);
1030
1026
  let z0 = z;
1031
1027
  let x0 = x;
1032
1028
  let y0 = y;
1033
1029
  let parent;
1034
-
1035
1030
  while (!parent && z0 > 0) {
1036
1031
  z0--;
1037
1032
  x0 = x0 >> 1;
1038
1033
  y0 = y0 >> 1;
1039
1034
  parent = this.tiles[toID(z0, x0, y0)];
1040
1035
  }
1041
-
1042
- if (!parent || !parent.source) return null;
1043
-
1036
+ if (!parent?.source)
1037
+ return null;
1044
1038
  // if we found a parent tile containing the original geometry, we can drill down from it
1045
1039
  if (debug > 1) {
1046
1040
  console.log('found parent tile z%d-%d-%d', z0, x0, y0);
1047
1041
  console.time('drilling down');
1048
1042
  }
1049
1043
  this.splitTile(parent.source, z0, x0, y0, z, x, y);
1050
- if (debug > 1) console.timeEnd('drilling down');
1051
-
1052
- return this.tiles[id] ? transformTile(this.tiles[id], extent) : null;
1044
+ if (debug > 1)
1045
+ console.timeEnd('drilling down');
1046
+ if (!this.tiles[id])
1047
+ return null;
1048
+ return transformTile(this.tiles[id], extent);
1053
1049
  }
1054
-
1055
- // invalidates (removes) tiles affected by the provided features
1050
+ /**
1051
+ * Invalidates (removes) tiles affected by the provided features
1052
+ * @internal
1053
+ * @param features
1054
+ */
1056
1055
  invalidateTiles(features) {
1057
1056
  const options = this.options;
1058
- const {debug} = options;
1059
-
1057
+ const { debug } = options;
1060
1058
  // calculate bounding box of all features for trivial reject
1061
1059
  let minX = Infinity;
1062
1060
  let maxX = -Infinity;
1063
1061
  let minY = Infinity;
1064
1062
  let maxY = -Infinity;
1065
-
1066
1063
  for (const feature of features) {
1067
1064
  minX = Math.min(minX, feature.minX);
1068
1065
  maxX = Math.max(maxX, feature.maxX);
1069
1066
  minY = Math.min(minY, feature.minY);
1070
1067
  maxY = Math.max(maxY, feature.maxY);
1071
1068
  }
1072
-
1073
1069
  // tile buffer clipping value - not halved as in splitTile above because checking against tile's own extent
1074
1070
  const k1 = options.buffer / options.extent;
1075
-
1076
1071
  // track removed tile ids for o(1) lookup
1077
1072
  const removedLookup = new Set();
1078
-
1079
1073
  // iterate through existing tiles and remove ones that are affected by features
1080
1074
  for (const id in this.tiles) {
1081
1075
  const tile = this.tiles[id];
1082
-
1083
1076
  // calculate tile bounds including buffer
1084
1077
  const z2 = 1 << tile.z;
1085
- const tileMinX = (tile.x - k1) / z2;
1078
+ const tileMinX = (tile.x - k1) / z2;
1086
1079
  const tileMaxX = (tile.x + 1 + k1) / z2;
1087
- const tileMinY = (tile.y - k1) / z2;
1080
+ const tileMinY = (tile.y - k1) / z2;
1088
1081
  const tileMaxY = (tile.y + 1 + k1) / z2;
1089
-
1090
1082
  // trivial reject if feature bounds don't intersect tile
1091
1083
  if (maxX < tileMinX || minX >= tileMaxX ||
1092
1084
  maxY < tileMinY || minY >= tileMaxY) {
1093
1085
  continue;
1094
1086
  }
1095
-
1096
1087
  // check if any feature intersects with the tile
1097
1088
  let intersects = false;
1098
1089
  for (const feature of features) {
@@ -1102,80 +1093,66 @@ class GeoJSONVT {
1102
1093
  break;
1103
1094
  }
1104
1095
  }
1105
- if (!intersects) continue;
1106
-
1096
+ if (!intersects)
1097
+ continue;
1107
1098
  if (debug) {
1108
1099
  if (debug > 1) {
1109
- console.log('invalidate tile z%d-%d-%d (features: %d, points: %d, simplified: %d)',
1110
- tile.z, tile.x, tile.y, tile.numFeatures, tile.numPoints, tile.numSimplified);
1100
+ console.log('invalidate tile z%d-%d-%d (features: %d, points: %d, simplified: %d)', tile.z, tile.x, tile.y, tile.numFeatures, tile.numPoints, tile.numSimplified);
1111
1101
  }
1112
- const key = `z${ tile.z}`;
1102
+ const key = `z${tile.z}`;
1113
1103
  this.stats[key] = (this.stats[key] || 0) - 1;
1114
1104
  this.total--;
1115
1105
  }
1116
-
1117
1106
  delete this.tiles[id];
1118
1107
  removedLookup.add(id);
1119
1108
  }
1120
-
1121
1109
  // remove tile coords that are no longer in the index
1122
- if (removedLookup.size) this.tileCoords = this.tileCoords.filter(c => !removedLookup.has(c.id));
1110
+ if (removedLookup.size) {
1111
+ this.tileCoords = this.tileCoords.filter(c => !removedLookup.has(c.id));
1112
+ }
1123
1113
  }
1124
-
1125
- // updates the tile index by adding and/or removing geojson features
1126
- // invalidates tiles that are affected by the update for regeneration on next getTile call
1127
- // diff is an object with properties specified in difference.js
1114
+ /**
1115
+ * Updates the tile index by adding and/or removing geojson features
1116
+ * invalidates tiles that are affected by the update for regeneration on next getTile call.
1117
+ * @param diff - the source diff object
1118
+ */
1128
1119
  updateData(diff) {
1129
1120
  const options = this.options;
1130
1121
  const debug = options.debug;
1131
-
1132
- if (!options.updateable) throw new Error('to update tile geojson `updateable` option must be set to true');
1133
-
1122
+ if (!options.updateable)
1123
+ throw new Error('to update tile geojson `updateable` option must be set to true');
1134
1124
  // apply diff and collect affected features and updated source that will be used to invalidate tiles
1135
- const {affected, source} = applySourceDiff(this.source, diff, options);
1136
-
1125
+ const { affected, source } = applySourceDiff(this.source, diff, options);
1137
1126
  // nothing has changed
1138
- if (!affected.length) return;
1139
-
1127
+ if (!affected.length)
1128
+ return;
1140
1129
  // update source with new simplified feature set
1141
1130
  this.source = source;
1142
-
1143
1131
  if (debug > 1) {
1144
1132
  console.log('invalidating tiles');
1145
1133
  console.time('invalidating');
1146
1134
  }
1147
-
1148
1135
  this.invalidateTiles(affected);
1149
-
1150
- if (debug > 1) console.timeEnd('invalidating');
1151
-
1136
+ if (debug > 1)
1137
+ console.timeEnd('invalidating');
1152
1138
  // re-generate root tile with updated feature set
1153
1139
  const [z, x, y] = [0, 0, 0];
1154
1140
  const rootTile = createTile(this.source, z, x, y, this.options);
1155
1141
  rootTile.source = this.source;
1156
-
1157
1142
  // update tile index with new root tile - ready for getTile calls
1158
1143
  const id = toID(z, x, y);
1159
1144
  this.tiles[id] = rootTile;
1160
- this.tileCoords.push({z, x, y, id});
1161
-
1145
+ this.tileCoords.push({ z, x, y, id });
1162
1146
  if (debug) {
1163
- const key = `z${ z}`;
1147
+ const key = `z${z}`;
1164
1148
  this.stats[key] = (this.stats[key] || 0) + 1;
1165
1149
  this.total++;
1166
1150
  }
1167
1151
  }
1168
1152
  }
1169
-
1170
1153
  function toID(z, x, y) {
1171
1154
  return (((1 << z) * y + x) * 32) + z;
1172
1155
  }
1173
-
1174
- function extend(dest, src) {
1175
- for (const i in src) dest[i] = src[i];
1176
- return dest;
1177
- }
1178
-
1179
1156
  function geojsonvt(data, options) {
1180
1157
  return new GeoJSONVT(data, options);
1181
1158
  }