From a2306c2c6f670e87defca53bbc20a20a68cfec51 Mon Sep 17 00:00:00 2001
From: pietervdvn <pietervdvn@posteo.net>
Date: Fri, 29 Oct 2021 03:42:33 +0200
Subject: [PATCH] Add the possibility to define a postfix and prefix for
 opening hours

---
 Docs/CalculatedTags.md                       |   2 +-
 Docs/SpecialInputElements.md                 |  68 +++++-
 Docs/SpecialRenderings.md                    |   8 +-
 UI/Input/InputElementMap.ts                  |   2 +-
 UI/Input/ValidatedTextField.ts               | 217 +++++++++++++------
 UI/OpeningHours/OpeningHoursInput.ts         |  38 +++-
 UI/OpeningHours/OpeningHoursVisualization.ts |  10 +-
 UI/SpecialVisualizations.ts                  |  11 +-
 UI/SubstitutedTranslation.ts                 |   4 +-
 assets/layers/bike_shop/bike_shop.json       |   2 +-
 10 files changed, 281 insertions(+), 81 deletions(-)

diff --git a/Docs/CalculatedTags.md b/Docs/CalculatedTags.md
index b9bfe2104..9e0ce6fa7 100644
--- a/Docs/CalculatedTags.md
+++ b/Docs/CalculatedTags.md
@@ -100,7 +100,7 @@ Adds the time that the data got loaded - pretty much the time of downloading fro
 
 
 
-### _last_edit:contributor, _last_edit:contributor:uid, _last_edit:changeset, _last_edit:timestamp, _version_number 
+### _last_edit:contributor, _last_edit:contributor:uid, _last_edit:changeset, _last_edit:timestamp, _version_number, _backend 
 
 
 
diff --git a/Docs/SpecialInputElements.md b/Docs/SpecialInputElements.md
index 3c05b4c87..5a70109de 100644
--- a/Docs/SpecialInputElements.md
+++ b/Docs/SpecialInputElements.md
@@ -24,7 +24,40 @@ A geographical length in meters (rounded at two points). Will give an extra mini
 
 ## wikidata
 
-A wikidata identifier, e.g. Q42. Input helper arguments: [ key: the value of this tag will initialize search (default: name), options: { removePrefixes: string[], removePostfixes: string[] }  these prefixes and postfixes will be removed from the initial search value]
+A wikidata identifier, e.g. Q42. 
+### Helper arguments 
+
+ 
+
+name | doc
+------ | -----
+key | the value of this tag will initialize search (default: name)
+options | A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`. 
+
+subarg | doc
+-------- | -----
+removePrefixes | remove these snippets of text from the start of the passed string to search
+removePostfixes | remove these snippets of text from the end of the passed string to search
+
+ 
+### Example usage 
+
+ The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name```"freeform": {
+                "key": "name:etymology:wikidata",
+                "type": "wikidata",
+                "helperArgs": [
+                    "name",
+                    {
+                        "removePostfixes": [
+                            "street",
+                            "boulevard",
+                            "path",
+                            "square",
+                            "plaza",
+                        ]
+                    }
+                ]
+            },```
 
 ## int
 
@@ -60,7 +93,38 @@ A phone number
 
 ## opening_hours
 
-Has extra elements to easily input when a POI is opened
+Has extra elements to easily input when a POI is opened. 
+### Helper arguments 
+
+ 
+
+name | doc
+------ | -----
+options | A JSON-object of type `{ prefix: string, postfix: string }`.  
+
+subarg | doc
+-------- | -----
+prefix | Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse
+postfix | Piece of text that will always be added to the end of the generated opening hours
+
+ 
+### Example usage 
+
+ To add a conditional (based on time) access restriction:
+
+```
+            "freeform": {
+                "key": "access:conditional",
+                "type": "opening_hours",
+                "helperArgs": [
+                    {
+                      "prefix":"no @ (",
+                      "postfix":")"
+                    }
+                ]
+            },```
+
+*Don't forget to pass these in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`
 
 ## color
 
