diff --git a/Models/ThemeConfig/Json/LayerConfigJson.ts b/Models/ThemeConfig/Json/LayerConfigJson.ts index 69d1b972ed..eefd180851 100644 --- a/Models/ThemeConfig/Json/LayerConfigJson.ts +++ b/Models/ThemeConfig/Json/LayerConfigJson.ts @@ -4,6 +4,7 @@ import FilterConfigJson from "./FilterConfigJson"; import {DeleteConfigJson} from "./DeleteConfigJson"; import UnitConfigJson from "./UnitConfigJson"; import MoveConfigJson from "./MoveConfigJson"; +import PointRenderingConfigJson from "./PointRenderingConfigJson"; /** * Configuration for a single layer @@ -119,47 +120,8 @@ export interface LayerConfigJson { */ titleIcons?: (string | TagRenderingConfigJson)[]; - /** - * The icon for an element. - * Note that this also doubles as the icon for this layer (rendered with the overpass-tags) ánd the icon in the presets. - * - * The result of the icon is rendered as follows: - * the resulting string is interpreted as a _list_ of items, separated by ";". The bottommost layer is the first layer. - * As a result, on could use a generic pin, then overlay it with a specific icon. - * To make things even more practical, one can use all SVG's from the folder "assets/svg" and _substitute the color_ in it. - * E.g. to draw a red pin, use "pin:#f00", to have a green circle with your icon on top, use `circle:#0f0;` - * - */ - icon?: string | TagRenderingConfigJson; - /** - * IconsOverlays are a list of extra icons/badges to overlay over the icon. - * The 'badge'-toggle changes their behaviour. - * If badge is set, it will be added as a 25% height icon at the bottom right of the icon, with all the badges in a flex layout. - * If badges is false, it'll be a simple overlay - * - * Note: strings are interpreted as icons, so layering and substituting is supported - */ - iconOverlays?: { if: string | AndOrTagConfigJson, then: string | TagRenderingConfigJson, badge?: boolean }[] - - /** - * A string containing "width,height" or "width,height,anchorpoint" where anchorpoint is any of 'center', 'top', 'bottom', 'left', 'right', 'bottomleft','topright', ... - * Default is '40,40,center' - */ - iconSize?: string | TagRenderingConfigJson; - /** - * The rotation of an icon, useful for e.g. directions. - * Usage: as if it were a css property for 'rotate', thus has to end with 'deg', e.g. `90deg`, `{direction}deg`, `calc(90deg - {camera:direction}deg)`` - */ - rotation?: string | TagRenderingConfigJson; - /** - * A HTML-fragment that is shown below the icon, for example: - *
{name}
- * - * If the icon is undefined, then the label is shown in the center of the feature. - * Note that, if the wayhandling hides the icon then no label is shown as well. - */ - label?: string | TagRenderingConfigJson; + mapRendering: PointRenderingConfigJson[] /** * The color for way-elements and SVG-elements. diff --git a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts new file mode 100644 index 0000000000..694d98f261 --- /dev/null +++ b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts @@ -0,0 +1,62 @@ +import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; +import {AndOrTagConfigJson} from "./TagConfigJson"; + +/** + * The PointRenderingConfig gives all details onto how to render a single point of a feature. + * + * This can be used if: + * + * - The feature is a point + * - To render something at the centroid of an area, or at the start, end or projected centroid of a way + */ +export default interface PointRenderingConfigJson { + + /** + * All the locations that this point should be rendered at. + * Using `location: ["point", "centroid"] will always render centerpoint + */ + location: ("point" | "centroid")[] + + /** + * The icon for an element. + * Note that this also doubles as the icon for this layer (rendered with the overpass-tags) ánd the icon in the presets. + * + * The result of the icon is rendered as follows: + * the resulting string is interpreted as a _list_ of items, separated by ";". The bottommost layer is the first layer. + * As a result, on could use a generic pin, then overlay it with a specific icon. + * To make things even more practical, one can use all SVG's from the folder "assets/svg" and _substitute the color_ in it. + * E.g. to draw a red pin, use "pin:#f00", to have a green circle with your icon on top, use `circle:#0f0;` + * + */ + icon?: string | TagRenderingConfigJson; + + /** + * IconsOverlays are a list of extra icons/badges to overlay over the icon. + * The 'badge'-toggle changes their behaviour. + * If badge is set, it will be added as a 25% height icon at the bottom right of the icon, with all the badges in a flex layout. + * If badges is false, it'll be a simple overlay + * + * Note: strings are interpreted as icons, so layering and substituting is supported + */ + iconOverlays?: { if: string | AndOrTagConfigJson, then: string | TagRenderingConfigJson, badge?: boolean }[] + + + /** + * A string containing "width,height" or "width,height,anchorpoint" where anchorpoint is any of 'center', 'top', 'bottom', 'left', 'right', 'bottomleft','topright', ... + * Default is '40,40,center' + */ + iconSize?: string | TagRenderingConfigJson; + /** + * The rotation of an icon, useful for e.g. directions. + * Usage: as if it were a css property for 'rotate', thus has to end with 'deg', e.g. `90deg`, `{direction}deg`, `calc(90deg - {camera:direction}deg)`` + */ + rotation?: string | TagRenderingConfigJson; + /** + * A HTML-fragment that is shown below the icon, for example: + *
{name}
+ * + * If the icon is undefined, then the label is shown in the center of the feature. + * Note that, if the wayhandling hides the icon then no label is shown as well. + */ + label?: string | TagRenderingConfigJson; +} \ No newline at end of file diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index bf365ee6ab..6241a37ff9 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -6,22 +6,17 @@ import PresetConfig from "./PresetConfig"; import {LayerConfigJson} from "./Json/LayerConfigJson"; import Translations from "../../UI/i18n/Translations"; import {TagUtils} from "../../Logic/Tags/TagUtils"; -import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; -import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson"; import {Utils} from "../../Utils"; import {UIEventSource} from "../../Logic/UIEventSource"; import BaseUIElement from "../../UI/BaseUIElement"; -import {FixedUiElement} from "../../UI/Base/FixedUiElement"; -import Combine from "../../UI/Base/Combine"; -import {VariableUiElement} from "../../UI/Base/VariableUIElement"; import FilterConfig from "./FilterConfig"; import {Unit} from "../Unit"; import DeleteConfig from "./DeleteConfig"; -import Svg from "../../Svg"; -import Img from "../../UI/Base/Img"; import MoveConfig from "./MoveConfig"; +import PointRenderingConfig from "./PointRenderingConfig"; +import WithContextLoader from "./WithContextLoader"; -export default class LayerConfig { +export default class LayerConfig extends WithContextLoader{ static WAYHANDLING_DEFAULT = 0; static WAYHANDLING_CENTER_ONLY = 1; static WAYHANDLING_CENTER_AND_WAY = 2; @@ -39,11 +34,9 @@ export default class LayerConfig { maxzoom: number; title?: TagRenderingConfig; titleIcons: TagRenderingConfig[]; - icon: TagRenderingConfig; - iconOverlays: { if: TagsFilter; then: TagRenderingConfig; badge: boolean }[]; - iconSize: TagRenderingConfig; - label: TagRenderingConfig; - rotation: TagRenderingConfig; + + public readonly mapRendering: PointRenderingConfig[] + color: TagRenderingConfig; width: TagRenderingConfig; dashArray: TagRenderingConfig; @@ -63,25 +56,9 @@ export default class LayerConfig { context?: string, official: boolean = true ) { - context = context + "." + json.id; - const self = this; + super(json, context) this.id = json.id; - this.allowSplit = json.allowSplit ?? false; - this.name = Translations.T(json.name, context + ".name"); - this.units = (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`))) - - if (json.description !== undefined) { - if (Object.keys(json.description).length === 0) { - json.description = undefined; - } - } - - this.description = Translations.T( - json.description, - context + ".description" - ); - let legacy = undefined; if (json["overpassTags"] !== undefined) { // @ts-ignore @@ -111,7 +88,7 @@ export default class LayerConfig { throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)"; } - this.source = new SourceConfig( + this. source = new SourceConfig( { osmTags: osmTags, geojsonSource: json.source["geoJson"], @@ -119,14 +96,33 @@ export default class LayerConfig { overpassScript: json.source["overpassScript"], isOsmCache: json.source["isOsmCache"], }, - this.id + json.id ); } else { - this.source = new SourceConfig({ + this. source = new SourceConfig({ osmTags: legacy, }); } + + + this.id = json.id; + this.allowSplit = json.allowSplit ?? false; + this.name = Translations.T(json.name, context + ".name"); + this.units = (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`))) + + if (json.description !== undefined) { + if (Object.keys(json.description).length === 0) { + json.description = undefined; + } + } + + this.description = Translations.T( + json.description, + context + ".description" + ); + + this.calculatedTags = undefined; if (json.calculatedTags !== undefined) { if (!official) { @@ -202,101 +198,15 @@ export default class LayerConfig { return config; }); - /** Given a key, gets the corresponding property from the json (or the default if not found - * - * The found value is interpreted as a tagrendering and fetched/parsed - * */ - function tr(key: string, deflt) { - const v = json[key]; - if (v === undefined || v === null) { - if (deflt === undefined) { - return undefined; - } - return new TagRenderingConfig( - deflt, - self.source.osmTags, - `${context}.${key}.default value` - ); - } - if (typeof v === "string") { - const shared = SharedTagRenderings.SharedTagRendering.get(v); - if (shared) { - return shared; - } - } - return new TagRenderingConfig( - v, - self.source.osmTags, - `${context}.${key}` - ); + + + this.mapRendering = json.mapRendering.map((r, i) => new PointRenderingConfig(r, context+".mapRendering["+i+"]")) + + if(this.mapRendering.length > 1){ + throw "Invalid maprendering for "+this.id+", currently only one mapRendering is supported!" } - /** - * Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig - * A string is interpreted as a name to call - */ - function trs( - tagRenderings?: (string | { builtin: string, override: any } | TagRenderingConfigJson)[], - readOnly = false - ) { - if (tagRenderings === undefined) { - return []; - } - - return Utils.NoNull( - tagRenderings.map((renderingJson, i) => { - if (typeof renderingJson === "string") { - renderingJson = {builtin: renderingJson, override: undefined} - } - - if (renderingJson["builtin"] !== undefined) { - const renderingId = renderingJson["builtin"] - if (renderingId === "questions") { - if (readOnly) { - throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}. The offending tagrendering is ${JSON.stringify( - renderingJson - )}`; - } - - return new TagRenderingConfig("questions", undefined, context); - } - - if (renderingJson["override"] !== undefined) { - const sharedJson = SharedTagRenderings.SharedTagRenderingJson.get(renderingId) - return new TagRenderingConfig( - Utils.Merge(renderingJson["override"], sharedJson), - self.source.osmTags, - `${context}.tagrendering[${i}]+override` - ); - } - - const shared = SharedTagRenderings.SharedTagRendering.get(renderingId); - - if (shared !== undefined) { - return shared; - } - if (Utils.runningFromConsole) { - return undefined; - } - - const keys = Array.from( - SharedTagRenderings.SharedTagRendering.keys() - ); - throw `Predefined tagRendering ${renderingId} not found in ${context}.\n Try one of ${keys.join( - ", " - )}\n If you intent to output this text literally, use {\"render\": } instead"}`; - } - - return new TagRenderingConfig( - renderingJson, - self.source.osmTags, - `${context}.tagrendering[${i}]` - ); - }) - ); - } - - this.tagRenderings = trs(json.tagRenderings, false); + this.tagRenderings = this.trs(json.tagRenderings, false); const missingIds = json.tagRenderings?.filter(tr => typeof tr !== "string" && tr["builtin"] === undefined && tr["id"] === undefined) ?? []; @@ -329,43 +239,13 @@ export default class LayerConfig { } } - this.titleIcons = trs(titleIcons, true); + this.titleIcons = this.trs(titleIcons, true); - this.title = tr("title", undefined); - this.icon = tr("icon", ""); - this.iconOverlays = (json.iconOverlays ?? []).map((overlay, i) => { - let tr = new TagRenderingConfig( - overlay.then, - self.source.osmTags, - `iconoverlays.${i}` - ); - if ( - typeof overlay.then === "string" && - SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined - ) { - tr = SharedTagRenderings.SharedIcons.get(overlay.then); - } - return { - if: TagUtils.Tag(overlay.if), - then: tr, - badge: overlay.badge ?? false, - }; - }); - - const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt; - if (iconPath.startsWith(Utils.assets_path)) { - const iconKey = iconPath.substr(Utils.assets_path.length); - if (Svg.All[iconKey] === undefined) { - throw "Builtin SVG asset not found: " + iconPath; - } - } - this.isShown = tr("isShown", "yes"); - this.iconSize = tr("iconSize", "40,40,center"); - this.label = tr("label", ""); - this.color = tr("color", "#0000ff"); - this.width = tr("width", "7"); - this.rotation = tr("rotation", "0"); - this.dashArray = tr("dashArray", ""); + this.title = this.tr("title", undefined); + this.isShown = this.tr("isShown", "yes"); + this.color = this.tr("color", "#0000ff"); + this.width = this.tr("width", "7"); + this.dashArray = this.tr("dashArray", ""); this.deletion = null; if (json.deletion === true) { @@ -398,54 +278,9 @@ export default class LayerConfig { if (this.calculatedTags === undefined) { return []; } - return this.calculatedTags.map((code) => code[1]); } - public AddRoamingRenderings(addAll: { - tagRenderings: TagRenderingConfig[]; - titleIcons: TagRenderingConfig[]; - iconOverlays: { - if: TagsFilter; - then: TagRenderingConfig; - badge: boolean; - }[]; - }): LayerConfig { - let insertionPoint = this.tagRenderings - .map((tr) => tr.IsQuestionBoxElement()) - .indexOf(true); - if (insertionPoint < 0) { - // No 'questions' defined - we just add them all to the end - insertionPoint = this.tagRenderings.length; - } - this.tagRenderings.splice(insertionPoint, 0, ...addAll.tagRenderings); - - this.iconOverlays.push(...addAll.iconOverlays); - for (const icon of addAll.titleIcons) { - this.titleIcons.splice(0, 0, icon); - } - return this; - } - - public GetRoamingRenderings(): { - tagRenderings: TagRenderingConfig[]; - titleIcons: TagRenderingConfig[]; - iconOverlays: { - if: TagsFilter; - then: TagRenderingConfig; - badge: boolean; - }[]; - } { - const tagRenderings = this.tagRenderings.filter((tr) => tr.roaming); - const titleIcons = this.titleIcons.filter((tr) => tr.roaming); - const iconOverlays = this.iconOverlays.filter((io) => io.then.roaming); - - return { - tagRenderings: tagRenderings, - titleIcons: titleIcons, - iconOverlays: iconOverlays, - }; - } public GenerateLeafletStyle( tags: UIEventSource, @@ -463,13 +298,6 @@ export default class LayerConfig { weight: number; dashArray: number[]; } { - function num(str, deflt = 40) { - const n = Number(str); - if (isNaN(n)) { - return deflt; - } - return n; - } function rendernum(tr: TagRenderingConfig, deflt: number) { const str = Number(render(tr, "" + deflt)); @@ -488,7 +316,6 @@ export default class LayerConfig { return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, ""); } - const iconSize = render(this.iconSize, "40,40,center").split(","); const dashArray = render(this.dashArray)?.split(" ")?.map(Number); let color = render(this.color, "#00f"); @@ -500,134 +327,10 @@ export default class LayerConfig { const weight = rendernum(this.width, 5); - const iconW = num(iconSize[0]); - let iconH = num(iconSize[1]); - const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center"; - - let anchorW = iconW / 2; - let anchorH = iconH / 2; - if (mode === "left") { - anchorW = 0; - } - if (mode === "right") { - anchorW = iconW; - } - - if (mode === "top") { - anchorH = 0; - } - if (mode === "bottom") { - anchorH = iconH; - } - - const iconUrlStatic = render(this.icon); - const self = this; - - function genHtmlFromString(sourcePart: string, rotation: string): BaseUIElement { - const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; - let html: BaseUIElement = new FixedUiElement( - `` - ); - const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/); - if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { - html = new Img( - (Svg.All[match[1] + ".svg"] as string).replace( - /#000000/g, - match[2] - ), - true - ).SetStyle(style); - } - return html; - } - - - const mappedHtml = tags?.map((tgs) => { - // What do you mean, 'tgs' is never read? - // It is read implicitly in the 'render' method - const iconUrl = render(self.icon); - const rotation = render(self.rotation, "0deg"); - - let htmlParts: BaseUIElement[] = []; - let sourceParts = Utils.NoNull( - iconUrl.split(";").filter((prt) => prt != "") - ); - for (const sourcePart of sourceParts) { - htmlParts.push(genHtmlFromString(sourcePart, rotation)); - } - - let badges = []; - for (const iconOverlay of self.iconOverlays) { - if (!iconOverlay.if.matchesProperties(tgs)) { - continue; - } - if (iconOverlay.badge) { - const badgeParts: BaseUIElement[] = []; - const renderValue = iconOverlay - .then - .GetRenderValue(tgs) - - if (renderValue === undefined) { - continue; - } - - const partDefs = renderValue.txt.split(";") - .filter((prt) => prt != ""); - - for (const badgePartStr of partDefs) { - badgeParts.push(genHtmlFromString(badgePartStr, "0")); - } - - const badgeCompound = new Combine(badgeParts).SetStyle( - "display:flex;position:relative;width:100%;height:100%;" - ); - - badges.push(badgeCompound); - } else { - htmlParts.push( - genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt, "0") - ); - } - } - - if (badges.length > 0) { - const badgesComponent = new Combine(badges).SetStyle( - "display:flex;height:50%;width:100%;position:absolute;top:50%;left:50%;" - ); - htmlParts.push(badgesComponent); - } - - if (sourceParts.length == 0) { - iconH = 0; - } - try { - const label = self.label - ?.GetRenderValue(tgs) - ?.Subs(tgs) - ?.SetClass("block text-center") - ?.SetStyle("margin-top: " + (iconH + 2) + "px"); - if (label !== undefined) { - htmlParts.push( - new Combine([label]).SetClass("flex flex-col items-center") - ); - } - } catch (e) { - console.error(e, tgs); - } - return new Combine(htmlParts); - }); - + const icon = this.mapRendering[0].GenerateLeafletStyle(tags, clickable) + return { - icon: { - html: mappedHtml === undefined ? new FixedUiElement(self.icon.render.txt) : new VariableUiElement(mappedHtml), - iconSize: [iconW, iconH], - iconAnchor: [anchorW, anchorH], - popupAnchor: [0, 3 - anchorH], - iconUrl: iconUrlStatic, - className: clickable - ? "leaflet-div-icon" - : "leaflet-div-icon unclickable", - }, + icon, color: color, weight: weight, dashArray: dashArray, @@ -638,18 +341,17 @@ export default class LayerConfig { const parts: Set[] = []; parts.push(...this.tagRenderings?.map((tr) => tr.ExtractImages(false))); parts.push(...this.titleIcons?.map((tr) => tr.ExtractImages(true))); - parts.push(this.icon?.ExtractImages(true)); - parts.push( - ...this.iconOverlays?.map((overlay) => overlay.then.ExtractImages(true)) - ); for (const preset of this.presets) { parts.push(new Set(preset.description?.ExtractImages(false))); } - + for (const pointRenderingConfig of this.mapRendering) { + parts.push(pointRenderingConfig.ExtractImages()) + } const allIcons = new Set(); for (const part of parts) { part?.forEach(allIcons.add, allIcons); } + return allIcons; } diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index 98e0515358..56d169fc54 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -25,7 +25,6 @@ export default class LayoutConfig { public readonly startLat: number; public readonly startLon: number; public readonly widenFactor: number; - public readonly roamingRenderings: TagRenderingConfig[]; public readonly defaultBackgroundId?: string; public layers: LayerConfig[]; public tileLayerSources: TilesourceConfig[] @@ -97,44 +96,11 @@ export default class LayoutConfig { throw "Widenfactor is very big, use a value between 1 and 5 (current value is "+json.widenFactor+") at "+context } this.widenFactor = json.widenFactor ?? 1.5; - this.roamingRenderings = (json.roamingRenderings ?? []).map((tr, i) => { - if (typeof tr === "string") { - if (SharedTagRenderings.SharedTagRendering.get(tr) !== undefined) { - return SharedTagRenderings.SharedTagRendering.get(tr); - } - } - return new TagRenderingConfig(tr, undefined, `${this.id}.roaming_renderings[${i}]`); - } - ); + this.defaultBackgroundId = json.defaultBackgroundId; this.tileLayerSources = (json.tileLayerSources??[]).map((config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`)) this.layers = LayoutConfig.ExtractLayers(json, official, context); - // ALl the layers are constructed, let them share tagRenderings now! - const roaming: { r, source: LayerConfig }[] = [] - for (const layer of this.layers) { - roaming.push({r: layer.GetRoamingRenderings(), source: layer}); - } - - for (const layer of this.layers) { - for (const r of roaming) { - if (r.source == layer) { - continue; - } - layer.AddRoamingRenderings(r.r); - } - } - - for (const layer of this.layers) { - layer.AddRoamingRenderings( - { - titleIcons: [], - iconOverlays: [], - tagRenderings: this.roamingRenderings - } - ); - } - this.clustering = { maxZoom: 16, minNeededElements: 25, diff --git a/Models/ThemeConfig/PointRenderingConfig.ts b/Models/ThemeConfig/PointRenderingConfig.ts new file mode 100644 index 0000000000..75f3820290 --- /dev/null +++ b/Models/ThemeConfig/PointRenderingConfig.ts @@ -0,0 +1,244 @@ +import PointRenderingConfigJson from "./Json/PointRenderingConfigJson"; +import TagRenderingConfig from "./TagRenderingConfig"; +import {TagsFilter} from "../../Logic/Tags/TagsFilter"; +import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; +import {TagUtils} from "../../Logic/Tags/TagUtils"; +import {Utils} from "../../Utils"; +import Svg from "../../Svg"; +import WithContextLoader from "./WithContextLoader"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../../UI/BaseUIElement"; +import {FixedUiElement} from "../../UI/Base/FixedUiElement"; +import Img from "../../UI/Base/Img"; +import Combine from "../../UI/Base/Combine"; +import {VariableUiElement} from "../../UI/Base/VariableUIElement"; + +export default class PointRenderingConfig extends WithContextLoader { + + public readonly icon: TagRenderingConfig; + public readonly iconOverlays: { if: TagsFilter; then: TagRenderingConfig; badge: boolean }[]; + public readonly iconSize: TagRenderingConfig; + public readonly label: TagRenderingConfig; + public readonly rotation: TagRenderingConfig; + + constructor(json: PointRenderingConfigJson, context: string) { + super(json, context) + this.icon = this.tr("icon", ""); + this.iconOverlays = (json.iconOverlays ?? []).map((overlay, i) => { + let tr = new TagRenderingConfig( + overlay.then, + undefined, + `iconoverlays.${i}` + ); + if ( + typeof overlay.then === "string" && + SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined + ) { + tr = SharedTagRenderings.SharedIcons.get(overlay.then); + } + return { + if: TagUtils.Tag(overlay.if), + then: tr, + badge: overlay.badge ?? false, + }; + }); + + const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt; + if (iconPath.startsWith(Utils.assets_path)) { + const iconKey = iconPath.substr(Utils.assets_path.length); + if (Svg.All[iconKey] === undefined) { + throw "Builtin SVG asset not found: " + iconPath; + } + } + this.iconSize = this.tr("iconSize", "40,40,center"); + this.label = this.tr("label", ""); + this.rotation = this.tr("rotation", "0"); + } + + + public ExtractImages(): Set { + const parts: Set[] = []; + parts.push(this.icon?.ExtractImages(true)); + parts.push( + ...this.iconOverlays?.map((overlay) => overlay.then.ExtractImages(true)) + ); + + const allIcons = new Set(); + for (const part of parts) { + part?.forEach(allIcons.add, allIcons); + } + return allIcons; + } + + + + public GenerateLeafletStyle( + tags: UIEventSource, + clickable: boolean + ): + { + html: BaseUIElement; + iconSize: [number, number]; + iconAnchor: [number, number]; + popupAnchor: [number, number]; + iconUrl: string; + className: string; + } + { + function num(str, deflt = 40) { + const n = Number(str); + if (isNaN(n)) { + return deflt; + } + return n; + } + + function rendernum(tr: TagRenderingConfig, deflt: number) { + const str = Number(render(tr, "" + deflt)); + const n = Number(str); + if (isNaN(n)) { + return deflt; + } + return n; + } + + function render(tr: TagRenderingConfig, deflt?: string) { + if (tags === undefined) { + return deflt + } + const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt; + return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, ""); + } + + const iconSize = render(this.iconSize, "40,40,center").split(","); + + const iconW = num(iconSize[0]); + let iconH = num(iconSize[1]); + const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center"; + + let anchorW = iconW / 2; + let anchorH = iconH / 2; + if (mode === "left") { + anchorW = 0; + } + if (mode === "right") { + anchorW = iconW; + } + + if (mode === "top") { + anchorH = 0; + } + if (mode === "bottom") { + anchorH = iconH; + } + + const iconUrlStatic = render(this.icon); + const self = this; + + function genHtmlFromString(sourcePart: string, rotation: string): BaseUIElement { + const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; + let html: BaseUIElement = new FixedUiElement( + `` + ); + const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/); + if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { + html = new Img( + (Svg.All[match[1] + ".svg"] as string).replace( + /#000000/g, + match[2] + ), + true + ).SetStyle(style); + } + return html; + } + + + const mappedHtml = tags?.map((tgs) => { + // What do you mean, 'tgs' is never read? + // It is read implicitly in the 'render' method + const iconUrl = render(self.icon); + const rotation = render(self.rotation, "0deg"); + + let htmlParts: BaseUIElement[] = []; + let sourceParts = Utils.NoNull( + iconUrl.split(";").filter((prt) => prt != "") + ); + for (const sourcePart of sourceParts) { + htmlParts.push(genHtmlFromString(sourcePart, rotation)); + } + + let badges = []; + for (const iconOverlay of self.iconOverlays) { + if (!iconOverlay.if.matchesProperties(tgs)) { + continue; + } + if (iconOverlay.badge) { + const badgeParts: BaseUIElement[] = []; + const renderValue = iconOverlay + .then + .GetRenderValue(tgs) + + if (renderValue === undefined) { + continue; + } + + const partDefs = renderValue.txt.split(";") + .filter((prt) => prt != ""); + + for (const badgePartStr of partDefs) { + badgeParts.push(genHtmlFromString(badgePartStr, "0")); + } + + const badgeCompound = new Combine(badgeParts).SetStyle( + "display:flex;position:relative;width:100%;height:100%;" + ); + + badges.push(badgeCompound); + } else { + htmlParts.push( + genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt, "0") + ); + } + } + + if (badges.length > 0) { + const badgesComponent = new Combine(badges).SetStyle( + "display:flex;height:50%;width:100%;position:absolute;top:50%;left:50%;" + ); + htmlParts.push(badgesComponent); + } + + if (sourceParts.length == 0) { + iconH = 0; + } + try { + const label = self.label + ?.GetRenderValue(tgs) + ?.Subs(tgs) + ?.SetClass("block text-center") + ?.SetStyle("margin-top: " + (iconH + 2) + "px"); + if (label !== undefined) { + htmlParts.push( + new Combine([label]).SetClass("flex flex-col items-center") + ); + } + } catch (e) { + console.error(e, tgs); + } + return new Combine(htmlParts); + }); + + return { + html: mappedHtml === undefined ? new FixedUiElement(self.icon.render.txt) : new VariableUiElement(mappedHtml), + iconSize: [iconW, iconH], + iconAnchor: [anchorW, anchorH], + popupAnchor: [0, 3 - anchorH], + iconUrl: iconUrlStatic, + className: clickable + ? "leaflet-div-icon" + : "leaflet-div-icon unclickable", + }; + } + +} \ No newline at end of file diff --git a/Models/ThemeConfig/TagRenderingConfig.ts b/Models/ThemeConfig/TagRenderingConfig.ts index 4edd0da1d2..a498894d1a 100644 --- a/Models/ThemeConfig/TagRenderingConfig.ts +++ b/Models/ThemeConfig/TagRenderingConfig.ts @@ -221,7 +221,6 @@ export default class TagRenderingConfig { } } - /** * Returns true if it is known or not shown, false if the question should be asked * @constructor diff --git a/Models/ThemeConfig/WithContextLoader.ts b/Models/ThemeConfig/WithContextLoader.ts new file mode 100644 index 0000000000..9a4758a231 --- /dev/null +++ b/Models/ThemeConfig/WithContextLoader.ts @@ -0,0 +1,117 @@ +import TagRenderingConfig from "./TagRenderingConfig"; +import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; +import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson"; +import {Utils} from "../../Utils"; + +export default class WithContextLoader { + private readonly _json: any; + private readonly _context: string; + + constructor(json: any, context: string) { + this._json = json; + this._context = context; + } + + /** Given a key, gets the corresponding property from the json (or the default if not found + * + * The found value is interpreted as a tagrendering and fetched/parsed + * */ + public tr(key: string, deflt) { + const v = this._json[key]; + if (v === undefined || v === null) { + if (deflt === undefined) { + return undefined; + } + return new TagRenderingConfig( + deflt, + undefined, + `${this._context}.${key}.default value` + ); + } + if (typeof v === "string") { + const shared = SharedTagRenderings.SharedTagRendering.get(v); + if (shared) { + return shared; + } + } + return new TagRenderingConfig( + v, + undefined, + `${this._context}.${key}` + ); + } + + /** + * Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig + * A string is interpreted as a name to call + */ + public trs( + tagRenderings?: (string | { builtin: string, override: any } | TagRenderingConfigJson)[], + readOnly = false + ) { + if (tagRenderings === undefined) { + return []; + } + + const context = this._context + + const renderings: TagRenderingConfig[] = [] + + for (let i = 0; i < tagRenderings.length; i++) { + let renderingJson= tagRenderings[i] + if (typeof renderingJson === "string") { + renderingJson = {builtin: renderingJson, override: undefined} + } + + if (renderingJson["builtin"] !== undefined) { + const renderingId = renderingJson["builtin"] + if (renderingId === "questions") { + if (readOnly) { + throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}. The offending tagrendering is ${JSON.stringify( + renderingJson + )}`; + } + + const tr = new TagRenderingConfig("questions", undefined, context); + renderings.push(tr) + continue; + } + + if (renderingJson["override"] !== undefined) { + const sharedJson = SharedTagRenderings.SharedTagRenderingJson.get(renderingId) + const tr = new TagRenderingConfig( + Utils.Merge(renderingJson["override"], sharedJson), + undefined, + `${context}.tagrendering[${i}]+override` + ); + renderings.push(tr) + continue + } + + const shared = SharedTagRenderings.SharedTagRendering.get(renderingId); + + if (shared !== undefined) { + renderings.push( shared) + continue + } + if (Utils.runningFromConsole) { + continue + } + + const keys = Array.from( SharedTagRenderings.SharedTagRendering.keys() ); + throw `Predefined tagRendering ${renderingId} not found in ${context}.\n Try one of ${keys.join( + ", " + )}\n If you intent to output this text literally, use {\"render\": } instead"}`; + } + + const tr = new TagRenderingConfig( + renderingJson, + undefined, + `${context}.tagrendering[${i}]` + ); + renderings.push(tr) + } + + return renderings; + } +} \ No newline at end of file diff --git a/scripts/lint.ts b/scripts/lint.ts index a4f3902a64..35632ed208 100644 --- a/scripts/lint.ts +++ b/scripts/lint.ts @@ -1,4 +1,3 @@ - /* * This script reads all theme and layer files and reformats them inplace * Use with caution, make a commit beforehand! @@ -6,29 +5,47 @@ import ScriptUtils from "./ScriptUtils"; -import {readFileSync, writeFileSync} from "fs"; -import {tag} from "@turf/turf"; +import {writeFileSync} from "fs"; import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; /** * In place fix */ -function fixLayerConfig(config: LayerConfigJson) : void{ - if(config.tagRenderings === undefined){ - return - } - - for (const tagRendering of config.tagRenderings) { - if(tagRendering["#"] !== undefined){ - tagRendering["id"] = tagRendering["#"] - delete tagRendering["#"] - } - if(tagRendering["id"] === undefined){ - if(tagRendering["freeform"]?.key !== undefined ) { - tagRendering["id"] = config.id+"-"+tagRendering["freeform"]["key"] +function fixLayerConfig(config: LayerConfigJson): void { + if (config.tagRenderings !== undefined) { + for (const tagRendering of config.tagRenderings) { + if (tagRendering["#"] !== undefined) { + tagRendering["id"] = tagRendering["#"] + delete tagRendering["#"] + } + if (tagRendering["id"] === undefined) { + if (tagRendering["freeform"]?.key !== undefined) { + tagRendering["id"] = config.id + "-" + tagRendering["freeform"]["key"] + } } } } + + if(config.mapRendering === undefined){ + // This is a legacy format, lets create a pointRendering + let location: ("point"|"centroid")[] = ["point"] + if(config.wayHandling === 2){ + location = ["point", "centroid"] + } + config.mapRendering = [ + { + icon: config["icon"], + iconOverlays: config["iconOverlays"], + label: config["label"], + iconSize: config["iconSize"], + location, + rotation: config["rotation"] + } + ] + + + } + } const layerFiles = ScriptUtils.getLayerFiles(); @@ -40,7 +57,7 @@ for (const layerFile of layerFiles) { const themeFiles = ScriptUtils.getThemeFiles() for (const themeFile of themeFiles) { for (const layerConfig of themeFile.parsed.layers ?? []) { - if(typeof layerConfig === "string" || layerConfig["builtin"]!== undefined){ + if (typeof layerConfig === "string" || layerConfig["builtin"] !== undefined) { continue } // @ts-ignore