@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.
Files changed (40) hide show
  1. package/build/min/package.json +1 -1
  2. package/package.json +1 -1
  3. package/src/data/array_types.js +115 -64
  4. package/src/data/bucket/circle_bucket.js +42 -5
  5. package/src/data/bucket/fill_bucket.js +31 -13
  6. package/src/data/bucket/fill_extrusion_bucket.js +8 -6
  7. package/src/data/bucket/line_bucket.js +38 -14
  8. package/src/data/bucket/symbol_attributes.js +13 -5
  9. package/src/data/bucket/symbol_bucket.js +87 -33
  10. package/src/data/bucket/symbol_collision_buffers.js +1 -1
  11. package/src/data/bucket.js +3 -1
  12. package/src/data/feature_index.js +24 -11
  13. package/src/data/segment.js +15 -7
  14. package/src/render/draw_circle.js +45 -4
  15. package/src/render/draw_symbol.js +190 -22
  16. package/src/render/painter.js +1 -1
  17. package/src/source/geojson_source.js +118 -21
  18. package/src/source/geojson_source_diff.js +148 -0
  19. package/src/source/geojson_tiler.js +89 -0
  20. package/src/source/source.js +16 -5
  21. package/src/source/source_cache.js +6 -6
  22. package/src/source/source_state.js +4 -2
  23. package/src/source/tile.js +5 -3
  24. package/src/source/vector_tile_source.js +2 -0
  25. package/src/source/worker_tile.js +4 -2
  26. package/src/style/pauseable_placement.js +39 -7
  27. package/src/style/style.js +86 -34
  28. package/src/style/style_layer/circle_style_layer_properties.js +8 -1
  29. package/src/style/style_layer/fill_style_layer_properties.js +8 -1
  30. package/src/style/style_layer/line_style_layer_properties.js +4 -0
  31. package/src/style/style_layer/symbol_style_layer_properties.js +17 -2
  32. package/src/style-spec/reference/v8.json +161 -4
  33. package/src/symbol/one_em.js +4 -0
  34. package/src/symbol/placement.js +406 -173
  35. package/src/symbol/projection.js +3 -3
  36. package/src/symbol/quads.js +1 -6
  37. package/src/symbol/shaping.js +16 -27
  38. package/src/symbol/symbol_layout.js +243 -81
  39. package/src/util/vectortile_to_geojson.js +3 -4
  40. 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.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
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.bind(
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
- drawSymbolElements(buffers, layer, painter, program, depthMode, stencilMode, colorMode, uniformValues);
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(buffers, layer, painter, program, depthMode, stencilMode, colorMode, uniformValues) {
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
- buffers.segments,
397
+ segments,
230
398
  layer._paint,
231
399
  painter.transform.zoom,
232
400
  buffers.programConfigurations.get(layer.id),
@@ -417,7 +417,7 @@ export default class Painter {
417
417
  }
418
418
  this.id = layer.id;
419
419
 
420
- draw[layer.type](painter, sourceCache, layer, coords);
420
+ draw[layer.type](painter, sourceCache, layer, coords, this.style.placement.variableOffsets);
421
421
  }
422
422
 
423
423
  /**
@@ -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 GeoJSONWorkerSource from './geojson_worker_source.js';
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
- #worker;
57
+ #dataUpdateable;
58
+ #pendingUpdate;
59
+ #pendingData;
60
+ #tiler;
57
61
 
58
- constructor(id, options, eventedParent, { resources, layerIndex }) {
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.data = options.data;
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.#worker = new GeoJSONWorkerSource(resources, layerIndex);
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.data = data;
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
- await this._updateWorkerData(this.data);
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 WorkerSource's geojson.loadData target, which
168
- * handles loading the geojson data and preparing to serve it up as tiles,
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 _updateWorkerData(data) {
172
- const json = typeof data === 'function' ? await data().catch(() => {}) : data;
173
- if (!json) {
174
- throw new Error('no GeoJSON data');
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
- const options = { ...this.workerOptions, data: json };
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
- return await this.#worker.loadData(options);
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.#worker.loadTile(params).finally(() => tile.unloadVectorData());
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
- export default GeoJSONSource;
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
+ }