Themes: move generated themes into assets, remove known_themes, support pruning of borrowed icons

This commit is contained in:
Pieter Vander Vennet 2025-01-11 01:18:56 +01:00
parent 310b973846
commit ee64d84d27
18 changed files with 431 additions and 400 deletions

View file

@ -41,7 +41,6 @@
]
}
},
"minzoom": 15,
"title": {
"render": {

View file

@ -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": {

View file

@ -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",

View file

@ -180,6 +180,7 @@ class DownloadNsiLogos extends Script {
],
filter: [
<any>{
"#":"ignore-possible-duplicate",
id: type,
strict: true,
options: [{ question: type }, ...filterOptions],

View file

@ -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"

View file

@ -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)
})

View file

@ -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 = <ThemeConfigJson> JSON.parse(readFileSync(paths[i], "utf8"))
if (theme !== undefined && layoutConfigJson.id !== theme) {

View file

@ -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

View file

@ -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<string, ThemeConfig> = 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 = <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(<LayerConfigJson> favourite)
}
continue
}

View file

@ -7,12 +7,9 @@ export class AllSharedLayers {
public static sharedLayers: Map<string, LayerConfig> = AllSharedLayers.getSharedLayers()
public static getSharedLayersConfigs(): Map<string, LayerConfigJson> {
const sharedLayers = new Map<string, LayerConfigJson>()
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, <any> layer)
}

View file

@ -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<ThemeConfigJson>("./assets/generated/themes/"+id+".json", 1000*60*60*60)
return new ThemeConfig(config, true)
}
private static getSharedTagRenderings(): Map<string, QuestionableTagRenderingConfigJson> {

View file

@ -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<string, (TagRenderingConfigJson & { id: string })[]>
// 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<string, (TagRenderingConfigJson & { id: string })[]>()
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<TagRenderingConfigJson & { id: string }>(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"], <QuestionableTagRenderingConfigJson>tr)
}
}
}
return result
}
private pruneMappings<T extends (TagRenderingConfigJson & {
id: string
})>(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<any>(foundTr)
ctx.MergeObjectsForOverride(tagRenderingConfigJson["override"] ?? {}, foundTr)
foundTr["id"] = tagRenderingConfigJson["id"] ?? foundTr["id"]
result.push(<any>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<TagRenderingConfigJson & { id: string }>("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 [
<any>{
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<any>(foundTr)
ctx.MergeObjectsForOverride(tr["override"] ?? {}, foundTr)
if (names.length == 1) {
foundTr["id"] = tr["id"] ?? foundTr["id"]
}
trs.push(foundTr)
}
}
return trs
}
return [tr]
}
}

View file

@ -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<LayerConfigJson> {
constructor() {
@ -103,359 +93,6 @@ class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
}
}
class ExpandTagRendering extends Conversion<
| string
| TagRenderingConfigJson
| {
builtin: string | string[]
override: any
},
TagRenderingConfigJson[]
> {
private readonly _state: DesugaringContext
private readonly _tagRenderingsByLabel: Map<string, TagRenderingConfigJson[]>
// 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<string, TagRenderingConfigJson[]>()
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"], <QuestionableTagRenderingConfigJson>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<any>(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 = <TagRenderingConfigJson[]>(
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<TagRenderingConfigJson>("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 [
<any>{
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<any>(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<QuestionableTagRenderingConfigJson> {
constructor() {
super(
@ -1114,19 +751,19 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
}
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 = <MinimalTagRenderingConfigJson>(
expander.convert(<any>json.icon, context.enter("icon"))[0]
)
) ?? json.icon
} else {
result.icon = json.icon
}
if (json.color && json.color["builtin"]) {
result.color = <MinimalTagRenderingConfigJson>(
expander.convert(<any>json.color, context.enter("color"))[0]
)
) ?? json.color
} else {
result.color = json.color
}

View file

@ -605,5 +605,5 @@ export interface LayerConfigJson {
/**
* group: hidden
*/
"#dont-translate": "*"
"#dont-translate"?: "*"
}

View file

@ -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"

View file

@ -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
}

View file

@ -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"

View file

@ -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<string, QuestionableTagRenderingConfigJson>(),
sharedLayers,
publicLayers: new Set<string>(),
tagRenderingOrder: []
})
let themeConfigJsonPrepared = prepareStep.convertStrict(theme, ConversionContext.test())
const themeConfig = new ThemeConfig(themeConfigJsonPrepared)
@ -86,6 +87,7 @@ describe("PrepareTheme", () => {
tagRenderings: new Map<string, QuestionableTagRenderingConfigJson>(),
sharedLayers,
publicLayers: new Set<string>(),
tagRenderingOrder: [],
}).convertStrict(themeConfigJson, ConversionContext.test())
const themeConfig = new ThemeConfig(themeConfigJsonPrepared)
const layerUnderTest = <LayerConfig>(
@ -101,6 +103,7 @@ describe("PrepareTheme", () => {
tagRenderings: new Map<string, QuestionableTagRenderingConfigJson>(),
sharedLayers,
publicLayers: new Set<string>(),
tagRenderingOrder: [],
}).convertStrict(
{
...themeConfigJson,
@ -148,6 +151,7 @@ describe("PrepareTheme", () => {
sharedLayers,
tagRenderings: new Map<string, QuestionableTagRenderingConfigJson>(),
publicLayers: new Set<string>(),
tagRenderingOrder: []
}
const layout: ThemeConfigJson = {
description: "A testing theme",