Themes: allow to easily import tagrenderings and add a prefix key to all tags

This commit is contained in:
Pieter Vander Vennet 2025-04-21 02:51:41 +02:00
parent 4d9bdaf877
commit 01680f236c
9 changed files with 201 additions and 31 deletions

View file

@ -0,0 +1,134 @@
import { DesugaringStep } from "./Conversion"
import { ConversionContext } from "./ConversionContext"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
import { Translatable } from "../Json/Translatable"
import { TagConfigJson } from "../Json/TagConfigJson"
import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
export default class AddPrefixToTagRenderingConfig extends DesugaringStep<QuestionableTagRenderingConfigJson> {
private readonly _prefix: string
constructor(prefix: string) {
super("Adds `prefix` to _all_ keys. Used to add information about a subamenity withing a bigger amenity (e.g. toilets in a restaurant, a sauna in a water park, ...)", ["*"], "AddPrefixToTagRenderingConfig")
this._prefix = prefix
}
/**
*
* const edit = new AddPrefixToTagRenderingConfig("PREFIX")
* edit.updateString("Some string") // => "Some string"
* edit.updateString("Some string {key0}") // => "Some string {PREFIX:key0}"
*
* // Should prefix a key in a special visualisation
* new AddPrefixToTagRenderingConfig("PREFIX").updateString("{opening_hours_table(opening_hours)}") // => "{opening_hours_table(PREFIX:opening_hours,,)}"
*
* // Should prefix the default key in a special visualisation
* new AddPrefixToTagRenderingConfig("PREFIX").updateString("{opening_hours_table()}") // => "{opening_hours_table(PREFIX:opening_hours,,)}"
*/
private updateString(str: string): string {
const parsed = SpecialVisualizations.constructSpecification(str)
const fixedSpec: string[] = []
for (const spec of parsed) {
if (typeof spec === "string") {
const part = spec.replace(/{([a-zA-Z0-9:_-]+)}/g, `{${this._prefix}:$1}`)
fixedSpec.push(part)
} else {
const newArgs: string[] = []
for (let i = 0; i < spec.func.args.length; i++) {
const argDoc = spec.func.args[i]
const argV = spec.args[i]
if (argDoc.type === "key") {
newArgs.push(this._prefix + ":" + (argV ?? argDoc.defaultValue ?? ""))
} else {
newArgs.push(argV ?? "")
}
}
fixedSpec.push("{" + spec.func.funcName + "(" + newArgs.join(",") + ")}")
}
}
return fixedSpec.join("")
}
private updateTranslatable(val: Translatable | undefined): Translatable | undefined {
if (!val) {
return val
}
if (typeof val === "string") {
return this.updateString(val)
}
const newTranslations: Record<string, string> = {}
for (const lng in val) {
newTranslations[lng] = this.updateString(val[lng])
}
return newTranslations
}
private updateTag(tags: string): string;
private updateTag(tags: TagConfigJson): TagConfigJson;
private updateTag(tags: TagConfigJson): TagConfigJson {
if (!tags) {
return tags
}
if (tags["and"]) {
return { and: this.updateTags(tags["and"]) }
}
if (tags["or"]) {
return { or: this.updateTags(tags["or"]) }
}
return this._prefix + ":" + tags
}
private updateTags(tags: ReadonlyArray<string>): string[] {
return tags?.map(tag => this.updateTag(tag))
}
private updateMapping(mapping: Readonly<MappingConfigJson>): MappingConfigJson {
return {
...mapping,
addExtraTags: this.updateTags(mapping.addExtraTags),
if: this.updateTag(mapping.if),
then: this.updateTranslatable(mapping.then),
alsoShowIf: this.updateTag(mapping.alsoShowIf),
ifnot: this.updateTag(mapping.ifnot),
priorityIf: this.updateTag(mapping.priorityIf),
hideInAnswer: mapping.hideInAnswer === true || mapping.hideInAnswer === false ? mapping.hideInAnswer : this.updateTag(mapping.hideInAnswer)
}
}
public convert(json: Readonly<QuestionableTagRenderingConfigJson>, context: ConversionContext): QuestionableTagRenderingConfigJson {
let freeform = json.freeform
if (freeform) {
const ff = json.freeform
freeform = {
...ff,
key: this._prefix + ":" + ff.key,
addExtraTags: this.updateTags(ff.addExtraTags)
}
}
return <QuestionableTagRenderingConfigJson>{
...json,
id: this._prefix + "_" + json.id,
question: this.updateTranslatable(json.question),
questionHint: this.updateTranslatable(json.questionHint),
render: this.updateTranslatable(<Translatable>json.render),
freeform,
editButtonAriaLabel: json.editButtonAriaLabel,
onSoftDelete: this.updateTags(json.onSoftDelete),
invalidValues: this.updateTag(json.invalidValues),
mappings: json.mappings?.map(mapping => this.updateMapping(mapping)),
condition: this.updateTag(json.condition),
metacondition: json.metacondition, // no update here
filter: json.filter === true, // We break references to filters, as those references won't have the updated tags
_appliedPrefix: this._prefix
}
}
}

