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