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

geosolutions-it / MapStore2 / 16593937075

29 Jul 2025 10:47AM UTC coverage: 76.887% (-0.04%) from 76.925%
16593937075

push

github

web-flow
skip click on layer should return intersected features test on Cesium tests (#11365)

31344 of 48788 branches covered (64.25%)

38855 of 50535 relevant lines covered (76.89%)

36.48 hits per line

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

47.09
/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 uuid from 'uuid/v1';
6

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

26
const featureToCartesianPositions = (feature) => {
1✔
27
    if (feature.geometry.type === 'Point') {
40✔
28
        const validatedCoords = validateCoordinatesValue([
20✔
29
            feature.geometry.coordinates[0],
30
            feature.geometry.coordinates[1],
31
            feature.geometry.coordinates[2] || 0
36✔
32
        ]);
33
        return validatedCoords ? [[Cesium.Cartesian3.fromDegrees(
20!
34
            validatedCoords[0],
35
            validatedCoords[1],
36
            validatedCoords[2] || 0
36✔
37
        )]] : null;
38
    }
39
    if (feature.geometry.type === 'LineString') {
20✔
40
        const positions = feature.geometry.coordinates.map((coords, idx) =>
8✔
41
            validateCoordinatesValue(coords, feature.geometry.coordinates[idx - 1])
32!
42
                ? Cesium.Cartesian3.fromDegrees(coords[0], coords[1], coords[2])
43
                : null
44
        ).filter(coords => coords !== null);
32✔
45
        return positions.length > 1 ? [positions] : null;
8!
46
    }
47
    if (feature.geometry.type === 'Polygon') {
12!
48
        const positions = feature.geometry.coordinates.map(ring => {
12✔
49
            const ringPositions = ring.map((coords, idx) =>
12✔
50
                validateCoordinatesValue(coords, ring[idx - 1])
56✔
51
                    ? Cesium.Cartesian3.fromDegrees(coords[0], coords[1], coords[2])
52
                    : null
53
            ).filter(coords => coords !== null);
56✔
54
            return ringPositions.length > 2 ? ringPositions : null;
12✔
55
        }).filter(ring => ring !== null);
12✔
56
        return positions.length > 0 ? positions : null;
12✔
57
    }
58
    return null;
×
59
};
60
/**
61
 * Class to manage styles for layer with an array of features
62
 * @param {string} options.id layer identifier
63
 * @param {object} options.map a Cesium map instance
64
 * @param {number} options.opacity opacity of the layer
65
 * @param {boolean} options.queryable if false the features will not be queryable, default is true
66
 * @param {array} options.features array of valid geojson features
67
 * @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
68
 * @param {func} featureFilter a function to filter feature, it receives a GeoJSON feature as argument and it must return a boolean
69
 */
70
class GeoJSONStyledFeatures {
71
    constructor(options = {}) {
×
72
        this._msId = options.id;
10✔
73
        this._dataSource = new Cesium.CustomDataSource(options.id);
10✔
74
        this._primitives = new Cesium.PrimitiveCollection({ destroyPrimitives: true });
10✔
75
        this._map = options.map;
10✔
76
        if (this._map) {
10✔
77
            this._map.scene.primitives.add(this._primitives);
8✔
78
            this._map.dataSources.add(this._dataSource);
8✔
79
        }
80
        this._styledFeatures = [];
10✔
81
        this._entities = [];
10✔
82
        this._opacity = options.opacity ?? 1;
10✔
83
        this._queryable = options.queryable === undefined ? true : !!options.queryable;
10✔
84
        this._mergePolygonFeatures = !!options?.mergePolygonFeatures;
10✔
85
        this._featureFilter = options.featureFilter;
10✔
86
        this._dataSource.entities.collectionChanged.addEventListener(() => {
10✔
87
            setTimeout(() => this._map.scene.requestRender(), 300);
3✔
88
        });
89
        // internal key to associate features with original features
90
        this._uuidKey = '__ms_uuid_key__' + uuid();
10✔
91
        // needs to be run after this._uuidKey
92
        this.setFeatures(options.features);
10✔
93
    }
94
    _addCustomProperties(obj) {
95
        obj._msIsQueryable = () => this._queryable;
3✔
96
        obj._msGetFeatureById = (id) => {
3✔
97
            const _id = id instanceof Cesium.Entity ? id.id : id;
×
98
            const [featureId] = _id.split(':');
×
99
            const feature = this._features.find((f) => f.id === featureId);
×
100
            const originalFeature = this._originalFeatures.find((f) => f.properties[this._uuidKey] === feature.properties[this._uuidKey]) || {};
×
101
            const { properties } = originalFeature;
×
102
            return {
×
103
                feature: {
104
                    ...originalFeature,
105
                    properties: omit(properties, [this._uuidKey])
106
                },
107
                msId: this._msId
108
            };
109
        };
110
    }
111
    _getEntityOptions(primitive) {
112
        const { entity, geometry, orientation, minimumHeights, maximumHeights } = primitive;
3✔
113
        if (entity.polygon) {
3!
114
            return {
×
115
                polygon: {
116
                    ...entity.polygon,
117
                    hierarchy: new Cesium.ConstantProperty(new Cesium.PolygonHierarchy(
118
                        geometry[0].map(cartesian => cartesian.clone()),
×
119
                        geometry
120
                            .filter((hole, idx) => idx > 0)
×
121
                            .map(hole => new Cesium.PolygonHierarchy(hole.map((cartesian) => cartesian.clone())))
×
122
                    ))
123
                }
124
            };
125
        }
126
        if (entity.wall) {
3!
127
            return {
×
128
                wall: {
129
                    ...entity.wall,
130
                    positions: geometry[0],
131
                    minimumHeights: minimumHeights?.[0],
132
                    maximumHeights: maximumHeights?.[0]
133
                }
134
            };
135
        }
136
        if (entity.polyline) {
3!
137
            return {
×
138
                polyline: {
139
                    ...entity.polyline,
140
                    positions: geometry[0]
141
                }
142
            };
143
        }
144
        if (entity.polylineVolume) {
3!
145
            return {
×
146
                polylineVolume: {
147
                    ...entity.polylineVolume,
148
                    positions: geometry[0]
149
                }
150
            };
151
        }
152
        return {
3✔
153
            ...entity,
154
            ...(orientation && { orientation }),
3!
155
            position: geometry
156
        };
157
    }
158
    _updatePointEntity(primitive, previous) {
159
        if (previous && (primitive.entity.billboard || primitive.entity.label || primitive.entity.model)) {
3!
160
            const entity = previous.entity;
×
161
            if (primitive.orientation) {
×
162
                entity.orientation = primitive.orientation;
×
163
            }
164
            if (primitive.geometry) {
×
165
                entity.position = primitive.geometry.clone();
×
166
            }
167
            const graphicKey = Object.keys(primitive.entity)[0];
×
168
            // it's better to recreate the point entity in case height reference change
169
            if (entity?.[graphicKey]?.heightReference?.getValue() !== primitive?.entity?.[graphicKey]?.heightReference) {
×
170
                return null;
×
171
            }
172
            const propertyKeys = Object.keys(primitive.entity[graphicKey]);
×
173
            propertyKeys.forEach((propertyKey) => {
×
174
                entity[graphicKey][propertyKey] = primitive.entity[graphicKey][propertyKey];
×
175
            });
176
            return previous;
×
177
        }
178
        return null;
3✔
179
    }
180
    _filterEntities({ primitive }) {
181
        return (this._mergePolygonFeatures && primitive?.entity?.polygon) ? false : !!primitive.entity;
3!
182
    }
183
    _updateEntities(newStyledFeatures, forceUpdate) {
184
        const previousEntities = this._styledFeatures.filter((feature) => this._filterEntities(feature));
4✔
185
        const entities = newStyledFeatures.filter((feature) => this._filterEntities(feature));
4✔
186
        const previousIds = previousEntities.map(({ id }) => id);
4✔
187
        const currentIds = entities.map(({ id }) => id);
4✔
188
        const removeIds = previousIds
4✔
189
            .filter(id => !currentIds.includes(id));
×
190
        const entitiesToRemove = removeIds.map((id) => this._entities.find(entry => entry.id === id));
4✔
191
        entitiesToRemove.forEach(({ entity }) => {
4✔
192
            this._dataSource.entities.remove(entity);
×
193
        });
194
        const newEntities = entities.map(({ id, action, primitive }) => {
4✔
195
            const previous = this._entities.find(entry => entry.id === id);
3✔
196
            if (!forceUpdate && action === 'none') {
3!
197
                return previous;
×
198
            }
199
            const updatedPoint = this._updatePointEntity(primitive, previous);
3✔
200
            if (updatedPoint) {
3!
201
                return updatedPoint;
×
202
            }
203
            if (previous) {
3!
204
                this._dataSource.entities.remove(previous);
×
205
            }
206
            const entity = this._dataSource.entities.add({
3✔
207
                id,
208
                ...this._getEntityOptions(primitive)
209
            });
210
            this._addCustomProperties(entity);
3✔
211
            return { id, entity };
3✔
212
        });
213
        this._entities = newEntities;
4✔
214
    }
215
    _updatePolygonPrimitive(newStyledFeatures, forceUpdate) {
216
        const previousPolygonPrimitives = this._styledFeatures.filter(({ primitive }) => primitive.type === 'polygon' && !primitive.clampToGround);
×
217
        const polygonPrimitives = newStyledFeatures.filter(({ primitive }) => primitive.type === 'polygon' && !primitive.clampToGround);
×
218
        const polygonPrimitivesIds = polygonPrimitives.map(({ id }) => id);
×
219
        const removedPrimitives = previousPolygonPrimitives.filter(({ id }) => !polygonPrimitivesIds.includes(id));
×
220
        const noActions = !removedPrimitives.length && polygonPrimitives.length ? polygonPrimitives.every(({ action }) => !forceUpdate && action === 'none') : false;
×
221
        if (noActions) {
×
222
            return;
×
223
        }
224
        if (this?._polygonPrimitives?.length) {
×
225
            this._polygonPrimitives.forEach((primitive) => {
×
226
                this._primitives.remove(primitive);
×
227
            });
228
            this._polygonPrimitives = [];
×
229
        }
230
        if (polygonPrimitives.length <= 0) {
×
231
            return;
×
232
        }
233
        const groupByTranslucencyAndExtrusion = polygonPrimitives.reduce((acc, options) => {
×
234
            const { primitive } = options;
×
235
            const { material, extrudedHeight } = primitive?.entity?.polygon || {};
×
236
            const key = `t:${material.alpha === 1},e:${extrudedHeight !== undefined ? 'true' : 'false'}`;
×
237
            return {
×
238
                ...acc,
239
                [key]: [...(acc[key] || []), options]
×
240
            };
241
        }, {});
242
        this._polygonPrimitives = Object.keys(groupByTranslucencyAndExtrusion).map((key) => {
×
243
            const styledFeatures = groupByTranslucencyAndExtrusion[key];
×
244
            const cesiumPrimitive = new Cesium.Primitive({
×
245
                geometryInstances: styledFeatures.map(({ primitive, id }) => {
246
                    const polygon = primitive?.entity?.polygon || {};
×
247
                    return new Cesium.GeometryInstance({
×
248
                        geometry: new Cesium.PolygonGeometry({
249
                            polygonHierarchy: new Cesium.PolygonHierarchy(
250
                                primitive.geometry[0].map(cartesian => cartesian.clone()),
×
251
                                primitive.geometry
252
                                    .filter((hole, idx) => idx > 0)
×
253
                                    .map(hole => new Cesium.PolygonHierarchy(hole.map((cartesian) => cartesian.clone())))
×
254
                            ),
255
                            arcType: polygon.arcType,
256
                            perPositionHeight: polygon.height !== undefined ? false : polygon.perPositionHeight,
×
257
                            ...(polygon.height !== undefined && { height: polygon.height }),
×
258
                            ...(polygon.extrudedHeight !== undefined && { extrudedHeight: polygon.extrudedHeight }),
×
259
                            vertexFormat: Cesium.VertexFormat.POSITION_AND_NORMAL
260
                        }),
261
                        id,
262
                        attributes: {
263
                            color: new Cesium.ColorGeometryInstanceAttribute(
264
                                polygon.material.red,
265
                                polygon.material.green,
266
                                polygon.material.blue,
267
                                polygon.material.alpha
268
                            ),
269
                            // allow to click on multiple instances
270
                            show: new Cesium.ShowGeometryInstanceAttribute(true)
271
                        }
272
                    });
273
                }),
274
                appearance: new Cesium.PerInstanceColorAppearance({
275
                    flat: styledFeatures[0].primitive?.entity?.polygon?.extrudedHeight === undefined,
276
                    translucent: styledFeatures[0].primitive?.entity?.polygon?.material?.alpha !== 1
277
                }),
278
                asynchronous: true
279
            });
280
            this._addCustomProperties(cesiumPrimitive);
×
281
            return cesiumPrimitive;
×
282
        });
283
        this._polygonPrimitives.forEach((primitive) => {
×
284
            this._primitives.add(primitive);
×
285
        });
286
    }
287
    _updateGroundPolygonPrimitive(newStyledFeatures, forceUpdate) {
288
        const previousGroundPolygonPrimitives = this._styledFeatures.filter(({ primitive }) => primitive.type === 'polygon' && !!primitive.clampToGround);
×
289
        const groundPolygonPrimitives = newStyledFeatures.filter(({ primitive }) => primitive.type === 'polygon' && !!primitive.clampToGround);
×
290
        const groundPolygonPrimitivesIds = groundPolygonPrimitives.map(({ id }) => id);
×
291
        const removedPrimitives = previousGroundPolygonPrimitives.filter(({ id }) => !groundPolygonPrimitivesIds.includes(id));
×
292
        const noActions = !removedPrimitives.length && groundPolygonPrimitives.length ? groundPolygonPrimitives.every(({ action }) => !forceUpdate && action === 'none') : false;
×
293
        if (noActions) {
×
294
            return;
×
295
        }
296
        if (this?._groundPolygonPrimitives?.length) {
×
297
            this._groundPolygonPrimitives.forEach((primitive) => {
×
298
                this._primitives.remove(primitive);
×
299
            });
300
            this._groundPolygonPrimitives = [];
×
301
        }
302
        if (groundPolygonPrimitives.length <= 0) {
×
303
            return;
×
304
        }
305
        const groupByColorAndClassification = groundPolygonPrimitives.reduce((acc, options) => {
×
306
            const { primitive } = options;
×
307
            const { material, classificationType } = primitive?.entity?.polygon || {};
×
308
            const key = `r:${material.red},g:${material.green},b:${material.blue},a:${material.alpha},c:${classificationType}`;
×
309
            return {
×
310
                ...acc,
311
                [key]: [...(acc[key] || []), options]
×
312
            };
313
        }, {});
314

315
        this._groundPolygonPrimitives = Object.keys(groupByColorAndClassification).map((key) => {
×
316
            const styledFeatures = groupByColorAndClassification[key];
×
317
            const cesiumPrimitive = new Cesium.GroundPrimitive({
×
318
                classificationType: styledFeatures[0].primitive?.entity?.polygon?.classificationType,
319
                geometryInstances: styledFeatures.map(({ primitive, id }) => {
320
                    const polygon = primitive?.entity?.polygon || {};
×
321
                    return new Cesium.GeometryInstance({
×
322
                        geometry: new Cesium.PolygonGeometry({
323
                            polygonHierarchy: new Cesium.PolygonHierarchy(
324
                                primitive.geometry[0].map(cartesian => cartesian.clone()),
×
325
                                primitive.geometry
326
                                    .filter((hole, idx) => idx > 0)
×
327
                                    .map(hole => new Cesium.PolygonHierarchy(hole.map((cartesian) => cartesian.clone())))
×
328
                            ),
329
                            arcType: polygon.arcType,
330
                            perPositionHeight: polygon.perPositionHeight
331
                        }),
332
                        id,
333
                        attributes: {
334
                            color: new Cesium.ColorGeometryInstanceAttribute(
335
                                polygon.material.red,
336
                                polygon.material.green,
337
                                polygon.material.blue,
338
                                polygon.material.alpha
339
                            ),
340
                            // allow to click on multiple instances
341
                            show: new Cesium.ShowGeometryInstanceAttribute(true)
342
                        }
343
                    });
344
                }),
345
                appearance: new Cesium.PerInstanceColorAppearance({
346
                    flat: true,
347
                    translucent: styledFeatures[0].primitive?.entity?.polygon?.material?.alpha !== 1
348
                }),
349
                asynchronous: true
350
            });
351

352
            this._addCustomProperties(cesiumPrimitive);
×
353
            return cesiumPrimitive;
×
354
        });
355

356
        this._groundPolygonPrimitives.forEach((primitive) => {
×
357
            this._primitives.add(primitive);
×
358
        });
359
    }
360
    _update(forceUpdate) {
361
        if (this._timeout) {
4!
362
            clearTimeout(this._timeout);
×
363
        }
364
        this._timeout = setTimeout(() => {
4✔
365
            this._timeout = undefined;
4✔
366
            if (this._styleFunction) {
4!
367
                this._styleFunction({
4✔
368
                    map: this._map,
369
                    opacity: this._opacity,
370
                    features: this._featureFilter ? this._features.filter(this._featureFilter) : this._features,
4!
371
                    getPreviousStyledFeature: (styledFeature) => {
372
                        const editingStyleFeature = this._styledFeatures.find(({ id }) => id === styledFeature.id);
3✔
373
                        return editingStyleFeature;
3✔
374
                    }
375
                })
376
                    .then((styledFeatures) => {
377
                        this._updateEntities(styledFeatures, forceUpdate);
4✔
378
                        if (this._mergePolygonFeatures) {
4!
379
                            this._updatePolygonPrimitive(styledFeatures, forceUpdate);
×
380
                            this._updateGroundPolygonPrimitive(styledFeatures, forceUpdate);
×
381
                        }
382
                        this._styledFeatures = [...styledFeatures];
4✔
383
                        setTimeout(() => this._map.scene.requestRender());
4✔
384
                    });
385
            }
386
        }, 300);
387
    }
388
    setFeatures(newFeatures) {
389
        this._originalFeatures = (newFeatures ?? []).map((feature) => {
10!
390
            return {
9✔
391
                ...feature,
392
                properties: {
393
                    ...feature.properties,
394
                    [this._uuidKey]: feature?.properties?.[this._uuidKey] ?? uuid()
18✔
395
                }
396
            };
397
        });
398
        const { features } = turfFlatten({ type: 'FeatureCollection', features: this._originalFeatures });
10✔
399
        this._features = features.filter(feature => feature.geometry)
10✔
400
            .map((feature) => {
401
                return {
9✔
402
                    ...feature,
403
                    id: uuid(),
404
                    positions: featureToCartesianPositions(feature)
405
                };
406
            }).filter(feature => feature.positions !== null);
9✔
407

408
    }
409
    setOpacity(opacity) {
410
        const previousOpacity = this._opacity;
×
411
        this._opacity = opacity;
×
412
        this._update(previousOpacity !== opacity);
×
413
    }
414
    setStyleFunction(styleFunction) {
415
        this._styleFunction = styleFunction;
4✔
416
        this._update();
4✔
417
    }
418
    setFeatureFilter(featureFilter) {
419
        this._featureFilter = featureFilter;
×
420
        this._update();
×
421
    }
422
    destroy() {
423
        this._primitives.removeAll();
7✔
424
        this._map.scene.primitives.remove(this._primitives);
7✔
425
        this._dataSource.entities.removeAll();
7✔
426
        this._map.dataSources.remove(this._dataSource);
7✔
427
    }
428
}
429

430
GeoJSONStyledFeatures.featureToCartesianPositions = featureToCartesianPositions;
1✔
431

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

© 2026 Coveralls, Inc