geostyler-openlayers-parser
Version:
GeoStyler Style Parser implementation for OpenLayers styles
1,175 lines • 60.2 kB
JavaScript
import { parseFont } from 'css-font-parser';
import { isGeoStylerBooleanFunction, isGeoStylerFunction, isGeoStylerStringFunction, isIconSymbolizer, isMarkSymbolizer, isSprite } from 'geostyler-style';
import OlImageState from 'ol/ImageState';
import OlGeomPoint from 'ol/geom/Point';
import OlStyle from 'ol/style/Style';
import OlStyleImage from 'ol/style/Image';
import OlStyleStroke from 'ol/style/Stroke';
import OlStyleText from 'ol/style/Text';
import OlStyleCircle from 'ol/style/Circle';
import OlStyleFill from 'ol/style/Fill';
import OlStyleIcon from 'ol/style/Icon';
import OlStyleRegularshape from 'ol/style/RegularShape';
import { METERS_PER_UNIT } from 'ol/proj/Units';
import OlStyleUtil from './Util/OlStyleUtil';
import { toContext } from 'ol/render';
/**
* This parser can be used with the GeoStyler.
* It implements the GeoStyler-Style Parser interface to work with OpenLayers styles.
*
* @class OlStyleParser
* @implements StyleParser
*/
export class OlStyleParser {
/**
* The name of the OlStyleParser.
*/
static title = 'OpenLayers Style Parser';
unsupportedProperties = {
Symbolizer: {
MarkSymbolizer: {
avoidEdges: 'none',
blur: 'none',
offsetAnchor: 'none',
pitchAlignment: 'none',
pitchScale: 'none'
},
FillSymbolizer: {
antialias: 'none',
opacity: {
support: 'none',
info: 'Use fillOpacity instead.'
}
},
IconSymbolizer: {
allowOverlap: 'none',
anchor: 'none',
avoidEdges: 'none',
color: 'none',
haloBlur: 'none',
haloColor: 'none',
haloWidth: 'none',
keepUpright: 'none',
offsetAnchor: 'none',
size: {
support: 'partial',
info: 'Will set/get the width of the ol Icon.'
},
optional: 'none',
padding: 'none',
pitchAlignment: 'none',
rotationAlignment: 'none',
textFit: 'none',
textFitPadding: 'none'
},
LineSymbolizer: {
blur: 'none',
gapWidth: 'none',
gradient: 'none',
miterLimit: 'none',
roundLimit: 'none',
spacing: 'none',
graphicFill: 'none',
graphicStroke: 'none',
perpendicularOffset: 'none'
},
RasterSymbolizer: 'none',
TextSymbolizer: {
anchor: 'none',
placement: {
support: 'partial',
info: 'point and line supported. line-center will be mapped to line.'
}
}
},
Function: {
double2bool: {
support: 'none',
info: 'Always returns false'
},
atan2: {
support: 'none',
info: 'Currently returns the first argument'
},
rint: {
support: 'none',
info: 'Currently returns the first argument'
},
numberFormat: {
support: 'none',
info: 'Currently returns the first argument'
},
strAbbreviate: {
support: 'none',
info: 'Currently returns the first argument'
}
}
};
title = 'OpenLayers Style Parser';
olIconStyleCache = {};
OlStyleConstructor = OlStyle;
OlStyleImageConstructor = OlStyleImage;
OlStyleFillConstructor = OlStyleFill;
OlStyleStrokeConstructor = OlStyleStroke;
OlStyleTextConstructor = OlStyleText;
OlStyleCircleConstructor = OlStyleCircle;
OlStyleIconConstructor = OlStyleIcon;
OlStyleRegularshapeConstructor = OlStyleRegularshape;
constructor(ol) {
if (ol) {
this.OlStyleConstructor = ol.style.Style;
this.OlStyleImageConstructor = ol.style.Image;
this.OlStyleFillConstructor = ol.style.Fill;
this.OlStyleStrokeConstructor = ol.style.Stroke;
this.OlStyleTextConstructor = ol.style.Text;
this.OlStyleCircleConstructor = ol.style.Circle;
this.OlStyleIconConstructor = ol.style.Icon;
this.OlStyleRegularshapeConstructor = ol.style.RegularShape;
}
}
isOlParserStyleFct = (x) => {
return typeof x === 'function';
};
/**
* Get the GeoStyler-Style PointSymbolizer from an OpenLayers Style object.
*
* @param olStyle The OpenLayers Style object
* @return The GeoStyler-Style PointSymbolizer
*/
getPointSymbolizerFromOlStyle(olStyle) {
let pointSymbolizer;
if (olStyle.getImage() instanceof this.OlStyleCircleConstructor) {
// circle
const olCircleStyle = olStyle.getImage();
const olFillStyle = olCircleStyle.getFill();
const olStrokeStyle = olCircleStyle.getStroke();
const offset = olCircleStyle.getDisplacement();
const circleSymbolizer = {
kind: 'Mark',
wellKnownName: 'circle',
color: olFillStyle ? OlStyleUtil.getHexColor(olFillStyle.getColor()) : undefined,
opacity: olCircleStyle.getOpacity() !== 1 ? olCircleStyle.getOpacity() : undefined,
fillOpacity: olFillStyle ? OlStyleUtil.getOpacity(olFillStyle.getColor()) : undefined,
radius: (olCircleStyle.getRadius() !== 0) ? olCircleStyle.getRadius() : 5,
strokeColor: olStrokeStyle ? olStrokeStyle.getColor() : undefined,
strokeOpacity: olStrokeStyle ? OlStyleUtil.getOpacity(olStrokeStyle.getColor()) : undefined,
strokeWidth: olStrokeStyle ? olStrokeStyle.getWidth() : undefined,
offset: offset[0] || offset[1] ? offset : undefined
};
pointSymbolizer = circleSymbolizer;
}
else if (olStyle.getImage() instanceof this.OlStyleRegularshapeConstructor) {
// square, triangle, star, cross or x
const olRegularStyle = olStyle.getImage();
const olFillStyle = olRegularStyle.getFill();
const olStrokeStyle = olRegularStyle.getStroke();
const radius = olRegularStyle.getRadius();
const radius2 = olRegularStyle.getRadius2();
const points = olRegularStyle.getPoints();
const angle = olRegularStyle.getAngle();
const offset = olRegularStyle.getDisplacement();
const markSymbolizer = {
kind: 'Mark',
color: olFillStyle ? OlStyleUtil.getHexColor(olFillStyle.getColor()) : undefined,
opacity: olRegularStyle.getOpacity() !== 1 ? olRegularStyle.getOpacity() : undefined,
fillOpacity: olFillStyle ? OlStyleUtil.getOpacity(olFillStyle.getColor()) : undefined,
strokeColor: olStrokeStyle ? olStrokeStyle.getColor() : undefined,
strokeOpacity: olStrokeStyle ? OlStyleUtil.getOpacity(olStrokeStyle.getColor()) : undefined,
strokeWidth: olStrokeStyle ? olStrokeStyle.getWidth() : undefined,
radius: (radius !== 0) ? radius : 5,
// Rotation in openlayers is radians while we use degree
rotate: olRegularStyle.getRotation() / Math.PI * 180,
offset: offset[0] || offset[1] ? offset : undefined
};
switch (points) {
case 2:
switch (angle) {
case 0:
markSymbolizer.wellKnownName = 'shape://vertline';
break;
case Math.PI / 2:
markSymbolizer.wellKnownName = 'shape://horline';
break;
case Math.PI / 4:
markSymbolizer.wellKnownName = 'shape://slash';
break;
case 2 * Math.PI - (Math.PI / 4):
markSymbolizer.wellKnownName = 'shape://backslash';
break;
default:
break;
}
break;
case 3:
switch (angle) {
case 0:
markSymbolizer.wellKnownName = 'triangle';
break;
case Math.PI / 2:
markSymbolizer.wellKnownName = 'shape://carrow';
break;
default:
break;
}
break;
case 4:
if (Number.isFinite(radius2)) {
// cross or x
if (olRegularStyle.getAngle() === 0) {
// cross
markSymbolizer.wellKnownName = 'cross';
}
else {
// x
markSymbolizer.wellKnownName = 'x';
}
}
else {
// square
markSymbolizer.wellKnownName = 'square';
}
break;
case 5:
// star
markSymbolizer.wellKnownName = 'star';
break;
default:
throw new Error('Could not parse OlStyle. Only 2, 3, 4 or 5 point regular shapes are allowed');
}
pointSymbolizer = markSymbolizer;
}
else if (olStyle.getText() instanceof this.OlStyleTextConstructor) {
const olTextStyle = olStyle.getText();
const olFillStyle = olTextStyle.getFill();
const olStrokeStyle = olTextStyle.getStroke();
const rotation = olTextStyle.getRotation();
let char = olTextStyle.getText() || 'a';
const font = olTextStyle.getFont() || '10px sans-serif';
const fontName = OlStyleUtil.getFontNameFromOlFont(font);
const radius = OlStyleUtil.getSizeFromOlFont(font);
const offset = [olTextStyle.getOffsetX(), olTextStyle.getOffsetY()];
if (Array.isArray(char)) {
char = char[0];
}
pointSymbolizer = {
kind: 'Mark',
wellKnownName: `ttf://${fontName}#0x${char.charCodeAt(0).toString(16)}`,
color: olFillStyle ? OlStyleUtil.getHexColor(olFillStyle.getColor()) : undefined,
opacity: olFillStyle ? OlStyleUtil.getOpacity(olFillStyle.getColor()) : undefined,
strokeColor: olStrokeStyle ? olStrokeStyle.getColor() : undefined,
strokeOpacity: olStrokeStyle ? OlStyleUtil.getOpacity(olStrokeStyle.getColor()) : undefined,
strokeWidth: olStrokeStyle ? olStrokeStyle.getWidth() : undefined,
radius: (radius !== 0) ? radius : 5,
// Rotation in openlayers is radians while we use degree
rotate: rotation ? rotation / Math.PI * 180 : 0,
offset: offset[0] || offset[1] ? offset : undefined
};
}
else {
// icon
const olIconStyle = olStyle.getImage();
const displacement = olIconStyle.getDisplacement();
// initialOptions_ as fallback when image is not yet loaded
const image = this.getImageFromIconStyle(olIconStyle);
// this always gets calculated from ol so this might not have been set initially
const size = olIconStyle.getWidth();
const rotation = olIconStyle.getRotation() / Math.PI * 180;
const opacity = olIconStyle.getOpacity();
const iconSymbolizer = {
kind: 'Icon',
image,
opacity: opacity < 1 ? opacity : undefined,
size,
// Rotation in openlayers is radians while we use degree
rotate: rotation !== 0 ? rotation : undefined,
offset: displacement[0] || displacement[1] ? displacement : undefined
};
pointSymbolizer = iconSymbolizer;
}
return pointSymbolizer;
}
/**
*
* @param olIconStyle An ol style Icon representation
* @returns A string or Sprite configuration
*/
getImageFromIconStyle(olIconStyle) {
const size = olIconStyle.getSize();
if (Array.isArray(size)) {
// TODO: create getters (and setters?) in openlayers
// @ts-expect-error offset_ is private
const position = olIconStyle.offset_;
// @ts-expect-error offsetOrigin_ is private
const offsetOrigin = olIconStyle.offsetOrigin_;
if (offsetOrigin && offsetOrigin !== 'top-left') {
throw new Error(`Offset origin ${offsetOrigin} not supported`);
}
return {
source: olIconStyle.getSrc(),
position,
size: size
};
}
else {
return olIconStyle.getSrc() ? olIconStyle.getSrc() : undefined;
}
}
/**
* Get the GeoStyler-Style LineSymbolizer from an OpenLayers Style object.
*
* @param olStyle The OpenLayers Style object
* @return The GeoStyler-Style LineSymbolizer
*/
getLineSymbolizerFromOlStyle(olStyle) {
const olStrokeStyle = olStyle.getStroke();
// getLineDash returns null not undefined. So we have to double check
const dashArray = olStrokeStyle ? olStrokeStyle.getLineDash() : undefined;
return {
kind: 'Line',
color: olStrokeStyle ? OlStyleUtil.getHexColor(olStrokeStyle.getColor()) : undefined,
opacity: olStrokeStyle ? OlStyleUtil.getOpacity(olStrokeStyle.getColor()) : undefined,
width: olStrokeStyle ? olStrokeStyle.getWidth() : undefined,
cap: olStrokeStyle ? olStrokeStyle.getLineCap() : 'butt',
join: olStrokeStyle ? olStrokeStyle.getLineJoin() : 'miter',
dasharray: dashArray ? dashArray : undefined,
dashOffset: olStrokeStyle ? olStrokeStyle.getLineDashOffset() : undefined
};
}
/**
* Get the GeoStyler-Style FillSymbolizer from an OpenLayers Style object.
*
* PolygonSymbolizer Stroke is just partially supported.
*
* @param olStyle The OpenLayers Style object
* @return The GeoStyler-Style FillSymbolizer
*/
getFillSymbolizerFromOlStyle(olStyle) {
const olFillStyle = olStyle.getFill();
const olStrokeStyle = olStyle.getStroke();
// getLineDash returns null not undefined. So we have to double check
const outlineDashArray = olStrokeStyle ? olStrokeStyle.getLineDash() : undefined;
const symbolizer = {
kind: 'Fill'
};
if (olFillStyle) {
symbolizer.color = OlStyleUtil.getHexColor(olFillStyle.getColor());
}
if (olFillStyle) {
symbolizer.fillOpacity = OlStyleUtil.getOpacity(olFillStyle.getColor());
}
if (olStrokeStyle) {
symbolizer.outlineColor = OlStyleUtil.getHexColor(olStrokeStyle.getColor());
}
if (outlineDashArray) {
symbolizer.outlineDasharray = outlineDashArray;
}
if (olStrokeStyle) {
symbolizer.outlineOpacity = OlStyleUtil.getOpacity(olStrokeStyle.getColor());
}
if (olStrokeStyle && olStrokeStyle.getWidth()) {
symbolizer.outlineWidth = olStrokeStyle.getWidth();
}
return symbolizer;
}
/**
* Get the GeoStyler-Style TextSymbolizer from an OpenLayers Style object.
*
*
* @param olStyle The OpenLayers Style object
* @return The GeoStyler-Style TextSymbolizer
*/
getTextSymbolizerFromOlStyle(olStyle) {
const olTextStyle = olStyle.getText();
if (!olTextStyle) {
throw new Error('Could not get text from olStyle.');
}
const olFillStyle = olTextStyle.getFill();
const olStrokeStyle = olTextStyle.getStroke();
const offsetX = olTextStyle.getOffsetX();
const offsetY = olTextStyle.getOffsetY();
const font = olTextStyle.getFont();
const rotation = olTextStyle.getRotation();
const allowOverlap = olTextStyle.getOverflow() ? olTextStyle.getOverflow() : undefined;
const placement = olTextStyle.getPlacement();
const text = olTextStyle.getText();
const label = Array.isArray(text) ? text[0] : text;
let fontSize = Infinity;
let fontFamily = undefined;
let fontWeight = undefined;
let fontStyle = undefined;
if (font) {
const fontObj = parseFont(font);
if (fontObj['font-weight']) {
fontWeight = fontObj['font-weight'];
}
if (fontObj['font-size']) {
fontSize = parseInt(fontObj['font-size'], 10);
}
if (fontObj['font-family']) {
const fontFamilies = fontObj['font-family'];
fontFamily = fontFamilies?.map((f) => f.includes(' ') ? '"' + f + '"' : f);
}
if (fontObj['font-style']) {
fontStyle = fontObj['font-style'];
}
}
return {
kind: 'Text',
label,
placement,
allowOverlap,
color: olFillStyle ? OlStyleUtil.getHexColor(olFillStyle.getColor()) : undefined,
size: isFinite(fontSize) ? fontSize : undefined,
font: fontFamily,
fontWeight: fontWeight || undefined,
fontStyle: fontStyle || undefined,
offset: (offsetX !== undefined) && (offsetY !== undefined) ? [offsetX, offsetY] : [0, 0],
haloColor: olStrokeStyle && olStrokeStyle.getColor() ?
OlStyleUtil.getHexColor(olStrokeStyle.getColor()) : undefined,
haloWidth: olStrokeStyle ? olStrokeStyle.getWidth() : undefined,
rotate: (rotation !== undefined) ? rotation / Math.PI * 180 : undefined
};
}
/**
* Get the GeoStyler-Style Symbolizer from an OpenLayers Style object.
*
* @param olStyles The OpenLayers Style object
* @return The GeoStyler-Style Symbolizer array
*/
getSymbolizersFromOlStyle(olStyles) {
const symbolizers = [];
olStyles.forEach(olStyle => {
let symbolizer;
const styleType = this.getStyleTypeFromOlStyle(olStyle);
switch (styleType) {
case 'Point':
if (olStyle.getText() && !OlStyleUtil.getIsMarkSymbolizerFont(olStyle.getText().getFont())) {
symbolizer = this.getTextSymbolizerFromOlStyle(olStyle);
}
else {
symbolizer = this.getPointSymbolizerFromOlStyle(olStyle);
}
break;
case 'Line':
symbolizer = this.getLineSymbolizerFromOlStyle(olStyle);
break;
case 'Fill':
symbolizer = this.getFillSymbolizerFromOlStyle(olStyle);
break;
default:
throw new Error('Failed to parse SymbolizerKind from OpenLayers Style');
}
symbolizers.push(symbolizer);
});
return symbolizers;
}
/**
* Get the GeoStyler-Style Rule from an OpenLayers Style object.
*
* @param olStyles The OpenLayers Style object
* @return The GeoStyler-Style Rule
*/
getRuleFromOlStyle(olStyles) {
let symbolizers;
const name = 'OL Style Rule 0';
if (Array.isArray(olStyles)) {
symbolizers = this.getSymbolizersFromOlStyle(olStyles);
}
else {
symbolizers = this.getSymbolizersFromOlStyle([olStyles]);
}
return {
name, symbolizers
};
}
/**
* Get the GeoStyler-Style Symbolizer from an OpenLayers Style object.
*
* @param olStyle The OpenLayers Style object
* @return The GeoStyler-Style Symbolizer
*/
getStyleTypeFromOlStyle(olStyle) {
let styleType;
if (olStyle.getImage() instanceof this.OlStyleImageConstructor) {
styleType = 'Point';
}
else if (olStyle.getText() instanceof this.OlStyleTextConstructor) {
styleType = 'Point';
}
else if (olStyle.getFill() instanceof this.OlStyleFillConstructor) {
styleType = 'Fill';
}
else if (olStyle.getStroke() && !olStyle.getFill()) {
styleType = 'Line';
}
else {
throw new Error('StyleType could not be detected');
}
return styleType;
}
/**
* Get the GeoStyler-Style Style from an OpenLayers Style object.
*
* @param olStyle The OpenLayers Style object
* @return The GeoStyler-Style Style
*/
olStyleToGeoStylerStyle(olStyle) {
const name = 'OL Style';
const rule = this.getRuleFromOlStyle(olStyle);
return {
name,
rules: [rule]
};
}
/**
* The readStyle implementation of the GeoStyler-Style StyleParser interface.
* It reads an OpenLayers Style, an array of OpenLayers Styles or an olParserStyleFct and returns a Promise.
*
* The Promise itself resolves with a GeoStyler-Style Style.
*
* @param olStyle The style to be parsed
* @return The Promise resolving with the GeoStyler-Style Style
*/
readStyle(olStyle) {
return new Promise((resolve) => {
try {
if (this.isOlParserStyleFct(olStyle)) {
resolve({
output: olStyle.__geoStylerStyle
});
}
else {
olStyle = olStyle;
const geoStylerStyle = this.olStyleToGeoStylerStyle(olStyle);
const unsupportedProperties = this.checkForUnsupportedProperties(geoStylerStyle);
resolve({
output: geoStylerStyle,
unsupportedProperties
});
}
}
catch (error) {
resolve({
errors: [error]
});
}
});
}
/**
* The writeStyle implementation of the GeoStyler-Style StyleParser interface.
* It reads a GeoStyler-Style Style and returns a Promise.
* The Promise itself resolves one of three types
*
* 1. OlStyle if input Style consists of
* one rule with one symbolizer, no filter, no scaleDenominator, no TextSymbolizer
* 2. OlStyle[] if input Style consists of
* one rule with multiple symbolizers, no filter, no scaleDenominator, no TextSymbolizer
* 3. OlParserStyleFct for everything else
*
* @param geoStylerStyle A GeoStyler-Style Style.
* @return The Promise resolving with one of above mentioned style types.
*/
writeStyle(geoStylerStyle) {
return new Promise((resolve) => {
const clonedStyle = structuredClone(geoStylerStyle);
const unsupportedProperties = this.checkForUnsupportedProperties(clonedStyle);
try {
const olStyle = this.getOlStyleTypeFromGeoStylerStyle(clonedStyle);
resolve({
output: olStyle,
unsupportedProperties,
warnings: unsupportedProperties && ['Your style contains unsupportedProperties!']
});
}
catch (error) {
resolve({
errors: [error]
});
}
});
}
checkForUnsupportedProperties(geoStylerStyle) {
const capitalizeFirstLetter = (a) => a[0].toUpperCase() + a.slice(1);
const unsupportedProperties = {};
geoStylerStyle.rules.forEach(rule => {
// ScaleDenominator and Filters are completly supported so we just check for symbolizers
rule.symbolizers.forEach(symbolizer => {
const key = capitalizeFirstLetter(`${symbolizer.kind}Symbolizer`);
const value = this.unsupportedProperties?.Symbolizer?.[key];
if (value) {
if (typeof value === 'string') {
if (!unsupportedProperties.Symbolizer) {
unsupportedProperties.Symbolizer = {};
}
unsupportedProperties.Symbolizer[key] = value;
}
else {
Object.keys(symbolizer).forEach(property => {
if (value[property]) {
if (!unsupportedProperties.Symbolizer) {
unsupportedProperties.Symbolizer = {};
}
if (!unsupportedProperties.Symbolizer[key]) {
unsupportedProperties.Symbolizer[key] = {};
}
unsupportedProperties.Symbolizer[key][property] = value[property];
}
});
}
}
});
});
if (Object.keys(unsupportedProperties).length > 0) {
return unsupportedProperties;
}
return undefined;
}
/**
* Decides which OlStyleType should be returned depending on given geoStylerStyle.
* Three OlStyleTypes are possible:
*
* 1. OlStyle if input Style consists of
* one rule with one symbolizer, no filter, no scaleDenominator, no TextSymbolizer
* 2. OlStyle[] if input Style consists of
* one rule with multiple symbolizers, no filter, no scaleDenominator, no TextSymbolizer
* 3. OlParserStyleFct for everything else
*
* @param geoStylerStyle A GeoStyler-Style Style
*/
getOlStyleTypeFromGeoStylerStyle(geoStylerStyle) {
const rules = geoStylerStyle.rules;
const nrRules = rules.length;
if (nrRules === 1) {
const hasFilter = geoStylerStyle?.rules?.[0]?.filter !== undefined ? true : false;
const hasMinScale = geoStylerStyle?.rules?.[0]?.scaleDenominator?.min !== undefined ? true : false;
const hasMaxScale = geoStylerStyle?.rules?.[0]?.scaleDenominator?.max !== undefined ? true : false;
const hasScaleDenominator = hasMinScale || hasMaxScale ? true : false;
const hasFunctions = OlStyleUtil.containsGeoStylerFunctions(geoStylerStyle);
const nrSymbolizers = geoStylerStyle.rules[0].symbolizers.length;
const hasTextSymbolizer = rules[0].symbolizers.some((symbolizer) => {
return symbolizer.kind === 'Text';
});
const hasDynamicIconSymbolizer = rules[0].symbolizers.some((symbolizer) => {
return symbolizer.kind === 'Icon' && typeof (symbolizer.image) === 'string' && symbolizer.image.includes('{{');
});
if (!hasFilter && !hasScaleDenominator && !hasTextSymbolizer && !hasDynamicIconSymbolizer && !hasFunctions) {
if (nrSymbolizers === 1) {
return this.geoStylerStyleToOlStyle(geoStylerStyle);
}
else {
return this.geoStylerStyleToOlStyleArray(geoStylerStyle);
}
}
else {
return this.geoStylerStyleToOlParserStyleFct(geoStylerStyle);
}
}
else {
return this.geoStylerStyleToOlParserStyleFct(geoStylerStyle);
}
}
/**
* Parses the first symbolizer of the first rule of a GeoStyler-Style Style.
*
* @param geoStylerStyle GeoStyler-Style Style
* @return An OpenLayers Style Object
*/
geoStylerStyleToOlStyle(geoStylerStyle) {
const rule = geoStylerStyle.rules[0];
const symbolizer = rule.symbolizers[0];
const olSymbolizer = this.getOlSymbolizerFromSymbolizer(symbolizer);
return olSymbolizer;
}
/**
* Parses all symbolizers of the first rule of a GeoStyler-Style Style.
*
* @param geoStylerStyle GeoStyler-Style Style
* @return An array of OpenLayers Style Objects
*/
geoStylerStyleToOlStyleArray(geoStylerStyle) {
const rule = geoStylerStyle.rules[0];
const olStyles = [];
rule.symbolizers.forEach((symbolizer) => {
const olSymbolizer = this.getOlSymbolizerFromSymbolizer(symbolizer);
olStyles.push(olSymbolizer);
});
return olStyles;
}
/**
* Get the OpenLayers Style object from an GeoStyler-Style Style
*
* @param geoStylerStyle A GeoStyler-Style Style.
* @return An OlParserStyleFct
*/
geoStylerStyleToOlParserStyleFct(geoStylerStyle) {
const rules = structuredClone(geoStylerStyle.rules);
const olStyle = (feature, resolution) => {
const styles = [];
// calculate scale for resolution (from ol-util MapUtil)
const dpi = 25.4 / 0.28;
const mpu = METERS_PER_UNIT.m;
const inchesPerMeter = 39.37;
const scale = resolution * mpu * inchesPerMeter * dpi;
rules.forEach((rule) => {
// handling scale denominator
let minScale = rule?.scaleDenominator?.min;
let maxScale = rule?.scaleDenominator?.max;
let isWithinScale = true;
if (minScale || maxScale) {
minScale = isGeoStylerFunction(minScale) ? OlStyleUtil.evaluateNumberFunction(minScale) : minScale;
maxScale = isGeoStylerFunction(maxScale) ? OlStyleUtil.evaluateNumberFunction(maxScale) : maxScale;
if (minScale && scale < minScale) {
isWithinScale = false;
}
if (maxScale && scale >= maxScale) {
isWithinScale = false;
}
}
// handling filter
let matchesFilter = false;
if (!rule.filter) {
matchesFilter = true;
}
else {
try {
matchesFilter = this.geoStylerFilterToOlParserFilter(feature, rule.filter);
}
catch (e) {
matchesFilter = false;
}
}
if (isWithinScale && matchesFilter) {
rule.symbolizers.forEach((symb) => {
if (symb.visibility === false) {
styles.push(null);
}
if (isGeoStylerBooleanFunction(symb.visibility)) {
const visibility = OlStyleUtil.evaluateBooleanFunction(symb.visibility);
if (!visibility) {
styles.push(null);
}
}
const olSymbolizer = this.getOlSymbolizerFromSymbolizer(symb, feature);
// either an OlStyle or an ol.StyleFunction. OpenLayers only accepts an array
// of OlStyles, not ol.StyleFunctions.
// So we have to check it and in case of an ol.StyleFunction call that function
// and add the returned style to const styles.
if (typeof olSymbolizer !== 'function') {
styles.push(olSymbolizer);
}
else {
const styleFromFct = olSymbolizer(feature, resolution);
styles.push(styleFromFct);
}
});
}
});
return styles;
};
const olStyleFct = olStyle;
olStyleFct.__geoStylerStyle = geoStylerStyle;
return olStyleFct;
}
/**
* Checks if a feature matches given filter expression(s)
* @param feature ol.Feature
* @param filter Filter
* @return boolean true if feature matches filter expression
*/
geoStylerFilterToOlParserFilter(feature, filter) {
const operatorMapping = {
'&&': true,
'||': true,
'!': true
};
let matchesFilter = true;
if (isGeoStylerBooleanFunction(filter)) {
return OlStyleUtil.evaluateBooleanFunction(filter, feature);
}
if (filter === true || filter === false) {
return filter;
}
const operator = filter[0];
let isNestedFilter = false;
if (operatorMapping[operator]) {
isNestedFilter = true;
}
try {
if (isNestedFilter) {
let intermediate;
let restFilter;
switch (filter[0]) {
case '&&':
intermediate = true;
restFilter = filter.slice(1);
restFilter.forEach((f) => {
if (!this.geoStylerFilterToOlParserFilter(feature, f)) {
intermediate = false;
}
});
matchesFilter = intermediate;
break;
case '||':
intermediate = false;
restFilter = filter.slice(1);
restFilter.forEach((f) => {
if (this.geoStylerFilterToOlParserFilter(feature, f)) {
intermediate = true;
}
});
matchesFilter = intermediate;
break;
case '!':
matchesFilter = !this.geoStylerFilterToOlParserFilter(feature, filter[1]);
break;
default:
throw new Error('Cannot parse Filter. Unknown combination or negation operator.');
}
}
else {
let arg1;
if (isGeoStylerFunction(filter[1])) {
arg1 = OlStyleUtil.evaluateFunction(filter[1], feature);
}
else {
arg1 = feature.get(filter[1]);
}
let arg2;
if (isGeoStylerFunction(filter[2])) {
arg2 = OlStyleUtil.evaluateFunction(filter[2], feature);
}
else {
arg2 = filter[2];
}
switch (filter[0]) {
case '==':
matchesFilter = ('' + arg1) === ('' + arg2);
break;
case '*=':
// inspired by
// https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/String/includes#Polyfill
if (typeof arg2 === 'string' && typeof arg1 === 'string') {
if (arg2.length > arg1.length) {
matchesFilter = false;
}
else {
matchesFilter = arg1.indexOf(arg2) !== -1;
}
}
break;
case '!=':
matchesFilter = ('' + arg1) !== ('' + arg2);
break;
case '<':
matchesFilter = Number(arg1) < Number(arg2);
break;
case '<=':
matchesFilter = Number(arg1) <= Number(arg2);
break;
case '>':
matchesFilter = Number(arg1) > Number(arg2);
break;
case '>=':
matchesFilter = Number(arg1) >= Number(arg2);
break;
default:
throw new Error('Cannot parse Filter. Unknown comparison operator.');
}
}
}
catch (e) {
throw new Error('Cannot parse Filter. Invalid structure.');
}
return matchesFilter;
}
/**
* Get the OpenLayers Style object or an OL StyleFunction from an
* GeoStyler-Style Symbolizer.
*
* @param symbolizer A GeoStyler-Style Symbolizer.
* @return The OpenLayers Style object or a StyleFunction
*/
getOlSymbolizerFromSymbolizer(symbolizer, feature) {
let olSymbolizer;
symbolizer = structuredClone(symbolizer);
switch (symbolizer.kind) {
case 'Mark':
olSymbolizer = this.getOlPointSymbolizerFromMarkSymbolizer(symbolizer, feature);
break;
case 'Icon':
olSymbolizer = this.getOlIconSymbolizerFromIconSymbolizer(symbolizer, feature);
break;
case 'Text':
olSymbolizer = this.getOlTextSymbolizerFromTextSymbolizer(symbolizer, feature);
break;
case 'Line':
olSymbolizer = this.getOlLineSymbolizerFromLineSymbolizer(symbolizer, feature);
break;
case 'Fill':
olSymbolizer = this.getOlPolygonSymbolizerFromFillSymbolizer(symbolizer, feature);
break;
default: {
// Return the OL default style since the TS type binding does not allow
// us to set olSymbolizer to undefined
const fill = new this.OlStyleFillConstructor({
color: 'rgba(255,255,255,0.4)'
});
const stroke = new this.OlStyleStrokeConstructor({
color: '#3399CC',
width: 1.25
});
olSymbolizer = new this.OlStyleConstructor({
image: new this.OlStyleCircleConstructor({
fill: fill,
stroke: stroke,
radius: 5
}),
fill: fill,
stroke: stroke
});
break;
}
}
return olSymbolizer;
}
/**
* Get the OL Style object from an GeoStyler-Style MarkSymbolizer.
*
* @param markSymbolizer A GeoStyler-Style MarkSymbolizer.
* @return The OL Style object
*/
getOlPointSymbolizerFromMarkSymbolizer(markSymbolizer, feature) {
let stroke;
for (const key of Object.keys(markSymbolizer)) {
if (isGeoStylerFunction(markSymbolizer[key])) {
markSymbolizer[key] = OlStyleUtil.evaluateFunction(markSymbolizer[key], feature);
}
}
const strokeColor = markSymbolizer.strokeColor;
const strokeOpacity = markSymbolizer.strokeOpacity;
const sColor = strokeColor && (strokeOpacity !== undefined)
? OlStyleUtil.getRgbaColor(strokeColor, strokeOpacity)
: markSymbolizer.strokeColor;
if (markSymbolizer.strokeColor || markSymbolizer.strokeWidth !== undefined) {
stroke = new this.OlStyleStrokeConstructor({
color: sColor,
width: markSymbolizer.strokeWidth
});
}
const color = markSymbolizer.color;
const opacity = markSymbolizer.opacity;
const radius = markSymbolizer.radius;
const fillOpacity = markSymbolizer.fillOpacity;
const fColor = color && (fillOpacity !== undefined)
? OlStyleUtil.getRgbaColor(color, fillOpacity ?? 1)
: color;
const fill = new this.OlStyleFillConstructor({
color: fColor
});
let olStyle;
const shapeOpts = {
fill: fill,
radius: radius ?? 5,
rotation: typeof (markSymbolizer.rotate) === 'number' ? markSymbolizer.rotate * Math.PI / 180 : undefined,
stroke: stroke,
displacement: Array.isArray(markSymbolizer.offset) ? markSymbolizer.offset.map(Number) : undefined
};
switch (markSymbolizer.wellKnownName) {
case 'shape://dot':
case 'circle':
olStyle = new this.OlStyleConstructor({
image: new this.OlStyleCircleConstructor(shapeOpts)
});
break;
case 'square':
olStyle = new this.OlStyleConstructor({
image: new this.OlStyleRegularshapeConstructor({
...shapeOpts,
points: 4,
angle: 45 * Math.PI / 180
})
});
break;
case 'triangle':
olStyle = new this.OlStyleConstructor({
image: new this.OlStyleRegularshapeConstructor({
...shapeOpts,
points: 3,
angle: 0
})
});
break;
case 'star':
olStyle = new this.OlStyleConstructor({
image: new this.OlStyleRegularshapeConstructor({
...shapeOpts,
points: 5,
radius2: shapeOpts.radius / 2.5,
angle: 0
})
});
break;
case 'shape://plus':
case 'cross':
// openlayers does not seem to set a default stroke color,
// which is needed for regularshapes with radius2 = 0
if (shapeOpts.stroke === undefined) {
shapeOpts.stroke = new this.OlStyleStrokeConstructor({
color: '#000'
});
}
olStyle = new this.OlStyleConstructor({
image: new this.OlStyleRegularshapeConstructor({
...shapeOpts,
points: 4,
radius2: 0,
angle: 0
})
});
break;
case 'shape://times':
case 'x':
// openlayers does not seem to set a default stroke color,
// which is needed for regularshapes with radius2 = 0
if (shapeOpts.stroke === undefined) {
shapeOpts.stroke = new this.OlStyleStrokeConstructor({
color: '#000'
});
}
olStyle = new this.OlStyleConstructor({
image: new this.OlStyleRegularshapeConstructor({
...shapeOpts,
points: 4,
radius2: 0,
angle: 45 * Math.PI / 180
})
});
break;
case 'shape://backslash':
// openlayers does not seem to set a default stroke color,
// which is needed for regularshapes with radius2 = 0
if (shapeOpts.stroke === undefined) {
shapeOpts.stroke = new this.OlStyleStrokeConstructor({
color: '#000'
});
}
olStyle = new this.OlStyleConstructor({
image: new this.OlStyleRegularshapeConstructor({
...shapeOpts,
points: 2,
angle: 2 * Math.PI - (Math.PI / 4)
})
});
break;
case 'shape://horline':
// openlayers does not seem to set a default stroke color,
// which is needed for regularshapes with radius2 = 0
if (shapeOpts.stroke === undefined) {
shapeOpts.stroke = new this.OlStyleStrokeConstructor({
color: '#000'
});
}
olStyle = new this.OlStyleConstructor({
image: new this.OlStyleRegularshapeConstructor({
...shapeOpts,
points: 2,
angle: Math.PI / 2
})
});
break;
// so far, both arrows are closed arrows. Also, shape is a regular triangle with
// all sides of equal length. In geoserver arrows only have two sides of equal length.
// TODO redefine shapes of arrows?
case 'shape://oarrow':
case 'shape://carrow':
olStyle = new this.OlStyleConstructor({
image: new this.OlStyleRegularshapeConstructor({
...shapeOpts,
points: 3,
angle: Math.PI / 2
})
});
break;
case 'shape://slash':
// openlayers does not seem to set a default stroke color,
// which is needed for regularshapes with radius2 = 0
if (shapeOpts.stroke === undefined) {
shapeOpts.stroke = new this.OlStyleStrokeConstructor({
color: '#000'
});
}
olStyle = new this.OlStyleConstructor({
image: new this.OlStyleRegularshapeConstructor({
...shapeOpts,
points: 2,
angle: Math.PI / 4
})
});
break;
case 'shape://vertline':
// openlayers does not seem to set a default stroke color,
// which is needed for regularshapes with radius2 = 0
if (shapeOpts.stroke === undefined) {
shapeOpts.stroke = new this.OlStyleStrokeConstructor({
color: '#000'
});
}
olStyle = new this.OlStyleConstructor({
image: new this.OlStyleRegularshapeConstructor({
...shapeOpts,
points: 2,
angle: 0
})
});
break;
default:
if (OlStyleUtil.getIsFontGlyphBased(markSymbolizer)) {
olStyle = new this.OlStyleConstructor({
text: new this.OlStyleTextConstructor({
text: OlStyleUtil.getCharacterForMarkSymbolizer(markSymbolizer),
font: OlStyleUtil.getTextFontForMarkSymbolizer(markSymbolizer),
fill: shapeOpts.fill,
stroke: shapeOpts.stroke,
rotation: shapeOpts.rotation
})
});
break;
}
throw new Error('MarkSymbolizer cannot be parsed. Unsupported WellKnownName.');
}
if (Number.isFinite(opacity) && olStyle.getImage()) {
olStyle.getImage().setOpacity(opacity);
}
return olStyle;
}
/**
* Get the OL Style object from an GeoStyler-Style IconSymbolizer.
*
* @param symbolizer A GeoStyler-Style IconSymbolizer.
* @return The OL Style object
*/
getOlIconSymbolizerFromIconSymbolizer(symbolizer, feat) {
for (const key of Object.keys(symbolizer)) {
if (isGeoStylerFunction(symbolizer[key])) {
symbolizer[key] = OlStyleUtil.evaluateFunction(symbolizer[key], feat);
}
}
const baseProps = {
src: isSprite(symbolizer.image) ? symbolizer.image.source : symbolizer.image,
crossOrigin: 'anonymous',
opacity: symbolizer.opacity,
width: symbolizer.size,
// Rotation in openlayers is radians while we use degree
rotation: (typeof (symbolizer.rotate) === 'number' ? symbolizer.rotate * Math.PI / 180 : undefined),
displacement: symbolizer.offset,
size: isSprite(symbolizer.image) ? symbolizer.image.size : undefined,
offset: isSprite(symbolizer.image) ? symbolizer.image.position : undefined,
};
// check if IconSymbolizer.image contains a placeholder
const prefix = '\\{\\{';
const suffix = '\\}\\}';
const regExp = new RegExp(prefix + '.*?' + suffix, 'g');
const regExpRes = typeof (symbolizer.image) === 'string' ? symbolizer.image.match(regExp) : null;
if (regExpRes) {
// if it contains a placeholder
// r