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

geosolutions-it / MapStore2 / 19735587487

27 Nov 2025 09:59AM UTC coverage: 76.667% (-0.3%) from 76.929%
19735587487

Pull #11119

github

web-flow
Fix: #11712 Support for template format on vector layers to visualize embedded conent (#11720)
Pull Request #11119: Layer Selection Plugin on ArcGIS, WFS & WMS layers

32268 of 50209 branches covered (64.27%)

7 of 13 new or added lines in 2 files covered. (53.85%)

3017 existing lines in 248 files now uncovered.

40158 of 52380 relevant lines covered (76.67%)

37.8 hits per line

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

54.42
/web/client/utils/styleparser/StyleParserUtils.js
1
/*
2
 * Copyright 2023, 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

9
// part of the code below is from https://github.com/geostyler/geostyler-openlayers-parser/tree/v4.1.2
10
// BSD 2-Clause License
11

12
// Copyright (c) 2018, terrestris GmbH & Co. KG
13
// All rights reserved.
14

15
// Redistribution and use in source and binary forms, with or without
16
// modification, are permitted provided that the following conditions are met:
17

18
// * Redistributions of source code must retain the above copyright notice, this
19
//   list of conditions and the following disclaimer.
20

21
// * Redistributions in binary form must reproduce the above copyright notice,
22
//   this list of conditions and the following disclaimer in the documentation
23
//   and/or other materials provided with the distribution.
24

25
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
26
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
28
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
29
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
30
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
31
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
33
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35

36
import tinycolor from 'tinycolor2';
37
import axios from 'axios';
38
import isNil from 'lodash/isNil';
39
import isObject from 'lodash/isObject';
40
import MarkerUtils from '../MarkerUtils';
41
import {randomInt} from '../RandomUtils';
42

43

44
export const isGeoStylerBooleanFunction = (got) => [
1,122✔
45
    'between',
46
    'double2bool',
47
    'in',
48
    'parseBoolean',
49
    'strEndsWith',
50
    'strEqualsIgnoreCase',
51
    'strMatches',
52
    'strStartsWith'
53
].includes(got?.name);
54
export const isGeoStylerNumberFunction = (got) => [
1,132✔
55
    'abs',
56
    'acos',
57
    'asin',
58
    'atan',
59
    'atan2',
60
    'ceil',
61
    'cos',
62
    'exp',
63
    'floor',
64
    'log',
65
    'max',
66
    'min',
67
    'modulo',
68
    'pi',
69
    'pow',
70
    'random',
71
    'rint',
72
    'round',
73
    'sin',
74
    'sqrt',
75
    'strIndexOf',
76
    'strLastIndexOf',
77
    'strLength',
78
    'tan',
79
    'toDegrees',
80
    'toRadians'
81
].includes(got?.name);
82
export const isGeoStylerStringFunction = (got) => [
1,153✔
83
    'numberFormat',
84
    'strAbbreviate',
85
    'strCapitalize',
86
    'strConcat',
87
    'strDefaultIfBlank',
88
    'strReplace',
89
    'strStripAccents',
90
    'strSubstring',
91
    'strSubstringStart',
92
    'strToLowerCase',
93
    'strToUpperCase',
94
    'strTrim'
95
].includes(got?.name);
96
export const isGeoStylerUnknownFunction = (got) => [
1,122✔
97
    'property'
98
].includes(got?.name);
99
export const isGeoStylerMapStoreFunction = (got) => got?.type === 'attribute' || [
1,103✔
100
    'msMarkerIcon'
101
].includes(got?.name);
102
export const isGeoStylerFunction = (got) =>
1✔
103
    isGeoStylerBooleanFunction(got)
1,120✔
104
    || isGeoStylerNumberFunction(got)
105
    || isGeoStylerStringFunction(got)
106
    || isGeoStylerUnknownFunction(got)
107
    || isGeoStylerMapStoreFunction(got);
108
const getFeatureProperties = (feature) => {
1✔
109
    return (feature?.getProperties
105!
110
        ? feature.getProperties()
111
        : feature?.properties) || {};
112
};
113
export const expressionsUtils = {
1✔
114
    evaluateFunction: (func, feature) => {
115
        const properties = getFeatureProperties(feature);
22✔
116
        if (func.name === 'property') {
22✔
117
            if (!feature) {
20!
UNCOV
118
                throw new Error(`Could not evalute 'property' function. Feature ${feature} is not defined.`);
×
119
            }
120
            if (isGeoStylerStringFunction(func.args[0])) {
20!
UNCOV
121
                return properties[expressionsUtils.evaluateStringFunction(func.args[0], feature)];
×
122
            }
123
            return properties[func.args[0]];
20✔
124
        }
125
        if (isGeoStylerStringFunction(func)) {
2!
UNCOV
126
            return expressionsUtils.evaluateStringFunction(func, feature);
×
127
        }
128
        if (isGeoStylerNumberFunction(func)) {
2!
UNCOV
129
            return expressionsUtils.evaluateNumberFunction(func, feature);
×
130
        }
131
        if (isGeoStylerBooleanFunction(func)) {
2!
UNCOV
132
            return expressionsUtils.evaluateBooleanFunction(func, feature);
×
133
        }
134
        if (isGeoStylerUnknownFunction(func)) {
2!
UNCOV
135
            return expressionsUtils.evaluateUnknownFunction(func, feature);
×
136
        }
137
        if (isGeoStylerMapStoreFunction(func)) {
2!
138
            return expressionsUtils.evaluateMapStoreFunction(func, feature);
2✔
139
        }
UNCOV
140
        return null;
×
141
    },
142
    evaluateBooleanFunction: (func, feature) => {
UNCOV
143
        const args = func.args.map(arg => {
×
144
            if (isGeoStylerFunction(arg)) {
×
145
                return expressionsUtils.evaluateFunction(arg, feature);
×
146
            }
UNCOV
147
            return arg;
×
148
        });
UNCOV
149
        switch (func.name) {
×
150
        case 'between':
UNCOV
151
            return (args[0]) >= (args[1]) && (args[0]) <= (args[2]);
×
152
        case 'double2bool':
153
            // TODO: evaluate this correctly
UNCOV
154
            return false;
×
155
        case 'in':
UNCOV
156
            return args.slice(1).includes(args[0]);
×
157
        case 'parseBoolean':
UNCOV
158
            return !!args[0];
×
159
        case 'strEndsWith':
UNCOV
160
            return (args[0]).endsWith(args[1]);
×
161
        case 'strEqualsIgnoreCase':
UNCOV
162
            return (args[0]).toLowerCase() === (args[1]).toLowerCase();
×
163
        case 'strMatches':
UNCOV
164
            return new RegExp(args[1]).test(args[0]);
×
165
        case 'strStartsWith':
UNCOV
166
            return (args[0]).startsWith(args[1]);
×
167
        default:
UNCOV
168
            return false;
×
169
        }
170
    },
171
    evaluateNumberFunction: (func, feature) => {
UNCOV
172
        if (func.name === 'pi') {
×
173
            return Math.PI;
×
174
        }
UNCOV
175
        if (func.name === 'random') {
×
176
            return randomInt();
×
177
        }
UNCOV
178
        const args = func.args.map(arg => {
×
179
            if (isGeoStylerFunction(arg)) {
×
180
                return expressionsUtils.evaluateFunction(arg, feature);
×
181
            }
UNCOV
182
            return arg;
×
183
        });
UNCOV
184
        switch (func.name) {
×
185
        case 'abs':
UNCOV
186
            return Math.abs(args[0]);
×
187
        case 'acos':
UNCOV
188
            return Math.acos(args[0]);
×
189
        case 'asin':
UNCOV
190
            return Math.asin(args[0]);
×
191
        case 'atan':
UNCOV
192
            return Math.atan(args[0]);
×
193
        case 'atan2':
194
            // TODO: evaluate this correctly
UNCOV
195
            return args[0];
×
196
        case 'ceil':
UNCOV
197
            return Math.ceil(args[0]);
×
198
        case 'cos':
UNCOV
199
            return Math.cos(args[0]);
×
200
        case 'exp':
UNCOV
201
            return Math.exp(args[0]);
×
202
        case 'floor':
UNCOV
203
            return Math.floor(args[0]);
×
204
        case 'log':
UNCOV
205
            return Math.log(args[0]);
×
206
        case 'max':
UNCOV
207
            return Math.max(...(args));
×
208
        case 'min':
UNCOV
209
            return Math.min(...(args));
×
210
        case 'modulo':
UNCOV
211
            return (args[0]) % (args[1]);
×
212
        case 'pow':
UNCOV
213
            return Math.pow(args[0], args[1]);
×
214
        case 'rint':
215
            // TODO: evaluate this correctly
UNCOV
216
            return args[0];
×
217
        case 'round':
UNCOV
218
            return Math.round(args[0]);
×
219
        case 'sin':
UNCOV
220
            return Math.sin(args[0]);
×
221
        case 'sqrt':
UNCOV
222
            return Math.sqrt(args[0]);
×
223
        case 'strIndexOf':
UNCOV
224
            return (args[0]).indexOf(args[1]);
×
225
        case 'strLastIndexOf':
UNCOV
226
            return (args[0]).lastIndexOf(args[1]);
×
227
        case 'strLength':
UNCOV
228
            return (args[0]).length;
×
229
        case 'tan':
UNCOV
230
            return Math.tan(args[0]);
×
231
        case 'toDegrees':
UNCOV
232
            return (args[0]) * (180 / Math.PI);
×
233
        case 'toRadians':
UNCOV
234
            return (args[0]) * (Math.PI / 180);
×
235
        default:
UNCOV
236
            return args[0];
×
237
        }
238
    },
239
    evaluateUnknownFunction: (func, feature) => {
UNCOV
240
        const args = func.args.map(arg => {
×
241
            if (isGeoStylerFunction(arg)) {
×
242
                return expressionsUtils.evaluateFunction(arg, feature);
×
243
            }
UNCOV
244
            return arg;
×
245
        });
UNCOV
246
        const properties = getFeatureProperties(feature);
×
247
        switch (func.name) {
×
248
        case 'property':
UNCOV
249
            return properties[args[0]];
×
250
        default:
UNCOV
251
            return args[0];
×
252
        }
253
    },
254
    evaluateStringFunction: (func, feature) => {
UNCOV
255
        const args = func.args.map(arg => {
×
256
            if (isGeoStylerFunction(arg)) {
×
257
                return expressionsUtils.evaluateFunction(arg, feature);
×
258
            }
UNCOV
259
            return arg;
×
260
        });
UNCOV
261
        switch (func.name) {
×
262
        case 'numberFormat':
263
            // TODO: evaluate this correctly
UNCOV
264
            return args[0];
×
265
        case 'strAbbreviate':
266
            // TODO: evaluate this correctly
UNCOV
267
            return args[0];
×
268
        case 'strCapitalize':
269
            // https://stackoverflow.com/a/32589289/10342669
UNCOV
270
            let splitStr = (args[0]).toLowerCase().split(' ');
×
271
            for (let part of splitStr) {
×
272
                part = part.charAt(0).toUpperCase() + part.substring(1);
×
273
            }
UNCOV
274
            return splitStr.join(' ');
×
275
        case 'strConcat':
UNCOV
276
            return args.join();
×
277
        case 'strDefaultIfBlank':
UNCOV
278
            return (args[0])?.length < 1 ? args[1] : args[0];
×
279
        case 'strReplace':
UNCOV
280
            if (args[3] === true) {
×
281
                return (args[0]).replaceAll(args[1], args[2]);
×
282
            }
UNCOV
283
            return (args[0]).replace(args[1], args[2]);
×
284
        case 'strStripAccents':
285
            // https://stackoverflow.com/a/37511463/10342669
UNCOV
286
            return (args[0]).normalize('NFKD').replace(/[\u0300-\u036f]/g, '');
×
287
        case 'strSubstring':
UNCOV
288
            return (args[0]).substring(args[1], args[2]);
×
289
        case 'strSubstringStart':
UNCOV
290
            return (args[0]).substring(args[1]);
×
291
        case 'strToLowerCase':
UNCOV
292
            return (args[0]).toLowerCase();
×
293
        case 'strToUpperCase':
UNCOV
294
            return (args[0]).toUpperCase();
×
295
        case 'strTrim':
UNCOV
296
            return (args[0]).trim();
×
297
        default:
UNCOV
298
            return args[0];
×
299
        }
300
    },
301
    evaluateMapStoreFunction: (func, feature) => {
302
        if (func.type === 'attribute') {
2✔
303
            return expressionsUtils.evaluateFunction({
1✔
304
                name: 'property',
305
                args: [func.name]
306
            }, feature);
307
        }
308
        const args = func.args.map(arg => {
1✔
309
            if (isGeoStylerFunction(arg)) {
1!
UNCOV
310
                return expressionsUtils.evaluateFunction(arg, feature);
×
311
            }
312
            return arg;
1✔
313
        });
314
        switch (func.name) {
1!
315
        case 'msMarkerIcon':
316
            return MarkerUtils.markers.extra.markerToDataUrl({
1✔
317
                iconColor: args[0].color,
318
                iconShape: args[0].shape,
319
                iconGlyph: args[0].glyph
320
            });
321
        default:
UNCOV
322
            return args[0];
×
323
        }
324
    }
325
};
326
/**
327
 * creates geometry function utils for Cesium library
328
 * @param {object} feature a GeoJSON feature
329
 * @param {array} filter array expresses the filter structure in geostyler format
330
 * @returns {boolean} true if the feature should be visualize based on the filter content
331
 */
332
export const geoStylerStyleFilter = (feature, filter) => {
1✔
333
    const properties = getFeatureProperties(feature);
77✔
334
    const operatorMapping = {
77✔
335
        '&&': true,
336
        '||': true,
337
        '!': true
338
    };
339
    let matchesFilter = true;
77✔
340
    const operator = filter[0];
77✔
341
    let isNestedFilter = false;
77✔
342
    if (operatorMapping[operator]) {
77✔
343
        isNestedFilter = true;
19✔
344
    }
345
    try {
77✔
346
        if (isNestedFilter) {
77✔
347
            let intermediate;
348
            let restFilter;
349
            switch (filter[0]) {
19!
350
            case '&&':
351
                intermediate = true;
17✔
352
                restFilter = filter.slice(1);
17✔
353
                restFilter.forEach((f) => {
17✔
354
                    if (!geoStylerStyleFilter(feature, f)) {
27✔
355
                        intermediate = false;
11✔
356
                    }
357
                });
358
                matchesFilter = intermediate;
17✔
359
                break;
17✔
360
            case '||':
361
                intermediate = false;
2✔
362
                restFilter = filter.slice(1);
2✔
363
                restFilter.forEach((f) => {
2✔
364
                    if (geoStylerStyleFilter(feature, f)) {
4✔
365
                        intermediate = true;
1✔
366
                    }
367
                });
368
                matchesFilter = intermediate;
2✔
369
                break;
2✔
370
            case '!':
UNCOV
371
                matchesFilter = !this.geoStylerFilterToOlParserFilter(feature, filter[1]);
×
372
                break;
×
373
            default:
UNCOV
374
                throw new Error('Cannot parse Filter. Unknown combination or negation operator.');
×
375
            }
376
        } else {
377
            let arg1;
378
            if (isGeoStylerFunction(filter[1])) {
58!
UNCOV
379
                arg1 = expressionsUtils.evaluateFunction(filter[1], feature);
×
380
            } else {
381
                arg1 = properties[filter[1]];
58✔
382
            }
383
            let arg2;
384
            if (isGeoStylerFunction(filter[2])) {
58!
UNCOV
385
                arg2 = properties[expressionsUtils.evaluateFunction(filter[2], feature)];
×
386
            } else {
387
                arg2 = filter[2];
58✔
388
            }
389
            switch (filter[0]) {
58!
390
            case '==':
391
                matchesFilter = ('' + arg1) === ('' + arg2);
27✔
392
                break;
27✔
393
            case '*=':
394
                // inspired by
395
                // https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/String/includes#Polyfill
396
                if (typeof arg2 === 'string' && typeof arg1 === 'string') {
6!
397
                    if (arg2.length > arg1.length) {
6!
UNCOV
398
                        matchesFilter = false;
×
399
                    } else {
400
                        matchesFilter = arg1.indexOf(arg2) !== -1;
6✔
401
                    }
402
                }
403
                break;
6✔
404
            case '!=':
405
                matchesFilter = ('' + arg1) !== ('' + arg2);
1✔
406
                break;
1✔
407
            case '<':
408
                matchesFilter = Number(arg1) < Number(arg2);
10✔
409
                break;
10✔
410
            case '<=':
411
                matchesFilter = Number(arg1) <= Number(arg2);
4✔
412
                break;
4✔
413
            case '>':
414
                matchesFilter = Number(arg1) > Number(arg2);
7✔
415
                break;
7✔
416
            case '>=':
417
                matchesFilter = Number(arg1) >= Number(arg2);
3✔
418
                break;
3✔
419
            default:
UNCOV
420
                throw new Error('Cannot parse Filter. Unknown comparison operator.');
×
421
            }
422
        }
423
    } catch (e) {
UNCOV
424
        throw new Error('Cannot parse Filter. Invalid structure.');
×
425
    }
426
    return matchesFilter;
77✔
427
};
428
/**
429
 * parse a string template and replace the placeholders with feature properties
430
 * @param {object} feature a GeoJSON feature
431
 * @param {string} template a string with properties placeholder, eg '{{label}} some plain text'
432
 * @param {string} noValueFoundText a fallback string for placeholder
433
 * @returns {string} true if the feature should be visualize based on the filter content
434
 */
