geostyler-sld-parser
Version:
GeoStyler Style Parser implementation for SLD
1,247 lines • 105 kB
JavaScript
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