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

geosolutions-it / MapStore2 / 10573476730

27 Aug 2024 07:15AM UTC coverage: 76.747% (-0.008%) from 76.755%
10573476730

push

github

web-flow
Fix #10238 Change Math.random with an alternative solution  (#10245) (#10519)

* Fix #10238 Change Math.random with an alternative solution

# Conflicts:
#	web/client/components/map/cesium/plugins/WMSLayer.js
#	web/client/components/style/vector/marker/SymbolLayout.jsx
#	web/client/plugins/TOC/components/Legend.jsx
#	web/client/utils/cesium/MathUtils.js

* add tests

* other replacement for master branch

* fix require paths

* fix paths

* Fix test

* change max number

* Fix math utils position

* fix path

* fix test

* Changed name into RandomUtils to avoid confusion

* Update web/client/utils/__tests__/RandomUtils-test.js

---------

Co-authored-by: Lorenzo Natali <lorenzo.natali@geosolutionsgroup.com>
(cherry picked from commit 422a7b639)

Co-authored-by: Matteo V <matteo.velludini@geosolutionsgroup.com>

30683 of 47994 branches covered (63.93%)

9 of 11 new or added lines in 6 files covered. (81.82%)

7 existing lines in 2 files now uncovered.

38323 of 49934 relevant lines covered (76.75%)

33.52 hits per line

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

57.11
/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,003✔
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,013✔
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,034✔
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,003✔
97
    'property'
98
].includes(got?.name);
99
export const isGeoStylerMapStoreFunction = (got) => got?.type === 'attribute' || [
984✔
100
    'msMarkerIcon'
101
].includes(got?.name);
102
export const isGeoStylerFunction = (got) =>
1✔
103
    isGeoStylerBooleanFunction(got)
1,001✔
104
    || isGeoStylerNumberFunction(got)
105
    || isGeoStylerStringFunction(got)
106
    || isGeoStylerUnknownFunction(got)
107
    || isGeoStylerMapStoreFunction(got);
108
const getFeatureProperties = (feature) => {
1✔
109
    return (feature?.getProperties
66!
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!
118
                throw new Error(`Could not evalute 'property' function. Feature ${feature} is not defined.`);
×
119
            }
120
            if (isGeoStylerStringFunction(func.args[0])) {
20!
121
                return properties[expressionsUtils.evaluateStringFunction(func.args[0], feature)];
×
122
            }
123
            return properties[func.args[0]];
20✔
124
        }
125
        if (isGeoStylerStringFunction(func)) {
2!
126
            return expressionsUtils.evaluateStringFunction(func, feature);
×
127
        }
128
        if (isGeoStylerNumberFunction(func)) {
2!
129
            return expressionsUtils.evaluateNumberFunction(func, feature);
×
130
        }
131
        if (isGeoStylerBooleanFunction(func)) {
2!
132
            return expressionsUtils.evaluateBooleanFunction(func, feature);
×
133
        }
134
        if (isGeoStylerUnknownFunction(func)) {
2!
135
            return expressionsUtils.evaluateUnknownFunction(func, feature);
×
136
        }
137
        if (isGeoStylerMapStoreFunction(func)) {
2!
138
            return expressionsUtils.evaluateMapStoreFunction(func, feature);
2✔
139
        }
140
        return null;
×
141
    },
142
    evaluateBooleanFunction: (func, feature) => {
143
        const args = func.args.map(arg => {
×
144
            if (isGeoStylerFunction(arg)) {
×
145
                return expressionsUtils.evaluateFunction(arg, feature);
×
146
            }
147
            return arg;
×
148
        });
149
        switch (func.name) {
×
150
        case 'between':
151
            return (args[0]) >= (args[1]) && (args[0]) <= (args[2]);
×
152
        case 'double2bool':
153
            // TODO: evaluate this correctly
154
            return false;
×
155
        case 'in':
156
            return args.slice(1).includes(args[0]);
×
157
        case 'parseBoolean':
158
            return !!args[0];
×
159
        case 'strEndsWith':
160
            return (args[0]).endsWith(args[1]);
×
161
        case 'strEqualsIgnoreCase':
162
            return (args[0]).toLowerCase() === (args[1]).toLowerCase();
×
163
        case 'strMatches':
164
            return new RegExp(args[1]).test(args[0]);
×
165
        case 'strStartsWith':
166
            return (args[0]).startsWith(args[1]);
×
167
        default:
168
            return false;
×
169
        }
170
    },
