Add the possibility to define a postfix and prefix for opening hours

This commit is contained in:
Pieter Vander Vennet 2021-10-29 03:42:33 +02:00
parent 9c147b6ec6
commit a2306c2c6f
10 changed files with 281 additions and 81 deletions

View file

@ -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

View file

@ -24,7 +24,40 @@ A geographical length in meters (rounded at two points). Will give an extra mini
## wikidata ## 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 ## int
@ -60,7 +93,38 @@ A phone number
## opening_hours ## 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 ## color

View file

@ -14,11 +14,11 @@
name | default | description 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 #### Example usage
`{image_carousel(image)}` `{image_carousel(image,mapillary,image,wikidata,wikimedia_commons,image,image)}`
### image_upload ### image_upload
Creates a button where a user can upload an image to IMGUR 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 name | default | description
------ | --------- | ------------- ------ | --------- | -------------
key | opening_hours | The tagkey from which the table is constructed. 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 #### 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 ### 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)} 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)}

View file

@ -25,8 +25,8 @@ export default class InputElementMap<T, X> extends InputElement<X> {
const self = this; const self = this;
this._value = inputElement.GetValue().map( this._value = inputElement.GetValue().map(
(t => { (t => {
const currentX = self.GetValue()?.data;
const newX = toX(t); const newX = toX(t);
const currentX = self.GetValue()?.data;
if (isSame(currentX, newX)) { if (isSame(currentX, newX)) {
return currentX; return currentX;
} }

View file

@ -19,6 +19,9 @@ import {FixedInputElement} from "./FixedInputElement";
import WikidataSearchBox from "../Wikipedia/WikidataSearchBox"; import WikidataSearchBox from "../Wikipedia/WikidataSearchBox";
import Wikidata from "../../Logic/Web/Wikidata"; import Wikidata from "../../Logic/Web/Wikidata";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import Table from "../Base/Table";
import Combine from "../Base/Combine";
import Title from "../Base/Title";
interface TextFieldDef { interface TextFieldDef {
name: string, name: string,
@ -28,12 +31,159 @@ interface TextFieldDef {
inputHelper?: (value: UIEventSource<string>, options?: { inputHelper?: (value: UIEventSource<string>, options?: {
location: [number, number], location: [number, number],
mapBackgroundLayer?: UIEventSource<any>, mapBackgroundLayer?: UIEventSource<any>,
args: (string | number | boolean)[] args: (string | number | boolean | any)[]
feature?: any feature?: any
}) => InputElement<string>, }) => InputElement<string>,
inputmode?: 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 { export default class ValidatedTextField {
public static tpList: TextFieldDef[] = [ public static tpList: TextFieldDef[] = [
@ -146,60 +296,7 @@ export default class ValidatedTextField {
}, },
"decimal" "decimal"
), ),
ValidatedTextField.tp( new WikidataTextField(),
"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)
})
}
),
ValidatedTextField.tp( ValidatedTextField.tp(
"int", "int",
@ -299,15 +396,7 @@ export default class ValidatedTextField {
undefined, undefined,
"tel" "tel"
), ),
ValidatedTextField.tp( new OpeningHoursTextField(),
"opening_hours",
"Has extra elements to easily input when a POI is opened",
() => true,
str => str,
(value) => {
return new OpeningHoursInput(value);
}
),
ValidatedTextField.tp( ValidatedTextField.tp(
"color", "color",
"Shows a color picker", "Shows a color picker",

View file

@ -23,11 +23,39 @@ export default class OpeningHoursInput extends InputElement<string> {
private readonly _value: UIEventSource<string>; private readonly _value: UIEventSource<string>;
private readonly _element: BaseUIElement; private readonly _element: BaseUIElement;
constructor(value: UIEventSource<string> = new UIEventSource<string>("")) { constructor(value: UIEventSource<string> = new UIEventSource<string>(""), prefix = "", postfix = "") {
super(); super();
this._value = value; 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) { if (str === undefined) {
return [] return []
} }
@ -45,9 +73,9 @@ export default class OpeningHoursInput extends InputElement<string> {
return leftOvers; return leftOvers;
}) })
// Note: MUST be bound AFTER the leftover rules! // 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) { if (str === undefined) {
return "" return ""
} }
@ -68,7 +96,7 @@ export default class OpeningHoursInput extends InputElement<string> {
...leftoverRules.data, ...leftoverRules.data,
ph.data ph.data
] ]
value.setData(Utils.NoEmpty(rules).join(";")); valueWithoutPrefix.setData(Utils.NoEmpty(rules).join(";"));
} }
rulesFromOhPicker.addCallback(update); rulesFromOhPicker.addCallback(update);

View file

@ -23,10 +23,16 @@ export default class OpeningHoursVisualization extends Toggle {
Translations.t.general.weekdays.abbreviations.sunday, 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 tagsDirect = tags.data;
const ohTable = new VariableUiElement(tags 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 => { .map(ohtext => {
try { try {
// noinspection JSPotentiallyInvalidConstructorUsage // noinspection JSPotentiallyInvalidConstructorUsage

View file

@ -253,9 +253,18 @@ export default class SpecialVisualizations {
name: "key", name: "key",
defaultValue: "opening_hours", defaultValue: "opening_hours",
doc: "The tagkey from which the table is constructed." 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) => { constr: (state: State, tagSource: UIEventSource<any>, args) => {
return new OpeningHoursVisualization(tagSource, args[0]) return new OpeningHoursVisualization(tagSource, args[0], args[1], args[2])
} }
}, },
{ {

View file

@ -85,7 +85,9 @@ export class SubstitutedTranslation extends VariableUiElement {
const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4], extraMappings); const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4], extraMappings);
const args = knownSpecial.args.map(arg => arg.defaultValue ?? ""); const args = knownSpecial.args.map(arg => arg.defaultValue ?? "");
if (argument.length > 0) { 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++) { for (let i = 0; i < realArgs.length; i++) {
if (args.length <= i) { if (args.length <= i) {
args.push(realArgs[i]); args.push(realArgs[i]);

View file

@ -298,7 +298,7 @@
"id": "bike_shop-email" "id": "bike_shop-email"
}, },
{ {
"render": "{opening_hours_table(opening_hours)}", "render": "{opening_hours_table()}",
"question": "When is this shop opened?", "question": "When is this shop opened?",
"freeform": { "freeform": {
"key": "opening_hours", "key": "opening_hours",