diff --git a/Customizations/JSON/FromJSON.ts b/Customizations/JSON/FromJSON.ts index b390127..2020a2d 100644 --- a/Customizations/JSON/FromJSON.ts +++ b/Customizations/JSON/FromJSON.ts @@ -9,8 +9,6 @@ import {LayerConfigJson} from "./LayerConfigJson"; import {LayerDefinition, Preset} from "../LayerDefinition"; import {TagDependantUIElementConstructor} from "../UIElementConstructor"; import Combine from "../../UI/Base/Combine"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; -import {ImageCarouselConstructor} from "../../UI/Image/ImageCarousel"; import * as drinkingWater from "../../assets/layers/drinking_water/drinking_water.json"; import * as ghostbikes from "../../assets/layers/ghost_bike/ghost_bike.json" import * as viewpoint from "../../assets/layers/viewpoint/viewpoint.json" @@ -20,12 +18,19 @@ import * as birdhides from "../../assets/layers/bird_hide/birdhides.json" import * as nature_reserve from "../../assets/layers/nature_reserve/nature_reserve.json" import {Utils} from "../../Utils"; +import ImageCarouselWithUploadConstructor from "../../UI/Image/ImageCarouselWithUpload"; +import {ImageCarouselConstructor} from "../../UI/Image/ImageCarousel"; +import {State} from "../../State"; export class FromJSON { public static sharedLayers: Map = FromJSON.getSharedLayers(); private static getSharedLayers() { + + // We inject a function into state while we are busy + State.FromBase64 = FromJSON.FromBase64; + const sharedLayers = new Map(); const sharedLayersList = [ @@ -50,6 +55,7 @@ export class FromJSON { } public static LayoutFromJSON(json: LayoutConfigJson): Layout { + console.log(json) const tr = FromJSON.Translation; const layers = json.layers.map(FromJSON.Layer); @@ -111,7 +117,6 @@ export class FromJSON { if (typeof json === "string") { - switch (json) { case "picture": { return new ImageCarouselWithUploadConstructor() @@ -123,7 +128,7 @@ export class FromJSON { return new ImageCarouselWithUploadConstructor() } case "images": { - return new ImageCarouselWithUploadConstructor() + return new ImageCarouselWithUploadConstructor() } case "picturesNoUpload": { return new ImageCarouselConstructor() @@ -193,11 +198,13 @@ export class FromJSON { let rendering = new TagRenderingOptions({ question: question, freeform: freeform, - mappings: mappings + mappings: mappings, + multiAnswer: json.multiAnswer }); - + if (json.condition) { - return rendering.OnlyShowIf(FromJSON.Tag(json.condition, `In tagrendering ${propertyName}.condition`)); + const condition = FromJSON.Tag(json.condition, `In tagrendering ${propertyName}.condition`); + return rendering.OnlyShowIf(condition); } return rendering; diff --git a/Customizations/JSON/TagRenderingConfigJson.ts b/Customizations/JSON/TagRenderingConfigJson.ts index c432cc8..7f2541b 100644 --- a/Customizations/JSON/TagRenderingConfigJson.ts +++ b/Customizations/JSON/TagRenderingConfigJson.ts @@ -38,7 +38,12 @@ export interface TagRenderingConfigJson { * Usefull to add a 'fixme=freeform textfield used - to be checked' **/ addExtraTags?: string[]; - } + }, + + /** + * If true, use checkboxes instead of radio buttons when asking the question + */ + multiAnswer?: boolean, /** * Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes diff --git a/Customizations/Layers/BikeCafes.ts b/Customizations/Layers/BikeCafes.ts index daa3270..24a96dc 100644 --- a/Customizations/Layers/BikeCafes.ts +++ b/Customizations/Layers/BikeCafes.ts @@ -1,6 +1,6 @@ import {LayerDefinition} from "../LayerDefinition"; import FixedText from "../Questions/FixedText"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; +import ImageCarouselWithUploadConstructor from "../../UI/Image/ImageCarouselWithUpload"; import Translations from "../../UI/i18n/Translations"; import CafeName from "../Questions/bike/CafeName"; import {And, Or, RegexTag, Tag} from "../../Logic/Tags"; diff --git a/Customizations/Layers/BikeOtherShops.ts b/Customizations/Layers/BikeOtherShops.ts index 33386a5..6dfb7be 100644 --- a/Customizations/Layers/BikeOtherShops.ts +++ b/Customizations/Layers/BikeOtherShops.ts @@ -1,7 +1,7 @@ import {LayerDefinition} from "../LayerDefinition"; import Translations from "../../UI/i18n/Translations"; import {And, RegexTag, Tag} from "../../Logic/Tags"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; +import ImageCarouselWithUploadConstructor from "../../UI/Image/ImageCarouselWithUpload"; import ShopRetail from "../Questions/bike/ShopRetail"; import ShopPump from "../Questions/bike/ShopPump"; import ShopRental from "../Questions/bike/ShopRental"; diff --git a/Customizations/Layers/BikeShops.ts b/Customizations/Layers/BikeShops.ts index 3c6640e..5537ca5 100644 --- a/Customizations/Layers/BikeShops.ts +++ b/Customizations/Layers/BikeShops.ts @@ -1,7 +1,7 @@ import {LayerDefinition} from "../LayerDefinition"; import Translations from "../../UI/i18n/Translations"; import {And, Tag} from "../../Logic/Tags"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; +import ImageCarouselWithUploadConstructor from "../../UI/Image/ImageCarouselWithUpload"; import ShopRetail from "../Questions/bike/ShopRetail"; import ShopPump from "../Questions/bike/ShopPump"; import ShopRental from "../Questions/bike/ShopRental"; diff --git a/Customizations/Layers/Bos.ts b/Customizations/Layers/Bos.ts index d002742..8fa93c2 100644 --- a/Customizations/Layers/Bos.ts +++ b/Customizations/Layers/Bos.ts @@ -5,7 +5,7 @@ import {OperatorTag} from "../Questions/OperatorTag"; import {NameQuestion} from "../Questions/NameQuestion"; import {NameInline} from "../Questions/NameInline"; import {DescriptionQuestion} from "../Questions/DescriptionQuestion"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; +import ImageCarouselWithUploadConstructor from "../../UI/Image/ImageCarouselWithUpload"; export class Bos extends LayerDefinition { diff --git a/Customizations/Layers/InformationBoard.ts b/Customizations/Layers/InformationBoard.ts index c918168..eebdf4c 100644 --- a/Customizations/Layers/InformationBoard.ts +++ b/Customizations/Layers/InformationBoard.ts @@ -1,8 +1,7 @@ import {LayerDefinition} from "../LayerDefinition"; -import FixedText from "../Questions/FixedText"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import {And, Tag} from "../../Logic/Tags"; import {TagRenderingOptions} from "../TagRenderingOptions"; +import ImageCarouselWithUploadConstructor from "../../UI/Image/ImageCarouselWithUpload"; export class InformationBoard extends LayerDefinition { constructor() { diff --git a/Customizations/Layers/Map.ts b/Customizations/Layers/Map.ts index 230cb8b..91afd7a 100644 --- a/Customizations/Layers/Map.ts +++ b/Customizations/Layers/Map.ts @@ -1,6 +1,6 @@ import {LayerDefinition} from "../LayerDefinition"; import FixedText from "../Questions/FixedText"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; +import ImageCarouselWithUploadConstructor from "../../UI/Image/ImageCarouselWithUpload"; import {Tag} from "../../Logic/Tags"; import {TagRenderingOptions} from "../TagRenderingOptions"; diff --git a/Customizations/Layers/NatureReserves.ts b/Customizations/Layers/NatureReserves.ts index 3725f84..4ebd93a 100644 --- a/Customizations/Layers/NatureReserves.ts +++ b/Customizations/Layers/NatureReserves.ts @@ -5,7 +5,7 @@ import {OperatorTag} from "../Questions/OperatorTag"; import {NameQuestion} from "../Questions/NameQuestion"; import {NameInline} from "../Questions/NameInline"; import {DescriptionQuestion} from "../Questions/DescriptionQuestion"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; +import ImageCarouselWithUploadConstructor from "../../UI/Image/ImageCarouselWithUpload"; import {TagRenderingOptions} from "../TagRenderingOptions"; export class NatureReserves extends LayerDefinition { diff --git a/Customizations/Layers/Park.ts b/Customizations/Layers/Park.ts index 6fe87f8..69c57b1 100644 --- a/Customizations/Layers/Park.ts +++ b/Customizations/Layers/Park.ts @@ -3,7 +3,7 @@ import {Or, Tag} from "../../Logic/Tags"; import {NameQuestion} from "../Questions/NameQuestion"; import {NameInline} from "../Questions/NameInline"; import {DescriptionQuestion} from "../Questions/DescriptionQuestion"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; +import ImageCarouselWithUploadConstructor from "../../UI/Image/ImageCarouselWithUpload"; import {TagRenderingOptions} from "../TagRenderingOptions"; export class Park extends LayerDefinition { diff --git a/Customizations/TagRenderingOptions.ts b/Customizations/TagRenderingOptions.ts index 3dab835..4de499b 100644 --- a/Customizations/TagRenderingOptions.ts +++ b/Customizations/TagRenderingOptions.ts @@ -21,6 +21,7 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { placeholder?: string | Translation; extraTags?: TagsFilter }; + multiAnswer?: boolean, mappings?: { k: TagsFilter; txt: string | Translation; priority?: number, substitute?: boolean, hideInAnwser?: boolean }[] }; @@ -54,8 +55,12 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { * * */ - mappings?: { k: TagsFilter, txt: Translation | string, priority?: number, substitute?: boolean , hideInAnswer?:boolean}[], + mappings?: { k: TagsFilter, txt: Translation | string, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[], + /** + * If true, use checkboxes to answer instead of radiobuttons + */ + multiAnswer?: boolean, /** * If one wants to render a freeform tag (thus no predefined key/values) or if there are a few well-known tags with a freeform object, @@ -113,12 +118,25 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { return template.Subs(tags); } - console.warn("No content defined for",tags," with mapping",this); + console.warn("No content defined for", tags, " with mapping", this); return undefined; } - public static tagRendering: (tags: UIEventSource, options: { priority?: number; question?: string | Translation; freeform?: { key: string; tagsPreprocessor?: (tags: any) => any; template: string | Translation; renderTemplate: string | Translation; placeholder?: string | Translation; extraTags?: TagsFilter }; mappings?: { k: TagsFilter; txt: string | Translation; priority?: number; substitute?: boolean, hideInAnswer?: boolean }[] }) => TagDependantUIElement; + public static tagRendering: (tags: UIEventSource, + options: { + priority?: number; + question?: string | Translation; + freeform?: { + key: string; + tagsPreprocessor?: (tags: any) => any; + template: string | Translation; + renderTemplate: string | Translation; + placeholder?: string | Translation; extraTags?: TagsFilter + }, + multiAnswer?: boolean, + mappings?: { k: TagsFilter; txt: string | Translation; priority?: number; substitute?: boolean, hideInAnswer?: boolean }[] + }) => TagDependantUIElement; construct(dependencies: Dependencies): TagDependantUIElement { return TagRenderingOptions.tagRendering(dependencies.tags, this.options); diff --git a/Logic/PersonalLayersPanel.ts b/Logic/PersonalLayersPanel.ts index 7a97f5a..99fff61 100644 --- a/Logic/PersonalLayersPanel.ts +++ b/Logic/PersonalLayersPanel.ts @@ -16,6 +16,7 @@ export class PersonalLayersPanel extends UIElement { super(State.state.favouriteLayers); this.ListenTo(State.state.osmConnection.userDetails); + this.ListenTo(State.state.favouriteLayers); this.UpdateView([]); const self = this; @@ -53,6 +54,9 @@ export class PersonalLayersPanel extends UIElement { this.checkboxes.push(header); for (const layer of layout.layers) { + if(typeof layer === "string"){ + continue; + } let icon = layer.icon; if (icon !== undefined && typeof (icon) !== "string") { icon = icon.GetContent({"id": "node/-1"}).txt ?? "./assets/bug.svg"; diff --git a/Logic/Tags.ts b/Logic/Tags.ts index a0afad8..ffcc4ca 100644 --- a/Logic/Tags.ts +++ b/Logic/Tags.ts @@ -344,5 +344,133 @@ export class TagUtils { } return properties; } - + + /** + * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set. + * E.g: + * + * FlattenMultiAnswer([and: [ "x=a", "y=0;1"], and: ["x=b", "y=2"], and: ["x=", "y=3"]]) + * will result in + * ["x=a;b", "y=0;1;2;3"] + * + * @param tagsFilters + * @constructor + */ + static FlattenMultiAnswer(tagsFilters: TagsFilter[]): And { + if (tagsFilters === undefined) { + return new And([]); + } + const keyValues = {} // Map string -> string[] + tagsFilters = [...tagsFilters] + while (tagsFilters.length > 0) { + const tagsFilter = tagsFilters.pop(); + + if (tagsFilter === undefined) { + continue; + } + + if (tagsFilter instanceof And) { + tagsFilters.push(...tagsFilter.and); + continue; + } + + if (tagsFilter instanceof Tag) { + if (keyValues[tagsFilter.key] === undefined) { + keyValues[tagsFilter.key] = []; + } + keyValues[tagsFilter.key].push(...tagsFilter.value.split(";")); + continue; + } + + console.error("Invalid type to flatten the multiAnswer", tagsFilter); + throw "Invalid type to FlattenMultiAnswer" + } + + const and: TagsFilter[] = [] + for (const key in keyValues) { + and.push(new Tag(key, Utils.Dedup(keyValues[key]).join(";"))); + } + + return new And(and); + + } + + /** + * Splits the actualTags onto a list of which the values are the same as the tagsFilters. + * Leftovers are returned in the list too if there is an 'undefined' value + */ + static SplitMultiAnswer(actualTags: TagsFilter, possibleTags: TagsFilter[], freeformKey: string, freeformExtraTags: TagsFilter): TagsFilter[] { + + const queue: TagsFilter[] = [actualTags] + + const keyValues = {} // key ==> value[] + + while (queue.length > 0) { + const tf = queue.pop(); + if (tf instanceof And) { + queue.push(...tf.and); + continue; + } + if (tf instanceof Tag) { + if (keyValues[tf.key] === undefined) { + keyValues[tf.key] = [] + } + keyValues[tf.key].push(...tf.value.split(";")); + continue; + } + + if (tf === undefined) { + continue; + } + + throw "Invalid tagfilter: " + JSON.stringify(tf) + } + + const foundValues = []; + for (const possibleTag of possibleTags) { + if (possibleTag === undefined) { + continue; + } + if (possibleTag instanceof Tag) { + const key = possibleTag.key; + const actualValues: string[] = keyValues[key] ?? []; + const possibleValues = possibleTag.value.split(";"); + + let allPossibleValuesFound = true; + for (const possibleValue of possibleValues) { + if (actualValues.indexOf(possibleValue) < 0) { + allPossibleValuesFound = false; + } + } + if (!allPossibleValuesFound) { + continue; + } + + // At this point, we know that 'possibleTag' is completely present in the tagset + // we add the possibleTag to the found values + foundValues.push(possibleTag); + + for (const possibleValue of possibleValues) { + actualValues.splice(actualValues.indexOf(possibleValue), 1); + } + + continue; + } + throw "Unsupported possibletag: " + JSON.stringify(possibleTag); + } + + let leftoverTag = undefined; + if (keyValues[freeformKey] !== undefined && keyValues[freeformKey].length !== 0) { + leftoverTag = new Tag(freeformKey, keyValues[freeformKey].join(";")); + if (freeformExtraTags !== undefined) { + leftoverTag = new And([ + leftoverTag, + freeformExtraTags + ]) + } + foundValues.push(leftoverTag); + } + + return foundValues; + } } diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index 36e8131..20df0ed 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -69,7 +69,7 @@ export class UIEventSource{ if(g !== undefined) { newSource.addCallback((latest) => { - self.setData((g(latest))); + self.setData(g(latest)); }) } diff --git a/State.ts b/State.ts index 416928f..91c9ab6 100644 --- a/State.ts +++ b/State.ts @@ -193,7 +193,7 @@ export class State { continue; } try { - const layout = FromJSON.FromBase64(customLayout.data); + const layout = State.FromBase64(customLayout.data); if(layout.id === undefined){ // This is an old style theme // We remove it @@ -252,4 +252,6 @@ export class State { } } + + public static FromBase64 : (data: string) => Layout = undefined; } diff --git a/UI/CustomGenerator/TagRenderingPreview.ts b/UI/CustomGenerator/TagRenderingPreview.ts index 8be7b24..d5757ae 100644 --- a/UI/CustomGenerator/TagRenderingPreview.ts +++ b/UI/CustomGenerator/TagRenderingPreview.ts @@ -39,7 +39,7 @@ export default class TagRenderingPreview extends UIElement { try { rendering = new VariableUiElement(es.map(tagRenderingConfig => { - const tr = FromJSON.TagRendering(tagRenderingConfig) + const tr = FromJSON.TagRendering(tagRenderingConfig, "preview") .construct({tags: this.previewTagValue}); return tr.Render(); } diff --git a/UI/Image/ImageCarouselWithUpload.ts b/UI/Image/ImageCarouselWithUpload.ts index 2984e77..99e84ed 100644 --- a/UI/Image/ImageCarouselWithUpload.ts +++ b/UI/Image/ImageCarouselWithUpload.ts @@ -9,7 +9,8 @@ import {OsmImageUploadHandler} from "../../Logic/Osm/OsmImageUploadHandler"; import {State} from "../../State"; import Translation from "../i18n/Translation"; -export class ImageCarouselWithUploadConstructor implements TagDependantUIElementConstructor{ +export default class ImageCarouselWithUploadConstructor implements TagDependantUIElementConstructor{ + IsKnown(properties: any): boolean { return true; } diff --git a/UI/Image/ImgurImage.ts b/UI/Image/ImgurImage.ts index e54ab8c..dbeefac 100644 --- a/UI/Image/ImgurImage.ts +++ b/UI/Image/ImgurImage.ts @@ -11,8 +11,8 @@ export class ImgurImage extends UIElement { * Dictionary from url to alreayd known license info */ static allLicenseInfos: any = {}; - private _imageMeta: UIEventSource; - private _imageLocation: string; + private readonly _imageMeta: UIEventSource; + private readonly _imageLocation: string; constructor(source: string) { super(undefined) diff --git a/UI/Image/WikimediaImage.ts b/UI/Image/WikimediaImage.ts index 6354716..68a7688 100644 --- a/UI/Image/WikimediaImage.ts +++ b/UI/Image/WikimediaImage.ts @@ -7,8 +7,8 @@ export class WikimediaImage extends UIElement { static allLicenseInfos: any = {}; - private _imageMeta: UIEventSource; - private _imageLocation : string; + private readonly _imageMeta: UIEventSource; + private readonly _imageLocation : string; constructor(source: string) { super(undefined) diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index 2e5c274..e515fca 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -13,17 +13,16 @@ export class CheckBoxes extends InputElement { private readonly value: UIEventSource; private readonly _elements: InputElement[] - private readonly _selectFirstAsDefault: boolean; - constructor(elements: InputElement[], - selectFirstAsDefault = true) { + constructor(elements: InputElement[]) { super(undefined); this._elements = Utils.NoNull(elements); - this._selectFirstAsDefault = selectFirstAsDefault; + this.dumbMode = false; this.value = new UIEventSource([]) this.ListenTo(this.value); + this.value.addCallback(latest => console.log("Latest is ", latest)) } @@ -35,6 +34,7 @@ export class CheckBoxes extends InputElement { let matchFound = false; for (const element of this._elements) { if (element.IsValid(t)) { + element.GetValue().setData(t); matchFound = true; break } @@ -56,7 +56,6 @@ export class CheckBoxes extends InputElement { } InnerRender(): string { - let body = ""; for (let i = 0; i < this._elements.length; i++) { let el = this._elements[i]; @@ -66,27 +65,30 @@ export class CheckBoxes extends InputElement { } - return `
${body}
`; + return `
${body}
`; } protected InnerUpdate(htmlElement: HTMLElement) { super.InnerUpdate(htmlElement); const self = this; + for (let i = 0; i < this._elements.length; i++) { const el = document.getElementById(this.IdFor(i)); const inputEl = this._elements[i]; - { - const v = inputEl.GetValue().data; - const index = self.value.data.indexOf(v); - if(index >= 0){ - // @ts-ignore - el.checked = true; + for (const t of this.value.data ?? []) { + if(t === undefined){ + continue; + } + let isValid = inputEl.IsValid(t); + // @ts-ignore + el.checked = isValid; + if(isValid){ + break; } } - el.onchange = e => { const v = inputEl.GetValue().data; const index = self.value.data.indexOf(v); diff --git a/UI/Input/InputElementMap.ts b/UI/Input/InputElementMap.ts new file mode 100644 index 0000000..b5611bb --- /dev/null +++ b/UI/Input/InputElementMap.ts @@ -0,0 +1,53 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; + + +export default class InputElementMap extends InputElement { + + private readonly _inputElement: InputElement; + private isSame: (x0: X, x1: X) => boolean; + private readonly fromX: (x: X) => T; + private readonly toX: (t: T) => X; + private readonly _value: UIEventSource; + + constructor(inputElement: InputElement, + isSame: (x0: X, x1: X) => boolean, + toX: (t: T) => X, + fromX: (x: X) => T + ) { + super(); + this.isSame = isSame; + this.fromX = fromX; + this.toX = toX; + this._inputElement = inputElement; + this.IsSelected = inputElement.IsSelected; + const self = this; + this._value = inputElement.GetValue().map( + (t => { + const currentX = self.GetValue()?.data; + const newX = toX(t); + if (isSame(currentX, newX)) { + return currentX; + } + return newX; + }), [], x => { + const newT = fromX(x); + return newT; + }); + } + + GetValue(): UIEventSource { + return this._value; + } + + InnerRender(): string { + return this._inputElement.InnerRender(); + } + + IsSelected: UIEventSource; + + IsValid(x: X): boolean { + return this._inputElement.IsValid(this.fromX(x)); + } + +} \ No newline at end of file diff --git a/Customizations/TagRendering.ts b/UI/TagRendering.ts similarity index 81% rename from Customizations/TagRendering.ts rename to UI/TagRendering.ts index c1b21ee..817d920 100644 --- a/Customizations/TagRendering.ts +++ b/UI/TagRendering.ts @@ -1,22 +1,23 @@ -import {UIElement} from "../UI/UIElement"; import {UIEventSource} from "../Logic/UIEventSource"; import {And, Tag, TagsFilter, TagUtils} from "../Logic/Tags"; -import {FixedUiElement} from "../UI/Base/FixedUiElement"; -import {SaveButton} from "../UI/SaveButton"; -import {VariableUiElement} from "../UI/Base/VariableUIElement"; -import {TagDependantUIElement} from "./UIElementConstructor"; -import {TextField, ValidatedTextField} from "../UI/Input/TextField"; -import {InputElement} from "../UI/Input/InputElement"; -import {InputElementWrapper} from "../UI/Input/InputElementWrapper"; -import {FixedInputElement} from "../UI/Input/FixedInputElement"; -import {RadioButton} from "../UI/Input/RadioButton"; import Translations from "../UI/i18n/Translations"; import Locale from "../UI/i18n/Locale"; import {State} from "../State"; -import {TagRenderingOptions} from "./TagRenderingOptions"; import Translation from "../UI/i18n/Translation"; import Combine from "../UI/Base/Combine"; - +import {TagDependantUIElement} from "../Customizations/UIElementConstructor"; +import {UIElement} from "./UIElement"; +import {VariableUiElement} from "./Base/VariableUIElement"; +import InputElementMap from "./Input/InputElementMap"; +import {CheckBoxes} from "./Input/Checkboxes"; +import {InputElement} from "./Input/InputElement"; +import {SaveButton} from "./SaveButton"; +import {RadioButton} from "./Input/RadioButton"; +import {InputElementWrapper} from "./Input/InputElementWrapper"; +import {FixedInputElement} from "./Input/FixedInputElement"; +import {TextField, ValidatedTextField} from "./Input/TextField"; +import {TagRenderingOptions} from "../Customizations/TagRenderingOptions"; +import {FixedUiElement} from "./Base/FixedUiElement"; export class TagRendering extends UIElement implements TagDependantUIElement { @@ -61,7 +62,6 @@ export class TagRendering extends UIElement implements TagDependantUIElement { priority?: number question?: string | Translation, - freeform?: { key: string, template: string | Translation, @@ -70,6 +70,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { extraTags?: TagsFilter, }, tagsPreprocessor?: ((tags: any) => any), + multiAnswer?: boolean, mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[] }) { super(tags); @@ -122,11 +123,10 @@ export class TagRendering extends UIElement implements TagDependantUIElement { }); } - // Prepare the actual input element -> pick an appropriate implementation this._questionElement = this.InputElementFor(options) ?? - new FixedInputElement("No input possible", new Tag("a","b")); + new FixedInputElement("No input possible", new Tag("a", "b")); const save = () => { const selection = self._questionElement.GetValue().data; console.log("Tagrendering: saving tags ", selection); @@ -193,59 +193,93 @@ export class TagRendering extends UIElement implements TagDependantUIElement { private InputElementFor(options: { freeform?: { - key: string, + key: string, template: string | Translation, renderTemplate: string | Translation, placeholder?: string | Translation, extraTags?: TagsFilter, }, + multiAnswer?: boolean, mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[] }): InputElement { - const elements = []; - - let freeformElement = undefined; + let freeformElement = undefined; if (options.freeform !== undefined) { freeformElement = this.InputForFreeForm(options.freeform); } - - if (options.mappings !== undefined) { - - const previousTexts= []; - for (const mapping of options.mappings) { - if(mapping.k === null){ - continue; - } - if(mapping.hideInAnswer){ - continue; - } - previousTexts.push(this.ApplyTemplate(mapping.txt)); - - elements.push(this.InputElementForMapping(mapping, mapping.substitute)); - } + + if (options.mappings === undefined || options.mappings.length === 0) { + return freeformElement; } - - if(freeformElement !== undefined) { + + + const elements: InputElement[] = []; + + for (const mapping of options.mappings) { + if (mapping.k === null) { + continue; + } + if (mapping.hideInAnswer) { + continue; + } + elements.push(this.InputElementForMapping(mapping, mapping.substitute)); + } + + if (freeformElement !== undefined) { elements.push(freeformElement); } - + if (options.multiAnswer) { + const possibleTags = elements.map(el => el.GetValue().data); + const checkBoxes = new CheckBoxes(elements); + + // In order to let this work, we are cheating a lot here + // First of all, it is very tricky to let the mapping stabilize + // The selection gets added as list and needs to be flattened into a new 'and' + // This new and causes the mapping to have a 'set value', which is unpacked to update the UI + // AFter which the UI reupdates and reapplies the value + // So, instead we opt to _always return the 'value' below which is statefully updated + // But, then we still have to figure out when to update... + // For this, we use the original inputElement + // This is very dirty code, I know + const value = new And([]); + const inputElement = new InputElementMap(checkBoxes, + (t0: And, t1: And) => { + return t0?.isEquivalent(t1) ?? t0 === t1 + }, + (fromUI) => { + if (fromUI === undefined) { + value.and = []; + return value; + } + const flattened = TagUtils.FlattenMultiAnswer(fromUI); + value.and = flattened.and; + return value; + }, + (fromTags) => { + return TagUtils.SplitMultiAnswer(fromTags, possibleTags, this._freeform?.key, this._freeform?.extraTags); + } + ); + + let previousSelectionCount = -1; + checkBoxes.GetValue().addCallback(selected => { + const newSelectionCount = selected.length; + if (newSelectionCount != previousSelectionCount) { + previousSelectionCount = newSelectionCount; + inputElement.GetValue().ping(); + } + }); + + return inputElement; - if (elements.length == 0) { - return new FixedInputElement("This should not happen: no tag renderings defined", undefined); } - if (elements.length == 1) { - return elements[0]; - } - return new RadioButton(elements, false); - } - private InputElementForMapping(mapping: { k: TagsFilter, txt: (string | Translation) }, substituteValues: boolean) { + private InputElementForMapping(mapping: { k: TagsFilter, txt: (string | Translation) }, substituteValues: boolean): FixedInputElement { if (substituteValues) { return new FixedInputElement(this.ApplyTemplate(mapping.txt), @@ -253,7 +287,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { (t0, t1) => t0.isEquivalent(t1) ); } - return new FixedInputElement(this.ApplyTemplate(mapping.txt),mapping.k, + return new FixedInputElement(this.ApplyTemplate(mapping.txt), mapping.k, (t0, t1) => t0.isEquivalent(t1)); } @@ -308,6 +342,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { const toString = (tag) => { + console.log("Decoding ", tag, "in freeform text element") if (tag instanceof And) { for (const subtag of tag.and) { if(subtag instanceof Tag && subtag.key === freeform.key){ @@ -497,7 +532,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { return tr.InnerRender(); } return tr.Subs(tags).InnerRender() - })); + })).ListenTo(Locale.language); } diff --git a/assets/layers/bike_repair_station/bike_repair_station.json b/assets/layers/bike_repair_station/bike_repair_station.json index 59daccf..21f8f7e 100644 --- a/assets/layers/bike_repair_station/bike_repair_station.json +++ b/assets/layers/bike_repair_station/bike_repair_station.json @@ -238,46 +238,38 @@ "gl": "Esta bomba de ar admite as seguintes válvulas: {valves}" }, "freeform": { - "addExtraTags": [ + "#addExtraTags": [ "fixme=Freeform 'valves'-tag used: possibly a wrong value" ], "key": "valves" }, + "multiAnswer": true, "mappings": [ - { - "if": "valves=sclaverand;schrader;dunlop", - "then": { - "en": "There is a default head, so Dunlop, Sclaverand and auto", - "nl": "Er is een standaard aansluiting, die dus voor Dunlop, Sclaverand en auto's", - "fr": "Il y a une valve par défaut, fonctionnant sur les valves Dunlop, Sclaverand et les valves de voitures", - "gl": "Hai un cabezal predeterminado que é compatíbel con Dunlop, Sclaverand e automóbil" - } - }, { "if": "valves=sclaverand", "then": { - "en": "Only Sclaverand (also known as Presta)", - "nl": "Enkel Sclaverand (ook gekend als Presta)", - "fr": "Seulement Sclaverand (aussi appelé Presta)", - "gl": "Só Sclaverand (tamén coñecido como Presta)" + "en": "Sclaverand (also known as Presta)", + "nl": "Sclaverand (ook gekend als Presta)", + "fr": "Sclaverand (aussi appelé Presta)", + "gl": "Sclaverand (tamén coñecido como Presta)" } }, { "if": "valves=dunlop", "then": { - "en": "Only Dunlop", - "nl": "Enkel Dunlop", - "fr": "Seulement Dunlop", - "gl": "Só Dunlop" + "en": "Dunlop", + "nl": "Dunlop", + "fr": "Dunlop", + "gl": "Dunlop" } }, { "if": "valves=schrader", "then": { - "en": "Only for cars", - "nl": "Enkel voor auto's", - "fr": "Seuelement les valves de voitures", - "gl": "Só para automóbiles" + "en": "Schrader (cars)", + "nl": "Schrader (auto's)", + "fr": "Schrader (les valves de voitures)", + "gl": "Schrader (para automóbiles)" } } ] diff --git a/assets/layers/bird_hide/birdhides.json b/assets/layers/bird_hide/birdhides.json index 86bcc6e..ddc1e60 100644 --- a/assets/layers/bird_hide/birdhides.json +++ b/assets/layers/bird_hide/birdhides.json @@ -159,21 +159,13 @@ }, "mappings": [ { - "if": { - "and": [ - "operator=Natuurpunt" - ] - }, + "if": "operator=Natuurpunt", "then": { "nl": "Beheer door Natuurpunt" } }, { - "if": { - "and": [ - "operator=Agentschap Natuur en Bos" - ] - }, + "if": "operator=Agentschap Natuur en Bos", "then": { "nl": "Beheer door het Agentschap Natuur en Bos " } diff --git a/createLayouts.ts b/createLayouts.ts index 9345abf..194590c 100644 --- a/createLayouts.ts +++ b/createLayouts.ts @@ -2,14 +2,15 @@ import {UIElement} from "./UI/UIElement"; // We HAVE to mark this while importing UIElement.runningFromConsole = true; -import {TagRendering} from "./Customizations/TagRendering"; import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; +import {FromJSON} from "./Customizations/JSON/FromJSON"; import {Layout} from "./Customizations/Layout"; import {readFileSync, writeFile, writeFileSync} from "fs"; import Locale from "./UI/i18n/Locale"; import svg2img from 'promise-svg2img'; import Translation from "./UI/i18n/Translation"; import Translations from "./UI/i18n/Translations"; +import {TagRendering} from "./UI/TagRendering"; TagRendering.injectFunction(); diff --git a/customGenerator.ts b/customGenerator.ts index 6d94d6e..74dd290 100644 --- a/customGenerator.ts +++ b/customGenerator.ts @@ -1,6 +1,6 @@ import {UIEventSource} from "./Logic/UIEventSource"; import {GenerateEmpty} from "./UI/CustomGenerator/GenerateEmpty"; -import {TagRendering} from "./Customizations/TagRendering"; +import {TagRendering} from "./UI/TagRendering"; import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson"; import {OsmConnection} from "./Logic/Osm/OsmConnection"; import CustomGeneratorPanel from "./UI/CustomGenerator/CustomGeneratorPanel"; diff --git a/deploy.sh b/deploy.sh index fd84c6c..054195f 100755 --- a/deploy.sh +++ b/deploy.sh @@ -20,27 +20,4 @@ fi git add . && git commit -m "New mapcomplete version" && git push cd - -# clean up the mess we made - # rm *.js - # rm Logic/*.js - # rm Logic/*.js - # rm Logic/*/*.js - # rm Logic/*/*/*.js - # rm UI/*.js - # rm UI/*/*.js - # rm UI/*/*/*.js - # rm Customizations/*.js - # rm Customizations/*/*.js - # rm Customizations/*/*/*.js - -rm *.webmanifest -rm assets/generated/* - -for f in ./*.html; do - if [[ "$f" == "./index.html" ]] || [[ "$f" == "./land.html" ]] || [[ "$f" == "./test.html" ]] || [[ "$f" == "./preferences.html" ]] || [[ "$f" == "./customGenerator.html" ]] - then - echo "Not removing $f" - else - rm $f - fi -done +./clean.sh diff --git a/index.ts b/index.ts index a8a89d3..ccdb343 100644 --- a/index.ts +++ b/index.ts @@ -1,4 +1,3 @@ -import {TagRendering} from "./Customizations/TagRendering"; import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; import {Layout} from "./Customizations/Layout"; import {FixedUiElement} from "./UI/Base/FixedUiElement"; @@ -7,6 +6,7 @@ import {QueryParameters} from "./Logic/Web/QueryParameters"; import {UIEventSource} from "./Logic/UIEventSource"; import * as $ from "jquery"; import {FromJSON} from "./Customizations/JSON/FromJSON"; +import {TagRendering} from "./UI/TagRendering"; TagRendering.injectFunction(); @@ -63,7 +63,6 @@ let layoutToUse: Layout = AllKnownLayouts.allSets[defaultLayout.toLowerCase()] ? const userLayoutParam = QueryParameters.GetQueryParameter("userlayout", "false"); const layoutFromBase64 = decodeURIComponent(userLayoutParam.data); -console.log(layoutFromBase64); if (layoutFromBase64.startsWith("wiki:")) { console.log("Downloading map theme from the wiki"); const themeName = layoutFromBase64.substr("wiki:".length);