171
    evaluateNumberFunction: (func, feature) => {
172
        if (func.name === 'pi') {
×
173
            return Math.PI;
×
174
        }
175
        if (func.name === 'random') {
×
NEW
176
            return randomInt();
×
177
        }
178
        const args = func.args.map(arg => {
×
179
            if (isGeoStylerFunction(arg)) {
×
180
                return expressionsUtils.evaluateFunction(arg, feature);
×
181
            }
182
            return arg;
×
183
        });
184
        switch (func.name) {
×
185
        case 'abs':
186
            return Math.abs(args[0]);
×
187
        case 'acos':
188
            return Math.acos(args[0]);
×
189
        case 'asin':
190
            return Math.asin(args[0]);
×
191
        case 'atan':
192
            return Math.atan(args[0]);
×
193
        case 'atan2':
194
            // TODO: evaluate this correctly
195
            return args[0];
×
196
        case 'ceil':
197
            return Math.ceil(args[0]);
×
198
        case 'cos':
199
            return Math.cos(args[0]);
×
200
        case 'exp':
201
            return Math.exp(args[0]);
×
202
        case 'floor':
203
            return Math.floor(args[0]);
×
204
        case 'log':
205
            return Math.log(args[0]);
×
206
        case 'max':
207
            return Math.max(...(args));
×
208
        case 'min':
209
            return Math.min(...(args));
×
210
        case 'modulo':
211
            return (args[0]) % (args[1]);
×
212
        case 'pow':
213
            return Math.pow(args[0], args[1]);
×
214
        case 'rint':
215
            // TODO: evaluate this correctly
216
            return args[0];
×
217
        case 'round':
218
            return Math.round(args[0]);
×
219
        case 'sin':
220
            return Math.sin(args[0]);
×
221
        case 'sqrt':
222
            return Math.sqrt(args[0]);
×
223
        case 'strIndexOf':
224
            return (args[0]).indexOf(args[1]);
×
225
        case 'strLastIndexOf':
226
            return (args[0]).lastIndexOf(args[1]);
×
227
        case 'strLength':
228
            return (args[0]).length;
×
229
        case 'tan':
230
            return Math.tan(args[0]);
×
231
        case 'toDegrees':
232
            return (args[0]) * (180 / Math.PI);
×
233
        case 'toRadians':
234
            return (args[0]) * (Math.PI / 180);
×
235
        default:
236
            return args[0];
×
237
        }
238
    },
239
    evaluateUnknownFunction: (func, feature) => {
240
        const args = func.args.map(arg => {
×
241
            if (isGeoStylerFunction(arg)) {
×
242
                return expressionsUtils.evaluateFunction(arg, feature);
×
243
            }
244
            return arg;
×
245
        });
246
        const properties = getFeatureProperties(feature);
×
247
        switch (func.name) {
×
248
        case 'property':
249
            return properties[args[0]];
×
250
        default:
251
            return args[0];
×
252
        }
253
    },
254
    evaluateStringFunction: (func, feature) => {
255
        const args = func.args.map(arg => {
×
256
            if (isGeoStylerFunction(arg)) {
×
257
                return expressionsUtils.evaluateFunction(arg, feature);
×
258
            }
259
            return arg;
×
260
        });
261
        switch (func.name) {
×
262
        case 'numberFormat':
263
            // TODO: evaluate this correctly
264
            return args[0];
×
265
        case 'strAbbreviate':
266
            // TODO: evaluate this correctly
267
            return args[0];
×
268
        case 'strCapitalize':
269
            // https://stackoverflow.com/a/32589289/10342669
270
            let splitStr = (args[0]).toLowerCase().split(' ');
×
271
            for (let part of splitStr) {
×
272
                part = part.charAt(0).toUpperCase() + part.substring(1);
×
273
            }
274
            return splitStr.join(' ');
×
275
        case 'strConcat':
276
            return args.join();
×
277
        case 'strDefaultIfBlank':
278
            return (args[0])?.length < 1 ? args[1] : args[0];
×
279
        case 'strReplace':
280
            if (args[3] === true) {
×
281
                return (args[0]).replaceAll(args[1], args[2]);
×
282
            }
283
            return (args[0]).replace(args[1], args[2]);
×
284
        case 'strStripAccents':
285
            // https://stackoverflow.com/a/37511463/10342669
286
            return (args[0]).normalize('NFKD').replace(/[\u0300-\u036f]/g, '');
×
287
        case 'strSubstring':
288
            return (args[0]).substring(args[1], args[2]);
×
289
        case 'strSubstringStart':
290
            return (args[0]).substring(args[1]);
×
291
        case 'strToLowerCase':
292
            return (args[0]).toLowerCase();
×
293
        case 'strToUpperCase':
294
            return (args[0]).toUpperCase();
×
295
        case 'strTrim':
296
            return (args[0]).trim();
×
297
        default:
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!
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:
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);
39✔
334
    const operatorMapping = {
39✔
335
        '&&': true,
336
        '||': true,
337
        '!': true
338
    };
339
    let matchesFilter = true;
39✔
340
    const operator = filter[0];
39✔
341
    let isNestedFilter = false;
39✔
342
    if (operatorMapping[operator]) {
39✔
343
        isNestedFilter = true;
4✔
344
    }
345
    try {
39✔
346
        if (isNestedFilter) {
39✔
347
            let intermediate;
348
            let restFilter;
349
            switch (filter[0]) {
4!
350
            case '&&':
351
                intermediate = true;
2✔
352
                restFilter = filter.slice(1);
2✔
353
                restFilter.forEach((f) => {
2✔
354
                    if (!geoStylerStyleFilter(feature, f)) {
4✔
355
                        intermediate = false;
1✔
356
                    }
357
                });
358
                matchesFilter = intermediate;
2✔
359
                break;
2✔
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 '!':
371
                matchesFilter = !this.geoStylerFilterToOlParserFilter(feature, filter[1]);
×
372
                break;
×
373
            default:
374
                throw new Error('Cannot parse Filter. Unknown combination or negation operator.');
×
375
            }
376
        } else {
377
            let arg1;
378
            if (isGeoStylerFunction(filter[1])) {
35!
379
                arg1 = expressionsUtils.evaluateFunction(filter[1], feature);
×
380
            } else {
381
                arg1 = properties[filter[1]];
35✔
382
            }
383
            let arg2;
384
            if (isGeoStylerFunction(filter[2])) {
35!
385
                arg2 = properties[expressionsUtils.evaluateFunction(filter[2], feature)];
×
386
            } else {
387
                arg2 = filter[2];
35✔
388
            }
389
            switch (filter[0]) {
35!
390
            case '==':
391
                matchesFilter = ('' + arg1) === ('' + arg2);
20✔
392
                break;
20✔
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!
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);
2✔
409
                break;
2✔
410
            case '<=':
411
                matchesFilter = Number(arg1) <= Number(arg2);
4✔
412
                break;
4✔
413
            case '>':
414
                matchesFilter = Number(arg1) > Number(arg2);
1✔
415
                break;
1✔
416
            case '>=':
417
                matchesFilter = Number(arg1) >= Number(arg2);
1✔
418
                break;
1✔
419
            default:
420
                throw new Error('Cannot parse Filter. Unknown comparison operator.');
×
421
            }
422
        }
423
    } catch (e) {
424
        throw new Error('Cannot parse Filter. Invalid structure.');
×
425
    }
426
    return matchesFilter;
39✔
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);
5✔
443

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

448
    // Find any character between two braces (including the braces in the result)
449
    let regExp = new RegExp(attributeTemplatePrefix + '(.*?)' + attributeTemplateSuffix, 'g');
5✔
450
    let regExpRes = _template.match(regExp);
5✔
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) {
5!
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 {
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!
479
                _template = _template.replace(res, noValueFoundText);
×
480
            }
481
        });
482
    }
483

484
    return _template;
5✔
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) {
74✔
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(':');
55✔
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);
74✔
518
};
519