diff --git a/Docs/SpecialRenderings.md b/Docs/SpecialRenderings.md
index 7fff50797..77b5592e3 100644
--- a/Docs/SpecialRenderings.md
+++ b/Docs/SpecialRenderings.md
@@ -14,11 +14,11 @@
 
 name | default | description
 ------ | --------- | -------------
-image key/prefix (multiple values allowed if comma-seperated) | image | The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... 
+image key/prefix (multiple values allowed if comma-seperated) | image,mapillary,image,wikidata,wikimedia_commons,image,image | The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... 
  
 #### Example usage 
 
- `{image_carousel(image)}` 
+ `{image_carousel(image,mapillary,image,wikidata,wikimedia_commons,image,image)}` 
 ### image_upload 
 
  Creates a button where a user can upload an image to IMGUR 
@@ -73,10 +73,12 @@ fallback | undefined | The identifier to use, if <i>tags[subjectKey]</i> as spec
 name | default | description
 ------ | --------- | -------------
 key | opening_hours | The tagkey from which the table is constructed.
+prefix |  | Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__
+postfix |  | Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__
  
 #### Example usage 
 
- `{opening_hours_table(opening_hours)}` 
+ A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}` 
 ### live 
 
  Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)} 
diff --git a/UI/Input/InputElementMap.ts b/UI/Input/InputElementMap.ts
index 548e50363..a2a50f9d3 100644
--- a/UI/Input/InputElementMap.ts
+++ b/UI/Input/InputElementMap.ts
@@ -25,8 +25,8 @@ export default class InputElementMap<T, X> extends InputElement<X> {
         const self = this;
         this._value = inputElement.GetValue().map(
             (t => {
-                const currentX = self.GetValue()?.data;
                 const newX = toX(t);
+                const currentX = self.GetValue()?.data;
                 if (isSame(currentX, newX)) {
                     return currentX;
                 }
diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts
index ad5554346..71db4a5b3 100644
--- a/UI/Input/ValidatedTextField.ts
+++ b/UI/Input/ValidatedTextField.ts
@@ -19,6 +19,9 @@ import {FixedInputElement} from "./FixedInputElement";
 import WikidataSearchBox from "../Wikipedia/WikidataSearchBox";
 import Wikidata from "../../Logic/Web/Wikidata";
 import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
+import Table from "../Base/Table";
+import Combine from "../Base/Combine";
+import Title from "../Base/Title";
 
 interface TextFieldDef {
     name: string,
@@ -28,12 +31,159 @@ interface TextFieldDef {
     inputHelper?: (value: UIEventSource<string>, options?: {
         location: [number, number],
         mapBackgroundLayer?: UIEventSource<any>,
-        args: (string | number | boolean)[]
+        args: (string | number | boolean | any)[]
         feature?: any
     }) => InputElement<string>,
     inputmode?: string
 }
 
+class WikidataTextField implements TextFieldDef {
+    name = "wikidata"
+    explanation =
+        new Combine([
+            "A wikidata identifier, e.g. Q42.",
+            new Title("Helper arguments"),
+            new Table(["name", "doc"],
+                [
+                    ["key", "the value of this tag will initialize search (default: name)"],
+                    ["options", new Combine(["A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.",
+                        new Table(
+                            ["subarg", "doc"],
+                            [["removePrefixes", "remove these snippets of text from the start of the passed string to search"],
+                                ["removePostfixes", "remove these snippets of text from the end of the passed string to search"],
+                            ]
+                        )])
+                    ]]),
+            new Title("Example usage"),
+            "The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name```" + `"freeform": {
+                "key": "name:etymology:wikidata",
+                "type": "wikidata",
+                "helperArgs": [
+                    "name",
+                    {
+                        "removePostfixes": [
+                            "street",
+                            "boulevard",
+                            "path",
+                            "square",
+                            "plaza",
+                        ]
+                    }
+                ]
+            },` + "```"
+        ]).AsMarkdown()
+
+
+    public isValid(str) {
+
+        if (str === undefined) {
+            return false;
+        }
+        if (str.length <= 2) {
+            return false;
+        }
+        return !str.split(";").some(str => Wikidata.ExtractKey(str) === undefined)
+    }
+
+    public reformat(str) {
+        if (str === undefined) {
+            return undefined;
+        }
+        let out = str.split(";").map(str => Wikidata.ExtractKey(str)).join("; ")
+        if (str.endsWith(";")) {
+            out = out + ";"
+        }
+        return out;
+    }
+
+    public inputHelper(currentValue, inputHelperOptions) {
+        const args = inputHelperOptions.args ?? []
+        const searchKey = args[0] ?? "name"
+
+        let searchFor = <string>inputHelperOptions.feature?.properties[searchKey]?.toLowerCase()
+
+        const options = args[1]
+        if (searchFor !== undefined && options !== undefined) {
+            const prefixes = <string[]>options["removePrefixes"]
+            const postfixes = <string[]>options["removePostfixes"]
+            for (const postfix of postfixes ?? []) {
+                if (searchFor.endsWith(postfix)) {
+                    searchFor = searchFor.substring(0, searchFor.length - postfix.length)
+                    break;
+                }
+            }
+
+            for (const prefix of prefixes ?? []) {
+                if (searchFor.startsWith(prefix)) {
+                    searchFor = searchFor.substring(prefix.length)
+                    break;
+                }
+            }
+
+        }
+
+        return new WikidataSearchBox({
+            value: currentValue,
+            searchText: new UIEventSource<string>(searchFor)
+        })
+    }
+}
+
+class OpeningHoursTextField implements TextFieldDef {
+    name = "opening_hours"
+    explanation =
+        new Combine([
+            "Has extra elements to easily input when a POI is opened.",
+            new Title("Helper arguments"),
+            new Table(["name", "doc"],
+                [
+                    ["options", new Combine([
+                        "A JSON-object of type `{ prefix: string, postfix: string }`. ",
+                        new Table(["subarg", "doc"],
+                            [
+                                ["prefix", "Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse"],
+                                ["postfix", "Piece of text that will always be added to the end of the generated opening hours"],
+                            ])
+
+                    ])
+                    ]
+                ]),
+            new Title("Example usage"),
+            "To add a conditional (based on time) access restriction:\n\n```" + `
+            "freeform": {
+                "key": "access:conditional",
+                "type": "opening_hours",
+                "helperArgs": [
+                    {
+                      "prefix":"no @ (",
+                      "postfix":")"
+                    }
+                ]
+            },` + "```\n\n*Don't forget to pass these in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`"]).AsMarkdown()
+
+
+    isValid() {
+        return true
+    }
+
+    reformat(str) {
+        return str
+    }
+
+    inputHelper(value: UIEventSource<string>, inputHelperOptions: {
+        location: [number, number],
+        mapBackgroundLayer?: UIEventSource<any>,
+        args: (string | number | boolean | any)[]
+        feature?: any
+    }) {
+        const args = (inputHelperOptions.args ?? [])[0]
+        const prefix = <string>args?.prefix ?? ""
+        const postfix = <string>args?.postfix ?? ""
+
+        return new OpeningHoursInput(value, prefix, postfix)
+    }
+}
+
 export default class ValidatedTextField {
 
     public static tpList: TextFieldDef[] = [
@@ -146,60 +296,7 @@ export default class ValidatedTextField {
             },
             "decimal"
         ),
-        ValidatedTextField.tp(
-            "wikidata",
-            "A wikidata identifier, e.g. Q42. Input helper arguments: [ key: the value of this tag will initialize search (default: name), options: { removePrefixes: string[], removePostfixes: string[] }  these prefixes and postfixes will be removed from the initial search value]",
-            (str) => {
-                if (str === undefined) {
-                    return false;
-                }
-                if(str.length <= 2){
-                    return false;
-                }
-                return !str.split(";").some(str => Wikidata.ExtractKey(str) === undefined)
-            },
-            (str) => {
-                if (str === undefined) {
-                    return undefined;
-                }
-                let out = str.split(";").map(str => Wikidata.ExtractKey(str)).join("; ")
-                if(str.endsWith(";")){
-                    out = out + ";"
-                }
-                return out;
-            },
-            (currentValue, inputHelperOptions) => {
-                const args = inputHelperOptions.args ?? []
-                const searchKey = args[0] ?? "name"
-
-                let searchFor = <string>inputHelperOptions.feature?.properties[searchKey]?.toLowerCase()
-
-                const options = args[1]
-                if (searchFor !== undefined && options !== undefined) {
-                    const prefixes = <string[]>options["removePrefixes"]
-                    const postfixes = <string[]>options["removePostfixes"]
-                    for (const postfix of postfixes ?? []) {
-                        if (searchFor.endsWith(postfix)) {
-                            searchFor = searchFor.substring(0, searchFor.length - postfix.length)
-                            break;
-                        }
-                    }
-
-                    for (const prefix of prefixes ?? []) {
-                        if (searchFor.startsWith(prefix)) {
-                            searchFor = searchFor.substring(prefix.length)
-                            break;
-                        }
-                    }
-
-                }
-
-                return new WikidataSearchBox({
-                    value: currentValue,
-                    searchText: new UIEventSource<string>(searchFor)
-                })
-            }
-        ),
+        new WikidataTextField(),
 
         ValidatedTextField.tp(
             "int",
@@ -299,15 +396,7 @@ export default class ValidatedTextField {
             undefined,
             "tel"
         ),
-        ValidatedTextField.tp(
-            "opening_hours",
-            "Has extra elements to easily input when a POI is opened",
-            () => true,
-            str => str,
-            (value) => {
-                return new OpeningHoursInput(value);
-            }
-        ),
+        new OpeningHoursTextField(),
         ValidatedTextField.tp(
             "color",
             "Shows a color picker",
diff --git a/UI/OpeningHours/OpeningHoursInput.ts b/UI/OpeningHours/OpeningHoursInput.ts
index d8d5ce16b..88b061a3d 100644
--- a/UI/OpeningHours/OpeningHoursInput.ts
+++ b/UI/OpeningHours/OpeningHoursInput.ts
@@ -23,11 +23,39 @@ export default class OpeningHoursInput extends InputElement<string> {
     private readonly _value: UIEventSource<string>;
     private readonly _element: BaseUIElement;
 
-    constructor(value: UIEventSource<string> = new UIEventSource<string>("")) {
+    constructor(value: UIEventSource<string> = new UIEventSource<string>(""), prefix = "", postfix = "") {
         super();
         this._value = value;
+        let valueWithoutPrefix = value
+        if (prefix !== "" && postfix !== "") {
+       
+            valueWithoutPrefix = value.map(str => {
+                if (str === undefined) {
+                    return undefined;
+                }
+                if(str === ""){
+                    return ""
+                }
+                if (str.startsWith(prefix) && str.endsWith(postfix)) {
+                    return str.substring(prefix.length, str.length - postfix.length)
+                }
+                return str
+            }, [], noPrefix => {
+                if (noPrefix === undefined) {
+                    return undefined;
+                }
+                if(noPrefix === ""){
+                    return ""
+                }
+                if (noPrefix.startsWith(prefix) && noPrefix.endsWith(postfix)) {
+                    return noPrefix
+                }
 
-        const leftoverRules = value.map<string[]>(str => {
+                return prefix + noPrefix + postfix
+            })
+        }
+
+        const leftoverRules = valueWithoutPrefix.map<string[]>(str => {
             if (str === undefined) {
                 return []
             }
@@ -45,9 +73,9 @@ export default class OpeningHoursInput extends InputElement<string> {
             return leftOvers;
         })
         // Note: MUST be bound AFTER the leftover rules!
-        const rulesFromOhPicker = value.map(OH.Parse);
+        const rulesFromOhPicker = valueWithoutPrefix.map(OH.Parse);
 
-        const ph = value.map<string>(str => {
+        const ph = valueWithoutPrefix.map<string>(str => {
             if (str === undefined) {
                 return ""
             }
@@ -68,7 +96,7 @@ export default class OpeningHoursInput extends InputElement<string> {
                 ...leftoverRules.data,
                 ph.data
             ]
-            value.setData(Utils.NoEmpty(rules).join(";"));
+            valueWithoutPrefix.setData(Utils.NoEmpty(rules).join(";"));
         }
 
         rulesFromOhPicker.addCallback(update);
diff --git a/UI/OpeningHours/OpeningHoursVisualization.ts b/UI/OpeningHours/OpeningHoursVisualization.ts
index 785d046a9..fb8ae05d6 100644
--- a/UI/OpeningHours/OpeningHoursVisualization.ts
+++ b/UI/OpeningHours/OpeningHoursVisualization.ts
@@ -23,10 +23,16 @@ export default class OpeningHoursVisualization extends Toggle {
         Translations.t.general.weekdays.abbreviations.sunday,
     ]
 
-    constructor(tags: UIEventSource<any>, key: string) {
+    constructor(tags: UIEventSource<any>, key: string, prefix = "", postfix = "") {
         const tagsDirect = tags.data;
         const ohTable = new VariableUiElement(tags
-            .map(tags => tags[key]) // This mapping will absorb all other changes to tags in order to prevent regeneration
+            .map(tags => {
+                const value : string = tags[key];
+                if(value.startsWith(prefix) && value.endsWith(postfix)){
+                    return value.substring(prefix.length, value.length - postfix.length)
+                }
+                return value;
+            }) // This mapping will absorb all other changes to tags in order to prevent regeneration
             .map(ohtext => {
                     try {
                         // noinspection JSPotentiallyInvalidConstructorUsage
diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts
index 680b3e571..fa3362107 100644
--- a/UI/SpecialVisualizations.ts
+++ b/UI/SpecialVisualizations.ts
@@ -253,9 +253,18 @@ export default class SpecialVisualizations {
                     name: "key",
                     defaultValue: "opening_hours",
                     doc: "The tagkey from which the table is constructed."
+                },{
+                    name: "prefix",
+                    defaultValue: "",
+                    doc:"Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__"
+                },{
+                    name: "postfix",
+                    defaultValue: "",
+                    doc:"Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__"
                 }],
+                example: "A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`",
                 constr: (state: State, tagSource: UIEventSource<any>, args) => {
-                    return new OpeningHoursVisualization(tagSource, args[0])
+                    return new OpeningHoursVisualization(tagSource, args[0], args[1], args[2])
                 }
             },
             {
diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts
index 361540e65..ff2d3a447 100644
--- a/UI/SubstitutedTranslation.ts
+++ b/UI/SubstitutedTranslation.ts
@@ -85,7 +85,9 @@ export class SubstitutedTranslation extends VariableUiElement {
                 const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4], extraMappings);
                 const args = knownSpecial.args.map(arg => arg.defaultValue ?? "");
                 if (argument.length > 0) {
-                    const realArgs = argument.split(",").map(str => str.trim());
+                    const realArgs = argument.split(",").map(str => str.trim()
+                        .replace(/&LPARENS/g, '(')
+                        .replace(/&RPARENS/g, ')'));
                     for (let i = 0; i < realArgs.length; i++) {
                         if (args.length <= i) {
                             args.push(realArgs[i]);
diff --git a/assets/layers/bike_shop/bike_shop.json b/assets/layers/bike_shop/bike_shop.json
index 6cd8890c3..1be9adee9 100644
--- a/assets/layers/bike_shop/bike_shop.json
+++ b/assets/layers/bike_shop/bike_shop.json
@@ -298,7 +298,7 @@
             "id": "bike_shop-email"
         },
         {
-            "render": "{opening_hours_table(opening_hours)}",
+            "render": "{opening_hours_table()}",
             "question": "When is this shop opened?",
             "freeform": {
                 "key": "opening_hours",