From a35b80afbb906d603c730dee4ff3e385a4881f37 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jan 2021 01:11:07 +0100 Subject: [PATCH] Fix rendering of multianswers without explicit 'render'-field --- Customizations/JSON/TagRenderingConfig.ts | 7 +- Logic/MetaTagging.ts | 20 +++- Logic/Tags.ts | 116 +++++++++++++--------- Models/Constants.ts | 2 +- UI/BigComponents/MoreScreen.ts | 13 ++- UI/BigComponents/ShareScreen.ts | 6 +- UI/OpeningHours/OhVisualization.ts | 2 +- UI/Popup/EditableTagRendering.ts | 45 +++++---- UI/Popup/QuestionBox.ts | 38 +++++-- UI/Popup/TagRenderingAnswer.ts | 41 ++++++-- UI/Popup/TagRenderingQuestion.ts | 2 + 11 files changed, 195 insertions(+), 97 deletions(-) diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts index 53ff1dc..d18b282 100644 --- a/Customizations/JSON/TagRenderingConfig.ts +++ b/Customizations/JSON/TagRenderingConfig.ts @@ -21,7 +21,7 @@ export default class TagRenderingConfig { addExtraTags: TagsFilter[]; }; - multiAnswer: boolean; + readonly multiAnswer: boolean; mappings?: { if: TagsFilter, @@ -69,7 +69,7 @@ export default class TagRenderingConfig { if (mapping.then === undefined) { throw "Invalid mapping: if without body" } - let hideInAnswer : boolean | TagsFilter = false; + let hideInAnswer: boolean | TagsFilter = false; if (typeof mapping.hideInAnswer === "boolean") { hideInAnswer = mapping.hideInAnswer; } else if (mapping.hideInAnswer !== undefined) { @@ -111,7 +111,8 @@ export default class TagRenderingConfig { } } - if (this.freeform?.key === undefined){ + + if (this.freeform?.key === undefined) { return this.render; } diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index e4eb43c..bfad13e 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -101,6 +101,14 @@ export default class MetaTagging { // AUtomatically triggered on the next change const updateTags = () => { const oldValueIsOpen = tags["_isOpen"]; + const oldNextChange =tags["_isOpen:nextTrigger"] ?? 0; + + if(oldNextChange > (new Date()).getTime() && + tags["_isOpen:oldvalue"] === tags["opening_hours"]){ + // Already calculated and should not yet be triggered + return; + } + tags["_isOpen"] = oh.getState() ? "yes" : "no"; const comment = oh.getComment(); if (comment) { @@ -113,10 +121,16 @@ export default class MetaTagging { const nextChange = oh.getNextChange(); if (nextChange !== undefined) { + const timeout = nextChange.getTime() - (new Date()).getTime(); + tags["_isOpen:nextTrigger"] = nextChange.getTime(); + tags["_isOpen:oldvalue"] = tags.opening_hours window.setTimeout( - updateTags, - (nextChange.getTime() - (new Date()).getTime()) - ) + () => { + console.log("Updating the _isOpen tag for ", tags.id); + updateTags(); + }, + timeout + ) } } updateTags(); diff --git a/Logic/Tags.ts b/Logic/Tags.ts index 031b1e1..f409cc1 100644 --- a/Logic/Tags.ts +++ b/Logic/Tags.ts @@ -2,8 +2,11 @@ import {Utils} from "../Utils"; export abstract class TagsFilter { abstract matches(tags: { k: string, v: string }[]): boolean + abstract asOverpass(): string[] - abstract substituteValues(tags: any) : TagsFilter; + + abstract substituteValues(tags: any): TagsFilter; + abstract isUsableAsAnswer(): boolean; abstract isEquivalent(other: TagsFilter): boolean; @@ -28,15 +31,8 @@ export class RegexTag extends TagsFilter { this.invert = invert; } - asOverpass(): string[] { - if (typeof this.key === "string") { - return [`['${this.key}'${this.invert ? "!" : ""}~'${RegexTag.source(this.value)}']`]; - } - return [`[~'${this.key.source}'${this.invert ? "!" : ""}~'${RegexTag.source(this.value)}']`]; - } - private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean { - if(typeof possibleRegex === "string"){ + if (typeof possibleRegex === "string") { return fromTag === possibleRegex; } return fromTag.match(possibleRegex) !== null; @@ -48,14 +44,21 @@ export class RegexTag extends TagsFilter { } return r.source; } - + + asOverpass(): string[] { + if (typeof this.key === "string") { + return [`['${this.key}'${this.invert ? "!" : ""}~'${RegexTag.source(this.value)}']`]; + } + return [`[~'${this.key.source}'${this.invert ? "!" : ""}~'${RegexTag.source(this.value)}']`]; + } + isUsableAsAnswer(): boolean { return false; } matches(tags: { k: string; v: string }[]): boolean { for (const tag of tags) { - if (RegexTag.doesMatch(tag.k, this.key)){ + if (RegexTag.doesMatch(tag.k, this.key)) { return RegexTag.doesMatch(tag.v, this.value) != this.invert; } } @@ -78,7 +81,7 @@ export class RegexTag extends TagsFilter { if (other instanceof RegexTag) { return other.asHumanString() == this.asHumanString(); } - if(other instanceof Tag){ + if (other instanceof Tag) { return RegexTag.doesMatch(other.key, this.key) && RegexTag.doesMatch(other.value, this.value); } return false; @@ -94,27 +97,27 @@ export class Tag extends TagsFilter { super() this.key = key this.value = value - if(key === undefined || key === ""){ + if (key === undefined || key === "") { throw "Invalid key: undefined or empty"; } - if(value === undefined){ + if (value === undefined) { throw "Invalid value: value is undefined"; } - if(value === "*"){ - console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}~* ?`) + if (value === "*") { + console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}~* ?`) } } matches(tags: { k: string; v: string }[]): boolean { - + for (const tag of tags) { if (this.key == tag.k) { return this.value === tag.v; } } - + // The tag was not found - if(this.value === ""){ + if (this.value === "") { // and it shouldn't be found! return true; } @@ -146,16 +149,16 @@ export class Tag extends TagsFilter { } return this.key + "=" + v; } - + isUsableAsAnswer(): boolean { return true; } - + isEquivalent(other: TagsFilter): boolean { - if(other instanceof Tag){ + if (other instanceof Tag) { return this.key === other.key && this.value === other.value; } - if(other instanceof RegexTag){ + if (other instanceof RegexTag) { other.isEquivalent(this); } return false; @@ -185,7 +188,7 @@ export class Or extends TagsFilter { const choices = []; for (const tagsFilter of this.or) { const subChoices = tagsFilter.asOverpass(); - for(const subChoice of subChoices){ + for (const subChoice of subChoices) { choices.push(subChoice) } } @@ -203,21 +206,21 @@ export class Or extends TagsFilter { asHumanString(linkToWiki: boolean, shorten: boolean) { return this.or.map(t => t.asHumanString(linkToWiki, shorten)).join("|"); } - + isUsableAsAnswer(): boolean { return false; } - + isEquivalent(other: TagsFilter): boolean { - if(other instanceof Or){ + if (other instanceof Or) { for (const selfTag of this.or) { let matchFound = false; - for (let i = 0; i < other.or.length && !matchFound; i++){ + for (let i = 0; i < other.or.length && !matchFound; i++) { let otherTag = other.or[i]; matchFound = selfTag.isEquivalent(otherTag); } - if(!matchFound){ + if (!matchFound) { return false; } } @@ -236,6 +239,14 @@ export class And extends TagsFilter { this.and = and; } + private static combine(filter: string, choices: string[]): string[] { + const values = []; + for (const or of choices) { + values.push(filter + or); + } + return values; + } + matches(tags: { k: string; v: string }[]): boolean { for (const tagsFilter of this.and) { if (!tagsFilter.matches(tags)) { @@ -246,14 +257,6 @@ export class And extends TagsFilter { return true; } - private static combine(filter: string, choices: string[]): string[] { - const values = []; - for (const or of choices) { - values.push(filter + or); - } - return values; - } - asOverpass(): string[] { let allChoices: string[] = null; for (const andElement of this.and) { @@ -285,16 +288,16 @@ export class And extends TagsFilter { asHumanString(linkToWiki: boolean, shorten: boolean) { return this.and.map(t => t.asHumanString(linkToWiki, shorten)).join("&"); } - + isUsableAsAnswer(): boolean { for (const t of this.and) { - if(!t.isUsableAsAnswer()){ + if (!t.isUsableAsAnswer()) { return false; } } return true; } - + isEquivalent(other: TagsFilter): boolean { if (!(other instanceof And)) { return false; @@ -343,7 +346,6 @@ export class And extends TagsFilter { } - export class TagUtils { static proprtiesToKV(properties: any): { k: string, v: string }[] { const result = []; @@ -374,13 +376,13 @@ export class TagUtils { /** * Given two hashes of {key --> values[]}, makes sure that every neededTag is present in availableTags */ - static AllKeysAreContained(availableTags: any, neededTags: any){ + static AllKeysAreContained(availableTags: any, neededTags: any) { for (const neededKey in neededTags) { - const availableValues : string[] = availableTags[neededKey] - if(availableValues === undefined){ + const availableValues: string[] = availableTags[neededKey] + if (availableValues === undefined) { return false; } - const neededValues : string[] = neededTags[neededKey]; + const neededValues: string[] = neededTags[neededKey]; for (const neededValue of neededValues) { if (availableValues.indexOf(neededValue) < 0) { return false; @@ -392,11 +394,11 @@ export class TagUtils { /*** * Creates a hash {key --> [values]}, with all the values present in the tagsfilter - * + * * @param tagsFilters * @constructor */ - static SplitKeys(tagsFilters: TagsFilter[]){ + static SplitKeys(tagsFilters: TagsFilter[]) { const keyValues = {} // Map string -> string[] tagsFilters = [...tagsFilters] // copy all while (tagsFilters.length > 0) { @@ -425,6 +427,7 @@ export class TagUtils { } return keyValues; } + /** * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set. * E.g: @@ -449,4 +452,21 @@ export class TagUtils { return new And(and); } -} + static MatchesMultiAnswer(tag: TagsFilter, tags: any): boolean { + const splitted = TagUtils.SplitKeys([tag]); + console.log("Matching multianswer", tag, tags) + for (const splitKey in splitted) { + const neededValues = splitted[splitKey]; + const actualValue = tags[splitKey].split(";"); + for (const neededValue of neededValues) { + console.log("needed", neededValue, "have: ", actualValue, actualValue.indexOf(neededValue) ) + if (actualValue.indexOf(neededValue) < 0) { + console.log("NOT FOUND") + return false; + } + } + } + console.log("OK") + return true; + } +} \ No newline at end of file diff --git a/Models/Constants.ts b/Models/Constants.ts index f9651bf..5df0c1f 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -1,7 +1,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.3.0a"; + public static vNumber = "0.3.0c"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index 7fe81eb..e0f1ffe 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -40,15 +40,22 @@ export default class MoreScreen extends UIElement { } const currentLocation = State.state.locationControl.data; + let path = window.location.pathname; + // Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html' + path = path.substr(0, path.lastIndexOf("/")); + // Path will now contain '/dir/dir', or empty string in case of nothing + if(path === ""){ + path = "." + } let linkText = - `./${layout.id.toLowerCase()}.html?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` + `${path}/${layout.id.toLowerCase()}?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { - linkText = `./index.html?layout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` + linkText = `${path}/index.html?layout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` } if (customThemeDefinition) { - linkText = `./index.html?userlayout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}` + linkText = `${path}/?userlayout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}` } diff --git a/UI/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts index 3f61e43..4301894 100644 --- a/UI/BigComponents/ShareScreen.ts +++ b/UI/BigComponents/ShareScreen.ts @@ -147,13 +147,15 @@ export default class ShareScreen extends UIElement { const url = (currentLocation ?? new UIEventSource(undefined)).map(() => { const host = window.location.host; - let literalText = `https://${host}/${layout.id.toLowerCase()}.html` + let path = window.location.pathname; + path = path.substr(0, path.lastIndexOf("/")); + let literalText = `https://${host}${path}/${layout.id.toLowerCase()}` const parts = Utils.NoEmpty(Utils.NoNull(optionParts.map((eventSource) => eventSource.data))); let hash = ""; if (layoutDefinition !== undefined) { - literalText = `https://${host}/index.html` + literalText = `https://${host}${path}/` if (layout.id.startsWith("wiki:")) { parts.push("userlayout=" + encodeURIComponent(layout.id)) } else { diff --git a/UI/OpeningHours/OhVisualization.ts b/UI/OpeningHours/OhVisualization.ts index 6e36879..8f49b9c 100644 --- a/UI/OpeningHours/OhVisualization.ts +++ b/UI/OpeningHours/OhVisualization.ts @@ -196,7 +196,7 @@ export default class OpeningHoursVisualization extends UIElement { // Closed! const opensAtDate = oh.getNextChange(); if(opensAtDate === undefined){ - const comm = oh.getComment(); + const comm = oh.getComment() ?? oh.getUnknown(); if(comm !== undefined){ return new FixedUiElement(comm).SetClass("ohviz-closed").Render(); } diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index 68d1452..11af718 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -7,6 +7,7 @@ import Combine from "../Base/Combine"; import TagRenderingAnswer from "./TagRenderingAnswer"; import State from "../../State"; import Svg from "../../Svg"; +import {TagUtils} from "../../Logic/Tags"; export default class EditableTagRendering extends UIElement { private readonly _tags: UIEventSource; @@ -45,6 +46,29 @@ export default class EditableTagRendering extends UIElement { } } + InnerRender(): string { + if (!this._configuration?.condition?.matchesProperties(this._tags.data)) { + return ""; + } + if (this._editMode.data) { + return this._question.Render(); + } + if (this._configuration.multiAnswer) { + const atLeastOneMatch = this._configuration.mappings.some(mp =>TagUtils.MatchesMultiAnswer(mp.if, this._tags.data)); + console.log("SOME MATCH?", atLeastOneMatch) + if (!atLeastOneMatch) { + return ""; + } + } else if (this._configuration.GetRenderValue(this._tags.data) === undefined) { + return ""; + } + + return new Combine([this._answer, + (State.state?.osmConnection?.userDetails?.data?.loggedIn ?? true) ? this._editButton : undefined + ]).SetClass("answer") + .Render(); + } + private GenerateQuestion() { const self = this; if (this._configuration.question !== undefined) { @@ -64,25 +88,4 @@ export default class EditableTagRendering extends UIElement { } } - - InnerRender(): string { - - if (this._editMode.data) { - return this._question.Render(); - } - - if(this._configuration.GetRenderValue(this._tags.data)=== undefined){ - return ""; - } - - if(!this._configuration?.condition?.matchesProperties(this._tags.data)){ - return ""; - } - - return new Combine([this._answer, - (State.state?.osmConnection?.userDetails?.data?.loggedIn ?? true) ? this._editButton : undefined - ]).SetClass("answer") - .Render(); - } - } \ No newline at end of file diff --git a/UI/Popup/QuestionBox.ts b/UI/Popup/QuestionBox.ts index 991d11a..0b7e85b 100644 --- a/UI/Popup/QuestionBox.ts +++ b/UI/Popup/QuestionBox.ts @@ -3,6 +3,7 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import TagRenderingQuestion from "./TagRenderingQuestion"; import Translations from "../i18n/Translations"; +import {TagUtils} from "../../Logic/Tags"; /** @@ -46,20 +47,39 @@ export default class QuestionBox extends UIElement { }) } + /** + * Returns true if it is known or not shown, false if the question should be asked + * @constructor + */ + IsKnown(tagRendering: TagRenderingConfig): boolean { + if (tagRendering.condition && + !tagRendering.condition.matchesProperties(this._tags.data)) { + // Filtered away by the condition + return true; + } + if(tagRendering.multiAnswer){ + for (const m of tagRendering.mappings) { + if(TagUtils.MatchesMultiAnswer(m.if, this._tags.data)){ + return true; + } + } + } + + if (tagRendering.GetRenderValue(this._tags.data) !== undefined) { + // This value is known and can be rendered + return true; + } + + return false; + } + InnerRender(): string { for (let i = 0; i < this._tagRenderingQuestions.length; i++) { let tagRendering = this._tagRenderings[i]; - if(tagRendering.condition && - !tagRendering.condition.matchesProperties(this._tags.data)){ - // Filtered away by the condition + + if(this.IsKnown(tagRendering)){ continue; } - - if (tagRendering.GetRenderValue(this._tags.data) !== undefined) { - // This value is known - continue; - } - if (this._skippedQuestions.data.indexOf(i) >= 0) { continue; diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 57ddf68..f02bcd8 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -2,6 +2,9 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import {UIElement} from "../UIElement"; import {SubstitutedTranslation} from "../SpecialVisualizations"; +import {Utils} from "../../Utils"; +import Combine from "../Base/Combine"; +import {TagUtils} from "../../Logic/Tags"; /*** * Displays the correct value for a known tagrendering @@ -15,7 +18,7 @@ export default class TagRenderingAnswer extends UIElement { super(tags); this._tags = tags; this._configuration = configuration; - if(configuration === undefined){ + if (configuration === undefined) { throw "Trying to generate a tagRenderingAnswer without configuration..." } } @@ -32,12 +35,38 @@ export default class TagRenderingAnswer extends UIElement { return ""; } const tr = this._configuration.GetRenderValue(tags); - if (tr === undefined) { - return ""; + if (tr !== undefined) { + this._content = new SubstitutedTranslation(tr, this._tags); + return this._content.Render(); } - // Bit of a hack; remember that the fields are updated - this._content = new SubstitutedTranslation(tr, this._tags); - return this._content.Render(); + + // The render value doesn't work well with multi-answers (checkboxes), so we have to check for them manually + if (this._configuration.multiAnswer) { + const applicableThens = Utils.NoNull(this._configuration.mappings.map(mapping => { + if (mapping.if === undefined) { + return mapping.then; + } + if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) { + return mapping.then; + } + return undefined; + })) + if (applicableThens.length >= 0) { + if (applicableThens.length === 1) { + this._content = applicableThens[0]; + } else { + this._content = new Combine(["" + ]) + + } + return this._content.Render(); + } + } + return ""; + } } \ No newline at end of file diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index bfeb82e..7fd00a7 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -53,7 +53,9 @@ export default class TagRenderingQuestion extends UIElement { this._inputElement = this.GenerateInputElement() const self = this; const save = () => { + console.log("Save clicked!") const selection = self._inputElement.GetValue().data; + console.log("Selection is", selection) if (selection) { (State.state?.changes ?? new Changes()) .addTag(tags.data.id, selection, tags);