View file

@ -6,6 +6,8 @@ import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRende
import { TagUtils } from "../../../Logic/Tags/TagUtils" import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import { AddContextToTranslations } from "./AddContextToTranslations" import { AddContextToTranslations } from "./AddContextToTranslations"
import AddPrefixToTagRenderingConfig from "./AddPrefixToTagRenderingConfig"
import { Translatable } from "../Json/Translatable"
export class ExpandTagRendering extends Conversion< export class ExpandTagRendering extends Conversion<
| string | string
@ -208,6 +210,21 @@ export class ExpandTagRendering extends Conversion<
let matchingTrs: (TagRenderingConfigJson & { id: string })[] let matchingTrs: (TagRenderingConfigJson & { id: string })[]
if (id === "*") { if (id === "*") {
matchingTrs = layerTrs matchingTrs = layerTrs
} else if (id === "title") {
const title = layer.title
if (title["render"] || title["mappings"]) {
const titleTr = <TagRenderingConfigJson>layer.title
return [{
...titleTr,
id: layer.id + "_title"
}]
} else {
const transl = <Translatable>layer.title
return [{
render: transl,
id: layer.id + "_title"
}]
}
} else if (id.startsWith("*")) { } else if (id.startsWith("*")) {
const id_ = id.substring(1) const id_ = id.substring(1)
matchingTrs = layerTrs.filter((tr) => tr["labels"]?.indexOf(id_) >= 0) matchingTrs = layerTrs.filter((tr) => tr["labels"]?.indexOf(id_) >= 0)
@ -249,6 +266,25 @@ export class ExpandTagRendering extends Conversion<
return undefined return undefined
} }
/**
* Returns a variation of 'tr' where every key has been prefixed by the given 'prefix'-key.
* If the given key is undefined, returns the original tagRendering.
*
* Note: metacondition will _not_ be prefixed
* @param key
* @param tr
* @private
*/
private static applyKeyPrefix(key: string | undefined, tr: Readonly<QuestionableTagRenderingConfigJson>, ctx: ConversionContext): QuestionableTagRenderingConfigJson {
if (key === undefined || key === null) {
return tr
}
if (key.endsWith(":")) {
ctx.err("A 'prefix'-key should not end with a colon. The offending prefix value is: " + key)
}
return new AddPrefixToTagRenderingConfig(key).convert(tr, ctx.enter("prefix"))
}
private convertOnce( private convertOnce(
tr: string | { builtin: string | string[] } | TagRenderingConfigJson, tr: string | { builtin: string | string[] } | TagRenderingConfigJson,
ctx: ConversionContext ctx: ConversionContext
@ -310,6 +346,7 @@ export class ExpandTagRendering extends Conversion<
if ( if (
key === "builtin" || key === "builtin" ||
key === "override" || key === "override" ||
key === "prefix" ||
key === "id" || key === "id" ||
key.startsWith("#") key.startsWith("#")
) { ) {
@ -379,6 +416,7 @@ export class ExpandTagRendering extends Conversion<
} }
for (let foundTr of lookup) { for (let foundTr of lookup) {
foundTr = Utils.Clone(foundTr) foundTr = Utils.Clone(foundTr)
foundTr = ExpandTagRendering.applyKeyPrefix(tr["prefix"], foundTr, ctx)
ctx.MergeObjectsForOverride(tr["override"] ?? {}, foundTr) ctx.MergeObjectsForOverride(tr["override"] ?? {}, foundTr)
if (names.length == 1) { if (names.length == 1) {
foundTr["id"] = tr["id"] ?? foundTr["id"] foundTr["id"] = tr["id"] ?? foundTr["id"]

View file

@ -1,14 +1,4 @@
import { import { Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault } from "./Conversion"
Concat,
Conversion,
DesugaringContext,
DesugaringStep,
Each,
Fuse,
On,
Pass,
SetDefault,
} from "./Conversion"
import { ThemeConfigJson } from "../Json/ThemeConfigJson" import { ThemeConfigJson } from "../Json/ThemeConfigJson"
import { PrepareLayer, RewriteSpecial } from "./PrepareLayer" import { PrepareLayer, RewriteSpecial } from "./PrepareLayer"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
@ -163,9 +153,8 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
const unused = Array.from(hideLabels).filter((l) => !usedLabels.has(l)) const unused = Array.from(hideLabels).filter((l) => !usedLabels.has(l))
if (unused.length > 0) { if (unused.length > 0) {
context.err( context.err(
"This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " + `You are attempting to import layer '${found.id}' in this theme. This layer import specifies that certain tagrenderings have to be removed based on forbidden ids and/or labels. One or more of these forbidden ids did not match any tagRenderings and caused no deletions: ${unused.join(", ")}
unused.join(", ") + This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore`
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore"
) )
} }
found.tagRenderings = filtered found.tagRenderings = filtered

View file

@ -422,8 +422,15 @@ export interface LayerConfigJson {
| string | string
| { | {
id?: string id?: string
/**
* Special value: "<layerid>.title" will return the layer's title for an element
*/
builtin: string | string[] builtin: string | string[]
override: Partial<QuestionableTagRenderingConfigJson> override: Partial<QuestionableTagRenderingConfigJson>,
/**
* Add this prefix to all keys. This is applied _before_ the override, thus keys added in 'override' will not be prefixed
*/
prefix?: string
} }
| QuestionableTagRenderingConfigJson | QuestionableTagRenderingConfigJson
| (RewritableConfigJson< | (RewritableConfigJson<

View file

@ -289,7 +289,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
* Extra arguments to configure the input element * Extra arguments to configure the input element
* group: hidden * group: hidden
*/ */
helperArgs: any helperArgs?: any
} }
/** /**

View file

@ -14,7 +14,7 @@ import NearbyImagesCollapsed from "../Image/NearbyImagesCollapsed.svelte"
class NearbyImageVis implements SpecialVisualizationSvelte { class NearbyImageVis implements SpecialVisualizationSvelte {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [ args: [
{ {
name: "mode", name: "mode",
defaultValue: "closed", defaultValue: "closed",
@ -65,6 +65,7 @@ export class ImageVisualisations {
args: [ args: [
{ {
name: "image_key", name: "image_key",
type: "key",
defaultValue: AllImageProviders.defaultKeys.join(";"), defaultValue: AllImageProviders.defaultKeys.join(";"),
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ", doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ",
}, },
@ -95,6 +96,7 @@ export class ImageVisualisations {
needsUrls: [Imgur.apiUrl, ...Imgur.supportingUrls], needsUrls: [Imgur.apiUrl, ...Imgur.supportingUrls],
args: [ args: [
{ {
type: "key",
name: "image_key", name: "image_key",
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
defaultValue: "panoramax", defaultValue: "panoramax",

View file

@ -1,11 +1,7 @@
import { Store, UIEventSource } from "../Logic/UIEventSource" import { Store, UIEventSource } from "../Logic/UIEventSource"
import BaseUIElement from "./BaseUIElement" import BaseUIElement from "./BaseUIElement"
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig" import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
import { import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
FeatureSource,
IndexedFeatureSource,
WritableFeatureSource,
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection" import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { Changes } from "../Logic/Osm/Changes" import { Changes } from "../Logic/Osm/Changes"
import { ExportableMap, MapProperties } from "../Models/MapProperties" import { ExportableMap, MapProperties } from "../Models/MapProperties"
@ -100,7 +96,8 @@ export interface SpecialVisualization {
name: string name: string
defaultValue?: string defaultValue?: string
doc: string doc: string
required?: false | boolean required?: false | boolean,
type?: "key"
}[] }[]
readonly getLayerDependencies?: (argument: string[]) => string[] readonly getLayerDependencies?: (argument: string[]) => string[]
@ -133,7 +130,8 @@ export interface SpecialVisualizationSvelte {
name: string name: string
defaultValue?: string defaultValue?: string
doc: string doc: string
required?: false | boolean required?: false | boolean,
type?: "key" | string
}[] }[]
readonly getLayerDependencies?: (argument: string[]) => string[] readonly getLayerDependencies?: (argument: string[]) => string[]

View file

@ -257,6 +257,7 @@ export default class SpecialVisualizations {
{ {
name: "key", name: "key",
defaultValue: "opening_hours", defaultValue: "opening_hours",
type: "key",
doc: "The tagkey from which the table is constructed.", doc: "The tagkey from which the table is constructed.",
}, },
{ {
@ -284,6 +285,7 @@ export default class SpecialVisualizations {
args: [ args: [
{ {
name: "key", name: "key",
type: "key",
defaultValue: "opening_hours", defaultValue: "opening_hours",
doc: "The tagkey from which the opening hours are read.", doc: "The tagkey from which the opening hours are read.",
}, },
@ -324,6 +326,7 @@ export default class SpecialVisualizations {
args: [ args: [
{ {
name: "key", name: "key",
type: "key",
doc: "The key of the tag to give the canonical text for", doc: "The key of the tag to give the canonical text for",
required: true, required: true,
}, },
@ -412,6 +415,7 @@ export default class SpecialVisualizations {
args: [ args: [
{ {
name: "key", name: "key",
type: "key",
doc: "The attribute to interpret as json", doc: "The attribute to interpret as json",
defaultValue: "value", defaultValue: "value",
}, },
@ -463,6 +467,7 @@ export default class SpecialVisualizations {
args: [ args: [
{ {
name: "key", name: "key",
type: "key",
defaultValue: "value", defaultValue: "value",
doc: "The key to look for the tags", doc: "The key to look for the tags",
}, },
@ -470,9 +475,7 @@ export default class SpecialVisualizations {
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[]
feature: Feature,
layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
const key = argument[0] ?? "value" const key = argument[0] ?? "value"
return new VariableUiElement( return new VariableUiElement(
@ -508,8 +511,7 @@ export default class SpecialVisualizations {
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature
layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
return new SvelteUIElement(DirectionIndicator, { state, feature }) return new SvelteUIElement(DirectionIndicator, { state, feature })
}, },
@ -520,6 +522,7 @@ export default class SpecialVisualizations {
args: [ args: [
{ {
name: "key", name: "key",
type: "key",
doc: "The attribute containing the degrees", doc: "The attribute containing the degrees",
defaultValue: "_direction:centerpoint", defaultValue: "_direction:centerpoint",
}, },

View file

@ -466,8 +466,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
if (v !== undefined && v !== null) { if (v !== undefined && v !== null) {
if (v["toISOString"] != undefined) { if (v["toISOString"] != undefined) {
// This is a date, probably the timestamp of the object // This is a date, probably the timestamp of the object
// @ts-ignore const date: Date = v
const date: Date = el
v = date.toISOString() v = date.toISOString()
} }