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