435
export const resolveAttributeTemplate = (
1✔
436
    feature,
437
    template,
438
    noValueFoundText = 'n.v.',
×
439
    valueAdjust = (key, val) => val
5✔
440
) => {
441

442
    const properties = getFeatureProperties(feature);
6✔
443

444
    let _template = template || '';
6!
445
    let attributeTemplatePrefix = '\\{\\{';
6✔
446
    let attributeTemplateSuffix = '\\}\\}';
6✔
447

448
    // Find any character between two braces (including the braces in the result)
449
    let regExp = new RegExp(attributeTemplatePrefix + '(.*?)' + attributeTemplateSuffix, 'g');
6✔
450
    let regExpRes = _template.match(regExp);
6✔
451

452
    // If we have a regex result, it means we found a placeholder in the
453
    // template and have to replace the placeholder with its appropriate value.
454
    if (regExpRes) {
6✔
455
        // Iterate over all regex match results and find the proper attribute
456
        // for the given placeholder, finally set the desired value to the hover.
457
        // field text
458
        regExpRes.forEach(res => {
5✔
459
            // We count every non matching candidate. If this count is equal to
460
            // the objects length, we assume that there is no match at all and
461
            // set the output value to the value of "noValueFoundText".
462
            let noMatchCnt = 0;
5✔
463

464
            for (let [key, value] of Object.entries(properties)) {
5✔
465
                // Remove the suffixes and find the matching attribute column.
466
                let attributeName = res.slice(2, res.length - 2);
5✔
467

468
                if (attributeName.toLowerCase() === key.toLowerCase()) {
5!
469
                    _template = _template.replace(res, valueAdjust(key, value));
5✔
470
                    break;
5✔
471
                } else {
UNCOV
472
                    noMatchCnt++;
×
473
                }
474
            }
475

476
            // No key match found for this feature (e.g. if key not
477
            // present or value is null).
478
            if (noMatchCnt === Object.keys(properties).length) {
5!
UNCOV
479
                _template = _template.replace(res, noValueFoundText);
×
480
            }
481
        });
482
    }
483

484
    return _template;
6✔
485
};
486

