UNPKG

geostyler-openlayers-parser

Version:

GeoStyler Style Parser implementation for OpenLayers styles

413 lines (412 loc) 14.1 kB
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 };