import SimpleMetaTaggers, { SimpleMetaTagger } from "./SimpleMetaTagger" import { ExtraFuncParams, ExtraFunctions } from "./ExtraFunctions" import LayerConfig from "../Models/ThemeConfig/LayerConfig" import { ElementStorage } from "./ElementStorage" /** * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... * * All metatags start with an underscore */ export default class MetaTagging { private static errorPrintCount = 0 private static readonly stopErrorOutputAt = 10 private static retaggingFuncCache = new Map void)[]>() /** * This method (re)calculates all metatags and calculated tags on every given object. * The given features should be part of the given layer * * Returns true if at least one feature has changed properties */ public static addMetatags( features: { feature: any; freshness: Date }[], params: ExtraFuncParams, layer: LayerConfig, state?: { allElements?: ElementStorage }, options?: { includeDates?: true | boolean includeNonDates?: true | boolean evaluateStrict?: false | boolean } ): boolean { if (features === undefined || features.length === 0) { return } console.log("Recalculating metatags...") const metatagsToApply: SimpleMetaTagger[] = [] for (const metatag of SimpleMetaTaggers.metatags) { if (metatag.includesDates) { if (options?.includeDates ?? true) { metatagsToApply.push(metatag) } } else { if (options?.includeNonDates ?? true) { metatagsToApply.push(metatag) } } } // The calculated functions - per layer - which add the new keys const layerFuncs = this.createRetaggingFunc(layer, state) let atLeastOneFeatureChanged = false for (let i = 0; i < features.length; i++) { const ff = features[i] const feature = ff.feature const freshness = ff.freshness let somethingChanged = false let definedTags = new Set(Object.getOwnPropertyNames(feature.properties)) for (const metatag of metatagsToApply) { try { if (!metatag.keys.some((key) => feature.properties[key] === undefined)) { // All keys are already defined, we probably already ran this one continue } if (metatag.isLazy) { if (!metatag.keys.some((key) => !definedTags.has(key))) { // All keys are defined - lets skip! continue } somethingChanged = true metatag.applyMetaTagsOnFeature(feature, freshness, layer, state) if (options?.evaluateStrict) { for (const key of metatag.keys) { feature.properties[key] } } } else { const newValueAdded = metatag.applyMetaTagsOnFeature( feature, freshness, layer, state ) /* Note that the expression: * `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)` * Is WRONG * * IF something changed is `true` due to an earlier run, it will short-circuit and _not_ evaluate the right hand of the OR, * thus not running an update! */ somethingChanged = newValueAdded || somethingChanged } } catch (e) { console.error( "Could not calculate metatag for ", metatag.keys.join(","), ":", e, e.stack ) } } if (layerFuncs !== undefined) { let retaggingChanged = false try { retaggingChanged = layerFuncs(params, feature) } catch (e) { console.error(e) } somethingChanged = somethingChanged || retaggingChanged } if (somethingChanged) { state?.allElements?.getEventSourceById(feature.properties.id)?.ping() atLeastOneFeatureChanged = true } } return atLeastOneFeatureChanged } private static createFunctionsForFeature( layerId: string, calculatedTags: [string, string, boolean][] ): ((feature: any) => void)[] { const functions: ((feature: any) => any)[] = [] for (const entry of calculatedTags) { const key = entry[0] const code = entry[1] const isStrict = entry[2] if (code === undefined) { continue } const calculateAndAssign: (feat: any) => any = (feat) => { try { let result = new Function("feat", "return " + code + ";")(feat) if (result === "") { result === undefined } if (result !== undefined && typeof result !== "string") { // Make sure it is a string! result = JSON.stringify(result) } delete feat.properties[key] feat.properties[key] = result return result } catch (e) { if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { console.warn( "Could not calculate a " + (isStrict ? "strict " : "") + " calculated tag for key " + key + " defined by " + code + " (in layer" + layerId + ") due to \n" + e + "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e, e.stack ) MetaTagging.errorPrintCount++ if (MetaTagging.errorPrintCount == MetaTagging.stopErrorOutputAt) { console.error( "Got ", MetaTagging.stopErrorOutputAt, " errors calculating this metatagging - stopping output now" ) } } return undefined } } if (isStrict) { functions.push(calculateAndAssign) continue } // Lazy function const f = (feature: any) => { delete feature.properties[key] Object.defineProperty(feature.properties, key, { configurable: true, enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this get: function () { return calculateAndAssign(feature) }, }) return undefined } functions.push(f) } return functions } /** * Creates the function which adds all the calculated tags to a feature. Called once per layer * @param layer * @param state * @private */ private static createRetaggingFunc( layer: LayerConfig, state ): (params: ExtraFuncParams, feature: any) => boolean { const calculatedTags: [string, string, boolean][] = layer.calculatedTags if (calculatedTags === undefined || calculatedTags.length === 0) { return undefined } let functions: ((feature: any) => void)[] = MetaTagging.retaggingFuncCache.get(layer.id) if (functions === undefined) { functions = MetaTagging.createFunctionsForFeature(layer.id, calculatedTags) MetaTagging.retaggingFuncCache.set(layer.id, functions) } return (params: ExtraFuncParams, feature) => { const tags = feature.properties if (tags === undefined) { return } try { ExtraFunctions.FullPatchFeature(params, feature) for (const f of functions) { f(feature) } } catch (e) { console.error("Invalid syntax in calculated tags or some other error: ", e) } return true // Something changed } } }