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

geosolutions-it / MapStore2 / 15811185418

22 Jun 2025 09:57PM UTC coverage: 76.901% (-0.03%) from 76.934%
15811185418

Pull #11130

github

web-flow
Merge 0b5467418 into 7cde38ac9
Pull Request #11130: #10839: Allow printing by freely setting the scale factor

31173 of 48566 branches covered (64.19%)

39 of 103 new or added lines in 7 files covered. (37.86%)

1644 existing lines in 149 files now uncovered.

38769 of 50414 relevant lines covered (76.9%)

36.38 hits per line

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

63.38
/web/client/utils/styleparser/OLStyleParser.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 OlImageState from 'ol/ImageState';
37

38
import OlGeomPoint from 'ol/geom/Point';
39

40
import OlStyle from 'ol/style/Style';
41
import OlStyleImage from 'ol/style/Image';
42
import OlStyleStroke from 'ol/style/Stroke';
43
import OlStyleText from 'ol/style/Text';
44
import OlStyleCircle from 'ol/style/Circle';
45
import OlStyleFill from 'ol/style/Fill';
46
import OlStyleIcon from 'ol/style/Icon';
47
import OlStyleRegularshape from 'ol/style/RegularShape';
48
import { METERS_PER_UNIT } from 'ol/proj/Units';
49
import { getCenter } from 'ol/extent';
50
import OlGeomLineString from 'ol/geom/LineString';
51
import OlGeomCircle from 'ol/geom/Circle';
52
import { toContext } from 'ol/render';
53
import GeoJSON from 'ol/format/GeoJSON';
54
import OlGeomPolygon, { circular } from 'ol/geom/Polygon';
55
import { transform } from 'ol/proj';
56
import {
57
    expressionsUtils,
58
    resolveAttributeTemplate,
59
    isGeoStylerFunction,
60
    isGeoStylerStringFunction,
61
    isGeoStylerNumberFunction,
62
    geoStylerStyleFilter,
63
    getImageIdFromSymbolizer
64
} from './StyleParserUtils';
65
import { drawIcons } from './IconUtils';
66

67
import isString from 'lodash/isString';
68
import { geometryFunctionsLibrary } from './GeometryFunctionsUtils';
69

70
const getGeometryFunction = geometryFunctionsLibrary.openlayers({
1✔
71
    Point: OlGeomPoint,
72
    LineString: OlGeomLineString,
73
    Polygon: OlGeomPolygon,
74
    GeoJSON,
75
    getCenter
76
});
77
// create a cached accessor for image src data
78
export const createGetImagesSrc = () => {
1✔
79
    // note that images are canvas elements, identified by ID.
80
    const imgCache = {};
71✔
81
    const getImageSrcFromCache = (image, id) => {
71✔
82
        if (!id) {
8!
UNCOV
83
            return image.toDataURL();
×
84
        }
85
        if (!imgCache[id]) {
8✔
86
            imgCache[id] = image.toDataURL();
6✔
87
        }
88
        return imgCache[id];
8✔
89
    };
90
    return getImageSrcFromCache;
71✔
91
};
92
const anchorStringToFraction = (anchor) => {
1✔
93
    switch (anchor) {
2!
94
    case 'top-left':
95
        return [0.0, 0.0];
1✔
96
    case 'top':
UNCOV
97
        return [0.5, 0.0];
×
98
    case 'top-right':
UNCOV
99
        return [1.0, 0.0];
×
100
    case 'left':
UNCOV
101
        return [0.0, 0.5];
×
102
    case 'center':
UNCOV
103
        return [0.5, 0.5];
×
104
    case 'right':
UNCOV
105
        return [1.0, 0.5];
×
106
    case 'bottom-left':
UNCOV
107
        return [0.0, 1.0];
×
108
    case 'bottom':
UNCOV
109
        return [0.5, 1.0];
×
110
    case 'bottom-right':
UNCOV
111
        return [1.0, 1.0];
×
112
    default:
113
        return [0.5, 0.5];
1✔
114
    }
115
};
116

117
const anchorStringToTextProperties = (anchor) => {
1✔
118
    switch (anchor) {
1!
119
    case 'top-left':
120
        return {
1✔
121
            textBaseline: 'top',
122
            textAlign: 'left'
123
        };
124
    case 'top':
UNCOV
125
        return {
×
126
            textBaseline: 'top',
127
            textAlign: 'center'
128
        };
129
    case 'top-right':
UNCOV
130
        return {
×
131
            textBaseline: 'top',
132
            textAlign: 'right'
133
        };
134
    case 'left':
UNCOV
135
        return {
×
136
            textBaseline: 'middle',
137
            textAlign: 'left'
138
        };
139
    case 'center':
UNCOV
140
        return {
×
141
            textBaseline: 'middle',
142
            textAlign: 'center'
143
        };
144
    case 'right':
UNCOV
145
        return {
×
146
            textBaseline: 'middle',
147
            textAlign: 'right'
148
        };
149
    case 'bottom-left':
UNCOV
150
        return {
×
151
            textBaseline: 'bottom',
152
            textAlign: 'left'
153
        };
154
    case 'bottom':
UNCOV
155
        return {
×
156
            textBaseline: 'bottom',
157
            textAlign: 'center'
158
        };
159
    case 'bottom-right':
UNCOV
160
        return {
×
161
            textBaseline: 'bottom',
162
            textAlign: 'right'
163
        };
164
    default:
UNCOV
165
        return {
×
166
            textBaseline: 'top',
167
            textAlign: 'center'
168
        };
169
    }
170
};
171

172
const getRgbaColor = (_colorString, _opacity) => {
1✔
173
    let colorString = _colorString;
10✔
174
    let opacity = _opacity;
10✔
175
    if (isGeoStylerStringFunction(colorString)) {
10!
UNCOV
176
        colorString = expressionsUtils.evaluateStringFunction(colorString);
×
177
    }
178

179
    if (typeof (colorString) !== 'string') {
10!
UNCOV
180
        return null;
×
181
    }
182
    if (colorString.startsWith('rgba(')) {
10!
UNCOV
183
        return colorString;
×
184
    }
185

186
    // check if is valid HEX color - see also here
187
    // https://stackoverflow.com/questions/8027423/how-to-check-if-a-string-is-a-valid-hex-color-representation/8027444
188
    const isHexColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(colorString);
10✔
189
    if (!isHexColor) {
10!
UNCOV
190
        return null;
×
191
    }
192

193
    const r = parseInt(colorString.slice(1, 3), 16);
10✔
194
    const g = parseInt(colorString.slice(3, 5), 16);
10✔
195
    const b = parseInt(colorString.slice(5, 7), 16);
10✔
196

197
    if (isGeoStylerNumberFunction(opacity)) {
10!
UNCOV
198
        opacity = expressionsUtils.evaluateNumberFunction(opacity);
×
199
    }
200

201
    if (opacity < 0) {
10!
UNCOV
202
        opacity = 1;
×
203
    }
204
    return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + opacity + ')';
10✔
205
};
206

207
/**
208
 * This parser can be used with the GeoStyler.
209
 * It implements the GeoStyler-Style Parser interface to work with OpenLayers styles.
210
 *
211
 * @class OlStyleParser
212
 * @implements StyleParser
213
 */
