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

geostyler / geostyler-sld-parser / 12764151114

14 Jan 2025 08:52AM UTC coverage: 90.803% (+0.2%) from 90.597%
12764151114

push

github

web-flow
fix: linting (#976)

554 of 654 branches covered (84.71%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

43 existing lines in 1 file now uncovered.

996 of 1053 relevant lines covered (94.59%)

448.92 hits per line

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

90.01
/src/SldStyleParser.ts
1
import {
2
  Channel,
3
  ChannelSelection,
4
  ColorMap,
5
  ColorMapEntry,
6
  ColorMapType,
7
  CombinationOperator,
8
  ComparisonFilter,
9
  ComparisonOperator,
10
  ContrastEnhancement,
11
  Expression,
12
  FillSymbolizer,
13
  Filter,
14
  IconSymbolizer,
15
  LineSymbolizer,
16
  MarkSymbolizer,
17
  PointSymbolizer,
18
  RangeFilter,
19
  RasterSymbolizer,
20
  ReadStyleResult,
21
  Rule,
22
  ScaleDenominator,
23
  Style,
24
  StyleParser,
25
  Symbolizer,
26
  TextSymbolizer,
27
  UnsupportedProperties,
28
  WellKnownName,
29
  WriteStyleResult
30
} from 'geostyler-style/dist/style';
31
import { GeoStylerFunction } from 'geostyler-style/dist/functions';
32
import {
33
  X2jOptions,
34
  XMLBuilder,
35
  XmlBuilderOptions,
36
  XMLParser
37
} from 'fast-xml-parser';
38

39
import { merge } from 'lodash';
40

41
import {
42
  geoStylerFunctionToSldFunction,
43
  get,
44
  getAttribute,
45
  getChildren,
46
  getParameterValue,
47
  getVendorOptionValue,
48
  isSymbolizer,
49
  keysByValue,
50
  numberExpression
51
} from './Util/SldUtil';
52
import {
53
  isCombinationFilter,
54
  isComparisonFilter,
55
  isGeoStylerFunction,
56
  isGeoStylerNumberFunction,
57
  isNegationFilter
58
} from 'geostyler-style/dist/typeguards';
59

60
const SLD_VERSIONS = ['1.0.0', '1.1.0'] as const;
8✔
61

62
export type SldVersion = (typeof SLD_VERSIONS)[number];
63

64
export type ParserOptions = Omit<X2jOptions,
65
'ignoreDeclaration' |
66
'removeNSPrefix' |
67
'ignoreAttributes' |
68
'preserveOrder' |
69
'trimValues'
70
>;
71

72
export type BuilderOptions = Omit<XmlBuilderOptions,
73
'cdataPropName' |
74
'ignoreAttributes' |
75
'suppressEmptyNode' |
76
'preserveOrder'
77
>;
78

79
export type ConstructorParams = {
80
  numericFilterFields?: string[];
81
  boolFilterFields?: string[];
82
  /* optional for reading style (it will be guessed from sld style) and mandatory for writing */
83
  sldVersion?: SldVersion;
84
  withGeoServerVendorOption?: boolean;
85
  symbolizerUnits?: string;
86
  parserOptions?: ParserOptions;
87
  builderOptions?: XmlBuilderOptions;
88
  translations?: SldStyleParserTranslations;
89
  locale?: string;
90
};
91

92
const WELLKNOWNNAME_TTF_REGEXP = /^ttf:\/\/(.+)#(.+)$/;
8✔
93

94
const COMPARISON_MAP = {
8✔
95
  PropertyIsEqualTo: '==',
96
  PropertyIsNotEqualTo: '!=',
97
  PropertyIsLike: '*=',
98
  PropertyIsLessThan: '<',
99
  PropertyIsLessThanOrEqualTo: '<=',
100
  PropertyIsGreaterThan: '>',
101
  PropertyIsGreaterThanOrEqualTo: '>=',
102
  PropertyIsNull: '==',
103
  PropertyIsBetween: '<=x<='
104
};
105

106
type ComparisonType = keyof typeof COMPARISON_MAP;
107

108
const NEGATION_OPERATOR_MAP = {
8✔
109
  Not: '!'
110
};
111

112
const COMBINATION_MAP = {
8✔
113
  And: '&&',
114
  Or: '||',
115
  PropertyIsBetween: '&&'
116
};
117

118
type CombinationType = keyof typeof COMBINATION_MAP;
119

120
export type SldStyleParserTranslationKeys = {
121
  marksymbolizerParseFailedUnknownWellknownName?: (params: {wellKnownName: string}) => string;
122
  noFilterDetected?: string;
123
  symbolizerKindParseFailed?: (params: {sldSymbolizerName: string}) => string;
124
  colorMapEntriesParseFailedColorUndefined?: string;
125
  contrastEnhancParseFailedHistoAndNormalizeMutuallyExclusive?: string;
126
  channelSelectionParseFailedRGBAndGrayscaleMutuallyExclusive?: string;
127
  channelSelectionParseFailedRGBChannelsUndefined?: string;
128
};
129

130
export type SldStyleParserTranslations = Record<string, SldStyleParserTranslationKeys>;
131

132
export const defaultTranslations: SldStyleParserTranslations = {
8✔
133
  en: {
134
    marksymbolizerParseFailedUnknownWellknownName: ({wellKnownName}) =>
135
      `MarkSymbolizer cannot be parsed. WellKnownName ${wellKnownName} is not supported.`,
2✔
136
    noFilterDetected: 'No Filter detected.',
137
    symbolizerKindParseFailed: ({sldSymbolizerName}) =>
138
      `Failed to parse SymbolizerKind ${sldSymbolizerName} from SldRule.`,
×
139
    colorMapEntriesParseFailedColorUndefined: 'Cannot parse ColorMapEntries. color is undefined.',
140
    contrastEnhancParseFailedHistoAndNormalizeMutuallyExclusive:
141
      'Cannot parse ContrastEnhancement. Histogram and Normalize are mutually exclusive.',
142
    channelSelectionParseFailedRGBAndGrayscaleMutuallyExclusive:
143
      'Cannot parse ChannelSelection. RGB and Grayscale are mutually exclusive.',
144
    channelSelectionParseFailedRGBChannelsUndefined:
145
      'Cannot parse ChannelSelection. Red, Green and Blue channels must be defined.'
146
  },
147
  de: {},
148
  fr: {
149
    marksymbolizerParseFailedUnknownWellknownName: ({wellKnownName}) =>
150
      `Échec de lecture du symbole de type MarkSymbolizer. Le WellKnownName ${wellKnownName} n'est pas supporté.`,
2✔
151
    noFilterDetected: 'Aucun filtre détecté.',
152
    symbolizerKindParseFailed: ({sldSymbolizerName}) =>
153
      `Échec de lecture du type de symbole ${sldSymbolizerName} à partir de SldRule.`,
×
154
    colorMapEntriesParseFailedColorUndefined: 'Lecture de ColorMapEntries échoué. color n\'est pas défini.',
155
    contrastEnhancParseFailedHistoAndNormalizeMutuallyExclusive:
156
      'Échec de lecture des propriétés de contraste ContrastEnhancement échoué. '
157
      +'Histogram et Normalize sont mutuellement exclusifs.',
158
    channelSelectionParseFailedRGBAndGrayscaleMutuallyExclusive:
159
      'Échec de lecture de la sélection de canaux ChannelSelection. '
160
      +'RGB et Grayscale sont mutuellement exclusifs.',
161
    channelSelectionParseFailedRGBChannelsUndefined:
162
      'Échec de lecture de la sélection de canaux ChannelSelection. '
163
      +'Les canaux Rouge, Vert et Bleu doivent être définis.',
164

165
  },
166
} as const;
167

168
/**
169
 * @returns true if the provided value is null or undefined. Returns false otherwise.
170
 */
171
const isNil = (val: unknown): boolean => val === undefined || val === null;
5,438✔
172

173
/**
174
 * This parser can be used with the GeoStyler.
175
 * It implements the geostyler-style StyleParser interface.
176
 *
177
 * @class SldStyleParser
178
 * @implements StyleParser
179
 */
180
export class SldStyleParser implements StyleParser<string> {
181

182
  /**
183
   * The name of the SLD Style Parser.
184
   */
185
  public static title = 'SLD Style Parser';
8✔
186

187
  title = 'SLD Style Parser';
534✔
188

189
  unsupportedProperties: UnsupportedProperties = {
534✔
190
    Symbolizer: {
191
      MarkSymbolizer: {
192
        avoidEdges: 'none',
193
        blur: 'none',
194
        offset: {
195
          support: 'partial',
196
          info: 'Only supported for SLD Version 1.1.0'
197
        },
198
        offsetAnchor: 'none',
199
        pitchAlignment: 'none',
200
        pitchScale: 'none',
201
        visibility: 'none'
202
      },
203
      FillSymbolizer: {
204
        antialias: 'none',
205
        opacity: {
206
          support: 'none',
207
          info: 'General opacity is not supported. Use fillOpacity and strokeOpacity instead.'
208
        },
209
        visibility: 'none'
210
      },
211
      IconSymbolizer: {
212
        allowOverlap: 'none',
213
        anchor: 'none',
214
        avoidEdges: 'none',
215
        color: 'none',
216
        haloBlur: 'none',
217
        haloColor: 'none',
218
        haloOpacity: 'none',
219
        haloWidth: 'none',
220
        keepUpright: 'none',
221
        offset: {
222
          support: 'partial',
223
          info: 'Only supported for SLD Version 1.1.0'
224
        },
225
        offsetAnchor: 'none',
226
        optional: 'none',
227
        padding: 'none',
228
        pitchAlignment: 'none',
229
        rotationAlignment: 'none',
230
        textFit: 'none',
231
        image: {
232
          support: 'partial',
233
          info: 'Sprites are not supported'
234
        },
235
        textFitPadding: 'none',
236
        visibility: 'none'
237
      },
238
      LineSymbolizer: {
239
        blur: 'none',
240
        gapWidth: 'none',
241
        gradient: 'none',
242
        miterLimit: 'none',
243
        roundLimit: 'none',
244
        spacing: 'none',
245
        visibility: 'none'
246
      },
247
      RasterSymbolizer: {
248
        brightnessMax: 'none',
249
        brightnessMin: 'none',
250
        contrast: 'none',
251
        fadeDuration: 'none',
252
        hueRotate: 'none',
253
        resampling: 'none',
254
        saturation: 'none',
255
        visibility: 'none'
256
      },
257
      TextSymbolizer: {
258
        placement: {
259
          support: 'partial',
260
          info: 'Only "line" and "point" are currently supported'
261
        }
262
      }
263
    }
264
  };
265

266
  translations: SldStyleParserTranslations = defaultTranslations;
534✔
267

268
  locale: string = 'en';
534✔
269

270
  constructor(opts?: ConstructorParams) {
271
    this.parser = new XMLParser({
534✔
272
      ...opts?.parserOptions,
273
      // Fixed attributes
274
      ignoreDeclaration: true,
275
      removeNSPrefix: true,
276
      ignoreAttributes: false,
277
      preserveOrder: true,
278
      trimValues: true
279
    });
280

281
    this.builder = new XMLBuilder({
534✔
282
      ...opts?.builderOptions,
283
      // Fixed attributes
284
      cdataPropName: '#cdata',
285
      ignoreAttributes: false,
286
      suppressEmptyNode: true,
287
      preserveOrder: true
288
    });
289

290
    if (opts?.sldVersion) {
534✔
291
      this.sldVersion = opts?.sldVersion;
216✔
292
    }
293

294
    if (opts?.withGeoServerVendorOption !== undefined) {
534✔
295
      this.withGeoServerVendorOption = opts.withGeoServerVendorOption;
96✔
296
    }
297

298
    if (opts?.locale) {
534✔
299
      this.locale = opts.locale;
4✔
300
    }
301

302
    if (opts?.translations){
534✔
303
      this.translations = merge(this.translations, opts.translations);
2✔
304
    }
305

306
    Object.assign(this, opts);
534✔
307
  }
308

309
  translate(key: keyof SldStyleParserTranslationKeys, params?: any): string {
310
    const trans = this.translations?.[this.locale]?.[key] ?? key;
6!
311

312
    if (typeof trans === 'function') {
6!
313
      return trans(params);
6✔
314
    }
315

316
    return trans;
×
317
  }
318

319
  private _parser: XMLParser;
320
  get parser(): XMLParser {
321
    return this._parser;
412✔
322
  }
323
  set parser(parser: XMLParser) {
324
    this._parser = parser;
534✔
325
  }
326

327
  private _builder: XMLBuilder;
328
  get builder(): XMLBuilder {
329
    return this._builder;
208✔
330
  }
331
  set builder(builder: XMLBuilder) {
332
    this._builder = builder;
534✔
333
  }
334

335
  /**
336
   * Array of field / property names in a filter, which are casted to numerics
337
   * while parsing a SLD.
338
   */
339
  private _numericFilterFields: string[] = [];
534✔
340
  /**
341
   * Getter for _numericFilterFields
342
   * @return The numericFilterFields
343
   */
344
  get numericFilterFields(): string[] {
345
    return this._numericFilterFields;
×
346
  }
347
  /**
348
   * Setter for _numericFilterFields
349
   * @param numericFilterFields The numericFilterFields to set
350
   */
351
  set numericFilterFields(numericFilterFields: string[]) {
352
    this._numericFilterFields = numericFilterFields;
4✔
353
  }
354

355
  /**
356
   * Array of field / property names in a filter, which are casted to boolean
357
   * while parsing a SLD.
358
   */
359
  private _boolFilterFields: string[] = [];
534✔
360
  /**
361
   * Getter for _boolFilterFields
362
   * @return The boolFilterFields
363
   */
364
  get boolFilterFields(): string[] {
365
    return this._boolFilterFields;
×
366
  }
367
  /**
368
   * Setter for _boolFilterFields
369
   * @param boolFilterFields The boolFilterFields to set
370
   */
371
  set boolFilterFields(boolFilterFields: string[]) {
372
    this._boolFilterFields = boolFilterFields;
4✔
373
  }
374

375
  /**
376
   * String indicating the SLD version to use. 1.1.0 will make use of
377
   * Symbology Encoding.
378
   */
379
  private _sldVersion: SldVersion = '1.0.0';
534✔
380

381
  /**
382
   * Getter for _sldVersion
383
   * @return
384
   */
385
  get sldVersion(): SldVersion {
386
    return this._sldVersion;
5,714✔
387
  }
388

389
  /**
390
   * Setter for _sldVersion
391
   * @param sldVersion The _sldVersion value to set
392
   */
393
  set sldVersion(sldVersion: SldVersion) {
394
    this._sldVersion = sldVersion;
432✔
395
  }
396

397
  /**
398
   * Indicates whether additional GeoServer vendorOption should be included in
399
   * sld write/parse operations. Set to `false` by default.
400
   */
401
  private _withGeoServerVendorOption = false;
534✔
402

403
  /**
404
   * Getter for _withGeoServerVendorOption
405
   */
406
  get withGeoServerVendorOption(): boolean {
407
    return this._withGeoServerVendorOption;
138✔
408
  }
409

410
  /**
411
   * Setter for _withGeoServerVendorOption
412
   */
413
  set withGeoServerVendorOption(withVendorOption: boolean) {
414
    this._withGeoServerVendorOption = withVendorOption;
192✔
415
  }
416

417

418
  /**
419
   * String indicating the SLD version used in reading mode
420
   */
421
  private _readingSldVersion: SldVersion = '1.0.0';
534✔
422

423
  /**
424
     * Getter for _readingSldVersion
425
     * @return
426
     */
427
  get readingSldVersion(): SldVersion {
428
    return this._readingSldVersion;
3,466✔
429
  }
430

431
  /**
432
   * Setter for _readingSldVersion
433
   * @param sldVersion The _readingSldVersion value to set
434
   */
435
  set readingSldVersion(sldVersion: SldVersion) {
436
    this._readingSldVersion = sldVersion;
×
437
  }
438

439
  /**
440
   * Used to add a `uom` attribute to the symbolizer tag. Can be one of
441
   * `metre`, `foot` or `pixel`. Defaults to pixel.
442
   */
443
  private _symbolizerUnits: string = 'pixel';
534✔
444

445
  /**
446
   * Getter for _symbolizerUnits
447
   * @return {string}
448
   */
449
  get symbolizerUnits(): string {
450
    return this._symbolizerUnits;
×
451
  }
452

453
  /**
454
   * Setter for _symbolizerUnits
455
   * @param {string} symbolizerUnits The _symbolizerUnits value to set
456
   */
457
  set symbolizerUnits(symbolizerUnits: string) {
458
    this._symbolizerUnits = symbolizerUnits;
×
459
  }
460

461
  /**
462
   * The readStyle implementation of the geostyler-style StyleParser interface.
463
   * It reads a SLD as a string and returns a Promise.
464
   * The Promise itself resolves with an object containing the geostyler-style.
465
   *
466
   * @param sldString A SLD as a string.
467
   * @return The Promise resolving with an object containing the geostyler-style.
468
   */
469
  readStyle(sldString: string): Promise<ReadStyleResult> {
470
    return new Promise<ReadStyleResult>((resolve) => {
412✔
471
      try {
412✔
472
        const sldObject = this.parser.parse(sldString);
412✔
473

474
        const version = getAttribute(sldObject[0], 'version');
412✔
475
        if (!SLD_VERSIONS.includes(version)) {
412!
476
          throw new Error(`SLD version must be ${SLD_VERSIONS.toString()}`);
×
477
        }
478
        this._readingSldVersion = version;
412✔
479

480
        const geoStylerStyle: Style = this.sldObjectToGeoStylerStyle(sldObject);
412✔
481
        resolve({
406✔
482
          output: geoStylerStyle
483
        });
484
      } catch (error) {
485
        resolve({
6✔
486
          errors: [error]
487
        });
488
      }
489
    });
490
  }
491

492
  /**
493
   * Get the geostyler-style from a SLD Object (created with fast-xml-parser).
494
   *
495
   * @param sldObject The SLD object representation (created with fast-xml-parser)
496
   * @return The geostyler-style
497
   */
498
  sldObjectToGeoStylerStyle(sldObject: any): Style {
499
    const rules = this.getRulesFromSldObject(sldObject);
412✔
500
    const name = this.getStyleNameFromSldObject(sldObject);
406✔
501
    return {
406✔
502
      name,
503
      rules
504
    };
505
  }
506

507
  /**
508
   * Get the geostyler-style rules from a SLD Object (created with fast-xml-parser).
509
   *
510
   * @param sldObject The SLD object representation (created with fast-xml-parser)
511
   * @return The geostyler-style rules
512
   */
513
  getRulesFromSldObject(sldObject: any): Rule[] {
514
    const layers = getChildren(sldObject[0].StyledLayerDescriptor, 'NamedLayer');
412✔
515
    const rules: Rule[] = [];
412✔
516
    layers.forEach(({ NamedLayer: layer }: any) => {
412✔
517
      getChildren(layer, 'UserStyle').forEach(({ UserStyle: userStyle }: any) => {
412✔
518
        getChildren(userStyle, 'FeatureTypeStyle').forEach(({ FeatureTypeStyle: featureTypeStyle }: any) => {
412✔
519
          getChildren(featureTypeStyle, 'Rule').forEach(({ Rule: sldRule }: any) => {
414✔
520
            const filter: Filter | undefined = this.getFilterFromRule(sldRule);
506✔
521
            const scaleDenominator: ScaleDenominator | undefined = this.getScaleDenominatorFromRule(sldRule);
506✔
522
            const symbolizers: Symbolizer[] = this.getSymbolizersFromRule(sldRule);
506✔
523
            const ruleTitle = get(sldRule, 'Title.#text');
500✔
524
            const ruleName = get(sldRule, 'Name.#text');
500✔
525
            const name = ruleTitle !== undefined
500✔
526
              ? ruleTitle
527
              : (ruleName !== undefined ? ruleName : '');
426✔
528
            const rule: Rule = <Rule>{
500✔
529
              name
530
            };
531
            if (filter) {
500✔
532
              rule.filter = filter;
124✔
533
            }
534
            if (scaleDenominator) {
500✔
535
              rule.scaleDenominator = scaleDenominator;
78✔
536
            }
537
            if (symbolizers) {
500!
538
              rule.symbolizers = symbolizers;
500✔
539
            }
540
            rules.push(rule);
500✔
541
          });
542
        });
543
      });
544
    });
545
    return rules;
406✔
546
  }
547

548
  /**
549
   * Get the name for the Style from the SLD Object. Returns the Title of the UserStyle
550
   * if defined or the Name of the NamedLayer if defined or an empty string.
551
   *
552
   * @param sldObject The SLD object representation (created with fast-xml-parser)
553
   * @return The name to be used for the GeoStyler Style Style
554
   */
555
  getStyleNameFromSldObject(sldObject: any): string {
556
    const userStyleTitle = get(sldObject, 'StyledLayerDescriptor.NamedLayer[0].UserStyle.Name.#text');
406✔
557
    const namedLayerName = get(sldObject, 'StyledLayerDescriptor.NamedLayer.Name.#text');
406✔
558
    return userStyleTitle ? userStyleTitle
406✔
559
      : namedLayerName ? namedLayerName : '';
90!
560
  }
561

562
  /**
563
   * Get the geostyler-style Filter from a SLD Rule.
564
   *
565
   * Currently only supports one Filter per Rule.
566
   *
567
   * @param sldRule The SLD Rule
568
   * @return The geostyler-style Filter
569
   */
570
  getFilterFromRule(sldRule: any[]): Filter | undefined {
571
    const sldFilter = get(sldRule, 'Filter');
506✔
572
    if (!sldFilter || sldFilter.length === 0) {
506✔
573
      return;
382✔
574
    }
575
    const operator = Object.keys(sldFilter[0])?.[0];
124✔
576
    if (!operator) {
124!
577
      return;
×
578
    }
579
    const filter = this.getFilterFromOperatorAndComparison(operator as ComparisonType, sldFilter);
124✔
580
    return filter;
124✔
581
  }
582

583
  /**
584
   * Get the geostyler-style ScaleDenominator from a SLD Rule.
585
   *
586
   * @param sldRule The SLD Rule
587
   * @return The geostyler-style ScaleDenominator
588
   */
589
  getScaleDenominatorFromRule(sldRule: any[]): ScaleDenominator | undefined {
590
    const scaleDenominator: ScaleDenominator = <ScaleDenominator>{};
506✔
591
    const min = get(sldRule, 'MinScaleDenominator.#text');
506✔
592
    if (min) {
506✔
593
      scaleDenominator.min = Number(min);
36✔
594
    }
595
    const max = get(sldRule, 'MaxScaleDenominator.#text');
506✔
596
    if (max) {
506✔
597
      scaleDenominator.max = Number(max);
74✔
598
    }
599

600
    return (Number.isFinite(scaleDenominator.min) || Number.isFinite(scaleDenominator.max))
506✔
601
      ? scaleDenominator
602
      : undefined;
603
  }
604

605
  /**
606
   * Get the geostyler-style Symbolizers from a SLD Rule.
607
   *
608
   * @param sldRule The SLD Rule
609
   * @return The geostyler-style Symbolizer array
610
   */
611
  getSymbolizersFromRule(sldRule: any[]): Symbolizer[] {
612
    const symbolizers: Symbolizer[] = sldRule
506✔
613
      .filter(isSymbolizer)
614
      .map((sldSymbolizer: any) => {
615
        const sldSymbolizerName: string = Object.keys(sldSymbolizer)[0];
526✔
616
        switch (sldSymbolizerName) {
526!
617
          case 'PointSymbolizer':
618
            return this.getPointSymbolizerFromSldSymbolizer(sldSymbolizer.PointSymbolizer);
186✔
619
          case 'LineSymbolizer':
620
            return this.getLineSymbolizerFromSldSymbolizer(sldSymbolizer.LineSymbolizer);
94✔
621
          case 'TextSymbolizer':
622
            return this.getTextSymbolizerFromSldSymbolizer(sldSymbolizer.TextSymbolizer);
72✔
623
          case 'PolygonSymbolizer':
624
            return this.getFillSymbolizerFromSldSymbolizer(sldSymbolizer.PolygonSymbolizer);
142✔
625
          case 'RasterSymbolizer':
626
            return this.getRasterSymbolizerFromSldSymbolizer(sldSymbolizer.RasterSymbolizer);
32✔
627
          default:
628
            throw new Error(this.translate('symbolizerKindParseFailed', { sldSymbolizerName: sldSymbolizerName }));
×
629
        }
630
      });
631

632
    return symbolizers;
500✔
633
  }
634

635
  /**
636
   * Creates a geostyler-style Filter from a given operator name and the js
637
   * SLD object representation (created with fast-xml-parser) of the SLD Filter.
638
   *
639
   * @param sldOperatorName The Name of the SLD Filter Operator
640
   * @param sldFilter The SLD Filter
641
   * @return The geostyler-style Filter
642
   */
643
  getFilterFromOperatorAndComparison(
644
    sldOperatorName: ComparisonType | 'Function',
645
    sldFilter: any
646
  ): Filter {
647
    let filter: Filter;
648

649
    if (sldOperatorName === 'Function') {
516✔
650
      const functionName = Array.isArray(sldFilter) ? sldFilter[0][':@']['@_name'] : sldFilter[':@']['@_name'];
32✔
651
      const tempFunctionName = functionName.charAt(0).toUpperCase() + functionName.slice(1);
32✔
652
      sldOperatorName = `PropertyIs${tempFunctionName}` as ComparisonType;
32✔
653
    }
654

655
    // we have to first check for PropertyIsBetween,
656
    // since it is also a comparisonOperator. But it
657
    // needs to be treated differently.
658
    if (sldOperatorName === 'PropertyIsBetween') {
516✔
659
      // TODO: PropertyIsBetween spec allows more than just a PropertyName as its first argument.
660
      const propertyName = get(sldFilter, 'PropertyIsBetween.PropertyName.#text');
16✔
661
      const lower = Number(get(sldFilter, 'PropertyIsBetween.LowerBoundary.Literal.#text'));
16✔
662
      const upper = Number(get(sldFilter, 'PropertyIsBetween.UpperBoundary.Literal.#text'));
16✔
663

664
      filter = ['<=x<=', propertyName, lower, upper];
16✔
665
    } else if (Object.keys(COMPARISON_MAP).includes(sldOperatorName)) {
500✔
666
      const comparisonOperator: ComparisonOperator = COMPARISON_MAP[sldOperatorName] as ComparisonOperator;
360✔
667
      const filterIsFunction = !!get(sldFilter, 'Function');
360✔
668
      let args: any[] = [];
360✔
669

670
      const children = get(sldFilter, filterIsFunction ? 'Function' : sldOperatorName) || [];
360!
671
      const childrenToArgs = function (child: any, index: number) {
360✔
672
        const propName = get([child], 'PropertyName.#text');
704✔
673
        if (propName !== undefined) {
704✔
674
          const isSingleArgOperator = children.length === 1;
360✔
675
          // Return property name for the first argument in case second argument is literal
676
          // or isSingleArgOperator eg (PropertyIsNull)
677
          if (isSingleArgOperator || (index === 0 && get([children[1]], 'PropertyName.#text') === undefined)) {
360✔
678
            return propName;
280✔
679
          }
680
          // ..otherwise + (second argument) return as property function
681
          return {
80✔
682
            name: 'property',
683
            args: [propName]
684
          };
685
        } else {
686
          return get([child], '#text');
344✔
687
        }
688
      };
689
      args = children.map(childrenToArgs);
360✔
690

691
      if (sldOperatorName === 'PropertyIsNull') {
360✔
692
        args[1] = null;
16✔
693
      }
694

695
      filter = [
360✔
696
        comparisonOperator,
697
        ...args
698
      ] as ComparisonFilter;
699

700
    } else if (Object.keys(COMBINATION_MAP).includes(sldOperatorName)) {
140✔
701
      const combinationOperator: CombinationOperator = COMBINATION_MAP[
124✔
702
        sldOperatorName as CombinationType] as CombinationOperator;
703
      const filters: Filter[] = get(sldFilter, sldOperatorName)?.map((op: any) => {
124✔
704
        const operatorName = Object.keys(op)?.[0];
376✔
705
        return this.getFilterFromOperatorAndComparison(operatorName as any, op);
376✔
706
      });
707
      filter = [
124✔
708
        combinationOperator,
709
        ...filters
710
      ];
711
    } else if (Object.keys(NEGATION_OPERATOR_MAP).includes(sldOperatorName)) {
16!
712
      const negationOperator = NEGATION_OPERATOR_MAP[sldOperatorName as 'Not'];
16✔
713
      const negatedOperator = Object.keys(sldFilter[sldOperatorName][0])[0];
16✔
714
      const negatedComparison = sldFilter[sldOperatorName][0];
16✔
715
      const negatedFilter: Filter = this.getFilterFromOperatorAndComparison(
16✔
716
        negatedOperator as any,
717
        negatedComparison
718
      );
719
      filter = [
16✔
720
        negationOperator as any,
721
        negatedFilter
722
      ];
723
    } else {
UNCOV
724
      throw new Error(this.translate('noFilterDetected'));
×
725
    }
726
    return filter;
516✔
727
  }
728

729
  /**
730
   * Get the geostyler-style PointSymbolizer from a SLD Symbolizer.
731
   *
732
   * The opacity of the Symbolizer is taken from the <Graphic>.
733
   *
734
   * @param sldSymbolizer The SLD Symbolizer
735
   * @return The geostyler-style PointSymbolizer
736
   */
737
  getPointSymbolizerFromSldSymbolizer(sldSymbolizer: any): PointSymbolizer {
738
    let pointSymbolizer: PointSymbolizer;
739
    const wellKnownName: string = get(sldSymbolizer, 'Graphic.Mark.WellKnownName.#text');
260✔
740
    const externalGraphic: any = get(sldSymbolizer, 'Graphic.ExternalGraphic');
260✔
741
    if (externalGraphic) {
260✔
742
      pointSymbolizer = this.getIconSymbolizerFromSldSymbolizer(sldSymbolizer);
56✔
743
    } else {
744
      // geoserver does not set a wellKnownName for square explicitly since it is the default value.
745
      // Therefore, we have to set the wellKnownName to square if no wellKownName is given.
746
      if (!wellKnownName) {
204!
747
        // TODO: Fix this. Idealy without lodash
748
        // _set(sldSymbolizer, 'Graphic[0].Mark[0].WellKnownName[0]._', 'square');
749
      }
750
      pointSymbolizer = this.getMarkSymbolizerFromSldSymbolizer(sldSymbolizer);
204✔
751
    }
752
    return pointSymbolizer;
254✔
753
  }
754

755
  /**
756
   * Get the geostyler-style LineSymbolizer from a SLD Symbolizer.
757
   *
758
   * Currently only the CssParameters are available.
759
   *
760
   * @param sldSymbolizer The SLD Symbolizer
761
   * @return The geostyler-style LineSymbolizer
762
   */
763
  getLineSymbolizerFromSldSymbolizer(sldSymbolizer: any): LineSymbolizer {
764
    const lineSymbolizer: LineSymbolizer = {
94✔
765
      kind: 'Line'
766
    };
767
    const strokeEl = get(sldSymbolizer, 'Stroke', this.readingSldVersion);
94✔
768
    const color = getParameterValue(strokeEl, 'stroke', this.readingSldVersion);
94✔
769
    const width = getParameterValue(strokeEl, 'stroke-width', this.readingSldVersion);
94✔
770
    const opacity = getParameterValue(strokeEl, 'stroke-opacity', this.readingSldVersion);
94✔
771
    const lineJoin = getParameterValue(strokeEl, 'stroke-linejoin', this.readingSldVersion);
94✔
772
    const lineCap = getParameterValue(strokeEl, 'stroke-linecap', this.readingSldVersion);
94✔
773
    const dashArray = getParameterValue(strokeEl, 'stroke-dasharray', this.readingSldVersion);
94✔
774
    const dashOffset = getParameterValue(strokeEl, 'stroke-dashoffset', this.readingSldVersion);
94✔
775

776
    if (!isNil(color)) {
94✔
777
      lineSymbolizer.color = color;
86✔
778
    }
779
    if (!isNil(width)) {
94✔
780
      lineSymbolizer.width = numberExpression(width);
84✔
781
    }
782
    if (!isNil(opacity)) {
94✔
783
      lineSymbolizer.opacity = numberExpression(opacity);
2✔
784
    }
785
    if (!isNil(lineJoin)) {
94✔
786
      // geostyler-style and ol use 'miter' whereas sld uses 'mitre'
787
      if (lineJoin === 'mitre') {
48✔
788
        lineSymbolizer.join = 'miter';
24✔
789
      } else {
790
        lineSymbolizer.join = lineJoin as 'bevel' | 'miter' | 'round' | undefined;
24✔
791
      }
792
    }
793
    if (!isNil(lineCap)) {
94✔
794
      lineSymbolizer.cap = lineCap as 'round' | 'butt' | 'square' | undefined;
48✔
795
    }
796

797
    if (!isNil(dashArray)) {
94✔
798
      const dashStringAsArray = dashArray.split(' ').map(numberExpression);
56✔
799
      lineSymbolizer.dasharray = dashStringAsArray;
56✔
800
    }
801
    if (!isNil(dashOffset)) {
94✔
802
      lineSymbolizer.dashOffset = numberExpression(dashOffset);
8✔
803
    }
804

805
    const graphicStroke = get(strokeEl, 'GraphicStroke');
94✔
806
    if (!isNil(graphicStroke)) {
94✔
807
      lineSymbolizer.graphicStroke = this.getPointSymbolizerFromSldSymbolizer(graphicStroke);
16✔
808
    }
809

810
    const graphicFill = get(strokeEl, 'GraphicFill');
94✔
811
    if (!isNil(graphicFill)) {
94✔
812
      lineSymbolizer.graphicFill = this.getPointSymbolizerFromSldSymbolizer(graphicFill);
16✔
813
    }
814

815
    const perpendicularOffset = get(sldSymbolizer, 'PerpendicularOffset.#text');
94✔
816
    if (!isNil(perpendicularOffset)) {
94✔
817
      lineSymbolizer.perpendicularOffset = numberExpression(perpendicularOffset);
8✔
818
    }
819

820
    return lineSymbolizer;
94✔
821
  }
822

823
  /**
824
   * Get the geostyler-style TextSymbolizer from a SLD Symbolizer.
825
   *
826
   * @param sldSymbolizer The SLD Symbolizer
827
   * @return The geostyler-style TextSymbolizer
828
   */
829
  getTextSymbolizerFromSldSymbolizer(sldSymbolizer: any): TextSymbolizer {
830
    const textSymbolizer: TextSymbolizer = {
72✔
831
      kind: 'Text'
832
    };
833
    const fontEl = get(sldSymbolizer, 'Font');
72✔
834
    const fillEl = get(sldSymbolizer, 'Fill');
72✔
835
    const labelEl = get(sldSymbolizer, 'Label');
72✔
836
    const haloEl = get(sldSymbolizer, 'Halo');
72✔
837
    const haloFillEl = get(haloEl, 'Fill');
72✔
838

839
    const color = getParameterValue(fillEl, 'fill', this.readingSldVersion);
72✔
840
    const opacity = getParameterValue(fillEl, 'fill-opacity', this.readingSldVersion);
72✔
841

842
    const fontFamily = getParameterValue(fontEl, 'font-family', this.readingSldVersion);
72✔
843
    const fontStyle = getParameterValue(fontEl, 'font-style', this.readingSldVersion);
72✔
844
    const fontSize = getParameterValue(fontEl, 'font-size', this.readingSldVersion);
72✔
845
    const fontWeight = getParameterValue(fontEl, 'font-weight', this.readingSldVersion);
72✔
846

847
    const haloColor = getParameterValue(haloFillEl, 'fill', this.readingSldVersion);
72✔
848

849
    if (!isNil(labelEl)) {
72!
850
      textSymbolizer.label = this.getTextSymbolizerLabelFromSldSymbolizer(labelEl);
72✔
851
    }
852

853
    textSymbolizer.color = color ? color : '#000000';
72✔
854

855
    if (!isNil(opacity)) {
72✔
856
      textSymbolizer.opacity = numberExpression(opacity);
12✔
857
    }
858

859
    const haloRadius = get(sldSymbolizer, 'Halo.Radius.#text');
72✔
860
    if (!isNil(haloRadius)) {
72✔
861
      textSymbolizer.haloWidth = numberExpression(haloRadius);
40✔
862
    }
863
    const haloOpacity = getParameterValue(haloFillEl, 'fill-opacity', this.readingSldVersion);
72✔
864
    if (!isNil(haloOpacity)) {
72✔
865
      textSymbolizer.haloOpacity = numberExpression(haloOpacity);
24✔
866
    }
867
    if (!isNil(haloColor)) {
72✔
868
      textSymbolizer.haloColor = haloColor;
40✔
869
    }
870
    const placement = get(sldSymbolizer, 'LabelPlacement');
72✔
871
    if (!isNil(placement)) {
72✔
872
      const pointPlacement = get(placement, 'PointPlacement');
64✔
873
      const linePlacement = get(placement, 'LinePlacement');
64✔
874
      if (!isNil(pointPlacement)) {
64✔
875
        textSymbolizer.placement = 'point';
54✔
876
        const displacement = get(pointPlacement, 'Displacement');
54✔
877
        if (!isNil(displacement)) {
54✔
878
          const x = get(displacement, 'DisplacementX.#text');
36✔
879
          const y = get(displacement, 'DisplacementY.#text');
36✔
880
          textSymbolizer.offset = [
36✔
881
            Number.isFinite(x) ? numberExpression(x) : 0,
36!
882
            Number.isFinite(y) ? -numberExpression(y) : 0,
36!
883
          ];
884
        }
885
        const rotation = get(pointPlacement, 'Rotation.#text');
54✔
886
        if (!isNil(rotation)) {
54✔
887
          textSymbolizer.rotate = numberExpression(rotation);
24✔
888
        }
889
      } else if (!isNil(linePlacement)) {
10!
890
        textSymbolizer.placement = 'line';
10✔
891
      }
892
    }
893
    if (!isNil(fontFamily)) {
72✔
894
      textSymbolizer.font = [fontFamily];
52✔
895
    }
896
    if (!isNil(fontStyle)) {
72✔
897
      textSymbolizer.fontStyle = fontStyle.toLowerCase() as 'normal' | 'italic' | 'oblique' | undefined;
48✔
898
    }
899
    if (!isNil(fontWeight)) {
72✔
900
      textSymbolizer.fontWeight = fontWeight.toLowerCase() as 'normal' | 'bold' | undefined;
44✔
901
    }
902
    if (!isNil(fontSize)) {
72✔
903
      textSymbolizer.size = numberExpression(fontSize);
56✔
904
    }
905
    return textSymbolizer;
72✔
906
  }
907

908
  /**
909
   * Create a template string from a TextSymbolizer Label element.
910
   * The ordering of the elemments inside the Label element is preserved.
911
   *
912
   * Examples:
913
   * <Label>
914
   *  <Literal>foo</Literal>
915
   *  <PropertyName>bar</PropertyName>
916
   * </Label>
917
   * --> "foo{{bar}}"
918
   *
919
   * <Label>
920
   *  <PropertyName>bar</PropertyName>
921
   *  <Literal>foo</Literal>
922
   * </Label>
923
   * --> "{{bar}}foo"
924
   *
925
   * <Label>
926
   *  <PropertyName>bar</PropertyName>
927
   *  <Literal>foo</Literal>
928
   *  <PropertyName>john</PropertyName>
929
   * </Label>
930
   * --> "{{bar}}foo{{john}}"
931
   *
932
   * <Label>
933
   *  <PropertyName>bar</PropertyName>
934
   *  <PropertyName>john</PropertyName>
935
   *  <Literal>foo</Literal>
936
   * </Label>
937
   * --> "{{bar}}{{john}}foo"
938
   *
939
   * <Label>
940
   *  <PropertyName>bar</PropertyName>
941
   *  <PropertyName>john</PropertyName>
942
   *  <Literal>foo</Literal>
943
   *  <PropertyName>doe</PropertyName>
944
   * </Label>
945
   * --> "{{bar}}{{john}}foo{{doe}}"
946
   *
947
   * @param sldLabel
948
   */
949
  getTextSymbolizerLabelFromSldSymbolizer = (sldLabel: any): string => {
534✔
950
    const label: string = sldLabel
72✔
951
      .map((labelEl: any) => {
952
        const labelName = Object.keys(labelEl)[0];
116✔
953
        switch (labelName.replace('ogc:', '')) {
116!
954
          case '#text':
955
            return labelEl['#text'];
6✔
956
          case 'Literal':
957
            return labelEl?.[labelName]?.[0]?.['#text'] || labelEl?.[labelName]?.[0]?.['#cdata']?.[0]?.['#text'];
42!
958
          case 'PropertyName':
959
            const propName = labelEl[labelName][0]['#text'];
68✔
960
            return `{{${propName}}}`;
68✔
961
          default:
UNCOV
962
            return '';
×
963
        }
964
      })
965
      .join('');
966
    return label;
72✔
967
  };
968

969
  /**
970
   * Get the geostyler-style FillSymbolizer from a SLD Symbolizer.
971
   *
972
   * PolygonSymbolizer Stroke is just partially supported.
973
   *
974
   * @param sldSymbolizer The SLD Symbolizer
975
   * @return The geostyler-style FillSymbolizer
976
   */
977
  getFillSymbolizerFromSldSymbolizer(sldSymbolizer: any): FillSymbolizer {
978
    const fillSymbolizer: FillSymbolizer = {
142✔
979
      kind: 'Fill'
980
    };
981
    const strokeEl = get(sldSymbolizer, 'Stroke');
142✔
982
    const fillEl = get(sldSymbolizer, 'Fill');
142✔
983

984
    const fillOpacity = getParameterValue(fillEl, 'fill-opacity', this.readingSldVersion);
142✔
985
    const color = getParameterValue(fillEl, 'fill', this.readingSldVersion);
142✔
986

987
    const outlineColor = getParameterValue(strokeEl, 'stroke', this.readingSldVersion);
142✔
988
    const outlineWidth = getParameterValue(strokeEl, 'stroke-width', this.readingSldVersion);
142✔
989
    const outlineOpacity = getParameterValue(strokeEl, 'stroke-opacity', this.readingSldVersion);
142✔
990
    const outlineDashArray = getParameterValue(strokeEl, 'stroke-dasharray', this.readingSldVersion);
142✔
991
    const outlineCap = getParameterValue(strokeEl, 'stroke-linecap', this.readingSldVersion);
142✔
992
    const outlineJoin = getParameterValue(strokeEl, 'stroke-linejoin', this.readingSldVersion);
142✔
993
    // const outlineDashOffset = getParameterValue(strokeEl, 'stroke-dashoffset', this.readingSldVersion);
994

995
    const graphicFill = get(sldSymbolizer, 'Fill.GraphicFill');
142✔
996
    if (!isNil(graphicFill)) {
142✔
997
      fillSymbolizer.graphicFill = this.getPointSymbolizerFromSldSymbolizer(
42✔
998
        graphicFill
999
      );
1000
    }
1001
    if (this.withGeoServerVendorOption) {
136✔
1002
      const graphicFillPadding = getVendorOptionValue(sldSymbolizer, 'graphic-margin');
74✔
1003
      if (!isNil(graphicFillPadding)) {
74✔
1004
        fillSymbolizer.graphicFillPadding = graphicFillPadding.split(',').map(numberExpression);
4✔
1005
      }
1006
    }
1007
    if (!isNil(color)) {
136✔
1008
      fillSymbolizer.color = color;
116✔
1009
    }
1010
    if (!isNil(fillOpacity)) {
136✔
1011
      fillSymbolizer.fillOpacity = numberExpression(fillOpacity);
44✔
1012
    }
1013

1014
    if (!isNil(outlineColor)) {
136✔
1015
      fillSymbolizer.outlineColor = outlineColor;
64✔
1016
    }
1017
    if (!isNil(outlineWidth)) {
136✔
1018
      fillSymbolizer.outlineWidth = numberExpression(outlineWidth);
52✔
1019
    }
1020
    if (!isNil(outlineOpacity)) {
136✔
1021
      fillSymbolizer.outlineOpacity = numberExpression(outlineOpacity);
12✔
1022
    }
1023
    if (!isNil(outlineDashArray)) {
136✔
1024
      fillSymbolizer.outlineDasharray = outlineDashArray.split(' ').map(numberExpression);
24✔
1025
    }
1026
    if (!isNil(outlineCap)) {
136✔
1027
      fillSymbolizer.outlineCap = outlineCap;
4✔
1028
    }
1029
    if (!isNil(outlineJoin)) {
136✔
1030
      fillSymbolizer.outlineJoin = outlineJoin;
4✔
1031
    }
1032
    // TODO: seems like this is missing in the geostyer-stlye
1033
    // if (outlineDashOffset) {
1034
    //   fillSymbolizer.outlineDashOffset = Number(outlineDashOffset);
1035
    // }
1036
    return fillSymbolizer;
136✔
1037
  }
1038

1039
  /**
1040
   * Get the geostyler-style RasterSymbolizer from a SLD Symbolizer.
1041
   *
1042
   * @param sldSymbolizer The SLD Symbolizer
1043
   */
1044
  getRasterSymbolizerFromSldSymbolizer(sldSymbolizer: any): RasterSymbolizer {
1045
    const rasterSymbolizer: RasterSymbolizer = {
32✔
1046
      kind: 'Raster'
1047
    };
1048
    // parse Opacity
1049
    let opacity = get(sldSymbolizer, 'Opacity.#text');
32✔
1050
    if (!isNil(opacity)) {
32✔
1051
      opacity = numberExpression(opacity);
30✔
1052
      rasterSymbolizer.opacity = opacity;
30✔
1053
    }
1054
    // parse ColorMap
1055
    const sldColorMap = get(sldSymbolizer, 'ColorMap');
32✔
1056
    const sldColorMapType = get(sldSymbolizer, 'ColorMap.@type');
32✔
1057
    const extended = get(sldSymbolizer, 'ColorMap.@extended');
32✔
1058
    if (!isNil(sldColorMap)) {
32✔
1059
      const colormap = this.getColorMapFromSldColorMap(sldColorMap, sldColorMapType, extended);
18✔
1060
      rasterSymbolizer.colorMap = colormap;
18✔
1061
    }
1062
    // parse ChannelSelection
1063
    const sldChannelSelection = get(sldSymbolizer, 'ChannelSelection');
32✔
1064
    if (!isNil(sldChannelSelection)) {
32✔
1065
      const channelSelection = this.getChannelSelectionFromSldChannelSelection(sldChannelSelection);
8✔
1066
      rasterSymbolizer.channelSelection = channelSelection;
8✔
1067
    }
1068
    // parse ContrastEnhancement
1069
    const sldContrastEnhancement = get(sldSymbolizer, 'ContrastEnhancement');
32✔
1070
    if (!isNil(sldContrastEnhancement)) {
32✔
1071
      const contrastEnhancement = this.getContrastEnhancementFromSldContrastEnhancement(sldContrastEnhancement);
10✔
1072
      rasterSymbolizer.contrastEnhancement = contrastEnhancement;
10✔
1073
    }
1074
    return rasterSymbolizer;
32✔
1075
  }
1076

1077
  /**
1078
   * Get the geostyler-style MarkSymbolizer from a SLD Symbolizer
1079
   *
1080
   * @param sldSymbolizer The SLD Symbolizer
1081
   * @return The geostyler-style MarkSymbolizer
1082
   */
1083
  getMarkSymbolizerFromSldSymbolizer(sldSymbolizer: any): MarkSymbolizer {
1084
    const wellKnownName: WellKnownName = get(sldSymbolizer, 'Graphic.Mark.WellKnownName.#text');
204✔
1085
    const strokeEl = get(sldSymbolizer, 'Graphic.Mark.Stroke');
204✔
1086
    const fillEl = get(sldSymbolizer, 'Graphic.Mark.Fill');
204✔
1087

1088
    const opacity = get(sldSymbolizer, 'Graphic.Opacity.#text');
204✔
1089
    const size = get(sldSymbolizer, 'Graphic.Size.#text');
204✔
1090
    const rotation = get(sldSymbolizer, 'Graphic.Rotation.#text');
204✔
1091
    const fillOpacity = getParameterValue(fillEl, 'fill-opacity', this.readingSldVersion);
204✔
1092
    const color = getParameterValue(fillEl, 'fill', this.readingSldVersion);
204✔
1093
    const displacement = get(sldSymbolizer, 'Graphic.Displacement');
204✔
1094

1095
    const markSymbolizer: MarkSymbolizer = {
204✔
1096
      kind: 'Mark',
1097
      wellKnownName: 'circle'
1098
    };
1099

1100
    if (!isNil(opacity)) {
204✔
1101
      markSymbolizer.opacity = numberExpression(opacity);
60✔
1102
    }
1103
    if (!isNil(fillOpacity)) {
204✔
1104
      markSymbolizer.fillOpacity = numberExpression(fillOpacity);
36✔
1105
    }
1106
    if (!isNil(color)) {
204✔
1107
      markSymbolizer.color = color;
192✔
1108
    }
1109
    if (!isNil(rotation)) {
204✔
1110
      markSymbolizer.rotate = numberExpression(rotation);
24✔
1111
    }
1112
    if (!isNil(size)) {
204✔
1113
      // edge case where the value has to be divided by 2 which has to be considered in the function
1114
      markSymbolizer.radius = isGeoStylerNumberFunction(size) ? size : Number(size) / 2;
190✔
1115
    }
1116
    if (displacement) {
204✔
1117
      const x = get(displacement, 'DisplacementX.#text');
4✔
1118
      const y = get(displacement, 'DisplacementY.#text');
4✔
1119
      markSymbolizer.offset = [
4✔
1120
        Number.isFinite(x) ? numberExpression(x) : 0,
4!
1121
        Number.isFinite(y) ? numberExpression(y) : 0,
4!
1122
      ];
1123
    }
1124

1125
    switch (wellKnownName) {
204✔
1126
      case 'circle':
1127
      case 'square':
1128
      case 'triangle':
1129
      case 'star':
1130
      case 'cross':
1131
      case 'x':
1132
      case 'shape://vertline':
1133
      case 'shape://horline':
1134
      case 'shape://slash':
1135
      case 'shape://backslash':
1136
      case 'shape://dot':
1137
      case 'shape://plus':
1138
      case 'shape://times':
1139
      case 'shape://oarrow':
1140
      case 'shape://carrow':
1141
        markSymbolizer.wellKnownName = wellKnownName;
190✔
1142
        break;
190✔
1143
      default:
1144
        if (WELLKNOWNNAME_TTF_REGEXP.test(wellKnownName)) {
14✔
1145
          markSymbolizer.wellKnownName = wellKnownName;
8✔
1146
          break;
8✔
1147
        }
1148
        throw new Error(
6✔
1149
          this.translate('marksymbolizerParseFailedUnknownWellknownName', { wellKnownName: wellKnownName })
1150
        );
1151
    }
1152

1153
    const strokeColor = getParameterValue(strokeEl, 'stroke', this.readingSldVersion);
198✔
1154
    if (!isNil(strokeColor)) {
198✔
1155
      markSymbolizer.strokeColor = strokeColor;
132✔
1156
    }
1157
    const strokeWidth = getParameterValue(strokeEl, 'stroke-width', this.readingSldVersion);
198✔
1158
    if (!isNil(strokeWidth)) {
198✔
1159
      markSymbolizer.strokeWidth = numberExpression(strokeWidth);
92✔
1160
    }
1161
    const strokeOpacity = getParameterValue(strokeEl, 'stroke-opacity', this.readingSldVersion);
198✔
1162
    if (!isNil(strokeOpacity)) {
198✔
1163
      markSymbolizer.strokeOpacity = numberExpression(strokeOpacity);
28✔
1164
    }
1165

1166
    return markSymbolizer;
198✔
1167
  }
1168

1169
  /**
1170
   * Get the geostyler-style IconSymbolizer from a SLD Symbolizer
1171
   *
1172
   * @param sldSymbolizer The SLD Symbolizer
1173
   * @return The geostyler-style IconSymbolizer
1174
   */
1175
  getIconSymbolizerFromSldSymbolizer(sldSymbolizer: any): IconSymbolizer {
1176
    const image = get(sldSymbolizer, 'Graphic.ExternalGraphic.OnlineResource.@href');
56✔
1177
    const iconSymbolizer: IconSymbolizer = <IconSymbolizer>{
56✔
1178
      kind: 'Icon',
1179
      image
1180
    };
1181
    const opacity: string = get(sldSymbolizer, 'Graphic.Opacity.#text');
56✔
1182
    const size: string = get(sldSymbolizer, 'Graphic.Size.#text');
56✔
1183
    const rotation: string = get(sldSymbolizer, 'Graphic.Rotation.#text');
56✔
1184
    const displacement = get(sldSymbolizer, 'Graphic.Displacement');
56✔
1185
    if (!isNil(opacity)) {
56✔
1186
      iconSymbolizer.opacity = numberExpression(opacity);
4✔
1187
    }
1188
    if (!isNil(size)) {
56✔
1189
      iconSymbolizer.size = numberExpression(size);
52✔
1190
    }
1191
    if (!isNil(rotation)) {
56✔
1192
      iconSymbolizer.rotate = numberExpression(rotation);
48✔
1193
    }
1194
    if (displacement) {
56✔
1195
      const x = get(displacement, 'DisplacementX.#text');
4✔
1196
      const y = get(displacement, 'DisplacementY.#text');
4✔
1197
      iconSymbolizer.offset = [
4✔
1198
        Number.isFinite(x) ? numberExpression(x) : 0,
4!
1199
        Number.isFinite(y) ? numberExpression(y) : 0,
4!
1200
      ];
1201
    }
1202
    return iconSymbolizer;
56✔
1203
  }
1204

1205
  /**
1206
   * Get the geostyler-style ColorMap from a SLD ColorMap.
1207
   *
1208
   * @param sldColorMap The SLD ColorMap
1209
   */
1210
  getColorMapFromSldColorMap(sldColorMap: any, type: ColorMapType = 'ramp', extended?: string): ColorMap {
12✔
1211
    const colorMap: ColorMap = {
18✔
1212
      type
1213
    };
1214

1215
    if (extended) {
18!
UNCOV
1216
      if (extended === 'true') {
×
1217
        colorMap.extended = true;
×
1218
      } else {
UNCOV
1219
        colorMap.extended = false;
×
1220
      }
1221
    }
1222

1223
    const colorMapEntries = getChildren(sldColorMap, 'ColorMapEntry');
18✔
1224
    if (Array.isArray(colorMapEntries)) {
18!
1225
      const cmEntries = colorMapEntries.map((cm) => {
18✔
1226
        const color = getAttribute(cm, 'color');
64✔
1227
        if (!color) {
64!
UNCOV
1228
          throw new Error(this.translate('colorMapEntriesParseFailedColorUndefined'));
×
1229
        }
1230
        let quantity = getAttribute(cm, 'quantity');
64✔
1231
        if (quantity) {
64!
1232
          quantity = numberExpression(quantity);
64✔
1233
        }
1234
        const label = getAttribute(cm, 'label');
64✔
1235
        let opacity = getAttribute(cm, 'opacity');
64✔
1236
        if (!isNil(opacity)) {
64✔
1237
          opacity = numberExpression(opacity);
24✔
1238
        }
1239
        return {
64✔
1240
          color,
1241
          quantity,
1242
          label,
1243
          opacity
1244
        } as ColorMapEntry;
1245
      });
1246
      colorMap.colorMapEntries = cmEntries;
18✔
1247
    }
1248

1249
    return colorMap;
18✔
1250
  }
1251

1252
  /**
1253
   * Get the geostyler-style ContrastEnhancement from a SLD ContrastEnhancement.
1254
   *
1255
   * @param sldContrastEnhancement The SLD ContrastEnhancement
1256
   */
1257
  getContrastEnhancementFromSldContrastEnhancement(sldContrastEnhancement: any): ContrastEnhancement {
1258
    const contrastEnhancement: ContrastEnhancement = {};
26✔
1259

1260
    // parse enhancementType
1261
    const hasHistogram = !!get(sldContrastEnhancement, 'Histogram');
26✔
1262
    const hasNormalize = !!get(sldContrastEnhancement, 'Normalize');
26✔
1263
    if (hasHistogram && hasNormalize) {
26!
UNCOV
1264
      throw new Error(this.translate('contrastEnhancParseFailedHistoAndNormalizeMutuallyExclusive'));
×
1265
    } else if (hasHistogram) {
26✔
1266
      contrastEnhancement.enhancementType = 'histogram';
16✔
1267
    } else if (hasNormalize) {
10✔
1268
      contrastEnhancement.enhancementType = 'normalize';
8✔
1269
    }
1270
    // parse gammavalue
1271
    let gammaValue = get(sldContrastEnhancement, 'GammaValue.#text');
26✔
1272
    if (gammaValue) {
26✔
1273
      gammaValue = numberExpression(gammaValue);
8✔
1274
    }
1275
    contrastEnhancement.gammaValue = gammaValue;
26✔
1276

1277
    return contrastEnhancement;
26✔
1278
  }
1279

1280
  /**
1281
   * Get the geostyler-style Channel from a SLD Channel.
1282
   *
1283
   * @param sldChannel The SLD Channel
1284
   */
1285
  getChannelFromSldChannel(sldChannel: any): Channel {
1286
    const sourceChannelName = get(sldChannel, 'SourceChannelName.#text')?.toString();
24✔
1287
    const channel: Channel = {
24✔
1288
      sourceChannelName
1289
    };
1290
    const contrastEnhancement = get(sldChannel, 'ContrastEnhancement');
24✔
1291
    if (contrastEnhancement) {
24✔
1292
      channel.contrastEnhancement = this.getContrastEnhancementFromSldContrastEnhancement(contrastEnhancement);
16✔
1293
    }
1294
    return channel;
24✔
1295
  }
1296

1297
  /**
1298
   * Get the geostyler-style ChannelSelection from a SLD ChannelSelection.
1299
   *
1300
   * @param sldChannelSelection The SLD ChannelSelection
1301
   */
1302
  getChannelSelectionFromSldChannelSelection(sldChannelSelection: any): ChannelSelection {
1303
    let channelSelection: ChannelSelection;
1304
    const red = get(sldChannelSelection, 'RedChannel');
8✔
1305
    const blue = get(sldChannelSelection, 'BlueChannel');
8✔
1306
    const green = get(sldChannelSelection, 'GreenChannel');
8✔
1307
    const gray = get(sldChannelSelection, 'GrayChannel');
8✔
1308

1309
    if (gray && red && blue && green) {
8!
UNCOV
1310
      throw new Error(this.translate('channelSelectionParseFailedRGBAndGrayscaleMutuallyExclusive'));
×
1311
    }
1312
    if (gray) {
8!
UNCOV
1313
      const grayChannel = this.getChannelFromSldChannel(gray);
×
UNCOV
1314
      channelSelection = {
×
1315
        grayChannel
1316
      };
1317
    } else if (red && green && blue) {
8!
1318
      const redChannel = this.getChannelFromSldChannel(red);
8✔
1319
      const blueChannel = this.getChannelFromSldChannel(blue);
8✔
1320
      const greenChannel = this.getChannelFromSldChannel(green);
8✔
1321
      channelSelection = {
8✔
1322
        redChannel,
1323
        blueChannel,
1324
        greenChannel
1325
      };
1326
    } else {
UNCOV
1327
      throw new Error(this.translate('channelSelectionParseFailedRGBChannelsUndefined'));
×
1328
    }
1329
    return channelSelection;
8✔
1330
  }
1331

1332
  /**
1333
   * The writeStyle implementation of the geostyler-style StyleParser interface.
1334
   * It reads a geostyler-style and returns a Promise.
1335
   * The Promise itself resolves with a SLD string.
1336
   *
1337
   * @param geoStylerStyle A geostyler-style.
1338
   * @return The Promise resolving with the SLD as a string.
1339
   */
1340
  writeStyle(geoStylerStyle: Style): Promise<WriteStyleResult<string>> {
1341
    return new Promise<WriteStyleResult<string>>(resolve => {
208✔
1342
      const unsupportedProperties = this.checkForUnsupportedProperties(geoStylerStyle);
208✔
1343
      try {
208✔
1344
        const sldObject = this.geoStylerStyleToSldObject(geoStylerStyle);
208✔
1345
        const sldString = this.builder.build(sldObject);
208✔
1346
        resolve({
208✔
1347
          output: sldString,
1348
          unsupportedProperties,
1349
          warnings: unsupportedProperties && ['Your style contains unsupportedProperties!']
260✔
1350
        });
1351
      } catch (error) {
UNCOV
1352
        resolve({
×
1353
          errors: [error]
1354
        });
1355
      }
1356
    });
1357
  }
1358

1359
  /**
1360
   * Get the correct tagName in dependency to the configured sldVersion.
1361
   *
1362
   * @param tagName
1363
   * @returns The tagName as used by the configured sldVersion
1364
   */
1365
  getTagName(tagName: string): string {
1366
    const ogcList = ['Filter'];
5,502✔
1367
    if (ogcList.includes(tagName)) {
5,502✔
1368
      return tagName;
208✔
1369
    }
1370
    if (tagName === 'CssParameter') {
5,294✔
1371
      return this.sldVersion === '1.1.0' ? 'se:SvgParameter' : 'CssParameter';
244✔
1372
    }
1373
    return this.sldVersion === '1.1.0' ? `se:${tagName}` : tagName;
5,050✔
1374
  }
1375

1376
  /**
1377
   * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style
1378
   *
1379
   * @param geoStylerStyle A geostyler-style.
1380
   * @return The object representation of a SLD Style (readable with fast-xml-parser)
1381
   */
1382
  geoStylerStyleToSldObject(geoStylerStyle: Style): any {
1383
    const rules: any[] = this.getSldRulesFromRules(geoStylerStyle.rules);
208✔
1384
    // add the ogc namespace to the filter element, if a filter is present
1385
    rules.forEach(rule => {
208✔
1386
      const ruleEl = get(rule, this.getTagName('Rule'));
240✔
1387
      const filter = getChildren(ruleEl, 'Filter').at(0);
240✔
1388
      if (filter) {
240✔
1389
        filter[':@'] = {
54✔
1390
          '@_xmlns': 'http://www.opengis.net/ogc'
1391
        };
1392
      }
1393
    });
1394

1395
    const featureTypeStyle = [
208✔
1396
      ...rules
1397
    ];
1398
    const Name = this.getTagName('Name');
208✔
1399
    const Title = this.getTagName('Title');
208✔
1400
    const FeatureTypeStyle = this.getTagName('FeatureTypeStyle');
208✔
1401

1402
    const attributes = {
208✔
1403
      '@_version': this.sldVersion,
1404
      '@_xsi:schemaLocation': 'http://www.opengis.net/sld StyledLayerDescriptor.xsd',
1405
      '@_xmlns': 'http://www.opengis.net/sld',
1406
      '@_xmlns:ogc': 'http://www.opengis.net/ogc',
1407
      '@_xmlns:xlink': 'http://www.w3.org/1999/xlink',
1408
      '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
1409
      '@_xmlns:se': 'http://www.opengis.net/se'
1410
    };
1411

1412
    const userStyle = [];
208✔
1413
    userStyle.push({
208✔
1414
      [Name]: [{ '#text': geoStylerStyle.name || '' }]
208!
1415
    });
1416
    if (this.sldVersion === '1.0.0') {
208✔
1417
      userStyle.push({
122✔
1418
        [Title]: [{ '#text': geoStylerStyle.name || '' }]
122!
1419
      });
1420
    }
1421
    userStyle.push({
208✔
1422
      [FeatureTypeStyle]: featureTypeStyle
1423
    });
1424

1425
    return [{
208✔
1426
      '?xml': [{ '#text': '' }],
1427
      ':@': {
1428
        '@_version': '1.0',
1429
        '@_encoding': 'UTF-8',
1430
        '@_standalone': 'yes'
1431
      },
1432
    }, {
1433
      StyledLayerDescriptor: [{
1434
        NamedLayer: [{
1435
          [Name]: [{ '#text': geoStylerStyle.name || '' }]
208!
1436
        }, {
1437
          UserStyle: userStyle
1438
        }]
1439
      }],
1440
      ':@': attributes
1441
    }];
1442
  }
1443

1444
  /**
1445
   * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style Rule.
1446
   *
1447
   * @param rules An array of geostyler-style Rules.
1448
   * @return The object representation of a SLD Rule (readable with fast-xml-parser)
1449
   */
1450
  getSldRulesFromRules(rules: Rule[]): any[] {
1451
    const Name = this.getTagName('Name');
208✔
1452
    const filterTag = this.getTagName('Filter');
208✔
1453
    const ruleTag = this.getTagName('Rule');
208✔
1454
    const MinScaleDenominator = this.getTagName('MinScaleDenominator');
208✔
1455
    const MaxScaleDenominator = this.getTagName('MaxScaleDenominator');
208✔
1456

1457
    return rules.map((rule: Rule) => {
208✔
1458
      const sldRule: any = {
240✔
1459
        [ruleTag]: []
1460
      };
1461
      if (rule.name) {
240✔
1462
        sldRule[ruleTag].push({
130✔
1463
          [Name]: [{
1464
            '#text': rule.name
1465
          }]
1466
        });
1467
      }
1468
      if (rule.filter) {
240✔
1469
        const filter = this.getSldFilterFromFilter(rule.filter);
54✔
1470
        sldRule[ruleTag].push({
54✔
1471
          [filterTag]: filter
1472
        });
1473
      }
1474
      if (rule.scaleDenominator) {
240✔
1475
        const { min, max } = rule.scaleDenominator;
34✔
1476
        if (min && Number.isFinite(min)) {
34✔
1477
          sldRule[ruleTag].push({
26✔
1478
            [MinScaleDenominator]: [{
1479
              '#text': min
1480
            }]
1481
          });
1482
        }
1483
        if (max && Number.isFinite(max)) {
34✔
1484
          sldRule[ruleTag].push({
32✔
1485
            [MaxScaleDenominator]: [{
1486
              '#text': max
1487
            }]
1488
          });
1489
        }
1490
      }
1491

1492
      // Remove empty Symbolizers and check if there is at least 1 symbolizer
1493
      const symbolizers = this.getSldSymbolizersFromSymbolizers(rule.symbolizers);
240✔
1494
      let symbolizerKeys: string[] = [];
240✔
1495
      if (symbolizers.length > 0) {
240!
1496
        symbolizerKeys = Object.keys(symbolizers[0]);
240✔
1497
      }
1498
      symbolizerKeys.forEach((key: string) => {
240✔
1499
        if (symbolizers[0][key].length === 0) {
240!
UNCOV
1500
          delete symbolizers[0][key];
×
1501
        }
1502
      });
1503

1504
      if (symbolizers.length > 0 && symbolizerKeys.length !== 0) {
240!
1505
        sldRule[ruleTag] = [
240✔
1506
          ...sldRule[ruleTag],
1507
          ...symbolizers
1508
        ];
1509
      }
1510
      return sldRule;
240✔
1511
    });
1512
  }
1513

1514
  /**
1515
   * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style ComparisonFilter.
1516
   *
1517
   * @param comparisonFilter A geostyler-style ComparisonFilter.
1518
   * @return The object representation of a SLD Filter Expression with a
1519
   * comparison operator (readable with fast-xml-parser)
1520
   */
1521
  getSldComparisonFilterFromComparisonFilter(comparisonFilter: ComparisonFilter): any[] {
1522
    const sldComparisonFilter: any = [];
212✔
1523
    const operator = comparisonFilter[0];
212✔
1524
    const key = comparisonFilter[1];
212✔
1525
    const value = comparisonFilter[2];
212✔
1526

1527
    const sldOperators: string[] = keysByValue(COMPARISON_MAP, operator);
212✔
1528
    const sldOperator: string = (sldOperators.length > 1 && value === null)
212✔
1529
      ? sldOperators[1] : sldOperators[0];
1530

1531
    const propertyKey = 'PropertyName';
212✔
1532

1533
    if (isGeoStylerFunction(key) || isGeoStylerFunction(value)) {
212✔
1534
      const tempOperator = sldOperator.replace('PropertyIs', '');
24✔
1535
      const sldFunctionOperator = tempOperator.charAt(0).toLowerCase() + tempOperator.slice(1);
24✔
1536
      const keyResult = isGeoStylerFunction(key) ? geoStylerFunctionToSldFunction(key) : key;
24!
1537
      const valueResult = isGeoStylerFunction(value) ? geoStylerFunctionToSldFunction(value) : value;
24✔
1538

1539
      const functionChildren: any = [];
24✔
1540

1541
      if (isGeoStylerFunction(key)) {
24!
1542
        functionChildren.unshift(Array.isArray(keyResult) ? keyResult?.[0] : keyResult);
24✔
1543
      } else {
UNCOV
1544
        functionChildren.unshift({
×
1545
          Literal: [{
1546
            '#text': keyResult
1547
          }]
1548
        });
1549
      }
1550

1551
      if (isGeoStylerFunction(value)) {
24✔
1552
        functionChildren.push(Array.isArray(valueResult) ? valueResult?.[0] : valueResult);
20!
1553
      } else {
1554
        functionChildren.push({
4✔
1555
          Literal: [{
1556
            '#text': valueResult
1557
          }]
1558
        });
1559
      }
1560

1561
      return [{
24✔
1562
        Function: functionChildren,
1563
        ':@': {
1564
          '@_name': sldFunctionOperator
1565
        }
1566
      }];
1567
    }
1568

1569
    if (sldOperator === 'PropertyIsNull') {
188✔
1570
      // empty, selfclosing Literals are not valid in a propertyIsNull filter
1571
      sldComparisonFilter.push({
12✔
1572
        [sldOperator]: [{
1573
          [propertyKey]: [{
1574
            '#text': key
1575
          }]
1576
        }]
1577
      });
1578
    } else if (sldOperator === 'PropertyIsLike') {
176✔
1579
      sldComparisonFilter.push({
24✔
1580
        [sldOperator]: [{
1581
          [propertyKey]: [{
1582
            '#text': key
1583
          }]
1584
        }, {
1585
          Literal: [{
1586
            '#text': value
1587
          }]
1588
        }],
1589
        [':@']: {
1590
          '@_wildCard': '*',
1591
          '@_singleChar': '.',
1592
          '@_escape': '!',
1593
        }
1594
      });
1595
    } else if (sldOperator === 'PropertyIsBetween') {
152✔
1596
      // Currently we only support Literals as values.
1597
      const betweenFilter = comparisonFilter as RangeFilter;
8✔
1598
      sldComparisonFilter.push({
8✔
1599
        [sldOperator]: [{
1600
          [propertyKey]: [{
1601
            '#text': key
1602
          }]
1603
        }, {
1604
          LowerBoundary: [{
1605
            Literal: [{
1606
              '#text': betweenFilter[2]
1607
            }]
1608
          }]
1609
        }, {
1610
          UpperBoundary: [{
1611
            Literal: [{
1612
              '#text': betweenFilter[3]
1613
            }]
1614
          }]
1615
        }]
1616
      });
1617
    } else {
1618
      sldComparisonFilter.push({
144✔
1619
        [sldOperator]: [{
1620
          [propertyKey]: [{
1621
            '#text': key
1622
          }]
1623
        }, {
1624
          Literal: [{
1625
            '#text': value
1626
          }]
1627
        }]
1628
      });
1629
    }
1630

1631

1632
    return sldComparisonFilter;
188✔
1633
  }
1634

1635
  /**
1636
   * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style Filter.
1637
   *
1638
   * @param filter A geostyler-style Filter.
1639
   * @return The object representation of a SLD Filter Expression (readable with fast-xml-parser)
1640
   */
1641
  getSldFilterFromFilter(filter: Filter): any[] {
1642
    let sldFilter: any[] = [];
298✔
1643

1644
    if (isComparisonFilter(filter)) {
298✔
1645
      sldFilter = this.getSldComparisonFilterFromComparisonFilter(filter);
212✔
1646
    } else if (isNegationFilter(filter)) {
86✔
1647
      sldFilter.push({
12✔
1648
        Not: this.getSldFilterFromFilter(filter[1])
1649
      });
1650
    } else if (isCombinationFilter(filter)) {
74!
1651
      const [
1652
        operator,
1653
        ...args
1654
      ] = filter;
74✔
1655
      const sldOperators: string[] = keysByValue(COMBINATION_MAP, operator);
74✔
1656
      const combinator = sldOperators[0];
74✔
1657
      const sldSubFilters = args.map(subFilter => this.getSldFilterFromFilter(subFilter)[0]);
232✔
1658
      sldFilter.push({
74✔
1659
        [combinator]: sldSubFilters
1660
      });
1661
    }
1662
    return sldFilter;
298✔
1663
  }
1664

1665
  /**
1666
   * Get the SLD Object (readable with fast-xml-parser) from geostyler-style Symbolizers.
1667
   *
1668
   * @param symbolizers A geostyler-style Symbolizer array.
1669
   * @return The object representation of a SLD Symbolizer (readable with fast-xml-parser)
1670
   */
1671
  getSldSymbolizersFromSymbolizers(symbolizers: Symbolizer[]): any {
1672
    const sldSymbolizers: any = [];
240✔
1673
    const pointSymbolizerTag = this.getTagName('PointSymbolizer');
240✔
1674
    const textSymbolizerTag = this.getTagName('TextSymbolizer');
240✔
1675
    const lineSymbolizerTag = this.getTagName('LineSymbolizer');
240✔
1676
    const polygonSymbolizerTag = this.getTagName('PolygonSymbolizer');
240✔
1677
    const rasterSymbolizerTag = this.getTagName('RasterSymbolizer');
240✔
1678

1679
    symbolizers.forEach(symb => {
240✔
1680
      const sldSymbolizer: any = {};
250✔
1681
      let sldSymb: any[];
1682
      switch (symb.kind) {
250!
1683
        case 'Mark':
1684
          sldSymb = this.getSldPointSymbolizerFromMarkSymbolizer(symb);
88✔
1685
          sldSymbolizer[pointSymbolizerTag] = sldSymb;
88✔
1686
          break;
88✔
1687
        case 'Icon':
1688
          sldSymb = this.getSldPointSymbolizerFromIconSymbolizer(symb);
12✔
1689
          sldSymbolizer[pointSymbolizerTag] = sldSymb;
12✔
1690
          break;
12✔
1691
        case 'Text':
1692
          sldSymb = this.getSldTextSymbolizerFromTextSymbolizer(symb);
34✔
1693
          sldSymbolizer[textSymbolizerTag] = sldSymb;
34✔
1694
          break;
34✔
1695
        case 'Line':
1696
          sldSymb = this.getSldLineSymbolizerFromLineSymbolizer(symb);
46✔
1697
          sldSymbolizer[lineSymbolizerTag] = sldSymb;
46✔
1698
          break;
46✔
1699
        case 'Fill':
1700
          sldSymb = this.getSldPolygonSymbolizerFromFillSymbolizer(symb);
56✔
1701
          sldSymbolizer[polygonSymbolizerTag] = sldSymb;
56✔
1702
          break;
56✔
1703
        case 'Raster':
1704
          sldSymb = this.getSldRasterSymbolizerFromRasterSymbolizer(symb);
14✔
1705
          sldSymbolizer[rasterSymbolizerTag] = sldSymb;
14✔
1706
          break;
14✔
1707
        default:
UNCOV
1708
          break;
×
1709
      }
1710
      sldSymbolizers.push(sldSymbolizer);
250✔
1711
    });
1712
    return sldSymbolizers;
240✔
1713
  }
1714

1715
  /**
1716
   * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style MarkSymbolizer.
1717
   *
1718
   * @param markSymbolizer A geostyler-style MarkSymbolizer.
1719
   * @return The object representation of a SLD PointSymbolizer with a Mark
1720
   */
1721
  getSldPointSymbolizerFromMarkSymbolizer(markSymbolizer: MarkSymbolizer): any {
1722
    const wellKnownName = this.getTagName('WellKnownName');
108✔
1723
    const CssParameter = this.getTagName('CssParameter');
108✔
1724
    const Fill = this.getTagName('Fill');
108✔
1725
    const Mark = this.getTagName('Mark');
108✔
1726
    const Stroke = this.getTagName('Stroke');
108✔
1727
    const Opacity = this.getTagName('Opacity');
108✔
1728
    const Rotation = this.getTagName('Rotation');
108✔
1729
    const Size = this.getTagName('Size');
108✔
1730
    const Graphic = this.getTagName('Graphic');
108✔
1731
    const Displacement = this.getTagName('Displacement');
108✔
1732
    const DisplacementX = this.getTagName('DisplacementX');
108✔
1733
    const DisplacementY = this.getTagName('DisplacementY');
108✔
1734

1735
    const isFontSymbol = WELLKNOWNNAME_TTF_REGEXP.test(markSymbolizer.wellKnownName);
108✔
1736
    const mark: any[] = [{
108✔
1737
      [wellKnownName]: [{
1738
        '#text': isFontSymbol ? markSymbolizer.wellKnownName : markSymbolizer.wellKnownName.toLowerCase()
108✔
1739
      }]
1740
    }];
1741

1742
    if (markSymbolizer.color || !isNil(markSymbolizer.fillOpacity)) {
108✔
1743
      const fillCssParamaters = [];
102✔
1744
      if (markSymbolizer.color) {
102!
1745
        if (isGeoStylerFunction(markSymbolizer.color)) {
102!
UNCOV
1746
          const children = geoStylerFunctionToSldFunction(markSymbolizer.color);
×
UNCOV
1747
          fillCssParamaters.push({
×
1748
            [CssParameter]: children,
1749
            ':@': {
1750
              '@_name': 'fill'
1751
            }
1752
          });
1753
        } else {
1754
          fillCssParamaters.push({
102✔
1755
            [CssParameter]: [{
1756
              '#text': markSymbolizer.color,
1757
            }],
1758
            ':@': {
1759
              '@_name': 'fill'
1760
            }
1761
          });
1762
        }
1763
      }
1764
      if (!isNil(markSymbolizer.fillOpacity)) {
102✔
1765
        if (isGeoStylerFunction(markSymbolizer.fillOpacity)) {
20!
UNCOV
1766
          const children = geoStylerFunctionToSldFunction(markSymbolizer.fillOpacity);
×
UNCOV
1767
          fillCssParamaters.push({
×
1768
            [CssParameter]: children,
1769
            ':@': {
1770
              '@_name': 'fill-opacity'
1771
            }
1772
          });
1773
        } else {
1774
          fillCssParamaters.push({
20✔
1775
            [CssParameter]: [{
1776
              '#text': markSymbolizer.fillOpacity,
1777
            }],
1778
            ':@': {
1779
              '@_name': 'fill-opacity'
1780
            }
1781
          });
1782
        }
1783
      }
1784
      mark.push({
102✔
1785
        [Fill]: fillCssParamaters
1786
      });
1787
    }
1788

1789
    if (markSymbolizer.strokeColor ||
108✔
1790
      Number.isFinite(markSymbolizer.strokeWidth) ||
1791
      Number.isFinite(markSymbolizer.strokeOpacity)
1792
    ) {
1793
      const strokeCssParameters = [];
76✔
1794
      if (markSymbolizer.strokeColor) {
76!
1795
        if (isGeoStylerFunction(markSymbolizer.strokeColor)) {
76!
UNCOV
1796
          const children = geoStylerFunctionToSldFunction(markSymbolizer.strokeColor);
×
UNCOV
1797
          strokeCssParameters.push({
×
1798
            [CssParameter]: children,
1799
            ':@': {
1800
              '@_name': 'stroke'
1801
            }
1802
          });
1803
        } else {
1804
          strokeCssParameters.push({
76✔
1805
            [CssParameter]: [{
1806
              '#text': markSymbolizer.strokeColor,
1807
            }],
1808
            ':@': {
1809
              '@_name': 'stroke'
1810
            }
1811
          });
1812
        }
1813
      }
1814
      if (markSymbolizer.strokeWidth) {
76✔
1815
        if (isGeoStylerFunction(markSymbolizer.strokeWidth)) {
54!
UNCOV
1816
          const children = geoStylerFunctionToSldFunction(markSymbolizer.strokeWidth);
×
UNCOV
1817
          strokeCssParameters.push({
×
1818
            [CssParameter]: children,
1819
            ':@': {
1820
              '@_name': 'stroke-width'
1821
            }
1822
          });
1823
        } else {
1824
          strokeCssParameters.push({
54✔
1825
            [CssParameter]: [{
1826
              '#text': markSymbolizer.strokeWidth,
1827
            }],
1828
            ':@': {
1829
              '@_name': 'stroke-width'
1830
            }
1831
          });
1832
        }
1833
      }
1834
      if (!isNil(markSymbolizer.strokeOpacity)) {
76✔
1835
        if (isGeoStylerFunction(markSymbolizer.strokeOpacity)) {
16!
UNCOV
1836
          const children = geoStylerFunctionToSldFunction(markSymbolizer.strokeOpacity);
×
UNCOV
1837
          strokeCssParameters.push({
×
1838
            [CssParameter]: children,
1839
            ':@': {
1840
              '@_name': 'stroke-opacity'
1841
            }
1842
          });
1843
        } else {
1844
          strokeCssParameters.push({
16✔
1845
            [CssParameter]: [{
1846
              '#text': markSymbolizer.strokeOpacity,
1847
            }],
1848
            ':@': {
1849
              '@_name': 'stroke-opacity'
1850
            }
1851
          });
1852
        }
1853
      }
1854
      mark.push({
76✔
1855
        [Stroke]: strokeCssParameters
1856
      });
1857
    }
1858

1859
    const graphic: any[] = [{
108✔
1860
      [Mark]: mark
1861
    }];
1862

1863
    if (!isNil(markSymbolizer.opacity)) {
108✔
1864
      graphic.push({
30✔
1865
        [Opacity]: [{
1866
          '#text': markSymbolizer.opacity!.toString()
1867
        }]
1868
      });
1869
    }
1870

1871
    if (markSymbolizer.radius !== undefined) {
108✔
1872
      if (isGeoStylerFunction(markSymbolizer.radius)) {
104✔
1873
        graphic.push({
4✔
1874
          // TODO: Double check if we have to multiply this by 2
1875
          [Size]: geoStylerFunctionToSldFunction(markSymbolizer.radius)
1876
        });
1877
      } else {
1878
        graphic.push({
100✔
1879
          [Size]: [{
1880
            '#text': (markSymbolizer.radius * 2).toString()
1881
          }]
1882
        });
1883
      }
1884
    }
1885

1886
    if (markSymbolizer.rotate) {
108✔
1887
      graphic.push({
12✔
1888
        [Rotation]: [{
1889
          '#text': markSymbolizer.rotate.toString()
1890
        }]
1891
      });
1892
    }
1893

1894
    if (markSymbolizer.offset && this.sldVersion === '1.1.0') {
108✔
1895
      graphic.push({
2✔
1896
        [Displacement]: [{
1897
          [DisplacementX]: [{
1898
            '#text': markSymbolizer.offset[0].toString()
1899
          }]
1900
        }, {
1901
          [DisplacementY]: [{
1902
            '#text': markSymbolizer.offset[1].toString()
1903
          }]
1904
        }]
1905
      });
1906
    }
1907

1908
    return [{
108✔
1909
      [Graphic]: graphic
1910
    }];
1911
  }
1912

1913
  /**
1914
   * Push a new GeoServerVendorOption in the given array if such options are allowed.
1915
   */
1916
  pushGeoServerVendorOption(elementArray: any[], name: string, text: string) {
1917
    if (this.withGeoServerVendorOption) {
2!
1918
      elementArray.push(this.createGeoServerVendorOption(name, text));
2✔
1919
    }
1920
  }
1921

1922
  /**
1923
   * @returns <VendorOption name="name">text</VendorOption>
1924
   */
1925
  createGeoServerVendorOption(name: string, text: string) {
1926
    const VendorOption = this.getTagName('VendorOption');
2✔
1927
    return {
2✔
1928
      [VendorOption]: [{
1929
        '#text': text,
1930
      }],
1931
      ':@': {
1932
        '@_name': name,
1933
      }
1934
    };
1935
  }
1936

1937
  /**
1938
   * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style IconSymbolizer.
1939
   *
1940
   * @param iconSymbolizer A geostyler-style IconSymbolizer.
1941
   * @return The object representation of a SLD PointSymbolizer with
1942
   * an "ExternalGraphic" (readable with fast-xml-parser)
1943
   */
1944
  getSldPointSymbolizerFromIconSymbolizer(iconSymbolizer: IconSymbolizer): any {
1945
    const ExternalGraphic = this.getTagName('ExternalGraphic');
26✔
1946
    const Format = this.getTagName('Format');
26✔
1947
    const OnlineResource = this.getTagName('OnlineResource');
26✔
1948
    const Opacity = this.getTagName('Opacity');
26✔
1949
    const Rotation = this.getTagName('Rotation');
26✔
1950
    const Size = this.getTagName('Size');
26✔
1951
    const Graphic = this.getTagName('Graphic');
26✔
1952
    const Displacement = this.getTagName('Displacement');
26✔
1953
    const DisplacementX = this.getTagName('DisplacementX');
26✔
1954
    const DisplacementY = this.getTagName('DisplacementY');
26✔
1955

1956
    const graphic: any[] = [{
26✔
1957
      [ExternalGraphic]: [{
1958
        [OnlineResource]: [],
1959
        ':@': {
1960
          '@_xlink:type': 'simple',
1961
          '@_xmlns:xlink': 'http://www.w3.org/1999/xlink',
1962
          '@_xlink:href': iconSymbolizer.image
1963
        }
1964
      }, {[Format]: []}]
1965
    }];
1966

1967
    if (typeof iconSymbolizer.image === 'string' || iconSymbolizer.image instanceof String) {
26!
1968
      const iconExt = iconSymbolizer.image.split('.').pop();
26✔
1969
      switch (iconExt) {
26!
1970
        case 'png':
1971
        case 'jpeg':
1972
        case 'gif':
1973
          graphic[0][ExternalGraphic][1][Format] = [{
18✔
1974
            '#text': `image/${iconExt}`
1975
          }];
1976
          break;
18✔
1977
        case 'jpg':
UNCOV
1978
          graphic[0][ExternalGraphic][1][Format] = [{
×
1979
            '#text': 'image/jpeg'
1980
          }];
UNCOV
1981
          break;
×
1982
        case 'svg':
1983
          graphic[0][ExternalGraphic][1][Format] = [{
8✔
1984
            '#text': 'image/svg+xml'
1985
          }];
1986
          break;
8✔
1987
        default:
UNCOV
1988
          break;
×
1989
      }
1990
    }
1991

1992
    if (!isNil(iconSymbolizer.opacity)) {
26✔
1993
      graphic.push({
2✔
1994
        [Opacity]: [{
1995
          '#text': iconSymbolizer.opacity
1996
        }]
1997
      });
1998
    }
1999
    if (iconSymbolizer.size) {
26✔
2000
      graphic.push({
24✔
2001
        [Size]: [{
2002
          '#text': iconSymbolizer.size
2003
        }]
2004
      });
2005
    }
2006
    if (iconSymbolizer.rotate) {
26✔
2007
      graphic.push({
22✔
2008
        [Rotation]: [{
2009
          '#text': iconSymbolizer.rotate
2010
        }]
2011
      });
2012
    }
2013
    if (iconSymbolizer.offset && this.sldVersion === '1.1.0') {
26✔
2014
      graphic.push({
2✔
2015
        [Displacement]: [{
2016
          [DisplacementX]: [{
2017
            '#text': iconSymbolizer.offset[0].toString()
2018
          }]
2019
        }, {
2020
          [DisplacementY]: [{
2021
            '#text': iconSymbolizer.offset[1].toString()
2022
          }]
2023
        }]
2024
      });
2025
    }
2026
    return [{
26✔
2027
      [Graphic]: graphic
2028
    }];
2029
  }
2030

2031
  /**
2032
   * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style TextSymbolizer.
2033
   *
2034
   * @param textSymbolizer A geostyler-style TextSymbolizer.
2035
   * @return The object representation of a SLD TextSymbolizer (readable with fast-xml-parser)
2036
   */
2037
  getSldTextSymbolizerFromTextSymbolizer(textSymbolizer: TextSymbolizer): any {
2038
    const CssParameter = this.getTagName('CssParameter');
34✔
2039
    const Fill = this.getTagName('Fill');
34✔
2040
    const Halo = this.getTagName('Halo');
34✔
2041
    const Font = this.getTagName('Font');
34✔
2042
    const Displacement = this.getTagName('Displacement');
34✔
2043
    const DisplacementX = this.getTagName('DisplacementX');
34✔
2044
    const DisplacementY = this.getTagName('DisplacementY');
34✔
2045
    const LabelPlacement = this.getTagName('LabelPlacement');
34✔
2046
    const PointPlacement = this.getTagName('PointPlacement');
34✔
2047
    const LinePlacement = this.getTagName('LinePlacement');
34✔
2048
    const Rotation = this.getTagName('Rotation');
34✔
2049
    const Radius = this.getTagName('Radius');
34✔
2050
    const Label = this.getTagName('Label');
34✔
2051

2052
    const sldTextSymbolizer: any = [{
34✔
2053
      [Label]: textSymbolizer.label ? this.getSldLabelFromTextSymbolizer(textSymbolizer.label) : undefined
34!
2054
    }];
2055

2056
    const fontPropertyMap = {
34✔
2057
      font: 'font-family',
2058
      size: 'font-size',
2059
      fontStyle: 'font-style',
2060
      fontWeight: 'font-weight'
2061
    };
2062

2063
    const fontCssParameters: any[] = Object.keys(textSymbolizer)
34✔
2064
      .filter((property: any) => property !== 'kind' && fontPropertyMap[property as keyof typeof fontPropertyMap])
356✔
2065
      .map((property: any) => {
2066
        if (isGeoStylerFunction(textSymbolizer[property as keyof typeof fontPropertyMap])) {
114!
UNCOV
2067
          const children = geoStylerFunctionToSldFunction(textSymbolizer[
×
2068
            property as keyof typeof textSymbolizer] as GeoStylerFunction);
UNCOV
2069
          return {
×
2070
            [CssParameter]: children,
2071
            ':@': {
2072
              '@_name': fontPropertyMap[property as keyof typeof fontPropertyMap]
2073
            }
2074
          };
2075
        } else {
2076
          return {
114✔
2077
            [CssParameter]: [{
2078
              '#text': property === 'font'
114✔
2079
                ? (textSymbolizer[property as keyof typeof textSymbolizer] as any[])?.[0]
2080
                : textSymbolizer[property as keyof typeof textSymbolizer],
2081
            }],
2082
            ':@': {
2083
              '@_name': fontPropertyMap[property as keyof typeof fontPropertyMap]
2084
            }
2085
          };
2086
        }
2087
      });
2088

2089
    if (fontCssParameters.length > 0) {
34✔
2090
      sldTextSymbolizer.push({
30✔
2091
        [Font]: fontCssParameters
2092
      });
2093
    }
2094

2095
    if (textSymbolizer.placement === 'line') {
34✔
2096
      sldTextSymbolizer.push({
4✔
2097
        [LabelPlacement]: [{
2098
          [LinePlacement]: []
2099
        }]
2100
      });
2101
    } else if (Number.isFinite(textSymbolizer.offset)
30!
2102
      || textSymbolizer.rotate !== undefined
2103
      || textSymbolizer.placement === 'point'
2104
    ) {
2105
      const pointPlacement: any = [];
30✔
2106
      if (textSymbolizer.offset) {
30✔
2107
        pointPlacement.push({
22✔
2108
          [Displacement]: [{
2109
            [DisplacementX]: [{
2110
              '#text': textSymbolizer.offset[0].toString()
2111
            }]
2112
          }, {
2113
            [DisplacementY]: [{
2114
              '#text': (-textSymbolizer.offset[1]).toString()
2115
            }]
2116
          }]
2117
        });
2118
      }
2119
      if (textSymbolizer.rotate !== undefined) {
30✔
2120
        pointPlacement.push({
16✔
2121
          [Rotation]: [{
2122
            '#text': textSymbolizer.rotate.toString()
2123
          }]
2124
        });
2125
      }
2126
      sldTextSymbolizer.push({
30✔
2127
        [LabelPlacement]: [{
2128
          [PointPlacement]: pointPlacement
2129
        }]
2130
      });
2131
    }
2132

2133
    if (Number.isFinite(textSymbolizer.haloWidth) || textSymbolizer.haloColor) {
34✔
2134
      const halo: any = [];
24✔
2135
      const haloFillCssParameter = [];
24✔
2136
      if (textSymbolizer.haloWidth) {
24!
2137
        halo.push({
24✔
2138
          [Radius]: [{
2139
            '#text': textSymbolizer.haloWidth.toString()
2140
          }]
2141
        });
2142
      }
2143
      if (textSymbolizer.haloColor) {
24!
2144
        haloFillCssParameter.push({
24✔
2145
          [CssParameter]: [{
2146
            '#text': textSymbolizer.haloColor,
2147
          }],
2148
          ':@': {
2149
            '@_name': 'fill'
2150
          }
2151
        });
2152
      }
2153
      if (textSymbolizer.haloOpacity) {
24✔
2154
        haloFillCssParameter.push({
12✔
2155
          [CssParameter]: [{
2156
            '#text': textSymbolizer.haloOpacity,
2157
          }],
2158
          ':@': {
2159
            '@_name': 'fill-opacity'
2160
          }
2161
        });
2162
      }
2163
      if (haloFillCssParameter.length > 0) {
24!
2164
        halo.push({
24✔
2165
          [Fill]: haloFillCssParameter
2166
        });
2167
      }
2168
      sldTextSymbolizer.push({
24✔
2169
        [Halo]: halo
2170
      });
2171
    }
2172
    if (textSymbolizer.color || Number.isFinite(textSymbolizer.opacity)) {
34!
2173
      const fill = [{
34✔
2174
        [CssParameter]: [{
2175
          '#text': textSymbolizer.color || '#000000',
34!
2176
        }],
2177
        ':@': {
2178
          '@_name': 'fill'
2179
        }
2180
      }];
2181
      if (Number.isFinite(textSymbolizer.opacity)) {
34✔
2182
        fill.push({
8✔
2183
          [CssParameter]: [{
2184
            '#text': `${textSymbolizer.opacity}`,
2185
          }],
2186
          ':@': {
2187
            '@_name': 'fill-opacity'
2188
          },
2189
        });
2190
      }
2191
      sldTextSymbolizer.push({
34✔
2192
        [Fill]: fill
2193
      });
2194
    }
2195

2196
    return sldTextSymbolizer;
34✔
2197
  }
2198

2199
  /**
2200
   * Get the Label from a TextSymbolizer
2201
   *
2202
   * @param template The Expression<string> representing the label
2203
   */
2204
  getSldLabelFromTextSymbolizer = (template: Expression<string>): any => {
534✔
2205
    // TODO: parse GeoStylerFunction
2206
    if (!(typeof template === 'string' || template instanceof String)) {
34!
UNCOV
2207
      return undefined;
×
2208
    }
2209

2210
    const openingBraces = '{{';
34✔
2211
    const closingBraces = '}}';
34✔
2212

2213
    const tokens = [];
34✔
2214
    let templateReducer = template;
34✔
2215

2216
    while (templateReducer.length) {
34✔
2217
      let tmpTemplateReducer = templateReducer;
50✔
2218
      let tmpPreTemplateLiteral;
2219
      const openingBracesIdx = tmpTemplateReducer.indexOf(openingBraces);
50✔
2220
      if (openingBracesIdx === -1) {
50✔
2221
        if (templateReducer.includes(' ')) {
8✔
2222
          tokens.push({
4✔
2223
            'ogc:Literal': [{
2224
              '#cdata': [{
2225
                '#text': templateReducer
2226
              }]
2227
            }]
2228
          });
2229
        } else {
2230
          tokens.push({
4✔
2231
            'ogc:Literal': [{
2232
              '#text': templateReducer
2233
            }]
2234
          });
2235
        }
2236
        break;
8✔
2237
      }
2238

2239
      if (openingBracesIdx > 0) {
42✔
2240
        tmpPreTemplateLiteral = tmpTemplateReducer.slice(0, openingBracesIdx);
16✔
2241
      }
2242
      tmpTemplateReducer = tmpTemplateReducer.slice(openingBracesIdx + openingBraces.length);
42✔
2243

2244
      const closingBracesIdx = tmpTemplateReducer.indexOf(closingBraces);
42✔
2245
      if (closingBracesIdx === -1) {
42✔
2246
        if (templateReducer.includes(' ')) {
4!
2247
          tokens.push({
4✔
2248
            'ogc:Literal': [{
2249
              '#cdata': [{
2250
                '#text': templateReducer
2251
              }]
2252
            }]
2253
          });
2254
        } else {
UNCOV
2255
          tokens.push({
×
2256
            'ogc:Literal': [{
2257
              '#text': templateReducer
2258
            }]
2259
          });
2260
        }
2261
        break;
4✔
2262
      }
2263
      const propertyName = tmpTemplateReducer.slice(0, closingBracesIdx);
38✔
2264
      tmpTemplateReducer = tmpTemplateReducer.slice(closingBracesIdx + closingBraces.length);
38✔
2265
      if (tmpPreTemplateLiteral) {
38✔
2266
        if (tmpPreTemplateLiteral.includes(' ')) {
12!
2267
          tokens.push({
12✔
2268
            'ogc:Literal': [{
2269
              '#cdata': [{
2270
                '#text': tmpPreTemplateLiteral
2271
              }]
2272
            }]
2273
          });
2274
        } else {
UNCOV
2275
          tokens.push({
×
2276
            'ogc:Literal': [{
2277
              '#text': tmpPreTemplateLiteral
2278
            }]
2279
          });
2280
        }
2281
      }
2282
      tokens.push({
38✔
2283
        'ogc:PropertyName': [{
2284
          '#text': propertyName
2285
        }]
2286
      });
2287
      templateReducer = tmpTemplateReducer;
38✔
2288
    }
2289

2290
    return tokens;
34✔
2291
  };
2292

2293
  /**
2294
   * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style LineSymbolizer.
2295
   *
2296
   * @param lineSymbolizer A geostyler-style LineSymbolizer.
2297
   * @return The object representation of a SLD LineSymbolizer (readable with fast-xml-parser)
2298
   */
2299
  getSldLineSymbolizerFromLineSymbolizer(lineSymbolizer: LineSymbolizer): any[] {
2300
    const CssParameter = this.getTagName('CssParameter');
46✔
2301
    const Stroke = this.getTagName('Stroke');
46✔
2302
    const GraphicStroke = this.getTagName('GraphicStroke');
46✔
2303
    const GraphicFill = this.getTagName('GraphicFill');
46✔
2304
    const PerpendicularOffset = this.getTagName('PerpendicularOffset');
46✔
2305

2306
    const propertyMap = {
46✔
2307
      color: 'stroke',
2308
      width: 'stroke-width',
2309
      opacity: 'stroke-opacity',
2310
      join: 'stroke-linejoin',
2311
      cap: 'stroke-linecap',
2312
      dasharray: 'stroke-dasharray',
2313
      dashOffset: 'stroke-dashoffset'
2314
    };
2315

2316
    const sldLineSymbolizer: any = [];
46✔
2317

2318
    const cssParameters: any[] = Object.keys(lineSymbolizer)
46✔
2319
      .filter((property: any) => property !== 'kind' && propertyMap[property as keyof typeof propertyMap] &&
230✔
2320
        lineSymbolizer[property as keyof typeof lineSymbolizer] !== undefined &&
2321
        lineSymbolizer[property as keyof typeof lineSymbolizer] !== null)
2322
      .map((property: any) => {
2323
        let value = lineSymbolizer[property as keyof typeof lineSymbolizer];
164✔
2324
        if (property === 'dasharray') {
164✔
2325
          value = lineSymbolizer.dasharray ? lineSymbolizer.dasharray.join(' ') : undefined;
28!
2326

2327
          return {
28✔
2328
            [CssParameter]: [{
2329
              '#text': value,
2330
            }],
2331
            ':@': {
2332
              '@_name': propertyMap[property as keyof typeof propertyMap]
2333
            }
2334
          };
2335
        }
2336
        // simple transformation since geostyler-style uses prop 'miter' whereas sld uses 'mitre'
2337
        if (property === 'join' && value === 'miter') {
136✔
2338
          value = 'mitre';
24✔
2339
        }
2340

2341
        if (isGeoStylerFunction(lineSymbolizer[property as keyof typeof lineSymbolizer])) {
136!
UNCOV
2342
          const children = geoStylerFunctionToSldFunction(lineSymbolizer[
×
2343
            property as keyof typeof lineSymbolizer] as GeoStylerFunction);
UNCOV
2344
          return {
×
2345
            [CssParameter]: children,
2346
            ':@': {
2347
              '@_name': propertyMap[property as keyof typeof propertyMap]
2348
            }
2349
          };
2350
        } else {
2351
          return {
136✔
2352
            [CssParameter]: [{
2353
              '#text': lineSymbolizer[property as keyof typeof lineSymbolizer],
2354
            }],
2355
            ':@': {
2356
              '@_name': propertyMap[property as keyof typeof propertyMap]
2357
            }
2358
          };
2359
        }
2360

2361
      });
2362

2363
    if (lineSymbolizer?.graphicStroke) {
46✔
2364
      if (!Array.isArray(sldLineSymbolizer?.[0]?.[Stroke])) {
8!
2365
        sldLineSymbolizer[0] = { [Stroke]: [] };
8✔
2366
      }
2367
      if (lineSymbolizer?.graphicStroke?.kind === 'Mark') {
8✔
2368
        const graphicStroke = this.getSldPointSymbolizerFromMarkSymbolizer(lineSymbolizer.graphicStroke);
4✔
2369
        sldLineSymbolizer[0][Stroke].push({
4✔
2370
          [GraphicStroke]: graphicStroke
2371
        });
2372
      } else if (lineSymbolizer?.graphicStroke?.kind === 'Icon') {
4!
2373
        const graphicStroke = this.getSldPointSymbolizerFromIconSymbolizer(lineSymbolizer.graphicStroke);
4✔
2374
        sldLineSymbolizer[0][Stroke].push({
4✔
2375
          [GraphicStroke]: graphicStroke
2376
        });
2377
      }
2378
    }
2379

2380
    if (lineSymbolizer?.graphicFill) {
46✔
2381
      if (!Array.isArray(sldLineSymbolizer?.[0]?.[Stroke])) {
8!
2382
        sldLineSymbolizer[0] = { [Stroke]: [] };
8✔
2383
      }
2384
      if (lineSymbolizer?.graphicFill?.kind === 'Mark') {
8✔
2385
        const graphicFill = this.getSldPointSymbolizerFromMarkSymbolizer(lineSymbolizer.graphicFill);
4✔
2386
        sldLineSymbolizer[0][Stroke].push({
4✔
2387
          [GraphicFill]: graphicFill
2388
        });
2389
      } else if (lineSymbolizer?.graphicFill?.kind === 'Icon') {
4!
2390
        const graphicFill = this.getSldPointSymbolizerFromIconSymbolizer(lineSymbolizer.graphicFill);
4✔
2391
        sldLineSymbolizer[0][Stroke].push({
4✔
2392
          [GraphicFill]: graphicFill
2393
        });
2394
      }
2395
    }
2396

2397
    if (cssParameters.length !== 0) {
46✔
2398
      if (!Array.isArray(sldLineSymbolizer?.[0]?.[Stroke])) {
44✔
2399
        sldLineSymbolizer[0] = { [Stroke]: [] };
28✔
2400
      }
2401
      sldLineSymbolizer[0][Stroke].push(...cssParameters);
44✔
2402
    }
2403
    if (lineSymbolizer.perpendicularOffset) {
46✔
2404
      sldLineSymbolizer.push({
4✔
2405
        [PerpendicularOffset]: [
2406
          {
2407
            '#text': lineSymbolizer.perpendicularOffset
2408
          }
2409
        ]
2410
      });
2411
    }
2412

2413
    if (sldLineSymbolizer.length === 0) {
46✔
2414
      sldLineSymbolizer.push({
2✔
2415
        [Stroke]: {}
2416
      });
2417
    }
2418

2419
    return sldLineSymbolizer;
46✔
2420
  }
2421

2422
  /**
2423
   * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style FillSymbolizer.
2424
   *
2425
   * @param fillSymbolizer A geostyler-style FillSymbolizer.
2426
   * @return The object representation of a SLD PolygonSymbolizer (readable with fast-xml-parser)
2427
   */
2428
  getSldPolygonSymbolizerFromFillSymbolizer(fillSymbolizer: FillSymbolizer): any {
2429
    const CssParameter = this.getTagName('CssParameter');
56✔
2430
    const Stroke = this.getTagName('Stroke');
56✔
2431
    const Fill = this.getTagName('Fill');
56✔
2432
    const strokePropertyMap = {
56✔
2433
      outlineColor: 'stroke',
2434
      outlineWidth: 'stroke-width',
2435
      outlineOpacity: 'stroke-opacity',
2436
      outlineDasharray: 'stroke-dasharray',
2437
      outlineCap: 'stroke-linecap',
2438
      outlineJoin: 'stroke-linejoin'
2439
    };
2440
    const fillPropertyMap = {
56✔
2441
      color: 'fill',
2442
      fillOpacity: 'fill-opacity'
2443
    };
2444
    const strokeCssParameters: any[] = [];
56✔
2445
    const fillCssParameters: any[] = [];
56✔
2446
    let graphicFill: any;
2447

2448
    if (fillSymbolizer?.graphicFill) {
56✔
2449
      if (fillSymbolizer?.graphicFill.kind === 'Mark') {
18✔
2450
        graphicFill = this.getSldPointSymbolizerFromMarkSymbolizer(fillSymbolizer.graphicFill);
12✔
2451
      } else if (fillSymbolizer?.graphicFill.kind === 'Icon') {
6!
2452
        graphicFill = this.getSldPointSymbolizerFromIconSymbolizer(fillSymbolizer.graphicFill);
6✔
2453
      }
2454
    }
2455

2456
    Object.keys(fillSymbolizer)
56✔
2457
      .filter((property: any) => property !== 'kind')
224✔
2458
      .filter((property: any) => fillSymbolizer[property as keyof typeof fillSymbolizer] !== undefined &&
168✔
2459
        fillSymbolizer[property as keyof typeof fillSymbolizer] !== null)
2460
      .forEach((property: any) => {
2461
        if (Object.keys(fillPropertyMap).includes(property)) {
168✔
2462
          if (isGeoStylerFunction(fillSymbolizer[property as keyof typeof fillSymbolizer])) {
68!
UNCOV
2463
            const children = geoStylerFunctionToSldFunction(fillSymbolizer[
×
2464
              property as keyof typeof fillSymbolizer] as GeoStylerFunction);
UNCOV
2465
            fillCssParameters.push({
×
2466
              [CssParameter]: children,
2467
              ':@': {
2468
                '@_name': fillPropertyMap[property as keyof typeof fillPropertyMap]
2469
              }
2470
            });
2471
          } else {
2472
            fillCssParameters.push({
68✔
2473
              [CssParameter]: [{
2474
                '#text': fillSymbolizer[property as keyof typeof fillSymbolizer],
2475
              }],
2476
              ':@': {
2477
                '@_name': fillPropertyMap[property as keyof typeof fillPropertyMap]
2478
              }
2479
            });
2480
          }
2481
        } else if (Object.keys(strokePropertyMap).includes(property)) {
100✔
2482

2483
          let transformedValue: string = '';
76✔
2484

2485
          if (property === 'outlineDasharray') {
76✔
2486
            const paramValue: number[] = fillSymbolizer[property as keyof typeof fillSymbolizer] as number[];
12✔
2487
            transformedValue = '';
12✔
2488
            paramValue.forEach((dash: number, idx) => {
12✔
2489
              transformedValue += dash;
24✔
2490
              if (idx < paramValue.length - 1) {
24✔
2491
                transformedValue += ' ';
12✔
2492
              }
2493
            });
2494
          } else if (property === 'outlineWidth') {
64✔
2495
            transformedValue = fillSymbolizer[property as keyof typeof fillSymbolizer] + '';
24✔
2496
          } else if (property === 'outlineOpacity') {
40✔
2497
            transformedValue = fillSymbolizer[property as keyof typeof fillSymbolizer] + '';
6✔
2498
          } else {
2499
            (transformedValue as any) = fillSymbolizer[property as keyof typeof fillSymbolizer];
34✔
2500
          }
2501

2502
          if (isGeoStylerFunction(fillSymbolizer[property as keyof typeof fillSymbolizer])) {
76✔
2503
            const children = geoStylerFunctionToSldFunction((fillSymbolizer as any)[property]);
4✔
2504
            strokeCssParameters.push({
4✔
2505
              [CssParameter]: children,
2506
              ':@': {
2507
                '@_name': strokePropertyMap[property as keyof typeof strokePropertyMap]
2508
              }
2509
            });
2510
          } else {
2511
            strokeCssParameters.push({
72✔
2512
              [CssParameter]: [{
2513
                '#text': transformedValue,
2514
              }],
2515
              ':@': {
2516
                '@_name': strokePropertyMap[property as keyof typeof strokePropertyMap]
2517
              }
2518
            });
2519
          }
2520

2521
        }
2522
      });
2523

2524
    const polygonSymbolizer: any = [];
56✔
2525
    if (fillCssParameters.length > 0 || graphicFill) {
56!
2526
      const fillArray: any[] = [];
56✔
2527
      const graphicFillPadding = fillSymbolizer.graphicFillPadding;
56✔
2528
      if (graphicFillPadding) {
56✔
2529
        this.pushGeoServerVendorOption(polygonSymbolizer, 'graphic-margin', `${graphicFillPadding}`);
2✔
2530
      }
2531
      polygonSymbolizer.push({ [Fill]: fillArray });
56✔
2532
      if (fillCssParameters.length > 0) {
56✔
2533
        fillArray.push(...fillCssParameters);
46✔
2534
      }
2535
      if (graphicFill) {
56✔
2536
        fillArray.push({ GraphicFill: graphicFill });
18✔
2537
      }
2538
    }
2539

2540
    if (strokeCssParameters.length > 0) {
56✔
2541
      polygonSymbolizer.push({
34✔
2542
        [Stroke]: strokeCssParameters
2543
      });
2544
    }
2545

2546
    return polygonSymbolizer;
56✔
2547
  }
2548

2549
  /**
2550
   * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style RasterSymbolizer.
2551
   *
2552
   * @param rasterSymbolizer A geostyler-style RasterSymbolizer.
2553
   * @return The object representation of a SLD RasterSymbolizer (readable with fast-xml-parser)
2554
   */
2555
  getSldRasterSymbolizerFromRasterSymbolizer(rasterSymbolizer: RasterSymbolizer): any {
2556
    const sldRasterSymbolizer: any = [];
14✔
2557
    if (rasterSymbolizer.opacity !== undefined) {
14!
2558
      sldRasterSymbolizer.push({});
14✔
2559
      sldRasterSymbolizer[0].Opacity = [{
14✔
2560
        '#text': rasterSymbolizer.opacity
2561
      }];
2562
    }
2563

2564
    let colorMap: any;
2565
    if (rasterSymbolizer.colorMap) {
14✔
2566
      colorMap = this.getSldColorMapFromColorMap(rasterSymbolizer.colorMap);
8✔
2567
      if (colorMap?.[0]) {
8!
2568
        sldRasterSymbolizer.push({
8✔
2569
          ColorMap: colorMap
2570
        });
2571
      }
2572
    }
2573

2574
    let channelSelection: any;
2575
    if (rasterSymbolizer.channelSelection) {
14✔
2576
      channelSelection = this.getSldChannelSelectionFromChannelSelection(rasterSymbolizer.channelSelection);
4✔
2577
      if (channelSelection?.[0]) {
4!
2578
        sldRasterSymbolizer.push({
4✔
2579
          ChannelSelection: channelSelection
2580
        });
2581
      }
2582
    }
2583

2584
    let contrastEnhancement: any;
2585
    if (rasterSymbolizer.contrastEnhancement) {
14✔
2586
      contrastEnhancement = this.getSldContrastEnhancementFromContrastEnhancement(rasterSymbolizer.contrastEnhancement);
4✔
2587
      if (contrastEnhancement?.[0]) {
4!
2588
        sldRasterSymbolizer.push({
4✔
2589
          ContrastEnhancement: contrastEnhancement
2590
        });
2591
      }
2592
    }
2593

2594
    return sldRasterSymbolizer;
14✔
2595
  }
2596

2597
  /**
2598
   * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style ColorMap.
2599
   *
2600
   * @param colorMap A geostyler-style ColorMap.
2601
   * @return The object representation of a SLD ColorMap (readable with fast-xml-parser)
2602
   */
2603
  getSldColorMapFromColorMap(colorMap: ColorMap): any {
2604
    const sldColorMap: any[] = [];
8✔
2605
    // parse colorMap.type
2606
    if (colorMap.type) {
8!
2607
      const type = colorMap.type;
8✔
2608
      (sldColorMap as any)[':@'] = {
8✔
2609
        '@_type': type
2610
      };
2611
    }
2612
    // parse colorMap.extended
2613
    if (colorMap.extended !== undefined) {
8!
UNCOV
2614
      const extended = colorMap.extended.toString();
×
UNCOV
2615
      if (!(sldColorMap as any)[':@']) {
×
UNCOV
2616
        (sldColorMap as any)[':@'] = {};
×
2617
      }
UNCOV
2618
      (sldColorMap as any)[':@']['@_extended'] = extended;
×
2619
    }
2620
    // parse colorMap.colorMapEntries
2621
    if (colorMap.colorMapEntries && colorMap.colorMapEntries.length > 0) {
8!
2622
      const colorMapEntries: any[] = colorMap.colorMapEntries.map((entry: ColorMapEntry) => {
8✔
2623
        const result: any = {
28✔
2624
          ColorMapEntry: [],
2625
          ':@': {}
2626
        };
2627
        if (entry.color) {
28!
2628
          result[':@']['@_color'] = entry.color;
28✔
2629
        }
2630
        if (typeof entry.quantity !== 'undefined') {
28!
2631
          result[':@']['@_quantity'] = entry.quantity.toString();
28✔
2632
        }
2633
        if (entry.label) {
28✔
2634
          result[':@']['@_label'] = entry.label;
18✔
2635
        }
2636
        if (typeof entry.opacity !== 'undefined') {
28✔
2637
          result[':@']['@_opacity'] = entry.opacity.toString();
8✔
2638
        }
2639
        return result;
28✔
2640
        // remove empty colorMapEntries
2641
      }).filter((entry: any) => Object.keys(entry).length > 0);
28✔
2642
      sldColorMap.push(...colorMapEntries);
8✔
2643
    }
2644
    return sldColorMap;
8✔
2645
  }
2646

2647
  /**
2648
   * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style ChannelSelection.
2649
   *
2650
   * @param channelSelection A geostyler-style ChannelSelection.
2651
   * @return The object representation of a SLD ChannelSelection (readable with fast-xml-parser)
2652
   */
2653
  getSldChannelSelectionFromChannelSelection(channelSelection: ChannelSelection): any {
2654
    const propertyMap = {
4✔
2655
      redChannel: 'RedChannel',
2656
      blueChannel: 'BlueChannel',
2657
      greenChannel: 'GreenChannel',
2658
      grayChannel: 'GrayChannel'
2659
    };
2660
    const keys = Object.keys(channelSelection);
4✔
2661
    const sldChannelSelection: any[] = [];
4✔
2662
    keys.forEach((key: string) => {
4✔
2663
      const channel: any = [];
12✔
2664
      // parse sourceChannelName
2665
      const sourceChannelName = (channelSelection?.[key as keyof ChannelSelection] as any)?.sourceChannelName;
12✔
2666
      const channelName = propertyMap[key as keyof typeof propertyMap];
12✔
2667
      // parse contrastEnhancement
2668
      const contrastEnhancement = (channelSelection?.[key as keyof ChannelSelection] as any)?.contrastEnhancement;
12✔
2669
      if (sourceChannelName || contrastEnhancement) {
12!
2670
        if (sourceChannelName) {
12!
2671
          channel.push({
12✔
2672
            SourceChannelName: [{
2673
              '#text': sourceChannelName
2674
            }]
2675
          });
2676
        }
2677
        if (contrastEnhancement) {
12✔
2678
          channel.push({
8✔
2679
            ContrastEnhancement: this.getSldContrastEnhancementFromContrastEnhancement(contrastEnhancement)
2680
          });
2681
        }
2682
        sldChannelSelection.push({
12✔
2683
          [channelName]: channel
2684
        });
2685
      }
2686
    });
2687

2688
    return sldChannelSelection;
4✔
2689
  }
2690

2691
  /**
2692
     * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style ContrastEnhancement.
2693
     *
2694
     * @param contrastEnhancement A geostyler-style ContrastEnhancement.
2695
     * @return The object representation of a SLD ContrastEnhancement (readable with fast-xml-parser)
2696
     */
2697
  getSldContrastEnhancementFromContrastEnhancement(contrastEnhancement: ContrastEnhancement): any {
2698
    const sldContrastEnhancement: any = [];
12✔
2699
    const enhancementType = contrastEnhancement?.enhancementType;
12✔
2700
    if (enhancementType === 'normalize') {
12✔
2701
      // parse normalize
2702
      sldContrastEnhancement.push({
4✔
2703
        Normalize: []
2704
      });
2705
    } else if (enhancementType === 'histogram') {
8!
2706
      // parse histogram
2707
      sldContrastEnhancement.push({
8✔
2708
        Histogram: []
2709
      });
2710
    }
2711
    // parse gammaValue
2712
    if (contrastEnhancement.gammaValue !== undefined) {
12✔
2713
      sldContrastEnhancement.push({
4✔
2714
        GammaValue: [{
2715
          '#text': contrastEnhancement.gammaValue
2716
        }]
2717
      });
2718
    }
2719
    return sldContrastEnhancement;
12✔
2720
  }
2721

2722
  checkForUnsupportedProperties(geoStylerStyle: Style): UnsupportedProperties | undefined {
2723
    const capitalizeFirstLetter = (a: string) => a[0].toUpperCase() + a.slice(1);
250✔
2724
    const unsupportedProperties: UnsupportedProperties = {};
208✔
2725
    geoStylerStyle.rules.forEach(rule => {
208✔
2726
      // ScaleDenominator and Filters are completely supported so we just check for symbolizers
2727
      rule.symbolizers.forEach(symbolizer => {
240✔
2728
        const key = capitalizeFirstLetter(`${symbolizer.kind}Symbolizer`);
250✔
2729
        const value = (this.unsupportedProperties?.Symbolizer as any)?.[key];
250✔
2730
        if (value) {
250!
2731
          if (typeof value === 'string' || value instanceof String) {
250!
UNCOV
2732
            if (!unsupportedProperties.Symbolizer) {
×
UNCOV
2733
              unsupportedProperties.Symbolizer = {};
×
2734
            }
UNCOV
2735
            (unsupportedProperties.Symbolizer as any)[key] = value;
×
2736
          } else {
2737
            Object.keys(symbolizer).forEach(property => {
250✔
2738
              if (value[property]) {
1,458✔
2739
                if (!unsupportedProperties.Symbolizer) {
54✔
2740
                  unsupportedProperties.Symbolizer = {};
52✔
2741
                }
2742
                if (!unsupportedProperties.Symbolizer[key as keyof typeof unsupportedProperties.Symbolizer]) {
54✔
2743
                  (unsupportedProperties.Symbolizer as any)[key] = {};
52✔
2744
                }
2745
                (unsupportedProperties.Symbolizer as any)[key][property] = value[property];
54✔
2746
              }
2747
            });
2748
          }
2749
        }
2750
      });
2751
    });
2752
    if (Object.keys(unsupportedProperties).length > 0) {
208✔
2753
      return unsupportedProperties;
52✔
2754
    }
2755
    return undefined;
156✔
2756
  }
2757

2758
}
2759

2760
export default SldStyleParser;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc