geostyler-openlayers-parser
Version:
GeoStyler Style Parser implementation for OpenLayers styles
413 lines (412 loc) • 14.1 kB
JavaScript
import { isGeoStylerStringFunction as l, isGeoStylerNumberFunction as f, isGeoStylerUnknownFunction as x, isGeoStylerBooleanFunction as v, isGeoStylerFunction as c } from "geostyler-style";
import { colors as F } from "./colors.js";
const h = /^ttf:\/\/(.+)#(.+)$/, d = "geostyler-mark-symbolizer", b = Math.PI / 180;
class s {
/**
* Transforms a HEX encoded color and an opacity value to a RGB(A) notation.
*
* @param colorString HEX encoded color
* @param opacity Opacity (Betweeen 0 and 1)
* @return the RGB(A) value of the input color
*/
static getRgbaColor(t, r) {
if (l(t) && (t = s.evaluateStringFunction(t)), typeof t != "string")
return;
if (t.startsWith("rgba("))
return t;
if (!/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(t))
return;
const n = parseInt(t.slice(1, 3), 16), a = parseInt(t.slice(3, 5), 16), o = parseInt(t.slice(5, 7), 16);
return f(r) && (r = s.evaluateNumberFunction(r)), r < 0 && (r = 1), "rgba(" + n + ", " + a + ", " + o + ", " + r + ")";
}
/**
* Splits a RGBA encoded color into its color values.
*
* @param {string} rgbaColor RGB(A) encoded color
* @return {number[]} Numeric color values as array
*/
static splitRgbaColor(t) {
const r = t.substring(t.indexOf("(") + 1, t.lastIndexOf(")")).split(/,\s*/), e = parseInt(r[0], 10), n = parseInt(r[1], 10), a = parseInt(r[2], 10), o = parseFloat(r[3]);
return [e, n, a, o];
}
/**
* Transforms a RGB(A) or named color value to a HEX encoded notation.
* If a HEX color is provided it will be returned untransformed.
*
* @param {string} inColor The color to transform
* @return {string | undefined} The HEX color representation of the given color
*/
static getHexColor(t) {
if (t.startsWith("#"))
return t;
if (t.startsWith("rgb")) {
const r = s.splitRgbaColor(t);
return s.getHexCodeFromRgbArray(r);
} else if (F[t.toLocaleLowerCase()] !== void 0) {
const r = F[t.toLocaleLowerCase()];
return s.getHexCodeFromRgbArray(r);
} else
return;
}
/**
* Returns the hex code for a given RGB(A) array.
*
* @param colorArr RGB(A) array. e.g. [255,0,0]
* @return {string} The HEX color representation of the given color
*/
static getHexCodeFromRgbArray(t) {
return "#" + t.map((r, e) => {
const n = r.toString(16);
return e < 3 ? n.length === 1 ? "0" + n : n : "";
}).join("");
}
/**
* Appends an alpha value to a HEX color code to form an RGBA-like hex code.
*
* @param hexColor The HEX color code to which the alpha value will be appended.
* It should start with the '#' character.
* @param opacity The opacity value between 0 and 1, representing the alpha channel.
* @return The RGBA-like hex color code with the appended alpha value as a string,
* or undefined if the input hexColor is not a valid HEX color.
*/
static getHexAlphaFromHexAndOpacity(t, r) {
if (!t.startsWith("#"))
return;
r = Math.max(0, Math.min(1, r));
const e = Math.round(r * 255).toString(16).padStart(2, "0");
return `${t}${e}`;
}
/**
* Returns the opacity value of a RGB(A) color value.
*
* @param color RGBA or hex encoded color
* @return {string | undefined} The opacity value of the given RGBA color
*/
static getOpacity(t) {
const r = t.startsWith("rgba("), e = t.startsWith("#") && t.length > 7;
if (!r && !e)
return;
let n;
if (r ? n = s.splitRgbaColor(t) : n = [void 0, void 0, void 0, parseInt(t.slice(7, 9), 16) / 255], n.length === 4)
return n[3];
}
/**
* Checks if the given opacity value is valid.
* A valid opacity is a number between 0 and 1.
* Value 1 is ignored as this the default value.
* If the value is not valid, false is returned.
* @param opacity The opacity value to check
* @return true if the opacity is valid, false otherwise
*/
static checkOpacity(t) {
return typeof t == "number" && t >= 0 && t < 1;
}
/**
* Returns an OL compliant font string.
*
* @param symbolizer The TextSymbolizer to derive the font string from
*/
static getTextFont(t) {
const r = t.fontWeight ?? "normal", e = t.fontStyle ?? "normal", n = t.size, a = t.font;
return r + " " + e + " " + n + "px " + a?.join(", ");
}
/**
* Returns true if the given mark symbolizer is based on a font glyph
* (i.e. has a well known name property starting with 'ttf://').
*
* @param symbolizer The TextSymbolizer to derive the font string from
*/
static getIsFontGlyphBased(t) {
return h.test(t.wellKnownName);
}
/**
* Returns whether the given font (as used in the OpenLayers Text Style `font` property)
* is intended for a mark symbolizer or not.
* This is done by checking whether the dummy DUMMY_MARK_SYMBOLIZER_FONT font name is present.
*
* @param font The text font to analyze
*/
static getIsMarkSymbolizerFont(t) {
if (!t)
return !1;
const r = d;
return t.substring(t.length - r.length, t.length) === r;
}
/**
* Returns an OL compliant font string, to be used for mark symbolizers
* using a font glyph.
* This also includes a dummy DUMMY_MARK_SYMBOLIZER_FONT font name at the end of the
* string to allow determining that this font was intended for a mark symbolizer
* later on.
*
* @param symbolizer The TextSymbolizer to derive the font string from
*/
static getTextFontForMarkSymbolizer(t) {
const r = t.wellKnownName.match(h);
if (!r)
throw new Error(`Could not parse font-based well known name: ${t.wellKnownName}`);
const e = r[1];
return `Normal ${t.radius || 5}px '${e}', ${d}`;
}
/**
* Returns a 1-char string to be used as text for mark symbolizers using a font glyph.
*
* @param symbolizer The MarkSymbolizer to derive the character string from
*/
static getCharacterForMarkSymbolizer(t) {
const r = t.wellKnownName.match(h);
if (!r)
throw new Error(`Could not parse font-based well known name: ${t.wellKnownName}`);
return String.fromCharCode(parseInt(r[2], 16));
}
/**
* Returns the font name used in the OpenLayers text style `font` property.
*
* @param olFont the `font` property of an OpenLayers text style
*/
static getFontNameFromOlFont(t) {
const r = t.match(/(?:\d+\S+) '?"?([^,'"]+)/);
if (!r)
throw new Error(`Could not find font family name in the following string: ${t}`);
return r[1];
}
/**
* Returns the size in pixels specified in the OpenLayers text style `font` property,
* or 0 if not found.
*
* @param olFont the `font` property of an OpenLayers text style
*/
static getSizeFromOlFont(t) {
const r = t.match(/(?:(\d+)px)/);
return r ? parseInt(r[1], 10) : 0;
}
/**
* Resolves the given template string with the given feature attributes, e.g.
* the template "Size of area is {{AREA_SIZE}} km²" would be to resolved
* to "Size of area is 1909 km²" (assuming the feature's attribute AREA_SIZE
* really exists).
*
* @param feature The feature to get the attributes from.
* @param template The template string to resolve.
* @param [noValueFoundText] The text to apply, if the templated value
* could not be found, default is to 'n.v.'.
* @param [valueAdjust] A method that will be called with each
* key/value match, we'll use what this function returns for the actual
* replacement. Optional, defaults to a function which will return the raw
* value it received. This can be used for last minute adjustments before
* replacing happens, e.g. to filter out falsy values or to do number
* formatting and such.
* @return The resolved template string.
*/
static resolveAttributeTemplate(t, r, e = "n.v.", n = (a, o) => o) {
const a = "\\{\\{", o = "\\}\\}", u = new RegExp(a + "(.*?)" + o, "g"), g = r.match(u);
return g && g.forEach((i) => {
let m = 0;
for (const [p, w] of Object.entries(t.getProperties()))
if (i.slice(2, i.length - 2).toLowerCase() === p.toLowerCase()) {
r = r.replace(i, n(p, w));
break;
} else
m++;
m === Object.keys(t.getProperties()).length && (r = r.replace(i, e));
}), r;
}
static evaluateFunction(t, r) {
if (t.name === "property") {
if (!r)
throw new Error(`Could not evalute 'property' function. Feature ${r} is not defined.`);
return l(t.args[0]) ? r?.get(s.evaluateStringFunction(t.args[0], r)) : r?.get(t.args[0]);
}
if (x(t))
return s.evaluateUnknownFunction(t, r);
if (l(t))
return s.evaluateStringFunction(t, r);
if (f(t))
return s.evaluateNumberFunction(t, r);
if (v(t))
return s.evaluateBooleanFunction(t, r);
}
static evaluateBooleanFunction(t, r) {
const e = t.args.map((n) => c(n) ? s.evaluateFunction(n, r) : n);
switch (t.name) {
case "all":
return e.map((n) => this.evaluateBooleanFunction(n, r)).every((n) => n === !0);
case "any":
return e.map((n) => this.evaluateBooleanFunction(n, r)).some((n) => n === !0);
case "between":
return e[0] >= e[1] && e[0] <= e[2];
case "double2bool":
return !1;
case "equalTo":
return e[0] === e[1];
case "greaterThan":
return e[0] > e[1];
case "greaterThanOrEqualTo":
return e[0] >= e[1];
case "in":
return e.slice(1).includes(e[0]);
case "lessThan":
return e[0] < e[1];
case "lessThanOrEqualTo":
return e[0] <= e[1];
case "not":
return !e[0];
case "notEqualTo":
return e[0] !== e[1];
case "parseBoolean":
return !!e[0];
case "strEndsWith":
return e[0].endsWith(e[1]);
case "strEqualsIgnoreCase":
return e[0].toLowerCase() === e[1].toLowerCase();
case "strMatches": {
const a = e[1].match(/\/(.*?)\/([gimy]{0,4})$/);
return a && a.length === 3 ? new RegExp(a[1], a[2]).test(e[0]) : !1;
}
case "strStartsWith":
return e[0].startsWith(e[1]);
default:
return !1;
}
}
static evaluateNumberFunction(t, r) {
if (t.name === "pi")
return Math.PI;
if (t.name === "random")
return Math.random();
const e = t.args.map((n) => c(n) ? s.evaluateFunction(n, r) : n);
switch (t.name) {
case "abs":
return Math.abs(e[0]);
case "acos":
return Math.acos(e[0]);
case "add":
return e[0] + e[1];
case "asin":
return Math.asin(e[0]);
case "atan":
return Math.atan(e[0]);
case "atan2":
return e[0];
case "ceil":
return Math.ceil(e[0]);
case "cos":
return Math.cos(e[0]);
case "div":
return e[0] / e[1];
case "exp":
return Math.exp(e[0]);
case "floor":
return Math.floor(e[0]);
case "log":
return Math.log(e[0]);
case "max":
return Math.max(...e);
case "min":
return Math.min(...e);
case "modulo":
return e[0] % e[1];
case "mul":
return e[0] * e[1];
case "pow":
return Math.pow(e[0], e[1]);
case "rint":
return e[0];
case "round":
return Math.round(e[0]);
case "sin":
return Math.sin(e[0]);
case "sqrt":
return Math.sqrt(e[0]);
case "strIndexOf":
return e[0].indexOf(e[1]);
case "strLastIndexOf":
return e[0].lastIndexOf(e[1]);
case "strLength":
return e[0].length;
case "sub":
return e[0] - e[1];
case "tan":
return Math.tan(e[0]);
case "toDegrees":
return e[0] / b;
case "toRadians":
return e[0] * b;
default:
return e[0];
}
}
static evaluateUnknownFunction(t, r) {
const e = t.args.map((n) => c(n) ? s.evaluateFunction(n, r) : n);
switch (t.name) {
case "property":
return r?.get(e[0]);
case "case": {
const n = e;
let a;
for (let o = 0; o < n.length; o++) {
const u = n[o];
if (o === n.length - 1) {
a = u;
break;
} else if (u.case === !0) {
a = u.value;
break;
} else if (s.evaluateBooleanFunction(u.case, r)) {
a = u.value;
break;
}
}
return a;
}
default:
return e[0];
}
}
static evaluateStringFunction(t, r) {
const e = t.args.map((n) => c(n) ? s.evaluateFunction(n, r) : n);
switch (t.name) {
case "numberFormat":
return e[0];
case "strAbbreviate":
return e[0];
case "strCapitalize": {
const n = e[0].toLowerCase().split(" ");
for (let a of n)
a = a.charAt(0).toUpperCase() + a.substring(1);
return n.join(" ");
}
case "strConcat":
return e.join();
case "strDefaultIfBlank":
return e[0]?.length < 1 ? e[1] : e[0];
case "strReplace":
return e[3] === !0 ? e[0].replaceAll(e[1], e[2]) : e[0].replace(e[1], e[2]);
case "strStripAccents":
return e[0].normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
case "strSubstring":
return e[0].substring(e[1], e[2]);
case "strSubstringStart":
return e[0].substring(e[1]);
case "strToLowerCase":
return e[0].toLowerCase();
case "strToUpperCase":
return e[0].toUpperCase();
case "strTrim":
return e[0].trim();
default:
return e[0];
}
}
static containsGeoStylerFunctions(t) {
return t.rules.some((r) => {
const e = Array.isArray(r.filter) && r.filter?.some(c), n = r.symbolizers?.some((o) => Object.values(o).some(c)), a = c(r?.scaleDenominator?.max) || c(r?.scaleDenominator?.min);
return e || n || a;
});
}
}
export {
b as DEGREES_TO_RADIANS,
d as DUMMY_MARK_SYMBOLIZER_FONT,
s as default
};