520
/**
521
 * prefetch images of a icon symbolizer
522
 * @param {object} symbolizer icon symbolizer
523
 * @returns {promise} returns the image
524
 */
525
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!
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) {
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✔
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✔
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);
10✔
606
    if (imagesCache[id]) {
10✔
607
        const { image, ...other } = imagesCache[id];
1✔
608
        return { ...other, canvas: image };
1✔
609
    }
610
    const hasStroke = !!symbolizer?.strokeWidth
9✔
611
        && !!symbolizer?.strokeOpacity;
612
    const hasFill = !!symbolizer?.fillOpacity
9✔
613
        && !(symbolizer.wellKnownName || '').includes('shape://');
8!
614
    const canvas = document.createElement('canvas');
9✔
615
    const ctx = canvas.getContext('2d');
9✔
616
    const radius = symbolizer.radius;
9✔
617
    const strokePadding = hasStroke ? symbolizer.strokeWidth / 2 : 4;
9✔
618
    const x = strokePadding;
9✔
619
    const y = strokePadding;
9✔
620
    const cx = radius + strokePadding;
9✔
621
    const cy = radius + strokePadding;
9✔
622
    const width = symbolizer.radius * 2;
9✔
623
    const height = symbolizer.radius * 2;
9✔
624
    canvas.setAttribute('width', width + strokePadding * 2);
9✔
625
    canvas.setAttribute('height', height + strokePadding * 2);
9✔
626

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

644
    switch (symbolizer.wellKnownName) {
9!
645
    case 'Circle': {
646
        ctx.arc(cx, cy, radius, 0, 2 * Math.PI);
7✔
647
        break;
7✔
648
    }
649
    case 'Square': {
650
        ctx.rect(x, y, width, height);
×
651
        break;
×
652
    }
653
    case 'Triangle': {
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': {
668
        paintCross(ctx, cx, cy, radius * 2, 0.2);
×
669
        break;
×
670
    }
671
    case 'X': {
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': {
679
        ctx.moveTo(cx, y);
×
680
        ctx.lineTo(cx, height);
×
681
        ctx.closePath();
×
682
        break;
×
683
    }
684
    case 'shape://horline': {
685
        ctx.moveTo(x, cy);
×
686
        ctx.lineTo(width, cy);
×
687
        ctx.closePath();
×
688
        break;
×
689
    }
690
    case 'shape://slash': {
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': {
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': {
708
        ctx.moveTo(cx - 1, cy - 1);
×
709
        ctx.lineTo(cx + 1, cy + 1);
×
710
        ctx.closePath();
×
711
        break;
×
712
    }
713
    case 'shape://plus': {
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': {
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': {
733
        ctx.moveTo(x, y);
×
734
        ctx.lineTo(width, cy);
×
735
        ctx.lineTo(x, height);
×
736
        break;
×
737
    }
738
    case 'shape://carrow': {
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);
×
747
    }
748
    if (hasFill) {
9✔
749
        ctx.fill();
8✔
750
    }
751
    if (hasStroke) {
9✔
752
        ctx.stroke();
8✔
753
    }
754
    return { width, height, canvas};
9✔
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) {
799
                        reject(e);
×
800
                    }
801
                };
802
                icon.onerror = (e) => { reject(e); };
1✔
803
                icon.src = symbolUrlCustomized;
1✔
804
            })
805
            .catch((e) => {
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);
28✔
818
    if (imagesCache[id]) {
28✔
819
        return Promise.resolve(imagesCache[id]);
18✔
820
    }
821
    return new Promise((resolve, reject) => {
10✔
822
        if (!document?.createElement) {
10!
823
            reject(id);
×
824
        }
825
        if (symbolizer?.wellKnownName?.includes('.svg')) {
10✔
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(() => {
840
                    reject(id);
×
841
                });
842
        } else {
843
            const { width, height, canvas} = drawWellKnownNameImageFromSymbolizer(symbolizer);
9✔
844
            imagesCache[id] = { id, image: canvas, src: canvas.toDataURL(), width, height };
9✔
845
            resolve(imagesCache[id]);
9✔
846
        }
847
    });
848
};
849

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

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

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