UNPKG

geostyler-sld-parser

Version:
1,247 lines 105 kB
import { isCombinationFilter, isComparisonFilter, isGeoStylerFunction, isGeoStylerNumberFunction, isNegationFilter, } from 'geostyler-style'; import { XMLBuilder, XMLParser } from 'fast-xml-parser'; import { merge } from 'lodash'; import { geoStylerFunctionToSldFunction, get, getAttribute, getChildren, getParameterValue, getVendorOptionValue, isSymbolizer, keysByValue, numberExpression } from './Util/SldUtil'; const SLD_VERSIONS = ['1.0.0', '1.1.0']; /** GeoServer allows VendorOptions and mix some SLD versions */ export const sldEnvGeoServer = 'GeoServer'; const SLD_ENVIRONMENTS = [sldEnvGeoServer]; const WELLKNOWNNAME_TTF_REGEXP = /^ttf:\/\/(.+)#(.+)$/; const COMPARISON_MAP = { PropertyIsEqualTo: '==', PropertyIsNotEqualTo: '!=', PropertyIsLike: '*=', PropertyIsLessThan: '<', PropertyIsLessThanOrEqualTo: '<=', PropertyIsGreaterThan: '>', PropertyIsGreaterThanOrEqualTo: '>=', PropertyIsNull: '==', PropertyIsBetween: '<=x<=' }; const NEGATION_OPERATOR_MAP = { Not: '!' }; const COMBINATION_MAP = { And: '&&', Or: '||', PropertyIsBetween: '&&' }; const ARITHMETIC_OPERATORS = [ 'add', 'sub', 'mul', 'div', ]; export const defaultTranslations = { en: { marksymbolizerParseFailedUnknownWellknownName: ({ wellKnownName }) => `MarkSymbolizer cannot be parsed. WellKnownName ${wellKnownName} is not supported.`, noFilterDetected: 'No Filter detected.', symbolizerKindParseFailed: ({ sldSymbolizerName }) => `Failed to parse SymbolizerKind ${sldSymbolizerName} from SldRule.`, colorMapEntriesParseFailedColorUndefined: 'Cannot parse ColorMapEntries. color is undefined.', contrastEnhancParseFailedHistoAndNormalizeMutuallyExclusive: 'Cannot parse ContrastEnhancement. Histogram and Normalize are mutually exclusive.', channelSelectionParseFailedRGBAndGrayscaleMutuallyExclusive: 'Cannot parse ChannelSelection. RGB and Grayscale are mutually exclusive.', channelSelectionParseFailedRGBChannelsUndefined: 'Cannot parse ChannelSelection. Red, Green and Blue channels must be defined.' }, de: {}, fr: { marksymbolizerParseFailedUnknownWellknownName: ({ wellKnownName }) => `Échec de lecture du symbole de type MarkSymbolizer. Le WellKnownName ${wellKnownName} n'est pas supporté.`, noFilterDetected: 'Aucun filtre détecté.', symbolizerKindParseFailed: ({ sldSymbolizerName }) => `Échec de lecture du type de symbole ${sldSymbolizerName} à partir de SldRule.`, colorMapEntriesParseFailedColorUndefined: 'Lecture de ColorMapEntries échoué. color n\'est pas défini.', contrastEnhancParseFailedHistoAndNormalizeMutuallyExclusive: 'Échec de lecture des propriétés de contraste ContrastEnhancement échoué. ' + 'Histogram et Normalize sont mutuellement exclusifs.', channelSelectionParseFailedRGBAndGrayscaleMutuallyExclusive: 'Échec de lecture de la sélection de canaux ChannelSelection. ' + 'RGB et Grayscale sont mutuellement exclusifs.', channelSelectionParseFailedRGBChannelsUndefined: 'Échec de lecture de la sélection de canaux ChannelSelection. ' + 'Les canaux Rouge, Vert et Bleu doivent être définis.', }, }; /** * @returns true if the provided value is null or undefined. Returns false otherwise. */ const isNil = (val) => val === undefined || val === null; /** * This parser can be used with the GeoStyler. * It implements the geostyler-style StyleParser interface. * * @class SldStyleParser * @implements StyleParser */ export class SldStyleParser { /** * The name of the SLD Style Parser. */ static title = 'SLD Style Parser'; title = 'SLD Style Parser'; unsupportedProperties = { Symbolizer: { MarkSymbolizer: { avoidEdges: 'none', blur: 'none', offset: { support: 'partial', info: 'Only supported for SLD Version 1.1.0' }, offsetAnchor: 'none', pitchAlignment: 'none', pitchScale: 'none', visibility: 'none' }, FillSymbolizer: { antialias: 'none', opacity: { support: 'none', info: 'General opacity is not supported. Use fillOpacity and strokeOpacity instead.' }, visibility: 'none' }, IconSymbolizer: { allowOverlap: 'none', anchor: 'none', avoidEdges: 'none', color: 'none', haloBlur: 'none', haloColor: 'none', haloOpacity: 'none', haloWidth: 'none', keepUpright: 'none', offset: { support: 'partial', info: 'Only supported for SLD Version 1.1.0' }, offsetAnchor: 'none', optional: 'none', padding: 'none', pitchAlignment: 'none', rotationAlignment: 'none', textFit: 'none', image: { support: 'partial', info: 'Sprites are not supported' }, textFitPadding: 'none', visibility: 'none' }, LineSymbolizer: { blur: 'none', gapWidth: 'none', gradient: 'none', miterLimit: 'none', roundLimit: 'none', spacing: 'none', visibility: 'none' }, RasterSymbolizer: { brightnessMax: 'none', brightnessMin: 'none', contrast: 'none', fadeDuration: 'none', hueRotate: 'none', resampling: 'none', saturation: 'none', visibility: 'none' }, TextSymbolizer: { placement: { support: 'partial', info: 'Only "line" and "point" are currently supported' } } } }; translations = defaultTranslations; locale = 'en'; constructor(opts) { this.parser = new XMLParser({ ...opts?.parserOptions, // Fixed attributes ignoreDeclaration: true, removeNSPrefix: true, ignoreAttributes: false, preserveOrder: true, trimValues: true }); this.builder = new XMLBuilder({ ...opts?.builderOptions, // Fixed attributes cdataPropName: '#cdata', ignoreAttributes: false, suppressEmptyNode: true, preserveOrder: true }); if (opts?.sldVersion) { this.sldVersion = opts?.sldVersion; } if (opts?.sldEnvironment !== undefined) { this.sldEnvironment = opts.sldEnvironment; } if (opts?.locale) { this.locale = opts.locale; } if (opts?.translations) { this.translations = merge(this.translations, opts.translations); } Object.assign(this, opts); } translate(key, params) { const trans = this.translations?.[this.locale]?.[key] ?? key; if (typeof trans === 'function') { return trans(params); } return trans; } _parser; get parser() { return this._parser; } set parser(parser) { this._parser = parser; } _builder; get builder() { return this._builder; } set builder(builder) { this._builder = builder; } /** * Array of field / property names in a filter, which are casted to numerics * while parsing a SLD. */ _numericFilterFields = []; /** * Getter for _numericFilterFields * @return The numericFilterFields */ get numericFilterFields() { return this._numericFilterFields; } /** * Setter for _numericFilterFields * @param numericFilterFields The numericFilterFields to set */ set numericFilterFields(numericFilterFields) { this._numericFilterFields = numericFilterFields; } /** * Array of field / property names in a filter, which are casted to boolean * while parsing a SLD. */ _boolFilterFields = []; /** * Getter for _boolFilterFields * @return The boolFilterFields */ get boolFilterFields() { return this._boolFilterFields; } /** * Setter for _boolFilterFields * @param boolFilterFields The boolFilterFields to set */ set boolFilterFields(boolFilterFields) { this._boolFilterFields = boolFilterFields; } /** * String indicating the SLD version to use. 1.1.0 will make use of * Symbology Encoding. */ _sldVersion = '1.0.0'; /** * Getter for _sldVersion * @return */ get sldVersion() { return this._sldVersion; } /** * Setter for _sldVersion * @param sldVersion The _sldVersion value to set */ set sldVersion(sldVersion) { this._sldVersion = sldVersion; } /** * Indicate the sld environment to parse the SLD or write the SLD. * This allows or restrict some SLD tags. * @private */ _sldEnvironment = null; /** * Getter for _sldEnvironment * @return SldEnvironment or null. */ get sldEnvironment() { return this._sldEnvironment; } /** * Setter for _sldEnvironment * @param a SldEnvironment or null. */ set sldEnvironment(env) { this._sldEnvironment = env; } /** * Check if the given SldEnvironment match the current environment. * @param env the SldEnvironment to check. * @private */ isSldEnv(env) { return this.sldEnvironment === env; } /** * String indicating the SLD version used in reading mode */ _readingSldVersion = '1.0.0'; /** * Getter for _readingSldVersion * @return */ get readingSldVersion() { return this._readingSldVersion; } /** * Setter for _readingSldVersion * @param sldVersion The _readingSldVersion value to set */ set readingSldVersion(sldVersion) { this._readingSldVersion = sldVersion; } /** * Used to add a `uom` attribute to the symbolizer tag. Can be one of * `metre`, `foot` or `pixel`. Defaults to pixel. */ _symbolizerUnits = 'pixel'; /** * Getter for _symbolizerUnits * @return {string} */ get symbolizerUnits() { return this._symbolizerUnits; } /** * Setter for _symbolizerUnits * @param {string} symbolizerUnits The _symbolizerUnits value to set */ set symbolizerUnits(symbolizerUnits) { this._symbolizerUnits = symbolizerUnits; } /** * The readStyle implementation of the geostyler-style StyleParser interface. * It reads a SLD as a string and returns a Promise. * The Promise itself resolves with an object containing the geostyler-style. * * @param sldString A SLD as a string. * @return The Promise resolving with an object containing the geostyler-style. */ readStyle(sldString) { return new Promise((resolve) => { try { const sldObject = this.parser.parse(sldString); const version = getAttribute(sldObject[0], 'version'); if (!SLD_VERSIONS.includes(version)) { throw new Error(`SLD version must be ${SLD_VERSIONS.toString()}`); } this._readingSldVersion = version; const geoStylerStyle = this.sldObjectToGeoStylerStyle(sldObject); resolve({ output: geoStylerStyle }); } catch (error) { resolve({ errors: [error] }); } }); } /** * Get the geostyler-style from a SLD Object (created with fast-xml-parser). * * @param sldObject The SLD object representation (created with fast-xml-parser) * @return The geostyler-style */ sldObjectToGeoStylerStyle(sldObject) { const rules = this.getRulesFromSldObject(sldObject); const name = this.getStyleNameFromSldObject(sldObject); return { name, rules }; } /** * Get the geostyler-style rules from a SLD Object (created with fast-xml-parser). * * @param sldObject The SLD object representation (created with fast-xml-parser) * @return The geostyler-style rules */ getRulesFromSldObject(sldObject) { const layers = getChildren(sldObject[0].StyledLayerDescriptor, 'NamedLayer'); const rules = []; layers.forEach(({ NamedLayer: layer }) => { getChildren(layer, 'UserStyle').forEach(({ UserStyle: userStyle }) => { getChildren(userStyle, 'FeatureTypeStyle').forEach(({ FeatureTypeStyle: featureTypeStyle }) => { getChildren(featureTypeStyle, 'Rule').forEach(({ Rule: sldRule }) => { const filter = this.getFilterFromRule(sldRule); const scaleDenominator = this.getScaleDenominatorFromRule(sldRule); const symbolizers = this.getSymbolizersFromRule(sldRule); const ruleTitle = get(sldRule, 'Title.#text'); const ruleName = get(sldRule, 'Name.#text'); const name = ruleTitle !== undefined ? ruleTitle : (ruleName !== undefined ? ruleName : ''); const rule = { name }; if (filter) { rule.filter = filter; } if (scaleDenominator) { rule.scaleDenominator = scaleDenominator; } if (symbolizers) { rule.symbolizers = symbolizers; } rules.push(rule); }); }); }); }); return rules; } /** * Get the name for the Style from the SLD Object. Returns the Title of the UserStyle * if defined or the Name of the NamedLayer if defined or an empty string. * * @param sldObject The SLD object representation (created with fast-xml-parser) * @return The name to be used for the GeoStyler Style Style */ getStyleNameFromSldObject(sldObject) { const userStyleTitle = get(sldObject, 'StyledLayerDescriptor.NamedLayer[0].UserStyle.Name.#text'); const namedLayerName = get(sldObject, 'StyledLayerDescriptor.NamedLayer.Name.#text'); return userStyleTitle ? userStyleTitle : namedLayerName ? namedLayerName : ''; } /** * Get the geostyler-style Filter from a SLD Rule. * * Currently only supports one Filter per Rule. * * @param sldRule The SLD Rule * @return The geostyler-style Filter */ getFilterFromRule(sldRule) { const sldFilter = get(sldRule, 'Filter'); if (!sldFilter || sldFilter.length === 0) { return; } const operator = Object.keys(sldFilter[0])?.[0]; if (!operator) { return; } const filter = this.getFilterFromOperatorAndComparison(operator, sldFilter); return filter; } /** * Get the geostyler-style ScaleDenominator from a SLD Rule. * * @param sldRule The SLD Rule * @return The geostyler-style ScaleDenominator */ getScaleDenominatorFromRule(sldRule) { const scaleDenominator = {}; const min = get(sldRule, 'MinScaleDenominator.#text'); if (min) { scaleDenominator.min = Number(min); } const max = get(sldRule, 'MaxScaleDenominator.#text'); if (max) { scaleDenominator.max = Number(max); } return (Number.isFinite(scaleDenominator.min) || Number.isFinite(scaleDenominator.max)) ? scaleDenominator : undefined; } /** * Get the geostyler-style Symbolizers from a SLD Rule. * * @param sldRule The SLD Rule * @return The geostyler-style Symbolizer array */ getSymbolizersFromRule(sldRule) { const symbolizers = sldRule .filter(isSymbolizer) .map((sldSymbolizer) => { const sldSymbolizerName = Object.keys(sldSymbolizer)[0]; switch (sldSymbolizerName) { case 'PointSymbolizer': return this.getPointSymbolizerFromSldSymbolizer(sldSymbolizer.PointSymbolizer); case 'LineSymbolizer': return this.getLineSymbolizerFromSldSymbolizer(sldSymbolizer.LineSymbolizer); case 'TextSymbolizer': return this.getTextSymbolizerFromSldSymbolizer(sldSymbolizer.TextSymbolizer); case 'PolygonSymbolizer': return this.getFillSymbolizerFromSldSymbolizer(sldSymbolizer.PolygonSymbolizer); case 'RasterSymbolizer': return this.getRasterSymbolizerFromSldSymbolizer(sldSymbolizer.RasterSymbolizer); default: throw new Error(this.translate('symbolizerKindParseFailed', { sldSymbolizerName: sldSymbolizerName })); } }); return symbolizers; } /** * Creates a geostyler-style Filter from a given operator name and the js * SLD object representation (created with fast-xml-parser) of the SLD Filter. * * @param sldOperatorName The Name of the SLD Filter Operator * @param sldFilter The SLD Filter * @return The geostyler-style Filter */ getFilterFromOperatorAndComparison(sldOperatorName, sldFilter) { let filter; if (sldOperatorName === 'Function') { const functionName = Array.isArray(sldFilter) ? sldFilter[0][':@']['@_name'] : sldFilter[':@']['@_name']; const tempFunctionName = functionName.charAt(0).toUpperCase() + functionName.slice(1); sldOperatorName = `PropertyIs${tempFunctionName}`; } // we have to first check for PropertyIsBetween, // since it is also a comparisonOperator. But it // needs to be treated differently. if (sldOperatorName === 'PropertyIsBetween') { // TODO: PropertyIsBetween spec allows more than just a PropertyName as its first argument. const propertyName = get(sldFilter, 'PropertyIsBetween.PropertyName.#text'); const lower = Number(get(sldFilter, 'PropertyIsBetween.LowerBoundary.Literal.#text')); const upper = Number(get(sldFilter, 'PropertyIsBetween.UpperBoundary.Literal.#text')); filter = ['<=x<=', propertyName, lower, upper]; } else if (Object.keys(COMPARISON_MAP).includes(sldOperatorName)) { filter = this.getFilterFromComparisonOperator(sldOperatorName, sldFilter); } else if (Object.keys(COMBINATION_MAP).includes(sldOperatorName)) { const combinationOperator = COMBINATION_MAP[sldOperatorName]; const filters = get(sldFilter, sldOperatorName)?.map((op) => { const operatorName = Object.keys(op)?.[0]; return this.getFilterFromOperatorAndComparison(operatorName, op); }); filter = [ combinationOperator, ...filters ]; } else if (Object.keys(NEGATION_OPERATOR_MAP).includes(sldOperatorName)) { const negationOperator = NEGATION_OPERATOR_MAP[sldOperatorName]; const negatedOperator = Object.keys(sldFilter[sldOperatorName][0])[0]; const negatedComparison = sldFilter[sldOperatorName][0]; const negatedFilter = this.getFilterFromOperatorAndComparison(negatedOperator, negatedComparison); filter = [ negationOperator, negatedFilter ]; } else { throw new Error(this.translate('noFilterDetected')); } return filter; } getFilterFromComparisonOperator(sldOperatorName, sldFilter) { if (sldOperatorName === 'Function') { const functionName = Array.isArray(sldFilter) ? sldFilter[0][':@']['@_name'] : sldFilter[':@']['@_name']; const tempFunctionName = functionName.charAt(0).toUpperCase() + functionName.slice(1); sldOperatorName = `PropertyIs${tempFunctionName}`; } const comparisonOperator = COMPARISON_MAP[sldOperatorName]; const filterIsFunction = !!get(sldFilter, 'Function'); let args = []; const children = get(sldFilter, filterIsFunction ? 'Function' : sldOperatorName) || []; args = children.map((child, index) => { const operatorName = Object.keys(child)?.[0]; if (ARITHMETIC_OPERATORS.includes(operatorName.toLowerCase())) { const arithmeticOperator = child[operatorName]; return this.getFilterArgsFromArithmeticOperators(operatorName, arithmeticOperator); } return this.getFilterArgsFromPropertyName(child, children, index); }); if (sldOperatorName === 'PropertyIsNull') { args[1] = null; } return [ comparisonOperator, ...args ]; } /** * Creates a FunctionCall from arithmetic operators in SLD filters. * Handles nested arithmetic operations recursively. */ getFilterArgsFromArithmeticOperators(arithmeticOperatorName, arithmeticOperator) { const [leftSide, rightSide] = arithmeticOperator; return { name: arithmeticOperatorName.toLowerCase(), args: [ this.processArithmeticOperand(leftSide, arithmeticOperator), this.processArithmeticOperand(rightSide, arithmeticOperator) ] }; } /** * Processes a single operand in an arithmetic operation. * If the operand is itself an arithmetic operator, processes it recursively. */ processArithmeticOperand(operand, parentOperator) { const operatorName = Object.keys(operand)?.[0]; if (operatorName && ARITHMETIC_OPERATORS.includes(operatorName.toLowerCase())) { return this.getFilterArgsFromArithmeticOperators(operatorName, operand[operatorName]); } return this.getFilterArgsFromPropertyName(operand, parentOperator, 0); } getFilterArgsFromPropertyName(child, children, index) { const propName = get([child], 'PropertyName.#text'); if (propName !== undefined) { const isSingleArgOperator = children.length === 1; // Return property name for the first argument in case second argument is literal // or isSingleArgOperator eg (PropertyIsNull) if (isSingleArgOperator || (index === 0 && get([children[1]], 'PropertyName.#text') === undefined)) { return propName; } // ..otherwise + (second argument) return as property function return { name: 'property', args: [propName] }; } else { return get([child], '#text'); } } /** * Get the geostyler-style PointSymbolizer from a SLD Symbolizer. * * The opacity of the Symbolizer is taken from the <Graphic>. * * @param sldSymbolizer The SLD Symbolizer * @return The geostyler-style PointSymbolizer */ getPointSymbolizerFromSldSymbolizer(sldSymbolizer) { let pointSymbolizer; const wellKnownName = get(sldSymbolizer, 'Graphic.Mark.WellKnownName.#text'); const externalGraphic = get(sldSymbolizer, 'Graphic.ExternalGraphic'); if (externalGraphic) { pointSymbolizer = this.getIconSymbolizerFromSldSymbolizer(sldSymbolizer); } else { // geoserver does not set a wellKnownName for square explicitly since it is the default value. // Therefore, we have to set the wellKnownName to square if no wellKownName is given. if (!wellKnownName) { // TODO: Fix this. Idealy without lodash // _set(sldSymbolizer, 'Graphic[0].Mark[0].WellKnownName[0]._', 'square'); } pointSymbolizer = this.getMarkSymbolizerFromSldSymbolizer(sldSymbolizer); } return pointSymbolizer; } /** * Get the geostyler-style LineSymbolizer from a SLD Symbolizer. * * Currently only the CssParameters are available. * * @param sldSymbolizer The SLD Symbolizer * @return The geostyler-style LineSymbolizer */ getLineSymbolizerFromSldSymbolizer(sldSymbolizer) { const lineSymbolizer = { kind: 'Line' }; const strokeEl = get(sldSymbolizer, 'Stroke', this.readingSldVersion); const color = getParameterValue(strokeEl, 'stroke', this.readingSldVersion); const width = getParameterValue(strokeEl, 'stroke-width', this.readingSldVersion); const opacity = getParameterValue(strokeEl, 'stroke-opacity', this.readingSldVersion); const lineJoin = getParameterValue(strokeEl, 'stroke-linejoin', this.readingSldVersion); const lineCap = getParameterValue(strokeEl, 'stroke-linecap', this.readingSldVersion); const dashArray = getParameterValue(strokeEl, 'stroke-dasharray', this.readingSldVersion); const dashOffset = getParameterValue(strokeEl, 'stroke-dashoffset', this.readingSldVersion); if (!isNil(color)) { lineSymbolizer.color = color; } if (!isNil(width)) { lineSymbolizer.width = numberExpression(width); } if (!isNil(opacity)) { lineSymbolizer.opacity = numberExpression(opacity); } if (!isNil(lineJoin)) { // geostyler-style and ol use 'miter' whereas sld uses 'mitre' if (lineJoin === 'mitre') { lineSymbolizer.join = 'miter'; } else { lineSymbolizer.join = lineJoin; } } if (!isNil(lineCap)) { lineSymbolizer.cap = lineCap; } if (!isNil(dashArray)) { const dashStringAsArray = dashArray.split(' ').map(numberExpression); lineSymbolizer.dasharray = dashStringAsArray; } if (!isNil(dashOffset)) { lineSymbolizer.dashOffset = numberExpression(dashOffset); } const graphicStroke = get(strokeEl, 'GraphicStroke'); if (!isNil(graphicStroke)) { lineSymbolizer.graphicStroke = this.getPointSymbolizerFromSldSymbolizer(graphicStroke); } const graphicFill = get(strokeEl, 'GraphicFill'); if (!isNil(graphicFill)) { lineSymbolizer.graphicFill = this.getPointSymbolizerFromSldSymbolizer(graphicFill); } const perpendicularOffset = get(sldSymbolizer, 'PerpendicularOffset.#text'); if (!isNil(perpendicularOffset)) { lineSymbolizer.perpendicularOffset = numberExpression(perpendicularOffset); } return lineSymbolizer; } /** * Get the geostyler-style TextSymbolizer from a SLD Symbolizer. * * @param sldSymbolizer The SLD Symbolizer * @return The geostyler-style TextSymbolizer */ getTextSymbolizerFromSldSymbolizer(sldSymbolizer) { const textSymbolizer = { kind: 'Text' }; const fontEl = get(sldSymbolizer, 'Font'); const fillEl = get(sldSymbolizer, 'Fill'); const labelEl = get(sldSymbolizer, 'Label'); const haloEl = get(sldSymbolizer, 'Halo'); const haloFillEl = get(haloEl, 'Fill'); const color = getParameterValue(fillEl, 'fill', this.readingSldVersion); const opacity = getParameterValue(fillEl, 'fill-opacity', this.readingSldVersion); const fontFamily = getParameterValue(fontEl, 'font-family', this.readingSldVersion); const fontStyle = getParameterValue(fontEl, 'font-style', this.readingSldVersion); const fontSize = getParameterValue(fontEl, 'font-size', this.readingSldVersion); const fontWeight = getParameterValue(fontEl, 'font-weight', this.readingSldVersion); const haloColor = getParameterValue(haloFillEl, 'fill', this.readingSldVersion); if (!isNil(labelEl)) { textSymbolizer.label = this.getTextSymbolizerLabelFromSldSymbolizer(labelEl); } textSymbolizer.color = color ? color : '#000000'; if (!isNil(opacity)) { textSymbolizer.opacity = numberExpression(opacity); } const haloRadius = get(sldSymbolizer, 'Halo.Radius.#text'); if (!isNil(haloRadius)) { textSymbolizer.haloWidth = numberExpression(haloRadius); } const haloOpacity = getParameterValue(haloFillEl, 'fill-opacity', this.readingSldVersion); if (!isNil(haloOpacity)) { textSymbolizer.haloOpacity = numberExpression(haloOpacity); } if (!isNil(haloColor)) { textSymbolizer.haloColor = haloColor; } const placement = get(sldSymbolizer, 'LabelPlacement'); if (!isNil(placement)) { const pointPlacement = get(placement, 'PointPlacement'); const linePlacement = get(placement, 'LinePlacement'); if (!isNil(pointPlacement)) { textSymbolizer.placement = 'point'; const displacement = get(pointPlacement, 'Displacement'); if (!isNil(displacement)) { const x = get(displacement, 'DisplacementX.#text'); const y = get(displacement, 'DisplacementY.#text'); textSymbolizer.offset = [ Number.isFinite(x) ? numberExpression(x) : 0, Number.isFinite(y) ? -numberExpression(y) : 0, ]; } const rotation = get(pointPlacement, 'Rotation.#text'); if (!isNil(rotation)) { textSymbolizer.rotate = numberExpression(rotation); } } else if (!isNil(linePlacement)) { textSymbolizer.placement = 'line'; } } if (!isNil(fontFamily)) { textSymbolizer.font = [fontFamily]; } if (!isNil(fontStyle)) { textSymbolizer.fontStyle = fontStyle.toLowerCase(); } if (!isNil(fontWeight)) { textSymbolizer.fontWeight = fontWeight.toLowerCase(); } if (!isNil(fontSize)) { textSymbolizer.size = numberExpression(fontSize); } return textSymbolizer; } /** * Create a template string from a TextSymbolizer Label element. * The ordering of the elemments inside the Label element is preserved. * * Examples: * <Label> * <Literal>foo</Literal> * <PropertyName>bar</PropertyName> * </Label> * --> "foo{{bar}}" * * <Label> * <PropertyName>bar</PropertyName> * <Literal>foo</Literal> * </Label> * --> "{{bar}}foo" * * <Label> * <PropertyName>bar</PropertyName> * <Literal>foo</Literal> * <PropertyName>john</PropertyName> * </Label> * --> "{{bar}}foo{{john}}" * * <Label> * <PropertyName>bar</PropertyName> * <PropertyName>john</PropertyName> * <Literal>foo</Literal> * </Label> * --> "{{bar}}{{john}}foo" * * <Label> * <PropertyName>bar</PropertyName> * <PropertyName>john</PropertyName> * <Literal>foo</Literal> * <PropertyName>doe</PropertyName> * </Label> * --> "{{bar}}{{john}}foo{{doe}}" * * @param sldLabel */ getTextSymbolizerLabelFromSldSymbolizer = (sldLabel) => { const label = sldLabel .map((labelEl) => { const labelName = Object.keys(labelEl)[0]; switch (labelName.replace('ogc:', '')) { case '#text': return labelEl['#text']; case 'Literal': return labelEl?.[labelName]?.[0]?.['#text'] || labelEl?.[labelName]?.[0]?.['#cdata']?.[0]?.['#text']; case 'PropertyName': const propName = labelEl[labelName][0]['#text']; return `{{${propName}}}`; default: return ''; } }) .join(''); return label; }; /** * Get the geostyler-style FillSymbolizer from a SLD Symbolizer. * * PolygonSymbolizer Stroke is just partially supported. * * @param sldSymbolizer The SLD Symbolizer * @return The geostyler-style FillSymbolizer */ getFillSymbolizerFromSldSymbolizer(sldSymbolizer) { const fillSymbolizer = { kind: 'Fill' }; const strokeEl = get(sldSymbolizer, 'Stroke'); const fillEl = get(sldSymbolizer, 'Fill'); const fillOpacity = getParameterValue(fillEl, 'fill-opacity', this.readingSldVersion); const color = getParameterValue(fillEl, 'fill', this.readingSldVersion); const outlineColor = getParameterValue(strokeEl, 'stroke', this.readingSldVersion); const outlineWidth = getParameterValue(strokeEl, 'stroke-width', this.readingSldVersion); const outlineOpacity = getParameterValue(strokeEl, 'stroke-opacity', this.readingSldVersion); const outlineDashArray = getParameterValue(strokeEl, 'stroke-dasharray', this.readingSldVersion); const outlineCap = getParameterValue(strokeEl, 'stroke-linecap', this.readingSldVersion); const outlineJoin = getParameterValue(strokeEl, 'stroke-linejoin', this.readingSldVersion); // const outlineDashOffset = getParameterValue(strokeEl, 'stroke-dashoffset', this.readingSldVersion); const graphicFill = get(sldSymbolizer, 'Fill.GraphicFill'); if (!isNil(graphicFill)) { fillSymbolizer.graphicFill = this.getPointSymbolizerFromSldSymbolizer(graphicFill); } if (this.isSldEnv(sldEnvGeoServer)) { const graphicFillPadding = getVendorOptionValue(sldSymbolizer, 'graphic-margin'); if (!isNil(graphicFillPadding)) { fillSymbolizer.graphicFillPadding = graphicFillPadding.split(/\s/).map(numberExpression); } } if (!isNil(color)) { fillSymbolizer.color = color; } if (!isNil(fillOpacity)) { fillSymbolizer.fillOpacity = numberExpression(fillOpacity); } if (!isNil(outlineColor)) { fillSymbolizer.outlineColor = outlineColor; } if (!isNil(outlineWidth)) { fillSymbolizer.outlineWidth = numberExpression(outlineWidth); } if (!isNil(outlineOpacity)) { fillSymbolizer.outlineOpacity = numberExpression(outlineOpacity); } if (!isNil(outlineDashArray)) { fillSymbolizer.outlineDasharray = outlineDashArray.split(' ').map(numberExpression); } if (!isNil(outlineCap)) { fillSymbolizer.outlineCap = outlineCap; } if (!isNil(outlineJoin)) { fillSymbolizer.outlineJoin = outlineJoin; } // TODO: seems like this is missing in the geostyer-stlye // if (outlineDashOffset) { // fillSymbolizer.outlineDashOffset = Number(outlineDashOffset); // } return fillSymbolizer; } /** * Get the geostyler-style RasterSymbolizer from a SLD Symbolizer. * * @param sldSymbolizer The SLD Symbolizer */ getRasterSymbolizerFromSldSymbolizer(sldSymbolizer) { const rasterSymbolizer = { kind: 'Raster' }; // parse Opacity let opacity = get(sldSymbolizer, 'Opacity.#text'); if (!isNil(opacity)) { opacity = numberExpression(opacity); rasterSymbolizer.opacity = opacity; } // parse ColorMap const sldColorMap = get(sldSymbolizer, 'ColorMap'); const sldColorMapType = get(sldSymbolizer, 'ColorMap.@type'); const extended = get(sldSymbolizer, 'ColorMap.@extended'); if (!isNil(sldColorMap)) { const colormap = this.getColorMapFromSldColorMap(sldColorMap, sldColorMapType, extended); rasterSymbolizer.colorMap = colormap; } // parse ChannelSelection const sldChannelSelection = get(sldSymbolizer, 'ChannelSelection'); if (!isNil(sldChannelSelection)) { const channelSelection = this.getChannelSelectionFromSldChannelSelection(sldChannelSelection); rasterSymbolizer.channelSelection = channelSelection; } // parse ContrastEnhancement const sldContrastEnhancement = get(sldSymbolizer, 'ContrastEnhancement'); if (!isNil(sldContrastEnhancement)) { const contrastEnhancement = this.getContrastEnhancementFromSldContrastEnhancement(sldContrastEnhancement); rasterSymbolizer.contrastEnhancement = contrastEnhancement; } return rasterSymbolizer; } /** * Get the geostyler-style MarkSymbolizer from a SLD Symbolizer * * @param sldSymbolizer The SLD Symbolizer * @return The geostyler-style MarkSymbolizer */ getMarkSymbolizerFromSldSymbolizer(sldSymbolizer) { const wellKnownName = get(sldSymbolizer, 'Graphic.Mark.WellKnownName.#text'); const strokeEl = get(sldSymbolizer, 'Graphic.Mark.Stroke'); const fillEl = get(sldSymbolizer, 'Graphic.Mark.Fill'); const opacity = get(sldSymbolizer, 'Graphic.Opacity.#text'); const size = get(sldSymbolizer, 'Graphic.Size.#text'); const rotation = get(sldSymbolizer, 'Graphic.Rotation.#text'); const fillOpacity = getParameterValue(fillEl, 'fill-opacity', this.readingSldVersion); const color = getParameterValue(fillEl, 'fill', this.readingSldVersion); const displacement = get(sldSymbolizer, 'Graphic.Displacement'); const markSymbolizer = { kind: 'Mark', wellKnownName: 'circle' }; if (!isNil(opacity)) { markSymbolizer.opacity = numberExpression(opacity); } if (!isNil(fillOpacity)) { markSymbolizer.fillOpacity = numberExpression(fillOpacity); } if (!isNil(color)) { markSymbolizer.color = color; } if (!isNil(rotation)) { markSymbolizer.rotate = numberExpression(rotation); } if (!isNil(size)) { // edge case where the value has to be divided by 2 which has to be considered in the function markSymbolizer.radius = isGeoStylerNumberFunction(size) ? size : Number(size) / 2; } if (displacement) { const x = get(displacement, 'DisplacementX.#text'); const y = get(displacement, 'DisplacementY.#text'); markSymbolizer.offset = [ Number.isFinite(x) ? numberExpression(x) : 0, Number.isFinite(y) ? numberExpression(y) : 0, ]; } switch (wellKnownName) { case 'arrow': case 'arrowhead': case 'asterisk_fill': case 'backslash': case 'circle': case 'cross': case 'cross2': case 'cross_fill': case 'decagon': case 'diagonal_half_square': case 'diamond': case 'equilateral_triangle': case 'filled_arrowhead': case 'half_arc': case 'half_square': case 'heart': case 'hexagon': case 'horline': case 'left_half_triangle': case 'line': case 'octagon': case 'parallelogram_left': case 'parallelogram_right': case 'pentagon': case 'quarter_arc': case 'quarter_circle': case 'quarter_square': case 'right_half_triangle': case 'rounded_square': case 'semi_circle': case 'shield': case 'slash': case 'square': case 'square_with_corners': case 'star': case 'star_diamond': case 'third_arc': case 'third_circle': case 'trapezoid': case 'triangle': case 'x': case 'shape://vertline': case 'shape://horline': case 'shape://slash': case 'shape://backslash': case 'shape://dot': case 'shape://plus': case 'shape://times': case 'shape://oarrow': case 'shape://carrow': case 'brush://dense1': case 'brush://dense2': case 'brush://dense3': case 'brush://dense4': case 'brush://dense5': case 'brush://dense6': case 'brush://dense7': markSymbolizer.wellKnownName = wellKnownName; break; default: if (WELLKNOWNNAME_TTF_REGEXP.test(wellKnownName)) { markSymbolizer.wellKnownName = wellKnownName; break; } throw new Error(this.translate('marksymbolizerParseFailedUnknownWellknownName', { wellKnownName: wellKnownName })); } const strokeColor = getParameterValue(strokeEl, 'stroke', this.readingSldVersion); if (!isNil(strokeColor)) { markSymbolizer.strokeColor = strokeColor; } const strokeWidth = getParameterValue(strokeEl, 'stroke-width', this.readingSldVersion); if (!isNil(strokeWidth)) { markSymbolizer.strokeWidth = numberExpression(strokeWidth); } const strokeOpacity = getParameterValue(strokeEl, 'stroke-opacity', this.readingSldVersion); if (!isNil(strokeOpacity)) { markSymbolizer.strokeOpacity = numberExpression(strokeOpacity); } const strokeDasharray = getParameterValue(strokeEl, 'stroke-dasharray', this.readingSldVersion); if (!isNil(strokeDasharray)) { const dashStringAsArray = strokeDasharray.split(' ').map(numberExpression); markSymbolizer.strokeDasharray = dashStringAsArray; } return markSymbolizer; } /** * Get the geostyler-style IconSymbolizer from a SLD Symbolizer * * @param sldSymbolizer The SLD Symbolizer * @return The geostyler-style IconSymbolizer */ getIconSymbolizerFromSldSymbolizer(sldSymbolizer) { const image = get(sldSymbolizer, 'Graphic.ExternalGraphic.OnlineResource.@href'); const iconSymbolizer = { kind: 'Icon', image }; const opacity = get(sldSymbolizer, 'Graphic.Opacity.#text'); const size = get(sldSymbolizer, 'Graphic.Size.#text'); const rotation = get(sldSymbolizer, 'Graphic.Rotation.#text'); const displacement = get(sldSymbolizer, 'Graphic.Displacement'); if (!isNil(opacity)) { iconSymbolizer.opacity = numberExpression(opacity); } if (!isNil(size)) { iconSymbolizer.size = numberExpression(size); } if (!isNil(rotation)) { iconSymbolizer.rotate = numberExpression(rotation); } if (displacement) { const x = get(displacement, 'DisplacementX.#text'); const y = get(displacement, 'DisplacementY.#text'); iconSymbolizer.offset = [ Number.isFinite(x) ? numberExpression(x) : 0, Number.isFinite(y) ? numberExpression(y) : 0, ]; } return iconSymbolizer; } /** * Get the geostyler-style ColorMap from a SLD ColorMap. * * @param sldColorMap The SLD ColorMap */ getColorMapFromSldColorMap(sldColorMap, type = 'ramp', extended) { const colorMap = { type }; if (extended) { if (extended === 'true') { colorMap.extended = true; } else { colorMap.extended = false; } } const colorMapEntries = getChildren(sldColorMap, 'ColorMapEntry'); if (Array.isArray(colorMapEntries)) { const cmEntries = colorMapEntries.map((cm) => { const color = getAttribute(cm, 'color'); if (!color) { throw new Error(this.translate('colorMapEntriesParseFailedColorUndefined')); } let quantity = getAttribute(cm, 'quantity'); if (quantity) { quantity = numberExpression(quantity); } const label = getAttribute(cm, 'label'); let opacity = getAttribute(cm, 'opacity'); if (!isNil(opacity)) { opacity = numberExpression(opacity); } return { color, quantity, label, opacity }; }); colorMap.colorMapEntries = cmEntries; } return colorMap; } /** * Get the geostyler-style ContrastEnhancement from a SLD ContrastEnhancement. * * @param sldContrastEnhancement The SLD ContrastEnhancement */ getContrastEnhancementFromSldContrastEnhancement(sldContrastEnhancement) { const contrastEnhancement = {}; // parse enhancementType const hasHistogram = !!get(sldContrastEnhancement, 'Histogram'); const hasNormalize = !!get(sldContrastEnhancement, 'Normalize'); if (hasHistogram && hasNormalize) { throw new Error(this.translate('contrastEnhancParseFailedHistoAndNormalizeMutuallyExclusive')); } else if (hasHistogram) { contrastEnhancement.enhancementType = 'histogram'; } else if (hasNormalize) { contrastEnhancement.enhancementType = 'normalize'; } // parse gammavalue let gammaValue = get(sldContrastEnhancement, 'GammaValue.#text'); if (gammaValue) { gammaValue = numberExpression(gammaValue); } contrastEnhancement.gammaValue = gammaValue; return contrastEnhancement; } /** * Get the geostyler-style Channel from a SLD Channel. * * @param sldChannel The SLD Channel */ getChannelFromSldChannel(sldChannel) { const sourceChannelName = get(sldChannel, 'SourceChannelName.#text')?.toString(); const channel = { sourceChannelName }; const contrastEnhancement = get(sldChannel, 'ContrastEnhancement'); if (contrastEnhancement) { channel.contrastEnhancement = this.getContrastEnhancementFromSldContrastEnhancement(contrastEnhancement); } return channel; } /** * Get the geostyler-style ChannelSelection from a SLD ChannelSelection. * * @param sldChannelSelection The SLD ChannelSelection */ getChannelSelectionFromSldChannelSelection(sldChannelSelection) { let channelSelection; const red = get(sldChannelSelection, 'RedChannel'); const blue = get(sldChannelSelection, 'BlueChannel'); const green = get(sldChannelSelection, 'GreenChannel'); const gray = get(sldChannelSelection, 'GrayChannel'); if (gray && red && blue && green) { throw new Error(this.translate('channelSelectionParseFailedRGBAndGrayscaleMutuallyExclusive')); } if (gray) { const grayChannel = this.getChannelFromSldChannel(gray); channelSelection = { grayChannel }; } else if (red && green && blue) { const redChannel = this.getChannelFromSldChannel(red); const blueChannel = this.getChannelFromSldChannel(blue); const greenChannel = this.getChannelFromSldChannel(green); channelSelection = { redChannel, blueChannel, greenChannel }; } else { throw new Error(this.translate('channelSelectionParseFailedRGBChannelsUndefined')); } return channelSelection; } /** * The writeStyle implementation of the geostyler-style StyleParser interface. * It reads a geostyler-style and returns a Promise. * The Promise itself resolves with a SLD string. * * @param geoStylerStyle A geostyler-style. * @return The Promise resolving with the SLD as a string. */ writeStyle(geoStylerStyle) { return new Promise(resolve => { const unsupportedProperties = this.checkForUnsupportedProperties(geoStylerStyle); try { const sldObject = this.geoStylerStyleToSldObject(geoStylerStyle); const sldString = this.builder.build(sldObject); resolve({ output: sldString, unsupportedProperties, warnings: unsupportedProp