From ee64d84d270f141b0dafb2ac2726899afce5297e Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sat, 11 Jan 2025 01:18:56 +0100 Subject: [PATCH] Themes: move generated themes into assets, remove known_themes, support pruning of borrowed icons --- assets/layers/shops/shops.json | 1 - langs/layers/en.json | 22 ++ package.json | 1 - scripts/downloadNsiLogos.ts | 1 + scripts/generateDocs.ts | 2 +- scripts/generateLayerOverview.ts | 6 +- scripts/generateLayouts.ts | 8 +- scripts/initFiles.sh | 2 +- src/Customizations/AllKnownLayouts.ts | 5 +- src/Customizations/AllSharedLayers.ts | 5 +- src/Logic/DetermineTheme.ts | 20 +- .../Conversion/ExpandTagRendering.ts | 370 +++++++++++++++++ .../ThemeConfig/Conversion/PrepareLayer.ts | 373 +----------------- .../ThemeConfig/Json/LayerConfigJson.ts | 2 +- src/UI/AllThemesGui.svelte | 2 +- src/UI/Studio/EditItemButton.svelte | 3 +- test/Logic/Actors/Actors.spec.ts | 2 +- .../Conversion/PrepareTheme.spec.ts | 6 +- 18 files changed, 431 insertions(+), 400 deletions(-) create mode 100644 src/Models/ThemeConfig/Conversion/ExpandTagRendering.ts diff --git a/assets/layers/shops/shops.json b/assets/layers/shops/shops.json index ed159f323..9b06d9ba6 100644 --- a/assets/layers/shops/shops.json +++ b/assets/layers/shops/shops.json @@ -41,7 +41,6 @@ ] } }, - "minzoom": 15, "title": { "render": { diff --git a/langs/layers/en.json b/langs/layers/en.json index 0ce9e27ec..08f6a4d5b 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -2327,6 +2327,28 @@ "render": "Camper site {name}" } }, + "charge_point": { + "description": "Layer showing individual charge points within a charging station", + "name": "Charge points", + "presets": { + "0": { + "description": "Add an individual charge point within a larger charging station", + "title": "a charge point" + } + }, + "tagRenderings": { + "ref": { + "freeform": { + "placeholder": "Reference number of the charge point, e.g. 2126" + }, + "question": "What is the reference number of this charge point?", + "render": "The reference of this charge point is {ref}" + } + }, + "title": { + "render": "Charge point" + } + }, "charging_station": { "description": "A charging station", "filter": { diff --git a/package.json b/package.json index 1e9bc0863..9c7ae0f76 100644 --- a/package.json +++ b/package.json @@ -205,7 +205,6 @@ "jspdf": "^2.5.1", "latlon2country": "^1.2.6", "libphonenumber-js": "^1.10.8", - "lz-string": "^1.4.4", "mangrove-reviews-typescript": "^1.1.0", "maplibre-gl": "^4.1.1", "marked": "^12.0.2", diff --git a/scripts/downloadNsiLogos.ts b/scripts/downloadNsiLogos.ts index 53d2fb7a3..d09a3659e 100644 --- a/scripts/downloadNsiLogos.ts +++ b/scripts/downloadNsiLogos.ts @@ -180,6 +180,7 @@ class DownloadNsiLogos extends Script { ], filter: [ { + "#":"ignore-possible-duplicate", id: type, strict: true, options: [{ question: type }, ...filterOptions], diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index ccdda66c8..094b494c9 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -9,7 +9,7 @@ import ScriptUtils from "./ScriptUtils" import Translations from "../src/UI/i18n/Translations" import themeOverview from "../src/assets/generated/theme_overview.json" import ThemeConfig from "../src/Models/ThemeConfig/ThemeConfig" -import bookcases from "../src/assets/generated/themes/bookcases.json" +import bookcases from "../public/assets/generated/themes/bookcases.json" import fakedom from "fake-dom" import unit from "../src/assets/generated/layers/unit.json" import Hotkeys from "../src/UI/Base/Hotkeys" diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 7cce54ff5..48ad8af3f 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -140,7 +140,7 @@ class AddIconSummary extends DesugaringStep<{ raw: LayerConfigJson; parsed: Laye class LayerOverviewUtils extends Script { public static readonly layerPath = "./src/assets/generated/layers/" - public static readonly themePath = "./src/assets/generated/themes/" + public static readonly themePath = "./public/assets/generated/themes/" constructor() { super("Reviews and generates the compiled themes") @@ -319,12 +319,12 @@ class LayerOverviewUtils extends Script { keywords, layers: theme.layers.filter((l) => sharedLayers.has(l["id"])).map((l) => l["id"]), } - perId.set(theme.id, data) + perId.set(data.id, data) } const sorted = Constants.themeOrder.map((id) => { if (!perId.has(id)) { - throw "Ordered theme id " + id + " not found" + throw "Ordered theme '" + id + "' not found" } return perId.get(id) }) diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index a8158658e..483e20f8d 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -298,8 +298,8 @@ class GenerateLayouts extends Script { Origin: "https://mapcomplete.org", }) urls.push(...(f.properties["connect-src"] ?? [])) - for (const key of Object.keys(styleSpec?.sources ?? {})) { - const url = styleSpec.sources[key].url + for (const key of Object.keys(styleSpec?.["sources"] ?? {})) { + const url = styleSpec["sources"][key].url if (!url) { continue } @@ -585,7 +585,7 @@ class GenerateLayouts extends Script { const filename = "index_" + theme.id + ".ts" const imports = [ - `import layout from "./src/assets/generated/themes/${theme.id}.json"`, + `import layout from "./public/assets/generated/themes/${theme.id}.json"`, `import { ThemeMetaTagging } from "./src/assets/generated/metatagging/${theme.id}"`, ] for (const layerName of Constants.added_by_default) { @@ -640,7 +640,7 @@ class GenerateLayouts extends Script { if (theme !== undefined) { console.warn("Only generating layout " + theme) } - const paths = ScriptUtils.readDirRecSync("./src/assets/generated/themes/",1) + const paths = ScriptUtils.readDirRecSync("./public/assets/generated/themes/",1) for (const i in paths) { const layoutConfigJson = JSON.parse(readFileSync(paths[i], "utf8")) if (theme !== undefined && layoutConfigJson.id !== theme) { diff --git a/scripts/initFiles.sh b/scripts/initFiles.sh index fde4a51f9..a4c3f1958 100755 --- a/scripts/initFiles.sh +++ b/scripts/initFiles.sh @@ -5,7 +5,7 @@ mkdir -p ./src/assets/generated/layers echo '{"layers": []}' > ./src/assets/generated/known_layers.json rm -f ./src/assets/generated/layers/*.json -rm -f ./src/assets/generated/themes/*.json +rm -f ./public/assets/generated/themes/*.json cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json echo '{}' > ./src/assets/generated/layers/favourite.json echo '{}' > ./src/assets/generated/layers/summary.json diff --git a/src/Customizations/AllKnownLayouts.ts b/src/Customizations/AllKnownLayouts.ts index 9d41f9764..9d71643f0 100644 --- a/src/Customizations/AllKnownLayouts.ts +++ b/src/Customizations/AllKnownLayouts.ts @@ -5,6 +5,7 @@ import { AllSharedLayers } from "./AllSharedLayers" import Constants from "../Models/Constants" import ScriptUtils from "../../scripts/ScriptUtils" import { readFileSync } from "fs" +import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" /** * Somewhat of a dictionary, which lazily parses needed themes @@ -14,14 +15,14 @@ export class AllKnownLayoutsLazy { private readonly dict: Map = new Map() constructor(includeFavouriteLayer = true) { - const paths = ScriptUtils.readDirRecSync("./src/assets/generated/themes/",1) + const paths = ScriptUtils.readDirRecSync("./public/assets/generated/themes/",1) for (const path of paths) { const themeConfigJson = JSON.parse(readFileSync(path, "utf8")) for (const layerId of Constants.added_by_default) { if (layerId === "favourite" && favourite.id) { if (includeFavouriteLayer) { - themeConfigJson.layers.push(favourite) + themeConfigJson.layers.push( favourite) } continue } diff --git a/src/Customizations/AllSharedLayers.ts b/src/Customizations/AllSharedLayers.ts index 3a4b6521e..d970e69a1 100644 --- a/src/Customizations/AllSharedLayers.ts +++ b/src/Customizations/AllSharedLayers.ts @@ -7,12 +7,9 @@ export class AllSharedLayers { public static sharedLayers: Map = AllSharedLayers.getSharedLayers() public static getSharedLayersConfigs(): Map { const sharedLayers = new Map() - for (const layer of (known_layers).layers) { + for (const layer of known_layers["layers"]) { if(layer.id === undefined){ - console.error("Layer without id! "+JSON.stringify(layer).slice(0,80), known_layers.layers.length) continue - }else{ - console.log("Loaded",layer.id) } sharedLayers.set(layer.id, layer) } diff --git a/src/Logic/DetermineTheme.ts b/src/Logic/DetermineTheme.ts index 7d7ca80a6..4eab97a39 100644 --- a/src/Logic/DetermineTheme.ts +++ b/src/Logic/DetermineTheme.ts @@ -1,9 +1,7 @@ -import ThemeConfig from "../Models/ThemeConfig/ThemeConfig" +import ThemeConfig, { MinimalThemeInformation } from "../Models/ThemeConfig/ThemeConfig" import { QueryParameters } from "./Web/QueryParameters" -import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" import { FixedUiElement } from "../UI/Base/FixedUiElement" import { Utils } from "../Utils" -import LZString from "lz-string" import { FixLegacyTheme } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert" import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" import known_layers from "../assets/generated/known_layers.json" @@ -15,10 +13,10 @@ import questions from "../assets/generated/layers/questions.json" import { DoesImageExist, PrevalidateTheme } from "../Models/ThemeConfig/Conversion/Validation" import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion" import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson" -import Hash from "./Web/Hash" import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" import { ThemeConfigJson } from "../Models/ThemeConfig/Json/ThemeConfigJson" import { ValidateThemeAndLayers } from "../Models/ThemeConfig/Conversion/ValidateThemeAndLayers" +import * as theme_overview from "../assets/generated/theme_overview.json" export default class DetermineTheme { private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path)) @@ -87,14 +85,15 @@ export default class DetermineTheme { "The layout to load into MapComplete" ).data const id = layoutId?.toLowerCase() - const layouts = AllKnownLayouts.allKnownLayouts - if (layouts.size() == 0) { + const themes: MinimalThemeInformation[] = theme_overview.themes + if (themes.length == 0) { throw "Build failed or running, no layouts are known at all" } - if (layouts.getConfig(id) === undefined) { + const themeInfo = themes.find(th => th.id === id) + if (themeInfo === undefined) { const alternatives = Utils.sortedByLevenshteinDistance( id, - Array.from(layouts.keys()), + themes.map(th => th.id), (i) => i ).slice(0, 3) const msg = `No builtin map theme with name ${layoutId} exists. Perhaps you meant one of ${alternatives.join( @@ -102,7 +101,10 @@ export default class DetermineTheme { )}` throw msg } - return layouts.get(id) + // Actually fetch the theme + + const config = await Utils.downloadJsonCached("./assets/generated/themes/"+id+".json", 1000*60*60*60) + return new ThemeConfig(config, true) } private static getSharedTagRenderings(): Map { diff --git a/src/Models/ThemeConfig/Conversion/ExpandTagRendering.ts b/src/Models/ThemeConfig/Conversion/ExpandTagRendering.ts new file mode 100644 index 000000000..879d3cf1d --- /dev/null +++ b/src/Models/ThemeConfig/Conversion/ExpandTagRendering.ts @@ -0,0 +1,370 @@ +import { Conversion, DesugaringContext } from "./Conversion" +import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" +import { LayerConfigJson } from "../Json/LayerConfigJson" +import { ConversionContext } from "./ConversionContext" +import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" +import { TagUtils } from "../../../Logic/Tags/TagUtils" +import { Utils } from "../../../Utils" +import { AddContextToTranslations } from "./AddContextToTranslations" + +export class ExpandTagRendering extends Conversion< + | string + | TagRenderingConfigJson + | { + builtin: string | string[] + override: any +}, + TagRenderingConfigJson[] +> { + private readonly _state: DesugaringContext + private readonly _tagRenderingsByLabel: Map + // Only used for self-reference + private readonly _self: LayerConfigJson + private readonly _options: { + /* If true, will copy the 'osmSource'-tags into the condition */ + applyCondition?: true | boolean + noHardcodedStrings?: false | boolean + addToContext?: false | boolean + } + + constructor( + state: DesugaringContext, + self: LayerConfigJson, + options?: { + applyCondition?: true | boolean + noHardcodedStrings?: false | boolean + // If set, a question will be added to the 'sharedTagRenderings'. Should only be used for 'questions.json' + addToContext?: false | boolean + }, + ) { + super( + "Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question and reusing the builtins", + [], + "ExpandTagRendering", + ) + this._state = state + this._self = self + this._options = options + this._tagRenderingsByLabel = new Map() + for (const trconfig of state.tagRenderings?.values() ?? []) { + for (const label of trconfig["labels"] ?? []) { + let withLabel = this._tagRenderingsByLabel.get(label) + if (withLabel === undefined) { + withLabel = [] + this._tagRenderingsByLabel.set(label, withLabel) + } + withLabel.push(trconfig) + } + } + } + + public convert( + spec: string | any, + ctx: ConversionContext, + ): QuestionableTagRenderingConfigJson[] { + const trs = this.convertOnce(spec, ctx) + ?.map(tr => this.pruneMappings(tr, ctx)) + if (!Array.isArray(trs)) { + ctx.err("Result of lookup for " + spec + " is not iterable; got " + trs) + return undefined + } + const result = [] + for (const tr of trs) { + if (typeof tr === "string" || tr["builtin"] !== undefined) { + const stable = this.convert(tr, ctx.inOperation("recursive_resolve")) + .map(tr => this.pruneMappings(tr, ctx)) + result.push(...stable) + if (this._options?.addToContext) { + for (const tr of stable) { + this._state.tagRenderings?.set(tr.id, tr) + } + } + } else { + result.push(tr) + if (this._options?.addToContext) { + this._state.tagRenderings?.set(tr["id"], tr) + } + } + } + + return result + } + + private pruneMappings(tagRendering: T, ctx: ConversionContext): T { + if (!tagRendering["strict"]) { + return tagRendering + } + if(!this._self.source["osmTags"]){ + return tagRendering + } + ctx.inOperation("expandTagRendering:pruning").enters(tagRendering.id) + .info(`PRUNING! Tagrendering to prune: ${tagRendering.id} in the context of layer ${this._self.id} Sourcetags: ${this._self.source["osmTags"]}`) + const before = tagRendering.mappings?.length ?? 0 + + const alwaysTags = TagUtils.Tag(this._self.source["osmTags"]) + const newMappings = tagRendering.mappings?.filter(mapping => { + const condition = TagUtils.Tag(mapping.if) + return condition.shadows(alwaysTags); + + + }).map(mapping => { + const newIf = TagUtils.removeKnownParts( + TagUtils.Tag(mapping.if), alwaysTags) + if (typeof newIf === "boolean") { + throw "Invalid removeKnownParts" + } + return { + ...mapping, + if: newIf.asJson(), + } + }) + const after = newMappings?.length ?? 0 + if (before - after > 0) { + ctx.info(`Pruned mappings for ${tagRendering.id}, from ${before} to ${after} (removed ${before - after})`) + } + const tr = { + ...tagRendering, + mappings: newMappings, + } + delete tr["strict"] + return tr + } + + private lookup(name: string, ctx: ConversionContext): (TagRenderingConfigJson & { id: string })[] | undefined { + const direct = this.directLookup(name) + + if (direct === undefined) { + return undefined + } + const result: (TagRenderingConfigJson & { id: string })[] = [] + for (const tagRenderingConfigJson of direct) { + const nm: string | string[] | undefined = tagRenderingConfigJson["builtin"] + if (nm !== undefined) { + let indirect: TagRenderingConfigJson[] + if (typeof nm === "string") { + indirect = this.lookup(nm, ctx) + } else { + indirect = [].concat(...nm.map((n) => this.lookup(n, ctx))) + } + for (let foundTr of indirect) { + foundTr = Utils.Clone(foundTr) + ctx.MergeObjectsForOverride(tagRenderingConfigJson["override"] ?? {}, foundTr) + foundTr["id"] = tagRenderingConfigJson["id"] ?? foundTr["id"] + result.push(foundTr) + } + } else { + result.push(tagRenderingConfigJson) + } + } + return result + } + + /** + * Looks up a tagRendering or group of tagRenderings based on the name. + */ + private directLookup(name: string): (TagRenderingConfigJson & { id: string })[] | undefined { + const state = this._state + if (state.tagRenderings.has(name)) { + return [state.tagRenderings.get(name)] + } + if (this._tagRenderingsByLabel.has(name)) { + return this._tagRenderingsByLabel.get(name) + } + + if (name.indexOf(".") < 0) { + return undefined + } + + const spl = name.split(".") + let layer = state.sharedLayers?.get(spl[0]) + if (spl[0] === this._self?.id) { + layer = this._self + } + + if (spl.length !== 2 || !layer) { + return undefined + } + + const id = spl[1] + + const layerTrs = <(TagRenderingConfigJson & { id: string })[]>( + layer.tagRenderings.filter((tr) => tr["id"] !== undefined) + ) + let matchingTrs: (TagRenderingConfigJson & { id: string })[] + if (id === "*") { + matchingTrs = layerTrs + } else if (id.startsWith("*")) { + const id_ = id.substring(1) + matchingTrs = layerTrs.filter((tr) => tr["labels"]?.indexOf(id_) >= 0) + } else { + matchingTrs = layerTrs.filter((tr) => tr["id"] === id || tr["labels"]?.indexOf(id) >= 0) + } + + const contextWriter = new AddContextToTranslations("layers:") + for (let i = 0; i < matchingTrs.length; i++) { + let found: (TagRenderingConfigJson & { id: string }) = Utils.Clone(matchingTrs[i]) + if (this._options?.applyCondition) { + // The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown + if (typeof layer.source !== "string") { + if (found.condition === undefined) { + found.condition = layer.source["osmTags"] + } else { + found.condition = { and: [found.condition, layer.source["osmTags"]] } + } + } + } + + found = contextWriter.convertStrict( + found, + ConversionContext.construct( + [layer.id, "tagRenderings", found["id"]], + ["AddContextToTranslations"], + ), + ) + matchingTrs[i] = found + } + + if (matchingTrs.length !== 0) { + return matchingTrs + } + return undefined + } + + private convertOnce(tr: string | any, ctx: ConversionContext): (TagRenderingConfigJson & { id: string })[] { + const state = this._state + + if (typeof tr === "string") { + if (this._state.tagRenderings !== null) { + const lookup = this.lookup(tr, ctx) + if (lookup) { + return lookup + } + } + if ( + this._state.sharedLayers?.size > 0 && + ctx.path.at(-1) !== "icon" && + !ctx.path.find((p) => p === "pointRendering") + ) { + ctx.warn( + `A literal rendering was detected: ${tr} + Did you perhaps forgot to add a layer name as 'layername.${tr}'? ` + + Array.from(state.sharedLayers.keys()).join(", "), + ) + } + + if (this._options?.noHardcodedStrings && this._state?.sharedLayers?.size > 0) { + ctx.err( + "Detected an invocation to a builtin tagRendering, but this tagrendering was not found: " + + tr + + " \n Did you perhaps forget to add the layer as prefix, such as `icons." + + tr + + "`? ", + ) + } + + return [ + { + render: tr, + id: tr.replace(/[^a-zA-Z0-9]/g, ""), + }, + ] + } + + if (tr["builtin"] !== undefined) { + let names: string | string[] = tr["builtin"] + if (typeof names === "string") { + names = [names] + } + + if (this._state.tagRenderings === null) { + return [] + } + + for (const key of Object.keys(tr)) { + if ( + key === "builtin" || + key === "override" || + key === "id" || + key.startsWith("#") + ) { + continue + } + ctx.err( + "An object calling a builtin can only have keys `builtin` or `override`, but a key with name `" + + key + + "` was found. This won't be picked up! The full object is: " + + JSON.stringify(tr), + ) + } + + const trs: (TagRenderingConfigJson & { id: string })[] = [] + for (const name of names) { + const lookup = this.lookup(name, ctx) + if (lookup === undefined) { + let candidates = Array.from(state.tagRenderings.keys()) + if (name.indexOf(".") > 0) { + const [layerName] = name.split(".") + if (layerName === undefined) { + ctx.err("Layername is undefined", name) + } + let layer = state.sharedLayers.get(layerName) + if (layerName === this._self?.id) { + layer = this._self + } + if (layer === undefined) { + const candidates = Utils.sortedByLevenshteinDistance( + layerName, + Utils.NoNull(Array.from(state.sharedLayers.keys())), + (s) => s, + ) + if (state.sharedLayers.size === 0) { + ctx.warn( + "BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " + + name + + ": layer " + + layerName + + " not found for now, but ignoring as this is a bootstrapping run. ", + ) + } else { + ctx.err( + ": While reusing tagrendering: " + + name + + ": layer " + + layerName + + " not found. Maybe you meant one of " + + candidates.slice(0, 3).join(", "), + ) + } + continue + } + candidates = Utils.NoNull(layer.tagRenderings.map((tr) => tr["id"])).map( + (id) => layerName + "." + id, + ) + } + candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i) + ctx.err( + "The tagRendering with identifier " + + name + + " was not found.\n\tDid you mean one of " + + candidates.join(", ") + + "?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first", + ) + continue + } + for (let foundTr of lookup) { + foundTr = Utils.Clone(foundTr) + ctx.MergeObjectsForOverride(tr["override"] ?? {}, foundTr) + if (names.length == 1) { + foundTr["id"] = tr["id"] ?? foundTr["id"] + } + trs.push(foundTr) + } + } + return trs + } + + return [tr] + } +} diff --git a/src/Models/ThemeConfig/Conversion/PrepareLayer.ts b/src/Models/ThemeConfig/Conversion/PrepareLayer.ts index 1a9615d30..8fe4f327c 100644 --- a/src/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/src/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -1,14 +1,4 @@ -import { - Concat, - Conversion, - DesugaringContext, - DesugaringStep, - Each, - FirstOf, - Fuse, - On, - SetDefault, -} from "./Conversion" +import { Concat, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault } from "./Conversion" import { LayerConfigJson } from "../Json/LayerConfigJson" import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" import { Utils } from "../../../Utils" @@ -17,7 +7,6 @@ import SpecialVisualizations from "../../../UI/SpecialVisualizations" import Translations from "../../../UI/i18n/Translations" import { Translation } from "../../../UI/i18n/Translation" import tagrenderingconfigmeta from "../../../../src/assets/schemas/tagrenderingconfigmeta.json" -import { AddContextToTranslations } from "./AddContextToTranslations" import FilterConfigJson from "../Json/FilterConfigJson" import { TagConfigJson } from "../Json/TagConfigJson" import PointRenderingConfigJson, { IconConfigJson } from "../Json/PointRenderingConfigJson" @@ -30,6 +19,7 @@ import { ConversionContext } from "./ConversionContext" import { ExpandRewrite } from "./ExpandRewrite" import { TagUtils } from "../../../Logic/Tags/TagUtils" import { ExpandFilter, PruneFilters } from "./ExpandFilter" +import { ExpandTagRendering } from "./ExpandTagRendering" class AddFiltersFromTagRenderings extends DesugaringStep { constructor() { @@ -103,359 +93,6 @@ class AddFiltersFromTagRenderings extends DesugaringStep { } } -class ExpandTagRendering extends Conversion< - | string - | TagRenderingConfigJson - | { - builtin: string | string[] - override: any - }, - TagRenderingConfigJson[] -> { - private readonly _state: DesugaringContext - private readonly _tagRenderingsByLabel: Map - // Only used for self-reference - private readonly _self: LayerConfigJson - private readonly _options: { - /* If true, will copy the 'osmSource'-tags into the condition */ - applyCondition?: true | boolean - noHardcodedStrings?: false | boolean - addToContext?: false | boolean - } - - constructor( - state: DesugaringContext, - self: LayerConfigJson, - options?: { - applyCondition?: true | boolean - noHardcodedStrings?: false | boolean - // If set, a question will be added to the 'sharedTagRenderings'. Should only be used for 'questions.json' - addToContext?: false | boolean - } - ) { - super( - "Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question and reusing the builtins", - [], - "ExpandTagRendering" - ) - this._state = state - this._self = self - this._options = options - this._tagRenderingsByLabel = new Map() - for (const trconfig of state.tagRenderings?.values() ?? []) { - for (const label of trconfig["labels"] ?? []) { - let withLabel = this._tagRenderingsByLabel.get(label) - if (withLabel === undefined) { - withLabel = [] - this._tagRenderingsByLabel.set(label, withLabel) - } - withLabel.push(trconfig) - } - } - } - - public convert( - spec: string | any, - ctx: ConversionContext - ): QuestionableTagRenderingConfigJson[] { - const trs = this.convertOnce(spec, ctx) - const result = [] - if(!Array.isArray(trs)){ - ctx.err("Result of lookup for "+spec+" is not iterable; got "+trs) - return undefined - } - for (const tr of trs) { - if (typeof tr === "string" || tr["builtin"] !== undefined) { - const stable = this.convert(tr, ctx.inOperation("recursive_resolve")) - .map(tr => this.pruneMappings(tr, ctx)) - result.push(...stable) - if (this._options?.addToContext) { - for (const tr of stable) { - this._state.tagRenderings?.set(tr.id, tr) - } - } - } else { - result.push(tr) - if (this._options?.addToContext) { - this._state.tagRenderings?.set(tr["id"], tr) - } - } - } - - return result - } - - private pruneMappings(tagRendering: QuestionableTagRenderingConfigJson, ctx: ConversionContext): QuestionableTagRenderingConfigJson{ - if(!tagRendering["strict"]){ - return tagRendering - } - const before = tagRendering.mappings?.length ?? 0 - - const alwaysTags = TagUtils.Tag(this._self.source["osmTags"]) - const newMappings = tagRendering.mappings?.filter(mapping => { - const condition = TagUtils.Tag( mapping.if) - return condition.shadows(alwaysTags); - - }).map(mapping => { - const newIf =TagUtils.removeKnownParts( - TagUtils.Tag(mapping.if), alwaysTags ) - if(typeof newIf === "boolean"){ - throw "Invalid removeKnownParts" - } - return { - ...mapping, - if: newIf.asJson() - } - }) - const after = newMappings?.length ?? 0 - if(before - after > 0){ - ctx.info(`Pruned mappings for ${tagRendering.id}, from ${before} to ${after} (removed ${before - after})`) - } - const tr = { - ...tagRendering, - mappings: newMappings - } - delete tr["strict"] - return tr - } - - private lookup(name: string, ctx: ConversionContext): TagRenderingConfigJson[] | undefined { - const direct = this.directLookup(name) - - if (direct === undefined) { - return undefined - } - const result: TagRenderingConfigJson[] = [] - for (const tagRenderingConfigJson of direct) { - const nm: string | string[] | undefined = tagRenderingConfigJson["builtin"] - if (nm !== undefined) { - let indirect: TagRenderingConfigJson[] - if (typeof nm === "string") { - indirect = this.lookup(nm, ctx) - } else { - indirect = [].concat(...nm.map((n) => this.lookup(n, ctx))) - } - for (let foundTr of indirect) { - foundTr = Utils.Clone(foundTr) - ctx.MergeObjectsForOverride(tagRenderingConfigJson["override"] ?? {}, foundTr) - foundTr["id"] = tagRenderingConfigJson["id"] ?? foundTr["id"] - result.push(foundTr) - } - } else { - result.push(tagRenderingConfigJson) - } - } - return result - } - - /** - * Looks up a tagRendering or group of tagRenderings based on the name. - */ - private directLookup(name: string): TagRenderingConfigJson[] | undefined { - const state = this._state - if (state.tagRenderings.has(name)) { - return [state.tagRenderings.get(name)] - } - if (this._tagRenderingsByLabel.has(name)) { - return this._tagRenderingsByLabel.get(name) - } - - if (name.indexOf(".") < 0) { - return undefined - } - - const spl = name.split(".") - let layer = state.sharedLayers?.get(spl[0]) - if (spl[0] === this._self?.id) { - layer = this._self - } - - if (spl.length !== 2 || !layer) { - return undefined - } - - const id = spl[1] - - const layerTrs = ( - layer.tagRenderings.filter((tr) => tr["id"] !== undefined) - ) - let matchingTrs: TagRenderingConfigJson[] - if (id === "*") { - matchingTrs = layerTrs - } else if (id.startsWith("*")) { - const id_ = id.substring(1) - matchingTrs = layerTrs.filter((tr) => tr["labels"]?.indexOf(id_) >= 0) - } else { - matchingTrs = layerTrs.filter((tr) => tr["id"] === id || tr["labels"]?.indexOf(id) >= 0) - } - - const contextWriter = new AddContextToTranslations("layers:") - for (let i = 0; i < matchingTrs.length; i++) { - let found: TagRenderingConfigJson = Utils.Clone(matchingTrs[i]) - if (this._options?.applyCondition) { - // The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown - if (typeof layer.source !== "string") { - if (found.condition === undefined) { - found.condition = layer.source["osmTags"] - } else { - found.condition = { and: [found.condition, layer.source["osmTags"]] } - } - } - } - - found = contextWriter.convertStrict( - found, - ConversionContext.construct( - [layer.id, "tagRenderings", found["id"]], - ["AddContextToTranslations"] - ) - ) - matchingTrs[i] = found - } - - if (matchingTrs.length !== 0) { - return matchingTrs - } - return undefined - } - - private convertOnce(tr: string | any, ctx: ConversionContext): TagRenderingConfigJson[] { - const state = this._state - - if (typeof tr === "string") { - if (this._state.tagRenderings !== null) { - const lookup = this.lookup(tr, ctx) - if(lookup){ - return lookup - } - } - if ( - this._state.sharedLayers?.size > 0 && - ctx.path.at(-1) !== "icon" && - !ctx.path.find((p) => p === "pointRendering") - ) { - ctx.warn( - `A literal rendering was detected: ${tr} - Did you perhaps forgot to add a layer name as 'layername.${tr}'? ` + - Array.from(state.sharedLayers.keys()).join(", "), - ) - } - - if (this._options?.noHardcodedStrings && this._state?.sharedLayers?.size > 0) { - ctx.err( - "Detected an invocation to a builtin tagRendering, but this tagrendering was not found: " + - tr + - " \n Did you perhaps forget to add the layer as prefix, such as `icons." + - tr + - "`? ", - ) - } - - return [ - { - render: tr, - id: tr.replace(/[^a-zA-Z0-9]/g, ""), - }, - ] - } - - if (tr["builtin"] !== undefined) { - let names: string | string[] = tr["builtin"] - if (typeof names === "string") { - names = [names] - } - - if (this._state.tagRenderings === null) { - return [] - } - - for (const key of Object.keys(tr)) { - if ( - key === "builtin" || - key === "override" || - key === "id" || - key.startsWith("#") - ) { - continue - } - ctx.err( - "An object calling a builtin can only have keys `builtin` or `override`, but a key with name `" + - key + - "` was found. This won't be picked up! The full object is: " + - JSON.stringify(tr) - ) - } - - const trs: TagRenderingConfigJson[] = [] - for (const name of names) { - const lookup = this.lookup(name, ctx) - if (lookup === undefined) { - let candidates = Array.from(state.tagRenderings.keys()) - if (name.indexOf(".") > 0) { - const [layerName] = name.split(".") - if(layerName === undefined){ - ctx.err("Layername is undefined", name) - } - let layer = state.sharedLayers.get(layerName) - if (layerName === this._self?.id) { - layer = this._self - } - if (layer === undefined) { - const candidates = Utils.sortedByLevenshteinDistance( - layerName, - Utils.NoNull(Array.from(state.sharedLayers.keys())), - (s) => s - ) - if (state.sharedLayers.size === 0) { - ctx.warn( - "BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " + - name + - ": layer " + - layerName + - " not found for now, but ignoring as this is a bootstrapping run. " - ) - } else { - ctx.err( - ": While reusing tagrendering: " + - name + - ": layer " + - layerName + - " not found. Maybe you meant one of " + - candidates.slice(0, 3).join(", ") - ) - } - continue - } - candidates = Utils.NoNull(layer.tagRenderings.map((tr) => tr["id"])).map( - (id) => layerName + "." + id - ) - } - candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i) - ctx.err( - "The tagRendering with identifier " + - name + - " was not found.\n\tDid you mean one of " + - candidates.join(", ") + - "?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first" - ) - continue - } - for (let foundTr of lookup) { - foundTr = Utils.Clone(foundTr) - ctx.MergeObjectsForOverride(tr["override"] ?? {}, foundTr) - if (names.length == 1) { - foundTr["id"] = tr["id"] ?? foundTr["id"] - } - trs.push(foundTr) - } - } - return trs - } - - return [tr] - } -} - class DetectInline extends DesugaringStep { constructor() { super( @@ -1114,19 +751,19 @@ class ExpandMarkerRenderings extends DesugaringStep { } convert(json: IconConfigJson, context: ConversionContext): IconConfigJson { - const expander = new ExpandTagRendering(this._state, this._layer) + const expander = new ExpandTagRendering(this._state, this._layer, {applyCondition: false}) const result: IconConfigJson = { icon: undefined, color: undefined } if (json.icon && json.icon["builtin"]) { result.icon = ( expander.convert(json.icon, context.enter("icon"))[0] - ) + ) ?? json.icon } else { result.icon = json.icon } if (json.color && json.color["builtin"]) { result.color = ( expander.convert(json.color, context.enter("color"))[0] - ) + ) ?? json.color } else { result.color = json.color } diff --git a/src/Models/ThemeConfig/Json/LayerConfigJson.ts b/src/Models/ThemeConfig/Json/LayerConfigJson.ts index 6b5ad7717..f6ca6d014 100644 --- a/src/Models/ThemeConfig/Json/LayerConfigJson.ts +++ b/src/Models/ThemeConfig/Json/LayerConfigJson.ts @@ -605,5 +605,5 @@ export interface LayerConfigJson { /** * group: hidden */ - "#dont-translate": "*" + "#dont-translate"?: "*" } diff --git a/src/UI/AllThemesGui.svelte b/src/UI/AllThemesGui.svelte index 9eba326d6..ae30a0c56 100644 --- a/src/UI/AllThemesGui.svelte +++ b/src/UI/AllThemesGui.svelte @@ -10,7 +10,7 @@ import LoginToggle from "./Base/LoginToggle.svelte" import Pencil from "../assets/svg/Pencil.svelte" import Constants from "../Models/Constants" - import { ImmutableStore, Store, Stores, UIEventSource } from "../Logic/UIEventSource" + import { Store, Stores, UIEventSource } from "../Logic/UIEventSource" import ThemesList from "./BigComponents/ThemesList.svelte" import { MinimalThemeInformation } from "../Models/ThemeConfig/ThemeConfig" import Eye from "../assets/svg/Eye.svelte" diff --git a/src/UI/Studio/EditItemButton.svelte b/src/UI/Studio/EditItemButton.svelte index cbcc4680e..7b8c2415f 100644 --- a/src/UI/Studio/EditItemButton.svelte +++ b/src/UI/Studio/EditItemButton.svelte @@ -3,7 +3,6 @@ import { OsmConnection } from "../../Logic/Osm/OsmConnection" import Marker from "../Map/Marker.svelte" import NextButton from "../Base/NextButton.svelte" - import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts" import { AllSharedLayers } from "../../Customizations/AllSharedLayers" import { createEventDispatcher } from "svelte" @@ -19,7 +18,7 @@ function fetchIconDescription(layerId): any { if (category === "themes") { - return AllKnownLayouts.allKnownLayouts.get(layerId).icon + return undefined } return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon } diff --git a/test/Logic/Actors/Actors.spec.ts b/test/Logic/Actors/Actors.spec.ts index 5d46d0556..26e6fb7ab 100644 --- a/test/Logic/Actors/Actors.spec.ts +++ b/test/Logic/Actors/Actors.spec.ts @@ -1,7 +1,7 @@ import { Utils } from "../../../src/Utils" import ThemeConfig from "../../../src/Models/ThemeConfig/ThemeConfig" -import * as bookcaseJson from "../../../src/assets/generated/themes/bookcases.json" +import * as bookcaseJson from "../../../public/assets/generated/themes/bookcases.json" import { OsmTags } from "../../../src/Models/OsmFeature" import { Feature, Geometry } from "geojson" import { expect, it } from "vitest" diff --git a/test/Models/ThemeConfig/Conversion/PrepareTheme.spec.ts b/test/Models/ThemeConfig/Conversion/PrepareTheme.spec.ts index d2466a61b..084ca36e6 100644 --- a/test/Models/ThemeConfig/Conversion/PrepareTheme.spec.ts +++ b/test/Models/ThemeConfig/Conversion/PrepareTheme.spec.ts @@ -5,7 +5,7 @@ import ThemeConfig from "../../../../src/Models/ThemeConfig/ThemeConfig" import bookcaseLayer from "../../../../src/assets/generated/layers/public_bookcase.json" import LayerConfig from "../../../../src/Models/ThemeConfig/LayerConfig" import { ExtractImages } from "../../../../src/Models/ThemeConfig/Conversion/FixImages" -import cyclofix from "../../../../src/assets/generated/themes/cyclofix.json" +import cyclofix from "../../../../public/assets/generated/themes/cyclofix.json" import { Tag } from "../../../../src/Logic/Tags/Tag" import { DesugaringContext } from "../../../../src/Models/ThemeConfig/Conversion/Conversion" import { And } from "../../../../src/Logic/Tags/And" @@ -68,6 +68,7 @@ describe("PrepareTheme", () => { tagRenderings: new Map(), sharedLayers, publicLayers: new Set(), + tagRenderingOrder: [] }) let themeConfigJsonPrepared = prepareStep.convertStrict(theme, ConversionContext.test()) const themeConfig = new ThemeConfig(themeConfigJsonPrepared) @@ -86,6 +87,7 @@ describe("PrepareTheme", () => { tagRenderings: new Map(), sharedLayers, publicLayers: new Set(), + tagRenderingOrder: [], }).convertStrict(themeConfigJson, ConversionContext.test()) const themeConfig = new ThemeConfig(themeConfigJsonPrepared) const layerUnderTest = ( @@ -101,6 +103,7 @@ describe("PrepareTheme", () => { tagRenderings: new Map(), sharedLayers, publicLayers: new Set(), + tagRenderingOrder: [], }).convertStrict( { ...themeConfigJson, @@ -148,6 +151,7 @@ describe("PrepareTheme", () => { sharedLayers, tagRenderings: new Map(), publicLayers: new Set(), + tagRenderingOrder: [] } const layout: ThemeConfigJson = { description: "A testing theme",