487
let imagesCache = {};
1✔
488

489
/**
490
 * generate an id based on a Mark symbolizer
491
 * @param {object} symbolizer mark symbolizer
492
 * @returns {string} an id for the mark symbolizer
493
 */
494
export const _getImageIdFromSymbolizer = ({
1✔
495
    image,
496
    color,
497
    fillOpacity,
498
    strokeColor,
499
    strokeOpacity,
500
    strokeWidth,
501
    strokeDasharray,
502
    radius,
503
    wellKnownName
504
}) => {
505
    if (image) {
86✔
506
        return image?.name === 'msMarkerIcon' ? `msMarkerIcon:${image?.args?.[0]?.color}:${image?.args?.[0]?.shape}:${image?.args?.[0]?.glyph}` : image;
19✔
507
    }
508
    return [wellKnownName, color, fillOpacity, strokeColor, strokeOpacity, (strokeDasharray || []).join('_'), strokeWidth, radius].join(':');
67✔
509
};
510
/**
511
 * generate an id based on a Mark symbolizer
512
 * @param {object} parsedSymbolizer the parsed mark symbolizer
513
 * @param {object} originalSymbolizer the original mark symbolizer
514
 * @returns {string} an id for the mark symbolizer
515
 */
516
export const getImageIdFromSymbolizer = (parsedSymbolizer, originalSymbolizer) => {
1✔
517
    return _getImageIdFromSymbolizer(originalSymbolizer?.image?.name === 'msMarkerIcon' ? originalSymbolizer : parsedSymbolizer);
86✔
518
};
519

