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

geosolutions-it / MapStore2 / 16467617135

23 Jul 2025 10:03AM UTC coverage: 76.923% (-0.001%) from 76.924%
16467617135

Pull #11331

github

web-flow
Merge 9ab7d3e8c into 13a50aa6b
Pull Request #11331: Fix #11103 Update cesium to latest stable 1.131.0 , reviewed all the cesium layers and cesium map.

31293 of 48685 branches covered (64.28%)

45 of 59 new or added lines in 9 files covered. (76.27%)

51 existing lines in 9 files now uncovered.

38834 of 50484 relevant lines covered (76.92%)

36.5 hits per line

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

83.52
/web/client/utils/styleparser/CesiumStyleParser.js
1
/*
2
 * Copyright 2022, GeoSolutions Sas.
3
 * All rights reserved.
4
 *
5
 * This source code is licensed under the BSD-style license found in the
6
 * LICENSE file in the root directory of this source tree.
7
 */
8
import * as Cesium from 'cesium';
9
import chroma from 'chroma-js';
10
import { castArray, isNumber, isEqual, range, isNaN } from 'lodash';
11
import { needProxy, getProxyUrl } from '../ProxyUtils';
12
import {
13
    resolveAttributeTemplate,
14
    geoStylerStyleFilter,
15
    getImageIdFromSymbolizer,
16
    parseSymbolizerExpressions
17
} from './StyleParserUtils';
18
import { drawIcons } from './IconUtils';
19
import { geometryFunctionsLibrary } from './GeometryFunctionsUtils';
20
import EllipseGeometryLibrary from '@cesium/engine/Source/Core/EllipseGeometryLibrary';
21
import CylinderGeometryLibrary from '@cesium/engine/Source/Core/CylinderGeometryLibrary';
22

23
const getGeometryFunction = geometryFunctionsLibrary.cesium({ Cesium });
1✔
24

25
function getCesiumColor({ color, opacity }) {
26
    if (!color) {
51!
27
        return new Cesium.Color(0, 0, 0, 0);
×
28
    }
29
    const [r, g, b, a] = chroma(color).gl();
51✔
30
    if (opacity !== undefined) {
51!
31
        return new Cesium.Color(r, g, b, opacity);
51✔
32
    }
33
    return new Cesium.Color(r, g, b, a);
×
34
}
35

36
function getCesiumDashArray({ color, opacity, dasharray }) {
37
    if (dasharray?.length <= 0) {
4!
38
        return getCesiumColor({ color, opacity });
×
39
    }
40
    const dashLength = dasharray.reduce((acc, value) => acc + value, 0);
8✔
41
    return new Cesium.PolylineDashMaterialProperty({
4✔
42
        color: getCesiumColor({ color, opacity }),
43
        dashLength,
44
        dashPattern: parseInt((dasharray
45
            .map((value) => Math.floor(value / dashLength * 16))
8✔
46
            .map((value, idx) => range(value).map(() => idx % 2 === 0 ? '1' : '0').join(''))
64✔
47
            .join('')), 2)
48
    });
49
}
50

51
const getNumberAttributeValue = (value) => {
1✔
52
    const constantHeight = parseFloat(value);
146✔
53
    if (!isNaN(constantHeight) && isNumber(constantHeight)) {
146✔
54
        return constantHeight;
35✔
55
    }
56
    return null;
111✔
57
};
58

59
const getPositionsRelativeToTerrain = ({
1✔
60
    map,
61
    positions,
62
    heightReference: _heightReference,
63
    sampleTerrain,
64
    initialHeight
65
}) => {
66

67
    const heightReference = _heightReference  ?? 'none';
25✔
68

69
    const heightReferenceMap = {
25✔
70
        none: (originalHeight) => originalHeight,
17✔
71
        relative: (originalHeight, sampledHeight) => originalHeight + sampledHeight,
21✔
72
        clamp: (originalHeight, sampledHeight) => sampledHeight
18✔
73
    };
74

75
    let originalHeights = [];
25✔
76

77
    const computeHeight = (cartographicPositions) => {
25✔
78
        const computeHeightReference = heightReferenceMap[heightReference];
25✔
79
        let minHeight = Infinity;
25✔
80
        let maxHeight = -Infinity;
25✔
81
        let minSampledHeight = Infinity;
25✔
82
        let maxSampledHeight = -Infinity;
25✔
83
        const newPositions = cartographicPositions.map((cartographic, idx) => {
25✔
84
            const originalHeight = originalHeights[idx] || 0;
56✔
85
            const sampledHeight = heightReference === 'none' ? 0 : cartographic.height || 0;
56✔
86
            const height = computeHeightReference(originalHeight, sampledHeight);
56✔
87
            minHeight = height < minHeight ? height : minHeight;
56✔
88
            maxHeight = height > maxHeight ? height : maxHeight;
56✔
89
            minSampledHeight = sampledHeight < minSampledHeight ? sampledHeight : minSampledHeight;
56✔
90
            maxSampledHeight = sampledHeight > maxSampledHeight ? sampledHeight : maxSampledHeight;
56✔
91
            return Cesium.Cartesian3.fromRadians(
56✔
92
                cartographic.longitude,
93
                cartographic.latitude,
94
                height
95
            );
96
        });
97
        return {
25✔
98
            height: {
99
                min: minHeight,
100
                max: maxHeight
101
            },
102
            sampledHeight: {
103
                min: minSampledHeight,
104
                max: maxSampledHeight
105
            },
106
            positions: newPositions
107
        };
108
    };
109

110
    const cartographicPositions = positions.map(cartesian => {
25✔
111
        const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
56✔
112
        originalHeights.push(initialHeight ?? cartographic.height ?? 0);
56!
113
        return new Cesium.Cartographic(cartographic.longitude, cartographic.latitude, initialHeight ?? 0);
56✔
114
    });
115

116
    const terrainProvider = map?.terrainProvider;
25✔
117

118
    if (heightReference === 'none' || !terrainProvider) {
25✔
119
        return Promise.resolve(computeHeight(cartographicPositions));
14✔
120
    }
121

122
    const promise = terrainProvider?.availability
11!
123
        ? Cesium.sampleTerrainMostDetailed(
124
            terrainProvider,
125
            cartographicPositions
126
        )
127
        : sampleTerrain(
128
            terrainProvider,
129
            terrainProvider?.sampleTerrainZoomLevel ?? 18,
22✔
130
            cartographicPositions
131
        );
132
    if (Cesium.defined(promise)) {
11!
133
        return promise
11✔
134
            .then((updatedCartographicPositions) => {
135
                return computeHeight(updatedCartographicPositions);
11✔
136
            })
137
            // the sampleTerrainMostDetailed from the Cesium Terrain is still using .otherwise
138
            // and it resolve everything in the .then
139
            // while the sampleTerrain uses .catch
140
            // the optional chain help us to avoid error if catch is not exposed by the promise
141
            ?.catch?.(() => {
NEW
142
                return computeHeight(cartographicPositions);
×
143
            });
144
    }
NEW
145
    return computeHeight(cartographicPositions);
×
146

147
};
148

