@tangram.gl/basemap-layers 0.0.0-stage → 0.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 +34 -2
- package/THIRD-PARTY-NOTICES.md +205 -0
- package/dist/geojson-worker.js +2160 -0
- package/dist/geojson-worker.js.map +6 -0
- package/dist/index.cjs +11388 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.js +11369 -0
- package/dist/index.js.map +6 -0
- package/dist/map-style.cjs +445 -0
- package/dist/map-style.cjs.map +6 -0
- package/dist/map-style.js +431 -0
- package/dist/map-style.js.map +6 -0
- package/dist/types/arabic-shaping.d.ts +7 -0
- package/dist/types/atmosphere-layer.d.ts +10 -0
- package/dist/types/basemap-layer.d.ts +150 -0
- package/dist/types/bidi.d.ts +8 -0
- package/dist/types/circle-layer.d.ts +49 -0
- package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
- package/dist/types/fill-outline-extension.d.ts +22 -0
- package/dist/types/fill-pattern.d.ts +99 -0
- package/dist/types/fill-style-extensions.d.ts +100 -0
- package/dist/types/geojson-source.d.ts +29 -0
- package/dist/types/geojson-tiler.d.ts +48 -0
- package/dist/types/geojson-worker.d.ts +40 -0
- package/dist/types/globe-horizon-extension.d.ts +61 -0
- package/dist/types/globe-layers.d.ts +239 -0
- package/dist/types/globe-line-angle-extension.d.ts +37 -0
- package/dist/types/glyph-font-renderer.d.ts +37 -0
- package/dist/types/glyph-pbf.d.ts +37 -0
- package/dist/types/glyph-store.d.ts +103 -0
- package/dist/types/grid-index.d.ts +47 -0
- package/dist/types/hillshade-layer.d.ts +154 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/is-globe-viewport.d.ts +2 -0
- package/dist/types/label-font-renderer.d.ts +18 -0
- package/dist/types/label-placement.d.ts +247 -0
- package/dist/types/label-text-layer.d.ts +231 -0
- package/dist/types/line-blur-extension.d.ts +73 -0
- package/dist/types/line-glyphs.d.ts +60 -0
- package/dist/types/line-placement.d.ts +93 -0
- package/dist/types/map-style-loader.d.ts +48 -0
- package/dist/types/map-style-schema.d.ts +72 -0
- package/dist/types/map-style.d.ts +6 -0
- package/dist/types/mapbox-style.d.ts +40 -0
- package/dist/types/mvt-label-layer.d.ts +923 -0
- package/dist/types/polylabel.d.ts +17 -0
- package/dist/types/raster-color-extension.d.ts +146 -0
- package/dist/types/shared-texture.d.ts +34 -0
- package/dist/types/sort-key.d.ts +10 -0
- package/dist/types/sprite.d.ts +82 -0
- package/dist/types/style-accessor.d.ts +45 -0
- package/dist/types/style-expression.d.ts +83 -0
- package/dist/types/style-resolver.d.ts +94 -0
- package/dist/types/text-font.d.ts +36 -0
- package/dist/types/text-halo-blur-extension.d.ts +55 -0
- package/dist/types/text-layout.d.ts +71 -0
- package/dist/types/util.d.ts +22 -0
- package/dist/types/zoom-paint-extension.d.ts +72 -0
- package/package.json +80 -3
- package/src/arabic-shaping.ts +366 -0
- package/src/atmosphere-layer.ts +19 -0
- package/src/basemap-layer.ts +533 -0
- package/src/bidi.ts +199 -0
- package/src/circle-layer.ts +191 -0
- package/src/fill-extrusion-lighting-extension.ts +206 -0
- package/src/fill-outline-extension.ts +70 -0
- package/src/fill-pattern.ts +400 -0
- package/src/fill-style-extensions.ts +177 -0
- package/src/geojson-source.ts +318 -0
- package/src/geojson-tiler.ts +185 -0
- package/src/geojson-worker.ts +98 -0
- package/src/globe-horizon-extension.ts +64 -0
- package/src/globe-layers.ts +3487 -0
- package/src/globe-line-angle-extension.ts +131 -0
- package/src/glyph-font-renderer.ts +173 -0
- package/src/glyph-pbf.ts +164 -0
- package/src/glyph-store.ts +326 -0
- package/src/grid-index.ts +177 -0
- package/src/hillshade-layer.ts +775 -0
- package/src/index.ts +18 -0
- package/src/is-globe-viewport.ts +24 -0
- package/src/label-font-renderer.ts +159 -0
- package/src/label-placement.ts +727 -0
- package/src/label-text-layer.ts +913 -0
- package/src/line-blur-extension.ts +237 -0
- package/src/line-glyphs.ts +364 -0
- package/src/line-placement.ts +446 -0
- package/src/map-style-loader.ts +56 -0
- package/src/map-style-schema.ts +83 -0
- package/src/map-style.ts +23 -0
- package/src/mapbox-style.ts +168 -0
- package/src/mvt-label-layer.ts +3744 -0
- package/src/polylabel.ts +312 -0
- package/src/raster-color-extension.ts +245 -0
- package/src/shared-texture.ts +153 -0
- package/src/sort-key.ts +63 -0
- package/src/sprite.ts +244 -0
- package/src/style-accessor.ts +95 -0
- package/src/style-expression.ts +327 -0
- package/src/style-resolver.ts +301 -0
- package/src/text-font.ts +150 -0
- package/src/text-halo-blur-extension.ts +115 -0
- package/src/text-layout.ts +441 -0
- package/src/util.ts +47 -0
- package/src/zoom-paint-extension.ts +207 -0
|
@@ -0,0 +1,2160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// ../../node_modules/@maplibre/geojson-vt/dist/geojson-vt.mjs
|
|
4
|
+
function simplify(coords, first, last, sqTolerance) {
|
|
5
|
+
const stack = [first, last];
|
|
6
|
+
while (stack.length) {
|
|
7
|
+
last = stack.pop();
|
|
8
|
+
first = stack.pop();
|
|
9
|
+
simplifySegment(coords, first, last, sqTolerance, stack);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function simplifySegment(coords, first, last, sqTolerance, stack) {
|
|
13
|
+
let maxSqDist = sqTolerance;
|
|
14
|
+
const mid = first + (last - first >> 1);
|
|
15
|
+
let minPosToMid = last - first;
|
|
16
|
+
let index;
|
|
17
|
+
const ax = coords[first];
|
|
18
|
+
const ay = coords[first + 1];
|
|
19
|
+
const bx = coords[last];
|
|
20
|
+
const by = coords[last + 1];
|
|
21
|
+
for (let i = first + 3; i < last; i += 3) {
|
|
22
|
+
const d = getSqSegDist(coords[i], coords[i + 1], ax, ay, bx, by);
|
|
23
|
+
if (d > maxSqDist) {
|
|
24
|
+
index = i;
|
|
25
|
+
maxSqDist = d;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (d === maxSqDist) {
|
|
29
|
+
const posToMid = Math.abs(i - mid);
|
|
30
|
+
if (posToMid < minPosToMid) {
|
|
31
|
+
index = i;
|
|
32
|
+
minPosToMid = posToMid;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (maxSqDist > sqTolerance) {
|
|
37
|
+
coords[index + 2] = maxSqDist;
|
|
38
|
+
if (index - first > 3) stack.push(first, index);
|
|
39
|
+
if (last - index > 3) stack.push(index, last);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function getSqSegDist(px, py, x, y, bx, by) {
|
|
43
|
+
let dx = bx - x;
|
|
44
|
+
let dy = by - y;
|
|
45
|
+
if (dx !== 0 || dy !== 0) {
|
|
46
|
+
const t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);
|
|
47
|
+
if (t > 1) {
|
|
48
|
+
x = bx;
|
|
49
|
+
y = by;
|
|
50
|
+
} else if (t > 0) {
|
|
51
|
+
x += dx * t;
|
|
52
|
+
y += dy * t;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
dx = px - x;
|
|
56
|
+
dy = py - y;
|
|
57
|
+
return dx * dx + dy * dy;
|
|
58
|
+
}
|
|
59
|
+
function createFeature(id, type, geom, tags) {
|
|
60
|
+
const data = {
|
|
61
|
+
type,
|
|
62
|
+
geom
|
|
63
|
+
};
|
|
64
|
+
const feature = {
|
|
65
|
+
id: id == null ? null : id,
|
|
66
|
+
type: data.type,
|
|
67
|
+
geometry: data.geom,
|
|
68
|
+
tags,
|
|
69
|
+
minX: Infinity,
|
|
70
|
+
minY: Infinity,
|
|
71
|
+
maxX: -Infinity,
|
|
72
|
+
maxY: -Infinity
|
|
73
|
+
};
|
|
74
|
+
switch (data.type) {
|
|
75
|
+
case "Point":
|
|
76
|
+
case "MultiPoint":
|
|
77
|
+
calcLineBBox(feature, data.geom);
|
|
78
|
+
break;
|
|
79
|
+
case "LineString":
|
|
80
|
+
calcLineBBox(feature, data.geom.points);
|
|
81
|
+
break;
|
|
82
|
+
case "Polygon":
|
|
83
|
+
calcLineBBox(feature, data.geom[0].points);
|
|
84
|
+
break;
|
|
85
|
+
case "MultiLineString":
|
|
86
|
+
for (const line of data.geom) calcLineBBox(feature, line.points);
|
|
87
|
+
break;
|
|
88
|
+
case "MultiPolygon":
|
|
89
|
+
for (const polygon of data.geom) calcLineBBox(feature, polygon[0].points);
|
|
90
|
+
}
|
|
91
|
+
return feature;
|
|
92
|
+
}
|
|
93
|
+
function optimizeLineMemory(line) {
|
|
94
|
+
const lineImmutable = line;
|
|
95
|
+
if (line.points.length > 64) lineImmutable.points = new Float64Array(line.points);
|
|
96
|
+
}
|
|
97
|
+
function calcLineBBox(feature, geom) {
|
|
98
|
+
for (let i = 0; i < geom.length; i += 3) {
|
|
99
|
+
feature.minX = Math.min(feature.minX, geom[i]);
|
|
100
|
+
feature.minY = Math.min(feature.minY, geom[i + 1]);
|
|
101
|
+
feature.maxX = Math.max(feature.maxX, geom[i]);
|
|
102
|
+
feature.maxY = Math.max(feature.maxY, geom[i + 1]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
var MercatorWorldCoordinateHelper = class {
|
|
106
|
+
projectX(lng) {
|
|
107
|
+
return lng / 360 + 0.5;
|
|
108
|
+
}
|
|
109
|
+
projectY(lat) {
|
|
110
|
+
const sin = Math.sin(lat * Math.PI / 180);
|
|
111
|
+
const y = 0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
|
|
112
|
+
return y < 0 ? 0 : y > 1 ? 1 : y;
|
|
113
|
+
}
|
|
114
|
+
unprojectX(x) {
|
|
115
|
+
return (x - 0.5) * 360;
|
|
116
|
+
}
|
|
117
|
+
unprojectY(y) {
|
|
118
|
+
const y2 = (180 - y * 360) * Math.PI / 180;
|
|
119
|
+
return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var IdentityWorldCoordinateHelper = class {
|
|
123
|
+
projectX(x) {
|
|
124
|
+
return x;
|
|
125
|
+
}
|
|
126
|
+
projectY(y) {
|
|
127
|
+
return y;
|
|
128
|
+
}
|
|
129
|
+
unprojectX(x) {
|
|
130
|
+
return x;
|
|
131
|
+
}
|
|
132
|
+
unprojectY(y) {
|
|
133
|
+
return y;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
var mercatorWorldCoordinateHelper = new MercatorWorldCoordinateHelper();
|
|
137
|
+
var identityWorldCoordinateHelper = new IdentityWorldCoordinateHelper();
|
|
138
|
+
function getWorldCoordinateHelper(coordinates) {
|
|
139
|
+
return coordinates === "unitSquare" ? identityWorldCoordinateHelper : mercatorWorldCoordinateHelper;
|
|
140
|
+
}
|
|
141
|
+
var MAX_GEOMETRY_COLLECTION_DEPTH = 1024;
|
|
142
|
+
function convertToInternal(data, options, unlocated) {
|
|
143
|
+
const features = [];
|
|
144
|
+
switch (data.type) {
|
|
145
|
+
case "FeatureCollection":
|
|
146
|
+
for (let i = 0; i < data.features.length; i++) convertFeature(features, data.features[i], options, unlocated, i);
|
|
147
|
+
break;
|
|
148
|
+
case "Feature":
|
|
149
|
+
convertFeature(features, data, options, unlocated);
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
convertFeature(features, {
|
|
153
|
+
type: "Feature",
|
|
154
|
+
geometry: data,
|
|
155
|
+
properties: void 0
|
|
156
|
+
}, options, unlocated);
|
|
157
|
+
}
|
|
158
|
+
return features;
|
|
159
|
+
}
|
|
160
|
+
function convertFeature(features, geojson, options, unlocated, index) {
|
|
161
|
+
const count = features.length;
|
|
162
|
+
featureToInternal(features, geojson, options, index);
|
|
163
|
+
if (!unlocated) return;
|
|
164
|
+
const id = getFeatureId(geojson, options, index);
|
|
165
|
+
if (id == null) return;
|
|
166
|
+
if (features.length > count) unlocated.delete(id);
|
|
167
|
+
else unlocated.set(id, {
|
|
168
|
+
...geojson,
|
|
169
|
+
id
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function featureToInternal(features, geojson, options, index, depth = 0) {
|
|
173
|
+
if (!geojson.geometry) return;
|
|
174
|
+
if (depth > MAX_GEOMETRY_COLLECTION_DEPTH) throw new Error("GeometryCollection nesting exceeds supported depth: 1024");
|
|
175
|
+
if (geojson.geometry.type === "GeometryCollection") {
|
|
176
|
+
convertGeometryCollection(features, geojson, geojson.geometry, options, index, depth + 1);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!geojson.geometry.coordinates?.length) return;
|
|
180
|
+
const id = getFeatureId(geojson, options, index);
|
|
181
|
+
const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
|
|
182
|
+
const worldCoordinateHelper = getWorldCoordinateHelper(options.coordinates);
|
|
183
|
+
switch (geojson.geometry.type) {
|
|
184
|
+
case "Point":
|
|
185
|
+
convertPointFeature(features, id, geojson.geometry, worldCoordinateHelper, geojson.properties);
|
|
186
|
+
return;
|
|
187
|
+
case "MultiPoint":
|
|
188
|
+
convertMultiPointFeature(features, id, geojson.geometry, worldCoordinateHelper, geojson.properties);
|
|
189
|
+
return;
|
|
190
|
+
case "LineString":
|
|
191
|
+
convertLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
|
|
192
|
+
return;
|
|
193
|
+
case "MultiLineString":
|
|
194
|
+
convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, options, geojson.properties);
|
|
195
|
+
return;
|
|
196
|
+
case "Polygon":
|
|
197
|
+
convertPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
|
|
198
|
+
return;
|
|
199
|
+
case "MultiPolygon":
|
|
200
|
+
convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
|
|
201
|
+
return;
|
|
202
|
+
default:
|
|
203
|
+
throw new Error("Input data is not a valid GeoJSON object.");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function getFeatureId(geojson, options, index) {
|
|
207
|
+
if (options.promoteId) return geojson.properties?.[options.promoteId];
|
|
208
|
+
if (options.generateId) return index || 0;
|
|
209
|
+
return geojson.id;
|
|
210
|
+
}
|
|
211
|
+
function convertGeometryCollection(features, geojson, geometry, options, index, depth = 0) {
|
|
212
|
+
for (const geom of geometry.geometries) featureToInternal(features, {
|
|
213
|
+
id: geojson.id,
|
|
214
|
+
type: "Feature",
|
|
215
|
+
geometry: geom,
|
|
216
|
+
properties: geojson.properties
|
|
217
|
+
}, options, index, depth);
|
|
218
|
+
}
|
|
219
|
+
function convertPointFeature(features, id, geom, worldCoordinateHelper, properties) {
|
|
220
|
+
const out = [];
|
|
221
|
+
out.push(worldCoordinateHelper.projectX(geom.coordinates[0]), worldCoordinateHelper.projectY(geom.coordinates[1]), 0);
|
|
222
|
+
features.push(createFeature(id, "Point", out, properties));
|
|
223
|
+
}
|
|
224
|
+
function convertMultiPointFeature(features, id, geom, worldCoordinateHelper, properties) {
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const coords of geom.coordinates) out.push(worldCoordinateHelper.projectX(coords[0]), worldCoordinateHelper.projectY(coords[1]), 0);
|
|
227
|
+
features.push(createFeature(id, "MultiPoint", out, properties));
|
|
228
|
+
}
|
|
229
|
+
function convertLineStringFeature(features, id, geom, tolerance, worldCoordinateHelper, properties) {
|
|
230
|
+
const out = { points: [] };
|
|
231
|
+
convertLine(geom.coordinates, out, tolerance, false, worldCoordinateHelper);
|
|
232
|
+
features.push(createFeature(id, "LineString", out, properties));
|
|
233
|
+
}
|
|
234
|
+
function convertMultiLineStringFeature(features, id, geom, tolerance, worldCoordinateHelper, options, properties) {
|
|
235
|
+
if (options.lineMetrics) for (const line of geom.coordinates) {
|
|
236
|
+
const out = { points: [] };
|
|
237
|
+
convertLine(line, out, tolerance, false, worldCoordinateHelper);
|
|
238
|
+
features.push(createFeature(id, "LineString", out, properties));
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const out = [];
|
|
242
|
+
convertLines(geom.coordinates, out, tolerance, false, worldCoordinateHelper);
|
|
243
|
+
features.push(createFeature(id, "MultiLineString", out, properties));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function convertPolygonFeature(features, id, geom, tolerance, worldCoordinateHelper, properties) {
|
|
247
|
+
const out = [];
|
|
248
|
+
convertLines(geom.coordinates, out, tolerance, true, worldCoordinateHelper);
|
|
249
|
+
features.push(createFeature(id, "Polygon", out, properties));
|
|
250
|
+
}
|
|
251
|
+
function convertMultiPolygonFeature(features, id, geom, tolerance, worldCoordinateHelper, properties) {
|
|
252
|
+
const out = [];
|
|
253
|
+
for (const polygon of geom.coordinates) {
|
|
254
|
+
const polygonOut = [];
|
|
255
|
+
convertLines(polygon, polygonOut, tolerance, true, worldCoordinateHelper);
|
|
256
|
+
if (!polygonOut.length) continue;
|
|
257
|
+
out.push(polygonOut);
|
|
258
|
+
}
|
|
259
|
+
if (!out.length) return;
|
|
260
|
+
features.push(createFeature(id, "MultiPolygon", out, properties));
|
|
261
|
+
}
|
|
262
|
+
function convertLine(ring, out, tolerance, isPolygon, worldCoordinateHelper) {
|
|
263
|
+
let x0, y0;
|
|
264
|
+
let size = 0;
|
|
265
|
+
for (let j = 0; j < ring.length; j++) {
|
|
266
|
+
const x = worldCoordinateHelper.projectX(ring[j][0]);
|
|
267
|
+
const y = worldCoordinateHelper.projectY(ring[j][1]);
|
|
268
|
+
out.points.push(x, y, 0);
|
|
269
|
+
if (j > 0) {
|
|
270
|
+
if (isPolygon) size += (x0 * y - x * y0) / 2;
|
|
271
|
+
else size += Math.sqrt(Math.pow(x - x0, 2) + Math.pow(y - y0, 2));
|
|
272
|
+
}
|
|
273
|
+
x0 = x;
|
|
274
|
+
y0 = y;
|
|
275
|
+
}
|
|
276
|
+
const last = out.points.length - 3;
|
|
277
|
+
out.points[2] = 1;
|
|
278
|
+
if (tolerance > 0) simplify(out.points, 0, last, tolerance);
|
|
279
|
+
out.points[last + 2] = 1;
|
|
280
|
+
optimizeLineMemory(out);
|
|
281
|
+
out.size = Math.abs(size);
|
|
282
|
+
out.start = 0;
|
|
283
|
+
out.end = out.size;
|
|
284
|
+
}
|
|
285
|
+
function convertLines(rings, out, tolerance, isPolygon, worldCoordinateHelper) {
|
|
286
|
+
for (let i = 0; i < rings.length; i++) {
|
|
287
|
+
const geom = { points: [] };
|
|
288
|
+
convertLine(rings[i], geom, tolerance, isPolygon, worldCoordinateHelper);
|
|
289
|
+
out.push(geom);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function convertToGeoJSON(source, worldCoordinateHelper = mercatorWorldCoordinateHelper) {
|
|
293
|
+
return {
|
|
294
|
+
type: "FeatureCollection",
|
|
295
|
+
features: source.map((feature) => featureToGeoJSON(feature, worldCoordinateHelper))
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function featureToGeoJSON(feature, worldCoordinateHelper = mercatorWorldCoordinateHelper) {
|
|
299
|
+
const geojsonFeature = {
|
|
300
|
+
type: "Feature",
|
|
301
|
+
geometry: geometryToGeoJSON(feature, worldCoordinateHelper),
|
|
302
|
+
properties: feature.tags
|
|
303
|
+
};
|
|
304
|
+
if (feature.id != null) geojsonFeature.id = feature.id;
|
|
305
|
+
return geojsonFeature;
|
|
306
|
+
}
|
|
307
|
+
function geometryToGeoJSON(feature, worldCoordinateHelper) {
|
|
308
|
+
const { type, geometry } = feature;
|
|
309
|
+
switch (type) {
|
|
310
|
+
case "Point":
|
|
311
|
+
return {
|
|
312
|
+
type,
|
|
313
|
+
coordinates: unprojectPoint(geometry[0], geometry[1], worldCoordinateHelper)
|
|
314
|
+
};
|
|
315
|
+
case "MultiPoint":
|
|
316
|
+
return {
|
|
317
|
+
type,
|
|
318
|
+
coordinates: unprojectPoints(geometry, worldCoordinateHelper)
|
|
319
|
+
};
|
|
320
|
+
case "LineString":
|
|
321
|
+
return {
|
|
322
|
+
type,
|
|
323
|
+
coordinates: unprojectPoints(geometry.points, worldCoordinateHelper)
|
|
324
|
+
};
|
|
325
|
+
case "MultiLineString":
|
|
326
|
+
case "Polygon":
|
|
327
|
+
return {
|
|
328
|
+
type,
|
|
329
|
+
coordinates: geometry.map((ring) => unprojectPoints(ring.points, worldCoordinateHelper))
|
|
330
|
+
};
|
|
331
|
+
case "MultiPolygon":
|
|
332
|
+
return {
|
|
333
|
+
type,
|
|
334
|
+
coordinates: geometry.map((polygon) => polygon.map((ring) => unprojectPoints(ring.points, worldCoordinateHelper)))
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function unprojectPoints(coords, worldCoordinateHelper = mercatorWorldCoordinateHelper) {
|
|
339
|
+
const result = [];
|
|
340
|
+
for (let i = 0; i < coords.length; i += 3) result.push(unprojectPoint(coords[i], coords[i + 1], worldCoordinateHelper));
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
function unprojectPoint(x, y, worldCoordinateHelper) {
|
|
344
|
+
return [worldCoordinateHelper.unprojectX(x), worldCoordinateHelper.unprojectY(y)];
|
|
345
|
+
}
|
|
346
|
+
function clip(features, scale, start, end, axis, minAll, maxAll, options) {
|
|
347
|
+
start /= scale;
|
|
348
|
+
end /= scale;
|
|
349
|
+
if (minAll >= start && maxAll < end) return features;
|
|
350
|
+
if (maxAll < start || minAll >= end) return null;
|
|
351
|
+
const clipped = [];
|
|
352
|
+
for (const feature of features) {
|
|
353
|
+
const min = axis === 0 ? feature.minX : feature.minY;
|
|
354
|
+
const max = axis === 0 ? feature.maxX : feature.maxY;
|
|
355
|
+
if (min >= start && max < end) {
|
|
356
|
+
clipped.push(feature);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (max < start || min >= end) continue;
|
|
360
|
+
switch (feature.type) {
|
|
361
|
+
case "Point":
|
|
362
|
+
case "MultiPoint":
|
|
363
|
+
clipPointFeature(feature, clipped, start, end, axis);
|
|
364
|
+
continue;
|
|
365
|
+
case "LineString":
|
|
366
|
+
clipLineStringFeature(feature, clipped, start, end, axis, options);
|
|
367
|
+
continue;
|
|
368
|
+
case "MultiLineString":
|
|
369
|
+
clipMultiLineStringFeature(feature, clipped, start, end, axis);
|
|
370
|
+
continue;
|
|
371
|
+
case "Polygon":
|
|
372
|
+
clipPolygonFeature(feature, clipped, start, end, axis);
|
|
373
|
+
continue;
|
|
374
|
+
case "MultiPolygon":
|
|
375
|
+
clipMultiPolygonFeature(feature, clipped, start, end, axis);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (!clipped.length) return null;
|
|
380
|
+
return clipped;
|
|
381
|
+
}
|
|
382
|
+
function clipPointFeature(feature, clipped, start, end, axis) {
|
|
383
|
+
const geom = [];
|
|
384
|
+
clipPoints(feature.geometry, geom, start, end, axis);
|
|
385
|
+
if (!geom.length) return;
|
|
386
|
+
const type = geom.length === 3 ? "Point" : "MultiPoint";
|
|
387
|
+
clipped.push(createFeature(feature.id, type, geom, feature.tags));
|
|
388
|
+
}
|
|
389
|
+
function clipLineStringFeature(feature, clipped, start, end, axis, options) {
|
|
390
|
+
const geom = [];
|
|
391
|
+
clipLine(feature.geometry, geom, start, end, axis, false, options.lineMetrics);
|
|
392
|
+
if (!geom.length) return;
|
|
393
|
+
if (options.lineMetrics) {
|
|
394
|
+
for (const line of geom) clipped.push(createFeature(feature.id, "LineString", line, feature.tags));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (geom.length > 1) {
|
|
398
|
+
clipped.push(createFeature(feature.id, "MultiLineString", geom, feature.tags));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
clipped.push(createFeature(feature.id, "LineString", geom[0], feature.tags));
|
|
402
|
+
}
|
|
403
|
+
function clipMultiLineStringFeature(feature, clipped, start, end, axis) {
|
|
404
|
+
const geom = [];
|
|
405
|
+
clipLines(feature.geometry, geom, start, end, axis, false);
|
|
406
|
+
if (!geom.length) return;
|
|
407
|
+
if (geom.length === 1) {
|
|
408
|
+
clipped.push(createFeature(feature.id, "LineString", geom[0], feature.tags));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
clipped.push(createFeature(feature.id, "MultiLineString", geom, feature.tags));
|
|
412
|
+
}
|
|
413
|
+
function clipPolygonFeature(feature, clipped, start, end, axis) {
|
|
414
|
+
const geom = [];
|
|
415
|
+
clipLines(feature.geometry, geom, start, end, axis, true);
|
|
416
|
+
if (!geom.length) return;
|
|
417
|
+
clipped.push(createFeature(feature.id, "Polygon", geom, feature.tags));
|
|
418
|
+
}
|
|
419
|
+
function clipMultiPolygonFeature(feature, clipped, start, end, axis) {
|
|
420
|
+
const geom = [];
|
|
421
|
+
for (const polygon of feature.geometry) {
|
|
422
|
+
const newPolygon = [];
|
|
423
|
+
clipLines(polygon, newPolygon, start, end, axis, true);
|
|
424
|
+
if (!newPolygon.length) continue;
|
|
425
|
+
geom.push(newPolygon);
|
|
426
|
+
}
|
|
427
|
+
if (!geom.length) return;
|
|
428
|
+
clipped.push(createFeature(feature.id, "MultiPolygon", geom, feature.tags));
|
|
429
|
+
}
|
|
430
|
+
function clipPoints(geom, newGeom, start, end, axis) {
|
|
431
|
+
for (let i = 0; i < geom.length; i += 3) {
|
|
432
|
+
const a = geom[i + axis];
|
|
433
|
+
if (a >= start && a <= end) addPoint(newGeom, geom[i], geom[i + 1], geom[i + 2]);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function clipLine(geom, newGeom, start, end, axis, isPolygon, trackMetrics) {
|
|
437
|
+
let slice = newSlice(geom);
|
|
438
|
+
const intersect = axis === 0 ? intersectX : intersectY;
|
|
439
|
+
let len = geom.start;
|
|
440
|
+
let segLen, t;
|
|
441
|
+
for (let i = 0; i < geom.points.length - 3; i += 3) {
|
|
442
|
+
const ax2 = geom.points[i];
|
|
443
|
+
const ay2 = geom.points[i + 1];
|
|
444
|
+
const az2 = geom.points[i + 2];
|
|
445
|
+
const bx = geom.points[i + 3];
|
|
446
|
+
const by = geom.points[i + 4];
|
|
447
|
+
const a2 = axis === 0 ? ax2 : ay2;
|
|
448
|
+
const b = axis === 0 ? bx : by;
|
|
449
|
+
let exited = false;
|
|
450
|
+
if (trackMetrics) segLen = Math.sqrt(Math.pow(ax2 - bx, 2) + Math.pow(ay2 - by, 2));
|
|
451
|
+
if (a2 < start) {
|
|
452
|
+
if (b >= start) {
|
|
453
|
+
t = intersect(slice, ax2, ay2, bx, by, start);
|
|
454
|
+
if (trackMetrics) slice.start = len + segLen * t;
|
|
455
|
+
}
|
|
456
|
+
} else if (a2 > end) {
|
|
457
|
+
if (b <= end) {
|
|
458
|
+
t = intersect(slice, ax2, ay2, bx, by, end);
|
|
459
|
+
if (trackMetrics) slice.start = len + segLen * t;
|
|
460
|
+
}
|
|
461
|
+
} else addPoint(slice.points, ax2, ay2, az2);
|
|
462
|
+
if (b < start && a2 >= start) {
|
|
463
|
+
t = intersect(slice, ax2, ay2, bx, by, start);
|
|
464
|
+
exited = true;
|
|
465
|
+
}
|
|
466
|
+
if (b > end && a2 <= end) {
|
|
467
|
+
t = intersect(slice, ax2, ay2, bx, by, end);
|
|
468
|
+
exited = true;
|
|
469
|
+
}
|
|
470
|
+
if (!isPolygon && exited) {
|
|
471
|
+
if (trackMetrics) slice.end = len + segLen * t;
|
|
472
|
+
newGeom.push(slice);
|
|
473
|
+
slice = newSlice(geom);
|
|
474
|
+
}
|
|
475
|
+
if (trackMetrics) len += segLen;
|
|
476
|
+
}
|
|
477
|
+
let last = geom.points.length - 3;
|
|
478
|
+
const ax = geom.points[last];
|
|
479
|
+
const ay = geom.points[last + 1];
|
|
480
|
+
const az = geom.points[last + 2];
|
|
481
|
+
const a = axis === 0 ? ax : ay;
|
|
482
|
+
if (a >= start && a <= end) addPoint(slice.points, ax, ay, az);
|
|
483
|
+
last = slice.points.length - 3;
|
|
484
|
+
if (isPolygon && last >= 3 && (slice.points[last] !== slice.points[0] || slice.points[last + 1] !== slice.points[1])) addPoint(slice.points, slice.points[0], slice.points[1], slice.points[2]);
|
|
485
|
+
if (slice.points.length) {
|
|
486
|
+
optimizeLineMemory(slice);
|
|
487
|
+
newGeom.push(slice);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function newSlice(line) {
|
|
491
|
+
return {
|
|
492
|
+
points: [],
|
|
493
|
+
size: line.size,
|
|
494
|
+
start: line.start,
|
|
495
|
+
end: line.end
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function clipLines(geom, newGeom, start, end, axis, isPolygon) {
|
|
499
|
+
for (const line of geom) clipLine(line, newGeom, start, end, axis, isPolygon, false);
|
|
500
|
+
}
|
|
501
|
+
function addPoint(out, x, y, z) {
|
|
502
|
+
out.push(x, y, z);
|
|
503
|
+
}
|
|
504
|
+
function intersectX(out, ax, ay, bx, by, x) {
|
|
505
|
+
const t = (x - ax) / (bx - ax);
|
|
506
|
+
addPoint(out.points, x, ay + (by - ay) * t, 1);
|
|
507
|
+
return t;
|
|
508
|
+
}
|
|
509
|
+
function intersectY(out, ax, ay, bx, by, y) {
|
|
510
|
+
const t = (y - ay) / (by - ay);
|
|
511
|
+
addPoint(out.points, ax + (bx - ax) * t, y, 1);
|
|
512
|
+
return t;
|
|
513
|
+
}
|
|
514
|
+
function wrap(features, options) {
|
|
515
|
+
if (!options.wrap) return features;
|
|
516
|
+
const buffer = options.buffer / options.extent;
|
|
517
|
+
let merged = features;
|
|
518
|
+
const left = clip(features, 1, -1 - buffer, buffer, 0, -1, 2, options);
|
|
519
|
+
const right = clip(features, 1, 1 - buffer, 2 + buffer, 0, -1, 2, options);
|
|
520
|
+
if (!left && !right) return merged;
|
|
521
|
+
merged = clip(features, 1, -buffer, 1 + buffer, 0, -1, 2, options) || [];
|
|
522
|
+
if (left) merged = shiftFeatureCoords(left, 1).concat(merged);
|
|
523
|
+
if (right) merged = merged.concat(shiftFeatureCoords(right, -1));
|
|
524
|
+
return merged;
|
|
525
|
+
}
|
|
526
|
+
function shiftFeatureCoords(features, offset) {
|
|
527
|
+
const newFeatures = [];
|
|
528
|
+
for (const feature of features) switch (feature.type) {
|
|
529
|
+
case "Point":
|
|
530
|
+
case "MultiPoint": {
|
|
531
|
+
const newGeometry = shiftPointCoords(feature.geometry, offset);
|
|
532
|
+
newFeatures.push(createFeature(feature.id, feature.type, newGeometry, feature.tags));
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
case "LineString": {
|
|
536
|
+
const newGeometry = shiftLineCoords(feature.geometry, offset);
|
|
537
|
+
newFeatures.push(createFeature(feature.id, feature.type, newGeometry, feature.tags));
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
case "MultiLineString":
|
|
541
|
+
case "Polygon": {
|
|
542
|
+
const newGeometry = [];
|
|
543
|
+
for (const line of feature.geometry) newGeometry.push(shiftLineCoords(line, offset));
|
|
544
|
+
newFeatures.push(createFeature(feature.id, feature.type, newGeometry, feature.tags));
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
case "MultiPolygon": {
|
|
548
|
+
const newGeometry = [];
|
|
549
|
+
for (const polygon of feature.geometry) {
|
|
550
|
+
const newPolygon = [];
|
|
551
|
+
for (const line of polygon) newPolygon.push(shiftLineCoords(line, offset));
|
|
552
|
+
newGeometry.push(newPolygon);
|
|
553
|
+
}
|
|
554
|
+
newFeatures.push(createFeature(feature.id, feature.type, newGeometry, feature.tags));
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return newFeatures;
|
|
559
|
+
}
|
|
560
|
+
function shiftPointCoords(coords, offset) {
|
|
561
|
+
const newCoords = [];
|
|
562
|
+
for (let i = 0; i < coords.length; i += 3) newCoords.push(coords[i] + offset, coords[i + 1], coords[i + 2]);
|
|
563
|
+
return newCoords;
|
|
564
|
+
}
|
|
565
|
+
function shiftLineCoords(line, offset) {
|
|
566
|
+
const newLine = {
|
|
567
|
+
points: [],
|
|
568
|
+
size: line.size
|
|
569
|
+
};
|
|
570
|
+
if (line.start !== void 0) {
|
|
571
|
+
newLine.start = line.start;
|
|
572
|
+
newLine.end = line.end;
|
|
573
|
+
}
|
|
574
|
+
for (let i = 0; i < line.points.length; i += 3) newLine.points.push(line.points[i] + offset, line.points[i + 1], line.points[i + 2]);
|
|
575
|
+
optimizeLineMemory(newLine);
|
|
576
|
+
return newLine;
|
|
577
|
+
}
|
|
578
|
+
function applySourceDiff(source, dataDiff, options, unlocated) {
|
|
579
|
+
const diff = diffToHashed(dataDiff, options);
|
|
580
|
+
let affected = [];
|
|
581
|
+
if (diff.removeAll) {
|
|
582
|
+
affected = source;
|
|
583
|
+
source = [];
|
|
584
|
+
unlocated?.clear();
|
|
585
|
+
}
|
|
586
|
+
if (diff.remove.size || diff.add.size) {
|
|
587
|
+
for (const id of diff.remove) unlocated?.delete(id);
|
|
588
|
+
const removeFeatures = [];
|
|
589
|
+
for (const feature of source) if (diff.remove.has(feature.id) || diff.add.has(feature.id)) removeFeatures.push(feature);
|
|
590
|
+
if (removeFeatures.length) {
|
|
591
|
+
affected = affected.concat(removeFeatures);
|
|
592
|
+
const removeIds = new Set(removeFeatures.map((f) => f.id));
|
|
593
|
+
source = source.filter((f) => !removeIds.has(f.id));
|
|
594
|
+
}
|
|
595
|
+
if (diff.add.size) {
|
|
596
|
+
let addFeatures = convertToInternal({
|
|
597
|
+
type: "FeatureCollection",
|
|
598
|
+
features: Array.from(diff.add.values())
|
|
599
|
+
}, options, unlocated);
|
|
600
|
+
addFeatures = wrap(addFeatures, options);
|
|
601
|
+
affected = affected.concat(addFeatures);
|
|
602
|
+
source = source.concat(addFeatures);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (diff.update.size) {
|
|
606
|
+
const oldFeaturesMap = /* @__PURE__ */ new Map();
|
|
607
|
+
let keepFeatures = [];
|
|
608
|
+
for (const feature of source) if (diff.update.has(feature.id)) oldFeaturesMap.set(feature.id, [...oldFeaturesMap.get(feature.id) || [], feature]);
|
|
609
|
+
else keepFeatures.push(feature);
|
|
610
|
+
for (const [id, update] of diff.update) {
|
|
611
|
+
const oldFeatures = oldFeaturesMap.get(id);
|
|
612
|
+
if (!oldFeatures || oldFeatures.length === 0) {
|
|
613
|
+
if (!unlocated?.has(id)) continue;
|
|
614
|
+
const locatedFeatures = getUpdatedUnlocatedFeatures(id, update, options, unlocated);
|
|
615
|
+
affected = affected.concat(locatedFeatures);
|
|
616
|
+
keepFeatures = keepFeatures.concat(locatedFeatures);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
const updatedFeatures = getUpdatedFeatures(oldFeatures, update, options, unlocated);
|
|
620
|
+
affected = affected.concat(oldFeatures, updatedFeatures);
|
|
621
|
+
keepFeatures = keepFeatures.concat(updatedFeatures);
|
|
622
|
+
}
|
|
623
|
+
source = keepFeatures;
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
affected,
|
|
627
|
+
source
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
function getUpdatedFeatures(vtFeatures, update, options, unlocated) {
|
|
631
|
+
const changeGeometry = !!update.newGeometry;
|
|
632
|
+
const changeProps = hasPropertyChanges(update);
|
|
633
|
+
if (changeGeometry) {
|
|
634
|
+
const vtFeature = vtFeatures[0];
|
|
635
|
+
const properties = changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags;
|
|
636
|
+
return convertNewGeometry(vtFeature.id, update.newGeometry, properties, options, unlocated);
|
|
637
|
+
}
|
|
638
|
+
if (changeProps) {
|
|
639
|
+
const updated = [];
|
|
640
|
+
for (const vtFeature of vtFeatures) {
|
|
641
|
+
const feature = { ...vtFeature };
|
|
642
|
+
feature.tags = applyPropertyUpdates(feature.tags, update);
|
|
643
|
+
updated.push(feature);
|
|
644
|
+
}
|
|
645
|
+
return updated;
|
|
646
|
+
}
|
|
647
|
+
return vtFeatures;
|
|
648
|
+
}
|
|
649
|
+
function getUpdatedUnlocatedFeatures(id, update, options, unlocated) {
|
|
650
|
+
const feature = unlocated.get(id);
|
|
651
|
+
const properties = hasPropertyChanges(update) ? applyPropertyUpdates(feature.properties, update) : feature.properties;
|
|
652
|
+
if (!update.newGeometry) {
|
|
653
|
+
unlocated.set(id, {
|
|
654
|
+
...feature,
|
|
655
|
+
properties
|
|
656
|
+
});
|
|
657
|
+
return [];
|
|
658
|
+
}
|
|
659
|
+
return convertNewGeometry(id, update.newGeometry, properties, options, unlocated);
|
|
660
|
+
}
|
|
661
|
+
function convertNewGeometry(id, geometry, properties, options, unlocated) {
|
|
662
|
+
return wrap(convertToInternal({
|
|
663
|
+
type: "FeatureCollection",
|
|
664
|
+
features: [{
|
|
665
|
+
type: "Feature",
|
|
666
|
+
id,
|
|
667
|
+
geometry,
|
|
668
|
+
properties
|
|
669
|
+
}]
|
|
670
|
+
}, {
|
|
671
|
+
...options,
|
|
672
|
+
promoteId: void 0,
|
|
673
|
+
generateId: false
|
|
674
|
+
}, unlocated), options);
|
|
675
|
+
}
|
|
676
|
+
function hasPropertyChanges(update) {
|
|
677
|
+
return update.removeAllProperties || update.removeProperties?.length > 0 || update.addOrUpdateProperties?.length > 0;
|
|
678
|
+
}
|
|
679
|
+
function applyPropertyUpdates(tags, update) {
|
|
680
|
+
if (update.removeAllProperties) return {};
|
|
681
|
+
const properties = { ...tags || {} };
|
|
682
|
+
if (update.removeProperties) for (const key of update.removeProperties) delete properties[key];
|
|
683
|
+
if (update.addOrUpdateProperties) for (const { key, value } of update.addOrUpdateProperties) properties[key] = value;
|
|
684
|
+
return properties;
|
|
685
|
+
}
|
|
686
|
+
function diffToHashed(diff, options) {
|
|
687
|
+
if (!diff) return {
|
|
688
|
+
remove: /* @__PURE__ */ new Set(),
|
|
689
|
+
add: /* @__PURE__ */ new Map(),
|
|
690
|
+
update: /* @__PURE__ */ new Map()
|
|
691
|
+
};
|
|
692
|
+
return {
|
|
693
|
+
removeAll: diff.removeAll,
|
|
694
|
+
remove: new Set(diff.remove || []),
|
|
695
|
+
add: new Map(diff.add?.map((feature) => [options.promoteId ? feature.properties?.[options.promoteId] : feature.id, feature])),
|
|
696
|
+
update: new Map(diff.update?.map((update) => [update.id, update]))
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
var ARRAY_TYPES = [
|
|
700
|
+
Int8Array,
|
|
701
|
+
Uint8Array,
|
|
702
|
+
Uint8ClampedArray,
|
|
703
|
+
Int16Array,
|
|
704
|
+
Uint16Array,
|
|
705
|
+
Int32Array,
|
|
706
|
+
Uint32Array,
|
|
707
|
+
Float32Array,
|
|
708
|
+
Float64Array
|
|
709
|
+
];
|
|
710
|
+
var VERSION = 1;
|
|
711
|
+
var HEADER_SIZE = 8;
|
|
712
|
+
var STACK = /* @__PURE__ */ new Uint32Array(96);
|
|
713
|
+
var KDBush = class KDBush2 {
|
|
714
|
+
/**
|
|
715
|
+
* Creates an index from raw `ArrayBuffer` data.
|
|
716
|
+
* @param {ArrayBufferLike} data
|
|
717
|
+
*/
|
|
718
|
+
static from(data) {
|
|
719
|
+
if (!data || data.byteLength === void 0 || data.buffer) throw new Error("Data must be an instance of ArrayBuffer or SharedArrayBuffer.");
|
|
720
|
+
const [magic, versionAndType] = new Uint8Array(data, 0, 2);
|
|
721
|
+
if (magic !== 219) throw new Error("Data does not appear to be in a KDBush format.");
|
|
722
|
+
const version = versionAndType >> 4;
|
|
723
|
+
if (version !== VERSION) throw new Error(`Got v${version} data when expected v${VERSION}.`);
|
|
724
|
+
const ArrayType = ARRAY_TYPES[versionAndType & 15];
|
|
725
|
+
if (!ArrayType) throw new Error("Unrecognized array type.");
|
|
726
|
+
const [nodeSize] = new Uint16Array(data, 2, 1);
|
|
727
|
+
const [numItems] = new Uint32Array(data, 4, 1);
|
|
728
|
+
return new KDBush2(numItems, nodeSize, ArrayType, void 0, data);
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Creates an index that will hold a given number of items.
|
|
732
|
+
* @param {number} numItems
|
|
733
|
+
* @param {number} [nodeSize=64] Size of the KD-tree node (64 by default).
|
|
734
|
+
* @param {TypedArrayConstructor} [ArrayType=Float64Array] The array type used for coordinates storage (`Float64Array` by default).
|
|
735
|
+
* @param {ArrayBufferConstructor | SharedArrayBufferConstructor} [ArrayBufferType=ArrayBuffer] The array buffer type used for storage (`ArrayBuffer` by default).
|
|
736
|
+
* @param {ArrayBufferLike} [data] (For internal use only)
|
|
737
|
+
*/
|
|
738
|
+
constructor(numItems, nodeSize = 64, ArrayType = Float64Array, ArrayBufferType = ArrayBuffer, data) {
|
|
739
|
+
if (isNaN(numItems) || numItems < 0) throw new Error(`Unexpected numItems value: ${numItems}.`);
|
|
740
|
+
this.numItems = +numItems;
|
|
741
|
+
this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535);
|
|
742
|
+
this.ArrayType = ArrayType;
|
|
743
|
+
this.IndexArrayType = numItems < 65536 ? Uint16Array : Uint32Array;
|
|
744
|
+
const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType);
|
|
745
|
+
const coordsByteSize = numItems * 2 * this.ArrayType.BYTES_PER_ELEMENT;
|
|
746
|
+
const idsByteSize = numItems * this.IndexArrayType.BYTES_PER_ELEMENT;
|
|
747
|
+
const padCoords = (8 - idsByteSize % 8) % 8;
|
|
748
|
+
if (arrayTypeIndex < 0) throw new Error(`Unexpected typed array class: ${ArrayType}.`);
|
|
749
|
+
if (data) {
|
|
750
|
+
this.data = data;
|
|
751
|
+
this.ids = new this.IndexArrayType(data, HEADER_SIZE, numItems);
|
|
752
|
+
this.coords = new ArrayType(data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
|
|
753
|
+
this._pos = numItems * 2;
|
|
754
|
+
this._finished = true;
|
|
755
|
+
} else {
|
|
756
|
+
const data2 = this.data = new ArrayBufferType(HEADER_SIZE + coordsByteSize + idsByteSize + padCoords);
|
|
757
|
+
this.ids = new this.IndexArrayType(data2, HEADER_SIZE, numItems);
|
|
758
|
+
this.coords = new ArrayType(data2, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
|
|
759
|
+
this._pos = 0;
|
|
760
|
+
this._finished = false;
|
|
761
|
+
new Uint8Array(data2, 0, 2).set([219, 16 + arrayTypeIndex]);
|
|
762
|
+
new Uint16Array(data2, 2, 1)[0] = nodeSize;
|
|
763
|
+
new Uint32Array(data2, 4, 1)[0] = numItems;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Add a point to the index.
|
|
768
|
+
* @param {number} x
|
|
769
|
+
* @param {number} y
|
|
770
|
+
* @returns {number} An incremental index associated with the added item (starting from `0`).
|
|
771
|
+
*/
|
|
772
|
+
add(x, y) {
|
|
773
|
+
const index = this._pos >> 1;
|
|
774
|
+
this.ids[index] = index;
|
|
775
|
+
this.coords[this._pos++] = x;
|
|
776
|
+
this.coords[this._pos++] = y;
|
|
777
|
+
return index;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Perform indexing of the added points.
|
|
781
|
+
*/
|
|
782
|
+
finish() {
|
|
783
|
+
const numAdded = this._pos >> 1;
|
|
784
|
+
if (numAdded !== this.numItems) throw new Error(`Added ${numAdded} items when expected ${this.numItems}.`);
|
|
785
|
+
sort(this.ids, this.coords, this.nodeSize, 0, this.numItems - 1, 0);
|
|
786
|
+
this._finished = true;
|
|
787
|
+
return this;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Search the index for items within a given bounding box.
|
|
791
|
+
* @param {number} minX
|
|
792
|
+
* @param {number} minY
|
|
793
|
+
* @param {number} maxX
|
|
794
|
+
* @param {number} maxY
|
|
795
|
+
* @returns {number[]} An array of indices correponding to the found items.
|
|
796
|
+
*/
|
|
797
|
+
range(minX, minY, maxX, maxY) {
|
|
798
|
+
if (!this._finished) throw new Error("Data not yet indexed - call index.finish().");
|
|
799
|
+
const { ids, coords, nodeSize } = this;
|
|
800
|
+
STACK[0] = 0;
|
|
801
|
+
STACK[1] = ids.length - 1;
|
|
802
|
+
STACK[2] = 0;
|
|
803
|
+
let sp = 3;
|
|
804
|
+
const result = [];
|
|
805
|
+
while (sp > 0) {
|
|
806
|
+
const axis = STACK[--sp];
|
|
807
|
+
const right = STACK[--sp];
|
|
808
|
+
const left = STACK[--sp];
|
|
809
|
+
if (right - left <= nodeSize) {
|
|
810
|
+
for (let i = left; i <= right; i++) {
|
|
811
|
+
const x2 = coords[2 * i];
|
|
812
|
+
const y2 = coords[2 * i + 1];
|
|
813
|
+
if (x2 >= minX && x2 <= maxX && y2 >= minY && y2 <= maxY) result.push(ids[i]);
|
|
814
|
+
}
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
const m = left + right >> 1;
|
|
818
|
+
const x = coords[2 * m];
|
|
819
|
+
const y = coords[2 * m + 1];
|
|
820
|
+
if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[m]);
|
|
821
|
+
if (axis === 0 ? minX <= x : minY <= y) {
|
|
822
|
+
STACK[sp++] = left;
|
|
823
|
+
STACK[sp++] = m - 1;
|
|
824
|
+
STACK[sp++] = 1 - axis;
|
|
825
|
+
}
|
|
826
|
+
if (axis === 0 ? maxX >= x : maxY >= y) {
|
|
827
|
+
STACK[sp++] = m + 1;
|
|
828
|
+
STACK[sp++] = right;
|
|
829
|
+
STACK[sp++] = 1 - axis;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return result;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Search the index for items within a given radius.
|
|
836
|
+
* @param {number} qx
|
|
837
|
+
* @param {number} qy
|
|
838
|
+
* @param {number} r Query radius.
|
|
839
|
+
* @returns {number[]} An array of indices correponding to the found items.
|
|
840
|
+
*/
|
|
841
|
+
within(qx, qy, r) {
|
|
842
|
+
const result = [];
|
|
843
|
+
this.withinInto(qx, qy, r, result);
|
|
844
|
+
return result;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Search the index for items within a given radius, writing matching ids into `out`
|
|
848
|
+
* via indexed assignment (`out[i] = id`). Accepts any indexed-writable container —
|
|
849
|
+
* a typed array sized to the expected upper bound (allocation-free, fast) or a plain
|
|
850
|
+
* `Array` (which will grow as needed). Returns the number of matches written.
|
|
851
|
+
* @param {number} qx
|
|
852
|
+
* @param {number} qy
|
|
853
|
+
* @param {number} r Query radius.
|
|
854
|
+
* @param {number[] | TypedArray} out Container to write matching ids into.
|
|
855
|
+
* @returns {number} The number of matches written to `out`.
|
|
856
|
+
*/
|
|
857
|
+
withinInto(qx, qy, r, out) {
|
|
858
|
+
if (!this._finished) throw new Error("Data not yet indexed - call index.finish().");
|
|
859
|
+
const { ids, coords, nodeSize } = this;
|
|
860
|
+
STACK[0] = 0;
|
|
861
|
+
STACK[1] = ids.length - 1;
|
|
862
|
+
STACK[2] = 0;
|
|
863
|
+
let sp = 3;
|
|
864
|
+
let count = 0;
|
|
865
|
+
const r2 = r * r;
|
|
866
|
+
while (sp > 0) {
|
|
867
|
+
const axis = STACK[--sp];
|
|
868
|
+
const right = STACK[--sp];
|
|
869
|
+
const left = STACK[--sp];
|
|
870
|
+
if (right - left <= nodeSize) {
|
|
871
|
+
for (let i = left; i <= right; i++) if (sqDist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2) out[count++] = ids[i];
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
const m = left + right >> 1;
|
|
875
|
+
const x = coords[2 * m];
|
|
876
|
+
const y = coords[2 * m + 1];
|
|
877
|
+
if (sqDist(x, y, qx, qy) <= r2) out[count++] = ids[m];
|
|
878
|
+
if (axis === 0 ? qx - r <= x : qy - r <= y) {
|
|
879
|
+
STACK[sp++] = left;
|
|
880
|
+
STACK[sp++] = m - 1;
|
|
881
|
+
STACK[sp++] = 1 - axis;
|
|
882
|
+
}
|
|
883
|
+
if (axis === 0 ? qx + r >= x : qy + r >= y) {
|
|
884
|
+
STACK[sp++] = m + 1;
|
|
885
|
+
STACK[sp++] = right;
|
|
886
|
+
STACK[sp++] = 1 - axis;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return count;
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
function sort(ids, coords, nodeSize, left, right, axis) {
|
|
893
|
+
if (right - left <= nodeSize) return;
|
|
894
|
+
const m = left + right >> 1;
|
|
895
|
+
select(ids, coords, m, left, right, axis);
|
|
896
|
+
sort(ids, coords, nodeSize, left, m - 1, 1 - axis);
|
|
897
|
+
sort(ids, coords, nodeSize, m + 1, right, 1 - axis);
|
|
898
|
+
}
|
|
899
|
+
function select(ids, coords, k, left, right, axis) {
|
|
900
|
+
while (right > left) {
|
|
901
|
+
if (right - left > 600) {
|
|
902
|
+
const n = right - left + 1;
|
|
903
|
+
const m = k - left + 1;
|
|
904
|
+
const z = Math.log(n);
|
|
905
|
+
const s = 0.5 * Math.exp(2 * z / 3);
|
|
906
|
+
const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
|
|
907
|
+
select(ids, coords, k, Math.max(left, Math.floor(k - m * s / n + sd)), Math.min(right, Math.floor(k + (n - m) * s / n + sd)), axis);
|
|
908
|
+
}
|
|
909
|
+
const t = coords[2 * k + axis];
|
|
910
|
+
let i = left;
|
|
911
|
+
let j = right;
|
|
912
|
+
swapItem(ids, coords, left, k);
|
|
913
|
+
if (coords[2 * right + axis] > t) swapItem(ids, coords, left, right);
|
|
914
|
+
while (i < j) {
|
|
915
|
+
swapItem(ids, coords, i, j);
|
|
916
|
+
i++;
|
|
917
|
+
j--;
|
|
918
|
+
while (coords[2 * i + axis] < t) i++;
|
|
919
|
+
while (coords[2 * j + axis] > t) j--;
|
|
920
|
+
}
|
|
921
|
+
if (coords[2 * left + axis] === t) swapItem(ids, coords, left, j);
|
|
922
|
+
else {
|
|
923
|
+
j++;
|
|
924
|
+
swapItem(ids, coords, j, right);
|
|
925
|
+
}
|
|
926
|
+
if (j <= k) left = j + 1;
|
|
927
|
+
if (k <= j) right = j - 1;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function swapItem(ids, coords, i, j) {
|
|
931
|
+
swap(ids, i, j);
|
|
932
|
+
swap(coords, 2 * i, 2 * j);
|
|
933
|
+
swap(coords, 2 * i + 1, 2 * j + 1);
|
|
934
|
+
}
|
|
935
|
+
function swap(arr, i, j) {
|
|
936
|
+
const tmp = arr[i];
|
|
937
|
+
arr[i] = arr[j];
|
|
938
|
+
arr[j] = tmp;
|
|
939
|
+
}
|
|
940
|
+
function sqDist(ax, ay, bx, by) {
|
|
941
|
+
const dx = ax - bx;
|
|
942
|
+
const dy = ay - by;
|
|
943
|
+
return dx * dx + dy * dy;
|
|
944
|
+
}
|
|
945
|
+
var defaultClusterOptions = {
|
|
946
|
+
minZoom: 0,
|
|
947
|
+
maxZoom: 16,
|
|
948
|
+
minPoints: 2,
|
|
949
|
+
radius: 40,
|
|
950
|
+
extent: 512,
|
|
951
|
+
nodeSize: 64,
|
|
952
|
+
log: false,
|
|
953
|
+
generateId: false,
|
|
954
|
+
reduce: null,
|
|
955
|
+
map: (props) => props,
|
|
956
|
+
coordinates: "lngLat",
|
|
957
|
+
wrap: true
|
|
958
|
+
};
|
|
959
|
+
var MAX_ZOOM = 30;
|
|
960
|
+
var SCALE = 1073741824;
|
|
961
|
+
var MAX_INTEGER = 2147483647;
|
|
962
|
+
function encode(coord) {
|
|
963
|
+
return (coord - 0.5) * SCALE;
|
|
964
|
+
}
|
|
965
|
+
function decode(value) {
|
|
966
|
+
return value / SCALE + 0.5;
|
|
967
|
+
}
|
|
968
|
+
function fitsInteger(coord) {
|
|
969
|
+
return Math.abs(encode(coord)) < MAX_INTEGER;
|
|
970
|
+
}
|
|
971
|
+
var OFFSET_ZOOM = 0;
|
|
972
|
+
var OFFSET_ID = 1;
|
|
973
|
+
var OFFSET_PARENT = 2;
|
|
974
|
+
var OFFSET_NUM = 3;
|
|
975
|
+
var OFFSET_PROP = 4;
|
|
976
|
+
var ClusterTileIndex = class {
|
|
977
|
+
constructor(options) {
|
|
978
|
+
this.options = createOptions(options);
|
|
979
|
+
this.worldCoordinateHelper = getWorldCoordinateHelper(this.options.coordinates);
|
|
980
|
+
this.trees = new Array(this.options.maxZoom + 1);
|
|
981
|
+
this.stride = this.options.reduce ? 5 : 4;
|
|
982
|
+
this.clusterProps = [];
|
|
983
|
+
this.points = [];
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Loads GeoJSON Point and MultiPoint features and builds the internal clustering index.
|
|
987
|
+
* Each position of a MultiPoint is clustered as a point of its own, with the id and properties of its feature.
|
|
988
|
+
* @param points - GeoJSON Point and MultiPoint features to cluster.
|
|
989
|
+
*/
|
|
990
|
+
load(points) {
|
|
991
|
+
const features = [];
|
|
992
|
+
for (const point of points) {
|
|
993
|
+
if (!point.geometry) continue;
|
|
994
|
+
const positions = point.geometry.type === "MultiPoint" ? point.geometry.coordinates : [point.geometry.coordinates];
|
|
995
|
+
for (const [lng, lat] of positions) {
|
|
996
|
+
const [x, y] = [this.worldCoordinateHelper.projectX(lng), this.worldCoordinateHelper.projectY(lat)];
|
|
997
|
+
if (!this.options.wrap && !isInWorld([x, y])) continue;
|
|
998
|
+
const feature = {
|
|
999
|
+
id: point.id,
|
|
1000
|
+
type: "Point",
|
|
1001
|
+
geometry: [x, y],
|
|
1002
|
+
tags: point.properties
|
|
1003
|
+
};
|
|
1004
|
+
features.push(feature);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
this.createIndex(features);
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* @internal
|
|
1011
|
+
* Loads internal GeoJSONVT point features from a data source and builds the clustering index.
|
|
1012
|
+
* Each position of a MultiPoint feature becomes a point of its own, with the id and properties of its feature.
|
|
1013
|
+
* @param features - {@link GeoJSONVTInternalFeature} data source features to filter and cluster.
|
|
1014
|
+
*/
|
|
1015
|
+
initialize(features) {
|
|
1016
|
+
const points = [];
|
|
1017
|
+
for (const feature of features) {
|
|
1018
|
+
if (feature.type === "Point") {
|
|
1019
|
+
if (!this.options.wrap && !isInWorld(feature.geometry)) continue;
|
|
1020
|
+
points.push(feature);
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
if (feature.type !== "MultiPoint") continue;
|
|
1024
|
+
for (let i = 0; i < feature.geometry.length; i += 3) {
|
|
1025
|
+
const geometry = [feature.geometry[i], feature.geometry[i + 1]];
|
|
1026
|
+
if (!this.options.wrap && !isInWorld(geometry)) continue;
|
|
1027
|
+
points.push({
|
|
1028
|
+
id: feature.id,
|
|
1029
|
+
type: "Point",
|
|
1030
|
+
geometry,
|
|
1031
|
+
tags: feature.tags
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
this.createIndex(points);
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* @internal
|
|
1039
|
+
* Updates the cluster data by rebuilding.
|
|
1040
|
+
* @param features
|
|
1041
|
+
*/
|
|
1042
|
+
updateIndex(features, _affected, options) {
|
|
1043
|
+
this.options = createOptions({
|
|
1044
|
+
...options.clusterOptions,
|
|
1045
|
+
coordinates: this.options.coordinates,
|
|
1046
|
+
wrap: this.options.wrap
|
|
1047
|
+
});
|
|
1048
|
+
this.initialize(features);
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Builds a tree per zoom level from the points. A point whose position does not fit in an integer is left out,
|
|
1052
|
+
* see `fitsInteger`.
|
|
1053
|
+
*/
|
|
1054
|
+
createIndex(points) {
|
|
1055
|
+
const { log, minZoom, maxZoom } = this.options;
|
|
1056
|
+
this.trees = new Array(maxZoom + 1);
|
|
1057
|
+
this.stride = this.options.reduce ? 5 : 4;
|
|
1058
|
+
this.clusterProps = [];
|
|
1059
|
+
if (log) console.time("total time");
|
|
1060
|
+
const timerId = `prepare ${points.length} points`;
|
|
1061
|
+
if (log) console.time(timerId);
|
|
1062
|
+
this.points = points;
|
|
1063
|
+
const stride = this.stride;
|
|
1064
|
+
const notProcessed = maxZoom + 1;
|
|
1065
|
+
const coords = new Int32Array(points.length * 2);
|
|
1066
|
+
const data = new Int32Array(points.length * stride);
|
|
1067
|
+
let numItems = 0;
|
|
1068
|
+
for (let i = 0; i < points.length; i++) {
|
|
1069
|
+
const p = points[i];
|
|
1070
|
+
if (!p?.geometry || !fitsInteger(p.geometry[0]) || !fitsInteger(p.geometry[1])) continue;
|
|
1071
|
+
coords[numItems * 2] = Math.round(encode(p.geometry[0]));
|
|
1072
|
+
coords[numItems * 2 + 1] = Math.round(encode(p.geometry[1]));
|
|
1073
|
+
const k = numItems * stride;
|
|
1074
|
+
data[k + OFFSET_ZOOM] = notProcessed;
|
|
1075
|
+
data[k + OFFSET_ID] = i;
|
|
1076
|
+
data[k + OFFSET_PARENT] = -1;
|
|
1077
|
+
data[k + OFFSET_NUM] = 1;
|
|
1078
|
+
numItems++;
|
|
1079
|
+
}
|
|
1080
|
+
let tree = this.trees[maxZoom + 1] = this.createTree(coords, data, numItems);
|
|
1081
|
+
if (log) console.timeEnd(timerId);
|
|
1082
|
+
const nextCoords = new Int32Array(numItems * 2);
|
|
1083
|
+
const nextData = new Int32Array(numItems * stride);
|
|
1084
|
+
for (let z = maxZoom; z >= minZoom; z--) {
|
|
1085
|
+
const now = Date.now();
|
|
1086
|
+
const nextNumItems = this.cluster(tree, z, nextCoords, nextData);
|
|
1087
|
+
if (!this.isSameAsTree(tree, nextData, nextNumItems)) tree = this.createTree(nextCoords.slice(0, nextNumItems * 2), nextData.slice(0, nextNumItems * stride), nextNumItems);
|
|
1088
|
+
this.trees[z] = tree;
|
|
1089
|
+
if (log) console.log("z%d: %d clusters in %dms", z, tree.numItems, Date.now() - now);
|
|
1090
|
+
}
|
|
1091
|
+
if (log) console.timeEnd("total time");
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Returns clusters and/or points within a bounding box at a given zoom level.
|
|
1095
|
+
* @param bbox - Bounding box in `[westLng, southLat, eastLng, northLat]` order, or `[minX, minY, maxX, maxY]` in the unit square with `coordinates: 'unitSquare'`.
|
|
1096
|
+
* In a world that wraps, the box can reach across the antimeridian. In one that doesn't, only its part inside the world counts.
|
|
1097
|
+
* @param zoom - Zoom level to query.
|
|
1098
|
+
*/
|
|
1099
|
+
getClusters(bbox, zoom) {
|
|
1100
|
+
return (!this.options.wrap ? this.getClustersInWorld(bbox, zoom) : this.options.coordinates === "unitSquare" ? this.getClustersAcrossEdge(bbox, zoom) : this.getClustersInternal(bbox, zoom)).map((f) => featureToGeoJSON(f, this.worldCoordinateHelper));
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* The clusters and points in the part of a box inside a world that ends at its edges.
|
|
1104
|
+
*/
|
|
1105
|
+
getClustersInWorld(bbox, zoom) {
|
|
1106
|
+
const lngLat = this.options.coordinates !== "unitSquare";
|
|
1107
|
+
const y1 = this.worldCoordinateHelper.projectY(lngLat ? Math.max(-90, Math.min(90, bbox[1])) : bbox[1]);
|
|
1108
|
+
const y2 = this.worldCoordinateHelper.projectY(lngLat ? Math.max(-90, Math.min(90, bbox[3])) : bbox[3]);
|
|
1109
|
+
const minX = Math.max(0, this.worldCoordinateHelper.projectX(bbox[0]));
|
|
1110
|
+
const maxX = Math.min(1, this.worldCoordinateHelper.projectX(bbox[2]));
|
|
1111
|
+
if (minX > maxX) return [];
|
|
1112
|
+
return this.getClustersInRange([
|
|
1113
|
+
minX,
|
|
1114
|
+
Math.min(y1, y2),
|
|
1115
|
+
maxX,
|
|
1116
|
+
Math.max(y1, y2)
|
|
1117
|
+
], zoom);
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* The clusters and points in a box of a unit square that wraps, where the box can reach across its left or right edge.
|
|
1121
|
+
*/
|
|
1122
|
+
getClustersAcrossEdge([minX, minY, maxX, maxY], zoom) {
|
|
1123
|
+
if (maxX < minX) maxX += 1;
|
|
1124
|
+
if (maxX - minX >= 1) return this.getClustersInRange([
|
|
1125
|
+
0,
|
|
1126
|
+
minY,
|
|
1127
|
+
1,
|
|
1128
|
+
maxY
|
|
1129
|
+
], zoom);
|
|
1130
|
+
const shift = Math.floor(minX);
|
|
1131
|
+
const west = minX - shift;
|
|
1132
|
+
const east = maxX - shift;
|
|
1133
|
+
if (east <= 1) return this.getClustersInRange([
|
|
1134
|
+
west,
|
|
1135
|
+
minY,
|
|
1136
|
+
east,
|
|
1137
|
+
maxY
|
|
1138
|
+
], zoom);
|
|
1139
|
+
return this.getClustersInRange([
|
|
1140
|
+
west,
|
|
1141
|
+
minY,
|
|
1142
|
+
1,
|
|
1143
|
+
maxY
|
|
1144
|
+
], zoom).concat(this.getClustersInRange([
|
|
1145
|
+
0,
|
|
1146
|
+
minY,
|
|
1147
|
+
east - 1,
|
|
1148
|
+
maxY
|
|
1149
|
+
], zoom));
|
|
1150
|
+
}
|
|
1151
|
+
getClustersInternal(bbox, zoom) {
|
|
1152
|
+
let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
|
|
1153
|
+
const minLat = Math.max(-90, Math.min(90, bbox[1]));
|
|
1154
|
+
let maxLng = bbox[2] === 180 ? 180 : ((bbox[2] + 180) % 360 + 360) % 360 - 180;
|
|
1155
|
+
const maxLat = Math.max(-90, Math.min(90, bbox[3]));
|
|
1156
|
+
if (bbox[2] - bbox[0] >= 360) {
|
|
1157
|
+
minLng = -180;
|
|
1158
|
+
maxLng = 180;
|
|
1159
|
+
} else if (minLng > maxLng) {
|
|
1160
|
+
const easternHem = this.getClustersInternal([
|
|
1161
|
+
minLng,
|
|
1162
|
+
minLat,
|
|
1163
|
+
180,
|
|
1164
|
+
maxLat
|
|
1165
|
+
], zoom);
|
|
1166
|
+
const westernHem = this.getClustersInternal([
|
|
1167
|
+
-180,
|
|
1168
|
+
minLat,
|
|
1169
|
+
maxLng,
|
|
1170
|
+
maxLat
|
|
1171
|
+
], zoom);
|
|
1172
|
+
return easternHem.concat(westernHem);
|
|
1173
|
+
}
|
|
1174
|
+
return this.getClustersInRange([
|
|
1175
|
+
this.worldCoordinateHelper.projectX(minLng),
|
|
1176
|
+
this.worldCoordinateHelper.projectY(maxLat),
|
|
1177
|
+
this.worldCoordinateHelper.projectX(maxLng),
|
|
1178
|
+
this.worldCoordinateHelper.projectY(minLat)
|
|
1179
|
+
], zoom);
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* The clusters and points in a box of the internal coordinates, `[minX, minY, maxX, maxY]`.
|
|
1183
|
+
*/
|
|
1184
|
+
getClustersInRange([minX, minY, maxX, maxY], zoom) {
|
|
1185
|
+
const tree = this.trees[this.limitZoom(zoom)];
|
|
1186
|
+
const ids = tree.range(encode(minX), encode(minY), encode(maxX), encode(maxY));
|
|
1187
|
+
const coords = tree.flatCoords;
|
|
1188
|
+
const data = tree.flatData;
|
|
1189
|
+
const clusters = [];
|
|
1190
|
+
for (const id of ids) {
|
|
1191
|
+
const k = this.stride * id;
|
|
1192
|
+
clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterFeature(coords, id, data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
|
|
1193
|
+
}
|
|
1194
|
+
return clusters;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Returns the immediate children (clusters or points) of a cluster as GeoJSON.
|
|
1198
|
+
* @param clusterId - The target cluster id.
|
|
1199
|
+
*/
|
|
1200
|
+
getChildren(clusterId) {
|
|
1201
|
+
const originId = this.getOriginId(clusterId);
|
|
1202
|
+
const originZoom = this.getOriginZoom(clusterId);
|
|
1203
|
+
const clusterError = /* @__PURE__ */ new Error("No cluster with the specified id: " + clusterId);
|
|
1204
|
+
const tree = this.trees[originZoom];
|
|
1205
|
+
if (!tree) throw clusterError;
|
|
1206
|
+
const coords = tree.flatCoords;
|
|
1207
|
+
const data = tree.flatData;
|
|
1208
|
+
if (originId >= tree.numItems) throw clusterError;
|
|
1209
|
+
const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1)) * SCALE;
|
|
1210
|
+
const x = coords[originId * 2];
|
|
1211
|
+
const y = coords[originId * 2 + 1];
|
|
1212
|
+
const ids = tree.within(x, y, r);
|
|
1213
|
+
const children = [];
|
|
1214
|
+
for (const id of ids) {
|
|
1215
|
+
const k = id * this.stride;
|
|
1216
|
+
if (data[k + OFFSET_PARENT] === clusterId) children.push(data[k + OFFSET_NUM] > 1 ? getClusterGeoJSON(coords, id, data, k, this.clusterProps, this.worldCoordinateHelper) : featureToGeoJSON(this.points[data[k + OFFSET_ID]], this.worldCoordinateHelper));
|
|
1217
|
+
}
|
|
1218
|
+
if (children.length === 0) throw clusterError;
|
|
1219
|
+
return children;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Returns leaf point features under a cluster, paginated by `limit` and `offset`.
|
|
1223
|
+
* @param clusterId - The target cluster id.
|
|
1224
|
+
* @param limit - Maximum number of points to return (defaults to `10`).
|
|
1225
|
+
* @param offset - Number of points to skip before collecting results (defaults to `0`).
|
|
1226
|
+
*/
|
|
1227
|
+
getLeaves(clusterId, limit, offset) {
|
|
1228
|
+
limit = limit || 10;
|
|
1229
|
+
offset = offset || 0;
|
|
1230
|
+
const leaves = [];
|
|
1231
|
+
this.appendLeaves(leaves, clusterId, limit, offset, 0);
|
|
1232
|
+
return leaves;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Generates a vector-tile-like representation of a single tile.
|
|
1236
|
+
* @param z - Tile zoom.
|
|
1237
|
+
* @param x - Tile x coordinate.
|
|
1238
|
+
* @param y - Tile y coordinate.
|
|
1239
|
+
*/
|
|
1240
|
+
getTile(z, x, y) {
|
|
1241
|
+
const tree = this.trees[this.limitZoom(z)];
|
|
1242
|
+
if (!tree) return null;
|
|
1243
|
+
const z2 = Math.pow(2, z);
|
|
1244
|
+
if (!this.options.wrap && (x < 0 || x >= z2 || y < 0 || y >= z2)) return null;
|
|
1245
|
+
const { extent, radius } = this.options;
|
|
1246
|
+
const p = radius / extent;
|
|
1247
|
+
const top = encode((y - p) / z2);
|
|
1248
|
+
const bottom = encode((y + 1 + p) / z2);
|
|
1249
|
+
const tile = {
|
|
1250
|
+
transformed: true,
|
|
1251
|
+
features: [],
|
|
1252
|
+
source: null,
|
|
1253
|
+
x,
|
|
1254
|
+
y,
|
|
1255
|
+
z
|
|
1256
|
+
};
|
|
1257
|
+
this.addTileFeatures(tree.range(encode((x - p) / z2), top, encode((x + 1 + p) / z2), bottom), tree, x, y, z2, tile);
|
|
1258
|
+
if (this.options.wrap && x === 0) this.addTileFeatures(tree.range(encode(1 - p / z2), top, encode(1), bottom), tree, z2, y, z2, tile);
|
|
1259
|
+
if (this.options.wrap && x === z2 - 1) this.addTileFeatures(tree.range(encode(0), top, encode(p / z2), bottom), tree, -1, y, z2, tile);
|
|
1260
|
+
return tile;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Returns the zoom level at which a cluster expands into multiple children.
|
|
1264
|
+
* @param clusterId - The target cluster id.
|
|
1265
|
+
*/
|
|
1266
|
+
getClusterExpansionZoom(clusterId) {
|
|
1267
|
+
return this.getOriginZoom(clusterId);
|
|
1268
|
+
}
|
|
1269
|
+
appendLeaves(result, clusterId, limit, offset, skipped) {
|
|
1270
|
+
const children = this.getChildren(clusterId);
|
|
1271
|
+
for (const child of children) {
|
|
1272
|
+
const props = child.properties;
|
|
1273
|
+
if (props?.cluster) {
|
|
1274
|
+
if (skipped + props.point_count <= offset) skipped += props.point_count;
|
|
1275
|
+
else skipped = this.appendLeaves(result, props.cluster_id, limit, offset, skipped);
|
|
1276
|
+
} else if (skipped < offset) skipped++;
|
|
1277
|
+
else result.push(child);
|
|
1278
|
+
if (result.length === limit) break;
|
|
1279
|
+
}
|
|
1280
|
+
return skipped;
|
|
1281
|
+
}
|
|
1282
|
+
createTree(coords, data, numItems) {
|
|
1283
|
+
const tree = new KDBush(numItems, this.options.nodeSize, Int32Array);
|
|
1284
|
+
for (let i = 0; i < numItems; i++) tree.add(coords[i * 2], coords[i * 2 + 1]);
|
|
1285
|
+
tree.finish();
|
|
1286
|
+
tree.flatCoords = coords;
|
|
1287
|
+
tree.flatData = data;
|
|
1288
|
+
tree.data = null;
|
|
1289
|
+
return tree;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Whether clustering a tree left all its points and clusters as they were, in the same order.
|
|
1293
|
+
*/
|
|
1294
|
+
isSameAsTree(tree, nextData, nextNumItems) {
|
|
1295
|
+
if (nextNumItems !== tree.numItems) return false;
|
|
1296
|
+
const data = tree.flatData;
|
|
1297
|
+
for (let k = OFFSET_ID; k < nextNumItems * this.stride; k += this.stride) if (nextData[k] !== data[k]) return false;
|
|
1298
|
+
return true;
|
|
1299
|
+
}
|
|
1300
|
+
addTileFeatures(ids, tree, x, y, z2, tile) {
|
|
1301
|
+
const coords = tree.flatCoords;
|
|
1302
|
+
const data = tree.flatData;
|
|
1303
|
+
for (const i of ids) {
|
|
1304
|
+
const k = i * this.stride;
|
|
1305
|
+
const isCluster = data[k + OFFSET_NUM] > 1;
|
|
1306
|
+
let tags;
|
|
1307
|
+
let px;
|
|
1308
|
+
let py;
|
|
1309
|
+
if (isCluster) {
|
|
1310
|
+
tags = getClusterProperties(data, k, this.clusterProps);
|
|
1311
|
+
px = decode(coords[i * 2]);
|
|
1312
|
+
py = decode(coords[i * 2 + 1]);
|
|
1313
|
+
} else {
|
|
1314
|
+
const p = this.points[data[k + OFFSET_ID]];
|
|
1315
|
+
tags = p.tags;
|
|
1316
|
+
[px, py] = p.geometry;
|
|
1317
|
+
}
|
|
1318
|
+
const f = {
|
|
1319
|
+
type: 1,
|
|
1320
|
+
geometry: [[Math.round(this.options.extent * (px * z2 - x)), Math.round(this.options.extent * (py * z2 - y))]],
|
|
1321
|
+
tags
|
|
1322
|
+
};
|
|
1323
|
+
let id;
|
|
1324
|
+
if (isCluster || this.options.generateId) id = data[k + OFFSET_ID];
|
|
1325
|
+
else id = this.points[data[k + OFFSET_ID]].id;
|
|
1326
|
+
if (id !== void 0) f.id = id;
|
|
1327
|
+
tile.features.push(f);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
limitZoom(z) {
|
|
1331
|
+
return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Clusters the points of a tree on a zoom level, writing the result to `nextCoords` and `nextData`.
|
|
1335
|
+
* @returns the number of points and clusters written
|
|
1336
|
+
*/
|
|
1337
|
+
cluster(tree, zoom, nextCoords, nextData) {
|
|
1338
|
+
const { radius, extent, reduce, minPoints, maxZoom } = this.options;
|
|
1339
|
+
const r = radius / (extent * Math.pow(2, zoom)) * SCALE;
|
|
1340
|
+
const notProcessed = maxZoom + 1;
|
|
1341
|
+
const coords = tree.flatCoords;
|
|
1342
|
+
const data = tree.flatData;
|
|
1343
|
+
const stride = this.stride;
|
|
1344
|
+
const neighborIds = new Uint32Array(tree.numItems);
|
|
1345
|
+
let nextNumItems = 0;
|
|
1346
|
+
for (let index = 0; index < tree.numItems; index++) {
|
|
1347
|
+
const i = index * stride;
|
|
1348
|
+
if (data[i + OFFSET_ZOOM] <= zoom) continue;
|
|
1349
|
+
data[i + OFFSET_ZOOM] = zoom;
|
|
1350
|
+
const x = coords[index * 2];
|
|
1351
|
+
const y = coords[index * 2 + 1];
|
|
1352
|
+
const numNeighbors = tree.withinInto(x, y, r, neighborIds);
|
|
1353
|
+
const numPointsOrigin = data[i + OFFSET_NUM];
|
|
1354
|
+
let numPoints = numPointsOrigin;
|
|
1355
|
+
for (let n = 0; n < numNeighbors; n++) {
|
|
1356
|
+
const k = neighborIds[n] * stride;
|
|
1357
|
+
if (data[k + OFFSET_ZOOM] > zoom) numPoints += data[k + OFFSET_NUM];
|
|
1358
|
+
}
|
|
1359
|
+
if (numPoints > numPointsOrigin && numPoints >= minPoints) {
|
|
1360
|
+
let wx = x * numPointsOrigin;
|
|
1361
|
+
let wy = y * numPointsOrigin;
|
|
1362
|
+
let clusterProperties;
|
|
1363
|
+
let clusterPropIndex = -1;
|
|
1364
|
+
const id = (index << 5) + (zoom + 1) + this.points.length;
|
|
1365
|
+
for (let n = 0; n < numNeighbors; n++) {
|
|
1366
|
+
const neighborIndex = neighborIds[n];
|
|
1367
|
+
const k = neighborIndex * stride;
|
|
1368
|
+
if (data[k + OFFSET_ZOOM] <= zoom) continue;
|
|
1369
|
+
data[k + OFFSET_ZOOM] = zoom;
|
|
1370
|
+
const numPoints2 = data[k + OFFSET_NUM];
|
|
1371
|
+
wx += coords[neighborIndex * 2] * numPoints2;
|
|
1372
|
+
wy += coords[neighborIndex * 2 + 1] * numPoints2;
|
|
1373
|
+
data[k + OFFSET_PARENT] = id;
|
|
1374
|
+
if (reduce) {
|
|
1375
|
+
if (!clusterProperties) {
|
|
1376
|
+
clusterProperties = this.map(data, i, true);
|
|
1377
|
+
clusterPropIndex = this.clusterProps.length;
|
|
1378
|
+
this.clusterProps.push(clusterProperties);
|
|
1379
|
+
}
|
|
1380
|
+
reduce(clusterProperties, this.map(data, k));
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
data[i + OFFSET_PARENT] = id;
|
|
1384
|
+
nextCoords[nextNumItems * 2] = Math.round(wx / numPoints);
|
|
1385
|
+
nextCoords[nextNumItems * 2 + 1] = Math.round(wy / numPoints);
|
|
1386
|
+
const next = nextNumItems * stride;
|
|
1387
|
+
nextData[next + OFFSET_ZOOM] = notProcessed;
|
|
1388
|
+
nextData[next + OFFSET_ID] = id;
|
|
1389
|
+
nextData[next + OFFSET_PARENT] = -1;
|
|
1390
|
+
nextData[next + OFFSET_NUM] = numPoints;
|
|
1391
|
+
if (reduce) nextData[next + OFFSET_PROP] = clusterPropIndex;
|
|
1392
|
+
nextNumItems++;
|
|
1393
|
+
} else {
|
|
1394
|
+
nextCoords[nextNumItems * 2] = x;
|
|
1395
|
+
nextCoords[nextNumItems * 2 + 1] = y;
|
|
1396
|
+
for (let j = 0; j < stride; j++) nextData[nextNumItems * stride + j] = data[i + j];
|
|
1397
|
+
nextNumItems++;
|
|
1398
|
+
if (numPoints > 1) for (let n = 0; n < numNeighbors; n++) {
|
|
1399
|
+
const neighborIndex = neighborIds[n];
|
|
1400
|
+
const k = neighborIndex * stride;
|
|
1401
|
+
if (data[k + OFFSET_ZOOM] <= zoom) continue;
|
|
1402
|
+
data[k + OFFSET_ZOOM] = zoom;
|
|
1403
|
+
nextCoords[nextNumItems * 2] = coords[neighborIndex * 2];
|
|
1404
|
+
nextCoords[nextNumItems * 2 + 1] = coords[neighborIndex * 2 + 1];
|
|
1405
|
+
for (let j = 0; j < stride; j++) nextData[nextNumItems * stride + j] = data[k + j];
|
|
1406
|
+
nextNumItems++;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return nextNumItems;
|
|
1411
|
+
}
|
|
1412
|
+
getOriginId(clusterId) {
|
|
1413
|
+
return clusterId - this.points.length >> 5;
|
|
1414
|
+
}
|
|
1415
|
+
getOriginZoom(clusterId) {
|
|
1416
|
+
return (clusterId - this.points.length) % 32;
|
|
1417
|
+
}
|
|
1418
|
+
map(data, i, clone) {
|
|
1419
|
+
if (data[i + OFFSET_NUM] > 1) {
|
|
1420
|
+
const props = this.clusterProps[data[i + OFFSET_PROP]];
|
|
1421
|
+
return clone ? Object.assign({}, props) : props;
|
|
1422
|
+
}
|
|
1423
|
+
const original = this.points[data[i + OFFSET_ID]].tags;
|
|
1424
|
+
const result = this.options.map(original);
|
|
1425
|
+
return clone && result === original ? Object.assign({}, result) : result;
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
function createOptions(options) {
|
|
1429
|
+
const result = Object.assign(Object.create(defaultClusterOptions), options);
|
|
1430
|
+
result.minZoom = Math.min(result.minZoom, MAX_ZOOM);
|
|
1431
|
+
result.maxZoom = Math.min(result.maxZoom, MAX_ZOOM);
|
|
1432
|
+
return result;
|
|
1433
|
+
}
|
|
1434
|
+
function getClusterFeature(coords, index, data, i, clusterProps) {
|
|
1435
|
+
return {
|
|
1436
|
+
id: data[i + OFFSET_ID],
|
|
1437
|
+
type: "Point",
|
|
1438
|
+
tags: getClusterProperties(data, i, clusterProps),
|
|
1439
|
+
geometry: [decode(coords[index * 2]), decode(coords[index * 2 + 1])]
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
function getClusterGeoJSON(coords, index, data, i, clusterProps, worldCoordinateHelper) {
|
|
1443
|
+
return {
|
|
1444
|
+
type: "Feature",
|
|
1445
|
+
id: data[i + OFFSET_ID],
|
|
1446
|
+
properties: getClusterProperties(data, i, clusterProps),
|
|
1447
|
+
geometry: {
|
|
1448
|
+
type: "Point",
|
|
1449
|
+
coordinates: [worldCoordinateHelper.unprojectX(decode(coords[index * 2])), worldCoordinateHelper.unprojectY(decode(coords[index * 2 + 1]))]
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function getClusterProperties(data, i, clusterProps) {
|
|
1454
|
+
const count = data[i + OFFSET_NUM];
|
|
1455
|
+
const abbrev = count >= 1e4 ? `${Math.round(count / 1e3)}k` : count >= 1e3 ? `${Math.round(count / 100) / 10}k` : count;
|
|
1456
|
+
const propIndex = data[i + OFFSET_PROP];
|
|
1457
|
+
const properties = propIndex === -1 ? {} : Object.assign({}, clusterProps[propIndex]);
|
|
1458
|
+
return Object.assign(properties, {
|
|
1459
|
+
cluster: true,
|
|
1460
|
+
cluster_id: data[i + OFFSET_ID],
|
|
1461
|
+
point_count: count,
|
|
1462
|
+
point_count_abbreviated: abbrev
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
function isInWorld(geometry) {
|
|
1466
|
+
return geometry[0] >= 0 && geometry[0] <= 1 && geometry[1] >= 0 && geometry[1] <= 1;
|
|
1467
|
+
}
|
|
1468
|
+
var GEOJSONVT_CLIP_START = "geojsonvt_clip_start";
|
|
1469
|
+
var GEOJSONVT_CLIP_END = "geojsonvt_clip_end";
|
|
1470
|
+
function createTile(features, z, tx, ty, options) {
|
|
1471
|
+
const tolerance = z === options.maxZoom ? 0 : options.tolerance / ((1 << z) * options.extent);
|
|
1472
|
+
const tile = {
|
|
1473
|
+
transformed: false,
|
|
1474
|
+
features: [],
|
|
1475
|
+
source: null,
|
|
1476
|
+
x: tx,
|
|
1477
|
+
y: ty,
|
|
1478
|
+
z,
|
|
1479
|
+
minX: 2,
|
|
1480
|
+
minY: 1,
|
|
1481
|
+
maxX: -1,
|
|
1482
|
+
maxY: 0,
|
|
1483
|
+
numPoints: 0,
|
|
1484
|
+
numSimplified: 0,
|
|
1485
|
+
numFeatures: features.length
|
|
1486
|
+
};
|
|
1487
|
+
for (const feature of features) addFeature(tile, feature, tolerance, options);
|
|
1488
|
+
return tile;
|
|
1489
|
+
}
|
|
1490
|
+
function addFeature(tile, feature, tolerance, options) {
|
|
1491
|
+
tile.minX = Math.min(tile.minX, feature.minX);
|
|
1492
|
+
tile.minY = Math.min(tile.minY, feature.minY);
|
|
1493
|
+
tile.maxX = Math.max(tile.maxX, feature.maxX);
|
|
1494
|
+
tile.maxY = Math.max(tile.maxY, feature.maxY);
|
|
1495
|
+
switch (feature.type) {
|
|
1496
|
+
case "Point":
|
|
1497
|
+
case "MultiPoint":
|
|
1498
|
+
addPointsTileFeature(tile, feature);
|
|
1499
|
+
return;
|
|
1500
|
+
case "LineString":
|
|
1501
|
+
addLineTileFeautre(tile, feature, tolerance, options);
|
|
1502
|
+
return;
|
|
1503
|
+
case "MultiLineString":
|
|
1504
|
+
case "Polygon":
|
|
1505
|
+
addLinesTileFeature(tile, feature, tolerance);
|
|
1506
|
+
return;
|
|
1507
|
+
case "MultiPolygon":
|
|
1508
|
+
addMultiPolygonTileFeature(tile, feature, tolerance);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
function addPointsTileFeature(tile, feature) {
|
|
1513
|
+
const geometry = [];
|
|
1514
|
+
for (let i = 0; i < feature.geometry.length; i += 3) {
|
|
1515
|
+
geometry.push(feature.geometry[i], feature.geometry[i + 1]);
|
|
1516
|
+
tile.numPoints++;
|
|
1517
|
+
tile.numSimplified++;
|
|
1518
|
+
}
|
|
1519
|
+
if (!geometry.length) return;
|
|
1520
|
+
const tileFeature = {
|
|
1521
|
+
type: 1,
|
|
1522
|
+
tags: feature.tags || null,
|
|
1523
|
+
geometry
|
|
1524
|
+
};
|
|
1525
|
+
if (feature.id !== null) tileFeature.id = feature.id;
|
|
1526
|
+
tile.features.push(tileFeature);
|
|
1527
|
+
}
|
|
1528
|
+
function addLineTileFeautre(tile, feature, tolerance, options) {
|
|
1529
|
+
const geometry = [];
|
|
1530
|
+
addLine(geometry, feature.geometry, tile, tolerance, false, false);
|
|
1531
|
+
if (!geometry.length) return;
|
|
1532
|
+
let tags = feature.tags || null;
|
|
1533
|
+
if (options.lineMetrics) {
|
|
1534
|
+
tags = {};
|
|
1535
|
+
for (const key in feature.tags) tags[key] = feature.tags[key];
|
|
1536
|
+
tags[GEOJSONVT_CLIP_START] = feature.geometry.start / feature.geometry.size;
|
|
1537
|
+
tags[GEOJSONVT_CLIP_END] = feature.geometry.end / feature.geometry.size;
|
|
1538
|
+
}
|
|
1539
|
+
const tileFeature = {
|
|
1540
|
+
type: 2,
|
|
1541
|
+
tags,
|
|
1542
|
+
geometry
|
|
1543
|
+
};
|
|
1544
|
+
if (feature.id !== null) tileFeature.id = feature.id;
|
|
1545
|
+
tile.features.push(tileFeature);
|
|
1546
|
+
}
|
|
1547
|
+
function addLinesTileFeature(tile, feature, tolerance) {
|
|
1548
|
+
const geometry = [];
|
|
1549
|
+
for (let i = 0; i < feature.geometry.length; i++) addLine(geometry, feature.geometry[i], tile, tolerance, feature.type === "Polygon", i === 0);
|
|
1550
|
+
if (!geometry.length) return;
|
|
1551
|
+
const tileFeature = {
|
|
1552
|
+
type: feature.type === "Polygon" ? 3 : 2,
|
|
1553
|
+
tags: feature.tags || null,
|
|
1554
|
+
geometry
|
|
1555
|
+
};
|
|
1556
|
+
if (feature.id !== null) tileFeature.id = feature.id;
|
|
1557
|
+
tile.features.push(tileFeature);
|
|
1558
|
+
}
|
|
1559
|
+
function addMultiPolygonTileFeature(tile, feature, tolerance) {
|
|
1560
|
+
const geometry = [];
|
|
1561
|
+
for (let k = 0; k < feature.geometry.length; k++) {
|
|
1562
|
+
const polygon = feature.geometry[k];
|
|
1563
|
+
for (let i = 0; i < polygon.length; i++) addLine(geometry, polygon[i], tile, tolerance, true, i === 0);
|
|
1564
|
+
}
|
|
1565
|
+
if (!geometry.length) return;
|
|
1566
|
+
const tileFeature = {
|
|
1567
|
+
type: 3,
|
|
1568
|
+
tags: feature.tags || null,
|
|
1569
|
+
geometry
|
|
1570
|
+
};
|
|
1571
|
+
if (feature.id !== null) tileFeature.id = feature.id;
|
|
1572
|
+
tile.features.push(tileFeature);
|
|
1573
|
+
}
|
|
1574
|
+
function addLine(result, geom, tile, tolerance, isPolygon, isOuter) {
|
|
1575
|
+
const sqTolerance = tolerance * tolerance;
|
|
1576
|
+
if (tolerance > 0 && geom.size < (isPolygon ? sqTolerance : tolerance)) {
|
|
1577
|
+
tile.numPoints += geom.points.length / 3;
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
const ring = [];
|
|
1581
|
+
for (let i = 0; i < geom.points.length; i += 3) {
|
|
1582
|
+
if (tolerance === 0 || geom.points[i + 2] > sqTolerance) {
|
|
1583
|
+
tile.numSimplified++;
|
|
1584
|
+
ring.push(geom.points[i], geom.points[i + 1]);
|
|
1585
|
+
}
|
|
1586
|
+
tile.numPoints++;
|
|
1587
|
+
}
|
|
1588
|
+
if (isPolygon) rewind(ring, isOuter);
|
|
1589
|
+
result.push(ring);
|
|
1590
|
+
}
|
|
1591
|
+
function rewind(ring, clockwise) {
|
|
1592
|
+
let area = 0;
|
|
1593
|
+
for (let i = 0, len = ring.length, j = len - 2; i < len; j = i, i += 2) area += (ring[i] - ring[j]) * (ring[i + 1] + ring[j + 1]);
|
|
1594
|
+
if (area > 0 !== clockwise) return;
|
|
1595
|
+
for (let i = 0, len = ring.length; i < len / 2; i += 2) {
|
|
1596
|
+
const x = ring[i];
|
|
1597
|
+
const y = ring[i + 1];
|
|
1598
|
+
ring[i] = ring[len - 2 - i];
|
|
1599
|
+
ring[i + 1] = ring[len - 1 - i];
|
|
1600
|
+
ring[len - 2 - i] = x;
|
|
1601
|
+
ring[len - 1 - i] = y;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
function transformTile(tile, extent) {
|
|
1605
|
+
if (tile.transformed) return tile;
|
|
1606
|
+
const z2 = 1 << tile.z;
|
|
1607
|
+
const tx = tile.x;
|
|
1608
|
+
const ty = tile.y;
|
|
1609
|
+
for (const feature of tile.features) if (feature.type === 1) transformPointFeature(feature, extent, z2, tx, ty);
|
|
1610
|
+
else transformNonPointFeature(feature, extent, z2, tx, ty);
|
|
1611
|
+
tile.transformed = true;
|
|
1612
|
+
return tile;
|
|
1613
|
+
}
|
|
1614
|
+
function transformPointFeature(feature, extent, z2, tx, ty) {
|
|
1615
|
+
const transformed = feature;
|
|
1616
|
+
const geometry = feature.geometry;
|
|
1617
|
+
const point = [];
|
|
1618
|
+
for (let i = 0; i < geometry.length; i += 2) point.push(transformPoint(geometry[i], geometry[i + 1], extent, z2, tx, ty));
|
|
1619
|
+
transformed.geometry = point;
|
|
1620
|
+
return transformed;
|
|
1621
|
+
}
|
|
1622
|
+
function transformNonPointFeature(feature, extent, z2, tx, ty) {
|
|
1623
|
+
const transformed = feature;
|
|
1624
|
+
const geometry = feature.geometry;
|
|
1625
|
+
const nonPoint = [];
|
|
1626
|
+
for (const geom of geometry) {
|
|
1627
|
+
const ring = [];
|
|
1628
|
+
for (let i = 0; i < geom.length; i += 2) ring.push(transformPoint(geom[i], geom[i + 1], extent, z2, tx, ty));
|
|
1629
|
+
nonPoint.push(ring);
|
|
1630
|
+
}
|
|
1631
|
+
transformed.geometry = nonPoint;
|
|
1632
|
+
return transformed;
|
|
1633
|
+
}
|
|
1634
|
+
function transformPoint(x, y, extent, z2, tx, ty) {
|
|
1635
|
+
return [Math.round(extent * (x * z2 - tx)), Math.round(extent * (y * z2 - ty))];
|
|
1636
|
+
}
|
|
1637
|
+
var TileIndex = class {
|
|
1638
|
+
constructor(options) {
|
|
1639
|
+
this.options = options;
|
|
1640
|
+
this.total = 0;
|
|
1641
|
+
this.stats = {};
|
|
1642
|
+
this.tiles = {};
|
|
1643
|
+
this.tileCoords = [];
|
|
1644
|
+
this.stats = {};
|
|
1645
|
+
this.total = 0;
|
|
1646
|
+
}
|
|
1647
|
+
initialize(features) {
|
|
1648
|
+
this.splitTile(this.clipToWorld(features), 0, 0, 0);
|
|
1649
|
+
if (this.options.debug) {
|
|
1650
|
+
if (features.length) console.log("features: %d, points: %d", this.tiles[0].numFeatures, this.tiles[0].numPoints);
|
|
1651
|
+
console.timeEnd("generate tiles");
|
|
1652
|
+
console.log("tiles generated:", this.total, JSON.stringify(this.stats));
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
/** {@inheritdoc} */
|
|
1656
|
+
updateIndex(source, affected, options) {
|
|
1657
|
+
if (options.debug > 1) {
|
|
1658
|
+
console.log("invalidating tiles");
|
|
1659
|
+
console.time("invalidating");
|
|
1660
|
+
}
|
|
1661
|
+
this.invalidateTiles(affected);
|
|
1662
|
+
if (options.debug > 1) console.timeEnd("invalidating");
|
|
1663
|
+
const [z, x, y] = [
|
|
1664
|
+
0,
|
|
1665
|
+
0,
|
|
1666
|
+
0
|
|
1667
|
+
];
|
|
1668
|
+
const rootSource = this.clipToWorld(source);
|
|
1669
|
+
const rootTile = createTile(rootSource, z, x, y, options);
|
|
1670
|
+
rootTile.source = rootSource;
|
|
1671
|
+
const id = toID(z, x, y);
|
|
1672
|
+
this.tiles[id] = rootTile;
|
|
1673
|
+
this.tileCoords.push({
|
|
1674
|
+
z,
|
|
1675
|
+
x,
|
|
1676
|
+
y,
|
|
1677
|
+
id
|
|
1678
|
+
});
|
|
1679
|
+
if (options.debug) {
|
|
1680
|
+
const key = `z${z}`;
|
|
1681
|
+
this.stats[key] = (this.stats[key] || 0) + 1;
|
|
1682
|
+
this.total++;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
/** {@inheritdoc} */
|
|
1686
|
+
getClusterExpansionZoom(_clusterId) {
|
|
1687
|
+
return null;
|
|
1688
|
+
}
|
|
1689
|
+
/** {@inheritdoc} */
|
|
1690
|
+
getChildren(_clusterId) {
|
|
1691
|
+
return null;
|
|
1692
|
+
}
|
|
1693
|
+
/** {@inheritdoc} */
|
|
1694
|
+
getLeaves(_clusterId, _limit, _offset) {
|
|
1695
|
+
return null;
|
|
1696
|
+
}
|
|
1697
|
+
/** {@inheritdoc} */
|
|
1698
|
+
getTile(z, x, y) {
|
|
1699
|
+
const { extent, debug } = this.options;
|
|
1700
|
+
const z2 = 1 << z;
|
|
1701
|
+
if (!this.options.wrap && (x < 0 || x >= z2)) return null;
|
|
1702
|
+
x = x + z2 & z2 - 1;
|
|
1703
|
+
const id = toID(z, x, y);
|
|
1704
|
+
if (this.tiles[id]) return transformTile(this.tiles[id], extent);
|
|
1705
|
+
if (debug > 1) console.log("drilling down to z%d-%d-%d", z, x, y);
|
|
1706
|
+
let z0 = z;
|
|
1707
|
+
let x0 = x;
|
|
1708
|
+
let y0 = y;
|
|
1709
|
+
let parent;
|
|
1710
|
+
while (!parent && z0 > 0) {
|
|
1711
|
+
z0--;
|
|
1712
|
+
x0 = x0 >> 1;
|
|
1713
|
+
y0 = y0 >> 1;
|
|
1714
|
+
parent = this.tiles[toID(z0, x0, y0)];
|
|
1715
|
+
}
|
|
1716
|
+
if (!parent?.source) return null;
|
|
1717
|
+
if (debug > 1) {
|
|
1718
|
+
console.log("found parent tile z%d-%d-%d", z0, x0, y0);
|
|
1719
|
+
console.time("drilling down");
|
|
1720
|
+
}
|
|
1721
|
+
this.splitTile(parent.source, z0, x0, y0, z, x, y);
|
|
1722
|
+
if (debug > 1) console.timeEnd("drilling down");
|
|
1723
|
+
if (!this.tiles[id]) return null;
|
|
1724
|
+
return transformTile(this.tiles[id], extent);
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* The features the top tile starts from. A world that wraps keeps them all left and right, where wrapping already
|
|
1728
|
+
* put them inside, and one that doesn't only what lies inside the world square and its buffer. Positions in the
|
|
1729
|
+
* unit square are also cut above and below in a world that wraps, where lng/lat is clamped already. The index keeps
|
|
1730
|
+
* the rest, so an update can still move a feature in from outside.
|
|
1731
|
+
*/
|
|
1732
|
+
clipToWorld(features) {
|
|
1733
|
+
const { wrap: wrap2, coordinates } = this.options;
|
|
1734
|
+
if (wrap2 && coordinates !== "unitSquare") return features;
|
|
1735
|
+
const buffer = this.options.buffer / this.options.extent;
|
|
1736
|
+
return clip(wrap2 ? features : clip(features, 1, -buffer, 1 + buffer, 0, -Infinity, Infinity, this.options) || [], 1, -buffer, 1 + buffer, 1, -Infinity, Infinity, this.options) || [];
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* splits features from a parent tile to sub-tiles.
|
|
1740
|
+
* z, x, and y are the coordinates of the parent tile
|
|
1741
|
+
* cz, cx, and cy are the coordinates of the target tile
|
|
1742
|
+
*
|
|
1743
|
+
* If no target tile is specified, splitting stops when we reach the maximum
|
|
1744
|
+
* zoom or the number of points is low as specified in the options.
|
|
1745
|
+
* @internal
|
|
1746
|
+
* @param features - features to split
|
|
1747
|
+
* @param z - tile zoom level
|
|
1748
|
+
* @param x - tile x coordinate
|
|
1749
|
+
* @param y - tile y coordinate
|
|
1750
|
+
* @param cz - target tile zoom level
|
|
1751
|
+
* @param cx - target tile x coordinate
|
|
1752
|
+
* @param cy - target tile y coordinate
|
|
1753
|
+
*/
|
|
1754
|
+
splitTile(features, z, x, y, cz, cx, cy) {
|
|
1755
|
+
const stack = [
|
|
1756
|
+
features,
|
|
1757
|
+
z,
|
|
1758
|
+
x,
|
|
1759
|
+
y
|
|
1760
|
+
];
|
|
1761
|
+
const options = this.options;
|
|
1762
|
+
const debug = options.debug;
|
|
1763
|
+
while (stack.length) {
|
|
1764
|
+
y = stack.pop();
|
|
1765
|
+
x = stack.pop();
|
|
1766
|
+
z = stack.pop();
|
|
1767
|
+
features = stack.pop();
|
|
1768
|
+
const z2 = 1 << z;
|
|
1769
|
+
const id = toID(z, x, y);
|
|
1770
|
+
let tile = this.tiles[id];
|
|
1771
|
+
if (!tile) {
|
|
1772
|
+
if (debug > 1) console.time("creation");
|
|
1773
|
+
tile = this.tiles[id] = createTile(features, z, x, y, options);
|
|
1774
|
+
this.tileCoords.push({
|
|
1775
|
+
z,
|
|
1776
|
+
x,
|
|
1777
|
+
y,
|
|
1778
|
+
id
|
|
1779
|
+
});
|
|
1780
|
+
if (debug) {
|
|
1781
|
+
if (debug > 1) {
|
|
1782
|
+
console.log("tile z%d-%d-%d (features: %d, points: %d, simplified: %d)", z, x, y, tile.numFeatures, tile.numPoints, tile.numSimplified);
|
|
1783
|
+
console.timeEnd("creation");
|
|
1784
|
+
}
|
|
1785
|
+
const key = `z${z}`;
|
|
1786
|
+
this.stats[key] = (this.stats[key] || 0) + 1;
|
|
1787
|
+
this.total++;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
tile.source = features;
|
|
1791
|
+
if (cz == null) {
|
|
1792
|
+
if (z === options.indexMaxZoom || tile.numPoints <= options.indexMaxPoints) continue;
|
|
1793
|
+
} else if (z === options.maxZoom || z === cz) continue;
|
|
1794
|
+
else if (cz != null) {
|
|
1795
|
+
const zoomSteps = cz - z;
|
|
1796
|
+
if (x !== cx >> zoomSteps || y !== cy >> zoomSteps) continue;
|
|
1797
|
+
}
|
|
1798
|
+
tile.source = null;
|
|
1799
|
+
if (!features.length) continue;
|
|
1800
|
+
if (debug > 1) console.time("clipping");
|
|
1801
|
+
const k1 = 0.5 * options.buffer / options.extent;
|
|
1802
|
+
const k2 = 0.5 - k1;
|
|
1803
|
+
const k3 = 0.5 + k1;
|
|
1804
|
+
const k4 = 1 + k1;
|
|
1805
|
+
let tl = null;
|
|
1806
|
+
let bl = null;
|
|
1807
|
+
let tr = null;
|
|
1808
|
+
let br = null;
|
|
1809
|
+
const left = clip(features, z2, x - k1, x + k3, 0, tile.minX, tile.maxX, options);
|
|
1810
|
+
const right = clip(features, z2, x + k2, x + k4, 0, tile.minX, tile.maxX, options);
|
|
1811
|
+
if (left) {
|
|
1812
|
+
tl = clip(left, z2, y - k1, y + k3, 1, tile.minY, tile.maxY, options);
|
|
1813
|
+
bl = clip(left, z2, y + k2, y + k4, 1, tile.minY, tile.maxY, options);
|
|
1814
|
+
}
|
|
1815
|
+
if (right) {
|
|
1816
|
+
tr = clip(right, z2, y - k1, y + k3, 1, tile.minY, tile.maxY, options);
|
|
1817
|
+
br = clip(right, z2, y + k2, y + k4, 1, tile.minY, tile.maxY, options);
|
|
1818
|
+
}
|
|
1819
|
+
if (debug > 1) console.timeEnd("clipping");
|
|
1820
|
+
stack.push(tl || [], z + 1, x * 2, y * 2);
|
|
1821
|
+
stack.push(bl || [], z + 1, x * 2, y * 2 + 1);
|
|
1822
|
+
stack.push(tr || [], z + 1, x * 2 + 1, y * 2);
|
|
1823
|
+
stack.push(br || [], z + 1, x * 2 + 1, y * 2 + 1);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Invalidates (removes) tiles affected by the provided features
|
|
1828
|
+
* @internal
|
|
1829
|
+
* @param features
|
|
1830
|
+
*/
|
|
1831
|
+
invalidateTiles(features) {
|
|
1832
|
+
if (!features.length) return;
|
|
1833
|
+
const options = this.options;
|
|
1834
|
+
const { debug } = options;
|
|
1835
|
+
let minX = Infinity;
|
|
1836
|
+
let maxX = -Infinity;
|
|
1837
|
+
let minY = Infinity;
|
|
1838
|
+
let maxY = -Infinity;
|
|
1839
|
+
for (const feature of features) {
|
|
1840
|
+
minX = Math.min(minX, feature.minX);
|
|
1841
|
+
maxX = Math.max(maxX, feature.maxX);
|
|
1842
|
+
minY = Math.min(minY, feature.minY);
|
|
1843
|
+
maxY = Math.max(maxY, feature.maxY);
|
|
1844
|
+
}
|
|
1845
|
+
const k1 = options.buffer / options.extent;
|
|
1846
|
+
const removedLookup = /* @__PURE__ */ new Set();
|
|
1847
|
+
for (const id in this.tiles) {
|
|
1848
|
+
const tile = this.tiles[id];
|
|
1849
|
+
const z2 = 1 << tile.z;
|
|
1850
|
+
const tileMinX = (tile.x - k1) / z2;
|
|
1851
|
+
const tileMaxX = (tile.x + 1 + k1) / z2;
|
|
1852
|
+
const tileMinY = (tile.y - k1) / z2;
|
|
1853
|
+
const tileMaxY = (tile.y + 1 + k1) / z2;
|
|
1854
|
+
if (maxX < tileMinX || minX >= tileMaxX || maxY < tileMinY || minY >= tileMaxY) continue;
|
|
1855
|
+
let intersects = false;
|
|
1856
|
+
for (const feature of features) if (feature.maxX >= tileMinX && feature.minX < tileMaxX && feature.maxY >= tileMinY && feature.minY < tileMaxY) {
|
|
1857
|
+
intersects = true;
|
|
1858
|
+
break;
|
|
1859
|
+
}
|
|
1860
|
+
if (!intersects) continue;
|
|
1861
|
+
if (debug) {
|
|
1862
|
+
if (debug > 1) console.log("invalidate tile z%d-%d-%d (features: %d, points: %d, simplified: %d)", tile.z, tile.x, tile.y, tile.numFeatures, tile.numPoints, tile.numSimplified);
|
|
1863
|
+
const key = `z${tile.z}`;
|
|
1864
|
+
this.stats[key] = (this.stats[key] || 0) - 1;
|
|
1865
|
+
this.total--;
|
|
1866
|
+
}
|
|
1867
|
+
delete this.tiles[id];
|
|
1868
|
+
removedLookup.add(id);
|
|
1869
|
+
}
|
|
1870
|
+
if (removedLookup.size) this.tileCoords = this.tileCoords.filter((c) => !removedLookup.has(c.id));
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
function toID(z, x, y) {
|
|
1874
|
+
return ((1 << z) * y + x) * 32 + z;
|
|
1875
|
+
}
|
|
1876
|
+
var defaultOptions = {
|
|
1877
|
+
maxZoom: 14,
|
|
1878
|
+
indexMaxZoom: 5,
|
|
1879
|
+
indexMaxPoints: 1e5,
|
|
1880
|
+
tolerance: 3,
|
|
1881
|
+
extent: 4096,
|
|
1882
|
+
buffer: 64,
|
|
1883
|
+
lineMetrics: false,
|
|
1884
|
+
promoteId: null,
|
|
1885
|
+
generateId: false,
|
|
1886
|
+
updateable: false,
|
|
1887
|
+
cluster: false,
|
|
1888
|
+
clusterOptions: defaultClusterOptions,
|
|
1889
|
+
debug: 0,
|
|
1890
|
+
coordinates: "lngLat",
|
|
1891
|
+
wrap: true
|
|
1892
|
+
};
|
|
1893
|
+
var GeoJSONVT = class {
|
|
1894
|
+
constructor(data, options) {
|
|
1895
|
+
options = this.options = Object.assign({}, defaultOptions, options);
|
|
1896
|
+
this.worldCoordinateHelper = getWorldCoordinateHelper(options.coordinates);
|
|
1897
|
+
const debug = options.debug;
|
|
1898
|
+
if (debug) console.time("preprocess data");
|
|
1899
|
+
if (options.maxZoom < 0 || options.maxZoom > 24) throw new Error("maxZoom should be in the 0-24 range");
|
|
1900
|
+
if (options.promoteId && options.generateId) throw new Error("promoteId and generateId cannot be used together.");
|
|
1901
|
+
if (options.updateable) this.unlocated = /* @__PURE__ */ new Map();
|
|
1902
|
+
let features = convertToInternal(data, options, this.unlocated);
|
|
1903
|
+
if (debug) {
|
|
1904
|
+
console.timeEnd("preprocess data");
|
|
1905
|
+
console.log("index: maxZoom: %d, maxPoints: %d", options.indexMaxZoom, options.indexMaxPoints);
|
|
1906
|
+
console.time("generate tiles");
|
|
1907
|
+
}
|
|
1908
|
+
features = wrap(features, options);
|
|
1909
|
+
if (options.updateable) this.source = features;
|
|
1910
|
+
this.initializeIndex(features, options);
|
|
1911
|
+
}
|
|
1912
|
+
initializeIndex(features, options) {
|
|
1913
|
+
this.tileIndex = options.cluster ? new ClusterTileIndex({
|
|
1914
|
+
...options.clusterOptions,
|
|
1915
|
+
coordinates: options.coordinates,
|
|
1916
|
+
wrap: options.wrap
|
|
1917
|
+
}) : new TileIndex(options);
|
|
1918
|
+
if (!features.length) return;
|
|
1919
|
+
this.tileIndex.initialize(features);
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Given z, x, and y tile coordinates, returns the corresponding tile with geometries in tile coordinates, much like MVT data is stored.
|
|
1923
|
+
* @param z - tile zoom level
|
|
1924
|
+
* @param x - tile x coordinate
|
|
1925
|
+
* @param y - tile y coordinate
|
|
1926
|
+
* @returns the transformed tile or null if not found
|
|
1927
|
+
*/
|
|
1928
|
+
getTile(z, x, y) {
|
|
1929
|
+
z = +z;
|
|
1930
|
+
x = +x;
|
|
1931
|
+
y = +y;
|
|
1932
|
+
if (z < 0 || z > 24) return null;
|
|
1933
|
+
return this.tileIndex.getTile(z, x, y);
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Updates the source data feature set using a {@link GeoJSONVTSourceDiff}
|
|
1937
|
+
* @param diff - the source diff object
|
|
1938
|
+
*/
|
|
1939
|
+
updateData(diff, filter) {
|
|
1940
|
+
const options = this.options;
|
|
1941
|
+
if (!options.updateable) throw new Error("to update tile geojson `updateable` option must be set to true");
|
|
1942
|
+
let { affected, source } = applySourceDiff(this.source, diff, options, this.unlocated);
|
|
1943
|
+
if (filter) ({ affected, source } = this.filterUpdate(source, affected, filter));
|
|
1944
|
+
if (!affected.length) return;
|
|
1945
|
+
this.source = source;
|
|
1946
|
+
this.tileIndex.updateIndex(source, affected, options);
|
|
1947
|
+
}
|
|
1948
|
+
/**
|
|
1949
|
+
* Filter an update using a predicate function, the features an update emptied included, so that one the filter
|
|
1950
|
+
* rejects can't come back with a later update. A feature without a geometry at all isn't passed to the predicate.
|
|
1951
|
+
* Returns the affected and updated source features.
|
|
1952
|
+
*/
|
|
1953
|
+
filterUpdate(source, affected, predicate) {
|
|
1954
|
+
const removeIds = /* @__PURE__ */ new Set();
|
|
1955
|
+
for (const feature of source) {
|
|
1956
|
+
if (feature.id == void 0) continue;
|
|
1957
|
+
if (predicate(featureToGeoJSON(feature, this.worldCoordinateHelper))) continue;
|
|
1958
|
+
affected.push(feature);
|
|
1959
|
+
removeIds.add(feature.id);
|
|
1960
|
+
}
|
|
1961
|
+
source = source.filter((feature) => !removeIds.has(feature.id));
|
|
1962
|
+
for (const [id, feature] of this.unlocated ?? []) if (feature.geometry && !predicate(feature)) this.unlocated.delete(id);
|
|
1963
|
+
return {
|
|
1964
|
+
affected,
|
|
1965
|
+
source
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Returns source data as GeoJSON - only available when `updateable` option is set to true.
|
|
1970
|
+
*/
|
|
1971
|
+
getData() {
|
|
1972
|
+
if (!this.options.updateable) throw new Error("to retrieve data the `updateable` option must be set to true");
|
|
1973
|
+
return convertToGeoJSON(this.source, this.worldCoordinateHelper);
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Update supercluster options and regenerate the index.
|
|
1977
|
+
* @param cluster - whether to enable clustering
|
|
1978
|
+
* @param clusterOptions - {@link SuperclusterOptions} without `coordinates` and `wrap`, which come from the index options
|
|
1979
|
+
*/
|
|
1980
|
+
updateClusterOptions(cluster, clusterOptions) {
|
|
1981
|
+
const wasCluster = this.options.cluster;
|
|
1982
|
+
this.options.cluster = cluster;
|
|
1983
|
+
this.options.clusterOptions = clusterOptions;
|
|
1984
|
+
if (wasCluster == cluster) {
|
|
1985
|
+
this.tileIndex.updateIndex(this.source, [], this.options);
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
this.initializeIndex(this.source, this.options);
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Returns the zoom level at which a cluster expands into multiple children.
|
|
1992
|
+
* @param clusterId - The target cluster id.
|
|
1993
|
+
* @returns the expansion zoom or null in case of non-clustered source
|
|
1994
|
+
*/
|
|
1995
|
+
getClusterExpansionZoom(clusterId) {
|
|
1996
|
+
return this.tileIndex.getClusterExpansionZoom(clusterId);
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Returns the immediate children (clusters or points) of a cluster as GeoJSON.
|
|
2000
|
+
* @param clusterId - The target cluster id.
|
|
2001
|
+
* @returns the immediate children or null in case of non-clustered source
|
|
2002
|
+
*/
|
|
2003
|
+
getClusterChildren(clusterId) {
|
|
2004
|
+
return this.tileIndex.getChildren(clusterId);
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Returns leaf point features under a cluster, paginated by `limit` and `offset`.
|
|
2008
|
+
* @param clusterId - The target cluster id.
|
|
2009
|
+
* @param limit - Maximum number of points to return (defaults to `10`).
|
|
2010
|
+
* @param offset - Number of points to skip before collecting results (defaults to `0`).
|
|
2011
|
+
* @returns leaf point features under a cluster or null in case of non-clustered source
|
|
2012
|
+
*/
|
|
2013
|
+
getClusterLeaves(clusterId, limit, offset) {
|
|
2014
|
+
return this.tileIndex.getLeaves(clusterId, limit, offset);
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
|
|
2018
|
+
// src/geojson-tiler.ts
|
|
2019
|
+
var GEOMETRY_DEPTHS = {
|
|
2020
|
+
Point: 0,
|
|
2021
|
+
MultiPoint: 1,
|
|
2022
|
+
LineString: 1,
|
|
2023
|
+
MultiLineString: 2,
|
|
2024
|
+
Polygon: 2,
|
|
2025
|
+
MultiPolygon: 3
|
|
2026
|
+
};
|
|
2027
|
+
function createTileIndex(data, options) {
|
|
2028
|
+
return new GeoJSONVT(data, options);
|
|
2029
|
+
}
|
|
2030
|
+
function getSignedArea(ring) {
|
|
2031
|
+
let sum = 0;
|
|
2032
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
2033
|
+
sum += (ring[j][0] - ring[i][0]) * (ring[i][1] + ring[j][1]);
|
|
2034
|
+
}
|
|
2035
|
+
return sum;
|
|
2036
|
+
}
|
|
2037
|
+
function classifyRings(rings) {
|
|
2038
|
+
const polygons = [];
|
|
2039
|
+
let polygon;
|
|
2040
|
+
let clockwise;
|
|
2041
|
+
for (const ring of rings) {
|
|
2042
|
+
const area = getSignedArea(ring);
|
|
2043
|
+
if (area === 0) {
|
|
2044
|
+
continue;
|
|
2045
|
+
}
|
|
2046
|
+
clockwise ??= area < 0;
|
|
2047
|
+
if (clockwise === area < 0) {
|
|
2048
|
+
polygon = [ring];
|
|
2049
|
+
polygons.push(polygon);
|
|
2050
|
+
} else {
|
|
2051
|
+
polygon?.push(ring);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
return polygons;
|
|
2055
|
+
}
|
|
2056
|
+
function getGeometry(feature) {
|
|
2057
|
+
if (feature.type === 1) {
|
|
2058
|
+
const points = feature.geometry;
|
|
2059
|
+
return points.length === 1 ? { type: "Point", coordinates: points[0] } : { type: "MultiPoint", coordinates: points };
|
|
2060
|
+
}
|
|
2061
|
+
if (feature.type === 2) {
|
|
2062
|
+
const lines = feature.geometry;
|
|
2063
|
+
return lines.length === 1 ? { type: "LineString", coordinates: lines[0] } : { type: "MultiLineString", coordinates: lines };
|
|
2064
|
+
}
|
|
2065
|
+
const polygons = classifyRings(feature.geometry);
|
|
2066
|
+
if (polygons.length === 0) {
|
|
2067
|
+
return null;
|
|
2068
|
+
}
|
|
2069
|
+
return polygons.length === 1 ? { type: "Polygon", coordinates: polygons[0] } : { type: "MultiPolygon", coordinates: polygons };
|
|
2070
|
+
}
|
|
2071
|
+
function cutTile(index, z, x, y) {
|
|
2072
|
+
const features = [];
|
|
2073
|
+
const positions = [];
|
|
2074
|
+
const lengths = [];
|
|
2075
|
+
const writeCoordinates = (coordinates, depth) => {
|
|
2076
|
+
if (depth === 0) {
|
|
2077
|
+
positions.push(coordinates[0], coordinates[1]);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
lengths.push(coordinates.length);
|
|
2081
|
+
for (const child of coordinates) {
|
|
2082
|
+
writeCoordinates(child, depth - 1);
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
for (const feature of index.getTile(z, x, y)?.features || []) {
|
|
2086
|
+
const geometry = getGeometry(feature);
|
|
2087
|
+
if (!geometry) {
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
writeCoordinates(geometry.coordinates, GEOMETRY_DEPTHS[geometry.type]);
|
|
2091
|
+
const encoded = { type: geometry.type, properties: feature.tags || {} };
|
|
2092
|
+
if (feature.id !== void 0) {
|
|
2093
|
+
encoded.id = feature.id;
|
|
2094
|
+
}
|
|
2095
|
+
features.push(encoded);
|
|
2096
|
+
}
|
|
2097
|
+
return { features, positions: Int32Array.from(positions), lengths: Uint32Array.from(lengths) };
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// src/geojson-worker.ts
|
|
2101
|
+
var pendingFeatures = /* @__PURE__ */ new Map();
|
|
2102
|
+
var indexes = /* @__PURE__ */ new Map();
|
|
2103
|
+
var scope = globalThis;
|
|
2104
|
+
scope.onmessage = ({ data: message }) => {
|
|
2105
|
+
switch (message.type) {
|
|
2106
|
+
case "features": {
|
|
2107
|
+
const features = pendingFeatures.get(message.source);
|
|
2108
|
+
if (features) {
|
|
2109
|
+
for (const feature of message.features) {
|
|
2110
|
+
features.push(feature);
|
|
2111
|
+
}
|
|
2112
|
+
} else {
|
|
2113
|
+
pendingFeatures.set(message.source, message.features);
|
|
2114
|
+
}
|
|
2115
|
+
break;
|
|
2116
|
+
}
|
|
2117
|
+
case "index": {
|
|
2118
|
+
const data = message.data ?? {
|
|
2119
|
+
type: "FeatureCollection",
|
|
2120
|
+
features: pendingFeatures.get(message.source) || []
|
|
2121
|
+
};
|
|
2122
|
+
pendingFeatures.delete(message.source);
|
|
2123
|
+
try {
|
|
2124
|
+
indexes.set(message.source, createTileIndex(data, message.options));
|
|
2125
|
+
} catch (error) {
|
|
2126
|
+
indexes.set(message.source, error instanceof Error ? error : new Error(String(error)));
|
|
2127
|
+
}
|
|
2128
|
+
break;
|
|
2129
|
+
}
|
|
2130
|
+
case "tile": {
|
|
2131
|
+
const index = indexes.get(message.source);
|
|
2132
|
+
try {
|
|
2133
|
+
if (!index) {
|
|
2134
|
+
throw new Error("GeoJSON source has no index");
|
|
2135
|
+
}
|
|
2136
|
+
if (index instanceof Error) {
|
|
2137
|
+
throw index;
|
|
2138
|
+
}
|
|
2139
|
+
const tile = cutTile(index, message.z, message.x, message.y);
|
|
2140
|
+
scope.postMessage({ request: message.request, tile }, [
|
|
2141
|
+
tile.positions.buffer,
|
|
2142
|
+
tile.lengths.buffer
|
|
2143
|
+
]);
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
scope.postMessage(
|
|
2146
|
+
{ request: message.request, error: error instanceof Error ? error.message : String(error) },
|
|
2147
|
+
[]
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
break;
|
|
2151
|
+
}
|
|
2152
|
+
case "remove":
|
|
2153
|
+
pendingFeatures.delete(message.source);
|
|
2154
|
+
indexes.delete(message.source);
|
|
2155
|
+
break;
|
|
2156
|
+
default:
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
})();
|
|
2160
|
+
//# sourceMappingURL=geojson-worker.js.map
|