214
export class OlStyleParser {
215

216
    /**
217
     * The name of the OlStyleParser.
218
     */
219
    static title = 'OpenLayers Style Parser';
1✔
220

221
    unsupportedProperties = {
61✔
222
        Symbolizer: {
223
            MarkSymbolizer: {
224
                avoidEdges: 'none',
225
                blur: 'none',
226
                offsetAnchor: 'none',
227
                pitchAlignment: 'none',
228
                pitchScale: 'none',
229
                visibility: 'none'
230
            },
231
            FillSymbolizer: {
232
                antialias: 'none',
233
                fillOpacity: {
234
                    support: 'none',
235
                    info: 'Use opacity instead.'
236
                },
237
                visibility: 'none'
238
            },
239
            IconSymbolizer: {
240
                allowOverlap: 'none',
241
                anchor: 'none',
242
                avoidEdges: 'none',
243
                color: 'none',
244
                haloBlur: 'none',
245
                haloColor: 'none',
246
                haloWidth: 'none',
247
                keepUpright: 'none',
248
                offsetAnchor: 'none',
249
                size: {
250
                    support: 'partial',
251
                    info: 'Will set/get the width of the ol Icon.'
252
                },
253
                optional: 'none',
254
                padding: 'none',
255
                pitchAlignment: 'none',
256
                rotationAlignment: 'none',
257
                textFit: 'none',
258
                textFitPadding: 'none',
259
                visibility: 'none'
260
            },
261
            LineSymbolizer: {
262
                blur: 'none',
263
                gapWidth: 'none',
264
                gradient: 'none',
265
                miterLimit: 'none',
266
                roundLimit: 'none',
267
                spacing: 'none',
268
                visibility: 'none',
269
                graphicFill: 'none',
270
                graphicStroke: 'none',
271
                perpendicularOffset: 'none'
272
            },
273
            RasterSymbolizer: 'none'
274
        },
275
        Function: {
276
            double2bool: {
277
                support: 'none',
278
                info: 'Always returns false'
279
            },
280
            atan2: {
281
                support: 'none',
282
                info: 'Currently returns the first argument'
283
            },
284
            rint: {
285
                support: 'none',
286
                info: 'Currently returns the first argument'
287
            },
288
            numberFormat: {
289
                support: 'none',
290
                info: 'Currently returns the first argument'
291
            },
292
            strAbbreviate: {
293
                support: 'none',
294
                info: 'Currently returns the first argument'
295
            }
296
        }
297
    };
298

299
    title = 'OpenLayers Style Parser';
61✔
300
    olIconStyleCache = {};
61✔
301

302
    OlStyleConstructor = OlStyle;
61✔
303
    OlStyleImageConstructor = OlStyleImage;
61✔
304
    OlStyleFillConstructor = OlStyleFill;
61✔
305
    OlStyleStrokeConstructor = OlStyleStroke;
61✔
306
    OlStyleTextConstructor = OlStyleText;
61✔
307
    OlStyleCircleConstructor = OlStyleCircle;
61✔
308
    OlStyleIconConstructor = OlStyleIcon;
61✔
309
    OlStyleRegularshapeConstructor = OlStyleRegularshape;
61✔
310

311
    constructor(ol) {
312
        if (ol) {
61!
313
            this.OlStyleConstructor = ol.style.Style;
×
314
            this.OlStyleImageConstructor = ol.style.Image;
×
315
            this.OlStyleFillConstructor = ol.style.Fill;
×
316
            this.OlStyleStrokeConstructor = ol.style.Stroke;
×
317
            this.OlStyleTextConstructor = ol.style.Text;
×
318
            this.OlStyleCircleConstructor = ol.style.Circle;
×
319
            this.OlStyleIconConstructor = ol.style.Icon;
×
UNCOV
320
            this.OlStyleRegularshapeConstructor = ol.style.RegularShape;
×
321
        }
322
    }
323

324
    isOlParserStyleFct = x => {
61✔
UNCOV
325
        return typeof x === 'function';
×
326
    };
327

328
    readStyle() {
329
        return new Promise((resolve, reject) => {
×
330
            try {
×
UNCOV
331
                resolve(null);
×
332
            } catch (error) {
UNCOV
333
                reject(error);
×
334
            }
335
        });
336
    }
337

338
    /**
339
     * The writeStyle implementation of the GeoStyler-Style StyleParser interface.
340
     * It reads a GeoStyler-Style Style and returns a Promise.
341
     * The Promise itself resolves one of three types
342
     *
343
     * 1. OlStyle if input Style consists of
344
     *    one rule with one symbolizer, no filter, no scaleDenominator, no TextSymbolizer
345
     * 2. OlStyle[] if input Style consists of
346
     *    one rule with multiple symbolizers, no filter, no scaleDenominator, no TextSymbolizer
347
     * 3. OlParserStyleFct for everything else
348
     *
349
     * @param geoStylerStyle A GeoStyler-Style Style.
350
     * @return The Promise resolving with one of above mentioned style types.
351
     */
352
    writeStyle(geoStylerStyle) {
353
        return new Promise((resolve) => {
70✔
354
            const unsupportedProperties = this.checkForUnsupportedProperties(geoStylerStyle);
70✔
355
            try {
70✔
356
                const olStyle = this.getOlStyleTypeFromGeoStylerStyle(geoStylerStyle);
70✔
357
                resolve(olStyle, {
70✔
358
                    unsupportedProperties,
359
                    warnings: unsupportedProperties && ['Your style contains unsupportedProperties!']
81✔
360
                });
361
            } catch (error) {
UNCOV
362
                resolve({
×
363
                    errors: [error]
364
                });
365
            }
366
        });
367
    }
368

369
    checkForUnsupportedProperties(geoStylerStyle) {
370
        const capitalizeFirstLetter = (a) => a[0].toUpperCase() + a.slice(1);
70✔
371
        const unsupportedProperties = {};
70✔
372
        geoStylerStyle?.rules?.forEach(rule => {
70✔
373
            // ScaleDenominator and Filters are completly supported so we just check for symbolizers
374
            rule.symbolizers.forEach(symbolizer => {
25✔
375
                const key = capitalizeFirstLetter(`${symbolizer.kind}Symbolizer`);
25✔
376
                const value = this.unsupportedProperties?.Symbolizer?.[key];
25✔
377
                if (value) {
25✔
378
                    if (typeof value === 'string' || value instanceof String) {
23!
379
                        if (!unsupportedProperties.Symbolizer) {
×
UNCOV
380
                            unsupportedProperties.Symbolizer = {};
×
381
                        }
UNCOV
382
                        unsupportedProperties.Symbolizer[key] = value;
×
383
                    } else {
384
                        Object.keys(symbolizer).forEach(property => {
23✔
385
                            if (value[property]) {
176✔
386
                                if (!unsupportedProperties.Symbolizer) {
12✔
387
                                    unsupportedProperties.Symbolizer = {};
11✔
388
                                }
389
                                if (!unsupportedProperties.Symbolizer[key]) {
12✔
390
                                    unsupportedProperties.Symbolizer[key] = {};
11✔
391
                                }
392
                                unsupportedProperties.Symbolizer[key][property] = value[property];
12✔
393
                            }
394
                        });
395
                    }
396
                }
397
            });
398
        });
399
        if (Object.keys(unsupportedProperties).length > 0) {
70✔
400
            return unsupportedProperties;
11✔
401
        }
402
        return null;
59✔
403
    }
404

405
    /**
406
     * Decides which OlStyleType should be returned depending on given geoStylerStyle.
407
     * Three OlStyleTypes are possible:
408
     *
409
     * 1. OlStyle if input Style consists of
410
     *    one rule with one symbolizer, no filter, no scaleDenominator, no TextSymbolizer
411
     * 2. OlStyle[] if input Style consists of
412
     *    one rule with multiple symbolizers, no filter, no scaleDenominator, no TextSymbolizer
413
     * 3. OlParserStyleFct for everything else
414
     *
415
     * @param geoStylerStyle A GeoStyler-Style Style
416
     */
417
    getOlStyleTypeFromGeoStylerStyle(geoStylerStyle) {
418
        return this.geoStylerStyleToOlParserStyleFct(geoStylerStyle);
70✔
419
    }
420

421
    /**
422
     * Get the OpenLayers Style object from an GeoStyler-Style Style
423
     *
424
     * @param geoStylerStyle A GeoStyler-Style Style.
425
     * @return An OlParserStyleFct
426
     */
427
    geoStylerStyleToOlParserStyleFct(geoStylerStyle) {
428
        const rules = geoStylerStyle.rules;
70✔
429
        const olStyle = ({ map, features } = {}) => drawIcons(geoStylerStyle, { features })
70✔
430
            .then((images) => {
431
                this._getImages = () => images;
70✔
432
                this._getImageSrc = createGetImagesSrc();
70✔
433
                this._computeIconScaleBasedOnSymbolizer = (symbolizer, _symbolizer) => {
70✔
434
                    const { image, width, height } = images.find(({ id }) => id === getImageIdFromSymbolizer(symbolizer, _symbolizer)) || {};
2!
435
                    if (image && width && height) {
2!
436
                        const side = width > height ? width : height;
2!
437
                        const scale = symbolizer.size / side;
2✔
438
                        return scale;
2✔
439
                    }
UNCOV
440
                    return symbolizer.size;
×
441
                };
442
                return (feature, resolution) => {
70✔
443
                    this._getMap = () => map;
14✔
444
                    const styles = [];
14✔
445

446
                    // calculate scale for resolution (from ol-util MapUtil)
447
                    const units = map
14✔
448
                        ? map.getView().getProjection().getUnits()
449
                        : 'm';
450
                    const dpi = 25.4 / 0.28;
14✔
451
                    const mpu = METERS_PER_UNIT[units];
14✔
452
                    const inchesPerMeter = 39.37;
14✔
453
                    const scale = resolution * mpu * inchesPerMeter * dpi;
14✔
454

455
                    rules.forEach((rule) => {
14✔
456
                        // handling scale denominator
457
                        let minScale = rule?.scaleDenominator?.min;
14✔
458
                        let maxScale = rule?.scaleDenominator?.max;
14✔
459
                        let isWithinScale = true;
14✔
460
                        if (minScale || maxScale) {
14!
461
                            minScale = isGeoStylerFunction(minScale) ? expressionsUtils.evaluateNumberFunction(minScale) : minScale;
×
462
                            maxScale = isGeoStylerFunction(maxScale) ? expressionsUtils.evaluateNumberFunction(maxScale) : maxScale;
×
463
                            if (minScale && scale < minScale) {
×
UNCOV
464
                                isWithinScale = false;
×
465
                            }
466
                            if (maxScale && scale >= maxScale) {
×
UNCOV
467
                                isWithinScale = false;
×
468
                            }
469
                        }
470

471
                        // handling filter
472
                        let matchesFilter = false;
14✔
473
                        if (!rule.filter) {
14!
474
                            matchesFilter = true;
14✔
475
                        } else {
476
                            try {
×
UNCOV
477
                                matchesFilter = geoStylerStyleFilter(feature, rule.filter);
×
478
                            } catch (e) {
UNCOV
479
                                matchesFilter = false;
×
480
                            }
481
                        }
482

483
                        if (isWithinScale && matchesFilter) {
14!
484
                            rule.symbolizers.forEach((symb) => {
14✔
485
                                const olSymbolizer = this.getOlSymbolizerFromSymbolizer(symb, feature);
14✔
486

487
                                // either an OlStyle or an ol.StyleFunction. OpenLayers only accepts an array
488
                                // of OlStyles, not ol.StyleFunctions.
489
                                // So we have to check it and in case of an ol.StyleFunction call that function
490
                                // and add the returned style to const styles.
491
                                if (typeof olSymbolizer !== 'function') {
14✔
492
                                    styles.push(olSymbolizer);
13✔
493
                                } else {
494
                                    const styleFromFct = olSymbolizer(feature, resolution);
1✔
495
                                    styles.push(styleFromFct);
1✔
496
                                }
497
                            });
498
                        }
499
                    });
500
                    return styles;
14✔
501
                };
502
            });
503
        const olStyleFct = olStyle;
70✔
504
        olStyleFct.__geoStylerStyle = geoStylerStyle;
70✔
505
        return olStyleFct;
70✔
506
    }
507

508
    /**
509
     * Get the OpenLayers Style object or an OL StyleFunction from an
510
     * GeoStyler-Style Symbolizer.
511
     *
512
     * @param symbolizer A GeoStyler-Style Symbolizer.
513
     * @return The OpenLayers Style object or a StyleFunction
514
     */
515
    getOlSymbolizerFromSymbolizer(symbolizer, feature) {
516
        let olSymbolizer;
517

518
        switch (symbolizer.kind) {
14!
519
        case 'Mark':
520
            olSymbolizer = this.getOlPointSymbolizerFromMarkSymbolizer(symbolizer, feature);
4✔
521
            break;
4✔
522
        case 'Icon':
523
            olSymbolizer = this.getOlIconSymbolizerFromIconSymbolizer(symbolizer, feature);
2✔
524
            break;
2✔
525
        case 'Text':
526
            olSymbolizer = this.getOlTextSymbolizerFromTextSymbolizer(symbolizer, feature);
1✔
527
            break;
1✔
528
        case 'Line':
529
            olSymbolizer = this.getOlLineSymbolizerFromLineSymbolizer(symbolizer, feature);
2✔
530
            break;
2✔
531
        case 'Fill':
532
            olSymbolizer = this.getOlPolygonSymbolizerFromFillSymbolizer(symbolizer, feature);
4✔
533
            break;
4✔
534
        case 'Circle':
535
            olSymbolizer = this.getOlCircleSymbolizerFromCircleSymbolizer(symbolizer, feature);
1✔
536
            break;
1✔
537
        default:
538
            // Return the OL default style since the TS type binding does not allow
539
            // us to set olSymbolizer to undefined
UNCOV
540
            const fill = new this.OlStyleFillConstructor({
×
541
                color: 'rgba(255,255,255,0.4)'
542
            });
UNCOV
543
            const stroke = new this.OlStyleStrokeConstructor({
×
544
                color: '#3399CC',
545
                width: 1.25
546
            });
UNCOV
547
            olSymbolizer = new this.OlStyleConstructor({
×
548
                image: new this.OlStyleCircleConstructor({
549
                    fill: fill,
550
                    stroke: stroke,
551
                    radius: 5
552
                }),
553
                fill: fill,
554
                stroke: stroke
555
            });
UNCOV
556
            break;
×
557
        }
558

559
        return olSymbolizer;
14✔
560
    }
561

562
    /**
563
     * Get the OL Style object  from an GeoStyler-Style MarkSymbolizer.
564
     *
565
     * @param markSymbolizer A GeoStyler-Style MarkSymbolizer.
566
     * @return The OL Style object
567
     */
568
    getOlPointSymbolizerFromMarkSymbolizer(_markSymbolizer, feature) {
569
        const markSymbolizer = {..._markSymbolizer};
4✔
570
        for (const key of Object.keys(markSymbolizer)) {
4✔
571
            if (isGeoStylerFunction(markSymbolizer[key])) {
35!
UNCOV
572
                markSymbolizer[key] = expressionsUtils.evaluateFunction(markSymbolizer[key], feature);
×
573
            }
574
        }
575
        const geometryFunc = getGeometryFunction(markSymbolizer, feature, this._getMap());
4✔
576
        const images = this._getImages();
4✔
577
        const imageId = getImageIdFromSymbolizer(markSymbolizer, _markSymbolizer);
4✔
578
        const { image, width, height } = images.find(({ id }) => id === imageId) || {};
4!
579
        if (image) {
4!
580
            const side = width > height ? width : height;
4!
581
            const scale = (markSymbolizer.radius * 2) / side;
4✔
582
            // const src = this._getImageSrc(image, imageId);
583
            // if _getImageSrc is not defined, perform the image.toDataURL() here
584
            const src = this._getImageSrc?.(image, imageId) ?? image.toDataURL();
4!
585
            return new this.OlStyleConstructor({
4✔
586
                image: new this.OlStyleIconConstructor({
587
                    src,
588
                    crossOrigin: 'anonymous',
589
                    opacity: 1,
590
                    scale,
591
                    // Rotation in openlayers is radians while we use degree
592
                    rotation: (typeof (markSymbolizer.rotate) === 'number' ? markSymbolizer.rotate * Math.PI / 180 : undefined)
4✔
593
                }),
594
                ...geometryFunc
595
            });
596
        }
UNCOV
597
        return new this.OlStyleConstructor();
×
598
    }
599

600
    /**
601
     * Get the OL Style object  from an GeoStyler-Style IconSymbolizer.
602
     *
603
     * @param symbolizer  A GeoStyler-Style IconSymbolizer.
604
     * @return The OL Style object
605
     */
606
    getOlIconSymbolizerFromIconSymbolizer(
607
        _symbolizer,
608
        feat
609
    ) {
610
        let symbolizer = { ..._symbolizer };
2✔
611
        for (const key of Object.keys(symbolizer)) {
2✔
612
            if (isGeoStylerFunction(symbolizer[key])) {
9!
UNCOV
613
                symbolizer[key] = expressionsUtils.evaluateFunction(symbolizer[key], feat);
×
614
            }
615
        }
616
        const geometryFunc = getGeometryFunction(symbolizer, feat, this._getMap());
2✔
617
        const baseProps = {
2✔
618
            src: symbolizer.image,
619
            crossOrigin: 'anonymous',
620
            opacity: symbolizer.opacity,
621
            scale: this._computeIconScaleBasedOnSymbolizer(symbolizer, _symbolizer),
622
            // Rotation in openlayers is radians while we use degree
623
            rotation: (typeof (symbolizer.rotate) === 'number' ? symbolizer.rotate * Math.PI / 180 : undefined),
2✔
624
            displacement: symbolizer.offset,
625
            anchor: anchorStringToFraction(symbolizer.anchor)
626

627
        };
628
        // check if IconSymbolizer.image contains a placeholder
629
        const prefix = '\\{\\{';
2✔
630
        const suffix = '\\}\\}';
2✔
631
        const regExp = new RegExp(prefix + '.*?' + suffix, 'g');
2✔
632
        const regExpRes = typeof (symbolizer.image) === 'string' ? symbolizer.image.match(regExp) : null;
2!
633
        if (regExpRes) {
2!
634
            // if it contains a placeholder
635
            // return olStyleFunction
636
            const olPointStyledIconFn = (feature) => {
×
UNCOV
637
                let src = resolveAttributeTemplate(feature, symbolizer.image, '');
×
638
                // src can't be blank, would trigger ol errors
639
                if (!src) {
×
UNCOV
640
                    src = symbolizer.image + '';
×
641
                }
642
                let image;
643
                if (this.olIconStyleCache[src]) {
×
644
                    image = this.olIconStyleCache[src];
×
645
                    if (baseProps.rotation !== undefined) {
×
UNCOV
646
                        image.setRotation(baseProps.rotation);
×
647
                    }
648
                    if (baseProps.opacity !== undefined) {
×
UNCOV
649
                        image.setOpacity(baseProps.opacity);
×
650
                    }
651
                } else {
UNCOV
652
                    image = new this.OlStyleIconConstructor({
×
653
                        ...baseProps,
654
                        src // order is important
655
                    });
UNCOV
656
                    this.olIconStyleCache[src] = image;
×
657
                }
UNCOV
658
                const style = new this.OlStyleConstructor({
×
659
                    image,
660
                    ...geometryFunc
661
                });
UNCOV
662
                return style;
×
663
            };
UNCOV
664
            return olPointStyledIconFn;
×
665
        }
666

667
        return new this.OlStyleConstructor({
2✔
668
            image: new this.OlStyleIconConstructor({
669
                ...baseProps
670
            }),
671
            ...geometryFunc
672
        });
673
    }
674

675
    /**
676
     * Get the OL Style object from an GeoStyler-Style LineSymbolizer.
677
     *
678
     * @param symbolizer A GeoStyler-Style LineSymbolizer.
679
     * @return The OL Style object
680
     */
681
    getOlLineSymbolizerFromLineSymbolizer(_symbolizer, feat) {
682
        let symbolizer = { ..._symbolizer };
2✔
683
        for (const key of Object.keys(symbolizer)) {
2✔
684
            if (isGeoStylerFunction(symbolizer[key])) {
9!
UNCOV
685
                symbolizer[key] = expressionsUtils.evaluateFunction(symbolizer[key], feat);
×
686
            }
687
        }
688
        const color = symbolizer.color;
2✔
689
        const opacity = symbolizer.opacity;
2✔
690
        const sColor = (color && opacity !== null && opacity !== undefined) ?
2!
691
            getRgbaColor(color, opacity) : color;
692

693
        const geometryFunc = getGeometryFunction(symbolizer, feat, this._getMap());
2✔
694

695
        return new this.OlStyleConstructor({
2✔
696
            ...geometryFunc,
697
            stroke: new this.OlStyleStrokeConstructor({
698
                color: sColor,
699
                width: symbolizer.width,
700
                lineCap: symbolizer.cap,
701
                lineJoin: symbolizer.join,
702
                lineDash: symbolizer.dasharray,
703
                lineDashOffset: symbolizer.dashOffset
704
            })
705
        });
706
    }
707

708
    /**
709
     * Get the OL Style object from an GeoStyler-Style FillSymbolizer.
710
     *
711
     * @param symbolizer A GeoStyler-Style FillSymbolizer.
712
     * @return The OL Style object
713
     */
714
    getOlPolygonSymbolizerFromFillSymbolizer(_symbolizer, feat) {
715
        let symbolizer = { ..._symbolizer };
4✔
716
        for (const key of Object.keys(symbolizer)) {
4✔
717
            if (isGeoStylerFunction(symbolizer[key])) {
22✔
718
                symbolizer[key] = expressionsUtils.evaluateFunction(symbolizer[key], feat);
3✔
719
            }
720
        }
721
        const geometryFunc = getGeometryFunction(symbolizer, feat, this._getMap());
4✔
722

723
        const color = symbolizer.color;
4✔
724
        // fillOpacity is needed for legacy support
725
        const opacity = symbolizer.fillOpacity ?? symbolizer.opacity;
4!
726
        const fColor = color && Number.isFinite(opacity)
4!
727
            ? getRgbaColor(color, opacity)
728
            : color;
729

730
        let fill = color
4!
731
            ? new this.OlStyleFillConstructor({ color: fColor })
732
            : undefined;
733

734
        const outlineColor = symbolizer.outlineColor;
4✔
735
        const outlineOpacity = symbolizer.outlineOpacity;
4✔
736
        const oColor = (outlineColor && Number.isFinite(outlineOpacity))
4✔
737
            ? getRgbaColor(outlineColor, outlineOpacity)
738
            : outlineColor;
739

740
        const stroke = outlineColor || symbolizer.outlineWidth ? new this.OlStyleStrokeConstructor({
4✔
741
            color: oColor,
742
            width: symbolizer.outlineWidth,
743
            lineDash: symbolizer.outlineDasharray
744
        }) : undefined;
745

746
        const olStyle = new this.OlStyleConstructor({
4✔
747
            ...geometryFunc,
748
            fill,
749
            stroke
750
        });
751

752
        if (symbolizer.graphicFill) {
4!
753
            const pattern = this.getOlPatternFromGraphicFill(symbolizer.graphicFill);
×
754
            if (!fill) {
×
UNCOV
755
                fill = new this.OlStyleFillConstructor({});
×
756
            }
757
            if (pattern) {
×
UNCOV
758
                fill.setColor(pattern);
×
759
            }
UNCOV
760
            olStyle.setFill(fill);
×
761
        }
762

763
        return olStyle;
4✔
764
    }
765

766
    /**
767
     * Get the OL Style object from an GeoStyler-Style Custom CircleSymbolizer.
768
     *
769
     * @param symbolizer A GeoStyler-Style Custom CircleSymbolizer.
770
     * @return The OL Style object
771
     */
772
    getOlCircleSymbolizerFromCircleSymbolizer(_symbolizer, feat) {
773
        let symbolizer = {..._symbolizer};
1✔
774
        for (const key of Object.keys(symbolizer)) {
1✔
775
            if (isGeoStylerFunction(symbolizer[key])) {
9!
UNCOV
776
                symbolizer[key] = expressionsUtils.evaluateFunction(symbolizer[key], feat);
×
777
            }
778
        }
779

780
        const color = symbolizer.color;
1✔
781
        const opacity = symbolizer.opacity;
1✔
782
        const fColor = color && Number.isFinite(opacity)
1!
783
            ? getRgbaColor(color, opacity)
784
            : color;
785

786
        let fill = color
1!
787
            ? new this.OlStyleFillConstructor({ color: fColor })
788
            : undefined;
789

790
        const outlineColor = symbolizer.outlineColor;
1✔
791
        const outlineOpacity = symbolizer.outlineOpacity;
1✔
792
        const oColor = (outlineColor && Number.isFinite(outlineOpacity))
1!
793
            ? getRgbaColor(outlineColor, outlineOpacity)
794
            : outlineColor;
795

796
        const stroke = outlineColor || symbolizer.outlineWidth ? new this.OlStyleStrokeConstructor({
1!
797
            color: oColor,
798
            width: symbolizer.outlineWidth,
799
            lineDash: symbolizer.outlineDasharray
800
        }) : undefined;
801

802
        const olStyle = new this.OlStyleConstructor({
1✔
803
            fill,
804
            stroke,
805
            geometry: (feature) => {
806
                const map = this._getMap();
×
807
                if (symbolizer.geodesic) {
×
808
                    const projectionCode = map.getView().getProjection().getCode();
×
809
                    const center = transform(feature.getGeometry().getCoordinates(), projectionCode, 'EPSG:4326');
×
810
                    const circle = circular(center, symbolizer.radius, 128);
×
811
                    circle.transform('EPSG:4326', projectionCode);
×
UNCOV
812
                    return  new OlGeomPolygon(circle.getCoordinates());
×
813
                }
UNCOV
814
                return new OlGeomCircle(
×
815
                    feature.getGeometry().getCoordinates(),
816
                    symbolizer.radius / METERS_PER_UNIT[map.getView().getProjection().getUnits()]
817
                );
818
            }
819
        });
820

821
        if (symbolizer.graphicFill) {
1!
822
            const pattern = this.getOlPatternFromGraphicFill(symbolizer.graphicFill);
×
823
            if (!fill) {
×
UNCOV
824
                fill = new this.OlStyleFillConstructor({});
×
825
            }
826
            if (pattern) {
×
UNCOV
827
                fill.setColor(pattern);
×
828
            }
UNCOV
829
            olStyle.setFill(fill);
×
830
        }
831

832
        return olStyle;
1✔
833
    }
834

835
    /**
836
     * Get the pattern for a graphicFill.
837
     *
838
     * This creates a CanvasPattern based on the
839
     * properties of the given PointSymbolizer. Currently,
840
     * only IconSymbolizer and MarkSymbolizer are supported.
841
     *
842
     * @param graphicFill The Symbolizer that holds the pattern config.
843
     * @returns The created CanvasPattern, or null.
844
     */
845
    getOlPatternFromGraphicFill(graphicFill) {
846
        let graphicFillStyle;
847
        if (graphicFill?.kind === 'Icon') {
×
848
            graphicFillStyle = this.getOlIconSymbolizerFromIconSymbolizer(graphicFill);
×
849
            const graphicFillImage = graphicFillStyle?.getImage();
×
UNCOV
850
            graphicFillImage?.load(); // Needed for Icon type images with a remote src
×
851
            // We can only work with the image once it's loaded
852
            if (graphicFillImage?.getImageState() !== OlImageState.LOADED) {
×
UNCOV
853
                return null;
×
854
            }
855
        } else if (graphicFill?.kind === 'Mark' && isString(graphicFill?.wellKnownName)) {
×
UNCOV
856
            graphicFillStyle = this.getOlPointSymbolizerFromMarkSymbolizer(graphicFill);
×
857
        } else {
UNCOV
858
            return null;
×
859
        }
860

861
        // We need to clone the style and image since we'll be changing the scale below (hack)
862
        const graphicFillStyleCloned = graphicFillStyle.clone();
×
UNCOV
863
        const imageCloned = graphicFillStyleCloned.getImage();
×
864

865
        // Temporary canvas.
866
        // TODO: Can/should we reuse an pre-existing one for efficiency?
867
        const tmpCanvas = document.createElement('canvas');
×
UNCOV
868
        const tmpContext = tmpCanvas.getContext('2d');
×
869

870
        // Hack to make scaling work for Icons.
871
        // TODO: find a better way than this.
872
        const scale = imageCloned.getScale() || 1;
×
873
        const pixelRatio = scale;
×
UNCOV
874
        imageCloned.setScale(1);
×
875

UNCOV
876
        const size = imageCloned.getSize();
×
877

878
        // Create the context where we'll be drawing the style on
UNCOV
879
        const vectorContext = toContext(tmpContext, {
×
880
            pixelRatio,
881
            size
882
        });
883

884
        // Draw the graphic
885
        vectorContext.setStyle(graphicFillStyleCloned);
×
886
        const pointCoords = size.map(item => item / 2);
×
UNCOV
887
        vectorContext.drawGeometry(new OlGeomPoint(pointCoords));
×
888

889
        // Create the actual pattern and return style
UNCOV
890
        return tmpContext.createPattern(tmpCanvas, 'repeat');
×
891
    }
892

893
    /**
894
     * Get the OL StyleFunction object from an GeoStyler-Style TextSymbolizer.
895
     *
896
     * @param {TextSymbolizer} textSymbolizer A GeoStyler-Style TextSymbolizer.
897
     * @return {object} The OL StyleFunction
898
     */
899
    getOlTextSymbolizerFromTextSymbolizer(
900
        _symbolizer,
901
        feat
902
    ) {
903
        let symbolizer = { ..._symbolizer };
1✔
904
        for (const key of Object.keys(symbolizer)) {
1✔
905
            if (isGeoStylerFunction(symbolizer[key])) {
12!
UNCOV
906
                symbolizer[key] = expressionsUtils.evaluateFunction(symbolizer[key], feat);
×
907
            }
908
        }
909
        const geometryFunc = getGeometryFunction(symbolizer, feat, this._getMap());
1✔
910
        const color = symbolizer.color;
1✔
911
        const opacity = symbolizer.opacity;
1✔
912
        const fColor = color && Number.isFinite(opacity)
1!
913
            ? getRgbaColor(color, opacity)
914
            : color;
915

916
        const haloColor = symbolizer.haloColor;
1✔
917
        const haloWidth = symbolizer.haloWidth;
1✔
918
        const sColor = haloColor && Number.isFinite(opacity)
1!
919
            ? getRgbaColor(haloColor, opacity)
920
            : haloColor;
921
        const fontWeight = symbolizer.fontWeight ?? 'normal';
1!
922
        const fontStyle = symbolizer.fontStyle ?? 'normal';
1!
923
        const size = symbolizer.size;
1✔
924
        const font = symbolizer.font;
1✔
925
        const baseProps = {
1✔
926
            font: fontWeight + ' ' + fontStyle + ' ' + size + 'px ' + font?.join(', '),
927
            fill: new this.OlStyleFillConstructor({
928
                color: fColor
929
            }),
930
            stroke: new this.OlStyleStrokeConstructor({
931
                color: sColor,
932
                width: haloWidth ? haloWidth : 0
1!
933
            }),
934
            overflow: symbolizer.allowOverlap,
935
            offsetX: (symbolizer.offset ? symbolizer.offset[0] : 0),
1!
936
            offsetY: (symbolizer.offset ? symbolizer.offset[1] : 0),
1!
937
            rotation: typeof (symbolizer.rotate) === 'number' ? symbolizer.rotate * Math.PI / 180 : undefined,
1!
938
            ...anchorStringToTextProperties(symbolizer.anchor)
939
        };
940

941
        // check if TextSymbolizer.label contains a placeholder
942
        const prefix = '\\{\\{';
1✔
943
        const suffix = '\\}\\}';
1✔
944
        const regExp = new RegExp(prefix + '.*?' + suffix, 'g');
1✔
945
        let regExpRes;
946
        if (!isGeoStylerStringFunction(symbolizer.label)) {
1!
947
            regExpRes = symbolizer.label ? symbolizer.label.match(regExp) : null;
1!
948
        }
949
        if (regExpRes) {
1!
950
            // if it contains a placeholder
951
            // return olStyleFunction
952
            const olPointStyledLabelFn = (feature) => {
1✔
953

954
                const text = new this.OlStyleTextConstructor({
1✔
955
                    text: resolveAttributeTemplate(feature, symbolizer.label, ''),
956
                    ...baseProps
957
                });
958

959
                const style = new this.OlStyleConstructor({
1✔
960
                    ...geometryFunc,
961
                    text: text
962
                });
963

964
                return style;
1✔
965
            };
966
            return olPointStyledLabelFn;
1✔
967
        }
968
        // if TextSymbolizer does not contain a placeholder
969
        // return OlStyle
UNCOV
970
        return new this.OlStyleConstructor({
×
971
            ...geometryFunc,
972
            text: new this.OlStyleTextConstructor({
973
                text: symbolizer.label,
974
                ...baseProps
975
            })
976
        });
977
    }
978

979
}
980

981
export default OlStyleParser;
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