@maplibre/geojson-vt 5.0.3 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -13
- package/dist/clip.d.ts +22 -0
- package/dist/clip.d.ts.map +1 -0
- package/dist/clip.test.d.ts +2 -0
- package/dist/clip.test.d.ts.map +1 -0
- package/dist/cluster-tile-index.d.ts +76 -0
- package/dist/cluster-tile-index.d.ts.map +1 -0
- package/dist/cluster-tile-index.test.d.ts +2 -0
- package/dist/cluster-tile-index.test.d.ts.map +1 -0
- package/dist/convert.d.ts +17 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/deconvert.d.ts +19 -0
- package/dist/deconvert.d.ts.map +1 -0
- package/dist/deconvert.test.d.ts +2 -0
- package/dist/deconvert.test.d.ts.map +1 -0
- package/dist/definitions.d.ts +241 -0
- package/dist/definitions.d.ts.map +1 -0
- package/dist/difference.d.ts +67 -0
- package/dist/difference.d.ts.map +1 -0
- package/dist/difference.test.d.ts +2 -0
- package/dist/difference.test.d.ts.map +1 -0
- package/dist/feature.d.ts +20 -0
- package/dist/feature.d.ts.map +1 -0
- package/dist/geojson-to-tile.d.ts +35 -0
- package/dist/geojson-to-tile.d.ts.map +1 -0
- package/dist/geojson-vt-dev.js +1582 -478
- package/dist/geojson-vt.js +1 -1
- package/dist/geojson-vt.mjs +1250 -473
- package/dist/geojson-vt.mjs.map +1 -1
- package/dist/geojsonvt.d.ts +76 -0
- package/dist/geojsonvt.d.ts.map +1 -0
- package/dist/geojsonvt.test.d.ts +2 -0
- package/dist/geojsonvt.test.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/simplify.d.ts +9 -0
- package/dist/simplify.d.ts.map +1 -0
- package/dist/simplify.test.d.ts +2 -0
- package/dist/simplify.test.d.ts.map +1 -0
- package/dist/tile-index.d.ts +51 -0
- package/dist/tile-index.d.ts.map +1 -0
- package/dist/tile.d.ts +12 -0
- package/dist/tile.d.ts.map +1 -0
- package/dist/transform.d.ts +10 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/wrap.d.ts +3 -0
- package/dist/wrap.d.ts.map +1 -0
- package/package.json +26 -12
- package/src/clip.ts +119 -81
- package/src/cluster-tile-index.test.ts +205 -0
- package/src/cluster-tile-index.ts +513 -0
- package/src/convert.ts +97 -75
- package/src/deconvert.test.ts +153 -0
- package/src/deconvert.ts +92 -0
- package/src/definitions.ts +196 -18
- package/src/difference.ts +3 -3
- package/src/feature.ts +11 -4
- package/src/geojson-to-tile.ts +58 -0
- package/src/geojsonvt.test.ts +39 -0
- package/src/geojsonvt.ts +209 -0
- package/src/index.ts +27 -378
- package/src/tile-index.ts +310 -0
- package/src/tile.ts +92 -103
- package/src/transform.ts +41 -39
- package/src/wrap.ts +4 -4
package/dist/geojson-vt.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
139
|
+
featureToInternal(features, data.features[i], options, i);
|
|
138
140
|
}
|
|
139
141
|
break;
|
|
140
142
|
case 'Feature':
|
|
141
|
-
|
|
143
|
+
featureToInternal(features, data, options);
|
|
142
144
|
break;
|
|
143
145
|
default:
|
|
144
|
-
|
|
146
|
+
featureToInternal(features, { geometry: data, properties: undefined }, options);
|
|
145
147
|
}
|
|
146
148
|
return features;
|
|
147
149
|
}
|
|
148
|
-
function
|
|
150
|
+
function featureToInternal(features, geojson, options, index) {
|
|
149
151
|
if (!geojson.geometry)
|
|
150
152
|
return;
|
|
151
153
|
if (geojson.geometry.type === 'GeometryCollection') {
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
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,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (minAll >=
|
|
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 <
|
|
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 ===
|
|
296
|
-
const max = axis ===
|
|
297
|
-
if (min >=
|
|
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 <
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 >=
|
|
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,
|
|
508
|
+
function clipLine(geom, newGeom, start, end, axis, isPolygon, trackMetrics) {
|
|
381
509
|
let slice = newSlice(geom);
|
|
382
|
-
const intersect = axis ===
|
|
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 ===
|
|
392
|
-
const b = axis ===
|
|
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 <
|
|
524
|
+
if (a < start) {
|
|
397
525
|
// ---|--> | (line enters the clip region from the left)
|
|
398
|
-
if (b >
|
|
399
|
-
t = intersect(slice, ax, ay, bx, by,
|
|
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 >
|
|
532
|
+
else if (a > end) {
|
|
405
533
|
// | <--|--- (line enters the clip region from the right)
|
|
406
|
-
if (b <
|
|
407
|
-
t = intersect(slice, ax, ay, bx, by,
|
|
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 <
|
|
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,
|
|
545
|
+
t = intersect(slice, ax, ay, bx, by, start);
|
|
418
546
|
exited = true;
|
|
419
547
|
}
|
|
420
|
-
if (b >
|
|
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,
|
|
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 ===
|
|
440
|
-
if (a >=
|
|
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,
|
|
587
|
+
function clipLines(geom, newGeom, start, end, axis, isPolygon) {
|
|
460
588
|
for (const line of geom) {
|
|
461
|
-
clipLine(line, newGeom,
|
|
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,
|
|
482
|
-
const right = clip(features, 1, 1 - buffer, 2 + buffer,
|
|
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,
|
|
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 =
|
|
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 =
|
|
750
|
+
let features = convertToInternal({ type: 'FeatureCollection', features: [geojsonFeature] }, options);
|
|
815
751
|
// wraps features (ie extreme west and extreme east)
|
|
816
752
|
features = wrap(features, options);
|
|
817
753
|
return features[0];
|
|
@@ -863,70 +799,771 @@ function diffToHashed(diff) {
|
|
|
863
799
|
return hashed;
|
|
864
800
|
}
|
|
865
801
|
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
extent:
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
|
|
877
|
-
|
|
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
|
-
*
|
|
820
|
+
* This class allow clustering of geojson points.
|
|
881
821
|
*/
|
|
882
|
-
class
|
|
822
|
+
class ClusterTileIndex {
|
|
883
823
|
options;
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
824
|
+
trees;
|
|
825
|
+
stride;
|
|
826
|
+
clusterProps;
|
|
827
|
+
points;
|
|
828
|
+
constructor(options) {
|
|
829
|
+
this.options = Object.assign(Object.create(defaultClusterOptions), options);
|
|
830
|
+
this.trees = new Array(this.options.maxZoom + 1);
|
|
831
|
+
this.stride = this.options.reduce ? 7 : 6;
|
|
832
|
+
this.clusterProps = [];
|
|
833
|
+
this.points = [];
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Loads GeoJSON point features and builds the internal clustering index.
|
|
837
|
+
* @param points - GeoJSON point features to cluster.
|
|
838
|
+
*/
|
|
839
|
+
load(points) {
|
|
840
|
+
const features = [];
|
|
841
|
+
// Convert GeoJSON point features to GeoJSONVT internal point features
|
|
842
|
+
for (const point of points) {
|
|
843
|
+
if (!point.geometry) {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const [lng, lat] = point.geometry.coordinates;
|
|
847
|
+
const [x, y] = [projectX(lng), projectY(lat)];
|
|
848
|
+
const feature = {
|
|
849
|
+
id: point.id,
|
|
850
|
+
type: 'Point',
|
|
851
|
+
geometry: [x, y],
|
|
852
|
+
tags: point.properties
|
|
853
|
+
};
|
|
854
|
+
features.push(feature);
|
|
855
|
+
}
|
|
856
|
+
this.createIndex(features);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* @internal
|
|
860
|
+
* Loads internal GeoJSONVT point features from a data source and builds the clustering index.
|
|
861
|
+
* @param features - {@link GeoJSONVTInternalFeature} data source features to filter and cluster.
|
|
862
|
+
*/
|
|
863
|
+
initialize(features) {
|
|
864
|
+
const points = [];
|
|
865
|
+
for (const feature of features) {
|
|
866
|
+
if (feature.type !== 'Point')
|
|
867
|
+
continue;
|
|
868
|
+
points.push(feature);
|
|
869
|
+
}
|
|
870
|
+
this.createIndex(points);
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* @internal
|
|
874
|
+
* Updates the cluster data by rebuilding.
|
|
875
|
+
* @param features
|
|
876
|
+
*/
|
|
877
|
+
updateIndex(features, _affected, options) {
|
|
878
|
+
this.options = Object.assign(Object.create(defaultClusterOptions), options.clusterOptions);
|
|
879
|
+
this.initialize(features);
|
|
880
|
+
}
|
|
881
|
+
createIndex(points) {
|
|
882
|
+
const { log, minZoom, maxZoom } = this.options;
|
|
883
|
+
if (log)
|
|
884
|
+
console.time('total time');
|
|
885
|
+
const timerId = `prepare ${points.length} points`;
|
|
886
|
+
if (log)
|
|
887
|
+
console.time(timerId);
|
|
888
|
+
this.points = points;
|
|
889
|
+
// generate a cluster object for each point and index input points into a KD-tree
|
|
890
|
+
const data = [];
|
|
891
|
+
for (let i = 0; i < points.length; i++) {
|
|
892
|
+
const p = points[i];
|
|
893
|
+
if (!p?.geometry)
|
|
894
|
+
continue;
|
|
895
|
+
let [x, y] = p.geometry;
|
|
896
|
+
x = Math.fround(x);
|
|
897
|
+
y = Math.fround(y);
|
|
898
|
+
// store internal point/cluster data in flat numeric arrays for performance
|
|
899
|
+
data.push(x, y, // projected point coordinates
|
|
900
|
+
Infinity, // the last zoom the point was processed at
|
|
901
|
+
i, // index of the source feature in the original input array
|
|
902
|
+
-1, // parent cluster id
|
|
903
|
+
1 // number of points in a cluster
|
|
904
|
+
);
|
|
905
|
+
if (this.options.reduce)
|
|
906
|
+
data.push(0); // noop
|
|
907
|
+
}
|
|
908
|
+
let tree = this.trees[maxZoom + 1] = this.createTree(data);
|
|
909
|
+
if (log)
|
|
910
|
+
console.timeEnd(timerId);
|
|
911
|
+
// cluster points on max zoom, then cluster the results on previous zoom, etc.;
|
|
912
|
+
// results in a cluster hierarchy across zoom levels
|
|
913
|
+
for (let z = maxZoom; z >= minZoom; z--) {
|
|
914
|
+
const now = Date.now();
|
|
915
|
+
// create a new set of clusters for the zoom and index them with a KD-tree
|
|
916
|
+
tree = this.trees[z] = this.createTree(this.cluster(tree, z));
|
|
917
|
+
if (log)
|
|
918
|
+
console.log('z%d: %d clusters in %dms', z, tree.numItems, Date.now() - now);
|
|
919
|
+
}
|
|
920
|
+
if (log)
|
|
921
|
+
console.timeEnd('total time');
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Returns clusters and/or points within a bounding box at a given zoom level.
|
|
925
|
+
* @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order.
|
|
926
|
+
* @param zoom - Zoom level to query.
|
|
927
|
+
*/
|
|
928
|
+
getClusters(bbox, zoom) {
|
|
929
|
+
const clusterInternal = this.getClustersInternal(bbox, zoom);
|
|
930
|
+
return clusterInternal.map((f) => featureToGeoJSON(f));
|
|
931
|
+
}
|
|
932
|
+
getClustersInternal(bbox, zoom) {
|
|
933
|
+
let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
|
|
934
|
+
const minLat = Math.max(-90, Math.min(90, bbox[1]));
|
|
935
|
+
let maxLng = bbox[2] === 180 ? 180 : ((bbox[2] + 180) % 360 + 360) % 360 - 180;
|
|
936
|
+
const maxLat = Math.max(-90, Math.min(90, bbox[3]));
|
|
937
|
+
if (bbox[2] - bbox[0] >= 360) {
|
|
938
|
+
minLng = -180;
|
|
939
|
+
maxLng = 180;
|
|
940
|
+
}
|
|
941
|
+
else if (minLng > maxLng) {
|
|
942
|
+
const easternHem = this.getClustersInternal([minLng, minLat, 180, maxLat], zoom);
|
|
943
|
+
const westernHem = this.getClustersInternal([-180, minLat, maxLng, maxLat], zoom);
|
|
944
|
+
return easternHem.concat(westernHem);
|
|
945
|
+
}
|
|
946
|
+
const tree = this.trees[this.limitZoom(zoom)];
|
|
947
|
+
const ids = tree.range(projectX(minLng), projectY(maxLat), projectX(maxLng), projectY(minLat));
|
|
948
|
+
const data = tree.flatData;
|
|
949
|
+
const clusters = [];
|
|
950
|
+
for (const id of ids) {
|
|
951
|
+
const k = this.stride * id;
|
|
952
|
+
clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
|
|
953
|
+
}
|
|
954
|
+
return clusters;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Returns the immediate children (clusters or points) of a cluster as GeoJSON.
|
|
958
|
+
* @param clusterId - The target cluster id.
|
|
959
|
+
*/
|
|
960
|
+
getChildren(clusterId) {
|
|
961
|
+
const originId = this.getOriginId(clusterId);
|
|
962
|
+
const originZoom = this.getOriginZoom(clusterId);
|
|
963
|
+
const clusterError = new Error('No cluster with the specified id: ' + clusterId);
|
|
964
|
+
const tree = this.trees[originZoom];
|
|
965
|
+
if (!tree)
|
|
966
|
+
throw clusterError;
|
|
967
|
+
const data = tree.flatData;
|
|
968
|
+
if (originId * this.stride >= data.length)
|
|
969
|
+
throw clusterError;
|
|
970
|
+
const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
|
|
971
|
+
const x = data[originId * this.stride];
|
|
972
|
+
const y = data[originId * this.stride + 1];
|
|
973
|
+
const ids = tree.within(x, y, r);
|
|
974
|
+
const children = [];
|
|
975
|
+
for (const id of ids) {
|
|
976
|
+
const k = id * this.stride;
|
|
977
|
+
if (data[k + OFFSET_PARENT] === clusterId) {
|
|
978
|
+
children.push(data[k + OFFSET_NUM] > 1 ? getClusterGeoJSON(data, k, this.clusterProps) : featureToGeoJSON(this.points[data[k + OFFSET_ID]]));
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (children.length === 0)
|
|
982
|
+
throw clusterError;
|
|
983
|
+
return children;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Returns leaf point features under a cluster, paginated by `limit` and `offset`.
|
|
987
|
+
* @param clusterId - The target cluster id.
|
|
988
|
+
* @param limit - Maximum number of points to return (defaults to `10`).
|
|
989
|
+
* @param offset - Number of points to skip before collecting results (defaults to `0`).
|
|
990
|
+
*/
|
|
991
|
+
getLeaves(clusterId, limit, offset) {
|
|
992
|
+
limit = limit || 10;
|
|
993
|
+
offset = offset || 0;
|
|
994
|
+
const leaves = [];
|
|
995
|
+
this.appendLeaves(leaves, clusterId, limit, offset, 0);
|
|
996
|
+
return leaves;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Generates a vector-tile-like representation of a single tile.
|
|
1000
|
+
* @param z - Tile zoom.
|
|
1001
|
+
* @param x - Tile x coordinate.
|
|
1002
|
+
* @param y - Tile y coordinate.
|
|
1003
|
+
*/
|
|
1004
|
+
getTile(z, x, y) {
|
|
1005
|
+
const tree = this.trees[this.limitZoom(z)];
|
|
1006
|
+
const z2 = Math.pow(2, z);
|
|
1007
|
+
const { extent, radius } = this.options;
|
|
1008
|
+
const p = radius / extent;
|
|
1009
|
+
const top = (y - p) / z2;
|
|
1010
|
+
const bottom = (y + 1 + p) / z2;
|
|
1011
|
+
const tile = {
|
|
1012
|
+
transformed: true,
|
|
1013
|
+
features: [],
|
|
1014
|
+
source: null,
|
|
1015
|
+
x: x,
|
|
1016
|
+
y: y,
|
|
1017
|
+
z: z
|
|
1018
|
+
};
|
|
1019
|
+
this.addTileFeatures(tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), tree.flatData, x, y, z2, tile);
|
|
1020
|
+
if (x === 0) {
|
|
1021
|
+
this.addTileFeatures(tree.range(1 - p / z2, top, 1, bottom), tree.flatData, z2, y, z2, tile);
|
|
1022
|
+
}
|
|
1023
|
+
if (x === z2 - 1) {
|
|
1024
|
+
this.addTileFeatures(tree.range(0, top, p / z2, bottom), tree.flatData, -1, y, z2, tile);
|
|
1025
|
+
}
|
|
1026
|
+
return tile;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Returns the zoom level at which a cluster expands into multiple children.
|
|
1030
|
+
* @param clusterId - The target cluster id.
|
|
1031
|
+
*/
|
|
1032
|
+
getClusterExpansionZoom(clusterId) {
|
|
1033
|
+
return this.getOriginZoom(clusterId);
|
|
1034
|
+
}
|
|
1035
|
+
appendLeaves(result, clusterId, limit, offset, skipped) {
|
|
1036
|
+
const children = this.getChildren(clusterId);
|
|
1037
|
+
for (const child of children) {
|
|
1038
|
+
const props = child.properties;
|
|
1039
|
+
if (props?.cluster) {
|
|
1040
|
+
if (skipped + props.point_count <= offset) {
|
|
1041
|
+
// skip the whole cluster
|
|
1042
|
+
skipped += props.point_count;
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
// enter the cluster
|
|
1046
|
+
skipped = this.appendLeaves(result, props.cluster_id, limit, offset, skipped);
|
|
1047
|
+
// exit the cluster
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
else if (skipped < offset) {
|
|
1051
|
+
// skip a single point
|
|
1052
|
+
skipped++;
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
// add a single point
|
|
1056
|
+
result.push(child);
|
|
1057
|
+
}
|
|
1058
|
+
if (result.length === limit)
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
return skipped;
|
|
1062
|
+
}
|
|
1063
|
+
createTree(data) {
|
|
1064
|
+
const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array);
|
|
1065
|
+
for (let i = 0; i < data.length; i += this.stride)
|
|
1066
|
+
tree.add(data[i], data[i + 1]);
|
|
1067
|
+
tree.finish();
|
|
1068
|
+
tree.flatData = data;
|
|
1069
|
+
tree.data = null; // clear original data to free memory as it isn't used later on.
|
|
1070
|
+
return tree;
|
|
1071
|
+
}
|
|
1072
|
+
addTileFeatures(ids, data, x, y, z2, tile) {
|
|
1073
|
+
for (const i of ids) {
|
|
1074
|
+
const k = i * this.stride;
|
|
1075
|
+
const isCluster = data[k + OFFSET_NUM] > 1;
|
|
1076
|
+
let tags;
|
|
1077
|
+
let px;
|
|
1078
|
+
let py;
|
|
1079
|
+
if (isCluster) {
|
|
1080
|
+
tags = getClusterProperties(data, k, this.clusterProps);
|
|
1081
|
+
px = data[k];
|
|
1082
|
+
py = data[k + 1];
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
const p = this.points[data[k + OFFSET_ID]];
|
|
1086
|
+
tags = p.tags;
|
|
1087
|
+
[px, py] = p.geometry;
|
|
1088
|
+
}
|
|
1089
|
+
const f = {
|
|
1090
|
+
type: 1,
|
|
1091
|
+
geometry: [[
|
|
1092
|
+
Math.round(this.options.extent * (px * z2 - x)),
|
|
1093
|
+
Math.round(this.options.extent * (py * z2 - y))
|
|
1094
|
+
]],
|
|
1095
|
+
tags
|
|
1096
|
+
};
|
|
1097
|
+
// assign id
|
|
1098
|
+
let id;
|
|
1099
|
+
if (isCluster || this.options.generateId) {
|
|
1100
|
+
// optionally generate id for points
|
|
1101
|
+
id = data[k + OFFSET_ID];
|
|
1102
|
+
}
|
|
1103
|
+
else {
|
|
1104
|
+
// keep id if already assigned
|
|
1105
|
+
id = this.points[data[k + OFFSET_ID]].id;
|
|
1106
|
+
}
|
|
1107
|
+
if (id !== undefined)
|
|
1108
|
+
f.id = id;
|
|
1109
|
+
tile.features.push(f);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
limitZoom(z) {
|
|
1113
|
+
return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
|
|
1114
|
+
}
|
|
1115
|
+
cluster(tree, zoom) {
|
|
1116
|
+
const { radius, extent, reduce, minPoints } = this.options;
|
|
1117
|
+
const r = radius / (extent * Math.pow(2, zoom));
|
|
1118
|
+
const data = tree.flatData;
|
|
1119
|
+
const nextData = [];
|
|
1120
|
+
const stride = this.stride;
|
|
1121
|
+
// loop through each point
|
|
1122
|
+
for (let i = 0; i < data.length; i += stride) {
|
|
1123
|
+
// if we've already visited the point at this zoom level, skip it
|
|
1124
|
+
if (data[i + OFFSET_ZOOM] <= zoom)
|
|
1125
|
+
continue;
|
|
1126
|
+
data[i + OFFSET_ZOOM] = zoom;
|
|
1127
|
+
// find all nearby points
|
|
1128
|
+
const x = data[i];
|
|
1129
|
+
const y = data[i + 1];
|
|
1130
|
+
const neighborIds = tree.within(data[i], data[i + 1], r);
|
|
1131
|
+
const numPointsOrigin = data[i + OFFSET_NUM];
|
|
1132
|
+
let numPoints = numPointsOrigin;
|
|
1133
|
+
// count the number of points in a potential cluster
|
|
1134
|
+
for (const neighborId of neighborIds) {
|
|
1135
|
+
const k = neighborId * stride;
|
|
1136
|
+
// filter out neighbors that are already processed
|
|
1137
|
+
if (data[k + OFFSET_ZOOM] > zoom)
|
|
1138
|
+
numPoints += data[k + OFFSET_NUM];
|
|
1139
|
+
}
|
|
1140
|
+
// if there were neighbors to merge, and there are enough points to form a cluster
|
|
1141
|
+
if (numPoints > numPointsOrigin && numPoints >= minPoints) {
|
|
1142
|
+
let wx = x * numPointsOrigin;
|
|
1143
|
+
let wy = y * numPointsOrigin;
|
|
1144
|
+
let clusterProperties;
|
|
1145
|
+
let clusterPropIndex = -1;
|
|
1146
|
+
// encode both zoom and point index on which the cluster originated -- offset by total length of features
|
|
1147
|
+
const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length;
|
|
1148
|
+
for (const neighborId of neighborIds) {
|
|
1149
|
+
const k = neighborId * stride;
|
|
1150
|
+
if (data[k + OFFSET_ZOOM] <= zoom)
|
|
1151
|
+
continue;
|
|
1152
|
+
data[k + OFFSET_ZOOM] = zoom; // save the zoom (so it doesn't get processed twice)
|
|
1153
|
+
const numPoints2 = data[k + OFFSET_NUM];
|
|
1154
|
+
wx += data[k] * numPoints2; // accumulate coordinates for calculating weighted center
|
|
1155
|
+
wy += data[k + 1] * numPoints2;
|
|
1156
|
+
data[k + OFFSET_PARENT] = id;
|
|
1157
|
+
if (reduce) {
|
|
1158
|
+
if (!clusterProperties) {
|
|
1159
|
+
clusterProperties = this.map(data, i, true);
|
|
1160
|
+
clusterPropIndex = this.clusterProps.length;
|
|
1161
|
+
this.clusterProps.push(clusterProperties);
|
|
1162
|
+
}
|
|
1163
|
+
reduce(clusterProperties, this.map(data, k));
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
data[i + OFFSET_PARENT] = id;
|
|
1167
|
+
nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints);
|
|
1168
|
+
if (reduce)
|
|
1169
|
+
nextData.push(clusterPropIndex);
|
|
1170
|
+
}
|
|
1171
|
+
else { // left points as unclustered
|
|
1172
|
+
for (let j = 0; j < stride; j++)
|
|
1173
|
+
nextData.push(data[i + j]);
|
|
1174
|
+
if (numPoints > 1) {
|
|
1175
|
+
for (const neighborId of neighborIds) {
|
|
1176
|
+
const k = neighborId * stride;
|
|
1177
|
+
if (data[k + OFFSET_ZOOM] <= zoom)
|
|
1178
|
+
continue;
|
|
1179
|
+
data[k + OFFSET_ZOOM] = zoom;
|
|
1180
|
+
for (let j = 0; j < stride; j++)
|
|
1181
|
+
nextData.push(data[k + j]);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return nextData;
|
|
1187
|
+
}
|
|
1188
|
+
// get index of the point from which the cluster originated
|
|
1189
|
+
getOriginId(clusterId) {
|
|
1190
|
+
return (clusterId - this.points.length) >> 5;
|
|
1191
|
+
}
|
|
1192
|
+
// get zoom of the point from which the cluster originated
|
|
1193
|
+
getOriginZoom(clusterId) {
|
|
1194
|
+
return (clusterId - this.points.length) % 32;
|
|
1195
|
+
}
|
|
1196
|
+
map(data, i, clone) {
|
|
1197
|
+
if (data[i + OFFSET_NUM] > 1) {
|
|
1198
|
+
const props = this.clusterProps[data[i + OFFSET_PROP]];
|
|
1199
|
+
return clone ? Object.assign({}, props) : props;
|
|
1200
|
+
}
|
|
1201
|
+
const original = this.points[data[i + OFFSET_ID]].tags;
|
|
1202
|
+
const result = this.options.map(original);
|
|
1203
|
+
return clone && result === original ? Object.assign({}, result) : result;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
function getClusterFeature(data, i, clusterProps) {
|
|
1207
|
+
return {
|
|
1208
|
+
id: data[i + OFFSET_ID],
|
|
1209
|
+
type: 'Point',
|
|
1210
|
+
tags: getClusterProperties(data, i, clusterProps),
|
|
1211
|
+
geometry: [data[i], data[i + 1]]
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
function getClusterGeoJSON(data, i, clusterProps) {
|
|
1215
|
+
return {
|
|
1216
|
+
type: 'Feature',
|
|
1217
|
+
id: data[i + OFFSET_ID],
|
|
1218
|
+
properties: getClusterProperties(data, i, clusterProps),
|
|
1219
|
+
geometry: {
|
|
1220
|
+
type: 'Point',
|
|
1221
|
+
coordinates: [unprojectX(data[i]), unprojectY(data[i + 1])]
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function getClusterProperties(data, i, clusterProps) {
|
|
1226
|
+
const count = data[i + OFFSET_NUM];
|
|
1227
|
+
const abbrev = count >= 10000 ? `${Math.round(count / 1000)}k` :
|
|
1228
|
+
count >= 1000 ? `${Math.round(count / 100) / 10}k` : count;
|
|
1229
|
+
const propIndex = data[i + OFFSET_PROP];
|
|
1230
|
+
const properties = propIndex === -1 ? {} : Object.assign({}, clusterProps[propIndex]);
|
|
1231
|
+
return Object.assign(properties, {
|
|
1232
|
+
cluster: true,
|
|
1233
|
+
cluster_id: data[i + OFFSET_ID],
|
|
1234
|
+
point_count: count,
|
|
1235
|
+
point_count_abbreviated: abbrev
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Creates a tile object from the given features
|
|
1241
|
+
* @param features - the features to include in the tile
|
|
1242
|
+
* @param z
|
|
1243
|
+
* @param tx
|
|
1244
|
+
* @param ty
|
|
1245
|
+
* @param options - the options object
|
|
1246
|
+
* @returns the created tile
|
|
1247
|
+
*/
|
|
1248
|
+
function createTile(features, z, tx, ty, options) {
|
|
1249
|
+
const tolerance = z === options.maxZoom ? 0 : options.tolerance / ((1 << z) * options.extent);
|
|
1250
|
+
const tile = {
|
|
1251
|
+
transformed: false,
|
|
1252
|
+
features: [],
|
|
1253
|
+
source: null,
|
|
1254
|
+
x: tx,
|
|
1255
|
+
y: ty,
|
|
1256
|
+
z: z,
|
|
1257
|
+
minX: 2,
|
|
1258
|
+
minY: 1,
|
|
1259
|
+
maxX: -1,
|
|
1260
|
+
maxY: 0,
|
|
1261
|
+
numPoints: 0,
|
|
1262
|
+
numSimplified: 0,
|
|
1263
|
+
numFeatures: features.length
|
|
1264
|
+
};
|
|
1265
|
+
for (const feature of features) {
|
|
1266
|
+
addFeature(tile, feature, tolerance, options);
|
|
1267
|
+
}
|
|
1268
|
+
return tile;
|
|
1269
|
+
}
|
|
1270
|
+
function addFeature(tile, feature, tolerance, options) {
|
|
1271
|
+
tile.minX = Math.min(tile.minX, feature.minX);
|
|
1272
|
+
tile.minY = Math.min(tile.minY, feature.minY);
|
|
1273
|
+
tile.maxX = Math.max(tile.maxX, feature.maxX);
|
|
1274
|
+
tile.maxY = Math.max(tile.maxY, feature.maxY);
|
|
1275
|
+
switch (feature.type) {
|
|
1276
|
+
case 'Point':
|
|
1277
|
+
case 'MultiPoint':
|
|
1278
|
+
addPointsTileFeature(tile, feature);
|
|
1279
|
+
return;
|
|
1280
|
+
case 'LineString':
|
|
1281
|
+
addLineTileFeautre(tile, feature, tolerance, options);
|
|
1282
|
+
return;
|
|
1283
|
+
case 'MultiLineString':
|
|
1284
|
+
case 'Polygon':
|
|
1285
|
+
addLinesTileFeature(tile, feature, tolerance);
|
|
1286
|
+
return;
|
|
1287
|
+
case 'MultiPolygon':
|
|
1288
|
+
addMultiPolygonTileFeature(tile, feature, tolerance);
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function addPointsTileFeature(tile, feature) {
|
|
1293
|
+
const geometry = [];
|
|
1294
|
+
for (let i = 0; i < feature.geometry.length; i += 3) {
|
|
1295
|
+
geometry.push(feature.geometry[i], feature.geometry[i + 1]);
|
|
1296
|
+
tile.numPoints++;
|
|
1297
|
+
tile.numSimplified++;
|
|
1298
|
+
}
|
|
1299
|
+
if (!geometry.length)
|
|
1300
|
+
return;
|
|
1301
|
+
const tileFeature = {
|
|
1302
|
+
type: 1,
|
|
1303
|
+
tags: feature.tags || null,
|
|
1304
|
+
geometry: geometry
|
|
1305
|
+
};
|
|
1306
|
+
if (feature.id !== null) {
|
|
1307
|
+
tileFeature.id = feature.id;
|
|
1308
|
+
}
|
|
1309
|
+
tile.features.push(tileFeature);
|
|
1310
|
+
}
|
|
1311
|
+
function addLineTileFeautre(tile, feature, tolerance, options) {
|
|
1312
|
+
const geometry = [];
|
|
1313
|
+
addLine(geometry, feature.geometry, tile, tolerance, false, false);
|
|
1314
|
+
if (!geometry.length)
|
|
1315
|
+
return;
|
|
1316
|
+
let tags = feature.tags || null;
|
|
1317
|
+
if (options.lineMetrics) {
|
|
1318
|
+
tags = {};
|
|
1319
|
+
for (const key in feature.tags)
|
|
1320
|
+
tags[key] = feature.tags[key];
|
|
1321
|
+
tags['geojsonvt_clip_start'] = feature.geometry.start / feature.geometry.size;
|
|
1322
|
+
tags['geojsonvt_clip_end'] = feature.geometry.end / feature.geometry.size;
|
|
1323
|
+
}
|
|
1324
|
+
const tileFeature = {
|
|
1325
|
+
type: 2,
|
|
1326
|
+
tags: tags,
|
|
1327
|
+
geometry: geometry
|
|
1328
|
+
};
|
|
1329
|
+
if (feature.id !== null) {
|
|
1330
|
+
tileFeature.id = feature.id;
|
|
1331
|
+
}
|
|
1332
|
+
tile.features.push(tileFeature);
|
|
1333
|
+
}
|
|
1334
|
+
function addLinesTileFeature(tile, feature, tolerance) {
|
|
1335
|
+
const geometry = [];
|
|
1336
|
+
for (let i = 0; i < feature.geometry.length; i++) {
|
|
1337
|
+
addLine(geometry, feature.geometry[i], tile, tolerance, feature.type === 'Polygon', i === 0);
|
|
1338
|
+
}
|
|
1339
|
+
if (!geometry.length)
|
|
1340
|
+
return;
|
|
1341
|
+
const tileFeature = {
|
|
1342
|
+
type: feature.type === 'Polygon' ? 3 : 2,
|
|
1343
|
+
tags: feature.tags || null,
|
|
1344
|
+
geometry: geometry
|
|
1345
|
+
};
|
|
1346
|
+
if (feature.id !== null) {
|
|
1347
|
+
tileFeature.id = feature.id;
|
|
1348
|
+
}
|
|
1349
|
+
tile.features.push(tileFeature);
|
|
1350
|
+
}
|
|
1351
|
+
function addMultiPolygonTileFeature(tile, feature, tolerance) {
|
|
1352
|
+
const geometry = [];
|
|
1353
|
+
for (let k = 0; k < feature.geometry.length; k++) {
|
|
1354
|
+
const polygon = feature.geometry[k];
|
|
1355
|
+
for (let i = 0; i < polygon.length; i++) {
|
|
1356
|
+
addLine(geometry, polygon[i], tile, tolerance, true, i === 0);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (!geometry.length)
|
|
1360
|
+
return;
|
|
1361
|
+
const tileFeature = {
|
|
1362
|
+
type: 3,
|
|
1363
|
+
tags: feature.tags || null,
|
|
1364
|
+
geometry: geometry
|
|
1365
|
+
};
|
|
1366
|
+
if (feature.id !== null) {
|
|
1367
|
+
tileFeature.id = feature.id;
|
|
1368
|
+
}
|
|
1369
|
+
tile.features.push(tileFeature);
|
|
1370
|
+
}
|
|
1371
|
+
function addLine(result, geom, tile, tolerance, isPolygon, isOuter) {
|
|
1372
|
+
const sqTolerance = tolerance * tolerance;
|
|
1373
|
+
if (tolerance > 0 && (geom.size < (isPolygon ? sqTolerance : tolerance))) {
|
|
1374
|
+
tile.numPoints += geom.length / 3;
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
const ring = [];
|
|
1378
|
+
for (let i = 0; i < geom.length; i += 3) {
|
|
1379
|
+
if (tolerance === 0 || geom[i + 2] > sqTolerance) {
|
|
1380
|
+
tile.numSimplified++;
|
|
1381
|
+
ring.push(geom[i], geom[i + 1]);
|
|
1382
|
+
}
|
|
1383
|
+
tile.numPoints++;
|
|
1384
|
+
}
|
|
1385
|
+
if (isPolygon)
|
|
1386
|
+
rewind(ring, isOuter);
|
|
1387
|
+
result.push(ring);
|
|
1388
|
+
}
|
|
1389
|
+
function rewind(ring, clockwise) {
|
|
1390
|
+
let area = 0;
|
|
1391
|
+
for (let i = 0, len = ring.length, j = len - 2; i < len; j = i, i += 2) {
|
|
1392
|
+
area += (ring[i] - ring[j]) * (ring[i + 1] + ring[j + 1]);
|
|
1393
|
+
}
|
|
1394
|
+
if (area > 0 !== clockwise)
|
|
1395
|
+
return;
|
|
1396
|
+
for (let i = 0, len = ring.length; i < len / 2; i += 2) {
|
|
1397
|
+
const x = ring[i];
|
|
1398
|
+
const y = ring[i + 1];
|
|
1399
|
+
ring[i] = ring[len - 2 - i];
|
|
1400
|
+
ring[i + 1] = ring[len - 1 - i];
|
|
1401
|
+
ring[len - 2 - i] = x;
|
|
1402
|
+
ring[len - 1 - i] = y;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Transforms the coordinates of each feature in the given tile from
|
|
1408
|
+
* mercator-projected space into (extent x extent) tile space.
|
|
1409
|
+
* @param tile - the tile to transform, this gets modified in place
|
|
1410
|
+
* @param extent - the tile extent (usually 4096)
|
|
1411
|
+
* @returns the transformed tile
|
|
1412
|
+
*/
|
|
1413
|
+
function transformTile(tile, extent) {
|
|
1414
|
+
if (tile.transformed) {
|
|
1415
|
+
return tile;
|
|
1416
|
+
}
|
|
1417
|
+
const z2 = 1 << tile.z;
|
|
1418
|
+
const tx = tile.x;
|
|
1419
|
+
const ty = tile.y;
|
|
1420
|
+
for (const feature of tile.features) {
|
|
1421
|
+
if (feature.type === 1) {
|
|
1422
|
+
transformPointFeature(feature, extent, z2, tx, ty);
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
transformNonPointFeature(feature, extent, z2, tx, ty);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
tile.transformed = true;
|
|
1429
|
+
return tile;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Transforms a single point feature from mercator-projected space into (extent x extent) tile space.
|
|
1433
|
+
*/
|
|
1434
|
+
function transformPointFeature(feature, extent, z2, tx, ty) {
|
|
1435
|
+
const transformed = feature;
|
|
1436
|
+
const geometry = feature.geometry;
|
|
1437
|
+
const point = [];
|
|
1438
|
+
for (let i = 0; i < geometry.length; i += 2) {
|
|
1439
|
+
point.push(transformPoint(geometry[i], geometry[i + 1], extent, z2, tx, ty));
|
|
1440
|
+
}
|
|
1441
|
+
transformed.geometry = point;
|
|
1442
|
+
return transformed;
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Transforms a single non-point feature from mercator-projected space into (extent x extent) tile space.
|
|
1446
|
+
*/
|
|
1447
|
+
function transformNonPointFeature(feature, extent, z2, tx, ty) {
|
|
1448
|
+
const transformed = feature;
|
|
1449
|
+
const geometry = feature.geometry;
|
|
1450
|
+
const nonPoint = [];
|
|
1451
|
+
for (const geom of geometry) {
|
|
1452
|
+
const ring = [];
|
|
1453
|
+
for (let i = 0; i < geom.length; i += 2) {
|
|
1454
|
+
ring.push(transformPoint(geom[i], geom[i + 1], extent, z2, tx, ty));
|
|
1455
|
+
}
|
|
1456
|
+
nonPoint.push(ring);
|
|
1457
|
+
}
|
|
1458
|
+
transformed.geometry = nonPoint;
|
|
1459
|
+
return transformed;
|
|
1460
|
+
}
|
|
1461
|
+
function transformPoint(x, y, extent, z2, tx, ty) {
|
|
1462
|
+
return [
|
|
1463
|
+
Math.round(extent * (x * z2 - tx)),
|
|
1464
|
+
Math.round(extent * (y * z2 - ty))
|
|
1465
|
+
];
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
class TileIndex {
|
|
1469
|
+
options;
|
|
1470
|
+
tileCoords;
|
|
1471
|
+
/** @internal */
|
|
1472
|
+
tiles;
|
|
1473
|
+
/** @internal */
|
|
1474
|
+
stats = {};
|
|
1475
|
+
/** @internal */
|
|
890
1476
|
total = 0;
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
options = this.options = Object.assign({}, defaultOptions, options);
|
|
894
|
-
const debug = options.debug;
|
|
895
|
-
if (debug)
|
|
896
|
-
console.time('preprocess data');
|
|
897
|
-
if (options.maxZoom < 0 || options.maxZoom > 24)
|
|
898
|
-
throw new Error('maxZoom should be in the 0-24 range');
|
|
899
|
-
if (options.promoteId && options.generateId)
|
|
900
|
-
throw new Error('promoteId and generateId cannot be used together.');
|
|
901
|
-
// projects and adds simplification info
|
|
902
|
-
let features = convert(data, options);
|
|
903
|
-
// tiles and tileCoords are part of the public API
|
|
1477
|
+
constructor(options) {
|
|
1478
|
+
this.options = options;
|
|
904
1479
|
this.tiles = {};
|
|
905
1480
|
this.tileCoords = [];
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
this.stats = {};
|
|
911
|
-
this.total = 0;
|
|
912
|
-
}
|
|
913
|
-
// wraps features (ie extreme west and extreme east)
|
|
914
|
-
features = wrap(features, options);
|
|
1481
|
+
this.stats = {};
|
|
1482
|
+
this.total = 0;
|
|
1483
|
+
}
|
|
1484
|
+
initialize(features) {
|
|
915
1485
|
// start slicing from the top tile down
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
}
|
|
919
|
-
// for updateable indexes, store a copy of the original simplified features
|
|
920
|
-
if (options.updateable) {
|
|
921
|
-
this.source = features;
|
|
922
|
-
}
|
|
923
|
-
if (debug) {
|
|
1486
|
+
this.splitTile(features, 0, 0, 0);
|
|
1487
|
+
if (this.options.debug) {
|
|
924
1488
|
if (features.length)
|
|
925
1489
|
console.log('features: %d, points: %d', this.tiles[0].numFeatures, this.tiles[0].numPoints);
|
|
926
1490
|
console.timeEnd('generate tiles');
|
|
927
1491
|
console.log('tiles generated:', this.total, JSON.stringify(this.stats));
|
|
928
1492
|
}
|
|
929
1493
|
}
|
|
1494
|
+
/** {@inheritdoc} */
|
|
1495
|
+
updateIndex(source, affected, options) {
|
|
1496
|
+
if (options.debug > 1) {
|
|
1497
|
+
console.log('invalidating tiles');
|
|
1498
|
+
console.time('invalidating');
|
|
1499
|
+
}
|
|
1500
|
+
this.invalidateTiles(affected);
|
|
1501
|
+
if (options.debug > 1)
|
|
1502
|
+
console.timeEnd('invalidating');
|
|
1503
|
+
// re-generate root tile with updated feature set
|
|
1504
|
+
const [z, x, y] = [0, 0, 0];
|
|
1505
|
+
const rootTile = createTile(source, z, x, y, options);
|
|
1506
|
+
rootTile.source = source;
|
|
1507
|
+
// update tile index with new root tile - ready for getTile calls
|
|
1508
|
+
const id = toID(z, x, y);
|
|
1509
|
+
this.tiles[id] = rootTile;
|
|
1510
|
+
this.tileCoords.push({ z, x, y, id });
|
|
1511
|
+
if (options.debug) {
|
|
1512
|
+
const key = `z${z}`;
|
|
1513
|
+
this.stats[key] = (this.stats[key] || 0) + 1;
|
|
1514
|
+
this.total++;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
/** {@inheritdoc} */
|
|
1518
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1519
|
+
getClusterExpansionZoom(_clusterId) {
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
/** {@inheritdoc} */
|
|
1523
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1524
|
+
getChildren(_clusterId) {
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
/** {@inheritdoc} */
|
|
1528
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1529
|
+
getLeaves(_clusterId, _limit, _offset) {
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
/** {@inheritdoc} */
|
|
1533
|
+
getTile(z, x, y) {
|
|
1534
|
+
const { extent, debug } = this.options;
|
|
1535
|
+
const z2 = 1 << z;
|
|
1536
|
+
x = (x + z2) & (z2 - 1); // wrap tile x coordinate
|
|
1537
|
+
const id = toID(z, x, y);
|
|
1538
|
+
if (this.tiles[id]) {
|
|
1539
|
+
return transformTile(this.tiles[id], extent);
|
|
1540
|
+
}
|
|
1541
|
+
if (debug > 1)
|
|
1542
|
+
console.log('drilling down to z%d-%d-%d', z, x, y);
|
|
1543
|
+
let z0 = z;
|
|
1544
|
+
let x0 = x;
|
|
1545
|
+
let y0 = y;
|
|
1546
|
+
let parent;
|
|
1547
|
+
while (!parent && z0 > 0) {
|
|
1548
|
+
z0--;
|
|
1549
|
+
x0 = x0 >> 1;
|
|
1550
|
+
y0 = y0 >> 1;
|
|
1551
|
+
parent = this.tiles[toID(z0, x0, y0)];
|
|
1552
|
+
}
|
|
1553
|
+
if (!parent?.source)
|
|
1554
|
+
return null;
|
|
1555
|
+
// if we found a parent tile containing the original geometry, we can drill down from it
|
|
1556
|
+
if (debug > 1) {
|
|
1557
|
+
console.log('found parent tile z%d-%d-%d', z0, x0, y0);
|
|
1558
|
+
console.time('drilling down');
|
|
1559
|
+
}
|
|
1560
|
+
this.splitTile(parent.source, z0, x0, y0, z, x, y);
|
|
1561
|
+
if (debug > 1)
|
|
1562
|
+
console.timeEnd('drilling down');
|
|
1563
|
+
if (!this.tiles[id])
|
|
1564
|
+
return null;
|
|
1565
|
+
return transformTile(this.tiles[id], extent);
|
|
1566
|
+
}
|
|
930
1567
|
/**
|
|
931
1568
|
* splits features from a parent tile to sub-tiles.
|
|
932
1569
|
* z, x, and y are the coordinates of the parent tile
|
|
@@ -1005,15 +1642,15 @@ class GeoJSONVT {
|
|
|
1005
1642
|
let bl = null;
|
|
1006
1643
|
let tr = null;
|
|
1007
1644
|
let br = null;
|
|
1008
|
-
const left = clip(features, z2, x - k1, x + k3,
|
|
1009
|
-
const right = clip(features, z2, x + k2, x + k4,
|
|
1645
|
+
const left = clip(features, z2, x - k1, x + k3, AxisType.X, tile.minX, tile.maxX, options);
|
|
1646
|
+
const right = clip(features, z2, x + k2, x + k4, AxisType.X, tile.minX, tile.maxX, options);
|
|
1010
1647
|
if (left) {
|
|
1011
|
-
tl = clip(left, z2, y - k1, y + k3,
|
|
1012
|
-
bl = clip(left, z2, y + k2, y + k4,
|
|
1648
|
+
tl = clip(left, z2, y - k1, y + k3, AxisType.Y, tile.minY, tile.maxY, options);
|
|
1649
|
+
bl = clip(left, z2, y + k2, y + k4, AxisType.Y, tile.minY, tile.maxY, options);
|
|
1013
1650
|
}
|
|
1014
1651
|
if (right) {
|
|
1015
|
-
tr = clip(right, z2, y - k1, y + k3,
|
|
1016
|
-
br = clip(right, z2, y + k2, y + k4,
|
|
1652
|
+
tr = clip(right, z2, y - k1, y + k3, AxisType.Y, tile.minY, tile.maxY, options);
|
|
1653
|
+
br = clip(right, z2, y + k2, y + k4, AxisType.Y, tile.minY, tile.maxY, options);
|
|
1017
1654
|
}
|
|
1018
1655
|
if (debug > 1)
|
|
1019
1656
|
console.timeEnd('clipping');
|
|
@@ -1023,59 +1660,14 @@ class GeoJSONVT {
|
|
|
1023
1660
|
stack.push(br || [], z + 1, x * 2 + 1, y * 2 + 1);
|
|
1024
1661
|
}
|
|
1025
1662
|
}
|
|
1026
|
-
/**
|
|
1027
|
-
* Given z, x, and y tile coordinates, returns the corresponding tile with geometries in tile coordinates, much like MVT data is stored.
|
|
1028
|
-
* @param z - tile zoom level
|
|
1029
|
-
* @param x - tile x coordinate
|
|
1030
|
-
* @param y - tile y coordinate
|
|
1031
|
-
* @returns the transformed tile or null if not found
|
|
1032
|
-
*/
|
|
1033
|
-
getTile(z, x, y) {
|
|
1034
|
-
z = +z;
|
|
1035
|
-
x = +x;
|
|
1036
|
-
y = +y;
|
|
1037
|
-
const options = this.options;
|
|
1038
|
-
const { extent, debug } = options;
|
|
1039
|
-
if (z < 0 || z > 24)
|
|
1040
|
-
return null;
|
|
1041
|
-
const z2 = 1 << z;
|
|
1042
|
-
x = (x + z2) & (z2 - 1); // wrap tile x coordinate
|
|
1043
|
-
const id = toID(z, x, y);
|
|
1044
|
-
if (this.tiles[id]) {
|
|
1045
|
-
return transformTile(this.tiles[id], extent);
|
|
1046
|
-
}
|
|
1047
|
-
if (debug > 1)
|
|
1048
|
-
console.log('drilling down to z%d-%d-%d', z, x, y);
|
|
1049
|
-
let z0 = z;
|
|
1050
|
-
let x0 = x;
|
|
1051
|
-
let y0 = y;
|
|
1052
|
-
let parent;
|
|
1053
|
-
while (!parent && z0 > 0) {
|
|
1054
|
-
z0--;
|
|
1055
|
-
x0 = x0 >> 1;
|
|
1056
|
-
y0 = y0 >> 1;
|
|
1057
|
-
parent = this.tiles[toID(z0, x0, y0)];
|
|
1058
|
-
}
|
|
1059
|
-
if (!parent?.source)
|
|
1060
|
-
return null;
|
|
1061
|
-
// if we found a parent tile containing the original geometry, we can drill down from it
|
|
1062
|
-
if (debug > 1) {
|
|
1063
|
-
console.log('found parent tile z%d-%d-%d', z0, x0, y0);
|
|
1064
|
-
console.time('drilling down');
|
|
1065
|
-
}
|
|
1066
|
-
this.splitTile(parent.source, z0, x0, y0, z, x, y);
|
|
1067
|
-
if (debug > 1)
|
|
1068
|
-
console.timeEnd('drilling down');
|
|
1069
|
-
if (!this.tiles[id])
|
|
1070
|
-
return null;
|
|
1071
|
-
return transformTile(this.tiles[id], extent);
|
|
1072
|
-
}
|
|
1073
1663
|
/**
|
|
1074
1664
|
* Invalidates (removes) tiles affected by the provided features
|
|
1075
1665
|
* @internal
|
|
1076
1666
|
* @param features
|
|
1077
1667
|
*/
|
|
1078
1668
|
invalidateTiles(features) {
|
|
1669
|
+
if (!features.length)
|
|
1670
|
+
return;
|
|
1079
1671
|
const options = this.options;
|
|
1080
1672
|
const { debug } = options;
|
|
1081
1673
|
// calculate bounding box of all features for trivial reject
|
|
@@ -1134,51 +1726,236 @@ class GeoJSONVT {
|
|
|
1134
1726
|
this.tileCoords = this.tileCoords.filter(c => !removedLookup.has(c.id));
|
|
1135
1727
|
}
|
|
1136
1728
|
}
|
|
1729
|
+
}
|
|
1730
|
+
function toID(z, x, y) {
|
|
1731
|
+
return (((1 << z) * y + x) * 32) + z;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const defaultOptions = {
|
|
1735
|
+
maxZoom: 14,
|
|
1736
|
+
indexMaxZoom: 5,
|
|
1737
|
+
indexMaxPoints: 100000,
|
|
1738
|
+
tolerance: 3,
|
|
1739
|
+
extent: 4096,
|
|
1740
|
+
buffer: 64,
|
|
1741
|
+
lineMetrics: false,
|
|
1742
|
+
promoteId: null,
|
|
1743
|
+
generateId: false,
|
|
1744
|
+
updateable: false,
|
|
1745
|
+
cluster: false,
|
|
1746
|
+
clusterOptions: defaultClusterOptions,
|
|
1747
|
+
debug: 0
|
|
1748
|
+
};
|
|
1749
|
+
/**
|
|
1750
|
+
* Main class for creating and managing a vector tile index from GeoJSON data.
|
|
1751
|
+
*/
|
|
1752
|
+
class GeoJSONVT {
|
|
1753
|
+
/**
|
|
1754
|
+
* @internal
|
|
1755
|
+
* This is for the tests
|
|
1756
|
+
*/
|
|
1757
|
+
get tiles() {
|
|
1758
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1759
|
+
return this.tileIndex?.tiles ?? {};
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* @internal
|
|
1763
|
+
* This is for the tests
|
|
1764
|
+
*/
|
|
1765
|
+
get stats() {
|
|
1766
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1767
|
+
return this.tileIndex.stats;
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* @internal
|
|
1771
|
+
* This is for the tests
|
|
1772
|
+
*/
|
|
1773
|
+
get total() {
|
|
1774
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1775
|
+
return this.tileIndex.total;
|
|
1776
|
+
}
|
|
1777
|
+
options;
|
|
1778
|
+
source;
|
|
1779
|
+
tileIndex;
|
|
1780
|
+
constructor(data, options) {
|
|
1781
|
+
options = this.options = Object.assign({}, defaultOptions, options);
|
|
1782
|
+
const debug = options.debug;
|
|
1783
|
+
if (debug)
|
|
1784
|
+
console.time('preprocess data');
|
|
1785
|
+
if (options.maxZoom < 0 || options.maxZoom > 24)
|
|
1786
|
+
throw new Error('maxZoom should be in the 0-24 range');
|
|
1787
|
+
if (options.promoteId && options.generateId)
|
|
1788
|
+
throw new Error('promoteId and generateId cannot be used together.');
|
|
1789
|
+
// projects and adds simplification info
|
|
1790
|
+
let features = convertToInternal(data, options);
|
|
1791
|
+
if (debug) {
|
|
1792
|
+
console.timeEnd('preprocess data');
|
|
1793
|
+
console.log('index: maxZoom: %d, maxPoints: %d', options.indexMaxZoom, options.indexMaxPoints);
|
|
1794
|
+
console.time('generate tiles');
|
|
1795
|
+
}
|
|
1796
|
+
// wraps features (ie extreme west and extreme east)
|
|
1797
|
+
features = wrap(features, options);
|
|
1798
|
+
// for updateable indexes, store a copy of the original simplified features
|
|
1799
|
+
if (options.updateable) {
|
|
1800
|
+
this.source = features;
|
|
1801
|
+
}
|
|
1802
|
+
this.initializeIndex(features, options);
|
|
1803
|
+
}
|
|
1804
|
+
initializeIndex(features, options) {
|
|
1805
|
+
this.tileIndex = options.cluster ? new ClusterTileIndex(options.clusterOptions) : new TileIndex(options);
|
|
1806
|
+
if (!features.length)
|
|
1807
|
+
return;
|
|
1808
|
+
this.tileIndex.initialize(features);
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Given z, x, and y tile coordinates, returns the corresponding tile with geometries in tile coordinates, much like MVT data is stored.
|
|
1812
|
+
* @param z - tile zoom level
|
|
1813
|
+
* @param x - tile x coordinate
|
|
1814
|
+
* @param y - tile y coordinate
|
|
1815
|
+
* @returns the transformed tile or null if not found
|
|
1816
|
+
*/
|
|
1817
|
+
getTile(z, x, y) {
|
|
1818
|
+
z = +z;
|
|
1819
|
+
x = +x;
|
|
1820
|
+
y = +y;
|
|
1821
|
+
if (z < 0 || z > 24)
|
|
1822
|
+
return null;
|
|
1823
|
+
return this.tileIndex.getTile(z, x, y);
|
|
1824
|
+
}
|
|
1137
1825
|
/**
|
|
1138
|
-
* Updates the
|
|
1139
|
-
* invalidates tiles that are affected by the update for regeneration on next getTile call.
|
|
1826
|
+
* Updates the source data feature set using a {@link GeoJSONVTSourceDiff}
|
|
1140
1827
|
* @param diff - the source diff object
|
|
1141
1828
|
*/
|
|
1142
|
-
updateData(diff) {
|
|
1829
|
+
updateData(diff, filter) {
|
|
1143
1830
|
const options = this.options;
|
|
1144
|
-
const debug = options.debug;
|
|
1145
1831
|
if (!options.updateable)
|
|
1146
1832
|
throw new Error('to update tile geojson `updateable` option must be set to true');
|
|
1147
1833
|
// apply diff and collect affected features and updated source that will be used to invalidate tiles
|
|
1148
|
-
|
|
1834
|
+
let { affected, source } = applySourceDiff(this.source, diff, options);
|
|
1835
|
+
if (filter) {
|
|
1836
|
+
({ affected, source } = this.filterUpdate(source, affected, filter));
|
|
1837
|
+
}
|
|
1149
1838
|
// nothing has changed
|
|
1150
1839
|
if (!affected.length)
|
|
1151
1840
|
return;
|
|
1152
1841
|
// update source with new simplified feature set
|
|
1153
1842
|
this.source = source;
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1843
|
+
this.tileIndex.updateIndex(source, affected, options);
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Filter an update using a predicate function. Returns the affected and updated source features.
|
|
1847
|
+
*/
|
|
1848
|
+
filterUpdate(source, affected, predicate) {
|
|
1849
|
+
const removeIds = new Set();
|
|
1850
|
+
for (const feature of source) {
|
|
1851
|
+
if (feature.id == undefined)
|
|
1852
|
+
continue;
|
|
1853
|
+
if (predicate(featureToGeoJSON(feature)))
|
|
1854
|
+
continue;
|
|
1855
|
+
affected.push(feature);
|
|
1856
|
+
removeIds.add(feature.id);
|
|
1157
1857
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
this.
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1858
|
+
source = source.filter(feature => !removeIds.has(feature.id));
|
|
1859
|
+
return { affected, source };
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Returns source data as GeoJSON - only available when `updateable` option is set to true.
|
|
1863
|
+
*/
|
|
1864
|
+
getData() {
|
|
1865
|
+
if (!this.options.updateable)
|
|
1866
|
+
throw new Error('to retrieve data the `updateable` option must be set to true');
|
|
1867
|
+
return convertToGeoJSON(this.source);
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Update supercluster options and regenerate the index.
|
|
1871
|
+
* @param cluster - whether to enable clustering
|
|
1872
|
+
* @param clusterOptions - {@link SuperclusterOptions}
|
|
1873
|
+
*/
|
|
1874
|
+
updateClusterOptions(cluster, clusterOptions) {
|
|
1875
|
+
const wasCluster = this.options.cluster;
|
|
1876
|
+
this.options.cluster = cluster;
|
|
1877
|
+
this.options.clusterOptions = clusterOptions;
|
|
1878
|
+
if (wasCluster == cluster) {
|
|
1879
|
+
this.tileIndex.updateIndex(this.source, [], this.options);
|
|
1880
|
+
return;
|
|
1173
1881
|
}
|
|
1882
|
+
this.initializeIndex(this.source, this.options);
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Returns the zoom level at which a cluster expands into multiple children.
|
|
1886
|
+
* @param clusterId - The target cluster id.
|
|
1887
|
+
* @returns the expansion zoom or null in case of non-clustered source
|
|
1888
|
+
*/
|
|
1889
|
+
getClusterExpansionZoom(clusterId) {
|
|
1890
|
+
return this.tileIndex.getClusterExpansionZoom(clusterId);
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Returns the immediate children (clusters or points) of a cluster as GeoJSON.
|
|
1894
|
+
* @param clusterId - The target cluster id.
|
|
1895
|
+
* @returns the immediate children or null in case of non-clustered source
|
|
1896
|
+
*/
|
|
1897
|
+
getClusterChildren(clusterId) {
|
|
1898
|
+
return this.tileIndex.getChildren(clusterId);
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Returns leaf point features under a cluster, paginated by `limit` and `offset`.
|
|
1902
|
+
* @param clusterId - The target cluster id.
|
|
1903
|
+
* @param limit - Maximum number of points to return (defaults to `10`).
|
|
1904
|
+
* @param offset - Number of points to skip before collecting results (defaults to `0`).
|
|
1905
|
+
* @returns leaf point features under a cluster or null in case of non-clustered source
|
|
1906
|
+
*/
|
|
1907
|
+
getClusterLeaves(clusterId, limit, offset) {
|
|
1908
|
+
return this.tileIndex.getLeaves(clusterId, limit, offset);
|
|
1174
1909
|
}
|
|
1175
1910
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1911
|
+
|
|
1912
|
+
/**
|
|
1913
|
+
* Converts GeoJSON data directly to a single vector tile without building a tile index.
|
|
1914
|
+
*
|
|
1915
|
+
* Unlike the {@link GeoJSONVT} class which builds a hierarchical tile index for efficient
|
|
1916
|
+
* repeated tile access, this function generates a single tile on-demand. This is useful when:
|
|
1917
|
+
* - You only need one specific tile and don't need to query multiple tiles
|
|
1918
|
+
* - The source data is already spatially filtered to the tile's bounding box
|
|
1919
|
+
* - You want to avoid the overhead of building a full tile index
|
|
1920
|
+
*
|
|
1921
|
+
* @example
|
|
1922
|
+
* ```ts
|
|
1923
|
+
* import {geoJSONToTile} from '@maplibre/geojson-vt';
|
|
1924
|
+
*
|
|
1925
|
+
* const geojson = {
|
|
1926
|
+
* type: 'FeatureCollection',
|
|
1927
|
+
* features: [{
|
|
1928
|
+
* type: 'Feature',
|
|
1929
|
+
* geometry: { type: 'Point', coordinates: [-77.03, 38.90] },
|
|
1930
|
+
* properties: { name: 'Washington, D.C.' }
|
|
1931
|
+
* }]
|
|
1932
|
+
* };
|
|
1933
|
+
*
|
|
1934
|
+
* const tile = geoJSONToTile(geojson, 10, 292, 391, { extent: 4096 });
|
|
1935
|
+
* ```
|
|
1936
|
+
*
|
|
1937
|
+
* @param data - GeoJSON data (Feature, FeatureCollection, or Geometry)
|
|
1938
|
+
* @param z - Tile zoom level
|
|
1939
|
+
* @param x - Tile x coordinate
|
|
1940
|
+
* @param y - Tile y coordinate
|
|
1941
|
+
* @param options - Optional configuration for tile generation
|
|
1942
|
+
* @returns The generated tile with geometries in tile coordinates, or null if no features
|
|
1943
|
+
*/
|
|
1944
|
+
function geoJSONToTile(data, z, x, y, options = {}) {
|
|
1945
|
+
options = { ...defaultOptions, ...options };
|
|
1946
|
+
const { wrap: shouldWrap = false, clip: shouldClip = false } = options;
|
|
1947
|
+
let features = convertToInternal(data, options);
|
|
1948
|
+
if (shouldWrap) {
|
|
1949
|
+
features = wrap(features, options);
|
|
1950
|
+
}
|
|
1951
|
+
if (shouldClip || options.lineMetrics) {
|
|
1952
|
+
const pow2 = 1 << z;
|
|
1953
|
+
const buffer = options.buffer / options.extent;
|
|
1954
|
+
const left = clip(features, pow2, (x - buffer), (x + 1 + buffer), AxisType.X, -1, 2, options);
|
|
1955
|
+
features = clip(left || [], pow2, (y - buffer), (y + 1 + buffer), AxisType.Y, -1, 2, options);
|
|
1956
|
+
}
|
|
1957
|
+
return transformTile(createTile(features ?? [], z, x, y, options), options.extent);
|
|
1181
1958
|
}
|
|
1182
1959
|
|
|
1183
|
-
export {
|
|
1960
|
+
export { GeoJSONVT, ClusterTileIndex as Supercluster, geoJSONToTile };
|
|
1184
1961
|
//# sourceMappingURL=geojson-vt.mjs.map
|