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

geostyler / geostyler-openlayers-parser / 15439929453

04 Jun 2025 10:27AM UTC coverage: 82.347%. First build
15439929453

Pull #842

github

web-flow
Merge 41a70509e into 98b416a12
Pull Request #842: fix: do not fetch all commits when running commit lint

364 of 484 branches covered (75.21%)

Branch coverage included in aggregate %.

513 of 581 relevant lines covered (88.3%)

30.88 hits per line

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

82.75
/src/OlStyleParser.ts
1
import { parseFont } from 'css-font-parser';
2

3
import {
4
  CapType,
5
  FillSymbolizer,
6
  Filter,
7
  IconSymbolizer,
8
  JoinType,
9
  LineSymbolizer,
10
  MarkSymbolizer,
11
  Operator,
12
  PointSymbolizer,
13
  ReadStyleResult,
14
  Rule,
15
  Style,
16
  StyleParser,
17
  StyleType,
18
  Symbolizer,
19
  TextSymbolizer,
20
  UnsupportedProperties,
21
  WriteStyleResult
22
} from 'geostyler-style/dist/style';
23

24
import {
25
  isGeoStylerBooleanFunction,
26
  isGeoStylerFunction,
27
  isGeoStylerStringFunction,
28
  isIconSymbolizer,
29
  isMarkSymbolizer,
30
  isSprite
31
} from 'geostyler-style/dist/typeguards';
32

33
import OlImageState from 'ol/ImageState';
34

35
import OlGeomPoint from 'ol/geom/Point';
36

37
import OlStyle, { StyleFunction as OlStyleFunction, StyleLike as OlStyleLike} from 'ol/style/Style';
38
import OlStyleImage from 'ol/style/Image';
39
import OlStyleStroke from 'ol/style/Stroke';
40
import OlStyleText, { Options as OlStyleTextOptions }  from 'ol/style/Text';
41
import OlStyleCircle, { Options as OlStyleCircleOptions } from 'ol/style/Circle';
42
import OlStyleFill from 'ol/style/Fill';
43
import OlStyleIcon, { Options as OlStyleIconOptions }  from 'ol/style/Icon';
44
import OlStyleRegularshape from 'ol/style/RegularShape';
45
import { METERS_PER_UNIT } from 'ol/proj/Units';
46

47
import OlStyleUtil from './Util/OlStyleUtil';
48
import { toContext } from 'ol/render';
49
import OlFeature from 'ol/Feature';
50

51
export interface OlParserStyleFct {
52
  (feature?: any, resolution?: number): any;
53
  __geoStylerStyle: Style;
54
}
55

56
type SymbolizerKeyType = keyof UnsupportedProperties['Symbolizer'];
57

58
/**
59
 * This parser can be used with the GeoStyler.
60
 * It implements the GeoStyler-Style Parser interface to work with OpenLayers styles.
61
 *
62
 * @class OlStyleParser
63
 * @implements StyleParser
64
 */
