From ae9d93138be13216ed0efedb1e4a0ef0135a98ec Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 20 Feb 2021 16:48:42 +0100 Subject: [PATCH] Add ifnot-tags for multianswers, in order to indicate something is _not_ possible (e.g.: playment:coins=no) --- Customizations/JSON/TagRenderingConfig.ts | 57 +++++++++++---- Customizations/JSON/TagRenderingConfigJson.ts | 5 ++ Logic/MetaTagging.ts | 2 +- Logic/Tags.ts | 26 ++++++- UI/Popup/TagRenderingQuestion.ts | 16 ++++- Utils.ts | 47 +------------ .../bicycle_tube_vending_machine.json | 8 +++ test.ts | 69 ------------------- 8 files changed, 99 insertions(+), 131 deletions(-) diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts index b06d063b6..e0093391c 100644 --- a/Customizations/JSON/TagRenderingConfig.ts +++ b/Customizations/JSON/TagRenderingConfig.ts @@ -4,6 +4,7 @@ import Translations from "../../UI/i18n/Translations"; import {FromJSON} from "./FromJSON"; import ValidatedTextField from "../../UI/Input/ValidatedTextField"; import {Translation} from "../../UI/i18n/Translation"; +import {Utils} from "../../Utils"; /*** * The parsed version of TagRenderingConfigJSON @@ -24,9 +25,10 @@ export default class TagRenderingConfig { readonly multiAnswer: boolean; readonly mappings?: { - readonly if: TagsFilter, - readonly then: Translation - readonly hideInAnswer: boolean | TagsFilter + readonly if: TagsFilter, + readonly ifnot?: TagsFilter, + readonly then: Translation + readonly hideInAnswer: boolean | TagsFilter }[] readonly roaming: boolean; @@ -74,7 +76,10 @@ export default class TagRenderingConfig { this.mappings = json.mappings.map((mapping, i) => { if (mapping.then === undefined) { - throw "Invalid mapping: if without body" + throw `${context}.mapping[${i}]: Invalid mapping: if without body` + } + if (mapping.ifnot !== undefined && !this.multiAnswer) { + throw `${context}.mapping[${i}]: Invalid mapping: ifnot defined, but the tagrendering is not a multianswer` } let hideInAnswer: boolean | TagsFilter = false; if (typeof mapping.hideInAnswer === "boolean") { @@ -82,25 +87,53 @@ export default class TagRenderingConfig { } else if (mapping.hideInAnswer !== undefined) { hideInAnswer = FromJSON.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`); } - return { + const mp = { if: FromJSON.Tag(mapping.if, `${context}.mapping[${i}].if`), + ifnot: (mapping.ifnot !== undefined ? FromJSON.Tag(mapping.ifnot, `${context}.mapping[${i}].ifnot`) : undefined), then: Translations.T(mapping.then), hideInAnswer: hideInAnswer }; + if (this.question) { + if (hideInAnswer !== true && !mp.if.isUsableAsAnswer()) { + throw `${context}.mapping[${i}].if: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'` + } + + if (hideInAnswer !== true && !(mp.ifnot?.isUsableAsAnswer() ?? true)) { + throw `${context}.mapping[${i}].ifnot: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'` + } + } + + return mp; }); } if (this.question && this.freeform?.key === undefined && this.mappings === undefined) { - throw `A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}` - } - - if(this.freeform && this.render === undefined){ - throw `Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}` + throw `${context}: A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}` } - if (json.multiAnswer) { + if (this.freeform && this.render === undefined) { + throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}` + } + + if (this.question !== undefined && json.multiAnswer) { if ((this.mappings?.length ?? 0) === 0) { - throw "MultiAnswer is set, but no mappings are defined" + throw `${context} MultiAnswer is set, but no mappings are defined` + } + + let allKeys = []; + let allHaveIfNot = true; + for (const mapping of this.mappings) { + if (mapping.hideInAnswer) { + continue; + } + if (mapping.ifnot === undefined) { + allHaveIfNot = false; + } + allKeys = allKeys.concat(mapping.if.usedKeys()); + } + allKeys = Utils.Dedup(allKeys); + if (allKeys.length > 1 && !allHaveIfNot) { + throw `${context}: A multi-answer is defined, which generates values over multiple keys. Please define ifnot-tags too on every mapping` } } diff --git a/Customizations/JSON/TagRenderingConfigJson.ts b/Customizations/JSON/TagRenderingConfigJson.ts index 441530dfd..a440b96fd 100644 --- a/Customizations/JSON/TagRenderingConfigJson.ts +++ b/Customizations/JSON/TagRenderingConfigJson.ts @@ -50,6 +50,11 @@ export interface TagRenderingConfigJson { */ mappings?: { if: AndOrTagConfigJson | string, + /** + * Only applicable if 'multiAnswer' is set. + * This tag is applied if the respective checkbox is unset + */ + ifnot?: AndOrTagConfigJson | string, then: string | any hideInAnswer?: boolean }[] diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index 2c163cab1..c86d7e55a 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -294,7 +294,7 @@ export default class MetaTagging { MetaTagging.isOpen, MetaTagging.carriageWayWidth, MetaTagging.directionSimplified, - MetaTagging.currentTime + // MetaTagging.currentTime ]; diff --git a/Logic/Tags.ts b/Logic/Tags.ts index 1b849511c..45e03b63c 100644 --- a/Logic/Tags.ts +++ b/Logic/Tags.ts @@ -1,4 +1,5 @@ import {Utils} from "../Utils"; +import {type} from "os"; export abstract class TagsFilter { abstract matches(tags: { k: string, v: string }[]): boolean @@ -16,6 +17,8 @@ export abstract class TagsFilter { } abstract asHumanString(linkToWiki: boolean, shorten: boolean); + + abstract usedKeys(): string[]; } @@ -86,6 +89,13 @@ export class RegexTag extends TagsFilter { } return false; } + + usedKeys(): string[] { + if (typeof this.key === "string") { + return [this.key]; + } + throw "Key cannot be determined as it is a regex" + } } @@ -163,6 +173,10 @@ export class Tag extends TagsFilter { } return false; } + + usedKeys(): string[] { + return [this.key]; + } } @@ -228,6 +242,10 @@ export class Or extends TagsFilter { } return false; } + + usedKeys(): string[] { + return [].concat(this.or.map(subkeys => subkeys.usedKeys())); + } } @@ -343,6 +361,10 @@ export class And extends TagsFilter { return true; } + + usedKeys(): string[] { + return [].concat(this.and.map(subkeys => subkeys.usedKeys())); + } } @@ -456,10 +478,10 @@ export class TagUtils { const splitted = TagUtils.SplitKeys([tag]); for (const splitKey in splitted) { const neededValues = splitted[splitKey]; - if(tags[splitKey] === undefined) { + if (tags[splitKey] === undefined) { return false; } - + const actualValue = tags[splitKey].split(";"); for (const neededValue of neededValues) { if (actualValue.indexOf(neededValue) < 0) { diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 6a516f1e0..e5fa9c8c9 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -107,14 +107,14 @@ export default class TagRenderingQuestion extends UIElement { } if (this._configuration.multiAnswer) { - return this.GenerateMultiAnswer(mappings, ff) + return this.GenerateMultiAnswer(mappings, ff, this._configuration.mappings.map(mp => mp.ifnot)) } else { return new RadioButton(mappings, false) } } - private GenerateMultiAnswer(elements: InputElement[], freeformField: InputElement): InputElement { + private GenerateMultiAnswer(elements: InputElement[], freeformField: InputElement, ifNotSelected: TagsFilter[]): InputElement { const checkBoxes = new CheckBoxes(elements); const inputEl = new InputElementMap( checkBoxes, @@ -126,6 +126,18 @@ export default class TagRenderingQuestion extends UIElement { return undefined; } const tags: TagsFilter[] = indices.map(i => elements[i].GetValue().data); + const oppositeTags: TagsFilter[] = []; + for (let i = 0; i < ifNotSelected.length; i++) { + if(indices.indexOf(i) >= 0){ + continue; + } + const notSelected = ifNotSelected[i]; + if(notSelected === undefined){ + continue; + } + oppositeTags.push(notSelected); + } + tags.push(TagUtils.FlattenMultiAnswer(oppositeTags)); return TagUtils.FlattenMultiAnswer(tags); }, (tags: TagsFilter) => { diff --git a/Utils.ts b/Utils.ts index 8cb53b1b0..3af878a60 100644 --- a/Utils.ts +++ b/Utils.ts @@ -141,21 +141,9 @@ export class Utils { } // Date will be undefined on failure - public static changesetDate(id: number, action: ((isFound: Date) => void)): void { - $.getJSON("https://www.openstreetmap.org/api/0.6/changeset/" + id, - function (data) { - console.log(data) - action(new Date(data.elements[0].created_at)); - }) - .fail(() => { - action(undefined); - }); - - } - public static LoadCustomCss(location: string){ - var head = document.getElementsByTagName('head')[0]; - var link = document.createElement('link'); + const head = document.getElementsByTagName('head')[0]; + const link = document.createElement('link'); link.id = "customCss"; link.rel = 'stylesheet'; link.type = 'text/css'; @@ -164,17 +152,6 @@ export class Utils { head.appendChild(link); console.log("Added custom layout ",location) } - - - static MatchKeys(object: any, prototype: any, context?: string){ - - for (const objectKey in object) { - if(prototype[objectKey] === undefined){ - console.error("Key ", objectKey, "might be not supported (in context",context,")") - } - } - } - static Merge(source: any, target: any){ target = JSON.parse(JSON.stringify(target)); source = JSON.parse(JSON.stringify(source)); @@ -195,24 +172,4 @@ export class Utils { } return target; } - - static ToMuchTags(source: any, toCheck: any, context: string){ - - for (const key in toCheck) { - const toCheckV = toCheck[key]; - const sourceV = source[key]; - if(sourceV === undefined){ - console.error("Probably a wrong tag in ", context, ": ", key, "might be wrong") - } - if(typeof toCheckV === "object"){ - if(typeof sourceV !== "object"){ - console.error("Probably a wrong value in ", context, ": ", key, "is a fixed value in the source") - }else{ - Utils.ToMuchTags(sourceV, toCheckV, context+"."+key); - } - } - } - - } - } diff --git a/assets/layers/bicycle_tube_vending_machine/bicycle_tube_vending_machine.json b/assets/layers/bicycle_tube_vending_machine/bicycle_tube_vending_machine.json index 53018e38f..f798716c5 100644 --- a/assets/layers/bicycle_tube_vending_machine/bicycle_tube_vending_machine.json +++ b/assets/layers/bicycle_tube_vending_machine/bicycle_tube_vending_machine.json @@ -104,14 +104,17 @@ "mappings": [ { "if": "payment:coins=yes", + "ifnot": "payment:coins=no", "then": "Payment with coins is possible" }, { "if": "payment:notes=yes", + "ifnot": "payment:notes=no", "then": "Payment with notes is possible" }, { "if": "payment:cards=yes", + "ifnot": "payment:cards=no", "then": "Payment with cards is possible" } ], @@ -157,22 +160,27 @@ "mappings": [ { "if": "vending:bicycle_light=yes", + "ifnot": "vending:bicycle_light=no", "then": "Bicycle lights are sold here" }, { "if": "vending:gloves=yes", + "ifnot": "vending:gloves=no", "then": "Gloves are sold here" }, { "if": "vending:bicycle_repair_kit=yes", + "ifnot": "vending:bicycle_repair_kit=no", "then": "Bicycle repair kits are sold here" }, { "if": "vending:bicycle_pump=yes", + "ifnot": "vending:bicycle_pump=no", "then": "Bicycle pumps are sold here" }, { "if": "vending:bicycle_lock=yes", + "ifnot": "vending:bicycle_lock=no", "then": "Bicycle locks are sold here" } ], diff --git a/test.ts b/test.ts index 878126b42..2c32da832 100644 --- a/test.ts +++ b/test.ts @@ -13,72 +13,3 @@ const images = new UIEventSource<{ url: string, key: string }[]>( new ImageCarousel(images, new UIEventSource({"image:1":"https://2.bp.blogspot.com/-fQiZkz9Zlzg/T_xe2X2Ia3I/AAAAAAAAA0Q/VPS8Mb8xtIQ/s1600/cat+15.jpg"})) .AttachTo("maindiv") -/*/ -import {Utils} from "./Utils"; -import {FixedUiElement} from "./UI/Base/FixedUiElement"; - - -function generateStats(action: (stats: string) => void) { - // Binary searches the latest changeset - function search(lowerBound: number, - upperBound: number, - onCsFound: ((id: number, lastDate: Date) => void), - depth = 0) { - if (depth > 30) { - return; - } - const tested = Math.floor((lowerBound + upperBound) / 2); - console.log("Testing", tested) - Utils.changesetDate(tested, (createdAtDate: Date) => { - new FixedUiElement(`Searching, value between ${lowerBound} and ${upperBound}. Queries till now: ${depth}`).AttachTo('maindiv') - if (lowerBound + 1 >= upperBound) { - onCsFound(lowerBound, createdAtDate); - return; - } - if (createdAtDate !== undefined) { - search(tested, upperBound, onCsFound, depth + 1) - } else { - search(lowerBound, tested, onCsFound, depth + 1); - } - }) - - } - - - search(91000000, 100000000, (last, lastDate: Date) => { - const link = "http://osm.org/changeset/" + last; - - const delta = 100000; - - Utils.changesetDate(last - delta, (prevDate) => { - - - const diff = (lastDate.getTime() - prevDate.getTime()) / 1000; - - // Diff: seconds needed/delta changesets - const secsPerCS = diff / delta; - - const stillNeeded = 1000000 - (last % 1000000); - const timeNeededSeconds = Math.floor(secsPerCS * stillNeeded); - - const secNeeded = timeNeededSeconds % 60; - const minNeeded = Math.floor(timeNeededSeconds / 60) % 60; - const hourNeeded = Math.floor(timeNeededSeconds / (60 * 60)) % 24; - const daysNeeded = Math.floor(timeNeededSeconds / (24 * 60 * 60)); - - const result = `Last changeset: ${link}
We needed ${(Math.floor(diff / 60))} minutes for the last ${delta} changesets.
-This is around ${secsPerCS} seconds/changeset.
The next million (still ${stillNeeded} away) will be broken in around ${daysNeeded} days ${hourNeeded}:${minNeeded}:${secNeeded}` - action(result); - }) - - } - ); - -} - -generateStats((stats) => { - new FixedUiElement(stats).AttachTo('maindiv') -}) - - -//*/ \ No newline at end of file