Fix rendering of multianswers without explicit 'render'-field

This commit is contained in:
pietervdvn 2021-01-06 01:11:07 +01:00
parent 52d9b2f452
commit a35b80afbb
11 changed files with 195 additions and 97 deletions

View file

@ -21,7 +21,7 @@ export default class TagRenderingConfig {
addExtraTags: TagsFilter[]; addExtraTags: TagsFilter[];
}; };
multiAnswer: boolean; readonly multiAnswer: boolean;
mappings?: { mappings?: {
if: TagsFilter, if: TagsFilter,
@ -69,7 +69,7 @@ export default class TagRenderingConfig {
if (mapping.then === undefined) { if (mapping.then === undefined) {
throw "Invalid mapping: if without body" throw "Invalid mapping: if without body"
} }
let hideInAnswer : boolean | TagsFilter = false; let hideInAnswer: boolean | TagsFilter = false;
if (typeof mapping.hideInAnswer === "boolean") { if (typeof mapping.hideInAnswer === "boolean") {
hideInAnswer = mapping.hideInAnswer; hideInAnswer = mapping.hideInAnswer;
} else if (mapping.hideInAnswer !== undefined) { } 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; return this.render;
} }

View file

@ -101,6 +101,14 @@ export default class MetaTagging {
// AUtomatically triggered on the next change // AUtomatically triggered on the next change
const updateTags = () => { const updateTags = () => {
const oldValueIsOpen = tags["_isOpen"]; 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"; tags["_isOpen"] = oh.getState() ? "yes" : "no";
const comment = oh.getComment(); const comment = oh.getComment();
if (comment) { if (comment) {
@ -113,10 +121,16 @@ export default class MetaTagging {
const nextChange = oh.getNextChange(); const nextChange = oh.getNextChange();
if (nextChange !== undefined) { if (nextChange !== undefined) {
const timeout = nextChange.getTime() - (new Date()).getTime();
tags["_isOpen:nextTrigger"] = nextChange.getTime();
tags["_isOpen:oldvalue"] = tags.opening_hours
window.setTimeout( window.setTimeout(
updateTags, () => {
(nextChange.getTime() - (new Date()).getTime()) console.log("Updating the _isOpen tag for ", tags.id);
) updateTags();
},
timeout
)
} }
} }
updateTags(); updateTags();

View file

@ -2,8 +2,11 @@ import {Utils} from "../Utils";
export abstract class TagsFilter { export abstract class TagsFilter {
abstract matches(tags: { k: string, v: string }[]): boolean abstract matches(tags: { k: string, v: string }[]): boolean
abstract asOverpass(): string[] abstract asOverpass(): string[]
abstract substituteValues(tags: any) : TagsFilter;
abstract substituteValues(tags: any): TagsFilter;
abstract isUsableAsAnswer(): boolean; abstract isUsableAsAnswer(): boolean;
abstract isEquivalent(other: TagsFilter): boolean; abstract isEquivalent(other: TagsFilter): boolean;
@ -28,15 +31,8 @@ export class RegexTag extends TagsFilter {
this.invert = invert; 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 { private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean {
if(typeof possibleRegex === "string"){ if (typeof possibleRegex === "string") {
return fromTag === possibleRegex; return fromTag === possibleRegex;
} }
return fromTag.match(possibleRegex) !== null; return fromTag.match(possibleRegex) !== null;
@ -48,14 +44,21 @@ export class RegexTag extends TagsFilter {
} }
return r.source; 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 { isUsableAsAnswer(): boolean {
return false; return false;
} }
matches(tags: { k: string; v: string }[]): boolean { matches(tags: { k: string; v: string }[]): boolean {
for (const tag of tags) { 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; return RegexTag.doesMatch(tag.v, this.value) != this.invert;
} }
} }
@ -78,7 +81,7 @@ export class RegexTag extends TagsFilter {
if (other instanceof RegexTag) { if (other instanceof RegexTag) {
return other.asHumanString() == this.asHumanString(); 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 RegexTag.doesMatch(other.key, this.key) && RegexTag.doesMatch(other.value, this.value);
} }
return false; return false;
@ -94,27 +97,27 @@ export class Tag extends TagsFilter {
super() super()
this.key = key this.key = key
this.value = value this.value = value
if(key === undefined || key === ""){ if (key === undefined || key === "") {
throw "Invalid key: undefined or empty"; throw "Invalid key: undefined or empty";
} }
if(value === undefined){ if (value === undefined) {
throw "Invalid value: value is undefined"; throw "Invalid value: value is undefined";
} }
if(value === "*"){ if (value === "*") {
console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}~* ?`) console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}~* ?`)
} }
} }
matches(tags: { k: string; v: string }[]): boolean { matches(tags: { k: string; v: string }[]): boolean {
for (const tag of tags) { for (const tag of tags) {
if (this.key == tag.k) { if (this.key == tag.k) {
return this.value === tag.v; return this.value === tag.v;
} }
} }
// The tag was not found // The tag was not found
if(this.value === ""){ if (this.value === "") {
// and it shouldn't be found! // and it shouldn't be found!
return true; return true;
} }
@ -146,16 +149,16 @@ export class Tag extends TagsFilter {
} }
return this.key + "=" + v; return this.key + "=" + v;
} }
isUsableAsAnswer(): boolean { isUsableAsAnswer(): boolean {
return true; return true;
} }
isEquivalent(other: TagsFilter): boolean { isEquivalent(other: TagsFilter): boolean {
if(other instanceof Tag){ if (other instanceof Tag) {
return this.key === other.key && this.value === other.value; return this.key === other.key && this.value === other.value;
} }
if(other instanceof RegexTag){ if (other instanceof RegexTag) {
other.isEquivalent(this); other.isEquivalent(this);
} }
return false; return false;
@ -185,7 +188,7 @@ export class Or extends TagsFilter {
const choices = []; const choices = [];
for (const tagsFilter of this.or) { for (const tagsFilter of this.or) {
const subChoices = tagsFilter.asOverpass(); const subChoices = tagsFilter.asOverpass();
for(const subChoice of subChoices){ for (const subChoice of subChoices) {
choices.push(subChoice) choices.push(subChoice)
} }
} }
@ -203,21 +206,21 @@ export class Or extends TagsFilter {
asHumanString(linkToWiki: boolean, shorten: boolean) { asHumanString(linkToWiki: boolean, shorten: boolean) {
return this.or.map(t => t.asHumanString(linkToWiki, shorten)).join("|"); return this.or.map(t => t.asHumanString(linkToWiki, shorten)).join("|");
} }
isUsableAsAnswer(): boolean { isUsableAsAnswer(): boolean {
return false; return false;
} }
isEquivalent(other: TagsFilter): boolean { isEquivalent(other: TagsFilter): boolean {
if(other instanceof Or){ if (other instanceof Or) {
for (const selfTag of this.or) { for (const selfTag of this.or) {
let matchFound = false; 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]; let otherTag = other.or[i];
matchFound = selfTag.isEquivalent(otherTag); matchFound = selfTag.isEquivalent(otherTag);
} }
if(!matchFound){ if (!matchFound) {
return false; return false;
} }
} }
@ -236,6 +239,14 @@ export class And extends TagsFilter {
this.and = and; 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 { matches(tags: { k: string; v: string }[]): boolean {
for (const tagsFilter of this.and) { for (const tagsFilter of this.and) {
if (!tagsFilter.matches(tags)) { if (!tagsFilter.matches(tags)) {
@ -246,14 +257,6 @@ export class And extends TagsFilter {
return true; 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[] { asOverpass(): string[] {
let allChoices: string[] = null; let allChoices: string[] = null;
for (const andElement of this.and) { for (const andElement of this.and) {
@ -285,16 +288,16 @@ export class And extends TagsFilter {
asHumanString(linkToWiki: boolean, shorten: boolean) { asHumanString(linkToWiki: boolean, shorten: boolean) {
return this.and.map(t => t.asHumanString(linkToWiki, shorten)).join("&"); return this.and.map(t => t.asHumanString(linkToWiki, shorten)).join("&");
} }
isUsableAsAnswer(): boolean { isUsableAsAnswer(): boolean {
for (const t of this.and) { for (const t of this.and) {
if(!t.isUsableAsAnswer()){ if (!t.isUsableAsAnswer()) {
return false; return false;
} }
} }
return true; return true;
} }
isEquivalent(other: TagsFilter): boolean { isEquivalent(other: TagsFilter): boolean {
if (!(other instanceof And)) { if (!(other instanceof And)) {
return false; return false;
@ -343,7 +346,6 @@ export class And extends TagsFilter {
} }
export class TagUtils { export class TagUtils {
static proprtiesToKV(properties: any): { k: string, v: string }[] { static proprtiesToKV(properties: any): { k: string, v: string }[] {
const result = []; const result = [];
@ -374,13 +376,13 @@ export class TagUtils {
/** /**
* Given two hashes of {key --> values[]}, makes sure that every neededTag is present in availableTags * 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) { for (const neededKey in neededTags) {
const availableValues : string[] = availableTags[neededKey] const availableValues: string[] = availableTags[neededKey]
if(availableValues === undefined){ if (availableValues === undefined) {
return false; return false;
} }
const neededValues : string[] = neededTags[neededKey]; const neededValues: string[] = neededTags[neededKey];
for (const neededValue of neededValues) { for (const neededValue of neededValues) {
if (availableValues.indexOf(neededValue) < 0) { if (availableValues.indexOf(neededValue) < 0) {
return false; return false;
@ -392,11 +394,11 @@ export class TagUtils {
/*** /***
* Creates a hash {key --> [values]}, with all the values present in the tagsfilter * Creates a hash {key --> [values]}, with all the values present in the tagsfilter
* *
* @param tagsFilters * @param tagsFilters
* @constructor * @constructor
*/ */
static SplitKeys(tagsFilters: TagsFilter[]){ static SplitKeys(tagsFilters: TagsFilter[]) {
const keyValues = {} // Map string -> string[] const keyValues = {} // Map string -> string[]
tagsFilters = [...tagsFilters] // copy all tagsFilters = [...tagsFilters] // copy all
while (tagsFilters.length > 0) { while (tagsFilters.length > 0) {
@ -425,6 +427,7 @@ export class TagUtils {
} }
return keyValues; return keyValues;
} }
/** /**
* Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set. * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set.
* E.g: * E.g:
@ -449,4 +452,21 @@ export class TagUtils {
return new And(and); 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;
}
}

View file

@ -1,7 +1,7 @@
import { Utils } from "../Utils"; import { Utils } from "../Utils";
export default class Constants { 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 // The user journey states thresholds when a new feature gets unlocked
public static userJourney = { public static userJourney = {

View file

@ -40,15 +40,22 @@ export default class MoreScreen extends UIElement {
} }
const currentLocation = State.state.locationControl.data; 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 = 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") { 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) { 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}`
} }

View file

@ -147,13 +147,15 @@ export default class ShareScreen extends UIElement {
const url = (currentLocation ?? new UIEventSource(undefined)).map(() => { const url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
const host = window.location.host; 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))); const parts = Utils.NoEmpty(Utils.NoNull(optionParts.map((eventSource) => eventSource.data)));
let hash = ""; let hash = "";
if (layoutDefinition !== undefined) { if (layoutDefinition !== undefined) {
literalText = `https://${host}/index.html` literalText = `https://${host}${path}/`
if (layout.id.startsWith("wiki:")) { if (layout.id.startsWith("wiki:")) {
parts.push("userlayout=" + encodeURIComponent(layout.id)) parts.push("userlayout=" + encodeURIComponent(layout.id))
} else { } else {

View file

@ -196,7 +196,7 @@ export default class OpeningHoursVisualization extends UIElement {
// Closed! // Closed!
const opensAtDate = oh.getNextChange(); const opensAtDate = oh.getNextChange();
if(opensAtDate === undefined){ if(opensAtDate === undefined){
const comm = oh.getComment(); const comm = oh.getComment() ?? oh.getUnknown();
if(comm !== undefined){ if(comm !== undefined){
return new FixedUiElement(comm).SetClass("ohviz-closed").Render(); return new FixedUiElement(comm).SetClass("ohviz-closed").Render();
} }

View file

@ -7,6 +7,7 @@ import Combine from "../Base/Combine";
import TagRenderingAnswer from "./TagRenderingAnswer"; import TagRenderingAnswer from "./TagRenderingAnswer";
import State from "../../State"; import State from "../../State";
import Svg from "../../Svg"; import Svg from "../../Svg";
import {TagUtils} from "../../Logic/Tags";
export default class EditableTagRendering extends UIElement { export default class EditableTagRendering extends UIElement {
private readonly _tags: UIEventSource<any>; private readonly _tags: UIEventSource<any>;
@ -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() { private GenerateQuestion() {
const self = this; const self = this;
if (this._configuration.question !== undefined) { 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();
}
} }

View file

@ -3,6 +3,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import TagRenderingQuestion from "./TagRenderingQuestion"; import TagRenderingQuestion from "./TagRenderingQuestion";
import Translations from "../i18n/Translations"; 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 { InnerRender(): string {
for (let i = 0; i < this._tagRenderingQuestions.length; i++) { for (let i = 0; i < this._tagRenderingQuestions.length; i++) {
let tagRendering = this._tagRenderings[i]; let tagRendering = this._tagRenderings[i];
if(tagRendering.condition &&
!tagRendering.condition.matchesProperties(this._tags.data)){ if(this.IsKnown(tagRendering)){
// Filtered away by the condition
continue; continue;
} }
if (tagRendering.GetRenderValue(this._tags.data) !== undefined) {
// This value is known
continue;
}
if (this._skippedQuestions.data.indexOf(i) >= 0) { if (this._skippedQuestions.data.indexOf(i) >= 0) {
continue; continue;

View file

@ -2,6 +2,9 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
import {SubstitutedTranslation} from "../SpecialVisualizations"; 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 * Displays the correct value for a known tagrendering
@ -15,7 +18,7 @@ export default class TagRenderingAnswer extends UIElement {
super(tags); super(tags);
this._tags = tags; this._tags = tags;
this._configuration = configuration; this._configuration = configuration;
if(configuration === undefined){ if (configuration === undefined) {
throw "Trying to generate a tagRenderingAnswer without configuration..." throw "Trying to generate a tagRenderingAnswer without configuration..."
} }
} }
@ -32,12 +35,38 @@ export default class TagRenderingAnswer extends UIElement {
return ""; return "";
} }
const tr = this._configuration.GetRenderValue(tags); const tr = this._configuration.GetRenderValue(tags);
if (tr === undefined) { if (tr !== undefined) {
return ""; 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); // The render value doesn't work well with multi-answers (checkboxes), so we have to check for them manually
return this._content.Render(); 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(["<ul>",
...applicableThens.map(tr => new Combine(["<li>", tr, "</li>"]))
,
"</ul>"
])
}
return this._content.Render();
}
}
return "";
} }
} }

View file

@ -53,7 +53,9 @@ export default class TagRenderingQuestion extends UIElement {
this._inputElement = this.GenerateInputElement() this._inputElement = this.GenerateInputElement()
const self = this; const self = this;
const save = () => { const save = () => {
console.log("Save clicked!")
const selection = self._inputElement.GetValue().data; const selection = self._inputElement.GetValue().data;
console.log("Selection is", selection)
if (selection) { if (selection) {
(State.state?.changes ?? new Changes()) (State.state?.changes ?? new Changes())
.addTag(tags.data.id, selection, tags); .addTag(tags.data.id, selection, tags);