149
const cachedLeaderLineCanvas = {};
1✔
150

151
function createLeaderLineCanvas({
152
    offset = [],
×
153
    msLeaderLineWidth
154
}) {
155
    const lineWidth = msLeaderLineWidth ?? 1;
1!
156
    const width = Math.abs(offset[0] || 1);
1!
157
    const height = Math.abs(offset[1] || 1);
1!
158
    const isLeftTopDiagonal = Math.sign(offset[0]) === Math.sign(offset[1]);
1✔
159
    const key = [width, height, lineWidth, isLeftTopDiagonal].join(';');
1✔
160
    if (cachedLeaderLineCanvas[key]) {
1!
161
        return cachedLeaderLineCanvas[key];
×
162
    }
163
    const canvas = document.createElement('canvas');
1✔
164
    canvas.setAttribute('width', width);
1✔
165
    canvas.setAttribute('height', height);
1✔
166
    const ctx = canvas.getContext('2d');
1✔
167
    ctx.strokeStyle = '#ffffff';
1✔
168
    ctx.lineWidth = lineWidth;
1✔
169
    ctx.beginPath();
1✔
170
    ctx.moveTo(...(isLeftTopDiagonal ? [0, 0] : [width, 0]));
1!
171
    ctx.lineTo(...(isLeftTopDiagonal ? [width, height] : [0, height]));
1!
172
    ctx.stroke();
1✔
173
    cachedLeaderLineCanvas[key] = canvas;
1✔
174
    return canvas;
1✔
175
}
176

177
const translatePoint = (cartesian, symbolizer) => {
1✔
178
    const { msTranslateX, msTranslateY } = symbolizer || {};
32!
179
    const x = getNumberAttributeValue(msTranslateX);
32✔
180
    const y = getNumberAttributeValue(msTranslateY);
32✔
181
    return (x || y)
32✔
182
        ? Cesium.Matrix4.multiplyByPoint(
183
            Cesium.Transforms.eastNorthUpToFixedFrame(cartesian),
184
            new Cesium.Cartesian3(x || 0, y || 0, 0),
4!
185
            new Cesium.Cartesian3()
186
        )
187
        : cartesian;
188
};
189

190
const HEIGHT_REFERENCE_CONSTANTS_MAP = {
1✔
191
    none: 'NONE',
192
    relative: 'RELATIVE_TO_GROUND',
193
    clamp: 'CLAMP_TO_GROUND'
194
};
195

196
const anchorToOrigin = (anchor) => {
1✔
197
    switch (anchor) {
4!
198
    case 'top-left':
199
        return {
×
200
            horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
201
            verticalOrigin: Cesium.VerticalOrigin.TOP
202
        };
203
    case 'top':
204
        return {
×
205
            horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
206
            verticalOrigin: Cesium.VerticalOrigin.TOP
207
        };
208
    case 'top-right':
209
        return {
1✔
210
            horizontalOrigin: Cesium.HorizontalOrigin.RIGHT,
211
            verticalOrigin: Cesium.VerticalOrigin.TOP
212
        };
213
    case 'left':
214
        return {
×
215
            horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
216
            verticalOrigin: Cesium.VerticalOrigin.CENTER
217
        };
218
    case 'center':
219
        return {
×
220
            horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
221
            verticalOrigin: Cesium.VerticalOrigin.CENTER
222
        };
223
    case 'right':
224
        return {
×
225
            horizontalOrigin: Cesium.HorizontalOrigin.RIGHT,
226
            verticalOrigin: Cesium.VerticalOrigin.CENTER
227
        };
228
    case 'bottom-left':
229
        return {
1✔
230
            horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
231
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM
232
        };
233
    case 'bottom':
234
        return {
×
235
            horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
236
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM
237
        };
238
    case 'bottom-right':
239
        return {
×
240
            horizontalOrigin: Cesium.HorizontalOrigin.RIGHT,
241
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM
242
        };
243
    default:
244
        return {
2✔
245
            horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
246
            verticalOrigin: Cesium.VerticalOrigin.CENTER
247
        };
248
    }
249
};
250

251
const getVolumeShape = (shape = 'Square', radius = 1) => {
1!
252
    if (shape === 'Circle') {
1!
253
        const positions = [];
1✔
254
        for (let i = 0; i < 360; i++) {
1✔
255
            const radians = Cesium.Math.toRadians(i);
360✔
256
            positions.push(
360✔
257
                new Cesium.Cartesian2(
258
                    radius * Math.cos(radians),
259
                    radius * Math.sin(radians)
260
                )
261
            );
262
        }
263
        return positions;
1✔
264
    }
265
    if (shape === 'Square') {
×
266
        return [
×
267
            new Cesium.Cartesian2(-radius, -radius),
268
            new Cesium.Cartesian2(radius, -radius),
269
            new Cesium.Cartesian2(radius, radius),
270
            new Cesium.Cartesian2(-radius, radius)
271
        ];
272
    }
273
    return [];
×
274
};
275

276
const isCompatibleGeometry = ({ geometry, symbolizer }) => {
1✔
277
    if (geometry.type === 'Point' && ['Fill', 'Line'].includes(symbolizer.kind)) {
37!
278
        return false;
×
279
    }
280
    if (geometry.type === 'LineString' && ['Fill'].includes(symbolizer.kind)) {
37!
281
        return false;
×
282
    }
283
    if (geometry.type === 'Polygon' && ['Line'].includes(symbolizer.kind)) {
37!
284
        return false;
×
285
    }
286
    return true;
37✔
287
};
288