520
/**
521
 * prefetch images of a icon symbolizer
522
 * @param {object} symbolizer icon symbolizer
523
 * @returns {promise} returns the image
524
 */
525
export const getImageFromSymbolizer = (symbolizer) => {
1✔
526
    const image = symbolizer.image;
8✔
527
    const id = getImageIdFromSymbolizer(symbolizer);
8✔
528
    if (imagesCache[id]) {
8✔
529
        return Promise.resolve(imagesCache[id]);
6✔
530
    }
531
    return new Promise((resolve, reject) => {
2✔
532
        const img = new Image();
2✔
533
        let src = image;
2✔
534
        if (isObject(src) && src.name === 'msMarkerIcon') {
2!
UNCOV
535
            try {
×
536
                const msMarkerIcon = src.args[0];
×
537
                src = MarkerUtils.markers.extra.markerToDataUrl({
×
538
                    iconColor: msMarkerIcon.color,
539
                    iconShape: msMarkerIcon.shape,
540
                    iconGlyph: msMarkerIcon.glyph
541
                });
542
            } catch (e) {
UNCOV
543
                reject(id);
×
544
            }
545
        }
546
        img.crossOrigin = 'anonymous';
2✔
547
        img.onload = () => {
2✔
548
            imagesCache[id] = { id, image: img, src, width: img.naturalWidth, height: img.naturalHeight };
2✔
549
            resolve(imagesCache[id]);
2✔
550
        };
551
        img.onerror = () => {
2✔
UNCOV
552
            reject(id);
×
553
        };
554
        img.src = src;
2✔
555
    });
556
};
557

