From d47fd7e7463f67910d9e14ddf069a4fbf1f301ec Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 31 Mar 2023 03:28:11 +0200 Subject: [PATCH] Add question box as special rendering --- Logic/Actors/TitleHandler.ts | 17 +- Logic/Tags/And.ts | 6 +- Logic/Tags/ComparingTag.ts | 6 +- Logic/Tags/Or.ts | 6 +- Logic/Tags/RegexTag.ts | 4 +- Logic/Tags/SubstitutingTag.ts | 8 +- Logic/Tags/Tag.ts | 10 +- Logic/Tags/TagsFilter.ts | 12 +- Logic/Web/QueryParameters.ts | 13 ++ Models/ThemeConfig/Conversion/PrepareLayer.ts | 116 +++++++++- Models/ThemeConfig/Conversion/PrepareTheme.ts | 4 +- Models/ThemeConfig/Conversion/Validation.ts | 4 +- .../ThemeConfig/Conversion/ValidationUtils.ts | 15 +- Models/ThemeConfig/PointRenderingConfig.ts | 4 +- Models/ThemeConfig/TagRenderingConfig.ts | 96 ++++++-- UI/Base/Tr.svelte | 2 +- UI/BigComponents/SelectedElementView.svelte | 46 ++-- UI/InputElement/ValidatedInput.svelte | 34 ++- UI/Popup/QuestionViz.ts | 51 +++++ UI/Popup/TagRendering/FreeformInput.svelte | 26 +++ UI/Popup/TagRendering/Inline.svelte | 26 +++ UI/Popup/TagRendering/Questionbox.svelte | 108 +++++++++ .../SpecialTranslation.svelte | 23 +- .../TagRendering/TagRenderingAnswer.svelte | 38 ++++ .../TagRendering/TagRenderingEditable.svelte | 46 ++++ .../TagRenderingMapping.svelte | 14 +- .../TagRendering/TagRenderingQuestion.svelte | 147 +++++++++++++ UI/Popup/TagRenderingAnswer.svelte | 34 --- UI/Popup/TagRenderingQuestion.svelte | 53 ----- UI/SpecialVisualization.ts | 9 +- UI/SpecialVisualizations.ts | 9 +- UI/ThemeViewGUI.svelte | 24 +- .../charging_station/charging_station.json | 207 +++++++++++++----- .../charging_station.protojson | 5 +- assets/layers/charging_station/csvToJson.ts | 6 +- .../cycleways_and_roads.json | 2 +- assets/layers/usersettings/usersettings.json | 22 +- assets/tagRenderings/questions.json | 4 +- assets/themes/grb/grb.json | 2 +- .../mapcomplete-changes.json | 2 +- .../mapcomplete-changes.proto.json | 2 +- assets/themes/onwheels/onwheels.json | 4 +- 42 files changed, 956 insertions(+), 311 deletions(-) create mode 100644 UI/Popup/QuestionViz.ts create mode 100644 UI/Popup/TagRendering/FreeformInput.svelte create mode 100644 UI/Popup/TagRendering/Inline.svelte create mode 100644 UI/Popup/TagRendering/Questionbox.svelte rename UI/Popup/{ => TagRendering}/SpecialTranslation.svelte (55%) create mode 100644 UI/Popup/TagRendering/TagRenderingAnswer.svelte create mode 100644 UI/Popup/TagRendering/TagRenderingEditable.svelte rename UI/Popup/{ => TagRendering}/TagRenderingMapping.svelte (54%) create mode 100644 UI/Popup/TagRendering/TagRenderingQuestion.svelte delete mode 100644 UI/Popup/TagRenderingAnswer.svelte delete mode 100644 UI/Popup/TagRenderingQuestion.svelte diff --git a/Logic/Actors/TitleHandler.ts b/Logic/Actors/TitleHandler.ts index d8af466c80..921afa8042 100644 --- a/Logic/Actors/TitleHandler.ts +++ b/Logic/Actors/TitleHandler.ts @@ -5,27 +5,27 @@ import { Utils } from "../../Utils" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import { Feature } from "geojson" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import SvelteUIElement from "../../UI/Base/SvelteUIElement" -import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer.svelte" +import TagRenderingAnswer from "../../UI/Popup/TagRendering/TagRenderingAnswer.svelte" +import { SpecialVisualizationState } from "../../UI/SpecialVisualization" export default class TitleHandler { constructor( selectedElement: Store, selectedLayer: Store, allElements: FeaturePropertiesStore, - layout: LayoutConfig + state: SpecialVisualizationState ) { const currentTitle: Store = selectedElement.map( (selected) => { - const defaultTitle = layout?.title?.txt ?? "MapComplete" + const defaultTitle = state.layout?.title?.txt ?? "MapComplete" if (selected === undefined) { return defaultTitle } const tags = selected.properties - for (const layer of layout?.layers ?? []) { + for (const layer of state.layout?.layers ?? []) { if (layer.title === undefined) { continue } @@ -33,7 +33,12 @@ export default class TitleHandler { const tagsSource = allElements.getStore(tags.id) ?? new UIEventSource>(tags) - const title = new SvelteUIElement(TagRenderingAnswer, { tags: tagsSource }) + const title = new SvelteUIElement(TagRenderingAnswer, { + tags: tagsSource, + state, + selectedElement: selectedElement.data, + layer: selectedLayer.data, + }) return ( new Combine([defaultTitle, " | ", title]).ConstructElement() ?.textContent ?? defaultTitle diff --git a/Logic/Tags/And.ts b/Logic/Tags/And.ts index 771dffe534..84b4794b03 100644 --- a/Logic/Tags/And.ts +++ b/Logic/Tags/And.ts @@ -39,7 +39,7 @@ export class And extends TagsFilter { return new And(ands) } - matchesProperties(tags: any): boolean { + matchesProperties(tags: Record): boolean { for (const tagsFilter of this.and) { if (!tagsFilter.matchesProperties(tags)) { return false @@ -147,7 +147,7 @@ export class And extends TagsFilter { return [].concat(...this.and.map((subkeys) => subkeys.usedTags())) } - asChange(properties: any): { k: string; v: string }[] { + asChange(properties: Record): { k: string; v: string }[] { const result = [] for (const tagsFilter of this.and) { result.push(...tagsFilter.asChange(properties)) @@ -375,7 +375,7 @@ export class And extends TagsFilter { return !this.and.some((t) => !t.isNegative()) } - visit(f: (TagsFilter: any) => void) { + visit(f: (tagsFilter: TagsFilter) => void) { f(this) this.and.forEach((sub) => sub.visit(f)) } diff --git a/Logic/Tags/ComparingTag.ts b/Logic/Tags/ComparingTag.ts index db66a1fdce..685cd2550a 100644 --- a/Logic/Tags/ComparingTag.ts +++ b/Logic/Tags/ComparingTag.ts @@ -15,11 +15,11 @@ export default class ComparingTag implements TagsFilter { this._representation = representation } - asChange(properties: any): { k: string; v: string }[] { + asChange(properties: Record): { k: string; v: string }[] { throw "A comparable tag can not be used to be uploaded to OSM" } - asHumanString(linkToWiki: boolean, shorten: boolean, properties: any) { + asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record) { return this._key + this._representation } @@ -44,7 +44,7 @@ export default class ComparingTag implements TagsFilter { * t.matchesProperties({key: 0}) // => true * t.matchesProperties({differentKey: 42}) // => false */ - matchesProperties(properties: any): boolean { + matchesProperties(properties: Record): boolean { return this._predicate(properties[this._key]) } diff --git a/Logic/Tags/Or.ts b/Logic/Tags/Or.ts index d2cca35a14..ad437cd8b5 100644 --- a/Logic/Tags/Or.ts +++ b/Logic/Tags/Or.ts @@ -17,7 +17,7 @@ export class Or extends TagsFilter { return new Or(or) } - matchesProperties(properties: any): boolean { + matchesProperties(properties: Record): boolean { for (const tagsFilter of this.or) { if (tagsFilter.matchesProperties(properties)) { return true @@ -82,7 +82,7 @@ export class Or extends TagsFilter { return [].concat(...this.or.map((subkeys) => subkeys.usedTags())) } - asChange(properties: any): { k: string; v: string }[] { + asChange(properties: Record): { k: string; v: string }[] { const result = [] for (const tagsFilter of this.or) { result.push(...tagsFilter.asChange(properties)) @@ -260,7 +260,7 @@ export class Or extends TagsFilter { return this.or.some((t) => t.isNegative()) } - visit(f: (TagsFilter: any) => void) { + visit(f: (tagsFilter: TagsFilter) => void) { f(this) this.or.forEach((t) => t.visit(f)) } diff --git a/Logic/Tags/RegexTag.ts b/Logic/Tags/RegexTag.ts index 988f9987c4..2b46e94074 100644 --- a/Logic/Tags/RegexTag.ts +++ b/Logic/Tags/RegexTag.ts @@ -122,7 +122,7 @@ export class RegexTag extends TagsFilter { * new RegexTag("key","value").matchesProperties({"otherkey":"value"}) // => false * new RegexTag("key","value",true).matchesProperties({"otherkey":"something"}) // => true */ - matchesProperties(tags: any): boolean { + matchesProperties(tags: Record): boolean { if (typeof this.key === "string") { const value = tags[this.key] ?? "" return RegexTag.doesMatch(value, this.value) != this.invert @@ -244,7 +244,7 @@ export class RegexTag extends TagsFilter { return [] } - asChange(properties: any): { k: string; v: string }[] { + asChange(properties: Record): { k: string; v: string }[] { if (this.invert) { return [] } diff --git a/Logic/Tags/SubstitutingTag.ts b/Logic/Tags/SubstitutingTag.ts index 50ea30193f..7d5435a068 100644 --- a/Logic/Tags/SubstitutingTag.ts +++ b/Logic/Tags/SubstitutingTag.ts @@ -22,7 +22,7 @@ export default class SubstitutingTag implements TagsFilter { this._invert = invert } - private static substituteString(template: string, dict: any): string { + private static substituteString(template: string, dict: Record): string { for (const k in dict) { template = template.replace(new RegExp("\\{" + k + "\\}", "g"), dict[k]) } @@ -72,7 +72,7 @@ export default class SubstitutingTag implements TagsFilter { * assign.matchesProperties({"_date:now": "2021-03-29"}) // => false * assign.matchesProperties({"some_key": "2021-03-29"}) // => false */ - matchesProperties(properties: any): boolean { + matchesProperties(properties: Record): boolean { const value = properties[this._key] if (value === undefined || value === "") { return false @@ -89,7 +89,7 @@ export default class SubstitutingTag implements TagsFilter { return [] } - asChange(properties: any): { k: string; v: string }[] { + asChange(properties: Record): { k: string; v: string }[] { if (this._invert) { throw "An inverted substituting tag can not be used to create a change" } @@ -108,7 +108,7 @@ export default class SubstitutingTag implements TagsFilter { return false } - visit(f: (TagsFilter: any) => void) { + visit(f: (tagsFilter: TagsFilter) => void) { f(this) } } diff --git a/Logic/Tags/Tag.ts b/Logic/Tags/Tag.ts index bab74dc6db..158be431d9 100644 --- a/Logic/Tags/Tag.ts +++ b/Logic/Tags/Tag.ts @@ -37,7 +37,7 @@ export class Tag extends TagsFilter { * isEmpty.matchesProperties({"key": undefined}) // => true * */ - matchesProperties(properties: any): boolean { + matchesProperties(properties: Record): boolean { const foundValue = properties[this.key] if (foundValue === undefined && (this.value === "" || this.value === undefined)) { // The tag was not found @@ -62,7 +62,11 @@ export class Tag extends TagsFilter { t.asHumanString() // => "key=value" t.asHumanString(true) // => "key=value" */ - asHumanString(linkToWiki?: boolean, shorten?: boolean, currentProperties?: any) { + asHumanString( + linkToWiki?: boolean, + shorten?: boolean, + currentProperties?: Record + ) { let v = this.value if (shorten) { v = Utils.EllipsesAfter(v, 25) @@ -134,7 +138,7 @@ export class Tag extends TagsFilter { return false } - visit(f: (TagsFilter) => void) { + visit(f: (tagsFilter: TagsFilter) => void) { f(this) } } diff --git a/Logic/Tags/TagsFilter.ts b/Logic/Tags/TagsFilter.ts index 4f8944ff37..b06158b4f9 100644 --- a/Logic/Tags/TagsFilter.ts +++ b/Logic/Tags/TagsFilter.ts @@ -9,9 +9,13 @@ export abstract class TagsFilter { */ abstract shadows(other: TagsFilter): boolean - abstract matchesProperties(properties: any): boolean + abstract matchesProperties(properties: Record): boolean - abstract asHumanString(linkToWiki: boolean, shorten: boolean, properties: any): string + abstract asHumanString( + linkToWiki: boolean, + shorten: boolean, + properties: Record + ): string abstract usedKeys(): string[] @@ -27,7 +31,7 @@ export abstract class TagsFilter { * * Note: properties are the already existing tags-object. It is only used in the substituting tag */ - abstract asChange(properties: any): { k: string; v: string }[] + abstract asChange(properties: Record): { k: string; v: string }[] /** * Returns an optimized version (or self) of this tagsFilter @@ -54,5 +58,5 @@ export abstract class TagsFilter { /** * Walks the entire tree, every tagsFilter will be passed into the function once */ - abstract visit(f: (TagsFilter) => void) + abstract visit(f: (tagsFilter: TagsFilter) => void) } diff --git a/Logic/Web/QueryParameters.ts b/Logic/Web/QueryParameters.ts index 29726ae974..3b2fe4439a 100644 --- a/Logic/Web/QueryParameters.ts +++ b/Logic/Web/QueryParameters.ts @@ -35,6 +35,14 @@ export class QueryParameters { return source } + public static SetDefaultFor(key: string, value: string) { + if (QueryParameters.defaults[key] === value) { + return + } + QueryParameters.defaults[key] = value + QueryParameters.Serialize() + } + public static GetBooleanQueryParameter( key: string, deflt: boolean, @@ -80,6 +88,11 @@ export class QueryParameters { } } + /** + * Set the query parameters of the page location + * @constructor + * @private + */ private static Serialize() { const parts = [] for (const key of QueryParameters.order) { diff --git a/Models/ThemeConfig/Conversion/PrepareLayer.ts b/Models/ThemeConfig/Conversion/PrepareLayer.ts index eadd9f0863..2c3a4be775 100644 --- a/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -23,6 +23,8 @@ import predifined_filters from "../../../assets/layers/filters/filters.json" import { TagConfigJson } from "../Json/TagConfigJson" import PointRenderingConfigJson from "../Json/PointRenderingConfigJson" import LineRenderingConfigJson from "../Json/LineRenderingConfigJson" +import ValidationUtils from "./ValidationUtils" +import { RenderingSpecification } from "../../../UI/SpecialVisualization" class ExpandFilter extends DesugaringStep { private static readonly predefinedFilters = ExpandFilter.load_filters() @@ -218,7 +220,7 @@ class ExpandTagRendering extends Conversion< matchingTrs = layerTrs } else if (id.startsWith("*")) { const id_ = id.substring(1) - matchingTrs = layerTrs.filter((tr) => tr.group === id_ || tr.labels?.indexOf(id_) >= 0) + matchingTrs = layerTrs.filter((tr) => tr.labels?.indexOf(id_) >= 0) } else { matchingTrs = layerTrs.filter((tr) => tr.id === id || tr.labels?.indexOf(id) >= 0) } @@ -255,13 +257,6 @@ class ExpandTagRendering extends Conversion< ctx: string ): TagRenderingConfigJson[] { const state = this._state - if (tr === "questions") { - return [ - { - id: "questions", - }, - ] - } if (typeof tr === "string") { const lookup = this.lookup(tr) @@ -415,6 +410,111 @@ class ExpandTagRendering extends Conversion< } } +export class AddQuestionBox extends DesugaringStep { + constructor() { + super( + "Adds a 'questions'-object if no question element is added yet", + ["tagRenderings"], + "AddQuestionBox" + ) + } + + convert( + json: LayerConfigJson, + context: string + ): { result: LayerConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } { + if (json.tagRenderings === undefined) { + return { result: json } + } + json = JSON.parse(JSON.stringify(json)) + const allSpecials: Exclude[] = [] + .concat( + ...json.tagRenderings.map((tr) => + ValidationUtils.getSpecialVisualsationsWithArgs(tr) + ) + ) + .filter((spec) => typeof spec !== "string") + + const questionSpecials = allSpecials.filter((sp) => sp.func.funcName === "questions") + const noLabels = questionSpecials.filter( + (sp) => sp.args.length === 0 || sp.args[0].trim() === "" + ) + + const errors: string[] = [] + const warnings: string[] = [] + if (noLabels.length > 1) { + console.log(json.tagRenderings) + errors.push( + "At " + + context + + ": multiple 'questions'-visualisations found which would show _all_ questions. Don't do this" + ) + } + + // ALl labels that are used in this layer + const allLabels = new Set( + [].concat(...json.tagRenderings.map((tr) => (tr).labels ?? [])) + ) + const seen = new Set() + for (const questionSpecial of questionSpecials) { + if (typeof questionSpecial === "string") { + continue + } + const used = questionSpecial.args[0] + ?.split(";") + ?.map((a) => a.trim()) + ?.filter((s) => s != "") + const blacklisted = questionSpecial.args[1] + ?.split(";") + ?.map((a) => a.trim()) + ?.filter((s) => s != "") + if (blacklisted?.length > 0 && used?.length > 0) { + errors.push( + "At " + + context + + ": the {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." + + "\n Whitelisted: " + + used.join(", ") + + "\n Blacklisted: " + + blacklisted.join(", ") + ) + } + for (const usedLabel of used) { + if (!allLabels.has(usedLabel)) { + errors.push( + "At " + + context + + ": this layers specifies a special question element for label `" + + usedLabel + + "`, but this label doesn't exist.\n" + + " Available labels are " + + Array.from(allLabels).join(", ") + ) + } + seen.add(usedLabel) + } + } + + if (noLabels.length == 0) { + /* At this point, we know which question labels are not yet handled and which already are handled, and we + * know there is no previous catch-all questions + */ + const question: TagRenderingConfigJson = { + id: "leftover-questions", + render: { + "*": `{questions( ,${Array.from(seen).join(";")})}`, + }, + } + json.tagRenderings.push(question) + } + return { + result: json, + errors, + warnings, + } + } +} + export class ExpandRewrite extends Conversion, T[]> { constructor() { super("Applies a rewrite", [], "ExpandRewrite") diff --git a/Models/ThemeConfig/Conversion/PrepareTheme.ts b/Models/ThemeConfig/Conversion/PrepareTheme.ts index 28f3b30c40..c9fb286b46 100644 --- a/Models/ThemeConfig/Conversion/PrepareTheme.ts +++ b/Models/ThemeConfig/Conversion/PrepareTheme.ts @@ -10,7 +10,7 @@ import { SetDefault, } from "./Conversion" import { LayoutConfigJson } from "../Json/LayoutConfigJson" -import { PrepareLayer } from "./PrepareLayer" +import { AddQuestionBox, PrepareLayer } from "./PrepareLayer" import { LayerConfigJson } from "../Json/LayerConfigJson" import { Utils } from "../../../Utils" import Constants from "../../Constants" @@ -336,7 +336,6 @@ export class AddMiniMap extends DesugaringStep { if (!hasMinimap) { layerConfig = { ...layerConfig } layerConfig.tagRenderings = [...layerConfig.tagRenderings] - layerConfig.tagRenderings.push(state.tagRenderings.get("questions")) layerConfig.tagRenderings.push(state.tagRenderings.get("minimap")) } @@ -662,6 +661,7 @@ export class PrepareTheme extends Fuse { : new AddDefaultLayers(state), new AddDependencyLayersToTheme(state), new AddImportLayers(), + new On("layers", new Each(new AddQuestionBox())), new On("layers", new Each(new AddMiniMap(state))) ) } diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index c480b7f692..8fa4c2ddea 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -619,12 +619,12 @@ class MiscTagRenderingChecks extends DesugaringStep { ': detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`' ) } - if (json.group) { + if (json["group"]) { errors.push( "At " + context + ': groups are deprecated, use `"label": ["' + - json.group + + json["group"] + '"]` instead' ) } diff --git a/Models/ThemeConfig/Conversion/ValidationUtils.ts b/Models/ThemeConfig/Conversion/ValidationUtils.ts index 691d3b2ec0..036d84185a 100644 --- a/Models/ThemeConfig/Conversion/ValidationUtils.ts +++ b/Models/ThemeConfig/Conversion/ValidationUtils.ts @@ -1,7 +1,7 @@ import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" import { Utils } from "../../../Utils" import SpecialVisualizations from "../../../UI/SpecialVisualizations" -import { SpecialVisualization } from "../../../UI/SpecialVisualization" +import { RenderingSpecification, SpecialVisualization } from "../../../UI/SpecialVisualization" export default class ValidationUtils { /** @@ -11,11 +11,18 @@ export default class ValidationUtils { public static getSpecialVisualisations( renderingConfig: TagRenderingConfigJson ): SpecialVisualization[] { + return ValidationUtils.getSpecialVisualsationsWithArgs(renderingConfig).map( + (spec) => spec["func"] + ) + } + public static getSpecialVisualsationsWithArgs( + renderingConfig: TagRenderingConfigJson + ): RenderingSpecification[] { const translations: any[] = Utils.NoNull([ renderingConfig.render, ...(renderingConfig.mappings ?? []).map((m) => m.then), ]) - const all: SpecialVisualization[] = [] + const all: RenderingSpecification[] = [] for (let translation of translations) { if (typeof translation == "string") { translation = { "*": translation } @@ -28,9 +35,7 @@ export default class ValidationUtils { const template = translation[key] const parts = SpecialVisualizations.constructSpecification(template) - const specials = parts - .filter((p) => typeof p !== "string") - .map((special) => special["func"]) + const specials = parts.filter((p) => typeof p !== "string") all.push(...specials) } } diff --git a/Models/ThemeConfig/PointRenderingConfig.ts b/Models/ThemeConfig/PointRenderingConfig.ts index d89f09e3e1..f13daa4ea3 100644 --- a/Models/ThemeConfig/PointRenderingConfig.ts +++ b/Models/ThemeConfig/PointRenderingConfig.ts @@ -244,8 +244,8 @@ export default class PointRenderingConfig extends WithContextLoader { iconAndBadges.SetClass("w-full h-full") } - const css = this.cssDef?.GetRenderValue(tags, undefined)?.txt - const cssClasses = this.cssClasses?.GetRenderValue(tags, undefined)?.txt + const css = this.cssDef?.GetRenderValue(tags)?.txt + const cssClasses = this.cssClasses?.GetRenderValue(tags)?.txt let label = this.GetLabel(tags) let htmlEl: BaseUIElement diff --git a/Models/ThemeConfig/TagRenderingConfig.ts b/Models/ThemeConfig/TagRenderingConfig.ts index a2f7ab90af..1320949d83 100644 --- a/Models/ThemeConfig/TagRenderingConfig.ts +++ b/Models/ThemeConfig/TagRenderingConfig.ts @@ -203,19 +203,6 @@ export default class TagRenderingConfig { throw `${context}: A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}` } - if (this.id === "questions" && this.render !== undefined) { - for (const ln in this.render.translations) { - const txt: string = this.render.translations[ln] - if (txt.indexOf("{questions}") >= 0) { - continue - } - throw `${context}: The rendering for language ${ln} does not contain {questions}. This is a bug, as this rendering should include exactly this to trigger those questions to be shown!` - } - if (this.freeform?.key !== undefined && this.freeform?.key !== "questions") { - throw `${context}: If the ID is questions to trigger a question box, the only valid freeform value is 'questions' as well. Set freeform to questions or remove the freeform all together` - } - } - if (this.freeform) { if (this.render === undefined) { throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}` @@ -509,15 +496,11 @@ export default class TagRenderingConfig { }) } } - return applicableMappings } - public GetRenderValue( - tags: any, - defltValue: any = undefined - ): TypedTranslation | undefined { - return this.GetRenderValueWithImage(tags, defltValue)?.then + public GetRenderValue(tags: Record): TypedTranslation | undefined { + return this.GetRenderValueWithImage(tags)?.then } /** @@ -526,8 +509,7 @@ export default class TagRenderingConfig { * @constructor */ public GetRenderValueWithImage( - tags: any, - defltValue: any = undefined + tags: Record ): { then: TypedTranslation; icon?: string } | undefined { if (this.condition !== undefined) { if (!this.condition.matchesProperties(tags)) { @@ -554,7 +536,7 @@ export default class TagRenderingConfig { return { then: this.render } } - return { then: defltValue } + return undefined } /** @@ -625,6 +607,76 @@ export default class TagRenderingConfig { } } + /** + * Given a value for the freeform key and an overview of the selected mappings, construct the correct tagsFilter to apply + * + * @param freeformValue The freeform value which will be applied as 'freeform.key'. Ignored if 'freeform.key' is not set + * + * @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform + * @param multiSelectedMapping (Only used if multiAnswer == true): all the mappings that must be applied. Set multiSelectedMapping[mappings.length] to use the freeform as well + */ + public constructChangeSpecification( + freeformValue: string | undefined, + singleSelectedMapping: number, + multiSelectedMapping: boolean[] | undefined + ): UploadableTag { + if ( + freeformValue === undefined && + singleSelectedMapping === undefined && + multiSelectedMapping === undefined + ) { + return undefined + } + if (this.mappings === undefined && freeformValue === undefined) { + return undefined + } + if ( + this.freeform !== undefined && + (this.mappings === undefined || + this.mappings.length == 0 || + (singleSelectedMapping === this.mappings.length && !this.multiAnswer)) + ) { + // Either no mappings, or this is a radio-button selected freeform value + return new And([ + new Tag(this.freeform.key, freeformValue), + ...(this.freeform.addExtraTags ?? []), + ]) + } + + if (this.multiAnswer) { + let selectedMappings: UploadableTag[] = this.mappings + .filter((_, i) => multiSelectedMapping[i]) + .map((m) => new And([m.if, ...(m.addExtraTags ?? [])])) + + let unselectedMappings: UploadableTag[] = this.mappings + .filter((_, i) => !multiSelectedMapping[i]) + .map((m) => m.ifnot) + + if (multiSelectedMapping.at(-1)) { + // The freeform value was selected as well + selectedMappings.push( + new And([ + new Tag(this.freeform.key, freeformValue), + ...(this.freeform.addExtraTags ?? []), + ]) + ) + } + return TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings]) + } else { + if (singleSelectedMapping === this.mappings.length) { + return new And([ + new Tag(this.freeform.key, freeformValue), + ...(this.freeform.addExtraTags ?? []), + ]) + } else { + return new And([ + this.mappings[singleSelectedMapping].if, + ...(this.mappings[singleSelectedMapping].addExtraTags ?? []), + ]) + } + } + } + GenerateDocumentation(): BaseUIElement { let withRender: (BaseUIElement | string)[] = [] if (this.freeform?.key !== undefined) { diff --git a/UI/Base/Tr.svelte b/UI/Base/Tr.svelte index 236379e48b..35ec819e43 100644 --- a/UI/Base/Tr.svelte +++ b/UI/Base/Tr.svelte @@ -13,7 +13,7 @@ // Text for the current language let txt: string | undefined; - onDestroy(Locale.language.addCallbackAndRunD(l => { + $: onDestroy(Locale.language.addCallbackAndRunD(l => { const translation = t?.textFor(l) if(translation === undefined){ return diff --git a/UI/BigComponents/SelectedElementView.svelte b/UI/BigComponents/SelectedElementView.svelte index 26bc288f1e..9e1f498c6c 100644 --- a/UI/BigComponents/SelectedElementView.svelte +++ b/UI/BigComponents/SelectedElementView.svelte @@ -3,52 +3,32 @@ import { UIEventSource } from "../../Logic/UIEventSource"; import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import type { SpecialVisualizationState } from "../SpecialVisualization"; - import TagRenderingAnswer from "../Popup/TagRenderingAnswer.svelte"; - import TagRenderingQuestion from "../Popup/TagRenderingQuestion.svelte"; + import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"; + import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"; + import { onDestroy } from "svelte"; export let selectedElement: Feature; export let layer: LayerConfig; export let tags: UIEventSource>; + let _tags: Record; + onDestroy(tags.addCallbackAndRun(tags => { + _tags = tags; + })); export let state: SpecialVisualizationState; - - /** - * const title = new TagRenderingAnswer( - * tags, - * layerConfig.title ?? new TagRenderingConfig("POI"), - * state - * ).SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2 text-2xl") - * const titleIcons = new Combine( - * layerConfig.titleIcons.map((icon) => { - * return new TagRenderingAnswer( - * tags, - * icon, - * state, - * "block h-8 max-h-8 align-baseline box-content sm:p-0.5 titleicon" - * ) - * }) - * ).SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") - * - * return new Combine([ - * new Combine([title, titleIcons]).SetClass( - * "flex flex-col sm:flex-row flex-grow justify-between" - * ), - * ]) - */ -

- +

{#each layer.titleIcons as titleIconConfig (titleIconConfig.id)}
- +
{/each}
@@ -58,10 +38,10 @@
{#each layer.tagRenderings as config (config.id)} - {#if config.IsKnown($tags)} - - {:else} - + {#if config.condition === undefined || config.condition.matchesProperties(_tags)} + {#if config.IsKnown(_tags)} + + {/if} {/if} {/each}
diff --git a/UI/InputElement/ValidatedInput.svelte b/UI/InputElement/ValidatedInput.svelte index ef4ac7d82a..4d568599ef 100644 --- a/UI/InputElement/ValidatedInput.svelte +++ b/UI/InputElement/ValidatedInput.svelte @@ -1,41 +1,53 @@ {#if validator.textArea} {:else }
- + {#if !$isValid} {/if} diff --git a/UI/Popup/QuestionViz.ts b/UI/Popup/QuestionViz.ts new file mode 100644 index 0000000000..8d5f8f4c2f --- /dev/null +++ b/UI/Popup/QuestionViz.ts @@ -0,0 +1,51 @@ +import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" +import { Feature } from "geojson" +import BaseUIElement from "../BaseUIElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import SvelteUIElement from "../Base/SvelteUIElement" +import Questionbox from "./TagRendering/Questionbox.svelte" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" + +/** + * Thin wrapper around QuestionBox.svelte to include it into the special Visualisations + */ +export default class QuestionViz implements SpecialVisualization { + funcName = "questions" + docs = + "The special element which shows the questions which are unkown. Added by default if not yet there" + args = [ + { + name: "labels", + doc: "One or more ';'-separated labels. If these are given, only questions with these labels will be given. Use `unlabeled` for all questions that don't have an explicit label. If none given, all questions will be shown", + }, + { + name: "blacklisted-labels", + doc: "One or more ';'-separated labels of questions which should _not_ be included", + }, + ] + + constr( + state: SpecialVisualizationState, + tags: UIEventSource>, + args: string[], + feature: Feature, + layer: LayerConfig + ): BaseUIElement { + const labels = args[0] + ?.split(";") + ?.map((s) => s.trim()) + ?.filter((s) => s !== "") + const blacklist = args[1] + ?.split(";") + ?.map((s) => s.trim()) + ?.filter((s) => s !== "") + return new SvelteUIElement(Questionbox, { + layer, + tags, + selectedElement: feature, + state, + onlyForLabels: labels, + notForLabels: blacklist, + }) + } +} diff --git a/UI/Popup/TagRendering/FreeformInput.svelte b/UI/Popup/TagRendering/FreeformInput.svelte new file mode 100644 index 0000000000..c3e37d7489 --- /dev/null +++ b/UI/Popup/TagRendering/FreeformInput.svelte @@ -0,0 +1,26 @@ + + + dispatch("selected")}> + +{#if $feedback !== undefined} +
+ +
+{/if} diff --git a/UI/Popup/TagRendering/Inline.svelte b/UI/Popup/TagRendering/Inline.svelte new file mode 100644 index 0000000000..2d1655049d --- /dev/null +++ b/UI/Popup/TagRendering/Inline.svelte @@ -0,0 +1,26 @@ + + + + {Utils.SubstituteKeys(before, _tags)} + + {Utils.SubstituteKeys(after, _tags)} + diff --git a/UI/Popup/TagRendering/Questionbox.svelte b/UI/Popup/TagRendering/Questionbox.svelte new file mode 100644 index 0000000000..93d5e7c0c5 --- /dev/null +++ b/UI/Popup/TagRendering/Questionbox.svelte @@ -0,0 +1,108 @@ + + +{#if _questionsToAsk.length === 0} + All done! You answered {answered} questions and skipped {$skippedQuestions.size} questions. + {#if $skippedQuestions.size > 0 } + + {/if} +{:else } +
+ +
+ {#each _questionsToAsk as question (question.id)} + + {/each} +
+ +
+ {skip(_firstQuestion, true)}}> + + + +
+ +
+
+{/if} diff --git a/UI/Popup/SpecialTranslation.svelte b/UI/Popup/TagRendering/SpecialTranslation.svelte similarity index 55% rename from UI/Popup/SpecialTranslation.svelte rename to UI/Popup/TagRendering/SpecialTranslation.svelte index cb0a6674dc..cb5cc70348 100644 --- a/UI/Popup/SpecialTranslation.svelte +++ b/UI/Popup/TagRendering/SpecialTranslation.svelte @@ -1,14 +1,15 @@ {#each specs as specpart} {#if typeof specpart === "string"} {:else if $tags !== undefined } - + {/if} {/each} diff --git a/UI/Popup/TagRendering/TagRenderingAnswer.svelte b/UI/Popup/TagRendering/TagRenderingAnswer.svelte new file mode 100644 index 0000000000..637356c981 --- /dev/null +++ b/UI/Popup/TagRendering/TagRenderingAnswer.svelte @@ -0,0 +1,38 @@ + + +{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties(_tags))} + {#if trs.length === 1} + + {/if} + {#if trs.length > 1} +
    + {#each trs as mapping} +
  • + +
  • + {/each} +
+ {/if} +{/if} diff --git a/UI/Popup/TagRendering/TagRenderingEditable.svelte b/UI/Popup/TagRendering/TagRenderingEditable.svelte new file mode 100644 index 0000000000..20f727c81f --- /dev/null +++ b/UI/Popup/TagRendering/TagRenderingEditable.svelte @@ -0,0 +1,46 @@ + + +{#if config.question} + {#if editMode} + + + + {:else} +
+ + +
+ {/if} +{:else } + +{/if} diff --git a/UI/Popup/TagRenderingMapping.svelte b/UI/Popup/TagRendering/TagRenderingMapping.svelte similarity index 54% rename from UI/Popup/TagRenderingMapping.svelte rename to UI/Popup/TagRendering/TagRenderingMapping.svelte index c7e4966337..95f83f707d 100644 --- a/UI/Popup/TagRenderingMapping.svelte +++ b/UI/Popup/TagRendering/TagRenderingMapping.svelte @@ -1,13 +1,15 @@ {#if mapping.icon !== undefined} -
+
- +
{:else if mapping.then !== undefined} - + {/if} diff --git a/UI/Popup/TagRendering/TagRenderingQuestion.svelte b/UI/Popup/TagRendering/TagRenderingQuestion.svelte new file mode 100644 index 0000000000..f8468558fc --- /dev/null +++ b/UI/Popup/TagRendering/TagRenderingQuestion.svelte @@ -0,0 +1,147 @@ + + +{#if config.question !== undefined} +
+ +
+ + {config.id} +
+ +
+ + {#if config.questionhint} +
+ +
+ {/if} + + {#if config.freeform?.key && !(config.mappings?.length > 0)} + + + {/if} + + {#if config.mappings !== undefined && !config.multiAnswer} + +
+ {#each config.mappings as mapping, i (mapping.then)} + {#if !mappingIsHidden(mapping) } + + {/if} + {/each} + {#if config.freeform?.key} + + {/if} +
+ {/if} + + + {#if config.mappings !== undefined && config.multiAnswer} + +
+ {#each config.mappings as mapping, i (mapping.then)} + {#if !mappingIsHidden(mapping) } + + {/if} + {/each} + {#if config.freeform?.key} + + {/if} +
+ {/if} + + + +
+ + + +
+ +
+{/if} diff --git a/UI/Popup/TagRenderingAnswer.svelte b/UI/Popup/TagRenderingAnswer.svelte deleted file mode 100644 index c25cc206c8..0000000000 --- a/UI/Popup/TagRenderingAnswer.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties(tags))} -
- {#if trs.length === 1} - - {/if} - {#if trs.length > 1} - {#each trs as mapping} - - {/each} - {/if} -
-{/if} diff --git a/UI/Popup/TagRenderingQuestion.svelte b/UI/Popup/TagRenderingQuestion.svelte deleted file mode 100644 index ac5f14cb66..0000000000 --- a/UI/Popup/TagRenderingQuestion.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - -{#if config.question !== undefined} -
- -
- - {config.id} -
- -
- - {#if config.questionhint} -
- -
- {/if} - - {#if config.freeform?.key && !(config.mappings?.length > 0)} - - - {/if} - - {#if config.mappings !== undefined} -
- {#each config.mappings as mapping} - {#if mapping.hideInAnswer === true || !(mapping.hideInAnswer) || (console.log(tags) || true) || !(mapping.hideInAnswer?.matchesProperties($tags)) } - - {/if} - {/each} -
- {/if} - -
-{/if} diff --git a/UI/SpecialVisualization.ts b/UI/SpecialVisualization.ts index 690427cdc6..60e8fd2094 100644 --- a/UI/SpecialVisualization.ts +++ b/UI/SpecialVisualization.ts @@ -11,6 +11,7 @@ import { Feature, Geometry } from "geojson" import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" import { MangroveIdentity } from "../Logic/Web/MangroveReviews" import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" /** * The state needed to render a special Visualisation. @@ -47,7 +48,10 @@ export interface SpecialVisualizationState { readonly fullNodeDatabase?: FullNodeDatabaseSource readonly perLayer: ReadonlyMap - readonly userRelatedState: { readonly mangroveIdentity: MangroveIdentity } + readonly userRelatedState: { + readonly mangroveIdentity: MangroveIdentity + readonly showAllQuestionsAtOnce: UIEventSource + } } export interface SpecialVisualization { @@ -73,7 +77,8 @@ export interface SpecialVisualization { state: SpecialVisualizationState, tagSource: UIEventSource>, argument: string[], - feature: Feature + feature: Feature, + layer: LayerConfig ): BaseUIElement } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 386cda60fd..6c06a7ecc3 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -55,6 +55,7 @@ import FeatureReviews from "../Logic/Web/MangroveReviews" import Maproulette from "../Logic/Maproulette" import SvelteUIElement from "./Base/SvelteUIElement" import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" +import QuestionViz from "./Popup/QuestionViz" export default class SpecialVisualizations { public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList() @@ -81,6 +82,10 @@ export default class SpecialVisualizations { return [] } + if (template["type"] !== undefined) { + console.trace("Got a non-expanded template while constructing the specification") + throw "Got a non-expanded template while constructing the specification" + } const allKnownSpecials = extraMappings.concat(SpecialVisualizations.specialVisualizations) for (const knownSpecial of allKnownSpecials) { // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way' @@ -226,6 +231,7 @@ export default class SpecialVisualizations { private static initList(): SpecialVisualization[] { const specialVisualizations: SpecialVisualization[] = [ + new QuestionViz(), new HistogramViz(), new StealViz(), new MinimapViz(), @@ -956,7 +962,8 @@ export default class SpecialVisualizations { state, new UIEventSource>(e.feature.properties), e.args, - e.feature + e.feature, + undefined ) }) return new Combine([new Title(s.funcName), s.docs, ...examples]) diff --git a/UI/ThemeViewGUI.svelte b/UI/ThemeViewGUI.svelte index 02ad5d7af5..5ad82bfcb9 100644 --- a/UI/ThemeViewGUI.svelte +++ b/UI/ThemeViewGUI.svelte @@ -67,7 +67,7 @@ mapproperties.zoom.update(z => z-1)}> - + @@ -79,9 +79,7 @@
- +
@@ -94,20 +92,20 @@ selected ? "tab-selected" : "tab-unselected"}> - + selected ? "tab-selected" : "tab-unselected"}> - + selected ? "tab-selected" : "tab-unselected"}>Tab 3 - + {#if layout.layers.some((l) => l.presets?.length > 0)} - + {/if} @@ -168,12 +166,12 @@
{#if $selectedElement !== undefined && $selectedLayer !== undefined} -
+
-
- - +
+ +
diff --git a/assets/layers/charging_station/charging_station.json b/assets/layers/charging_station/charging_station.json index 49ecada1cf..3977b4c09d 100644 --- a/assets/layers/charging_station/charging_station.json +++ b/assets/layers/charging_station/charging_station.json @@ -1591,7 +1591,9 @@ }, { "id": "voltage-0", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Schuko wall plug without ground pin (CEE7/4 type F)
offer?", "nl": "Welke spanning levert de stekker van type
Schuko stekker zonder aardingspin (CEE7/4 type F)
", @@ -1629,7 +1631,9 @@ }, { "id": "current-0", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Schuko wall plug without ground pin (CEE7/4 type F)
offer?", "nl": "Welke stroom levert de stekker van type
Schuko stekker zonder aardingspin (CEE7/4 type F)
?", @@ -1670,7 +1674,9 @@ }, { "id": "power-output-0", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Schuko wall plug without ground pin (CEE7/4 type F)
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Schuko stekker zonder aardingspin (CEE7/4 type F)
?", @@ -1711,7 +1717,9 @@ }, { "id": "voltage-1", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
European wall plug with ground pin (CEE7/4 type E)
offer?", "nl": "Welke spanning levert de stekker van type
Europese stekker met aardingspin (CEE7/4 type E)
", @@ -1749,7 +1757,9 @@ }, { "id": "current-1", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
European wall plug with ground pin (CEE7/4 type E)
offer?", "nl": "Welke stroom levert de stekker van type
Europese stekker met aardingspin (CEE7/4 type E)
?", @@ -1790,7 +1800,9 @@ }, { "id": "power-output-1", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
European wall plug with ground pin (CEE7/4 type E)
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Europese stekker met aardingspin (CEE7/4 type E)
?", @@ -1843,7 +1855,9 @@ }, { "id": "voltage-2", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Chademo
offer?", "nl": "Welke spanning levert de stekker van type
Chademo
", @@ -1883,7 +1897,9 @@ }, { "id": "current-2", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Chademo
offer?", "nl": "Welke stroom levert de stekker van type
Chademo
?", @@ -1923,7 +1939,9 @@ }, { "id": "power-output-2", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Chademo
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Chademo
?", @@ -1961,7 +1979,9 @@ }, { "id": "voltage-3", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Type 1 with cable (J1772)
offer?", "nl": "Welke spanning levert de stekker van type
Type 1 met kabel (J1772)
", @@ -2011,7 +2031,9 @@ }, { "id": "current-3", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Type 1 with cable (J1772)
offer?", "nl": "Welke stroom levert de stekker van type
Type 1 met kabel (J1772)
?", @@ -2052,7 +2074,9 @@ }, { "id": "power-output-3", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Type 1 with cable (J1772)
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Type 1 met kabel (J1772)
?", @@ -2102,7 +2126,9 @@ }, { "id": "voltage-4", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Type 1 without cable (J1772)
offer?", "nl": "Welke spanning levert de stekker van type
Type 1 zonder kabel (J1772)
", @@ -2152,7 +2178,9 @@ }, { "id": "current-4", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Type 1 without cable (J1772)
offer?", "nl": "Welke stroom levert de stekker van type
Type 1 zonder kabel (J1772)
?", @@ -2193,7 +2221,9 @@ }, { "id": "power-output-4", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Type 1 without cable (J1772)
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Type 1 zonder kabel (J1772)
?", @@ -2267,7 +2297,9 @@ }, { "id": "voltage-5", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Type 1 CCS (aka Type 1 Combo)
offer?", "nl": "Welke spanning levert de stekker van type
Type 1 CCS (ook gekend als Type 1 Combo)
", @@ -2317,7 +2349,9 @@ }, { "id": "current-5", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Type 1 CCS (aka Type 1 Combo)
offer?", "nl": "Welke stroom levert de stekker van type
Type 1 CCS (ook gekend als Type 1 Combo)
?", @@ -2371,7 +2405,9 @@ }, { "id": "power-output-5", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Type 1 CCS (aka Type 1 Combo)
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Type 1 CCS (ook gekend als Type 1 Combo)
?", @@ -2445,7 +2481,9 @@ }, { "id": "voltage-6", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Tesla Supercharger
offer?", "nl": "Welke spanning levert de stekker van type
Tesla Supercharger
", @@ -2483,7 +2521,9 @@ }, { "id": "current-6", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Tesla Supercharger
offer?", "nl": "Welke stroom levert de stekker van type
Tesla Supercharger
?", @@ -2537,7 +2577,9 @@ }, { "id": "power-output-6", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Tesla Supercharger
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Tesla Supercharger
?", @@ -2599,7 +2641,9 @@ }, { "id": "voltage-7", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Type 2 (mennekes)
offer?", "nl": "Welke spanning levert de stekker van type
Type 2 (mennekes)
", @@ -2649,7 +2693,9 @@ }, { "id": "current-7", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Type 2 (mennekes)
offer?", "nl": "Welke stroom levert de stekker van type
Type 2 (mennekes)
?", @@ -2703,7 +2749,9 @@ }, { "id": "power-output-7", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Type 2 (mennekes)
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Type 2 (mennekes)
?", @@ -2753,7 +2801,9 @@ }, { "id": "voltage-8", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Type 2 CCS (mennekes)
offer?", "nl": "Welke spanning levert de stekker van type
Type 2 CCS (mennekes)
", @@ -2803,7 +2853,9 @@ }, { "id": "current-8", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Type 2 CCS (mennekes)
offer?", "nl": "Welke stroom levert de stekker van type
Type 2 CCS (mennekes)
?", @@ -2857,7 +2909,9 @@ }, { "id": "power-output-8", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Type 2 CCS (mennekes)
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Type 2 CCS (mennekes)
?", @@ -2895,7 +2949,9 @@ }, { "id": "voltage-9", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Type 2 with cable (mennekes)
offer?", "nl": "Welke spanning levert de stekker van type
Type 2 met kabel (J1772)
", @@ -2945,7 +3001,9 @@ }, { "id": "current-9", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Type 2 with cable (mennekes)
offer?", "nl": "Welke stroom levert de stekker van type
Type 2 met kabel (J1772)
?", @@ -2998,7 +3056,9 @@ }, { "id": "power-output-9", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Type 2 with cable (mennekes)
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Type 2 met kabel (J1772)
?", @@ -3048,7 +3108,9 @@ }, { "id": "voltage-10", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Tesla Supercharger CCS (a branded Type 2 CSS)
offer?", "nl": "Welke spanning levert de stekker van type
Tesla Supercharger CCS (een type2 CCS met Tesla-logo)
", @@ -3098,7 +3160,9 @@ }, { "id": "current-10", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Tesla Supercharger CCS (a branded type2_css)
offer?", "nl": "Welke stroom levert de stekker van type
Tesla Supercharger CCS (een type2 CCS met Tesla-logo)
?", @@ -3152,7 +3216,9 @@ }, { "id": "power-output-10", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Tesla Supercharger CCS (a branded Type 2 CSS)
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Tesla Supercharger CCS (een type2 CCS met Tesla-logo)
?", @@ -3190,7 +3256,9 @@ }, { "id": "voltage-11", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Tesla Supercharger (Destination)
offer?", "nl": "Welke spanning levert de stekker van type
Tesla Supercharger (Destination)
", @@ -3228,7 +3296,9 @@ }, { "id": "current-11", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Tesla Supercharger (Destination)
offer?", "nl": "Welke stroom levert de stekker van type
Tesla Supercharger (Destination)
?", @@ -3284,7 +3354,9 @@ }, { "id": "power-output-11", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Tesla Supercharger (Destination)
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Tesla Supercharger (destination)
?", @@ -3346,7 +3418,9 @@ }, { "id": "voltage-12", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Tesla Supercharger (Destination) (A Type 2 with cable branded as Tesla)
offer?", "nl": "Welke spanning levert de stekker van type
Tesla supercharger (destination). (Een Type 2 met kabel en Tesla-logo)
?", @@ -3396,7 +3470,9 @@ }, { "id": "current-12", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Tesla Supercharger (Destination) (A Type 2 with cable branded as Tesla)
offer?", "nl": "Welke stroom levert de stekker van type
Tesla Supercharger (Destination) (Een Type 2 met kabel en Tesla-logo)
?", @@ -3449,7 +3525,9 @@ }, { "id": "power-output-12", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Tesla Supercharger (Destination) (A Type 2 with cable branded as Tesla)
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Tesla supercharger (destination) (Een Type 2 met kabel en Tesla-logo)
?", @@ -3499,7 +3577,9 @@ }, { "id": "voltage-13", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
USB to charge phones and small electronics
offer?", "nl": "Welke spanning levert de stekker van type
USB om GSMs en kleine electronica op te laden
", @@ -3537,7 +3617,9 @@ }, { "id": "current-13", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
USB to charge phones and small electronics
offer?", "nl": "Welke stroom levert de stekker van type
USB om GSMs en kleine electronica op te laden
?", @@ -3595,7 +3677,9 @@ }, { "id": "power-output-13", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
USB to charge phones and small electronics
offer?", "nl": "Welk vermogen levert een enkele stekker van type
USB om GSMs en kleine electronica op te laden
?", @@ -3645,7 +3729,9 @@ }, { "id": "voltage-14", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Bosch Active Connect with 3 pins and cable
offer?", "nl": "Welke spanning levert de stekker van type
Bosch Active Connect met 3 pinnen aan een kabel
", @@ -3670,7 +3756,9 @@ }, { "id": "current-14", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Bosch Active Connect with 3 pins and cable
offer?", "nl": "Welke stroom levert de stekker van type
Bosch Active Connect met 3 pinnen aan een kabel
?", @@ -3697,7 +3785,9 @@ }, { "id": "power-output-14", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Bosch Active Connect with 3 pins and cable
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Bosch Active Connect met 3 pinnen aan een kabel
?", @@ -3722,7 +3812,9 @@ }, { "id": "voltage-15", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What voltage do the plugs with
Bosch Active Connect with 5 pins and cable
offer?", "nl": "Welke spanning levert de stekker van type
Bosch Active Connect met 5 pinnen aan een kabel
", @@ -3747,7 +3839,9 @@ }, { "id": "current-15", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What current do the plugs with
Bosch Active Connect with 5 pins and cable
offer?", "nl": "Welke stroom levert de stekker van type
Bosch Active Connect met 5 pinnen aan een kabel
?", @@ -3774,7 +3868,9 @@ }, { "id": "power-output-15", - "label": ["technical"], + "labels": [ + "technical" + ], "question": { "en": "What power output does a single plug of type
Bosch Active Connect with 5 pins and cable
offer?", "nl": "Welk vermogen levert een enkele stekker van type
Bosch Active Connect met 5 pinnen aan een kabel
?", @@ -4471,16 +4567,13 @@ ] } }, + "questions", { - "id": "questions" - }, - { - "id": "questions", - "label": ["technical"], + "id": "questions_technical", "render": { - "en": "

Technical questions

The questions below are very technical. Feel free to ignore them
{questions}", - "nl": "

Technische vragen

De vragen hieronder zijn erg technisch - sla deze over indien je hier geen tijd voor hebt
{questions}", - "de": "

Technische Fragen

Die folgenden Fragen sind sehr technisch. Sie können sie gerne ignorieren
{questions}" + "en": "

Technical questions

The questions below are very technical. Feel free to ignore them
{questions(technical)}", + "nl": "

Technische vragen

De vragen hieronder zijn erg technisch - sla deze over indien je hier geen tijd voor hebt
{questions(technical)}", + "de": "

Technische Fragen

Die folgenden Fragen sind sehr technisch. Sie können sie gerne ignorieren
{questions(technical)}" } } ], @@ -5065,4 +5158,4 @@ }, "neededChangesets": 10 } -} +} \ No newline at end of file diff --git a/assets/layers/charging_station/charging_station.protojson b/assets/layers/charging_station/charging_station.protojson index 7b1e54a5a0..5d0a2750c1 100644 --- a/assets/layers/charging_station/charging_station.protojson +++ b/assets/layers/charging_station/charging_station.protojson @@ -722,10 +722,9 @@ }, { "id": "questions", - "label": ["technical"], "render": { - "en": "

Technical questions

The questions below are very technical. Feel free to ignore them
{questions}", - "nl": "

Technische vragen

De vragen hieronder zijn erg technisch - sla deze over indien je hier geen tijd voor hebt
{questions}" + "en": "

Technical questions

The questions below are very technical. Feel free to ignore them
{questions(technical)}", + "nl": "

Technische vragen

De vragen hieronder zijn erg technisch - sla deze over indien je hier geen tijd voor hebt
{questions(technical)}" } } ], diff --git a/assets/layers/charging_station/csvToJson.ts b/assets/layers/charging_station/csvToJson.ts index 9e43211b56..419eaac06e 100644 --- a/assets/layers/charging_station/csvToJson.ts +++ b/assets/layers/charging_station/csvToJson.ts @@ -161,7 +161,7 @@ function run(file, protojson) { technicalQuestions.push({ "id": "voltage-" + i, - group: "technical", + labels: ["technical"], question: { en: `What voltage do the plugs with ${descrWithImage_en} offer?`, nl: `Welke spanning levert de stekker van type ${descrWithImage_nl}` @@ -195,7 +195,7 @@ function run(file, protojson) { technicalQuestions.push({ "id": "current-" + i, - group:"technical", + labels:["technical"], question: { en: `What current do the plugs with ${descrWithImage_en} offer?`, nl: `Welke stroom levert de stekker van type ${descrWithImage_nl}?`, @@ -229,7 +229,7 @@ function run(file, protojson) { technicalQuestions.push({ "id": "power-output-" + i, - group:"technical", + labels:["technical"], question: { en: `What power output does a single plug of type ${descrWithImage_en} offer?`, nl: `Welk vermogen levert een enkele stekker van type ${descrWithImage_nl}?`, diff --git a/assets/layers/cycleways_and_roads/cycleways_and_roads.json b/assets/layers/cycleways_and_roads/cycleways_and_roads.json index 0d2cdd78c9..2b05545bf7 100644 --- a/assets/layers/cycleways_and_roads/cycleways_and_roads.json +++ b/assets/layers/cycleways_and_roads/cycleways_and_roads.json @@ -1694,4 +1694,4 @@ "fr": "Toutes les infrastructures sur lesquelles quelqu'un peut rouler, accompagnées de questions sur cette infrastructure", "ca": "Totes les infraestructures per les quals algú pot ciclar, acompanyades de preguntes sobre aquesta infraestructura" } -} +} \ No newline at end of file diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index 7a2d4a4bcc..2cc1e9688a 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -90,11 +90,15 @@ }, { "id": "translations-title", - "label": ["translations"], + "label": [ + "translations" + ], "render": "

Translating MapComplete

" }, { - "label": ["translations"], + "label": [ + "translations" + ], "id": "translation-mode", "question": { "en": "Do you want to help translating MapComplete?", @@ -127,7 +131,9 @@ ] }, { - "label": ["translations"], + "label": [ + "translations" + ], "id": "translation-help", "mappings": [ { @@ -153,7 +159,9 @@ ] }, { - "label": ["translations"], + "label": [ + "translations" + ], "id": "translation-completeness", "render": { "ca": "Les traduccions de {_theme} en {_language} tenen un {_translation_percentage}%: {_translation_translated_count} cadenes de {_translation_total} estan traduïdes", @@ -188,7 +196,9 @@ }, { "id": "translation-links", - "label": ["translations"], + "label": [ + "translations" + ], "condition": { "and": [ "_translation_links~*", @@ -318,4 +328,4 @@ } ], "mapRendering": null -} +} \ No newline at end of file diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index df8fdf00c9..809bbe2b4d 100644 --- a/assets/tagRenderings/questions.json +++ b/assets/tagRenderings/questions.json @@ -2,7 +2,7 @@ "id": "shared_questions", "questions": { "description": "Show the images block at this location", - "id": "questions" + "render": "{questions()}" }, "images": { "description": "This block shows the known images which are linked with the `image`-keys, but also via `mapillary` and `wikidata`", @@ -1984,4 +1984,4 @@ "pl": "Nazwa sieci to {internet_access:ssid}" } } -} \ No newline at end of file +} diff --git a/assets/themes/grb/grb.json b/assets/themes/grb/grb.json index 191378d53d..a92e57cffb 100644 --- a/assets/themes/grb/grb.json +++ b/assets/themes/grb/grb.json @@ -699,7 +699,7 @@ "_applicable=feat.get('_overlapping')?.filter(p => (p._imported_osm_object_found === 'true' || p._intersects_with_other_features === ''))?.map(p => p.id)", "_applicable_count=feat.get('_applicable')?.length" ], - "tagRenderings": [ + "tagRenderings+": [ { "id": "hw", "render": "There are {_applicable_count} applicable elements in view", diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.json index b6b71d1cbb..09270e0f33 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.json @@ -551,7 +551,7 @@ "builtin": "current_view", "override": { "title": "Statistics on changesets in the current view", - "tagRenderings": [ + "tagRenderings+": [ { "id": "link_to_more", "render": { diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json index 2ba80425da..0d07d694f8 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json @@ -249,7 +249,7 @@ "builtin": "current_view", "override": { "title": "Statistics on changesets in the current view", - "tagRenderings": [ + "tagRenderings+": [ { "id": "link_to_more", "render": { diff --git a/assets/themes/onwheels/onwheels.json b/assets/themes/onwheels/onwheels.json index bbb4ff7b40..28ca7159a8 100644 --- a/assets/themes/onwheels/onwheels.json +++ b/assets/themes/onwheels/onwheels.json @@ -397,7 +397,7 @@ "es": "Estadísticas" } }, - "tagRenderings": [ + "tagRenderings+": [ { "id": "stats", "render": "{statistics()}" @@ -495,4 +495,4 @@ ] }, "enableDownload": true -} \ No newline at end of file +}