65
export class OlStyleParser implements StyleParser<OlStyleLike> {
66

67
  /**
68
   * The name of the OlStyleParser.
69
   */
70
  public static title = 'OpenLayers Style Parser';
2✔
71

72
  unsupportedProperties: UnsupportedProperties = {
194✔
73
    Symbolizer: {
74
      MarkSymbolizer: {
75
        avoidEdges: 'none',
76
        blur: 'none',
77
        offsetAnchor: 'none',
78
        pitchAlignment: 'none',
79
        pitchScale: 'none'
80
      },
81
      FillSymbolizer: {
82
        antialias: 'none',
83
        opacity: {
84
          support: 'none',
85
          info: 'Use fillOpacity instead.'
86
        }
87
      },
88
      IconSymbolizer: {
89
        allowOverlap: 'none',
90
        anchor: 'none',
91
        avoidEdges: 'none',
92
        color: 'none',
93
        haloBlur: 'none',
94
        haloColor: 'none',
95
        haloWidth: 'none',
96
        keepUpright: 'none',
97
        offsetAnchor: 'none',
98
        size: {
99
          support: 'partial',
100
          info: 'Will set/get the width of the ol Icon.'
101
        },
102
        optional: 'none',
103
        padding: 'none',
104
        pitchAlignment: 'none',
105
        rotationAlignment: 'none',
106
        textFit: 'none',
107
        textFitPadding: 'none'
108
      },
109
      LineSymbolizer: {
110
        blur: 'none',
111
        gapWidth: 'none',
112
        gradient: 'none',
113
        miterLimit: 'none',
114
        roundLimit: 'none',
115
        spacing: 'none',
116
        graphicFill: 'none',
117
        graphicStroke: 'none',
118
        perpendicularOffset: 'none'
119
      },
120
      RasterSymbolizer: 'none',
121
      TextSymbolizer: {
122
        anchor: 'none',
123
        placement: {
124
          support:'partial',
125
          info: 'point and line supported. line-center will be mapped to line.'
126
        }
127
      }
128
    },
129
    Function: {
130
      double2bool: {
131
        support: 'none',
132
        info: 'Always returns false'
133
      },
134
      atan2: {
135
        support: 'none',
136
        info: 'Currently returns the first argument'
137
      },
138
      rint: {
139
        support: 'none',
140
        info: 'Currently returns the first argument'
141
      },
142
      numberFormat: {
143
        support: 'none',
144
        info: 'Currently returns the first argument'
145
      },
146
      strAbbreviate: {
147
        support: 'none',
148
        info: 'Currently returns the first argument'
149
      }
150
    }
151
  };
152

153
  title = 'OpenLayers Style Parser';
194✔
154
  olIconStyleCache: any = {};
194✔
155

156
  OlStyleConstructor = OlStyle;
194✔
157
  OlStyleImageConstructor = OlStyleImage;
194✔
158
  OlStyleFillConstructor = OlStyleFill;
194✔
159
  OlStyleStrokeConstructor = OlStyleStroke;
194✔
160
  OlStyleTextConstructor = OlStyleText;
194✔
161
  OlStyleCircleConstructor = OlStyleCircle;
194✔
162
  OlStyleIconConstructor = OlStyleIcon;
194✔
163
  OlStyleRegularshapeConstructor = OlStyleRegularshape;
194✔
164

165
  constructor(ol?: any) {
166
    if (ol) {
194!
167
      this.OlStyleConstructor = ol.style.Style;
×
168
      this.OlStyleImageConstructor = ol.style.Image;
×
169
      this.OlStyleFillConstructor = ol.style.Fill;
×
170
      this.OlStyleStrokeConstructor = ol.style.Stroke;
×
171
      this.OlStyleTextConstructor = ol.style.Text;
×
172
      this.OlStyleCircleConstructor = ol.style.Circle;
×
173
      this.OlStyleIconConstructor = ol.style.Icon;
×
174
      this.OlStyleRegularshapeConstructor = ol.style.RegularShape;
×
175
    }
176
  }
177

178
  isOlParserStyleFct = (x: any): x is OlParserStyleFct => {
194✔
179
    return typeof x === 'function';
58✔
180
  };
181

182
  /**
183
   * Get the GeoStyler-Style PointSymbolizer from an OpenLayers Style object.
184
   *
185
   * @param olStyle The OpenLayers Style object
186
   * @return The GeoStyler-Style PointSymbolizer
187
   */
188
  getPointSymbolizerFromOlStyle(olStyle: OlStyle): PointSymbolizer {
189
    let pointSymbolizer: PointSymbolizer;
190
    if (olStyle.getImage() instanceof this.OlStyleCircleConstructor) {
44✔
191
      // circle
192
      const olCircleStyle: OlStyleCircle = olStyle.getImage() as OlStyleCircle;
12✔
193
      const olFillStyle = olCircleStyle.getFill();
12✔
194
      const olStrokeStyle = olCircleStyle.getStroke();
12✔
195
      const offset = olCircleStyle.getDisplacement() as [number, number];
12✔
196

197
      const circleSymbolizer: MarkSymbolizer = {
12✔
198
        kind: 'Mark',
199
        wellKnownName: 'circle',
200
        color: olFillStyle ? OlStyleUtil.getHexColor(olFillStyle.getColor() as string) : undefined,
12!
201
        opacity: olCircleStyle.getOpacity() !== 1 ? olCircleStyle.getOpacity() : undefined,
12!
202
        fillOpacity: olFillStyle ? OlStyleUtil.getOpacity(olFillStyle.getColor() as string) : undefined,
12!
203
        radius: (olCircleStyle.getRadius() !== 0) ? olCircleStyle.getRadius() : 5,
12!
204
        strokeColor: olStrokeStyle ? olStrokeStyle.getColor() as string : undefined,
12!
205
        strokeOpacity: olStrokeStyle ? OlStyleUtil.getOpacity(olStrokeStyle.getColor() as string) : undefined,
12!
206
        strokeWidth: olStrokeStyle ? olStrokeStyle.getWidth() : undefined,
12!
207
        offset: offset[0] || offset[1] ? offset : undefined
34✔
208
      };
209
      pointSymbolizer = circleSymbolizer;
12✔
210
    } else if (olStyle.getImage() instanceof this.OlStyleRegularshapeConstructor) {
32✔
211
      // square, triangle, star, cross or x
212
      const olRegularStyle: OlStyleRegularshape = olStyle.getImage() as OlStyleRegularshape;
26✔
213
      const olFillStyle = olRegularStyle.getFill();
26✔
214
      const olStrokeStyle = olRegularStyle.getStroke();
26✔
215
      const radius = olRegularStyle.getRadius();
26✔
216
      const radius2 = olRegularStyle.getRadius2();
26✔
217
      const points = olRegularStyle.getPoints();
26✔
218
      const angle = olRegularStyle.getAngle();
26✔
219
      const offset = olRegularStyle.getDisplacement() as [number, number];
26✔
220

221
      const markSymbolizer: MarkSymbolizer = {
26✔
222
        kind: 'Mark',
223
        color: olFillStyle ? OlStyleUtil.getHexColor(olFillStyle.getColor() as string) : undefined,
26!
224
        opacity: olRegularStyle.getOpacity() !== 1 ? olRegularStyle.getOpacity() : undefined,
26!
225
        fillOpacity: olFillStyle ? OlStyleUtil.getOpacity(olFillStyle.getColor() as string) : undefined,
26!
226
        strokeColor: olStrokeStyle ? olStrokeStyle.getColor() as string : undefined,
26!
227
        strokeOpacity: olStrokeStyle ? OlStyleUtil.getOpacity(olStrokeStyle.getColor() as string) : undefined,
26!
228
        strokeWidth: olStrokeStyle ? olStrokeStyle.getWidth() : undefined,
26!
229
        radius: (radius !== 0) ? radius : 5,
26!
230
        // Rotation in openlayers is radians while we use degree
231
        rotate: olRegularStyle.getRotation() / Math.PI * 180,
232
        offset: offset[0] || offset[1] ? offset : undefined
76✔
233
      } as MarkSymbolizer;
234

235
      switch (points) {
26!
236
        case 2:
237
          switch (angle) {
8!
238
            case 0:
239
              markSymbolizer.wellKnownName = 'shape://vertline';
2✔
240
              break;
2✔
241
            case Math.PI / 2:
242
              markSymbolizer.wellKnownName = 'shape://horline';
2✔
243
              break;
2✔
244
            case Math.PI / 4:
245
              markSymbolizer.wellKnownName = 'shape://slash';
2✔
246
              break;
2✔
247
            case 2 * Math.PI - (Math.PI / 4):
248
              markSymbolizer.wellKnownName = 'shape://backslash';
2✔
249
              break;
2✔
250
            default:
251
              break;
×
252
          }
253
          break;
8✔
254
        case 3:
255
          switch (angle) {
6!
256
            case 0:
257
              markSymbolizer.wellKnownName = 'triangle';
2✔
258
              break;
2✔
259
            case Math.PI / 2:
260
              markSymbolizer.wellKnownName = 'shape://carrow';
4✔
261
              break;
4✔
262
            default:
263
              break;
×
264
          }
265
          break;
6✔
266
        case 4:
267
          if (Number.isFinite(radius2)) {
10✔
268
            // cross or x
269
            if (olRegularStyle.getAngle() === 0) {
8✔
270
              // cross
271
              markSymbolizer.wellKnownName = 'cross';
4✔
272
            } else {
273
              // x
274
              markSymbolizer.wellKnownName = 'x';
4✔
275
            }
276
          } else {
277
            // square
278
            markSymbolizer.wellKnownName = 'square';
2✔
279
          }
280
          break;
10✔
281
        case 5:
282
          // star
283
          markSymbolizer.wellKnownName = 'star';
2✔
284
          break;
2✔
285
        default:
286
          throw new Error('Could not parse OlStyle. Only 2, 3, 4 or 5 point regular shapes are allowed');
×
287
      }
288
      pointSymbolizer = markSymbolizer;
26✔
289
    } else if (olStyle.getText() instanceof this.OlStyleTextConstructor) {
6✔
290
      const olTextStyle: OlStyleText = olStyle.getText() as OlStyleText;
2✔
291
      const olFillStyle = olTextStyle.getFill();
2✔
292
      const olStrokeStyle = olTextStyle.getStroke();
2✔
293
      const rotation = olTextStyle.getRotation();
2✔
294
      let char = olTextStyle.getText() || 'a';
2!
295
      const font = olTextStyle.getFont() || '10px sans-serif';
2!
296
      const fontName = OlStyleUtil.getFontNameFromOlFont(font);
2✔
297
      const radius = OlStyleUtil.getSizeFromOlFont(font);
2✔
298
      const offset = [olTextStyle.getOffsetX(), olTextStyle.getOffsetY()];
2✔
299

300
      if (Array.isArray(char)) {
2!
301
        char = char[0];
×
302
      }
303

304
      pointSymbolizer = {
2✔
305
        kind: 'Mark',
306
        wellKnownName: `ttf://${fontName}#0x${char.charCodeAt(0).toString(16)}`,
307
        color: olFillStyle ? OlStyleUtil.getHexColor(olFillStyle.getColor() as string) : undefined,
2!
308
        opacity: olFillStyle ? OlStyleUtil.getOpacity(olFillStyle.getColor() as string) : undefined,
2!
309
        strokeColor: olStrokeStyle ? olStrokeStyle.getColor() as string : undefined,
2!
310
        strokeOpacity: olStrokeStyle ? OlStyleUtil.getOpacity(olStrokeStyle.getColor() as string) : undefined,
2!
311
        strokeWidth: olStrokeStyle ? olStrokeStyle.getWidth() : undefined,
2!
312
        radius: (radius !== 0) ? radius : 5,
2!
313
        // Rotation in openlayers is radians while we use degree
314
        rotate: rotation ? rotation / Math.PI * 180 : 0,
2!
315
        offset: offset[0] || offset[1] ? offset : undefined
4!
316
      } as MarkSymbolizer;
317
    } else {
318
      // icon
319
      const olIconStyle = olStyle.getImage() as OlStyleIcon;
4✔
320
      const displacement = olIconStyle.getDisplacement() as [number, number];
4✔
321
      // initialOptions_ as fallback when image is not yet loaded
322
      const image = this.getImageFromIconStyle(olIconStyle);
4✔
323
      // this always gets calculated from ol so this might not have been set initially
324
      let size = olIconStyle.getWidth();
4✔
325
      const rotation = olIconStyle.getRotation() / Math.PI * 180;
4✔
326
      const opacity = olIconStyle.getOpacity();
4✔
327

328
      const iconSymbolizer: IconSymbolizer = {
4✔
329
        kind: 'Icon',
330
        image,
331
        opacity: opacity < 1 ? opacity : undefined,
4✔
332
        size,
333
        // Rotation in openlayers is radians while we use degree
334
        rotate: rotation !== 0 ? rotation : undefined,
4✔
335
        offset: displacement[0] || displacement[1] ? displacement : undefined
10✔
336
      };
337
      pointSymbolizer = iconSymbolizer;
4✔
338
    }
339
    return pointSymbolizer;
44✔
340
  }
341

342
  /**
343
   *
344
   * @param olIconStyle An ol style Icon representation
345
   * @returns A string or Sprite configuration
346
   */
347
  getImageFromIconStyle(olIconStyle: OlStyleIcon): IconSymbolizer['image'] {
348
    const size = olIconStyle.getSize();
4✔
349
    if (Array.isArray(size)) {
4✔
350
      // TODO: create getters (and setters?) in openlayers
351
      // @ts-ignore
352
      let position = olIconStyle.offset_ as [number, number];
2✔
353
      // @ts-ignore
354
      const offsetOrigin = olIconStyle.offsetOrigin_ as string;
2✔
355
      if (offsetOrigin && offsetOrigin !== 'top-left') {
2!
356
        throw new Error(`Offset origin ${offsetOrigin} not supported`);
×
357
      }
358

359
      return {
2✔
360
        source: olIconStyle.getSrc()!,
361
        position,
362
        size: size as [number, number]
363
      };
364
    } else {
365
      return olIconStyle.getSrc() ? olIconStyle.getSrc() : undefined;
2!
366
    }
367

368
  }
369

370
  /**
371
   * Get the GeoStyler-Style LineSymbolizer from an OpenLayers Style object.
372
   *
373
   * @param olStyle The OpenLayers Style object
374
   * @return The GeoStyler-Style LineSymbolizer
375
   */
376
  getLineSymbolizerFromOlStyle(olStyle: OlStyle): LineSymbolizer {
377
    const olStrokeStyle = olStyle.getStroke();
4✔
378
    // getLineDash returns null not undefined. So we have to double check
379
    const dashArray = olStrokeStyle ? olStrokeStyle.getLineDash() : undefined;
4!
380

381
    return {
4✔
382
      kind: 'Line',
383
      color: olStrokeStyle ? OlStyleUtil.getHexColor(olStrokeStyle.getColor() as string) as string : undefined,
4!
384
      opacity: olStrokeStyle ? OlStyleUtil.getOpacity(olStrokeStyle.getColor() as string) : undefined,
4!
385
      width: olStrokeStyle ? olStrokeStyle.getWidth() : undefined,
4!
386
      cap: olStrokeStyle ? <LineSymbolizer['cap']> olStrokeStyle.getLineCap() : 'butt',
4!
387
      join: olStrokeStyle ? <LineSymbolizer['join']> olStrokeStyle.getLineJoin() : 'miter',
4!
388
      dasharray: dashArray ? dashArray : undefined,
4✔
389
      dashOffset: olStrokeStyle ? olStrokeStyle.getLineDashOffset() : undefined
4!
390
    };
391
  }
392

393
  /**
394
   * Get the GeoStyler-Style FillSymbolizer from an OpenLayers Style object.
395
   *
396
   * PolygonSymbolizer Stroke is just partially supported.
397
   *
398
   * @param olStyle The OpenLayers Style object
399
   * @return The GeoStyler-Style FillSymbolizer
400
   */
401
  getFillSymbolizerFromOlStyle(olStyle: OlStyle): FillSymbolizer {
402
    const olFillStyle = olStyle.getFill();
6✔
403
    const olStrokeStyle = olStyle.getStroke();
6✔
404
    // getLineDash returns null not undefined. So we have to double check
405
    const outlineDashArray = olStrokeStyle ? olStrokeStyle.getLineDash() : undefined;
6✔
406

407
    const symbolizer: FillSymbolizer = {
6✔
408
      kind: 'Fill'
409
    };
410

411
    if (olFillStyle) {
6!
412
      symbolizer.color = OlStyleUtil.getHexColor(olFillStyle.getColor() as string);
6✔
413
    }
414
    if (olFillStyle) {
6!
415
      symbolizer.fillOpacity = OlStyleUtil.getOpacity(olFillStyle.getColor() as string);
6✔
416
    }
417
    if (olStrokeStyle) {
6✔
418
      symbolizer.outlineColor = OlStyleUtil.getHexColor(olStrokeStyle.getColor() as string);
2✔
419
    }
420
    if (outlineDashArray) {
6✔
421
      symbolizer.outlineDasharray = outlineDashArray;
2✔
422
    }
423
    if (olStrokeStyle) {
6✔
424
      symbolizer.outlineOpacity = OlStyleUtil.getOpacity(olStrokeStyle.getColor() as string);
2✔
425
    }
426
    if (olStrokeStyle && olStrokeStyle.getWidth()) {
6✔
427
      symbolizer.outlineWidth = olStrokeStyle.getWidth();
2✔
428
    }
429
    return symbolizer;
6✔
430

431
  }
432

433
  /**
434
   * Get the GeoStyler-Style TextSymbolizer from an OpenLayers Style object.
435
   *
436
   *
437
   * @param olStyle The OpenLayers Style object
438
   * @return The GeoStyler-Style TextSymbolizer
439
   */
440
  getTextSymbolizerFromOlStyle(olStyle: OlStyle): TextSymbolizer {
441
    const olTextStyle = olStyle.getText();
14✔
442
    if (!olTextStyle) {
14!
443
      throw new Error('Could not get text from olStyle.');
×
444
    }
445
    const olFillStyle = olTextStyle.getFill();
14✔
446
    const olStrokeStyle = olTextStyle.getStroke();
14✔
447
    const offsetX = olTextStyle.getOffsetX();
14✔
448
    const offsetY = olTextStyle.getOffsetY();
14✔
449
    const font = olTextStyle.getFont();
14✔
450
    const rotation = olTextStyle.getRotation();
14✔
451
    const allowOverlap = olTextStyle.getOverflow() ? olTextStyle.getOverflow() : undefined;
14!
452
    const placement = olTextStyle.getPlacement();
14✔
453
    const text = olTextStyle.getText();
14✔
454
    const label = Array.isArray(text) ? text[0] : text;
14!
455
    let fontSize: number = Infinity;
14✔
456
    let fontFamily: string[]|undefined = undefined;
14✔
457
    let fontWeight: 'normal' | 'bold' | undefined = undefined;
14✔
458
    let fontStyle: 'normal' | 'italic' | 'oblique' | undefined = undefined;
14✔
459
    if (font) {
14✔
460
      const fontObj = parseFont(font);
10✔
461
      if (fontObj['font-weight']) {
10✔
462
        fontWeight = fontObj['font-weight'];
4✔
463
      }
464
      if (fontObj['font-size']) {
10!
465
        fontSize = parseInt(fontObj['font-size'], 10);
10✔
466
      }
467
      if (fontObj['font-family']) {
10!
468
        const fontFamilies = fontObj['font-family'];
10✔
469
        fontFamily = fontFamilies?.map((f: string) => f.includes(' ') ? '"' + f + '"' : f);
18✔
470
      }
471
      if (fontObj['font-style']) {
10✔
472
        fontStyle = fontObj['font-style'];
2✔
473
      }
474
    }
475

476
    return {
14✔
477
      kind: 'Text',
478
      label,
479
      placement,
480
      allowOverlap,
481
      color: olFillStyle ? OlStyleUtil.getHexColor(olFillStyle.getColor() as string) : undefined,
14!
482
      size: isFinite(fontSize) ? fontSize : undefined,
14✔
483
      font: fontFamily,
484
      fontWeight: fontWeight || undefined,
24✔
485
      fontStyle: fontStyle || undefined,
26✔
486
      offset: (offsetX !== undefined) && (offsetY !== undefined) ? [offsetX, offsetY] : [0, 0],
42!
487
      haloColor: olStrokeStyle && olStrokeStyle.getColor() ?
34✔
488
        OlStyleUtil.getHexColor(olStrokeStyle.getColor() as string) : undefined,
489
      haloWidth: olStrokeStyle ? olStrokeStyle.getWidth() : undefined,
14✔
490
      rotate: (rotation !== undefined) ? rotation / Math.PI * 180 : undefined
14✔
491
    };
492
  }
493

494
  /**
495
   * Get the GeoStyler-Style Symbolizer from an OpenLayers Style object.
496
   *
497
   * @param olStyles The OpenLayers Style object
498
   * @return The GeoStyler-Style Symbolizer array
499
   */
500
  getSymbolizersFromOlStyle(olStyles: OlStyle[]): Symbolizer[] {
501
    const symbolizers: Symbolizer[] = [];
58✔
502
    olStyles.forEach(olStyle => {
58✔
503
      let symbolizer: Symbolizer;
504
      const styleType: StyleType = this.getStyleTypeFromOlStyle(olStyle);
60✔
505
      switch (styleType) {
60!
506
        case 'Point':
507
          if (olStyle.getText() && !OlStyleUtil.getIsMarkSymbolizerFont((olStyle as any).getText().getFont())) {
50✔
508
            symbolizer = this.getTextSymbolizerFromOlStyle(olStyle);
6✔
509
          } else {
510
            symbolizer = this.getPointSymbolizerFromOlStyle(olStyle);
44✔
511
          }
512
          break;
50✔
513
        case 'Line':
514
          symbolizer = this.getLineSymbolizerFromOlStyle(olStyle);
4✔
515
          break;
4✔
516
        case 'Fill':
517
          symbolizer = this.getFillSymbolizerFromOlStyle(olStyle);
6✔
518
          break;
6✔
519
        default:
520
          throw new Error('Failed to parse SymbolizerKind from OpenLayers Style');
×
521
      }
522
      symbolizers.push(symbolizer);
60✔
523
    });
524

525
    return symbolizers;
58✔
526
  }
527

528
  /**
529
   * Get the GeoStyler-Style Rule from an OpenLayers Style object.
530
   *
531
   * @param olStyles The OpenLayers Style object
532
   * @return The GeoStyler-Style Rule
533
   */
534
  getRuleFromOlStyle(olStyles: OlStyle | OlStyle[]): Rule {
535
    let symbolizers: Symbolizer[];
536
    const name = 'OL Style Rule 0';
58✔
537

538
    if (Array.isArray(olStyles)) {
58✔
539
      symbolizers = this.getSymbolizersFromOlStyle(olStyles);
4✔
540
    } else {
541
      symbolizers = this.getSymbolizersFromOlStyle([olStyles]);
54✔
542
    }
543

544
    return {
58✔
545
      name, symbolizers
546
    };
547
  }
548

549
  /**
550
   * Get the GeoStyler-Style Symbolizer from an OpenLayers Style object.
551
   *
552
   * @param olStyle The OpenLayers Style object
553
   * @return The GeoStyler-Style Symbolizer
554
   */
555
  getStyleTypeFromOlStyle(olStyle: OlStyle): StyleType {
556
    let styleType: StyleType;
557

558
    if (olStyle.getImage() instanceof this.OlStyleImageConstructor) {
60✔
559
      styleType = 'Point';
42✔
560
    } else if (olStyle.getText() instanceof this.OlStyleTextConstructor) {
18✔
561
      styleType = 'Point';
8✔
562
    } else if (olStyle.getFill() instanceof this.OlStyleFillConstructor) {
10✔
563
      styleType = 'Fill';
6✔
564
    } else if (olStyle.getStroke() && !olStyle.getFill()) {
4!
565
      styleType = 'Line';
4✔
566
    } else {
567
      throw new Error('StyleType could not be detected');
×
568
    }
569

570
    return styleType;
60✔
571
  }
572

573
  /**
574
   * Get the GeoStyler-Style Style from an OpenLayers Style object.
575
   *
576
   * @param olStyle The OpenLayers Style object
577
   * @return The GeoStyler-Style Style
578
   */
579
  olStyleToGeoStylerStyle(olStyle: OlStyle | OlStyle[]): Style {
580
    const name = 'OL Style';
58✔
581
    const rule = this.getRuleFromOlStyle(olStyle);
58✔
582
    return {
58✔
583
      name,
584
      rules: [rule]
585
    };
586
  }
587

588
  /**
589
   * The readStyle implementation of the GeoStyler-Style StyleParser interface.
590
   * It reads an OpenLayers Style, an array of OpenLayers Styles or an olParserStyleFct and returns a Promise.
591
   *
592
   * The Promise itself resolves with a GeoStyler-Style Style.
593
   *
594
   * @param olStyle The style to be parsed
595
   * @return The Promise resolving with the GeoStyler-Style Style
596
   */
597
  readStyle(olStyle: OlStyleLike): Promise<ReadStyleResult> {
598
    return new Promise<ReadStyleResult>((resolve) => {
58✔
599
      try {
58✔
600
        if (this.isOlParserStyleFct(olStyle)) {
58!
601
          resolve({
×
602
            output: olStyle.__geoStylerStyle
603
          });
604
        } else {
605
          olStyle = olStyle as OlStyle | OlStyle[];
58✔
606
          const geoStylerStyle: Style = this.olStyleToGeoStylerStyle(olStyle);
58✔
607
          const unsupportedProperties = this.checkForUnsupportedProperties(geoStylerStyle);
58✔
608
          resolve({
58✔
609
            output: geoStylerStyle,
610
            unsupportedProperties
611
          });
612
        }
613
      } catch (error) {
614
        resolve({
×
615
          errors: [error]
616
        });
617
      }
618
    });
619
  }
620

621
  /**
622
   * The writeStyle implementation of the GeoStyler-Style StyleParser interface.
623
   * It reads a GeoStyler-Style Style and returns a Promise.
624
   * The Promise itself resolves one of three types
625
   *
626
   * 1. OlStyle if input Style consists of
627
   *    one rule with one symbolizer, no filter, no scaleDenominator, no TextSymbolizer
628
   * 2. OlStyle[] if input Style consists of
629
   *    one rule with multiple symbolizers, no filter, no scaleDenominator, no TextSymbolizer
630
   * 3. OlParserStyleFct for everything else
631
   *
632
   * @param geoStylerStyle A GeoStyler-Style Style.
633
   * @return The Promise resolving with one of above mentioned style types.
634
   */
635
  writeStyle(geoStylerStyle: Style): Promise<WriteStyleResult<OlStyle | OlStyle[] | OlParserStyleFct>> {
636
    return new Promise<WriteStyleResult>((resolve) => {
94✔
637
      const clonedStyle = structuredClone(geoStylerStyle);
94✔
638
      const unsupportedProperties = this.checkForUnsupportedProperties(clonedStyle);
94✔
639
      try {
94✔
640
        const olStyle = this.getOlStyleTypeFromGeoStylerStyle(clonedStyle);
94✔
641
        resolve({
94✔
642
          output: olStyle,
643
          unsupportedProperties,
644
          warnings: unsupportedProperties && ['Your style contains unsupportedProperties!']
112✔
645
        });
646
      } catch (error) {
647
        resolve({
×
648
          errors: [error]
649
        });
650
      }
651
    });
652
  }
653

654
  checkForUnsupportedProperties(geoStylerStyle: Style): UnsupportedProperties | undefined {
655
    const capitalizeFirstLetter = (a: string) => a[0].toUpperCase() + a.slice(1);
180✔
656
    const unsupportedProperties: UnsupportedProperties = {};
152✔
657
    geoStylerStyle.rules.forEach(rule => {
152✔
658
      // ScaleDenominator and Filters are completly supported so we just check for symbolizers
659
      rule.symbolizers.forEach(symbolizer => {
172✔
660
        const key = capitalizeFirstLetter(`${symbolizer.kind}Symbolizer`);
180✔
661
        const value = this.unsupportedProperties?.Symbolizer?.[key as SymbolizerKeyType];
180✔
662
        if (value) {
180!
663
          if (typeof value === 'string') {
180!
664
            if (!unsupportedProperties.Symbolizer) {
×
665
              unsupportedProperties.Symbolizer = {};
×
666
            }
667
            unsupportedProperties.Symbolizer[key as SymbolizerKeyType] = value;
×
668
          } else {
669
            Object.keys(symbolizer).forEach(property => {
180✔
670
              if (value[property]) {
1,200✔
671
                if (!unsupportedProperties.Symbolizer) {
30✔
672
                  unsupportedProperties.Symbolizer = {};
28✔
673
                }
674
                if (!unsupportedProperties.Symbolizer[key as SymbolizerKeyType]) {
30!
675
                  (unsupportedProperties.Symbolizer as any)[key] = {};
30✔
676
                }
677
                unsupportedProperties.Symbolizer
30✔
678
                  [key as SymbolizerKeyType][property] = value[property];
679
              }
680
            });
681
          }
682
        }
683
      });
684
    });
685
    if (Object.keys(unsupportedProperties).length > 0) {
152✔
686
      return unsupportedProperties;
28✔
687
    }
688
    return undefined;
124✔
689
  }
690

691
  /**
692
   * Decides which OlStyleType should be returned depending on given geoStylerStyle.
693
   * Three OlStyleTypes are possible:
694
   *
695
   * 1. OlStyle if input Style consists of
696
   *    one rule with one symbolizer, no filter, no scaleDenominator, no TextSymbolizer
697
   * 2. OlStyle[] if input Style consists of
698
   *    one rule with multiple symbolizers, no filter, no scaleDenominator, no TextSymbolizer
699
   * 3. OlParserStyleFct for everything else
700
   *
701
   * @param geoStylerStyle A GeoStyler-Style Style
702
   */
703
  getOlStyleTypeFromGeoStylerStyle(geoStylerStyle: Style): OlStyle | OlStyle[] | OlParserStyleFct {
704
    const rules = geoStylerStyle.rules;
94✔
705
    const nrRules = rules.length;
94✔
706
    if (nrRules === 1) {
94✔
707
      const hasFilter = geoStylerStyle?.rules?.[0]?.filter !== undefined ? true : false;
76!
708
      const hasMinScale = geoStylerStyle?.rules?.[0]?.scaleDenominator?.min !== undefined ? true : false;
76✔
709
      const hasMaxScale = geoStylerStyle?.rules?.[0]?.scaleDenominator?.max !== undefined ? true : false;
76✔
710
      const hasScaleDenominator = hasMinScale || hasMaxScale ? true : false;
76✔
711
      const hasFunctions = OlStyleUtil.containsGeoStylerFunctions(geoStylerStyle);
76✔
712

713
      const nrSymbolizers = geoStylerStyle.rules[0].symbolizers.length;
76✔
714
      const hasTextSymbolizer = rules[0].symbolizers.some((symbolizer: Symbolizer) => {
76✔
715
        return symbolizer.kind === 'Text';
82✔
716
      });
717
      const hasDynamicIconSymbolizer = rules[0].symbolizers.some((symbolizer: Symbolizer) => {
76✔
718
        return symbolizer.kind === 'Icon' && typeof(symbolizer.image) === 'string' && symbolizer.image.includes('{{');
82✔
719
      });
720
      if (!hasFilter && !hasScaleDenominator && !hasTextSymbolizer && !hasDynamicIconSymbolizer && !hasFunctions) {
76✔
721
        if (nrSymbolizers === 1) {
54✔
722
          return this.geoStylerStyleToOlStyle(geoStylerStyle);
48✔
723
        } else {
724
          return this.geoStylerStyleToOlStyleArray(geoStylerStyle);
6✔
725
        }
726
      } else {
727
        return this.geoStylerStyleToOlParserStyleFct(geoStylerStyle);
22✔
728
      }
729
    } else {
730
      return this.geoStylerStyleToOlParserStyleFct(geoStylerStyle);
18✔
731
    }
732
  }
733

734
  /**
735
   * Parses the first symbolizer of the first rule of a GeoStyler-Style Style.
736
   *
737
   * @param geoStylerStyle GeoStyler-Style Style
738
   * @return An OpenLayers Style Object
739
   */
740
  geoStylerStyleToOlStyle(geoStylerStyle: Style): OlStyle {
741
    const rule = geoStylerStyle.rules[0];
48✔
742
    const symbolizer = rule.symbolizers[0];
48✔
743
    const olSymbolizer = this.getOlSymbolizerFromSymbolizer(symbolizer);
48✔
744
    return olSymbolizer;
48✔
745
  }
746

747
  /**
748
   * Parses all symbolizers of the first rule of a GeoStyler-Style Style.
749
   *
750
   * @param geoStylerStyle GeoStyler-Style Style
751
   * @return An array of OpenLayers Style Objects
752
   */
753
  geoStylerStyleToOlStyleArray(geoStylerStyle: Style): OlStyle[] {
754
    const rule = geoStylerStyle.rules[0];
6✔
755
    const olStyles: any[] = [];
6✔
756
    rule.symbolizers.forEach((symbolizer: Symbolizer) => {
6✔
757
      const olSymbolizer: any = this.getOlSymbolizerFromSymbolizer(symbolizer);
12✔
758
      olStyles.push(olSymbolizer);
12✔
759
    });
760
    return olStyles;
6✔
761
  }
762

763
  /**
764
   * Get the OpenLayers Style object from an GeoStyler-Style Style
765
   *
766
   * @param geoStylerStyle A GeoStyler-Style Style.
767
   * @return An OlParserStyleFct
768
   */
769
  geoStylerStyleToOlParserStyleFct(geoStylerStyle: Style): OlParserStyleFct {
770
    const rules = structuredClone(geoStylerStyle.rules);
40✔
771
    const olStyle = (feature: any, resolution: number): any[] => {
40✔
772
      const styles: any[] = [];
64✔
773

774
      // calculate scale for resolution (from ol-util MapUtil)
775
      const dpi = 25.4 / 0.28;
64✔
776
      const mpu = METERS_PER_UNIT.m;
64✔
777
      const inchesPerMeter = 39.37;
64✔
778
      const scale = resolution * mpu * inchesPerMeter * dpi;
64✔
779

780
      rules.forEach((rule: Rule) => {
64✔
781
        // handling scale denominator
782
        let minScale = rule?.scaleDenominator?.min;
106✔
783
        let maxScale = rule?.scaleDenominator?.max;
106✔
784
        let isWithinScale = true;
106✔
785
        if (minScale || maxScale) {
106✔
786
          minScale = isGeoStylerFunction(minScale) ? OlStyleUtil.evaluateNumberFunction(minScale) : minScale;
28!
787
          maxScale = isGeoStylerFunction(maxScale) ? OlStyleUtil.evaluateNumberFunction(maxScale) : maxScale;
28!
788
          if (minScale && scale < minScale) {
28✔
789
            isWithinScale = false;
4✔
790
          }
791
          if (maxScale && scale >= maxScale) {
28✔
792
            isWithinScale = false;
10✔
793
          }
794
        }
795

796
        // handling filter
797
        let matchesFilter: boolean = false;
106✔
798
        if (!rule.filter) {
106✔
799
          matchesFilter = true;
72✔
800
        } else {
801
          try {
34✔
802
            matchesFilter = this.geoStylerFilterToOlParserFilter(feature, rule.filter);
34✔
803
          } catch (e) {
804
            matchesFilter = false;
2✔
805
          }
806
        }
807

808
        if (isWithinScale && matchesFilter) {
106✔
809
          rule.symbolizers.forEach((symb: Symbolizer) => {
70✔
810
            if (symb.visibility === false) {
70!
811
              styles.push(null);
×
812
            }
813

814
            if (isGeoStylerBooleanFunction(symb.visibility)) {
70!
815
              const visibility = OlStyleUtil.evaluateBooleanFunction(symb.visibility);
×
816
              if (!visibility) {
×
817
                styles.push(null);
×
818
              }
819
            }
820

821
            const olSymbolizer: any = this.getOlSymbolizerFromSymbolizer(symb, feature);
70✔
822
            // either an OlStyle or an ol.StyleFunction. OpenLayers only accepts an array
823
            // of OlStyles, not ol.StyleFunctions.
824
            // So we have to check it and in case of an ol.StyleFunction call that function
825
            // and add the returned style to const styles.
826
            if (typeof olSymbolizer !== 'function') {
70✔
827
              styles.push(olSymbolizer);
64✔
828
            } else {
829
              const styleFromFct: any = olSymbolizer(feature, resolution);
6✔
830
              styles.push(styleFromFct);
6✔
831
            }
832
          });
833
        }
834
      });
835
      return styles;
64✔
836
    };
837
    const olStyleFct: OlParserStyleFct = olStyle as OlParserStyleFct;
40✔
838
    olStyleFct.__geoStylerStyle = geoStylerStyle;
40✔
839
    return olStyleFct;
40✔
840
  }
841

842
  /**
843
   * Checks if a feature matches given filter expression(s)
844
   * @param feature ol.Feature
845
   * @param filter Filter
846
   * @return boolean true if feature matches filter expression
847
   */
848
  geoStylerFilterToOlParserFilter(feature: any, filter: Filter): boolean {
849
    const operatorMapping: any = {
84✔
850
      '&&': true,
851
      '||': true,
852
      '!': true
853
    };
854

855
    let matchesFilter: boolean = true;
84✔
856
    if (isGeoStylerBooleanFunction(filter)) {
84!
857
      return OlStyleUtil.evaluateBooleanFunction(filter, feature);
×
858
    }
859
    if (filter === true || filter === false) {
84!
860
      return filter;
×
861
    }
862
    const operator: Operator = filter[0];
84✔
863
    let isNestedFilter: boolean = false;
84✔
864
    if (operatorMapping[operator]) {
84✔
865
      isNestedFilter = true;
26✔
866
    }
867
    try {
84✔
868
      if (isNestedFilter) {
84✔
869
        let intermediate: boolean;
870
        let restFilter: any;
871
        switch (filter[0]) {
26!
872
          case '&&':
873
            intermediate = true;
14✔
874
            restFilter = filter.slice(1);
14✔
875
            restFilter.forEach((f: Filter) => {
14✔
876
              if (!this.geoStylerFilterToOlParserFilter(feature, f)) {
32✔
877
                intermediate = false;
8✔
878
              }
879
            });
880
            matchesFilter = intermediate;
12✔
881
            break;
12✔
882
          case '||':
883
            intermediate = false;
6✔
884
            restFilter = filter.slice(1);
6✔
885
            restFilter.forEach((f: Filter) => {
6✔
886
              if (this.geoStylerFilterToOlParserFilter(feature, f)) {
12!
887
                intermediate = true;
12✔
888
              }
889
            });
890
            matchesFilter = intermediate;
6✔
891
            break;
6✔
892
          case '!':
893
            matchesFilter = !this.geoStylerFilterToOlParserFilter(feature, filter[1]);
6✔
894
            break;
6✔
895
          default:
896
            throw new Error('Cannot parse Filter. Unknown combination or negation operator.');
×
897
        }
898
      } else {
899
        let arg1: any;
900
        if (isGeoStylerFunction(filter[1])) {
58✔
901
          arg1 = OlStyleUtil.evaluateFunction(filter[1], feature);
28✔
902
        } else {
903
          arg1 = feature.get(filter[1]);
30✔
904
        }
905
        let arg2: any;
906
        if (isGeoStylerFunction(filter[2])) {
58✔
907
          arg2 = OlStyleUtil.evaluateFunction(filter[2], feature);
24✔
908
        } else {
909
          arg2 = filter[2];
34✔
910
        }
911
        switch (filter[0]) {
58!
912
          case '==':
913
            matchesFilter = ('' + arg1) === ('' + arg2);
20✔
914
            break;
20✔
915
          case '*=':
916
            // inspired by
917
            // https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/String/includes#Polyfill
918
            if (typeof arg2 === 'string' && typeof arg1 === 'string') {
×
919
              if (arg2.length > arg1.length) {
×
920
                matchesFilter = false;
×
921
              } else {
922
                matchesFilter = arg1.indexOf(arg2) !== -1;
×
923
              }
924
            }
925
            break;
×
926
          case '!=':
927
            matchesFilter = ('' + arg1) !== ('' + arg2);
×
928
            break;
×
929
          case '<':
930
            matchesFilter = Number(arg1) < Number(arg2);
12✔
931
            break;
12✔
932
          case '<=':
933
            matchesFilter = Number(arg1) <= Number(arg2);
6✔
934
            break;
6✔
935
          case '>':
936
            matchesFilter = Number(arg1) > Number(arg2);
6✔
937
            break;
6✔
938
          case '>=':
939
            matchesFilter = Number(arg1) >= Number(arg2);
12✔
940
            break;
12✔
941
          default:
942
            throw new Error('Cannot parse Filter. Unknown comparison operator.');
2✔
943
        }
944
      }
945
    } catch (e) {
946
      throw new Error('Cannot parse Filter. Invalid structure.');
4✔
947
    }
948
    return matchesFilter;
80✔
949
  }
950

951
  /**
952
   * Get the OpenLayers Style object or an OL StyleFunction from an
953
   * GeoStyler-Style Symbolizer.
954
   *
955
   * @param symbolizer A GeoStyler-Style Symbolizer.
956
   * @return The OpenLayers Style object or a StyleFunction
957
   */
958
  getOlSymbolizerFromSymbolizer(symbolizer: Symbolizer, feature?: OlFeature): OlStyle {
959
    let olSymbolizer: any;
960
    symbolizer = structuredClone(symbolizer);
130✔
961

962
    switch (symbolizer.kind) {
130!
963
      case 'Mark':
964
        olSymbolizer = this.getOlPointSymbolizerFromMarkSymbolizer(symbolizer, feature);
76✔
965
        break;
76✔
966
      case 'Icon':
967
        olSymbolizer = this.getOlIconSymbolizerFromIconSymbolizer(symbolizer, feature);
8✔
968
        break;
8✔
969
      case 'Text':
970
        olSymbolizer = this.getOlTextSymbolizerFromTextSymbolizer(symbolizer, feature);
18✔
971
        break;
18✔
972
      case 'Line':
973
        olSymbolizer = this.getOlLineSymbolizerFromLineSymbolizer(symbolizer, feature);
14✔
974
        break;
14✔
975
      case 'Fill':
976
        olSymbolizer = this.getOlPolygonSymbolizerFromFillSymbolizer(symbolizer, feature);
14✔
977
        break;
14✔
978
      default:
979
        // Return the OL default style since the TS type binding does not allow
980
        // us to set olSymbolizer to undefined
981
        const fill = new this.OlStyleFillConstructor({
×
982
          color: 'rgba(255,255,255,0.4)'
983
        });
984
        const stroke = new this.OlStyleStrokeConstructor({
×
985
          color: '#3399CC',
986
          width: 1.25
987
        });
988
        olSymbolizer = new this.OlStyleConstructor({
×
989
          image: new this.OlStyleCircleConstructor({
990
            fill: fill,
991
            stroke: stroke,
992
            radius: 5
993
          }),
994
          fill: fill,
995
          stroke: stroke
996
        });
997
        break;
×
998
    }
999

1000
    return olSymbolizer;
130✔
1001
  }
1002

1003
  /**
1004
   * Get the OL Style object  from an GeoStyler-Style MarkSymbolizer.
1005
   *
1006
   * @param markSymbolizer A GeoStyler-Style MarkSymbolizer.
1007
   * @return The OL Style object
1008
   */
1009
  getOlPointSymbolizerFromMarkSymbolizer(markSymbolizer: MarkSymbolizer, feature?: OlFeature): OlStyleRegularshape {
1010
    let stroke: any;
1011

1012
    for (const key of Object.keys(markSymbolizer)) {
78✔
1013
      if (isGeoStylerFunction(markSymbolizer[key as keyof MarkSymbolizer])) {
344✔
1014
        (markSymbolizer as any)[key] = OlStyleUtil.evaluateFunction((markSymbolizer as any)[key], feature);
8✔
1015
      }
1016
    }
1017

1018
    const strokeColor = markSymbolizer.strokeColor as string;
78✔
1019
    const strokeOpacity = markSymbolizer.strokeOpacity as number;
78✔
1020

1021
    const sColor = strokeColor && (strokeOpacity !== undefined)
78!
1022
      ? OlStyleUtil.getRgbaColor(strokeColor, strokeOpacity)
1023
      : markSymbolizer.strokeColor as string;
1024

1025
    if (markSymbolizer.strokeColor || markSymbolizer.strokeWidth !== undefined) {
78✔
1026
      stroke = new this.OlStyleStrokeConstructor({
2✔
1027
        color: sColor,
1028
        width: markSymbolizer.strokeWidth as number
1029
      });
1030
    }
1031

1032
    const color = markSymbolizer.color as string;
78✔
1033
    const opacity = markSymbolizer.opacity as number;
78✔
1034
    const radius = markSymbolizer.radius as number;
78✔
1035
    const fillOpacity = markSymbolizer.fillOpacity as number;
78✔
1036
    const fColor = color && (fillOpacity !== undefined)
78!
1037
      ? OlStyleUtil.getRgbaColor(color, fillOpacity ?? 1)
×
1038
      : color;
1039

1040
    const fill = new this.OlStyleFillConstructor({
78✔
1041
      color: fColor
1042
    });
1043

1044
    let olStyle: any;
1045
    const shapeOpts = {
78✔
1046
      fill: fill,
1047
      radius: radius ?? 5,
80✔
1048
      rotation: typeof(markSymbolizer.rotate) === 'number' ? markSymbolizer.rotate * Math.PI / 180 : undefined,
78✔
1049
      stroke: stroke,
1050
      displacement: Array.isArray(markSymbolizer.offset) ? markSymbolizer.offset.map(Number) : undefined
78✔
1051
    };
1052

1053
    switch (markSymbolizer.wellKnownName) {
78✔
1054
      case 'shape://dot':
1055
      case 'circle':
1056
        olStyle = new this.OlStyleConstructor({
48✔
1057
          image: new this.OlStyleCircleConstructor(shapeOpts as OlStyleCircleOptions)
1058
        });
1059
        break;
48✔
1060
      case 'square':
1061
        olStyle = new this.OlStyleConstructor({
2✔
1062
          image: new this.OlStyleRegularshapeConstructor({
1063
            ...shapeOpts,
1064
            points: 4,
1065
            angle: 45 * Math.PI / 180
1066
          })
1067
        });
1068
        break;
2✔
1069
      case 'triangle':
1070
        olStyle = new this.OlStyleConstructor({
2✔
1071
          image: new this.OlStyleRegularshapeConstructor({
1072
            ...shapeOpts,
1073
            points: 3,
1074
            angle: 0
1075
          })
1076
        });
1077
        break;
2✔
1078
      case 'star':
1079
        olStyle = new this.OlStyleConstructor({
2✔
1080
          image: new this.OlStyleRegularshapeConstructor({
1081
            ...shapeOpts,
1082
            points: 5,
1083
            radius2: shapeOpts.radius! / 2.5,
1084
            angle: 0
1085
          })
1086
        });
1087
        break;
2✔
1088
      case 'shape://plus':
1089
      case 'cross':
1090
        // openlayers does not seem to set a default stroke color,
1091
        // which is needed for regularshapes with radius2 = 0
1092
        if (shapeOpts.stroke === undefined) {
6!
1093
          shapeOpts.stroke = new this.OlStyleStrokeConstructor({
6✔
1094
            color: '#000'
1095
          });
1096
        }
1097
        olStyle = new this.OlStyleConstructor({
6✔
1098
          image: new this.OlStyleRegularshapeConstructor({
1099
            ...shapeOpts,
1100
            points: 4,
1101
            radius2: 0,
1102
            angle: 0
1103
          })
1104
        });
1105
        break;
6✔
1106
      case 'shape://times':
1107
      case 'x':
1108
        // openlayers does not seem to set a default stroke color,
1109
        // which is needed for regularshapes with radius2 = 0
1110
        if (shapeOpts.stroke === undefined) {
4!
1111
          shapeOpts.stroke = new this.OlStyleStrokeConstructor({
4✔
1112
            color: '#000'
1113
          });
1114
        }
1115
        olStyle = new this.OlStyleConstructor({
4✔
1116
          image: new this.OlStyleRegularshapeConstructor({
1117
            ...shapeOpts,
1118
            points: 4,
1119
            radius2: 0,
1120
            angle: 45 * Math.PI / 180
1121
          })
1122
        });
1123
        break;
4✔
1124
      case 'shape://backslash':
1125
        // openlayers does not seem to set a default stroke color,
1126
        // which is needed for regularshapes with radius2 = 0
1127
        if (shapeOpts.stroke === undefined) {
2!
1128
          shapeOpts.stroke = new this.OlStyleStrokeConstructor({
2✔
1129
            color: '#000'
1130
          });
1131
        }
1132
        olStyle = new this.OlStyleConstructor({
2✔
1133
          image: new this.OlStyleRegularshapeConstructor({
1134
            ...shapeOpts,
1135
            points: 2,
1136
            angle: 2 * Math.PI - (Math.PI / 4)
1137
          })
1138
        });
1139
        break;
2✔
1140
      case 'shape://horline':
1141
        // openlayers does not seem to set a default stroke color,
1142
        // which is needed for regularshapes with radius2 = 0
1143
        if (shapeOpts.stroke === undefined) {
2!
1144
          shapeOpts.stroke = new this.OlStyleStrokeConstructor({
2✔
1145
            color: '#000'
1146
          });
1147
        }
1148
        olStyle = new this.OlStyleConstructor({
2✔
1149
          image: new this.OlStyleRegularshapeConstructor({
1150
            ...shapeOpts,
1151
            points: 2,
1152
            angle: Math.PI / 2
1153
          })
1154
        });
1155
        break;
2✔
1156
      // so far, both arrows are closed arrows. Also, shape is a regular triangle with
1157
      // all sides of equal length. In geoserver arrows only have two sides of equal length.
1158
      // TODO redefine shapes of arrows?
1159
      case 'shape://oarrow':
1160
      case 'shape://carrow':
1161
        olStyle = new this.OlStyleConstructor({
4✔
1162
          image: new this.OlStyleRegularshapeConstructor({
1163
            ...shapeOpts,
1164
            points: 3,
1165
            angle: Math.PI / 2
1166
          })
1167
        });
1168
        break;
4✔
1169
      case 'shape://slash':
1170
        // openlayers does not seem to set a default stroke color,
1171
        // which is needed for regularshapes with radius2 = 0
1172
        if (shapeOpts.stroke === undefined) {
2!
1173
          shapeOpts.stroke = new this.OlStyleStrokeConstructor({
2✔
1174
            color: '#000'
1175
          });
1176
        }
1177
        olStyle = new this.OlStyleConstructor({
2✔
1178
          image: new this.OlStyleRegularshapeConstructor({
1179
            ...shapeOpts,
1180
            points: 2,
1181
            angle: Math.PI / 4
1182
          })
1183
        });
1184
        break;
2✔
1185
      case 'shape://vertline':
1186
        // openlayers does not seem to set a default stroke color,
1187
        // which is needed for regularshapes with radius2 = 0
1188
        if (shapeOpts.stroke === undefined) {
2!
1189
          shapeOpts.stroke = new this.OlStyleStrokeConstructor({
2✔
1190
            color: '#000'
1191
          });
1192
        }
1193
        olStyle = new this.OlStyleConstructor({
2✔
1194
          image: new this.OlStyleRegularshapeConstructor({
1195
            ...shapeOpts,
1196
            points: 2,
1197
            angle: 0
1198
          })
1199
        });
1200
        break;
2✔
1201
      default:
1202
        if (OlStyleUtil.getIsFontGlyphBased(markSymbolizer)) {
2!
1203
          olStyle = new this.OlStyleConstructor({
2✔
1204
            text: new this.OlStyleTextConstructor({
1205
              text: OlStyleUtil.getCharacterForMarkSymbolizer(markSymbolizer),
1206
              font: OlStyleUtil.getTextFontForMarkSymbolizer(markSymbolizer),
1207
              fill: shapeOpts.fill,
1208
              stroke: shapeOpts.stroke,
1209
              rotation: shapeOpts.rotation
1210
            })
1211
          });
1212
          break;
2✔
1213
        }
1214
        throw new Error('MarkSymbolizer cannot be parsed. Unsupported WellKnownName.');
×
1215
    }
1216

1217
    if (Number.isFinite(opacity) && olStyle.getImage()) {
78!
1218
      olStyle.getImage().setOpacity(opacity);
×
1219
    }
1220

1221
    return olStyle;
78✔
1222
  }
1223

1224
  /**
1225
   * Get the OL Style object  from an GeoStyler-Style IconSymbolizer.
1226
   *
1227
   * @param symbolizer  A GeoStyler-Style IconSymbolizer.
1228
   * @return The OL Style object
1229
   */
1230
  getOlIconSymbolizerFromIconSymbolizer(
1231
    symbolizer: IconSymbolizer,
1232
    feat?: OlFeature
1233
  ): OlStyle | OlStyleIcon | OlStyleFunction {
1234
    for (const key of Object.keys(symbolizer)) {
8✔
1235
      if (isGeoStylerFunction(symbolizer[key as keyof IconSymbolizer])) {
28!
1236
        (symbolizer as any)[key] = OlStyleUtil.evaluateFunction((symbolizer as any)[key], feat);
×
1237
      }
1238
    }
1239

1240
    const baseProps: OlStyleIconOptions = {
8✔
1241
      src: isSprite(symbolizer.image) ? symbolizer.image.source as string : symbolizer.image as string,
8✔
1242
      crossOrigin: 'anonymous',
1243
      opacity: symbolizer.opacity as number,
1244
      width: symbolizer.size as number,
1245
      // Rotation in openlayers is radians while we use degree
1246
      rotation: (typeof(symbolizer.rotate) === 'number' ? symbolizer.rotate * Math.PI / 180 : undefined) as number,
8✔
1247
      displacement: symbolizer.offset as [number, number],
1248
      size: isSprite(symbolizer.image) ? symbolizer.image.size as [number, number] : undefined,
8✔
1249
      offset: isSprite(symbolizer.image) ? symbolizer.image.position as [number, number] : undefined,
8✔
1250
    };
1251

1252
    // check if IconSymbolizer.image contains a placeholder
1253
    const prefix = '\\{\\{';
8✔
1254
    const suffix = '\\}\\}';
8✔
1255
    const regExp = new RegExp(prefix + '.*?' + suffix, 'g');
8✔
1256
    const regExpRes = typeof(symbolizer.image) === 'string' ? symbolizer.image.match(regExp) : null;
8✔
1257
    if (regExpRes) {
8✔
1258
      // if it contains a placeholder
1259
      // return olStyleFunction
1260
      const olPointStyledIconFn = (feature: any) => {
2✔
1261
        let src: string = OlStyleUtil.resolveAttributeTemplate(feature, symbolizer.image as string, '');
2✔
1262
        // src can't be blank, would trigger ol errors
1263
        if (!src) {
2!
1264
          src = symbolizer.image + '';
×
1265
        }
1266
        let image;
1267
        if (this.olIconStyleCache[src]) {
2!
1268
          image = this.olIconStyleCache[src];
×
1269
          if (baseProps.rotation !== undefined) {
×
1270
            image.setRotation(baseProps.rotation);
×
1271
          }
1272
          if (baseProps.opacity !== undefined) {
×
1273
            image.setOpacity(baseProps.opacity);
×
1274
          }
1275
        } else {
1276
          image = new this.OlStyleIconConstructor({
2✔
1277
            ...baseProps,
1278
            src // order is important
1279
          });
1280
          this.olIconStyleCache[src] = image;
2✔
1281
        }
1282
        const style = new this.OlStyleConstructor({
2✔
1283
          image
1284
        });
1285
        return style;
2✔
1286
      };
1287
      return olPointStyledIconFn;
2✔
1288
    } else {
1289
      return new this.OlStyleConstructor({
6✔
1290
        image: new this.OlStyleIconConstructor({
1291
          ...baseProps
1292
        })
1293
      });
1294
    }
1295
  }
1296

1297
  /**
1298
   * Get the OL Style object from an GeoStyler-Style LineSymbolizer.
1299
   *
1300
   * @param symbolizer A GeoStyler-Style LineSymbolizer.
1301
   * @return The OL Style object
1302
   */
1303
  getOlLineSymbolizerFromLineSymbolizer(symbolizer: LineSymbolizer, feat?: OlFeature): OlStyle | OlStyleStroke {
1304
    for (const key of Object.keys(symbolizer)) {
14✔
1305
      if (isGeoStylerFunction(symbolizer[key as keyof LineSymbolizer])) {
90!
1306
        (symbolizer as any)[key] = OlStyleUtil.evaluateFunction((symbolizer as any)[key], feat);
×
1307
      }
1308
    }
1309
    const color = symbolizer.color as string;
14✔
1310
    const opacity = symbolizer.opacity as number;
14✔
1311
    const sColor = (color && opacity !== null && opacity !== undefined) ?
14!
1312
      OlStyleUtil.getRgbaColor(color, opacity) : color;
1313

1314
    return new this.OlStyleConstructor({
14✔
1315
      stroke: new this.OlStyleStrokeConstructor({
1316
        color: sColor,
1317
        width: symbolizer.width as number,
1318
        lineCap: symbolizer.cap as CapType,
1319
        lineJoin: symbolizer.join as JoinType,
1320
        lineDash: symbolizer.dasharray as number[],
1321
        lineDashOffset: symbolizer.dashOffset as number
1322
      })
1323
    });
1324
  }
1325

1326
  /**
1327
   * Get the OL Style object from an GeoStyler-Style FillSymbolizer.
1328
   *
1329
   * @param symbolizer A GeoStyler-Style FillSymbolizer.
1330
   * @return The OL Style object
1331
   */
1332
  getOlPolygonSymbolizerFromFillSymbolizer(symbolizer: FillSymbolizer, feat?: OlFeature): OlStyle | OlStyleFill {
1333
    for (const key of Object.keys(symbolizer)) {
14✔
1334
      if (isGeoStylerFunction(symbolizer[key as keyof FillSymbolizer])) {
50✔
1335
        (symbolizer as any)[key] = OlStyleUtil.evaluateFunction((symbolizer as any)[key], feat);
2✔
1336
      }
1337
    }
1338

1339
    const color = symbolizer.color as string;
14✔
1340
    const opacity = symbolizer.fillOpacity as number;
14✔
1341
    const fColor = color && Number.isFinite(opacity)
14✔
1342
      ? OlStyleUtil.getRgbaColor(color, opacity)
1343
      : color;
1344

1345
    let fill = color
14✔
1346
      ? new this.OlStyleFillConstructor({color: fColor})
1347
      : undefined;
1348

1349
    const outlineColor = symbolizer.outlineColor as string;
14✔
1350
    const outlineOpacity = symbolizer.outlineOpacity as number;
14✔
1351
    const oColor = (outlineColor && Number.isFinite(outlineOpacity))
14✔
1352
      ? OlStyleUtil.getRgbaColor(outlineColor, outlineOpacity)
1353
      : outlineColor;
1354

1355
    const stroke = outlineColor || symbolizer.outlineWidth ? new this.OlStyleStrokeConstructor({
14✔
1356
      color: oColor,
1357
      width: symbolizer.outlineWidth as number,
1358
      lineDash: symbolizer.outlineDasharray as number[],
1359
    }) : undefined;
1360

1361
    const olStyle = new this.OlStyleConstructor({
14✔
1362
      fill,
1363
      stroke
1364
    });
1365

1366
    if (symbolizer.graphicFill) {
14✔
1367
      const pattern = this.getOlPatternFromGraphicFill(symbolizer.graphicFill);
2✔
1368
      if (!fill) {
2!
1369
        fill = new this.OlStyleFillConstructor({});
2✔
1370
      }
1371
      if (pattern) {
2!
1372
        fill.setColor(pattern);
2✔
1373
      }
1374
      olStyle.setFill(fill);
2✔
1375
    }
1376

1377
    return olStyle;
14✔
1378
  }
1379

1380
  /**
1381
   * Get the pattern for a graphicFill.
1382
   *
1383
   * This creates a CanvasPattern based on the
1384
   * properties of the given PointSymbolizer. Currently,
1385
   * only IconSymbolizer and MarkSymbolizer are supported.
1386
   *
1387
   * @param graphicFill The Symbolizer that holds the pattern config.
1388
   * @returns The created CanvasPattern, or null.
1389
   */
1390
  getOlPatternFromGraphicFill(graphicFill: PointSymbolizer): CanvasPattern | null {
1391
    let graphicFillStyle: any;
1392
    if (isIconSymbolizer(graphicFill)) {
2!
1393
      graphicFillStyle = this.getOlIconSymbolizerFromIconSymbolizer(graphicFill);
×
1394
      const graphicFillImage = graphicFillStyle?.getImage();
×
1395
      graphicFillImage?.load(); // Needed for Icon type images with a remote src
×
1396
      // We can only work with the image once it's loaded
1397
      if (graphicFillImage?.getImageState() !== OlImageState.LOADED) {
×
1398
        return null;
×
1399
      }
1400
    } else if (isMarkSymbolizer(graphicFill)) {
2!
1401
      graphicFillStyle = this.getOlPointSymbolizerFromMarkSymbolizer(graphicFill);
2✔
1402
    } else {
1403
      return null;
×
1404
    }
1405

1406
    // We need to clone the style and image since we'll be changing the scale below (hack)
1407
    const graphicFillStyleCloned = graphicFillStyle.clone();
2✔
1408
    const imageCloned = graphicFillStyleCloned.getImage();
2✔
1409

1410
    // Temporary canvas.
1411
    // TODO: Can/should we reuse an pre-existing one for efficiency?
1412
    const tmpCanvas: HTMLCanvasElement = document.createElement('canvas');
2✔
1413
    const tmpContext = tmpCanvas.getContext('2d') as CanvasRenderingContext2D;
2✔
1414

1415
    // Hack to make scaling work for Icons.
1416
    // TODO: find a better way than this.
1417
    const scale = imageCloned.getScale() || 1;
2!
1418
    const pixelRatio = scale;
2✔
1419
    imageCloned.setScale(1);
2✔
1420

1421
    const size: [number, number] = imageCloned.getSize();
2✔
1422

1423
    // Create the context where we'll be drawing the style on
1424
    const vectorContext = toContext(tmpContext, {
2✔
1425
      pixelRatio,
1426
      size
1427
    });
1428

1429
    // Draw the graphic
1430
    vectorContext.setStyle(graphicFillStyleCloned);
2✔
1431
    const pointCoords = size.map(item  => item / 2);
4✔
1432
    vectorContext.drawGeometry(new OlGeomPoint(pointCoords));
2✔
1433

1434
    // Create the actual pattern and return style
1435
    return tmpContext.createPattern(tmpCanvas, 'repeat');
2✔
1436
  }
1437

1438
  /**
1439
   * Get the OL StyleFunction object from an GeoStyler-Style TextSymbolizer.
1440
   *
1441
   * @param {TextSymbolizer} textSymbolizer A GeoStyler-Style TextSymbolizer.
1442
   * @return {object} The OL StyleFunction
1443
   */
1444
  getOlTextSymbolizerFromTextSymbolizer(
1445
    symbolizer: TextSymbolizer,
1446
    feat?: OlFeature
1447
  ): OlStyle | OlStyleText | OlStyleFunction {
1448
    for (const key of Object.keys(symbolizer)) {
18✔
1449
      if (isGeoStylerFunction(symbolizer[key as keyof TextSymbolizer])) {
152!
1450
        (symbolizer as any)[key] = OlStyleUtil.evaluateFunction((symbolizer as any)[key], feat);
×
1451
      }
1452
    }
1453
    const color = symbolizer.color as string;
18✔
1454
    let placement = symbolizer.placement;
18✔
1455
    if (!placement) {
18✔
1456
      // When setting placement it must not be undefined.
1457
      // So we set it to the OL default value.
1458
      placement = 'point';
10✔
1459
    }
1460
    if (placement === 'line-center') {
18✔
1461
      // line-center not supported by OL.
1462
      // So we use the closest supported value.
1463
      placement = 'line';
2✔
1464
    }
1465
    const opacity = symbolizer.opacity as number;
18✔
1466
    const fColor = color && Number.isFinite(opacity)
18!
1467
      ? OlStyleUtil.getRgbaColor(color, opacity)
1468
      : color;
1469

1470
    const haloColor = symbolizer.haloColor as string;
18✔
1471
    const haloWidth = symbolizer.haloWidth as number;
18✔
1472
    const sColor = haloColor && Number.isFinite(opacity)
18!
1473
      ? OlStyleUtil.getRgbaColor(haloColor, opacity)
1474
      : haloColor;
1475
    const baseProps: OlStyleTextOptions = {
18✔
1476
      font: OlStyleUtil.getTextFont(symbolizer),
1477
      fill: new this.OlStyleFillConstructor({
1478
        color: fColor
1479
      }),
1480
      stroke: new this.OlStyleStrokeConstructor({
1481
        color: sColor,
1482
        width: haloWidth ? haloWidth : 0 as number
18✔
1483
      }),
1484
      overflow: symbolizer.allowOverlap as boolean,
1485
      offsetX: (symbolizer.offset ? symbolizer.offset[0] : 0) as number,
18✔
1486
      offsetY: (symbolizer.offset ? symbolizer.offset[1] : 0) as number,
18✔
1487
      rotation: typeof(symbolizer.rotate) === 'number' ? symbolizer.rotate * Math.PI / 180 : undefined,
18✔
1488
      placement: placement as 'line' | 'point'
1489
      // TODO check why props match
1490
      // textAlign: symbolizer.pitchAlignment,
1491
      // textBaseline: symbolizer.anchor
1492
    };
1493

1494
    // check if TextSymbolizer.label contains a placeholder
1495
    const prefix = '\\{\\{';
18✔
1496
    const suffix = '\\}\\}';
18✔
1497
    const regExp = new RegExp(prefix + '.*?' + suffix, 'g');
18✔
1498
    let regExpRes;
1499
    if (!isGeoStylerStringFunction(symbolizer.label)) {
18!
1500
      regExpRes = symbolizer.label ? symbolizer.label.match(regExp) : null;
18!
1501
    }
1502
    if (regExpRes) {
18✔
1503
      // if it contains a placeholder
1504
      // return olStyleFunction
1505
      const olPointStyledLabelFn = (feature: any) => {
4✔
1506

1507
        const text = new this.OlStyleTextConstructor({
4✔
1508
          text: OlStyleUtil.resolveAttributeTemplate(feature, symbolizer.label as string, ''),
1509
          ...baseProps
1510
        });
1511

1512
        const style = new this.OlStyleConstructor({
4✔
1513
          text: text
1514
        });
1515

1516
        return style;
4✔
1517
      };
1518
      return olPointStyledLabelFn;
4✔
1519
    } else {
1520
      // if TextSymbolizer does not contain a placeholder
1521
      // return OlStyle
1522
      return new this.OlStyleConstructor({
14✔
1523
        text: new this.OlStyleTextConstructor({
1524
          text: symbolizer.label as string,
1525
          ...baseProps
1526
        })
1527
      });
1528
    }
1529
  }
1530

1531
}
1532

1533
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