558
// http://jsfiddle.net/m1erickson/8j6kdf4o/
559
const paintStar = (ctx, cx, cy, spikes = 5, outerRadius, innerRadius) => {
1!
560
    let rot = Math.PI / 2 * 3;
2✔
561
    let x = cx;
2✔
562
    let y = cy;
2✔
563
    const step = Math.PI / spikes;
2✔
564
    ctx.moveTo(cx, cy - outerRadius);
2✔
565
    for (let i = 0; i < spikes; i++) {
2✔
566
        x = cx + Math.cos(rot) * outerRadius;
10✔
567
        y = cy + Math.sin(rot) * outerRadius;
10✔
568
        ctx.lineTo(x, y);
10✔
569
        rot += step;
10✔
570

571
        x = cx + Math.cos(rot) * innerRadius;
10✔
572
        y = cy + Math.sin(rot) * innerRadius;
10✔
573
        ctx.lineTo(x, y);
10✔
574
        rot += step;
10✔
575
    }
576
    ctx.lineTo(cx, cy - outerRadius);
2✔
577
    ctx.closePath();
2✔
578
};
579

580
const paintCross = (ctx, cx, cy, r, p) => {
1✔
UNCOV
581
    const w = r * p;
×
582
    const wm = w / 2;
×
583
    const rm = r / 2;
×
584
    ctx.moveTo(cx - wm, cy - rm);
×
585
    ctx.lineTo(cx + wm, cy - rm);
×
586
    ctx.lineTo(cx + wm, cy - wm);
×
587
    ctx.lineTo(cx + rm, cy - wm);
×
588
    ctx.lineTo(cx + rm, cy + wm);
×
589
    ctx.lineTo(cx + wm, cy + wm);
×
590
    ctx.lineTo(cx + wm, cy + rm);
×
591
    ctx.lineTo(cx - wm, cy + rm);
×
592
    ctx.lineTo(cx - wm, cy + wm);
×
593
    ctx.lineTo(cx - rm, cy + wm);
×
594
    ctx.lineTo(cx - rm, cy - wm);
×
595
    ctx.lineTo(cx - wm, cy - wm);
×
596
    ctx.closePath();
×
597
};
598

599
/**
600
 * draw on a canvas the mark symbol
601
 * @param {object} symbolizer mark symbolizer
602
 * @returns {object} { width, height, canvas }
603
 */
