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

geosolutions-it / MapStore2 / 12389428770

18 Dec 2024 08:48AM UTC coverage: 77.122% (+0.02%) from 77.102%
12389428770

Pull #10718

github

web-flow
Merge 3bc8b0d13 into d70bae0d5
Pull Request #10718: #10684: Legend filtering for GeoServer WMS layers

30294 of 47061 branches covered (64.37%)

66 of 71 new or added lines in 10 files covered. (92.96%)

111 existing lines in 4 files now uncovered.

37638 of 48803 relevant lines covered (77.12%)

34.81 hits per line

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

57.3
/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
import { getConfigProp } from '../ConfigUtils';
43

44

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

443
    const properties = getFeatureProperties(feature);
5✔
444

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

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

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

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

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

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

485
    return _template;
5✔
486
};
487

488
let imagesCache = {};
1✔
489

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

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

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

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

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

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

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

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

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

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

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

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

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

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

863
let fontAwesomeLoaded = false;
1✔
864
const loadFontAwesome = () => {
1✔
865
    if (fontAwesomeLoaded) {
8✔
866
        return Promise.resolve();
7✔
867
    }
868
    // async load of font awesome
869
    return import('font-awesome/css/font-awesome.min.css')
1✔
870
        .then(() => {
871
            // ensure the font is loaded
872
            return document.fonts.load('1rem FontAwesome')
1✔
873
                .then(() => {
874
                    fontAwesomeLoaded = true;
1✔
875
                    return fontAwesomeLoaded;
1✔
876
                });
877
        });
878
};
879

880
/**
881
 * prefetch all image or mark symbol in a geostyler style
882
 * @param {object} geoStylerStyle geostyler style
883
 * @returns {promise} all the prefetched images
884
 */
885
export const drawIcons = (geoStylerStyle, options) => {
1✔
886
    const { rules = [] } = geoStylerStyle || {};
116!
887
    const symbolizers = rules.reduce((acc, rule) => {
116✔
888
        const markIconSymbolizers = (rule?.symbolizers || []).filter(({ kind }) => ['Mark', 'Icon'].includes(kind));
74!
889
        const symbolizerHasExpression = markIconSymbolizers
74✔
890
            .some(properties => Object.keys(properties).some(key => !!properties[key]?.name));
292✔
891
        if (!symbolizerHasExpression) {
74✔
892
            return [
71✔
893
                ...acc,
894
                ...markIconSymbolizers
895
            ];
896
        }
897
        const features = options.features || [];
3!
898
        const supportedFeatures = rule.filter === undefined
3!
899
            ? features
UNCOV
900
            : features.filter((feature) => geoStylerStyleFilter(feature, rule.filter));
×
901
        return [
3✔
902
            ...acc,
903
            ...markIconSymbolizers.reduce((newSymbolizers, symbolizer) => {
904
                return [
3✔
905
                    ...newSymbolizers,
906
                    ...(supportedFeatures || []).map((feature) => {
3!
907
                        const newSymbolizer = parseSymbolizerExpressions(symbolizer, feature);
3✔
908
                        return {
3✔
909
                            ...newSymbolizer,
910
                            // exclude msMarkerIcon from parsing
911
                            // the getImageFromSymbolizer is already taking into account this case
912
                            ...(symbolizer?.image?.name === 'msMarkerIcon' && { image: symbolizer.image })
3!
913
                        };
914
                    })
915
                ];
916
            }, [])
917
        ];
918
    }, []);
919
    const marks = symbolizers.filter(({ kind }) => kind === 'Mark');
116✔
920
    const icons = symbolizers.filter(({ kind }) => kind === 'Icon');
116✔
921
    const loadFontAwesomeForIcons = getConfigProp("loadFontAwesomeForIcons");
116✔
922
    // if undefined or true it will load it to preserve previous behaviour
923
    const loadingPromise =  (isNil(loadFontAwesomeForIcons) || loadFontAwesomeForIcons) && icons?.length ? loadFontAwesome() : Promise.resolve();
116!
924
    return loadingPromise
116✔
925
        .then(
926
            () => new Promise((resolve) => {
116✔
927
                if (marks.length > 0 || icons.length > 0) {
116✔
928
                    Promise.all([
29✔
929
                        ...marks.map(getWellKnownNameImageFromSymbolizer),
930
                        ...icons.map(getImageFromSymbolizer)
931
                    ]).then((images) => {
932
                        resolve(images);
29✔
933
                    });
934
                } else {
935
                    resolve([]);
87✔
936
                }
937
            })
938
        );
939
};
940
export const getCachedImageById = (symbolizer) => {
1✔
941
    const id = getImageIdFromSymbolizer(symbolizer);
2✔
942
    return imagesCache[id] || {};
2✔
943
};
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