Feature: allow a fallback value in substituting tags

This commit is contained in:
Pieter Vander Vennet 2025-09-07 02:18:23 +02:00
parent 35c4222466
commit 4d4a7e9d84
3 changed files with 32 additions and 32 deletions

View file

@ -126,23 +126,7 @@ export class TagUtils {
"\n" + "\n" +
"An assigning tag _cannot_ be used to query OpenStreetMap/Overpass.\n" + "An assigning tag _cannot_ be used to query OpenStreetMap/Overpass.\n" +
"\n" + "\n" +
"If using a key or variable which might not be defined, add a condition in the mapping to hide the option. This is\n" + "It is possible to assign a default value with `key_to_assing:={some_other_key??fallback_value}`. If `some_other_key` is not present, then the literal value `fallback_value` will be used instead." +
"because, if `some_other_key` is not defined, one might actually upload the literal text `key={some_other_key}` to OSM -\n" +
"which we do not want.\n" +
"\n" +
"To mitigate this, use:\n" +
"\n" +
"```json\n" +
"{\n" +
' "mappings": [\n' +
" {\n" +
' "if":"key:={some_other_key}",\n' +
' "then": "...",\n' +
' "hideInAnswer": "some_other_key="\n' +
" }\n" +
" ]\n" +
"}\n" +
"```\n" +
"\n" + "\n" +
"One can use `key!:=prefix-{other_key}-postfix` as well, to match if `key` is _not_ the same\n" + "One can use `key!:=prefix-{other_key}-postfix` as well, to match if `key` is _not_ the same\n" +
"as `prefix-{other_key}-postfix` (with `other_key` substituted by the value)", "as `prefix-{other_key}-postfix` (with `other_key` substituted by the value)",
@ -263,8 +247,8 @@ export class TagUtils {
return tags return tags
} }
static SplitKeys(tagsFilters: UploadableTag[]): Record<string, string[]> { static SplitKeys(tagsFilters: UploadableTag[], currentProperties: Tags): Record<string, string[]> {
return this.SplitKeysRegex(tagsFilters, false) return this.SplitKeysRegex(tagsFilters, false, currentProperties)
} }
/*** /***
@ -272,24 +256,31 @@ export class TagUtils {
* *
* TagUtils.SplitKeysRegex([new Tag("isced:level", "bachelor; master")], true) // => {"isced:level": ["bachelor","master"]} * TagUtils.SplitKeysRegex([new Tag("isced:level", "bachelor; master")], true) // => {"isced:level": ["bachelor","master"]}
*/ */
static SplitKeysRegex(tagsFilters: UploadableTag[], allowRegex: false): Record<string, string[]> static SplitKeysRegex(tagsFilters: ReadonlyArray<UploadableTag>, allowRegex: false,
currentProperties: Tags): Record<string, string[]>
static SplitKeysRegex( static SplitKeysRegex(
tagsFilters: UploadableTag[], tagsFilters: ReadonlyArray<UploadableTag>,
allowRegex: boolean allowRegex: boolean,
currentProperties: Tags
): Record<string, (string | RegexTag)[]> ): Record<string, (string | RegexTag)[]>
static SplitKeysRegex( static SplitKeysRegex(
tagsFilters: UploadableTag[], tagsFiltersIn: ReadonlyArray<UploadableTag>,
allowRegex: boolean allowRegex: boolean,
currentProperties: Tags
): Record<string, (string | RegexTag)[]> { ): Record<string, (string | RegexTag)[]> {
const keyValues: Record<string, (string | RegexTag)[]> = {} const keyValues: Record<string, (string | RegexTag)[]> = {}
tagsFilters = [...tagsFilters] // copy all, use as queue const tagsFilters = [...tagsFiltersIn] // copy all, use as queue
while (tagsFilters.length > 0) { while (tagsFilters.length > 0) {
const tagsFilter = tagsFilters.shift() let tagsFilter = tagsFilters.shift()
if (tagsFilter === undefined) { if (tagsFilter === undefined) {
continue continue
} }
if (tagsFilter instanceof SubstitutingTag) {
tagsFilter = (<SubstitutingTag>tagsFilter).asTag(currentProperties)
}
if (tagsFilter instanceof And) { if (tagsFilter instanceof And) {
tagsFilters.push(...(<UploadableTag[]>tagsFilter.and)) tagsFilters.push(...(<UploadableTag[]>tagsFilter.and))
continue continue
@ -360,12 +351,12 @@ export class TagUtils {
* TagUtils.FlattenMultiAnswer(([new Tag("x","y"), new Tag("a","b")])) // => [new Tag("x","y"), new Tag("a","b")] * TagUtils.FlattenMultiAnswer(([new Tag("x","y"), new Tag("a","b")])) // => [new Tag("x","y"), new Tag("a","b")]
* TagUtils.FlattenMultiAnswer(([new Tag("x","")])) // => [new Tag("x","")] * TagUtils.FlattenMultiAnswer(([new Tag("x","")])) // => [new Tag("x","")]
*/ */
static FlattenMultiAnswer(tagsFilters: UploadableTag[]): UploadableTag[] { static FlattenMultiAnswer(tagsFilters: UploadableTag[], currentProperties: Tags): UploadableTag[] {
if (tagsFilters === undefined) { if (tagsFilters === undefined) {
return [] return []
} }
const keyValues = TagUtils.SplitKeys(tagsFilters) const keyValues = TagUtils.SplitKeys(tagsFilters, currentProperties)
const and: UploadableTag[] = [] const and: UploadableTag[] = []
for (const key in keyValues) { for (const key in keyValues) {
const values = Lists.dedup(keyValues[key]).filter((v) => v !== "") const values = Lists.dedup(keyValues[key]).filter((v) => v !== "")
@ -387,7 +378,7 @@ export class TagUtils {
* TagUtils.MatchesMultiAnswer(new Tag("isced:level","master"), {"isced:level":"bachelor; master"}) // => true * TagUtils.MatchesMultiAnswer(new Tag("isced:level","master"), {"isced:level":"bachelor; master"}) // => true
*/ */
static MatchesMultiAnswer(tag: UploadableTag, properties: Tags): boolean { static MatchesMultiAnswer(tag: UploadableTag, properties: Tags): boolean {
const splitted = TagUtils.SplitKeysRegex([tag], true) const splitted = TagUtils.SplitKeysRegex([tag], true, properties)
for (const splitKey in splitted) { for (const splitKey in splitted) {
const neededValues = splitted[splitKey] const neededValues = splitted[splitKey]
if (properties[splitKey] === undefined) { if (properties[splitKey] === undefined) {

View file

@ -897,7 +897,7 @@ export default class TagRenderingConfig {
]) ])
) )
} }
const and = TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings]) const and = TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings], currentProperties)
if (and.length === 0) { if (and.length === 0) {
return undefined return undefined
} }

View file

@ -300,6 +300,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
* Utils.SubstituteKeys("abc{def}ghi", {def: '{XYZ}'}) // => "abc{XYZ}ghi" * Utils.SubstituteKeys("abc{def}ghi", {def: '{XYZ}'}) // => "abc{XYZ}ghi"
* Utils.SubstituteKeys("abc\n\n{def}ghi", {def: '{XYZ}'}) // => "abc\n\n{XYZ}ghi" * Utils.SubstituteKeys("abc\n\n{def}ghi", {def: '{XYZ}'}) // => "abc\n\n{XYZ}ghi"
* *
* // Should support a default value
* Utils.SubstituteKeys("abc{def??XXX}ghi", {def: 'XYZ'}) // => "abcXYZghi"
* Utils.SubstituteKeys("abc{def??XXX}ghi", {randomKey: 'XYZ'}) // => "abcXXXghi"
*
* @param txt * @param txt
* @param tags * @param tags
* @param useLang * @param useLang
@ -322,8 +326,13 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
let result = "" let result = ""
while (match) { while (match) {
const [_, normal, key, leftover] = match const [_, normal, keyFallback, leftover] = match
let v = tags?.[key] let key = keyFallback
let fallback = ""
if (keyFallback.indexOf("??") >= 0) {
[key, fallback] = keyFallback.split("??")
}
let v = tags?.[key] ?? fallback
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