604
export const drawWellKnownNameImageFromSymbolizer = (symbolizer) => {
1✔
605
    const id = getImageIdFromSymbolizer(symbolizer);
12✔
606
    if (imagesCache[id]) {
12✔
607
        const { image, ...other } = imagesCache[id];
1✔
608
        return { ...other, canvas: image };
1✔
609
    }
610
    const hasStroke = !!symbolizer?.strokeWidth
11✔
611
        && !!symbolizer?.strokeOpacity;
612
    const hasFill = !!symbolizer?.fillOpacity
11✔
613
        && !(symbolizer.wellKnownName || '').includes('shape://');
9!
614
    const canvas = document.createElement('canvas');
11✔
615
    const ctx = canvas.getContext('2d');
11✔
616
    const radius = symbolizer.radius;
11✔
617
    const strokePadding = hasStroke ? symbolizer.strokeWidth / 2 : 4;
11✔
618
    const x = strokePadding;
11✔
619
    const y = strokePadding;
11✔
620
    const cx = radius + strokePadding;
11✔
621
    const cy = radius + strokePadding;
11✔
622
    const width = symbolizer.radius * 2;
11✔
623
    const height = symbolizer.radius * 2;
11✔
624
    canvas.setAttribute('width', width + strokePadding * 2);
11✔
625
    canvas.setAttribute('height', height + strokePadding * 2);
11✔
626

627
    if (hasFill) {
11✔
628
        const fill = tinycolor(symbolizer.color);
9✔
629
        fill.setAlpha(symbolizer.fillOpacity);
9✔
630
        ctx.fillStyle = fill.toRgbString();
9✔
631
    }
632
    if (hasStroke) {
11✔
633
        const stroke = tinycolor(symbolizer.strokeColor);
9✔
634
        stroke.setAlpha(symbolizer.strokeOpacity);
9✔
635
        ctx.strokeStyle = stroke.toRgbString();
9✔
636
        ctx.lineWidth = symbolizer.strokeWidth;
9✔
637
        ctx.lineJoin = 'round';
9✔
638
        ctx.lineCap = 'round';
9✔
639
        if (symbolizer.strokeDasharray) {
9!
UNCOV
640
            ctx.setLineDash(symbolizer.strokeDasharray);
×
641
        }
642
    }
643

644
    switch (symbolizer.wellKnownName) {
11!
645
    case 'Circle': {
646
        ctx.arc(cx, cy, radius, 0, 2 * Math.PI);
8✔
647
        break;
8✔
648
    }
649
    case 'Square': {
UNCOV
650
        ctx.rect(x, y, width, height);
×
651
        break;
×
652
    }
653
    case 'Triangle': {
UNCOV
654
        const h = Math.sqrt(3) * radius;
×
655
        const bc = h / 3;
×
656
        const marginY = (height - h) / 2;
×
657
        ctx.moveTo(cx, cy + marginY - 2 * bc);
×
658
        ctx.lineTo(cx + radius, cy + marginY + bc);
×
659
        ctx.lineTo(cx - radius, cy + marginY + bc);
×
660
        ctx.closePath();
×
661
        break;
×
662
    }
663
    case 'Star': {
664
        paintStar(ctx, cx, cy, 5, radius, radius / 2);
2✔
665
        break;
2✔
666
    }
667
    case 'Cross': {
UNCOV
668
        paintCross(ctx, cx, cy, radius * 2, 0.2);
×
669
        break;
×
670
    }
671
    case 'X': {
UNCOV
672
        ctx.translate(cx, cy);
×
673
        ctx.rotate(45 * Math.PI / 180);
×
674
        ctx.translate(-cx, -cy);
×
675
        paintCross(ctx, cx, cy, radius * 2, 0.2);
×
676
        break;
×
677
    }
678
    case 'shape://vertline': {
UNCOV
679
        ctx.moveTo(cx, y);
×
680
        ctx.lineTo(cx, height);
×
681
        ctx.closePath();
×
682
        break;
×
683
    }
684
    case 'shape://horline': {
UNCOV
685
        ctx.moveTo(x, cy);
×
686
        ctx.lineTo(width, cy);
×
687
        ctx.closePath();
×
688
        break;
×
689
    }
690
    case 'shape://slash': {
UNCOV
691
        ctx.translate(cx, cy);
×
692
        ctx.rotate(45 * Math.PI / 180);
×
693
        ctx.translate(-cx, -cy);
×
694
        ctx.moveTo(cx, y);
×
695
        ctx.lineTo(cx, height);
×
696
        ctx.closePath();
×
697
        break;
×
698
    }
699
    case 'shape://backslash': {
UNCOV
700
        ctx.translate(cx, cy);
×
701
        ctx.rotate(-45 * Math.PI / 180);
×
702
        ctx.translate(-cx, -cy);
×
703
        ctx.moveTo(cx, y);
×
704
        ctx.lineTo(cx, height);
×
705
        break;
×
706
    }
707
    case 'shape://dot': {
UNCOV
708
        ctx.moveTo(cx - 1, cy - 1);
×
709
        ctx.lineTo(cx + 1, cy + 1);
×
710
        ctx.closePath();
×
711
        break;
×
712
    }
713
    case 'shape://plus': {
UNCOV
714
        ctx.moveTo(cx, y);
×
715
        ctx.lineTo(cx, height);
×
716
        ctx.moveTo(x, cy);
×
717
        ctx.lineTo(width, cy);
×
718
        ctx.closePath();
×
719
        break;
×
720
    }
721
    case 'shape://times': {
UNCOV
722
        ctx.translate(cx, cy);
×
723
        ctx.rotate(45 * Math.PI / 180);
×
724
        ctx.translate(-cx, -cy);
×
725
        ctx.moveTo(cx, y);
×
726
        ctx.lineTo(cx, height);
×
727
        ctx.moveTo(x, cy);
×
728
        ctx.lineTo(width, cy);
×
729
        ctx.closePath();
×
730
        break;
×
731
    }
732
    case 'shape://oarrow': {
UNCOV
733
        ctx.moveTo(x, y);
×
734
        ctx.lineTo(width, cy);
×
735
        ctx.lineTo(x, height);
×
736
        break;
×
737
    }
738
    case 'shape://carrow': {
UNCOV
739
        ctx.moveTo(x, y);
×
740
        ctx.lineTo(width, cy);
×
741
        ctx.lineTo(x, height);
×
742
        ctx.closePath();
×
743
        break;
×
744
    }
745
    default:
746
        ctx.arc(cx, cy, radius, 0, 2 * Math.PI);
1✔
747
    }
748
    if (hasFill) {
11✔
749
        ctx.fill();
9✔
750
    }
751
    if (hasStroke) {
11✔
752
        ctx.stroke();
9✔
753
    }
754
    return { width, height, canvas};
11✔
755
};
756

757
const svgUrlToCanvas = (svgUrl, options) => {
1✔
758
    return new Promise((resolve, reject) => {
1✔
759
        axios.get(svgUrl, { 'Content-Type': "image/svg+xml;charset=utf-8" })
1✔
760
            .then((response) => {
761
                const DOMURL = window.URL || window.webkitURL || window;
1!
762
                const parser = new DOMParser();
1✔
763
                const doc = parser.parseFromString(response.data, 'image/svg+xml'); // create a dom element
1✔
764
                const svg = doc.firstElementChild; // fetch svg element
1✔
765

766
                const size = options.size || 32;
1!
767
                const strokeWidth = options.strokeWidth ?? 1;
1!
768
                const width = size + strokeWidth;
1✔
769
                const height = size + strokeWidth;
1✔
770
                // override attributes to the first svg tag
771
                svg.setAttribute("fill", options.fillColor || "#FFCC33");
1!
772
                svg.setAttribute("fill-opacity", !isNil(options.fillOpacity) ? options.fillOpacity : 0.2);
1!
773
                svg.setAttribute("stroke", options.strokeColor || "#FFCC33");
1!
774
                svg.setAttribute("stroke-opacity", !isNil(options.strokeOpacity) ? options.strokeOpacity : 1);
1!
775
                svg.setAttribute("stroke-width", strokeWidth);
1✔
776
                svg.setAttribute("width", width);
1✔
777
                svg.setAttribute("height", height);
1✔
778
                svg.setAttribute("stroke-dasharray", options.strokeDasharray || "none");
1!
779

780
                const element = document.createElement("div");
1✔
781
                element.appendChild(svg);
1✔
782

783
                const svgBlob = new Blob([element.innerHTML], { type: "image/svg+xml;charset=utf-8" });
1✔
784
                const symbolUrlCustomized = DOMURL.createObjectURL(svgBlob);
1✔
785
                const icon = new Image();
1✔
786
                icon.onload = () => {
1✔
787
                    try {
1✔
788
                        const canvas = document.createElement('canvas');
1✔
789
                        canvas.width = width;
1✔
790
                        canvas.height = height;
1✔
791
                        const ctx = canvas.getContext("2d");
1✔
792
                        ctx.drawImage(icon, (canvas.width / 2) - (icon.width / 2), (canvas.height / 2) - (icon.height / 2));
1✔
793
                        resolve({
1✔
794
                            width,
795
                            height,
796
                            canvas
797
                        });
798
                    } catch (e) {
UNCOV
799
                        reject(e);
×
800
                    }
801
                };
802
                icon.onerror = (e) => { reject(e); };
1✔
803
                icon.src = symbolUrlCustomized;
1✔
804
            })
805
            .catch((e) => {
UNCOV
806
                reject(e);
×
807
            });
808
    });
809
};
810

811
/**
812
 * prefetch images of a mark symbolizer
813
 * @param {object} symbolizer mark symbolizer
814
 * @returns {promise} returns the canvas with additional information
815
 */
816
export const getWellKnownNameImageFromSymbolizer = (symbolizer) => {
1✔
817
    const id = getImageIdFromSymbolizer(symbolizer);
37✔
818
    if (imagesCache[id]) {
37✔
819
        return Promise.resolve(imagesCache[id]);
25✔
820
    }
821
    return new Promise((resolve, reject) => {
12✔
822
        if (!document?.createElement) {
12!
UNCOV
823
            reject(id);
×
824
        }
825
        if (symbolizer?.wellKnownName?.includes('.svg')) {
12✔
826
            svgUrlToCanvas(symbolizer.wellKnownName, {
1✔
827
                fillColor: symbolizer.color,
828
                fillOpacity: symbolizer.fillOpacity,
829
                strokeColor: symbolizer.strokeColor,
830
                strokeOpacity: symbolizer.strokeOpacity,
831
                strokeWidth: symbolizer.strokeWidth,
832
                strokeDasharray: symbolizer.strokeDasharray,
833
                size: symbolizer.radius * 2
834
            })
835
                .then(({ width, height, canvas }) => {
836
                    imagesCache[id] = { id, image: canvas, src: canvas.toDataURL(), width, height };
1✔
837
                    resolve(imagesCache[id]);
1✔
838
                })
839
                .catch(() => {
UNCOV
840
                    reject(id);
×
841
                });
842
        } else {
843
            const { width, height, canvas} = drawWellKnownNameImageFromSymbolizer(symbolizer);
11✔
844
            imagesCache[id] = { id, image: canvas, src: canvas.toDataURL(), width, height };
11✔
845
            resolve(imagesCache[id]);
11✔
846
        }
847
    });
848
};
849

850
export const parseSymbolizerExpressions = (symbolizer, feature) => {
1✔
851
    if (!symbolizer) {
123!
UNCOV
852
        return {};
×
853
    }
854
    return Object.keys(symbolizer).reduce((acc, key) => ({
907✔
855
        ...acc,
856
        [key]: isGeoStylerFunction(symbolizer[key])
907✔
857
            ? expressionsUtils.evaluateFunction(symbolizer[key], feature)
858
            : symbolizer[key]
859
    }), {});
860
};
861

862

863
export const getCachedImageById = (symbolizer) => {
1✔
864
    const id = getImageIdFromSymbolizer(symbolizer);
2✔
865
    return imagesCache[id] || {};
2✔
866
};
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