• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geosolutions-it / MapStore2 / 30438724836

29 Jul 2026 09:13AM UTC coverage: 75.645% (+0.001%) from 75.644%
30438724836

push

github

web-flow
Fix #12689 - Remove unnecessary src=null on WMS tile load error (#12691) (#12749)

* Fix #12689 - Remove unnecessary src=null on WMS tile load error

Setting img.src = null does not clear the image; the DOM coerces it
to the string "null", causing a real (always-404) network request.
image.setState(3) alone already triggers the source's
tileloaderror/imageloaderror events (state-based, no DOM event
dependency), making the src=null line redundant.

* Add test for #12689 spurious /null request removal

Verifies the tile error handler no longer sets img.src = null on failure.

* Fix #12689 - set error state on single tile WMS load failure

ol/Image has no setState, unlike ol/ImageTile: notify the state change
manually so imageloaderror is emitted without setting img.src to null.
Tests now configure requestsConfigurationRules to reach the custom load
function error branch.

(cherry picked from commit 40197f081)

35740 of 56464 branches covered (63.3%)

7 of 9 new or added lines in 1 file covered. (77.78%)

3 existing lines in 2 files now uncovered.

44532 of 58870 relevant lines covered (75.64%)

122.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

41.77
/web/client/utils/cesium/GeoJSONStyledFeatures.js
1
import * as Cesium from 'cesium';
2
import turfFlatten from '@turf/flatten';
3
import omit from 'lodash/omit';
4
import isArray from 'lodash/isArray';
5
import { v1 as uuid } from 'uuid';
6
import { getMapScaleForCesium } from '../MapUtils';
7
import { geoStylerScaleDenominatorFilter } from '../styleparser/StyleParserUtils';
8

9
/**
10
 * validate the coordinates and ensure:
11
 * - values inside are not nested arrays
12
 * - the current coordinate are not a duplication of the previous one
13
 * @param {number[]} coords array of coordinates [longitude, latitude, height]
14
 * @param {number[]} prevCoords array of coordinates [longitude, latitude, height]
15
 * @returns the coordinates if valid and null if invalid
16
 */
17
const validateCoordinatesValue = (coords, prevCoords) => {
3✔
18
    if (!isArray(coords[0]) && !isArray(coords[1]) && !isArray(coords[2])) {
429!
19
        // remove duplicated points
20
        // to avoid normalization errors
21
        return prevCoords && (prevCoords[0] === coords[0] && prevCoords[1] === coords[1] && prevCoords[2] === coords[2])
429✔
22
            ? null
23
            : coords;
24
    }
25
    return null;
×
26
};
27

28
const featureToCartesianPositions = (feature) => {
3✔
29
    if (feature.geometry.type === 'Point') {
144✔
30
        const validatedCoords = validateCoordinatesValue([
60✔
31
            feature.geometry.coordinates[0],
32
            feature.geometry.coordinates[1],
33
            feature.geometry.coordinates[2] || 0
108✔
34
        ]);
35
        return validatedCoords ? [[Cesium.Cartesian3.fromDegrees(
60!
36
            validatedCoords[0],
37
            validatedCoords[1],
38
            validatedCoords[2] || 0
108✔
39
        )]] : null;
40
    }
41
    if (feature.geometry.type === 'LineString') {
84✔
42
        const positions = feature.geometry.coordinates.map((coords, idx) =>
33✔
43
            validateCoordinatesValue(coords, feature.geometry.coordinates[idx - 1])
132!
44
                ? Cesium.Cartesian3.fromDegrees(coords[0], coords[1], coords[2])
45
                : null
46
        ).filter(coords => coords !== null);
132✔
47
        return positions.length > 1 ? [positions] : null;
33!
48
    }
49
    if (feature.geometry.type === 'Polygon') {
51!
50
        const positions = feature.geometry.coordinates.map(ring => {
51✔
51
            const ringPositions = ring.map((coords, idx) =>
51✔
52
                validateCoordinatesValue(coords, ring[idx - 1])
237✔
53
                    ? Cesium.Cartesian3.fromDegrees(coords[0], coords[1], coords[2])
54
                    : null
55
            ).filter(coords => coords !== null);
237✔
56
            return ringPositions.length > 2 ? ringPositions : null;
51✔
57
        }).filter(ring => ring !== null);
51✔
58
        return positions.length > 0 ? positions : null;
51✔
59
    }
60
    return null;
×
61
};
62
/**
63
 * Class to manage styles for layer with an array of features
64
 * @param {string} options.id layer identifier
65
 * @param {object} options.map a Cesium map instance
66
 * @param {number} options.opacity opacity of the layer
67
 * @param {boolean} options.queryable if false the features will not be queryable, default is true
68
 * @param {array} options.features array of valid geojson features
69
 * @param {boolean} options.mergePolygonFeatures if true will merge all polygons with similar styles in a single primitive. This could help to reduce the draw call to the render
70
 * @param {func} featureFilter a function to filter feature, it receives a GeoJSON feature as argument and it must return a boolean
71
 */
