@mapwhit/tilerenderer 1.2.2 → 1.4.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/build/min/package.json +1 -1
- package/package.json +1 -1
- package/src/data/array_types.js +115 -64
- package/src/data/bucket/circle_bucket.js +42 -5
- package/src/data/bucket/fill_bucket.js +31 -13
- package/src/data/bucket/fill_extrusion_bucket.js +8 -6
- package/src/data/bucket/line_bucket.js +38 -14
- package/src/data/bucket/symbol_attributes.js +13 -5
- package/src/data/bucket/symbol_bucket.js +87 -33
- package/src/data/bucket/symbol_collision_buffers.js +1 -1
- package/src/data/bucket.js +3 -1
- package/src/data/feature_index.js +24 -11
- package/src/data/segment.js +15 -7
- package/src/render/draw_circle.js +45 -4
- package/src/render/draw_symbol.js +190 -22
- package/src/render/painter.js +1 -1
- package/src/source/geojson_source.js +118 -21
- package/src/source/geojson_source_diff.js +148 -0
- package/src/source/geojson_tiler.js +89 -0
- package/src/source/source.js +16 -5
- package/src/source/source_cache.js +6 -6
- package/src/source/source_state.js +4 -2
- package/src/source/tile.js +5 -3
- package/src/source/vector_tile_source.js +2 -0
- package/src/source/worker_tile.js +4 -2
- package/src/style/pauseable_placement.js +39 -7
- package/src/style/style.js +86 -34
- package/src/style/style_layer/circle_style_layer_properties.js +8 -1
- package/src/style/style_layer/fill_style_layer_properties.js +8 -1
- package/src/style/style_layer/line_style_layer_properties.js +4 -0
- package/src/style/style_layer/symbol_style_layer_properties.js +17 -2
- package/src/style-spec/reference/v8.json +161 -4
- package/src/symbol/one_em.js +4 -0
- package/src/symbol/placement.js +406 -173
- package/src/symbol/projection.js +3 -3
- package/src/symbol/quads.js +1 -6
- package/src/symbol/shaping.js +16 -27
- package/src/symbol/symbol_layout.js +243 -81
- package/src/util/vectortile_to_geojson.js +3 -4
- package/src/source/geojson_worker_source.js +0 -97
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import glMatrix from '@mapbox/gl-matrix';
|
|
2
|
+
import { addDynamicAttributes } from '../data/bucket/symbol_bucket.js';
|
|
3
|
+
import SegmentVector from '../data/segment.js';
|
|
2
4
|
import CullFaceMode from '../gl/cull_face_mode.js';
|
|
3
5
|
import DepthMode from '../gl/depth_mode.js';
|
|
4
6
|
import StencilMode from '../gl/stencil_mode.js';
|
|
5
7
|
import pixelsToTileUnits from '../source/pixels_to_tile_units.js';
|
|
6
8
|
import properties from '../style/style_layer/symbol_style_layer_properties.js';
|
|
9
|
+
import ONE_EM from '../symbol/one_em.js';
|
|
7
10
|
import * as symbolProjection from '../symbol/projection.js';
|
|
11
|
+
import { getAnchorAlignment } from '../symbol/shaping.js';
|
|
12
|
+
import { evaluateRadialOffset } from '../symbol/symbol_layout.js';
|
|
8
13
|
import * as symbolSize from '../symbol/symbol_size.js';
|
|
9
14
|
import drawCollisionDebug from './draw_collision_debug.js';
|
|
10
15
|
import { symbolIconUniformValues, symbolSDFUniformValues } from './program/symbol_program.js';
|
|
@@ -13,7 +18,7 @@ const { mat4 } = glMatrix;
|
|
|
13
18
|
const identityMat4 = mat4.identity(new Float32Array(16));
|
|
14
19
|
const symbolLayoutProperties = properties.layout;
|
|
15
20
|
|
|
16
|
-
export default function drawSymbols(painter, sourceCache, layer, coords) {
|
|
21
|
+
export default function drawSymbols(painter, sourceCache, layer, coords, variableOffsets) {
|
|
17
22
|
if (painter.renderPass !== 'translucent') {
|
|
18
23
|
return;
|
|
19
24
|
}
|
|
@@ -35,7 +40,8 @@ export default function drawSymbols(painter, sourceCache, layer, coords) {
|
|
|
35
40
|
layer._layout.get('icon-pitch-alignment'),
|
|
36
41
|
layer._layout.get('icon-keep-upright'),
|
|
37
42
|
stencilMode,
|
|
38
|
-
colorMode
|
|
43
|
+
colorMode,
|
|
44
|
+
variableOffsets
|
|
39
45
|
);
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -52,7 +58,8 @@ export default function drawSymbols(painter, sourceCache, layer, coords) {
|
|
|
52
58
|
layer._layout.get('text-pitch-alignment'),
|
|
53
59
|
layer._layout.get('text-keep-upright'),
|
|
54
60
|
stencilMode,
|
|
55
|
-
colorMode
|
|
61
|
+
colorMode,
|
|
62
|
+
variableOffsets
|
|
56
63
|
);
|
|
57
64
|
}
|
|
58
65
|
|
|
@@ -61,6 +68,77 @@ export default function drawSymbols(painter, sourceCache, layer, coords) {
|
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
function calculateVariableRenderShift(anchor, width, height, radialOffset, textBoxScale, renderTextSize) {
|
|
72
|
+
const { horizontalAlign, verticalAlign } = getAnchorAlignment(anchor);
|
|
73
|
+
const shiftX = -(horizontalAlign - 0.5) * width;
|
|
74
|
+
const shiftY = -(verticalAlign - 0.5) * height;
|
|
75
|
+
const offset = evaluateRadialOffset(anchor, radialOffset);
|
|
76
|
+
return {
|
|
77
|
+
x: (shiftX / textBoxScale + offset[0]) * renderTextSize,
|
|
78
|
+
y: (shiftY / textBoxScale + offset[1]) * renderTextSize
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function updateVariableAnchors(
|
|
83
|
+
bucket,
|
|
84
|
+
rotateWithMap,
|
|
85
|
+
pitchWithMap,
|
|
86
|
+
variableOffsets,
|
|
87
|
+
symbolSize,
|
|
88
|
+
transform,
|
|
89
|
+
labelPlaneMatrix,
|
|
90
|
+
posMatrix,
|
|
91
|
+
tileScale,
|
|
92
|
+
size
|
|
93
|
+
) {
|
|
94
|
+
const placedSymbols = bucket.text.placedSymbolArray;
|
|
95
|
+
const dynamicLayoutVertexArray = bucket.text.dynamicLayoutVertexArray;
|
|
96
|
+
dynamicLayoutVertexArray.clear();
|
|
97
|
+
for (let s = 0; s < placedSymbols.length; s++) {
|
|
98
|
+
const symbol = placedSymbols.get(s);
|
|
99
|
+
const variableOffset = !symbol.hidden && symbol.crossTileID ? variableOffsets[symbol.crossTileID] : null;
|
|
100
|
+
if (!variableOffset) {
|
|
101
|
+
// These symbols are from a justification that is not being used, or a label that wasn't placed
|
|
102
|
+
// so we don't need to do the extra math to figure out what incremental shift to apply.
|
|
103
|
+
symbolProjection.hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray);
|
|
104
|
+
} else {
|
|
105
|
+
const tileAnchor = { x: symbol.anchorX, y: symbol.anchorY };
|
|
106
|
+
const projectedAnchor = symbolProjection.project(tileAnchor, pitchWithMap ? posMatrix : labelPlaneMatrix);
|
|
107
|
+
const perspectiveRatio =
|
|
108
|
+
0.5 + 0.5 * (transform.cameraToCenterDistance / projectedAnchor.signedDistanceFromCamera);
|
|
109
|
+
let renderTextSize =
|
|
110
|
+
(symbolSize.evaluateSizeForFeature(bucket.textSizeData, size, symbol) * perspectiveRatio) / ONE_EM;
|
|
111
|
+
if (pitchWithMap) {
|
|
112
|
+
// Go from size in pixels to equivalent size in tile units
|
|
113
|
+
renderTextSize *= bucket.tilePixelRatio / tileScale;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { width, height, radialOffset, textBoxScale } = variableOffset;
|
|
117
|
+
|
|
118
|
+
const shift = calculateVariableRenderShift(
|
|
119
|
+
variableOffset.anchor,
|
|
120
|
+
width,
|
|
121
|
+
height,
|
|
122
|
+
radialOffset,
|
|
123
|
+
textBoxScale,
|
|
124
|
+
renderTextSize
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Usual case is that we take the projected anchor and add the pixel-based shift
|
|
128
|
+
// calculated above. In the (somewhat weird) case of pitch-aligned text, we add an equivalent
|
|
129
|
+
// tile-unit based shift to the anchor before projecting to the label plane.
|
|
130
|
+
const shiftedAnchor = pitchWithMap
|
|
131
|
+
? symbolProjection.project(tileAnchor.add(shift), labelPlaneMatrix).point
|
|
132
|
+
: projectedAnchor.point.add(rotateWithMap ? shift.rotate(-transform.angle) : shift);
|
|
133
|
+
|
|
134
|
+
for (let g = 0; g < symbol.numGlyphs; g++) {
|
|
135
|
+
addDynamicAttributes(dynamicLayoutVertexArray, shiftedAnchor, 0);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
bucket.text.dynamicLayoutVertexBuffer.updateData(dynamicLayoutVertexArray);
|
|
140
|
+
}
|
|
141
|
+
|
|
64
142
|
function drawLayerSymbols(
|
|
65
143
|
painter,
|
|
66
144
|
sourceCache,
|
|
@@ -73,7 +151,8 @@ function drawLayerSymbols(
|
|
|
73
151
|
pitchAlignment,
|
|
74
152
|
keepUpright,
|
|
75
153
|
stencilMode,
|
|
76
|
-
colorMode
|
|
154
|
+
colorMode,
|
|
155
|
+
variableOffsets
|
|
77
156
|
) {
|
|
78
157
|
const context = painter.context;
|
|
79
158
|
const gl = context.gl;
|
|
@@ -87,10 +166,15 @@ function drawLayerSymbols(
|
|
|
87
166
|
// Unpitched point labels need to have their rotation applied after projection
|
|
88
167
|
const rotateInShader = rotateWithMap && !pitchWithMap && !alongLine;
|
|
89
168
|
|
|
169
|
+
const sortFeaturesByKey = layer._layout.get('symbol-sort-key').constantOr(1) !== undefined;
|
|
170
|
+
|
|
90
171
|
const depthMode = painter.depthModeForSublayer(0, DepthMode.ReadOnly);
|
|
91
172
|
|
|
92
173
|
let program;
|
|
93
174
|
let size;
|
|
175
|
+
const variablePlacement = layer._layout.get('text-variable-anchor');
|
|
176
|
+
|
|
177
|
+
const tileRenderState = [];
|
|
94
178
|
|
|
95
179
|
for (const coord of coords) {
|
|
96
180
|
const tile = sourceCache.getTile(coord);
|
|
@@ -120,20 +204,21 @@ function drawLayerSymbols(
|
|
|
120
204
|
context.activeTexture.set(gl.TEXTURE0);
|
|
121
205
|
|
|
122
206
|
let texSize;
|
|
207
|
+
let atlasTexture;
|
|
208
|
+
let atlasInterpolation;
|
|
123
209
|
if (isText) {
|
|
124
|
-
tile.glyphAtlasTexture
|
|
210
|
+
atlasTexture = tile.glyphAtlasTexture;
|
|
211
|
+
atlasInterpolation = gl.LINEAR;
|
|
125
212
|
texSize = tile.glyphAtlasTexture.size;
|
|
126
213
|
} else {
|
|
127
214
|
const iconScaled = layer._layout.get('icon-size').constantOr(0) !== 1 || bucket.iconsNeedLinear;
|
|
128
215
|
const iconTransformed = pitchWithMap || tr.pitch !== 0;
|
|
129
216
|
|
|
130
|
-
tile.imageAtlasTexture
|
|
217
|
+
atlasTexture = tile.imageAtlasTexture;
|
|
218
|
+
atlasInterpolation =
|
|
131
219
|
isSDF || painter.options.rotating || painter.options.zooming || iconScaled || iconTransformed
|
|
132
220
|
? gl.LINEAR
|
|
133
|
-
: gl.NEAREST
|
|
134
|
-
gl.CLAMP_TO_EDGE
|
|
135
|
-
);
|
|
136
|
-
|
|
221
|
+
: gl.NEAREST;
|
|
137
222
|
texSize = tile.imageAtlasTexture.size;
|
|
138
223
|
}
|
|
139
224
|
|
|
@@ -164,16 +249,30 @@ function drawLayerSymbols(
|
|
|
164
249
|
pitchWithMap,
|
|
165
250
|
keepUpright
|
|
166
251
|
);
|
|
252
|
+
} else if (isText && size && variablePlacement) {
|
|
253
|
+
const tileScale = 2 ** (tr.zoom - tile.tileID.overscaledZ);
|
|
254
|
+
updateVariableAnchors(
|
|
255
|
+
bucket,
|
|
256
|
+
rotateWithMap,
|
|
257
|
+
pitchWithMap,
|
|
258
|
+
variableOffsets,
|
|
259
|
+
symbolSize,
|
|
260
|
+
tr,
|
|
261
|
+
labelPlaneMatrix,
|
|
262
|
+
coord.posMatrix,
|
|
263
|
+
tileScale,
|
|
264
|
+
size
|
|
265
|
+
);
|
|
167
266
|
}
|
|
168
267
|
|
|
169
268
|
const matrix = painter.translatePosMatrix(coord.posMatrix, tile, translate, translateAnchor);
|
|
170
|
-
const uLabelPlaneMatrix = alongLine ? identityMat4 : labelPlaneMatrix;
|
|
269
|
+
const uLabelPlaneMatrix = alongLine || (isText && variablePlacement) ? identityMat4 : labelPlaneMatrix;
|
|
171
270
|
const uglCoordMatrix = painter.translatePosMatrix(glCoordMatrix, tile, translate, translateAnchor, true);
|
|
172
271
|
|
|
272
|
+
const hasHalo = isSDF && layer._paint.get(isText ? 'text-halo-width' : 'icon-halo-width').constantOr(1) !== 0;
|
|
273
|
+
|
|
173
274
|
let uniformValues;
|
|
174
275
|
if (isSDF) {
|
|
175
|
-
const hasHalo = layer._paint.get(isText ? 'text-halo-width' : 'icon-halo-width').constantOr(1) !== 0;
|
|
176
|
-
|
|
177
276
|
uniformValues = symbolSDFUniformValues(
|
|
178
277
|
sizeData.functionType,
|
|
179
278
|
size,
|
|
@@ -187,12 +286,6 @@ function drawLayerSymbols(
|
|
|
187
286
|
texSize,
|
|
188
287
|
true
|
|
189
288
|
);
|
|
190
|
-
|
|
191
|
-
if (hasHalo) {
|
|
192
|
-
drawSymbolElements(buffers, layer, painter, program, depthMode, stencilMode, colorMode, uniformValues);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
uniformValues['u_is_halo'] = 0;
|
|
196
289
|
} else {
|
|
197
290
|
uniformValues = symbolIconUniformValues(
|
|
198
291
|
sizeData.functionType,
|
|
@@ -208,11 +301,86 @@ function drawLayerSymbols(
|
|
|
208
301
|
);
|
|
209
302
|
}
|
|
210
303
|
|
|
211
|
-
|
|
304
|
+
const state = {
|
|
305
|
+
program,
|
|
306
|
+
buffers,
|
|
307
|
+
uniformValues,
|
|
308
|
+
atlasTexture,
|
|
309
|
+
atlasInterpolation,
|
|
310
|
+
isSDF,
|
|
311
|
+
hasHalo
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
if (sortFeaturesByKey) {
|
|
315
|
+
const oldSegments = buffers.segments.get();
|
|
316
|
+
for (const segment of oldSegments) {
|
|
317
|
+
tileRenderState.push({
|
|
318
|
+
segments: new SegmentVector([segment]),
|
|
319
|
+
sortKey: segment.sortKey,
|
|
320
|
+
state
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
tileRenderState.push({
|
|
325
|
+
segments: buffers.segments,
|
|
326
|
+
sortKey: 0,
|
|
327
|
+
state
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (sortFeaturesByKey) {
|
|
333
|
+
tileRenderState.sort((a, b) => a.sortKey - b.sortKey);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const segmentState of tileRenderState) {
|
|
337
|
+
const state = segmentState.state;
|
|
338
|
+
|
|
339
|
+
state.atlasTexture.bind(state.atlasInterpolation, gl.CLAMP_TO_EDGE);
|
|
340
|
+
|
|
341
|
+
if (state.isSDF) {
|
|
342
|
+
const uniformValues = state.uniformValues;
|
|
343
|
+
if (state.hasHalo) {
|
|
344
|
+
uniformValues['u_is_halo'] = 1;
|
|
345
|
+
drawSymbolElements(
|
|
346
|
+
state.buffers,
|
|
347
|
+
segmentState.segments,
|
|
348
|
+
layer,
|
|
349
|
+
painter,
|
|
350
|
+
state.program,
|
|
351
|
+
depthMode,
|
|
352
|
+
stencilMode,
|
|
353
|
+
colorMode,
|
|
354
|
+
uniformValues
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
uniformValues['u_is_halo'] = 0;
|
|
358
|
+
}
|
|
359
|
+
drawSymbolElements(
|
|
360
|
+
state.buffers,
|
|
361
|
+
segmentState.segments,
|
|
362
|
+
layer,
|
|
363
|
+
painter,
|
|
364
|
+
state.program,
|
|
365
|
+
depthMode,
|
|
366
|
+
stencilMode,
|
|
367
|
+
colorMode,
|
|
368
|
+
state.uniformValues
|
|
369
|
+
);
|
|
212
370
|
}
|
|
213
371
|
}
|
|
214
372
|
|
|
215
|
-
function drawSymbolElements(
|
|
373
|
+
function drawSymbolElements(
|
|
374
|
+
buffers,
|
|
375
|
+
segments,
|
|
376
|
+
layer,
|
|
377
|
+
painter,
|
|
378
|
+
program,
|
|
379
|
+
depthMode,
|
|
380
|
+
stencilMode,
|
|
381
|
+
colorMode,
|
|
382
|
+
uniformValues
|
|
383
|
+
) {
|
|
216
384
|
const context = painter.context;
|
|
217
385
|
const gl = context.gl;
|
|
218
386
|
program.draw(
|
|
@@ -226,7 +394,7 @@ function drawSymbolElements(buffers, layer, painter, program, depthMode, stencil
|
|
|
226
394
|
layer.id,
|
|
227
395
|
buffers.layoutVertexBuffer,
|
|
228
396
|
buffers.indexBuffer,
|
|
229
|
-
|
|
397
|
+
segments,
|
|
230
398
|
layer._paint,
|
|
231
399
|
painter.transform.zoom,
|
|
232
400
|
buffers.programConfigurations.get(layer.id),
|
package/src/render/painter.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ErrorEvent, Event, Evented } from '@mapwhit/events';
|
|
2
|
-
|
|
2
|
+
import { createExpression } from '@mapwhit/style-expressions';
|
|
3
3
|
import EXTENT from '../data/extent.js';
|
|
4
4
|
import browser from '../util/browser.js';
|
|
5
|
-
import
|
|
5
|
+
import warn from '../util/warn.js';
|
|
6
|
+
import { applySourceDiff, isUpdateableGeoJSON, mergeSourceDiffs, toUpdateable } from './geojson_source_diff.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* A source containing GeoJSON.
|
|
@@ -49,13 +50,16 @@ import GeoJSONWorkerSource from './geojson_worker_source.js';
|
|
|
49
50
|
* @see [Add a GeoJSON line](https://www.mapbox.com/mapbox-gl-js/example/geojson-line/)
|
|
50
51
|
* @see [Create a heatmap from points](https://www.mapbox.com/mapbox-gl-js/example/heatmap/)
|
|
51
52
|
*/
|
|
52
|
-
class GeoJSONSource extends Evented {
|
|
53
|
+
export default class GeoJSONSource extends Evented {
|
|
53
54
|
#pendingDataEvents = new Set();
|
|
54
55
|
#newData = false;
|
|
55
56
|
#updateInProgress = false;
|
|
56
|
-
#
|
|
57
|
+
#dataUpdateable;
|
|
58
|
+
#pendingUpdate;
|
|
59
|
+
#pendingData;
|
|
60
|
+
#tiler;
|
|
57
61
|
|
|
58
|
-
constructor(id, options, eventedParent,
|
|
62
|
+
constructor(id, options, eventedParent, tiler) {
|
|
59
63
|
super();
|
|
60
64
|
|
|
61
65
|
this.id = id;
|
|
@@ -73,7 +77,7 @@ class GeoJSONSource extends Evented {
|
|
|
73
77
|
|
|
74
78
|
this.setEventedParent(eventedParent);
|
|
75
79
|
|
|
76
|
-
this
|
|
80
|
+
this.#pendingUpdate = { data: options.data };
|
|
77
81
|
this._options = Object.assign({}, options);
|
|
78
82
|
|
|
79
83
|
if (options.maxzoom !== undefined) {
|
|
@@ -82,6 +86,7 @@ class GeoJSONSource extends Evented {
|
|
|
82
86
|
if (options.type) {
|
|
83
87
|
this.type = options.type;
|
|
84
88
|
}
|
|
89
|
+
this.promoteId = options.promoteId;
|
|
85
90
|
|
|
86
91
|
const scale = EXTENT / this.tileSize;
|
|
87
92
|
|
|
@@ -114,7 +119,7 @@ class GeoJSONSource extends Evented {
|
|
|
114
119
|
},
|
|
115
120
|
options.workerOptions
|
|
116
121
|
);
|
|
117
|
-
this.#
|
|
122
|
+
this.#tiler = tiler;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
load() {
|
|
@@ -133,11 +138,44 @@ class GeoJSONSource extends Evented {
|
|
|
133
138
|
* @returns {GeoJSONSource} this
|
|
134
139
|
*/
|
|
135
140
|
setData(data) {
|
|
136
|
-
this
|
|
141
|
+
this.#pendingUpdate = { data };
|
|
142
|
+
this.#updateData();
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Updates the source's GeoJSON, and re-renders the map.
|
|
148
|
+
*
|
|
149
|
+
* For sources with lots of features, this method can be used to make updates more quickly.
|
|
150
|
+
*
|
|
151
|
+
* This approach requires unique IDs for every feature in the source. The IDs can either be specified on the feature,
|
|
152
|
+
* or by using the promoteId option to specify which property should be used as the ID.
|
|
153
|
+
*
|
|
154
|
+
* It is an error to call updateData on a source that did not have unique IDs for each of its features already.
|
|
155
|
+
*
|
|
156
|
+
* Updates are applied on a best-effort basis, updating an ID that does not exist will not result in an error.
|
|
157
|
+
*
|
|
158
|
+
* @param {GeoJSONSourceDiff} diff The changes that need to be applied.
|
|
159
|
+
* @returns {GeoJSONSource} this
|
|
160
|
+
*/
|
|
161
|
+
updateData(diff) {
|
|
162
|
+
this.#pendingUpdate = {
|
|
163
|
+
data: this.#pendingUpdate?.data,
|
|
164
|
+
diff: mergeSourceDiffs(this.#pendingUpdate?.diff, diff)
|
|
165
|
+
};
|
|
137
166
|
this.#updateData();
|
|
138
167
|
return this;
|
|
139
168
|
}
|
|
140
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Allows to get the source's actual GeoJSON data.
|
|
172
|
+
*
|
|
173
|
+
* @returns a promise which resolves to the source's actual GeoJSON data
|
|
174
|
+
*/
|
|
175
|
+
getData() {
|
|
176
|
+
return this.#pendingData;
|
|
177
|
+
}
|
|
178
|
+
|
|
141
179
|
async #updateData(sourceDataType = 'content') {
|
|
142
180
|
this.#newData = true;
|
|
143
181
|
this.#pendingDataEvents.add(sourceDataType);
|
|
@@ -150,7 +188,8 @@ class GeoJSONSource extends Evented {
|
|
|
150
188
|
this.fire(new Event('dataloading', { dataType: 'source' }));
|
|
151
189
|
while (this.#newData) {
|
|
152
190
|
this.#newData = false;
|
|
153
|
-
|
|
191
|
+
this.#pendingData = this.#updateTilerData();
|
|
192
|
+
await this.#pendingData;
|
|
154
193
|
}
|
|
155
194
|
this.#pendingDataEvents.forEach(sourceDataType =>
|
|
156
195
|
this.fire(new Event('data', { dataType: 'source', sourceDataType }))
|
|
@@ -164,18 +203,32 @@ class GeoJSONSource extends Evented {
|
|
|
164
203
|
}
|
|
165
204
|
|
|
166
205
|
/*
|
|
167
|
-
* Responsible for invoking
|
|
168
|
-
* handles
|
|
169
|
-
* using geojson-vt or supercluster as appropriate.
|
|
206
|
+
* Responsible for invoking tiler's `loadData` target, which
|
|
207
|
+
* handles creating tiles, using geojson-vt or supercluster as appropriate.
|
|
170
208
|
*/
|
|
171
|
-
async
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
209
|
+
async #updateTilerData() {
|
|
210
|
+
const { data, diff } = this.#pendingUpdate ?? {};
|
|
211
|
+
this.#pendingUpdate = undefined;
|
|
212
|
+
if (!(data || diff)) {
|
|
213
|
+
warn.once(`No data or diff provided to GeoJSONSource ${this.id}.`);
|
|
214
|
+
return this.data;
|
|
175
215
|
}
|
|
176
|
-
|
|
216
|
+
if (data) {
|
|
217
|
+
this.data = await loadJSON(data, this.id);
|
|
218
|
+
this.#dataUpdateable = updatableGeoJson(this.data, this.promoteId);
|
|
219
|
+
}
|
|
220
|
+
if (diff) {
|
|
221
|
+
if (!this.#dataUpdateable) {
|
|
222
|
+
throw new Error(`GeoJSONSource "${this.id}": GeoJSON data is not compatible with updateData`);
|
|
223
|
+
}
|
|
224
|
+
applySourceDiff(this.#dataUpdateable, diff, this.promoteId);
|
|
225
|
+
this.data = { type: 'FeatureCollection', features: Array.from(this.#dataUpdateable.values()) };
|
|
226
|
+
}
|
|
227
|
+
this.data = filterGeoJSON(this.data, this._options);
|
|
177
228
|
|
|
178
|
-
|
|
229
|
+
const options = { ...this.workerOptions, data: this.data };
|
|
230
|
+
await this.#tiler.loadData(options);
|
|
231
|
+
return this.data;
|
|
179
232
|
}
|
|
180
233
|
|
|
181
234
|
async loadTile(tile) {
|
|
@@ -190,11 +243,12 @@ class GeoJSONSource extends Evented {
|
|
|
190
243
|
pixelRatio: browser.devicePixelRatio,
|
|
191
244
|
showCollisionBoxes: this.map.showCollisionBoxes,
|
|
192
245
|
justReloaded: tile.workerID != null,
|
|
193
|
-
painter: this.map.painter
|
|
246
|
+
painter: this.map.painter,
|
|
247
|
+
promoteId: this.promoteId
|
|
194
248
|
};
|
|
195
249
|
|
|
196
250
|
tile.workerID ??= true;
|
|
197
|
-
const data = await this.#
|
|
251
|
+
const data = await this.#tiler.loadTile(params).finally(() => tile.unloadVectorData());
|
|
198
252
|
if (!tile.aborted) {
|
|
199
253
|
tile.loadVectorData(data, this.map.painter);
|
|
200
254
|
}
|
|
@@ -217,4 +271,47 @@ class GeoJSONSource extends Evented {
|
|
|
217
271
|
}
|
|
218
272
|
}
|
|
219
273
|
|
|
220
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Fetch and parse GeoJSON according to the given params.
|
|
276
|
+
*
|
|
277
|
+
* @param data Function loading GeoJSON dataor GeoJSON data directly. Must be provided.
|
|
278
|
+
* GeoJSON can be either an object or string literal to be parsed.
|
|
279
|
+
*/
|
|
280
|
+
async function loadJSON(data, source) {
|
|
281
|
+
if (typeof data === 'function') {
|
|
282
|
+
data = await data().catch(() => {});
|
|
283
|
+
if (!data) {
|
|
284
|
+
throw new Error('no GeoJSON data');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (typeof data === 'string') {
|
|
288
|
+
try {
|
|
289
|
+
data = JSON.parse(data);
|
|
290
|
+
} catch {
|
|
291
|
+
throw new Error(`Input data given to '${source}' is not a valid GeoJSON object.`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return data;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function filterGeoJSON(data, { filter }) {
|
|
298
|
+
if (!filter) {
|
|
299
|
+
return data;
|
|
300
|
+
}
|
|
301
|
+
const compiled = createExpression(filter, {
|
|
302
|
+
type: 'boolean',
|
|
303
|
+
'property-type': 'data-driven',
|
|
304
|
+
overridable: false,
|
|
305
|
+
transition: false
|
|
306
|
+
});
|
|
307
|
+
if (compiled.result === 'error') {
|
|
308
|
+
throw new Error(compiled.value.map(err => `${err.key}: ${err.message}`).join(', '));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const features = data.features.filter(feature => compiled.value.evaluate({ zoom: 0 }, feature));
|
|
312
|
+
return { type: 'FeatureCollection', features };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function updatableGeoJson(data, promoteId) {
|
|
316
|
+
return isUpdateableGeoJSON(data, promoteId) ? toUpdateable(data, promoteId) : false;
|
|
317
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
function getFeatureId(feature, promoteId) {
|
|
2
|
+
return promoteId ? feature.properties[promoteId] : feature.id;
|
|
3
|
+
}
|
|
4
|
+
export function isUpdateableGeoJSON(data, promoteId) {
|
|
5
|
+
// null can be updated
|
|
6
|
+
if (data == null) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
// {} can be updated
|
|
10
|
+
if (data.type == null) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
// a single feature with an id can be updated, need to explicitly check against null because 0 is a valid feature id that is falsy
|
|
14
|
+
if (data.type === 'Feature') {
|
|
15
|
+
return getFeatureId(data, promoteId) != null;
|
|
16
|
+
}
|
|
17
|
+
// a feature collection can be updated if every feature has an id, and the ids are all unique
|
|
18
|
+
// this prevents us from silently dropping features if ids get reused
|
|
19
|
+
if (data.type === 'FeatureCollection') {
|
|
20
|
+
const seenIds = new Set();
|
|
21
|
+
for (const feature of data.features) {
|
|
22
|
+
const id = getFeatureId(feature, promoteId);
|
|
23
|
+
if (id == null) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (seenIds.has(id)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
seenIds.add(id);
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
export function toUpdateable(data, promoteId) {
|
|
36
|
+
const result = new Map();
|
|
37
|
+
if (data == null || data.type == null) {
|
|
38
|
+
// empty result
|
|
39
|
+
} else if (data.type === 'Feature') {
|
|
40
|
+
result.set(getFeatureId(data, promoteId), data);
|
|
41
|
+
} else {
|
|
42
|
+
for (const feature of data.features) {
|
|
43
|
+
result.set(getFeatureId(feature, promoteId), feature);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
// mutates updateable
|
|
49
|
+
export function applySourceDiff(updateable, diff, promoteId) {
|
|
50
|
+
if (diff.removeAll) {
|
|
51
|
+
updateable.clear();
|
|
52
|
+
}
|
|
53
|
+
if (diff.remove) {
|
|
54
|
+
for (const id of diff.remove) {
|
|
55
|
+
updateable.delete(id);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (diff.add) {
|
|
59
|
+
for (const feature of diff.add) {
|
|
60
|
+
const id = getFeatureId(feature, promoteId);
|
|
61
|
+
if (id != null) {
|
|
62
|
+
updateable.set(id, feature);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (diff.update) {
|
|
67
|
+
for (const update of diff.update) {
|
|
68
|
+
let feature = updateable.get(update.id);
|
|
69
|
+
if (feature == null) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// be careful to clone the feature and/or properties objects to avoid mutating our input
|
|
73
|
+
const cloneFeature = update.newGeometry || update.removeAllProperties;
|
|
74
|
+
// note: removeAllProperties gives us a new properties object, so we can skip the clone step
|
|
75
|
+
const cloneProperties =
|
|
76
|
+
!update.removeAllProperties &&
|
|
77
|
+
(update.removeProperties?.length > 0 || update.addOrUpdateProperties?.length > 0);
|
|
78
|
+
if (cloneFeature || cloneProperties) {
|
|
79
|
+
feature = { ...feature };
|
|
80
|
+
updateable.set(update.id, feature);
|
|
81
|
+
if (cloneProperties) {
|
|
82
|
+
feature.properties = { ...feature.properties };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (update.newGeometry) {
|
|
86
|
+
feature.geometry = update.newGeometry;
|
|
87
|
+
}
|
|
88
|
+
if (update.removeAllProperties) {
|
|
89
|
+
feature.properties = {};
|
|
90
|
+
} else if (update.removeProperties?.length > 0) {
|
|
91
|
+
for (const prop of update.removeProperties) {
|
|
92
|
+
if (Object.prototype.hasOwnProperty.call(feature.properties, prop)) {
|
|
93
|
+
delete feature.properties[prop];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (update.addOrUpdateProperties?.length > 0) {
|
|
98
|
+
for (const { key, value } of update.addOrUpdateProperties) {
|
|
99
|
+
feature.properties[key] = value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function mergeSourceDiffs(existingDiff, newDiff) {
|
|
106
|
+
if (!existingDiff) {
|
|
107
|
+
return newDiff ?? {};
|
|
108
|
+
}
|
|
109
|
+
if (!newDiff) {
|
|
110
|
+
return existingDiff;
|
|
111
|
+
}
|
|
112
|
+
const merged = { ...existingDiff };
|
|
113
|
+
if (newDiff.removeAll) {
|
|
114
|
+
merged.removeAll = true;
|
|
115
|
+
}
|
|
116
|
+
if (newDiff.remove) {
|
|
117
|
+
const removedSet = new Set(merged.remove ? merged.remove.concat(newDiff.remove) : newDiff.remove);
|
|
118
|
+
merged.remove = Array.from(removedSet.values());
|
|
119
|
+
}
|
|
120
|
+
if (newDiff.add) {
|
|
121
|
+
const combinedAdd = merged.add ? merged.add.concat(newDiff.add) : newDiff.add;
|
|
122
|
+
const addMap = new Map(combinedAdd.map(feature => [feature.id, feature]));
|
|
123
|
+
merged.add = Array.from(addMap.values());
|
|
124
|
+
}
|
|
125
|
+
if (newDiff.update) {
|
|
126
|
+
const updateMap = new Map(merged.update?.map(feature => [feature.id, feature]));
|
|
127
|
+
for (const feature of newDiff.update) {
|
|
128
|
+
const featureUpdate = updateMap.get(feature.id) ?? { id: feature.id };
|
|
129
|
+
if (feature.newGeometry) {
|
|
130
|
+
featureUpdate.newGeometry = feature.newGeometry;
|
|
131
|
+
}
|
|
132
|
+
if (feature.addOrUpdateProperties) {
|
|
133
|
+
featureUpdate.addOrUpdateProperties = (featureUpdate.addOrUpdateProperties ?? []).concat(
|
|
134
|
+
feature.addOrUpdateProperties
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (feature.removeProperties) {
|
|
138
|
+
featureUpdate.removeProperties = (featureUpdate.removeProperties ?? []).concat(feature.removeProperties);
|
|
139
|
+
}
|
|
140
|
+
if (feature.removeAllProperties) {
|
|
141
|
+
featureUpdate.removeAllProperties = true;
|
|
142
|
+
}
|
|
143
|
+
updateMap.set(feature.id, featureUpdate);
|
|
144
|
+
}
|
|
145
|
+
merged.update = Array.from(updateMap.values());
|
|
146
|
+
}
|
|
147
|
+
return merged;
|
|
148
|
+
}
|