289
const getOrientation = (position, symbolizer) => {
1✔
290
    const { heading, pitch, roll } = symbolizer || {};
24!
291
    if (heading || pitch || roll) {
24!
292
        const hpr = new Cesium.HeadingPitchRoll(
×
293
            Cesium.Math.toRadians(heading ?? 0),
×
294
            Cesium.Math.toRadians(pitch ?? 0),
×
295
            Cesium.Math.toRadians(roll ?? 0));
×
296
        const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr);
×
297
        return orientation;
×
298
    }
299
    return null;
24✔
300
};
301

302
const getCirclePositions = (position, symbolizer) => {
1✔
303
    const radius = symbolizer.radius;
2✔
304
    const geodesic = symbolizer.geodesic;
2✔
305
    const slices = 128;
2✔
306
    const center = position;
2✔
307
    let positions;
308
    if (geodesic) {
2!
309
        const { outerPositions } = EllipseGeometryLibrary.computeEllipsePositions({
2✔
310
            granularity: 0.02,
311
            semiMajorAxis: radius,
312
            semiMinorAxis: radius,
313
            rotation: 0,
314
            center
315
        }, false, true);
316
        positions = Cesium.Cartesian3.unpackArray(outerPositions);
2✔
317
        positions = [...positions, positions[0]];
2✔
318
    } else {
319
        const modelMatrix = Cesium.Matrix4.multiplyByTranslation(
×
320
            Cesium.Transforms.eastNorthUpToFixedFrame(
321
                center
322
            ),
323
            new Cesium.Cartesian3(0, 0, 0),
324
            new Cesium.Matrix4()
325
        );
326
        positions = CylinderGeometryLibrary.computePositions(0.0, radius, radius, slices, false);
×
327
        positions = Cesium.Cartesian3.unpackArray(positions);
×
328
        positions = [...positions.splice(0, Math.ceil(positions.length / 2))];
×
329
        positions = positions.map((cartesian) =>
×
330
            Cesium.Matrix4.multiplyByPoint(modelMatrix, cartesian, new Cesium.Cartesian3())
×
331
        );
332
        positions = [...positions, positions[0]];
×
333
    }
334
    return positions;
2✔
335
};
336

337
const changePositionHeight = (cartesian, symbolizer) =>{
1✔
338
    const { msHeight } = symbolizer || {};
32!
339
    const height = getNumberAttributeValue(msHeight);
32✔
340
    if (height !== null) {
32✔
341
        const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
6✔
342
        return Cesium.Cartesian3.fromRadians(
6✔
343
            cartographic.longitude,
344
            cartographic.latitude,
345
            height
346
        );
347
    }
348
    return cartesian;
26✔
349
};
350

351
const primitiveGeometryTypes = {
1✔
352
    point: (options) => {
353
        const { feature, primitive, parsedSymbolizer } = options;
24✔
354
        if (feature.geometry.type === 'Point') {
24!
355
            const position = changePositionHeight(feature.positions[0][0], parsedSymbolizer);
24✔
356
            const orientation = getOrientation(position, parsedSymbolizer);
24✔
357
            return {
24✔
358
                ...options,
359
                primitive: {
360
                    ...primitive,
361
                    orientation,
362
                    geometry: translatePoint(position, parsedSymbolizer)
363
                }
364
            };
365
        }
366
        const geometryFunction = getGeometryFunction({ msGeometry: { name: 'centerPoint' }, ...parsedSymbolizer });
×
367
        const { position } = geometryFunction(feature);
×
368
        const orientation = getOrientation(position, parsedSymbolizer);
×
369
        return {
×
370
            ...options,
371
            primitive: {
372
                ...primitive,
373
                orientation,
374
                geometry: translatePoint(
375
                    changePositionHeight(position, parsedSymbolizer),
376
                    parsedSymbolizer
377
                )
378
            }
379
        };
380
    },
381
    leaderLine: (options, { map, sampleTerrain }) => {
382
        const { parsedSymbolizer } = options;
8✔
383
        // remove the translations and height options
384
        // to get the original position
385
        const { msTranslateX, msTranslateY, msHeight, ...pointParsedSymbolizer } = parsedSymbolizer;
8✔
386
        // use point function to compute geometry transformations
387
        const { primitive } = primitiveGeometryTypes.point({ ...options, parsedSymbolizer: pointParsedSymbolizer });
8✔
388
        return Promise.all([
8✔
389
            getPositionsRelativeToTerrain({
390
                map,
391
                positions: [primitive.geometry],
392
                heightReference: 'clamp',
393
                sampleTerrain
394
            }).then((computed) => computed.positions[0]),
8✔
395
            getPositionsRelativeToTerrain({
396
                map,
397
                positions: [
398
                    translatePoint(
399
                        changePositionHeight(primitive.geometry, parsedSymbolizer),
400
                        parsedSymbolizer
401
                    )
402
                ],
403
                heightReference: parsedSymbolizer.msHeightReference,
404
                sampleTerrain
405
            }).then((computed) => computed.positions[0])
8✔
406
        ]).then((positions) => {
407
            return {
8✔
408
                ...options,
409
                primitive: {
410
                    ...primitive,
411
                    geometry: [positions]
412
                }
413
            };
414
        });
415
    },
416
    polyline: (options, { map, sampleTerrain }) => {
417
        const { feature, primitive, parsedSymbolizer } = options;
13✔
418
        const extrudedHeight = getNumberAttributeValue(parsedSymbolizer.msExtrudedHeight);
13✔
419
        const height = getNumberAttributeValue(parsedSymbolizer.msHeight);
13✔
420
        if (height !== null || !!parsedSymbolizer.msExtrusionRelativeToGeometry) {
13✔
421
            let minHeight = Infinity;
5✔
422
            let maxHeight = -Infinity;
5✔
423
            return Promise.all(feature?.positions.map((positions) => {
5✔
424
                return getPositionsRelativeToTerrain({
5✔
425
                    map,
426
                    positions,
427
                    heightReference: parsedSymbolizer.msHeightReference,
428
                    sampleTerrain,
429
                    initialHeight: height
430
                }).then((computed) => {
431
                    const computedHeight = computed[parsedSymbolizer.msExtrusionRelativeToGeometry ? 'height' : 'sampledHeight'];
5✔
432
                    minHeight = computedHeight.min < minHeight ? computedHeight.min : minHeight;
5!
433
                    maxHeight = computedHeight.max > maxHeight ? computedHeight.max : maxHeight;
5!
434
                    return computed.positions;
5✔
435
                });
436
            })).then((geometry) => {
437
                return {
5✔
438
                    ...options,
439
                    primitive: {
440
                        ...primitive,
441
                        geometry,
442
                        // in case of relative or clamp
443
                        // extrusion will be relative to the height
444
                        // so 0 value should be considered undefined
445
                        extrudedHeight: extrudedHeight
5!
446
                            ? extrudedHeight + (
447
                                extrudedHeight > 0
5!
448
                                    ? maxHeight
449
                                    : minHeight
450
                            )
451
                            : undefined
452
                    }
453
                };
454
            });
455
        }
456
        return {
8✔
457
            ...options,
458
            primitive: {
459
                ...primitive,
460
                geometry: feature?.positions,
461
                ...(extrudedHeight !== null && { extrudedHeight })
9✔
462
            }
463
        };
464
    },
465
    wall: (options, configs) => {
466
        return Promise.resolve(primitiveGeometryTypes.polyline(options, configs))
5✔
467
            .then(({ primitive }) => {
468
                return {
5✔
469
                    ...options,
470
                    primitive: {
471
                        ...primitive,
472
                        geometry: primitive?.geometry,
473
                        minimumHeights: primitive?.geometry?.map((positions) => {
474
                            return positions.map((cartesian) => {
5✔
475
                                return Cesium.Cartographic.fromCartesian(cartesian).height;
20✔
476
                            });
477
                        }),
478
                        maximumHeights: primitive?.geometry?.map((positions) => {
479
                            return positions.map(() => {
5✔
480
                                return primitive.extrudedHeight;
20✔
481
                            });
482
                        })
483
                    }
484
                };
485
            });
486
    },
487
    polygon: (options, { map, sampleTerrain }) => {
488
        const { feature, primitive, parsedSymbolizer } = options;
10✔
489
        const extrudedHeight = getNumberAttributeValue(parsedSymbolizer.msExtrudedHeight);
10✔
490
        const height = getNumberAttributeValue(parsedSymbolizer.msHeight);
10✔
491
        if ((parsedSymbolizer.msHeightReference || 'none') !== 'none' || !!parsedSymbolizer.msExtrusionRelativeToGeometry) {
10✔
492
            let minHeight = Infinity;
4✔
493
            let maxHeight = -Infinity;
4✔
494
            return Promise.all(feature?.positions.map((positions) => {
4✔
495
                return getPositionsRelativeToTerrain({
4✔
496
                    map,
497
                    positions,
498
                    heightReference: parsedSymbolizer.msHeightReference,
499
                    sampleTerrain,
500
                    initialHeight: height
501
                }).then((computed) => {
502
                    const computedHeight = computed[parsedSymbolizer.msExtrusionRelativeToGeometry ? 'height' : 'sampledHeight'];
4✔
503
                    minHeight = computedHeight.min < minHeight ? computedHeight.min : minHeight;
4!
504
                    maxHeight = computedHeight.max > maxHeight ? computedHeight.max : maxHeight;
4!
505
                    return computed.positions;
4✔
506
                });
507
            })).then((geometry) => {
508
                const extrusionParams = {
4✔
509
                    // height will be computed on the geometry
510
                    height: undefined,
511
                    // in case of relative or clamp
512
                    // extrusion will be relative to the height
513
                    // so 0 value should be considered undefined
514
                    extrudedHeight: extrudedHeight
4!
515
                        ? extrudedHeight + (
516
                            extrudedHeight > 0
4!
517
                                ? maxHeight
518
                                : minHeight
519
                        )
520
                        : undefined
521
                };
522
                return {
4✔
523
                    ...options,
524
                    primitive: {
525
                        ...primitive,
526
                        geometry,
527
                        ...(primitive?.entity?.polygon
4!
528
                            ? {
529
                                entity: {
530
                                    polygon: {
531
                                        ...primitive?.entity?.polygon,
532
                                        ...extrusionParams
533
                                    }
534
                                }
535
                            }
536
                            : extrusionParams)
537
                    }
538
                };
539
            });
540
        }
541
        const extrusionParams = {
6✔
542
            ...(extrudedHeight !== null && { extrudedHeight }),
7✔
543
            ...(height !== null && {
7✔
544
                height,
545
                perPositionHeight: false
546
            })
547
        };
548
        return {
6✔
549
            ...options,
550
            primitive: {
551
                ...primitive,
552
                geometry: feature?.positions,
553
                ...(primitive?.entity?.polygon
6!
554
                    ? {
555
                        entity: {
556
                            polygon: {
557
                                ...primitive?.entity?.polygon,
558
                                ...extrusionParams
559
                            }
560
                        }
561
                    }
562
                    : extrusionParams)
563
            }
564
        };
565
    },
566
    circlePolyline: (options) => {
567
        const { feature, primitive, parsedSymbolizer } = options;
1✔
568
        if (feature.geometry.type === 'Point') {
1!
569
            const position = Cesium.Cartesian3.fromDegrees(
1✔
570
                feature.geometry.coordinates[0],
571
                feature.geometry.coordinates[1],
572
                feature.geometry.coordinates[2] || 0
2✔
573
            );
574
            const positions = getCirclePositions(position, parsedSymbolizer);
1✔
575
            return {
1✔
576
                ...options,
577
                primitive: {
578
                    ...primitive,
579
                    geometry: [positions]
580
                }
581
            };
582
        }
583
        return options;
×
584
    },
585
    circlePolygon: (options) => {
586
        const { feature, primitive, parsedSymbolizer } = options;
1✔
587
        if (feature.geometry.type === 'Point') {
1!
588
            const position = Cesium.Cartesian3.fromDegrees(
1✔
589
                feature.geometry.coordinates[0],
590
                feature.geometry.coordinates[1],
591
                feature.geometry.coordinates[2] || 0
2✔
592
            );
593
            const positions = getCirclePositions(position, parsedSymbolizer);
1✔
594
            return {
1✔
595
                ...options,
596
                primitive: {
597
                    ...primitive,
598
                    geometry: [positions]
599
                }
600
            };
601
        }
602
        return options;
×
603
    }
604
};
605

606
const symbolizerToPrimitives = {
1✔
607
    Mark: ({ parsedSymbolizer, globalOpacity, images, symbolizer }) => {
608
        const { image, width, height } = images.find(({ id }) => id === getImageIdFromSymbolizer(parsedSymbolizer, symbolizer)) || {};
9!
609
        const side = width > height ? width : height;
9!
610
        const scale = (parsedSymbolizer.radius * 2) / side;
9✔
611
        return image && !isNaN(scale) ? [
9✔
612
            {
613
                type: 'point',
614
                geometryType: 'point',
615
                entity: {
616
                    billboard: {
617
                        image,
618
                        scale,
619
                        rotation: Cesium.Math.toRadians(-1 * parsedSymbolizer.rotate || 0),
14✔
620
                        disableDepthTestDistance: parsedSymbolizer.msBringToFront ? Number.POSITIVE_INFINITY : 0,
8✔
621
                        heightReference: Cesium.HeightReference[HEIGHT_REFERENCE_CONSTANTS_MAP[parsedSymbolizer.msHeightReference] || 'NONE'],
13✔
622
                        color: getCesiumColor({
623
                            color: '#ffffff',
624
                            opacity: 1 * globalOpacity
625
                        })
626
                    }
627
                }
628
            },
629
            ...(parsedSymbolizer.msLeaderLineWidth ? [
8✔
630
                {
631
                    type: 'leaderLine',
632
                    geometryType: 'leaderLine',
633
                    entity: {
634
                        polyline: {
635
                            material: getCesiumColor({
636
                                color: parsedSymbolizer.msLeaderLineColor || '#000000',
4!
637
                                opacity: (parsedSymbolizer.msLeaderLineOpacity ?? 1) * globalOpacity
4!
638
                            }),
639
                            width: parsedSymbolizer.msLeaderLineWidth
640
                        }
641
                    }
642
                }
643
            ] : [])
644
        ] : [];
645
    },
646
    Icon: ({ parsedSymbolizer, globalOpacity, images, symbolizer }) => {
647
        const { image, width, height } = images.find(({ id }) => id === getImageIdFromSymbolizer(parsedSymbolizer, symbolizer)) || {};
4!
648
        const side = width > height ? width : height;
3!
649
        const scale = parsedSymbolizer.size / side;
3✔
650
        return image && !isNaN(scale) ? [{
3✔
651
            type: 'point',
652
            geometryType: 'point',
653
            entity: {
654
                billboard: {
655
                    image,
656
                    scale,
657
                    ...anchorToOrigin(parsedSymbolizer.anchor),
658
                    pixelOffset: parsedSymbolizer.offset ? new Cesium.Cartesian2(parsedSymbolizer.offset[0], parsedSymbolizer.offset[1]) : null,
2!
659
                    rotation: Cesium.Math.toRadians(-1 * parsedSymbolizer.rotate || 0),
2!
660
                    disableDepthTestDistance: parsedSymbolizer.msBringToFront ? Number.POSITIVE_INFINITY : 0,
2!
661
                    heightReference: Cesium.HeightReference[HEIGHT_REFERENCE_CONSTANTS_MAP[parsedSymbolizer.msHeightReference] || 'NONE'],
4✔
662
                    color: getCesiumColor({
663
                        color: '#ffffff',
664
                        opacity: parsedSymbolizer.opacity * globalOpacity
665
                    })
666
                }
667
            }
668
        },
669
        ...(parsedSymbolizer.msLeaderLineWidth ? [
2✔
670
            {
671
                type: 'leaderLine',
672
                geometryType: 'leaderLine',
673
                entity: {
674
                    polyline: {
675
                        material: getCesiumColor({
676
                            color: parsedSymbolizer.msLeaderLineColor || '#000000',
1!
677
                            opacity: (parsedSymbolizer.msLeaderLineOpacity ?? 1) * globalOpacity
1!
678
                        }),
679
                        width: parsedSymbolizer.msLeaderLineWidth
680
                    }
681
                }
682
            }
683
        ] : [])] : [];
684
    },
685
    Text: ({ parsedSymbolizer, feature, globalOpacity }) => {
686
        const offsetX = getNumberAttributeValue(parsedSymbolizer?.offset?.[0]);
2✔
687
        const offsetY = getNumberAttributeValue(parsedSymbolizer?.offset?.[1]);
2✔
688
        return [
2✔
689
            {
690
                type: 'point',
691
                geometryType: 'point',
692
                entity: {
693
                    label: {
694
                        text: resolveAttributeTemplate({ properties: feature.properties }, parsedSymbolizer.label, ''),
695
                        font: [parsedSymbolizer.fontStyle, parsedSymbolizer.fontWeight,  `${parsedSymbolizer.size}px`, castArray(parsedSymbolizer.font || ['serif']).join(', ')]
2!
696
                            .filter(val => val)
8✔
697
                            .join(' '),
698
                        fillColor: getCesiumColor({
699
                            color: parsedSymbolizer.color,
700
                            opacity: 1 * globalOpacity
701
                        }),
702
                        ...anchorToOrigin(parsedSymbolizer.anchor),
703
                        disableDepthTestDistance: parsedSymbolizer.msBringToFront ? Number.POSITIVE_INFINITY : 0,
2!
704
                        heightReference: Cesium.HeightReference[HEIGHT_REFERENCE_CONSTANTS_MAP[parsedSymbolizer.msHeightReference] || 'NONE'],
4✔
705
                        pixelOffset: new Cesium.Cartesian2(offsetX ?? 0, offsetY ?? 0),
4!
706
                        // rotation is not available as property
707
                        ...(parsedSymbolizer.haloWidth > 0 && {
4✔
708
                            style: Cesium.LabelStyle.FILL_AND_OUTLINE,
709
                            outlineColor: getCesiumColor({
710
                                color: parsedSymbolizer.haloColor,
711
                                opacity: 1 * globalOpacity
712
                            }),
713
                            outlineWidth: parsedSymbolizer.haloWidth
714
                        })
715
                    }
716
                }
717
            },
718
            ...(parsedSymbolizer.msLeaderLineWidth ? [
2✔
719
                {
720
                    type: 'leaderLine',
721
                    geometryType: 'leaderLine',
722
                    entity: {
723
                        polyline: {
724
                            material: getCesiumColor({
725
                                color: parsedSymbolizer.msLeaderLineColor || '#000000',
1!
726
                                opacity: (parsedSymbolizer.msLeaderLineOpacity ?? 1) * globalOpacity
1!
727
                            }),
728
                            width: parsedSymbolizer.msLeaderLineWidth
729
                        }
730
                    }
731
                }
732
            ] : []),
733
            ...(parsedSymbolizer.msLeaderLineWidth && (offsetX || offsetY) ? [
5!
734
                {
735
                    type: 'offset',
736
                    geometryType: 'point',
737
                    entity: {
738
                        billboard: {
739
                            image: createLeaderLineCanvas(parsedSymbolizer),
740
                            scale: 1,
741
                            pixelOffset: new Cesium.Cartesian2(
742
                                (offsetX || 0) / 2,
1!
743
                                (offsetY || 0) / 2
1!
744
                            ),
745
                            color: getCesiumColor({
746
                                color: parsedSymbolizer.msLeaderLineColor || '#000000',
1!
747
                                opacity: (parsedSymbolizer.msLeaderLineOpacity ?? 1) * globalOpacity
1!
748
                            })
749
                        }
750
                    }
751
                }
752
            ] : [])
753
        ];
754
    },
755
    Model: ({ parsedSymbolizer, globalOpacity }) => {
756
        return parsedSymbolizer?.model ? [
3!
757
            {
758
                type: 'point',
759
                geometryType: 'point',
760
                entity: {
761
                    model: {
762
                        uri: new Cesium.Resource({
763
                            proxy: needProxy(parsedSymbolizer?.model) ? new Cesium.DefaultProxy(getProxyUrl()) : undefined,
3!
764
                            url: parsedSymbolizer?.model
765
                        }),
766
                        color: getCesiumColor({
767
                            color: parsedSymbolizer.color ?? '#ffffff',
3!
768
                            opacity: (parsedSymbolizer.opacity ?? 1) * globalOpacity
3!
769
                        }),
770
                        scale: parsedSymbolizer?.scale ?? 1,
3!
771
                        heightReference: Cesium.HeightReference[HEIGHT_REFERENCE_CONSTANTS_MAP[parsedSymbolizer.msHeightReference] || 'NONE']
3!
772
                    }
773
                }
774
            },
775
            ...(parsedSymbolizer.msLeaderLineWidth ? [
3✔
776
                {
777
                    type: 'leaderLine',
778
                    geometryType: 'leaderLine',
779
                    entity: {
780
                        polyline: {
781
                            material: getCesiumColor({
782
                                color: parsedSymbolizer.msLeaderLineColor || '#000000',
2!
783
                                opacity: (parsedSymbolizer.msLeaderLineOpacity ?? 1) * globalOpacity
2!
784
                            }),
785
                            width: parsedSymbolizer.msLeaderLineWidth
786
                        }
787
                    }
788
                }
789
            ] : [])
790
        ] : [];
791
    },
792
    Line: ({ parsedSymbolizer, feature, globalOpacity }) => {
793
        const geometryFunction = getGeometryFunction(parsedSymbolizer);
8✔
794
        const additionalOptions = geometryFunction ? geometryFunction(feature) : {};
8!
795
        return [
8✔
796
            ...(parsedSymbolizer.color && parsedSymbolizer.width !== 0 ? [{
18✔
797
                type: 'polyline',
798
                geometryType: 'polyline',
799
                entity: {
800
                    polyline: {
801
                        material: parsedSymbolizer?.dasharray
2✔
802
                            ? getCesiumDashArray({
803
                                color: parsedSymbolizer.color,
804
                                opacity: parsedSymbolizer.opacity * globalOpacity,
805
                                dasharray: parsedSymbolizer.dasharray
806
                            })
807
                            : getCesiumColor({
808
                                color: parsedSymbolizer.color,
809
                                opacity: parsedSymbolizer.opacity * globalOpacity
810
                            }),
811
                        width: parsedSymbolizer.width,
812
                        clampToGround: parsedSymbolizer.msClampToGround,
813
                        arcType: parsedSymbolizer.msClampToGround
2✔
814
                            ? Cesium.ArcType.GEODESIC
815
                            : Cesium.ArcType.NONE,
816
                        ...additionalOptions
817
                    }
818
                }
819
            }] : []),
820
            ...((!parsedSymbolizer.msClampToGround && parsedSymbolizer.msExtrudedHeight && !parsedSymbolizer.msExtrusionType) ? [{
29✔
821
                type: 'polylineVolume',
822
                geometryType: 'wall',
823
                entity: {
824
                    wall: {
825
                        material: getCesiumColor({
826
                            color: parsedSymbolizer.msExtrusionColor || '#000000',
5!
827
                            opacity: (parsedSymbolizer.msExtrusionOpacity ?? 1) * globalOpacity
5!
828
                        })
829
                    }
830
                }
831
            }] : []),
832
            ...((!parsedSymbolizer.msClampToGround && parsedSymbolizer.msExtrudedHeight && parsedSymbolizer.msExtrusionType) ? [{
29✔
833
                type: 'polylineVolume',
834
                geometryType: 'polyline',
835
                entity: {
836
                    polylineVolume: {
837
                        material: getCesiumColor({
838
                            color: parsedSymbolizer.msExtrusionColor || '#000000',
1!
839
                            opacity: (parsedSymbolizer.msExtrusionOpacity ?? 1) * globalOpacity
1!
840
                        }),
841
                        shape: getVolumeShape(parsedSymbolizer.msExtrusionType, parsedSymbolizer.msExtrudedHeight / 2)
842
                    }
843
                }
844
            }] : [])
845
        ];
846
    },
847
    Fill: ({ parsedSymbolizer, feature, globalOpacity }) => {
848
        const isExtruded = !parsedSymbolizer.msClampToGround && !!parsedSymbolizer.msExtrudedHeight;
10✔
849
        const geometryFunction = getGeometryFunction(parsedSymbolizer);
10✔
850
        const additionalOptions = geometryFunction ? geometryFunction(feature) : {};
10!
851
        return [
10✔
852
            {
853
                type: 'polygon',
854
                geometryType: 'polygon',
855
                clampToGround: parsedSymbolizer.msClampToGround,
856
                entity: {
857
                    polygon: {
858
                        material: getCesiumColor({
859
                            color: parsedSymbolizer.color,
860
                            opacity: parsedSymbolizer.fillOpacity * globalOpacity
861
                        }),
862
                        perPositionHeight: !parsedSymbolizer.msClampToGround,
863
                        ...(!parsedSymbolizer.msClampToGround ? undefined : {classificationType: parsedSymbolizer.msClassificationType === 'terrain' ?
12!
864
                            Cesium.ClassificationType.TERRAIN :
865
                            parsedSymbolizer.msClassificationType === '3d' ?
×
866
                                Cesium.ClassificationType.CESIUM_3D_TILE :
867
                                Cesium.ClassificationType.BOTH} ),
868
                        arcType: parsedSymbolizer.msClampToGround
10✔
869
                            ? Cesium.ArcType.GEODESIC
870
                            : undefined,
871
                        ...additionalOptions
872
                    }
873
                }
874
            },
875
            // outline properties is not working in some browser see https://github.com/CesiumGS/cesium/issues/40
876
            // this is a workaround to visualize the outline with the correct side
877
            // this only for the footprint
878
            ...(parsedSymbolizer.outlineColor && parsedSymbolizer.outlineWidth !== 0 && !isExtruded ? [
30✔
879
                {
880
                    type: 'polyline',
881
                    geometryType: 'polyline',
882
                    entity: {
883
                        polyline: {
884
                            material: parsedSymbolizer?.outlineDasharray
5✔
885
                                ? getCesiumDashArray({
886
                                    color: parsedSymbolizer.outlineColor,
887
                                    opacity: parsedSymbolizer.outlineOpacity * globalOpacity,
888
                                    dasharray: parsedSymbolizer.outlineDasharray
889
                                })
890
                                : getCesiumColor({
891
                                    color: parsedSymbolizer.outlineColor,
892
                                    opacity: parsedSymbolizer.outlineOpacity * globalOpacity
893
                                }),
894
                            width: parsedSymbolizer.outlineWidth,
895
                            clampToGround: parsedSymbolizer.msClampToGround,
896
                            ...(!parsedSymbolizer.msClampToGround ? undefined : {classificationType: parsedSymbolizer.msClassificationType === 'terrain' ?
7!
897
                                Cesium.ClassificationType.TERRAIN :
898
                                parsedSymbolizer.msClassificationType === '3d' ?
×
899
                                    Cesium.ClassificationType.CESIUM_3D_TILE :
900
                                    Cesium.ClassificationType.BOTH} ),
901
                            arcType: parsedSymbolizer.msClampToGround
5✔
902
                                ? Cesium.ArcType.GEODESIC
903
                                : Cesium.ArcType.NONE,
904
                            ...additionalOptions
905
                        }
906
                    }
907
                }
908
            ] : [])
909
        ];
910
    },
911
    Circle: ({ parsedSymbolizer, globalOpacity }) => {
912
        return [{
1✔
913
            type: 'polygon',
914
            geometryType: 'circlePolygon',
915
            clampToGround: parsedSymbolizer.msClampToGround,
916
            entity: {
917
                polygon: {
918
                    material: getCesiumColor({
919
                        color: parsedSymbolizer.color,
920
                        opacity: parsedSymbolizer.opacity * globalOpacity
921
                    }),
922
                    ...(parsedSymbolizer.geodesic
1!
923
                        ? {
924
                            perPositionHeight: !parsedSymbolizer.msClampToGround,
925
                            ...(!parsedSymbolizer.msClampToGround ? undefined : {classificationType: parsedSymbolizer.msClassificationType === 'terrain' ?
1!
926
                                Cesium.ClassificationType.TERRAIN :
927
                                parsedSymbolizer.msClassificationType === '3d' ?
×
928
                                    Cesium.ClassificationType.CESIUM_3D_TILE :
929
                                    Cesium.ClassificationType.BOTH} ),
930
                            arcType: Cesium.ArcType.GEODESIC
931
                        }
932
                        : {
933
                            perPositionHeight: true,
934
                            arcType: undefined
935
                        })
936
                }
937
            }
938
        },
939
        // outline properties is not working in some browser see https://github.com/CesiumGS/cesium/issues/40
940
        // this is a workaround to visualize the outline with the correct side
941
        // this only for the footprint
942
        ...(parsedSymbolizer.outlineColor && parsedSymbolizer.outlineWidth !== 0) ? [{
3!
943
            type: 'polyline',
944
            geometryType: 'circlePolyline',
945
            entity: {
946
                polyline: {
947
                    material: parsedSymbolizer?.outlineDasharray
1!
948
                        ? getCesiumDashArray({
949
                            color: parsedSymbolizer.outlineColor,
950
                            opacity: parsedSymbolizer.outlineOpacity * globalOpacity,
951
                            dasharray: parsedSymbolizer.outlineDasharray
952
                        })
953
                        : getCesiumColor({
954
                            color: parsedSymbolizer.outlineColor,
955
                            opacity: parsedSymbolizer.outlineOpacity * globalOpacity
956
                        }),
957
                    width: parsedSymbolizer.outlineWidth,
958
                    ...(parsedSymbolizer.geodesic
1!
959
                        ? {
960
                            clampToGround: parsedSymbolizer.msClampToGround,
961
                            ...(!parsedSymbolizer.msClampToGround ? undefined : {classificationType: parsedSymbolizer.msClassificationType === 'terrain' ?
1!
962
                                Cesium.ClassificationType.TERRAIN :
963
                                parsedSymbolizer.msClassificationType === '3d' ?
×
964
                                    Cesium.ClassificationType.CESIUM_3D_TILE :
965
                                    Cesium.ClassificationType.BOTH} ),
966
                            arcType: Cesium.ArcType.GEODESIC
967
                        }
968
                        : {
969
                            clampToGround: false,
970
                            arcType: Cesium.ArcType.NONE
971
                        })
972
                }
973
            }
974
        }] : []];
975
    }
976
};
977

978
const isGeometryChanged = (previousSymbolizer, currentSymbolizer) => {
1✔
979
    return previousSymbolizer?.msGeometry?.name !== currentSymbolizer?.msGeometry?.name
×
980
        || previousSymbolizer?.msHeight !== currentSymbolizer?.msHeight
981
        || previousSymbolizer?.msHeightReference !== currentSymbolizer?.msHeightReference
982
        || previousSymbolizer?.msExtrudedHeight !== currentSymbolizer?.msExtrudedHeight
983
        || previousSymbolizer?.msExtrusionRelativeToGeometry !== currentSymbolizer?.msExtrusionRelativeToGeometry
984
        || previousSymbolizer?.msExtrusionType !== currentSymbolizer?.msExtrusionType
985
        || previousSymbolizer?.msTranslateX !== currentSymbolizer?.msTranslateX
986
        || previousSymbolizer?.msTranslateY !== currentSymbolizer?.msTranslateY
987
        || previousSymbolizer?.heading !== currentSymbolizer?.heading
988
        || previousSymbolizer?.pitch !== currentSymbolizer?.pitch
989
        || previousSymbolizer?.roll !== currentSymbolizer?.roll;
990
};
991

992
const isSymbolizerChanged = (previousSymbolizer, currentSymbolizer) => {
1✔
993
    const { msGeometry: previousMsGeometry, ...previous } = previousSymbolizer;
×
994
    const { msGeometry, ...current } = currentSymbolizer;
×
995
    return !isEqual(previous, current);
×
996
};
997

998
const getStyledFeatures = ({ rules, features, globalOpacity, images }) => {
1✔
999
    return rules?.map((rule) => {
33✔
1000
        const filteredFeatures = features
36✔
1001
            .filter(({ properties }) => !rule.filter || geoStylerStyleFilter({ properties: properties || {}}, rule.filter));
49!
1002
        return rule.symbolizers.map((symbolizer) => {
36✔
1003
            return filteredFeatures.filter(({ geometry }) => isCompatibleGeometry({ geometry, symbolizer })).map((feature) => {
37✔
1004
                const parsedSymbolizer = parseSymbolizerExpressions(symbolizer, feature);
37✔
1005
                const primitivesFunction = symbolizerToPrimitives[parsedSymbolizer.kind]
37✔
1006
                    ? symbolizerToPrimitives[parsedSymbolizer.kind]
1007
                    : () => [];
1✔
1008
                return primitivesFunction({
37✔
1009
                    feature,
1010
                    symbolizer,
1011
                    images,
1012
                    parsedSymbolizer,
1013
                    globalOpacity
1014
                }).map((primitive) => ({
49✔
1015
                    id: `${feature.id}:${parsedSymbolizer.symbolizerId}:${primitive.type}`,
1016
                    feature,
1017
                    primitive,
1018
                    symbolizer,
1019
                    parsedSymbolizer
1020
                }));
1021
            }).flat();
1022
        }).flat();
1023
    }).flat();
1024
};
1025

1026
function getStyleFuncFromRules({
×
1027
    rules = []
×
1028
} = {}) {
1029
    return ({
33✔
1030
        opacity: globalOpacity = 1,
28✔
1031
        features,
1032
        getPreviousStyledFeature = () => {},
28✔
1033
        map,
1034
        sampleTerrain = Cesium.sampleTerrain
24✔
1035
    }) => {
1036
        return drawIcons({ rules }, { features })
33✔
1037
            .then((images) => {
1038
                const styledFeatures = getStyledFeatures({ rules, features, globalOpacity, images });
33✔
1039
                return Promise.all(styledFeatures.map((currentFeature) => {
33✔
1040
                    const previousFeature = getPreviousStyledFeature(currentFeature);
49✔
1041
                    if (!previousFeature || isGeometryChanged(previousFeature.parsedSymbolizer, currentFeature.parsedSymbolizer)) {
49!
1042
                        const computeGeometry = primitiveGeometryTypes[currentFeature?.primitive?.geometryType]
49!
1043
                            ? primitiveGeometryTypes[currentFeature?.primitive?.geometryType]
1044
                            : () => currentFeature;
×
1045
                        return Promise.resolve(computeGeometry(currentFeature, { map, sampleTerrain }))
49✔
1046
                            .then((payload) => {
1047
                                return {
49✔
1048
                                    ...payload,
1049
                                    action: 'replace'
1050
                                };
1051
                            });
1052
                    }
1053
                    return Promise.resolve({
×
1054
                        ...currentFeature,
1055
                        primitive: {
1056
                            ...previousFeature?.primitive,
1057
                            ...currentFeature?.primitive,
1058
                            geometry: previousFeature?.primitive?.geometry,
1059
                            entity: {
1060
                                ...(Object.keys(currentFeature?.primitive?.entity || {}).reduce((acc, key) => {
×
1061
                                    return {
×
1062
                                        ...acc,
1063
                                        [key]: {
1064
                                            ...previousFeature?.primitive?.entity?.[key],
1065
                                            ...currentFeature?.primitive?.entity?.[key]
1066
                                        }
1067
                                    };
1068
                                }, {}))
1069
                            }
1070
                        },
1071
                        action: isSymbolizerChanged(previousFeature.parsedSymbolizer, currentFeature.parsedSymbolizer)
×
1072
                            ? 'update'
1073
                            : 'none'
1074
                    });
1075
                }));
1076
            })
1077
            .then((updatedStyledFeatures) => {
1078
                // remove all styled features without geometry
1079
                return updatedStyledFeatures.filter(({ primitive }) => !!primitive.geometry);
49✔
1080
            });
1081
    };
1082
}
1083

1084
class CesiumStyleParser {
1085

1086
    readStyle() {
1087
        return new Promise((resolve, reject) => {
1✔
1088
            try {
1✔
1089
                resolve(null);
1✔
1090
            } catch (error) {
1091
                reject(error);
×
1092
            }
1093
        });
1094
    }
1095

1096
    writeStyle(geoStylerStyle) {
1097
        return new Promise((resolve, reject) => {
33✔
1098
            try {
33✔
1099
                const styleFunc = getStyleFuncFromRules(geoStylerStyle);
33✔
1100
                resolve(styleFunc);
33✔
1101
            } catch (error) {
1102
                reject(error);
×
1103
            }
1104
        });
1105
    }
1106
}
1107

1108
export default CesiumStyleParser;
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