72
class GeoJSONStyledFeatures {
73
    constructor(options = {}) {
×
74
        this._msId = options.id;
39✔
75
        this._dataSource = new Cesium.CustomDataSource(options.id);
39✔
76
        this._primitives = new Cesium.PrimitiveCollection({ destroyPrimitives: true });
39✔
77
        this._map = options.map;
39✔
78
        this._onCameraMoveEndBound = this._updateScaleVisibility.bind(this);
39✔
79
        if (this._map) {
39✔
80
            this._map.scene.primitives.add(this._primitives);
30✔
81
            this._map.dataSources.add(this._dataSource);
30✔
82
            this._map.camera.moveEnd.addEventListener(this._onCameraMoveEndBound);
30✔
83
        }
84
        this._styledFeatures = [];
39✔
85
        // Map<id, { id, entity }>: keyed lookups during _updateEntities are
86
        // O(1) instead of O(N) scans. Wrapper shape preserved so the rest of
87
        // the class (and _updatePointEntity, _updateScaleVisibility, etc.)
88
        // can keep accessing { id, entity } unchanged.
89
        this._entities = new Map();
39✔
90
        this._opacity = options.opacity ?? 1;
39✔
91
        this._queryable = options.queryable === undefined ? true : !!options.queryable;
39✔
92
        this._mergePolygonFeatures = !!options?.mergePolygonFeatures;
39✔
93
        this._featureFilter = options.featureFilter;
39✔
94
        this._dataSource.entities.collectionChanged.addEventListener(() => {
39✔
95
            setTimeout(() => this._map.scene.requestRender(), 300);
9✔
96
        });
97
        // internal key to associate features with original features
98
        this._uuidKey = '__ms_uuid_key__' + uuid();
39✔
99
        // Concurrency control for _runUpdate. Only one style/render cycle
100
        // runs at a time. Requests received while a cycle is in flight
101
        // collapse into a single pending rerun, kicked off when the current
102
        // cycle completes. This serializes apply order (so an older, smaller
103
        // styledFeatures snapshot can't arrive after a newer one and erase
104
        // entities the newer one added) and still picks up later additions
105
        // (so a slow style function during streaming doesn't strand features).
106
        this._runUpdateInFlight = false;
39✔
107
        this._runUpdatePending = null;
39✔
108
        // needs to be run after this._uuidKey
109
        this.setFeatures(options.features);
39✔
110
        this._styleRules = options?.styleRules;
39✔
111
    }
112
    _addCustomProperties(obj) {
113
        obj._msIsQueryable = () => this._queryable;
9✔
114
        obj._msGetFeatureById = (id) => {
9✔
115
            const _id = id instanceof Cesium.Entity ? id.id : id;
×
116
            const [featureId] = _id.split(':');
×
117
            const feature = this._features.find((f) => f.id === featureId);
×
118
            const originalFeature = this._originalFeatures.find((f) => f.properties[this._uuidKey] === feature.properties[this._uuidKey]) || {};
×
119
            const { properties } = originalFeature;
×
120
            return {
×
121
                feature: {
122
                    ...originalFeature,
123
                    properties: omit(properties, [this._uuidKey])
124
                },
125
                msId: this._msId
126
            };
127
        };
128
    }
129
    _getEntityOptions(primitive) {
130
        const { entity, geometry, orientation, minimumHeights, maximumHeights } = primitive;
9✔
131
        if (entity.polygon) {
9!
132
            return {
×
133
                polygon: {
134
                    ...entity.polygon,
135
                    hierarchy: new Cesium.ConstantProperty(new Cesium.PolygonHierarchy(
136
                        geometry[0].map(cartesian => cartesian.clone()),
×
137
                        geometry
138
                            .filter((hole, idx) => idx > 0)
×
139
                            .map(hole => new Cesium.PolygonHierarchy(hole.map((cartesian) => cartesian.clone())))
×
140
                    ))
141
                }
142
            };
143
        }
144
        if (entity.wall) {
9!
145
            return {
×
146
                wall: {
147
                    ...entity.wall,
148
                    positions: geometry[0],
149
                    minimumHeights: minimumHeights?.[0],
150
                    maximumHeights: maximumHeights?.[0]
151
                }
152
            };
153
        }
154
        if (entity.polyline) {
9!
155
            return {
×
156
                polyline: {
157
                    ...entity.polyline,
158
                    positions: geometry[0]
159
                }
160
            };
161
        }
162
        if (entity.polylineVolume) {
9!
163
            return {
×
164
                polylineVolume: {
165
                    ...entity.polylineVolume,
166
                    positions: geometry[0]
167
                }
168
            };
169
        }
170
        return {
9✔
171
            ...entity,
172
            ...(orientation && { orientation }),
9!
173
            position: geometry
174
        };
175
    }
176
    _updatePointEntity(primitive, previous) {
177
        if (previous && (primitive.entity.billboard || primitive.entity.label || primitive.entity.model)) {
9!
178
            const entity = previous.entity;
×
179
            if (primitive.orientation) {
×
180
                entity.orientation = primitive.orientation;
×
181
            }
182
            if (primitive.geometry) {
×
183
                entity.position = primitive.geometry.clone();
×
184
            }
185
            const graphicKey = Object.keys(primitive.entity)[0];
×
186
            // it's better to recreate the point entity in case height reference change
187
            if (entity?.[graphicKey]?.heightReference?.getValue() !== primitive?.entity?.[graphicKey]?.heightReference) {
×
188
                return null;
×
189
            }
190
            const propertyKeys = Object.keys(primitive.entity[graphicKey]);
×
191
            propertyKeys.forEach((propertyKey) => {
×
192
                entity[graphicKey][propertyKey] = primitive.entity[graphicKey][propertyKey];
×
193
            });
194
            return previous;
×
195
        }
196
        return null;
9✔
197
    }
198
    _filterEntities({ primitive }) {
199
        return (this._mergePolygonFeatures && primitive?.entity?.polygon) ? false : !!primitive.entity;
9!
200
    }
201
    _updateEntities(newStyledFeatures, forceUpdate) {
202
        const previousEntities = this._styledFeatures.filter((feature) => this._filterEntities(feature));
15✔
203
        const entities = newStyledFeatures.filter((feature) => this._filterEntities(feature));
15✔
204
        const currentIdsSet = new Set();
15✔
205
        for (let i = 0; i < entities.length; i++) {
15✔
206
            currentIdsSet.add(entities[i].id);
9✔
207
        }
208
        // Do NOT wrap remove+add of same-id entities in suspendEvents: Cesium's
209
        // EntityCollection treats a queued remove followed by an add of the
210
        // same id as a net no-op (see add()/removeById() in EntityCollection.js
211
        // - both clear the opposite queue without populating their own).
212
        // The visualizer is then never notified, the new primitive is never
213
        // built, and the layer keeps rendering with the old style.
214
        for (let i = 0; i < previousEntities.length; i++) {
15✔
215
            const id = previousEntities[i].id;
×
216
            if (currentIdsSet.has(id)) {
×
217
                continue;
×
218
            }
219
            const entry = this._entities.get(id);
×
220
            if (entry) {
×
221
                this._dataSource.entities.remove(entry.entity);
×
222
            }
223
        }
224
        const newEntitiesMap = new Map();
15✔
225
        for (let i = 0; i < entities.length; i++) {
15✔
226
            const { id, action, primitive } = entities[i];
9✔
227
            const previous = this._entities.get(id);
9✔
228
            if (!forceUpdate && action === 'none') {
9!
229
                if (previous) {
×
230
                    newEntitiesMap.set(id, previous);
×
231
                }
232
                continue;
×
233
            }
234
            const updatedPoint = this._updatePointEntity(primitive, previous);
9✔
235
            if (updatedPoint) {
9!
236
                newEntitiesMap.set(id, updatedPoint);
×
237
                continue;
×
238
            }
239
            if (previous) {
9!
240
                this._dataSource.entities.remove(previous.entity);
×
241
            }
242
            const entity = this._dataSource.entities.add({
9✔
243
                id,
244
                ...this._getEntityOptions(primitive)
245
            });
246
            this._addCustomProperties(entity);
9✔
247
            newEntitiesMap.set(id, { id, entity });
9✔
248
        }
249
        this._entities = newEntitiesMap;
15✔
250
    }
251
    _updatePolygonPrimitive(newStyledFeatures, forceUpdate) {
252
        const previousPolygonPrimitives = this._styledFeatures.filter(({ primitive }) => primitive.type === 'polygon' && !primitive.clampToGround);
×
253
        const polygonPrimitives = newStyledFeatures.filter(({ primitive }) => primitive.type === 'polygon' && !primitive.clampToGround);
×
254
        const polygonPrimitivesIds = new Set(polygonPrimitives.map(({ id }) => id));
×
255
        const removedPrimitives = previousPolygonPrimitives.filter(({ id }) => !polygonPrimitivesIds.has(id));
×
256
        const noActions = !removedPrimitives.length && polygonPrimitives.length ? polygonPrimitives.every(({ action }) => !forceUpdate && action === 'none') : false;
×
257
        if (noActions) {
×
258
            return;
×
259
        }
260
        if (this?._polygonPrimitives?.length) {
×
261
            this._polygonPrimitives.forEach((primitive) => {
×
262
                this._primitives.remove(primitive);
×
263
            });
264
            this._polygonPrimitives = [];
×
265
        }
266
        if (polygonPrimitives.length <= 0) {
×
267
            return;
×
268
        }
269
        const groupByTranslucencyAndExtrusion = polygonPrimitives.reduce((acc, options) => {
×
270
            const { primitive } = options;
×
271
            const { material, extrudedHeight } = primitive?.entity?.polygon || {};
×
272
            const key = `t:${material.alpha === 1},e:${extrudedHeight !== undefined ? 'true' : 'false'}`;
×
273
            return {
×
274
                ...acc,
275
                [key]: [...(acc[key] || []), options]
×
276
            };
277
        }, {});
278
        this._polygonPrimitives = Object.keys(groupByTranslucencyAndExtrusion).map((key) => {
×
279
            const styledFeatures = groupByTranslucencyAndExtrusion[key];
×
280
            const cesiumPrimitive = new Cesium.Primitive({
×
281
                geometryInstances: styledFeatures.map(({ primitive, id }) => {
282
                    const polygon = primitive?.entity?.polygon || {};
×
283
                    return new Cesium.GeometryInstance({
×
284
                        geometry: new Cesium.PolygonGeometry({
285
                            polygonHierarchy: new Cesium.PolygonHierarchy(
286
                                primitive.geometry[0].map(cartesian => cartesian.clone()),
×
287
                                primitive.geometry
288
                                    .filter((hole, idx) => idx > 0)
×
289
                                    .map(hole => new Cesium.PolygonHierarchy(hole.map((cartesian) => cartesian.clone())))
×
290
                            ),
291
                            arcType: polygon.arcType,
292
                            perPositionHeight: polygon.height !== undefined ? false : polygon.perPositionHeight,
×
293
                            ...(polygon.height !== undefined && { height: polygon.height }),
×
294
                            ...(polygon.extrudedHeight !== undefined && { extrudedHeight: polygon.extrudedHeight }),
×
295
                            vertexFormat: Cesium.VertexFormat.POSITION_AND_NORMAL
296
                        }),
297
                        id,
298
                        attributes: {
299
                            color: new Cesium.ColorGeometryInstanceAttribute(
300
                                polygon.material.red,
301
                                polygon.material.green,
302
                                polygon.material.blue,
303
                                polygon.material.alpha
304
                            ),
305
                            // allow to click on multiple instances
306
                            show: new Cesium.ShowGeometryInstanceAttribute(true)
307
                        }
308
                    });
309
                }),
310
                appearance: new Cesium.PerInstanceColorAppearance({
311
                    flat: styledFeatures[0].primitive?.entity?.polygon?.extrudedHeight === undefined,
312
                    translucent: styledFeatures[0].primitive?.entity?.polygon?.material?.alpha !== 1
313
                }),
314
                asynchronous: true
315
            });
316
            this._addCustomProperties(cesiumPrimitive);
×
317
            return cesiumPrimitive;
×
318
        });
319
        this._polygonPrimitives.forEach((primitive) => {
×
320
            this._primitives.add(primitive);
×
321
        });
322
    }
323
    _updateGroundPolygonPrimitive(newStyledFeatures, forceUpdate) {
324
        const previousGroundPolygonPrimitives = this._styledFeatures.filter(({ primitive }) => primitive.type === 'polygon' && !!primitive.clampToGround);
×
325
        const groundPolygonPrimitives = newStyledFeatures.filter(({ primitive }) => primitive.type === 'polygon' && !!primitive.clampToGround);
×
326
        const groundPolygonPrimitivesIds = new Set(groundPolygonPrimitives.map(({ id }) => id));
×
327
        const removedPrimitives = previousGroundPolygonPrimitives.filter(({ id }) => !groundPolygonPrimitivesIds.has(id));
×
328
        const noActions = !removedPrimitives.length && groundPolygonPrimitives.length ? groundPolygonPrimitives.every(({ action }) => !forceUpdate && action === 'none') : false;
×
329
        if (noActions) {
×
330
            return;
×
331
        }
332
        if (this?._groundPolygonPrimitives?.length) {
×
333
            this._groundPolygonPrimitives.forEach((primitive) => {
×
334
                this._primitives.remove(primitive);
×
335
            });
336
            this._groundPolygonPrimitives = [];
×
337
        }
338
        if (groundPolygonPrimitives.length <= 0) {
×
339
            return;
×
340
        }
341
        const groupByColorAndClassification = groundPolygonPrimitives.reduce((acc, options) => {
×
342
            const { primitive } = options;
×
343
            const { material, classificationType } = primitive?.entity?.polygon || {};
×
344
            const key = `r:${material.red},g:${material.green},b:${material.blue},a:${material.alpha},c:${classificationType}`;
×
345
            return {
×
346
                ...acc,
347
                [key]: [...(acc[key] || []), options]
×
348
            };
349
        }, {});
350

351
        this._groundPolygonPrimitives = Object.keys(groupByColorAndClassification).map((key) => {
×
352
            const styledFeatures = groupByColorAndClassification[key];
×
353
            const cesiumPrimitive = new Cesium.GroundPrimitive({
×
354
                classificationType: styledFeatures[0].primitive?.entity?.polygon?.classificationType,
355
                geometryInstances: styledFeatures.map(({ primitive, id }) => {
356
                    const polygon = primitive?.entity?.polygon || {};
×
357
                    return new Cesium.GeometryInstance({
×
358
                        geometry: new Cesium.PolygonGeometry({
359
                            polygonHierarchy: new Cesium.PolygonHierarchy(
360
                                primitive.geometry[0].map(cartesian => cartesian.clone()),
×
361
                                primitive.geometry
362
                                    .filter((hole, idx) => idx > 0)
×
363
                                    .map(hole => new Cesium.PolygonHierarchy(hole.map((cartesian) => cartesian.clone())))
×
364
                            ),
365
                            arcType: polygon.arcType,
366
                            perPositionHeight: polygon.perPositionHeight
367
                        }),
368
                        id,
369
                        attributes: {
370
                            color: new Cesium.ColorGeometryInstanceAttribute(
371
                                polygon.material.red,
372
                                polygon.material.green,
373
                                polygon.material.blue,
374
                                polygon.material.alpha
375
                            ),
376
                            // allow to click on multiple instances
377
                            show: new Cesium.ShowGeometryInstanceAttribute(true)
378
                        }
379
                    });
380
                }),
381
                appearance: new Cesium.PerInstanceColorAppearance({
382
                    flat: true,
383
                    translucent: styledFeatures[0].primitive?.entity?.polygon?.material?.alpha !== 1
384
                }),
385
                asynchronous: true
386
            });
387

388
            this._addCustomProperties(cesiumPrimitive);
×
389
            return cesiumPrimitive;
×
390
        });
391

392
        this._groundPolygonPrimitives.forEach((primitive) => {
×
393
            this._primitives.add(primitive);
×
394
        });
395
    }
396
    _runUpdate(forceUpdate) {
397
        if (!this._styleFunction) {
15!
398
            return;
×
399
        }
400
        // If a cycle is already running, queue exactly one rerun. Multiple
401
        // requests during the same in-flight window collapse together; the
402
        // rerun, when it fires, will reflect whatever state (features, style,
403
        // filter, opacity) is current at that point.
404
        if (this._runUpdateInFlight) {
15!
405
            this._runUpdatePending = {
×
406
                forceUpdate: !!(this._runUpdatePending?.forceUpdate || forceUpdate)
×
407
            };
408
            return;
×
409
        }
410
        this._runUpdateInFlight = true;
15✔
411
        // Build an id-keyed lookup once per cycle so getPreviousStyledFeature
412
        // is O(1) per call instead of an Array.find over the entire previous
413
        // styled set. For large layers (where the style function calls this
414
        // for every feature) the prior O(N) per call multiplied to O(N²) per
415
        // cycle, the dominant cost during streaming.
416
        const previousStyledById = new Map();
15✔
417
        for (let i = 0; i < this._styledFeatures.length; i++) {
15✔
418
            const sf = this._styledFeatures[i];
×
419
            previousStyledById.set(sf.id, sf);
×
420
        }
421
        this._styleFunction({
15✔
422
            map: this._map,
423
            opacity: this._opacity,
424
            features: this._featureFilter ? this._features.filter(this._featureFilter) : this._features,
15!
425
            getPreviousStyledFeature: (styledFeature) => previousStyledById.get(styledFeature.id)
9✔
426
        })
427
            .then((styledFeatures) => {
428
                this._updateEntities(styledFeatures, forceUpdate);
15✔
429
                if (this._mergePolygonFeatures) {
15!
430
                    this._updatePolygonPrimitive(styledFeatures, forceUpdate);
×
431
                    this._updateGroundPolygonPrimitive(styledFeatures, forceUpdate);
×
432
                }
433
                this._styledFeatures = [...styledFeatures];
15✔
434
                this._updateScaleVisibility();
15✔
435
                setTimeout(() => this._map.scene.requestRender());
×
436
            })
437
            .finally(() => {
438
                this._runUpdateInFlight = false;
15✔
439
                if (this._runUpdatePending) {
15!
440
                    const pending = this._runUpdatePending;
×
441
                    this._runUpdatePending = null;
×
442
                    this._runUpdate(pending.forceUpdate);
×
443
                }
444
            });
445
    }
446
    _update(forceUpdate) {
447
        if (this._timeout) {
15!
448
            clearTimeout(this._timeout);
×
449
        }
450
        this._timeout = setTimeout(() => {
15✔
451
            this._timeout = undefined;
15✔
452
            this._runUpdate(forceUpdate);
15✔
453
        }, 300);
454
    }
455
    /**
456
     * Run any pending debounced _update synchronously. Used by the streaming
457
     * load path to force a render mid-load instead of waiting for the 300ms
458
     * debounce to fire after the last batch.
459
     */
460
    flushPendingUpdate(forceUpdate) {
461
        if (this._timeout) {
×
462
            clearTimeout(this._timeout);
×
463
            this._timeout = undefined;
×
464
        }
465
        this._runUpdate(forceUpdate);
×
466
    }
467
    setFeatures(newFeatures) {
468
        this._originalFeatures = (newFeatures ?? []).map((feature) => {
39!
469
            return {
33✔
470
                ...feature,
471
                properties: {
472
                    ...feature.properties,
473
                    [this._uuidKey]: feature?.properties?.[this._uuidKey] ?? uuid()
66✔
474
                }
475
            };
476
        });
477
        const { features } = turfFlatten({ type: 'FeatureCollection', features: this._originalFeatures });
39✔
478
        this._features = features.filter(feature => feature.geometry)
39✔
479
            .map((feature) => {
480
                return {
33✔
481
                    ...feature,
482
                    id: uuid(),
483
                    positions: featureToCartesianPositions(feature)
484
                };
485
            }).filter(feature => feature.positions !== null);
33✔
486

487
    }
488
    /**
489
     * Append features without rebuilding existing ones. Existing entities and
490
     * positions are preserved; only the new batch is transformed and added.
491
     * Triggers a debounced _update; callers that need a synchronous render can
492
     * follow up with flushPendingUpdate().
493
     */
494
    addFeatures(newFeatures) {
495
        if (!newFeatures || newFeatures.length === 0) {
×
496
            return;
×
497
        }
498
        const newOriginal = newFeatures.map((feature) => ({
×
499
            ...feature,
500
            properties: {
501
                ...feature.properties,
502
                [this._uuidKey]: feature?.properties?.[this._uuidKey] ?? uuid()
×
503
            }
504
        }));
505
        // In-place push avoids the O(N) re-allocation that array spread incurs
506
        // every batch; for 30k features streamed in 200-feature batches the
507
        // spread version is O(N²) cumulative.
508
        for (let i = 0; i < newOriginal.length; i++) {
×
509
            this._originalFeatures.push(newOriginal[i]);
×
510
        }
511
        const { features } = turfFlatten({ type: 'FeatureCollection', features: newOriginal });
×
512
        for (let i = 0; i < features.length; i++) {
×
513
            const feature = features[i];
×
514
            if (!feature.geometry) {
×
515
                continue;
×
516
            }
517
            const positions = featureToCartesianPositions(feature);
×
518
            if (positions === null) {
×
519
                continue;
×
520
            }
521
            this._features.push({
×
522
                ...feature,
523
                id: uuid(),
524
                positions
525
            });
526
        }
527
        this._update();
×
528
    }
529
    setOpacity(opacity) {
530
        const previousOpacity = this._opacity;
×
531
        this._opacity = opacity;
×
532
        this._update(previousOpacity !== opacity);
×
533
    }
534
    setStyleFunction(styleFunction) {
535
        this._styleFunction = styleFunction;
15✔
536
        this._update();
15✔
537
    }
538
    setFeatureFilter(featureFilter) {
539
        this._featureFilter = featureFilter;
×
540
        this._update();
×
541
    }
542
    _updateScaleVisibility() {
543
        if (!this._map) return;
15!
544
        const currentMapScale = getMapScaleForCesium(this._map);
15✔
UNCOV
545
        this._entities.forEach(wrapper => {
×
546
            const { entity} = wrapper;
×
547
            // If no custom rules → apply default visibility
548
            if (!this._styleRules || this._styleRules.length === 0) {
×
549
                entity.show = true; // Keep visible
×
550
                return;
×
551
            }
552
            const validRules = this._styleRules.filter(rule =>
×
553
                geoStylerScaleDenominatorFilter(rule, currentMapScale)
×
554
            );
555
            entity.show = validRules.length > 0;
×
556
        });
UNCOV
557
        this._map.scene.requestRender();
×
558
    }
559
    _setStyleRules(rules) {
560
        this._styleRules = rules;
×
561
    }
562
    destroy() {
563
        this._primitives.removeAll();
27✔
564
        this._map.scene.primitives.remove(this._primitives);
27✔
565
        this._dataSource.entities.removeAll();
27✔
566
        this._map.dataSources.remove(this._dataSource);
27✔
567
        this._map.camera.moveEnd.removeEventListener(this._onCameraMoveEndBound);
27✔
568
    }
569
}
570

571
GeoJSONStyledFeatures.featureToCartesianPositions = featureToCartesianPositions;
3✔
572

573
export default GeoJSONStyledFeatures;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc