From ce30b159f93ba023dd3392be44c7111a97e1cafd Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 5 Jan 2021 10:56:25 +0100 Subject: [PATCH 01/25] Performance improvement --- Logic/FeatureSource/FilteringFeatureSource.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Logic/FeatureSource/FilteringFeatureSource.ts b/Logic/FeatureSource/FilteringFeatureSource.ts index 9ab1097e4..57f6e3415 100644 --- a/Logic/FeatureSource/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/FilteringFeatureSource.ts @@ -44,7 +44,17 @@ export default class FilteringFeatureSource implements FeatureSource { } upstream.features.addCallback(() => { update()}); - location.map(l => l.zoom).addCallback(() => { + location.map(l => { + // We want something that is stable for the shown layers + const displayedLayerIndexes = []; + for (let i = 0; i < layers.length; i++) { + if(l.zoom <= layers[i].layerDef.minzoom){ + displayedLayerIndexes.push(i); + } + } + return displayedLayerIndexes.join(",") + }) + .addCallback(() => { update();}); From 614158e3c6f585e6b4e68bdbb1211badc532f1ef Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 5 Jan 2021 11:17:12 +0100 Subject: [PATCH 02/25] Performance improvements --- Logic/FeatureSource/FilteringFeatureSource.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Logic/FeatureSource/FilteringFeatureSource.ts b/Logic/FeatureSource/FilteringFeatureSource.ts index 57f6e3415..49fd2004b 100644 --- a/Logic/FeatureSource/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/FilteringFeatureSource.ts @@ -39,8 +39,6 @@ export default class FilteringFeatureSource implements FeatureSource { for (const layer of layers) { layerDict[layer.layerDef.id] = layer; - layer.isDisplayed.addCallback(() => { - update()}) } upstream.features.addCallback(() => { update()}); @@ -48,12 +46,16 @@ export default class FilteringFeatureSource implements FeatureSource { // We want something that is stable for the shown layers const displayedLayerIndexes = []; for (let i = 0; i < layers.length; i++) { - if(l.zoom <= layers[i].layerDef.minzoom){ - displayedLayerIndexes.push(i); + if(l.zoom < layers[i].layerDef.minzoom){ + continue; } + if(!layers[i].isDisplayed.data){ + continue; + } + displayedLayerIndexes.push(i); } return displayedLayerIndexes.join(",") - }) + }, layers.map(l => l.isDisplayed)) .addCallback(() => { update();}); From 52d9b2f452d02003d4e8a132037c2399a4deff17 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 5 Jan 2021 16:59:12 +0100 Subject: [PATCH 03/25] Stabilizing popup behaviour --- UI/ShowDataLayer.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 6a8317a8a..17ac8ae81 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -43,7 +43,7 @@ export default class ShowDataLayer { const feats = features.data.map(ff => ff.feature); let geoLayer = self.CreateGeojsonLayer(feats) if (layoutToUse.clustering.minNeededElements <= features.data.length) { - const cl = window["L"]; + const cl = window["L"]; // This is a dirty workaround, the clustering plugin binds to the L of the window, not of the namespace or something const cluster = cl.markerClusterGroup({ disableClusteringAtZoom: layoutToUse.clustering.maxZoom }); cluster.addLayer(geoLayer); geoLayer = cluster; @@ -116,7 +116,7 @@ export default class ShowDataLayer { const tags = State.state.allElements.getEventSourceFor(feature); const uiElement: LazyElement = new LazyElement(() => new FeatureInfoBox(tags, layer)); popup.setContent(uiElement.Render()); - popup.on('popupclose', () => { + popup.on('remove', () => { State.state.selectedElement.setData(undefined); }); leafletLayer.bindPopup(popup); @@ -126,13 +126,16 @@ export default class ShowDataLayer { leafletLayer.on("click", (e) => { // We set the element as selected... - // uiElement.Activate(); + uiElement.Activate(); State.state.selectedElement.setData(feature); }); const id = feature.properties.id+feature.geometry.type+feature._matching_layer_id; this._onSelectedTrigger[id] = () => { + if(popup.isOpen()){ + return; + } leafletLayer.openPopup(); uiElement.Activate(); } From a35b80afbb906d603c730dee4ff3e385a4881f37 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jan 2021 01:11:07 +0100 Subject: [PATCH 04/25] 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 53ff1dc3a..d18b282a1 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 e4eb43cf3..bfad13ed4 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 031b1e1c5..f409cc121 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 f9651bfbf..5df0c1f7c 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 7fe81eb58..e0f1ffedb 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 3f61e4380..43018948d 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 6e368791c..8f49b9c11 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 68d14526b..11af71860 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 991d11aac..0b7e85be9 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 57ddf6884..f02bcd88a 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(["
    ", + ...applicableThens.map(tr => new Combine(["
  • ", tr, "
  • "])) + , + "
" + ]) + + } + return this._content.Render(); + } + } + return ""; + } } \ No newline at end of file diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index bfeb82e3a..7fd00a7b7 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); From c359d43b1518cff3904c1c57d4b9035b5e958bd3 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jan 2021 02:09:04 +0100 Subject: [PATCH 05/25] Add standalone bicycle library theme, search now opens the popup of the found object --- Customizations/AllKnownLayouts.ts | 2 + InitUiElements.ts | 2 + Logic/Osm/Geocoding.ts | 3 +- Logic/Tags.ts | 8 +- Logic/Web/Hash.ts | 10 +- Logic/Web/QueryParameters.ts | 2 +- Models/Constants.ts | 2 +- State.ts | 2 +- UI/BigComponents/SearchAndGo.ts | 6 +- UI/OpeningHours/OpeningHoursInput.ts | 17 +- UI/ShowDataLayer.ts | 10 +- .../bicycle_library/bicycle_library.svg | 45 ++--- assets/tagRenderings/questions.json | 1 + .../bicycle_library/bicycle_library.json | 27 +++ assets/themes/bicycle_library/logo.svg | 166 ++++++++++++++++++ 15 files changed, 260 insertions(+), 43 deletions(-) create mode 100644 assets/themes/bicycle_library/bicycle_library.json create mode 100644 assets/themes/bicycle_library/logo.svg diff --git a/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts index 3195daa18..2a3467e3b 100644 --- a/Customizations/AllKnownLayouts.ts +++ b/Customizations/AllKnownLayouts.ts @@ -20,6 +20,7 @@ import * as surveillance_cameras from "../assets/themes/surveillance_cameras/sur import * as trees from "../assets/themes/trees/trees.json" import * as personal from "../assets/themes/personalLayout/personalLayout.json" import * as playgrounds from "../assets/themes/playgrounds/playgrounds.json" +import * as bicycle_lib from "../assets/themes/bicycle_library/bicycle_library.json" import LayerConfig from "./JSON/LayerConfig"; import LayoutConfig from "./JSON/LayoutConfig"; import SharedLayers from "./SharedLayers"; @@ -57,6 +58,7 @@ export class AllKnownLayouts { new LayoutConfig(drinking_water), new LayoutConfig(nature), new LayoutConfig(cyclestreets), + new LayoutConfig(bicycle_lib), new LayoutConfig(maps), new LayoutConfig(fritures), new LayoutConfig(benches), diff --git a/InitUiElements.ts b/InitUiElements.ts index c12ca087a..de9fe3f88 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -40,6 +40,7 @@ import FeatureSwitched from "./UI/Base/FeatureSwitched"; import FeatureDuplicatorPerLayer from "./Logic/FeatureSource/FeatureDuplicatorPerLayer"; import LayerConfig from "./Customizations/JSON/LayerConfig"; import ShowDataLayer from "./UI/ShowDataLayer"; +import Hash from "./Logic/Web/Hash"; export class InitUiElements { @@ -132,6 +133,7 @@ export class InitUiElements { if (feature === undefined) { State.state.fullScreenMessage.setData(undefined); + Hash.hash.setData(undefined); } if (feature?.properties === undefined) { return; diff --git a/Logic/Osm/Geocoding.ts b/Logic/Osm/Geocoding.ts index 85f5c5ea2..f2ee260f2 100644 --- a/Logic/Osm/Geocoding.ts +++ b/Logic/Osm/Geocoding.ts @@ -5,7 +5,8 @@ export class Geocoding { private static readonly host = "https://nominatim.openstreetmap.org/search?"; static Search(query: string, - handleResult: ((places: { display_name: string, lat: number, lon: number, boundingbox: number[] }[]) => void), + handleResult: ((places: { display_name: string, lat: number, lon: number, boundingbox: number[], + osm_type: string, osm_id: string}[]) => void), onFail: (() => void)) { const b = State.state.leafletMap.data.getBounds(); console.log(b); diff --git a/Logic/Tags.ts b/Logic/Tags.ts index f409cc121..1b849511c 100644 --- a/Logic/Tags.ts +++ b/Logic/Tags.ts @@ -454,19 +454,19 @@ export class TagUtils { 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]; + if(tags[splitKey] === undefined) { + return false; + } + 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/Logic/Web/Hash.ts b/Logic/Web/Hash.ts index 6ca2d99e5..f9329b2e2 100644 --- a/Logic/Web/Hash.ts +++ b/Logic/Web/Hash.ts @@ -2,11 +2,17 @@ import {UIEventSource} from "../UIEventSource"; export default class Hash { - public static Get() : UIEventSource{ + public static hash : UIEventSource = Hash.Get(); + + private static Get() : UIEventSource{ const hash = new UIEventSource(window.location.hash.substr(1)); hash.addCallback(h => { + if(h === undefined || h === ""){ + window.location.hash = ""; + return; + } h = h.replace(/\//g, "_"); - return window.location.hash = "#" + h; + window.location.hash = "#" + h; }); window.onhashchange = () => { hash.setData(window.location.hash.substr(1)) diff --git a/Logic/Web/QueryParameters.ts b/Logic/Web/QueryParameters.ts index 08cd37a76..715ee32a8 100644 --- a/Logic/Web/QueryParameters.ts +++ b/Logic/Web/QueryParameters.ts @@ -62,7 +62,7 @@ export class QueryParameters { parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data)) } - history.replaceState(null, "", "?" + parts.join("&") + "#" + Hash.Get().data); + history.replaceState(null, "", "?" + parts.join("&") + "#" + Hash.hash.data); } diff --git a/Models/Constants.ts b/Models/Constants.ts index 5df0c1f7c..35214dbb8 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.0c"; + public static vNumber = "0.3.1 "; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/State.ts b/State.ts index 7b92cf896..0f3e43d12 100644 --- a/State.ts +++ b/State.ts @@ -201,7 +201,7 @@ export default class State { ); - const h = Hash.Get(); + const h = Hash.hash; this.selectedElement.addCallback(selected => { if (selected === undefined) { h.setData(""); diff --git a/UI/BigComponents/SearchAndGo.ts b/UI/BigComponents/SearchAndGo.ts index 6603ef89e..9d945a65a 100644 --- a/UI/BigComponents/SearchAndGo.ts +++ b/UI/BigComponents/SearchAndGo.ts @@ -8,6 +8,7 @@ import State from "../../State"; import {TextField} from "../Input/TextField"; import {Geocoding} from "../../Logic/Osm/Geocoding"; import Translations from "../i18n/Translations"; +import Hash from "../../Logic/Web/Hash"; export default class SearchAndGo extends UIElement { @@ -61,11 +62,14 @@ export default class SearchAndGo extends UIElement { return; } - const bb = result[0].boundingbox; + const poi = result[0]; + const bb = poi.boundingbox; const bounds: [[number, number], [number, number]] = [ [bb[0], bb[2]], [bb[1], bb[3]] ] + State.state.selectedElement. setData(undefined); + Hash.hash.setData(poi.osm_type+"_"+poi.osm_id); State.state.leafletMap.data.fitBounds(bounds); self._placeholder.setData(Translations.t.general.search.search); }, diff --git a/UI/OpeningHours/OpeningHoursInput.ts b/UI/OpeningHours/OpeningHoursInput.ts index 7a9ca26ca..8e9a7c9b8 100644 --- a/UI/OpeningHours/OpeningHoursInput.ts +++ b/UI/OpeningHours/OpeningHoursInput.ts @@ -13,6 +13,7 @@ import {OH} from "./OpeningHours"; import {InputElement} from "../Input/InputElement"; import PublicHolidayInput from "./PublicHolidayInput"; import Translations from "../i18n/Translations"; +import {Utils} from "../../Utils"; export default class OpeningHoursInput extends InputElement { @@ -63,15 +64,13 @@ export default class OpeningHoursInput extends InputElement { this._phSelector = new PublicHolidayInput(ph); function update() { - let rules = OH.ToString(rulesFromOhPicker.data); - if (leftoverRules.data.length != 0) { - rules += ";" + leftoverRules.data.join(";") - } - const phData = ph.data; - if (phData !== undefined && phData !== "") { - rules += ";" + phData; - } - value.setData(rules); + const regular = OH.ToString(rulesFromOhPicker.data); + const rules : string[] = [ + regular, + ...leftoverRules.data, + ph.data + ] + value.setData(Utils.NoEmpty(rules).join(";")); } rulesFromOhPicker.addCallback(update); diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 17ac8ae81..9968d76f5 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -68,6 +68,12 @@ export default class ShowDataLayer { action(); } }); + Hash.hash.addCallback(id => { + const action = self._onSelectedTrigger[id]; + if(action){ + action(); + } + }) } @@ -139,9 +145,9 @@ export default class ShowDataLayer { leafletLayer.openPopup(); uiElement.Activate(); } - + this._onSelectedTrigger[feature.properties.id.replace("/","_")] = this._onSelectedTrigger[id]; - if (feature.properties.id.replace(/\//g, "_") === Hash.Get().data) { + if (feature.properties.id.replace(/\//g, "_") === Hash.hash.data) { // This element is in the URL, so this is a share link // We already open it uiElement.Activate(); diff --git a/assets/layers/bicycle_library/bicycle_library.svg b/assets/layers/bicycle_library/bicycle_library.svg index 3692a1b5d..a4d017c46 100644 --- a/assets/layers/bicycle_library/bicycle_library.svg +++ b/assets/layers/bicycle_library/bicycle_library.svg @@ -22,7 +22,7 @@ image/svg+xml - + @@ -81,34 +81,37 @@ inkscape:window-height="999" id="namedview6" showgrid="false" - inkscape:zoom="0.74071072" - inkscape:cx="204.95027" - inkscape:cy="554.1468" + inkscape:zoom="0.37035536" + inkscape:cx="-364.34347" + inkscape:cy="764.959" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" - inkscape:current-layer="layer2" /> + inkscape:current-layer="layer1" + showguides="true" + inkscape:guide-bbox="true"> + + - - - - + + diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index 16265f668..e9aeefa71 100644 --- a/assets/tagRenderings/questions.json +++ b/assets/tagRenderings/questions.json @@ -8,6 +8,7 @@ "phone": { "question": { "en": "What is the phone number of {name}?", + "nl": "Wat is het telefoonnummer van {name}?", "de": "Was ist die Telefonnummer von {name}?" }, "render": "{phone}", diff --git a/assets/themes/bicycle_library/bicycle_library.json b/assets/themes/bicycle_library/bicycle_library.json new file mode 100644 index 000000000..45f22c81a --- /dev/null +++ b/assets/themes/bicycle_library/bicycle_library.json @@ -0,0 +1,27 @@ +{ + "id": "bicyclelib", + "maintainer": "MapComplete", + "version": "2020-08-29", + "language": [ + "en", + "nl", + ], + "title": { + "en": "Bicycle libraries", + "nl": "Fietsbibliotheken", + }, + "description": { + "nl": "Een fietsbibliotheek is een plaats waar men een fiets kan lenen, vaak voor een klein bedrag per jaar. Een typisch voorbeeld zijn kinderfietsbibliotheken, waar men een fiets op maat van het kind kan lenen. Is het kind de fiets ontgroeid, dan kan het te kleine fietsje omgeruild worden voor een grotere.", + "en": "A bicycle library is a place where bicycles can be lent, often for a small yearly fee. A notable use case are bicycle libraries for kids, which allows them to change for a bigger bike when they've outgrown their current bike" + }, + "icon": "./assets/themes/bicycle_library/logo.svg", + "socialImage": null, + "startLat": 0, + "startLon": 0, + "startZoom": 1, + "widenFactor": 0.05, + "roamingRenderings": [], + "layers": [ + "bicycle_library" + ] +} \ No newline at end of file diff --git a/assets/themes/bicycle_library/logo.svg b/assets/themes/bicycle_library/logo.svg new file mode 100644 index 000000000..7256b8858 --- /dev/null +++ b/assets/themes/bicycle_library/logo.svg @@ -0,0 +1,166 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 66018cb4219f9322774d572f7218a705a60d2ebf Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jan 2021 02:21:50 +0100 Subject: [PATCH 06/25] Move runningFromConsole to utils --- Logic/Web/Hash.ts | 5 ++ Models/Constants.ts | 11 ++- UI/Base/Img.ts | 5 +- UI/UIElement.ts | 77 +++++++++---------- UI/i18n/Locale.ts | 4 +- Utils.ts | 21 +++-- .../bicycle_library/bicycle_library.json | 4 +- scripts/createLayouts.ts | 10 +-- test/Tag.spec.ts | 2 - 9 files changed, 73 insertions(+), 66 deletions(-) diff --git a/Logic/Web/Hash.ts b/Logic/Web/Hash.ts index f9329b2e2..4ef26916b 100644 --- a/Logic/Web/Hash.ts +++ b/Logic/Web/Hash.ts @@ -1,10 +1,15 @@ import {UIEventSource} from "../UIEventSource"; +import Constants from "../../Models/Constants"; +import {Utils} from "../../Utils"; export default class Hash { public static hash : UIEventSource = Hash.Get(); private static Get() : UIEventSource{ + if(Utils.runningFromConsole){ + return new UIEventSource(undefined); + } const hash = new UIEventSource(window.location.hash.substr(1)); hash.addCallback(h => { if(h === undefined || h === ""){ diff --git a/Models/Constants.ts b/Models/Constants.ts index 35214dbb8..2262323c9 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -14,7 +14,16 @@ export default class Constants { themeGeneratorReadOnlyUnlock: 200, themeGeneratorFullUnlock: 500, addNewPointWithUnreadMessagesUnlock: 500, - minZoomLevelToAddNewPoints: (Utils.isRetina() ? 18 : 19) + minZoomLevelToAddNewPoints: (Constants.isRetina() ? 18 : 19) }; + + private static isRetina(): boolean { + if (Utils.runningFromConsole) { + return; + } + // The cause for this line of code: https://github.com/pietervdvn/MapComplete/issues/115 + // See https://stackoverflow.com/questions/19689715/what-is-the-best-way-to-detect-retina-support-on-a-device-using-javascript + return ((window.matchMedia && (window.matchMedia('only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx), only screen and (min-resolution: 75.6dpcm)').matches || window.matchMedia('only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min--moz-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2)').matches)) || (window.devicePixelRatio && window.devicePixelRatio >= 2)); + } } \ No newline at end of file diff --git a/UI/Base/Img.ts b/UI/Base/Img.ts index 66b4e797d..1186ae9d5 100644 --- a/UI/Base/Img.ts +++ b/UI/Base/Img.ts @@ -1,9 +1,12 @@ +import Constants from "../../Models/Constants"; +import {Utils} from "../../Utils"; + export default class Img { public static runningFromConsole = false; static AsData(source:string){ - if(this.runningFromConsole){ + if(Utils.runningFromConsole){ return source; } return `data:image/svg+xml;base64,${(btoa(source))}`; diff --git a/UI/UIElement.ts b/UI/UIElement.ts index 6c91ad5ca..d1fda3f87 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -1,27 +1,25 @@ import {UIEventSource} from "../Logic/UIEventSource"; +import Constants from "../Models/Constants"; +import {Utils} from "../Utils"; export abstract class UIElement extends UIEventSource { - private static nextId: number = 0; - - public readonly id: string; - public readonly _source: UIEventSource; - private clss: string[] = [] - - private style: string; - - private _hideIfEmpty = false; - - public dumbMode = false; - - private lastInnerRender: string; - /** * In the 'deploy'-step, some code needs to be run by ts-node. * However, ts-node crashes when it sees 'document'. When running from console, we flag this and disable all code where document is needed. * This is a workaround and yet another hack */ public static runningFromConsole = false; + private static nextId: number = 0; + public readonly id: string; + public readonly _source: UIEventSource; + public dumbMode = false; + private clss: string[] = [] + private style: string; + private _hideIfEmpty = false; + private lastInnerRender: string; + private _onClick: () => void; + private _onHover: UIEventSource; protected constructor(source: UIEventSource = undefined) { super(""); @@ -32,7 +30,6 @@ export abstract class UIElement extends UIEventSource { this.ListenTo(source); } - public ListenTo(source: UIEventSource) { if (source === undefined) { return this; @@ -46,8 +43,6 @@ export abstract class UIElement extends UIEventSource { return this; } - private _onClick: () => void; - public onClick(f: (() => void)) { this.dumbMode = false; this._onClick = f; @@ -56,8 +51,6 @@ export abstract class UIElement extends UIEventSource { return this; } - private _onHover: UIEventSource; - public IsHovered(): UIEventSource { this.dumbMode = false; if (this._onHover !== undefined) { @@ -69,10 +62,10 @@ export abstract class UIElement extends UIEventSource { } Update(): void { - if (UIElement.runningFromConsole) { + if (Utils.runningFromConsole) { return; } - + let element = document.getElementById(this.id); if (element === undefined || element === null) { // The element is not painted or, in the case of 'dumbmode' this UI-element is not explicitely present @@ -101,7 +94,7 @@ export abstract class UIElement extends UIEventSource { const self = this; element.onclick = (e) => { // @ts-ignore - if(e.consumed){ + if (e.consumed) { return; } self._onClick(); @@ -123,31 +116,12 @@ export abstract class UIElement extends UIEventSource { } - private UpdateAllChildren() { - for (const i in this) { - const child = this[i]; - if (child instanceof UIElement) { - child.Update(); - } else if (child instanceof Array) { - for (const ch of child) { - if (ch instanceof UIElement) { - ch.Update(); - } - } - } - } - } - HideOnEmpty(hide: boolean) { this._hideIfEmpty = hide; this.Update(); return this; } - // Called after the HTML has been replaced. Can be used for css tricks - protected InnerUpdate(htmlElement: HTMLElement) { - } - Render(): string { this.lastInnerRender = this.InnerRender(); if (this.dumbMode) { @@ -192,6 +166,7 @@ export abstract class UIElement extends UIEventSource { } return this; } + public RemoveClass(clss: string): UIElement { const i = this.clss.indexOf(clss); if (i >= 0) { @@ -201,13 +176,31 @@ export abstract class UIElement extends UIEventSource { return this; } - public SetStyle(style: string): UIElement { this.dumbMode = false; this.style = style; this.Update(); return this; } + + // Called after the HTML has been replaced. Can be used for css tricks + protected InnerUpdate(htmlElement: HTMLElement) { + } + + private UpdateAllChildren() { + for (const i in this) { + const child = this[i]; + if (child instanceof UIElement) { + child.Update(); + } else if (child instanceof Array) { + for (const ch of child) { + if (ch instanceof UIElement) { + ch.Update(); + } + } + } + } + } } diff --git a/UI/i18n/Locale.ts b/UI/i18n/Locale.ts index 9482766a4..55365ce6c 100644 --- a/UI/i18n/Locale.ts +++ b/UI/i18n/Locale.ts @@ -1,6 +1,6 @@ import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; +import {Utils} from "../../Utils"; export default class Locale { @@ -9,7 +9,7 @@ export default class Locale { private static setup() { const source = LocalStorageSource.Get('language', "en"); - if (!UIElement.runningFromConsole) { + if (!Utils.runningFromConsole) { // @ts-ignore window.setLanguage = function (language: string) { source.setData(language) diff --git a/Utils.ts b/Utils.ts index 7866df29a..177d528ec 100644 --- a/Utils.ts +++ b/Utils.ts @@ -1,8 +1,16 @@ -import {UIElement} from "./UI/UIElement"; import * as $ from "jquery" +import Constants from "./Models/Constants"; + export class Utils { + /** + * In the 'deploy'-step, some code needs to be run by ts-node. + * However, ts-node crashes when it sees 'document'. When running from console, we flag this and disable all code where document is needed. + * This is a workaround and yet another hack + */ + public static runningFromConsole = false; + public static readonly assets_path = "./assets/svg/"; static EncodeXmlValue(str) { @@ -59,7 +67,7 @@ export class Utils { } static DoEvery(millis: number, f: (() => void)) { - if (UIElement.runningFromConsole) { + if (Utils.runningFromConsole) { return; } window.setTimeout( @@ -134,15 +142,6 @@ export class Utils { return [a.substr(0, index), a.substr(index + sep.length)]; } - public static isRetina(): boolean { - if (UIElement.runningFromConsole) { - return; - } - // The cause for this line of code: https://github.com/pietervdvn/MapComplete/issues/115 - // See https://stackoverflow.com/questions/19689715/what-is-the-best-way-to-detect-retina-support-on-a-device-using-javascript - return ((window.matchMedia && (window.matchMedia('only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx), only screen and (min-resolution: 75.6dpcm)').matches || window.matchMedia('only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min--moz-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2)').matches)) || (window.devicePixelRatio && window.devicePixelRatio >= 2)); - } - // 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, diff --git a/assets/themes/bicycle_library/bicycle_library.json b/assets/themes/bicycle_library/bicycle_library.json index 45f22c81a..93c8d01a9 100644 --- a/assets/themes/bicycle_library/bicycle_library.json +++ b/assets/themes/bicycle_library/bicycle_library.json @@ -4,11 +4,11 @@ "version": "2020-08-29", "language": [ "en", - "nl", + "nl" ], "title": { "en": "Bicycle libraries", - "nl": "Fietsbibliotheken", + "nl": "Fietsbibliotheken" }, "description": { "nl": "Een fietsbibliotheek is een plaats waar men een fiets kan lenen, vaak voor een klein bedrag per jaar. Een typisch voorbeeld zijn kinderfietsbibliotheken, waar men een fiets op maat van het kind kan lenen. Is het kind de fiets ontgroeid, dan kan het te kleine fietsje omgeruild worden voor een grotere.", diff --git a/scripts/createLayouts.ts b/scripts/createLayouts.ts index 38d3970e6..46ebd91ab 100644 --- a/scripts/createLayouts.ts +++ b/scripts/createLayouts.ts @@ -1,16 +1,16 @@ -import Img from "../UI/Base/Img" -import {UIElement} from "../UI/UIElement"; -Img.runningFromConsole = true; // We HAVE to mark this while importing -UIElement.runningFromConsole = true; +import {Utils} from "../Utils"; +Utils.runningFromConsole = true; +import LayoutConfig from "../Customizations/JSON/LayoutConfig"; import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; import {existsSync, mkdirSync, readFileSync, writeFile, writeFileSync} from "fs"; import Locale from "../UI/i18n/Locale"; import svg2img from 'promise-svg2img'; import Translations from "../UI/i18n/Translations"; import {Translation} from "../UI/i18n/Translation"; -import LayoutConfig from "../Customizations/JSON/LayoutConfig"; + + function enc(str: string): string { return encodeURIComponent(str.toLowerCase()); diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index 3bc9c9b1a..4ef6ddf7d 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -1,7 +1,5 @@ import {UIElement} from "../UI/UIElement"; UIElement.runningFromConsole = true; -import Img from "../UI/Base/Img"; -Img.runningFromConsole = true; import {equal} from "assert"; import T from "./TestHelper"; import {FromJSON} from "../Customizations/JSON/FromJSON"; From 93a16944a37d5aaf87743328e28f051fd9d40c5d Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jan 2021 02:52:38 +0100 Subject: [PATCH 07/25] Add overrie capabilities --- Customizations/JSON/LayoutConfig.ts | 30 +++++++--- Customizations/JSON/LayoutConfigJson.ts | 5 +- Customizations/JSON/TagRenderingConfig.ts | 2 +- Customizations/SharedLayers.ts | 71 +++++++++++++---------- Logic/Actors/UpdateFromOverpass.ts | 1 - Utils.ts | 39 +++++++++++++ assets/themes/cyclofix/cyclofix.json | 6 +- 7 files changed, 113 insertions(+), 41 deletions(-) diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts index 85f366c95..23a2264b5 100644 --- a/Customizations/JSON/LayoutConfig.ts +++ b/Customizations/JSON/LayoutConfig.ts @@ -4,6 +4,7 @@ import LayerConfig from "./LayerConfig"; import {LayoutConfigJson} from "./LayoutConfigJson"; import SharedLayers from "../SharedLayers"; import SharedTagRenderings from "../SharedTagRenderings"; +import {Utils} from "../../Utils"; export default class LayoutConfig { public readonly id: string; @@ -24,11 +25,11 @@ export default class LayoutConfig { public readonly roamingRenderings: TagRenderingConfig[]; public readonly defaultBackgroundId?: string; public readonly layers: LayerConfig[]; - public readonly clustering?: { + public readonly clustering?: { maxZoom: number, minNeededElements: number }; - + public readonly hideFromOverview: boolean; public readonly enableUserBadge: boolean; public readonly enableShareScreen: boolean; @@ -79,13 +80,28 @@ export default class LayoutConfig { ); this.defaultBackgroundId = json.defaultBackgroundId; this.layers = json.layers.map((layer, i) => { - if (typeof layer === "string"){ + if (typeof layer === "string") { if (SharedLayers.sharedLayers[layer] !== undefined) { return SharedLayers.sharedLayers[layer]; } else { throw "Unkown fixed layer " + layer; } } + // @ts-ignore + if (layer.builtin !== undefined) { + // @ts-ignore + const name = layer.builtin; + console.warn("Overwriting!") + const shared = SharedLayers.sharedLayersJson[name]; + if (shared === undefined) { + throw "Unkown fixed layer " + name; + } + console.log("PREMERGE", layer, shared) + // @ts-ignore + layer = Utils.Merge(layer.override, shared); + } + + // @ts-ignore return new LayerConfig(layer, this.roamingRenderings, `${this.id}.layers[${i}]`); }); @@ -94,14 +110,14 @@ export default class LayoutConfig { maxZoom: 16, minNeededElements: 250 }; - if(json.clustering){ + if (json.clustering) { this.clustering = { - maxZoom : json.clustering.maxZoom ?? 18, + maxZoom: json.clustering.maxZoom ?? 18, minNeededElements: json.clustering.minNeededElements ?? 1 } for (const layer of this.layers) { - if(layer.wayHandling !== LayerConfig.WAYHANDLING_CENTER_ONLY){ - console.error("WARNING: In order to allow clustering, every layer must be set to CENTER_ONLY. Layer", layer.id,"does not respect this for layout",this.id); + if (layer.wayHandling !== LayerConfig.WAYHANDLING_CENTER_ONLY) { + console.error("WARNING: In order to allow clustering, every layer must be set to CENTER_ONLY. Layer", layer.id, "does not respect this for layout", this.id); } } } diff --git a/Customizations/JSON/LayoutConfigJson.ts b/Customizations/JSON/LayoutConfigJson.ts index fd58ebb1d..9f56ac426 100644 --- a/Customizations/JSON/LayoutConfigJson.ts +++ b/Customizations/JSON/LayoutConfigJson.ts @@ -120,8 +120,11 @@ export interface LayoutConfigJson { * * *layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself * + * Note that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: "layername", override: ...} + * The 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer + * */ - layers: (LayerConfigJson | string)[], + layers: (LayerConfigJson | string | {builtin: string, override: any})[], /** * If defined, data will be clustered. diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts index d18b282a1..add0f72c2 100644 --- a/Customizations/JSON/TagRenderingConfig.ts +++ b/Customizations/JSON/TagRenderingConfig.ts @@ -46,7 +46,7 @@ export default class TagRenderingConfig { this.multiAnswer = false; return; } - + this.render = Translations.T(json.render); this.question = Translations.T(json.question); this.condition = FromJSON.Tag(json.condition ?? {"and": []}, `${context}.condition`); diff --git a/Customizations/SharedLayers.ts b/Customizations/SharedLayers.ts index 379ee94e7..6aed204c7 100644 --- a/Customizations/SharedLayers.ts +++ b/Customizations/SharedLayers.ts @@ -1,4 +1,3 @@ - import * as drinkingWater from "../assets/layers/drinking_water/drinking_water.json"; import * as ghostbikes from "../assets/layers/ghost_bike/ghost_bike.json" import * as viewpoint from "../assets/layers/viewpoint/viewpoint.json" @@ -21,45 +20,57 @@ import * as toilets from "../assets/layers/toilets/toilets.json" import * as bookcases from "../assets/layers/public_bookcases/public_bookcases.json" import * as tree_nodes from "../assets/layers/trees/tree_nodes.json" import LayerConfig from "./JSON/LayerConfig"; +import {LayerConfigJson} from "./JSON/LayerConfigJson"; export default class SharedLayers { - - - + + private static sharedLayersListRaw : LayerConfigJson[] = [ + drinkingWater, + ghostbikes, + viewpoint, + bike_parking, + bike_repair_station, + bike_monitoring_station, + birdhides, + nature_reserve, + bike_cafes, + bicycle_library, + cycling_themed_objects, + bike_shops, + bike_cleaning, + maps, + direction, + information_boards, + toilets, + bookcases, + surveillance_camera, + tree_nodes + ]; + + // Must be below the list... public static sharedLayers: Map = SharedLayers.getSharedLayers(); + public static sharedLayersJson: Map = SharedLayers.getSharedLayersJson(); - private static getSharedLayers(){ - const sharedLayersList = [ - new LayerConfig(drinkingWater,[], "shared_layers"), - new LayerConfig(ghostbikes,[], "shared_layers"), - new LayerConfig(viewpoint,[], "shared_layers"), - new LayerConfig(bike_parking,[], "shared_layers"), - new LayerConfig(bike_repair_station,[], "shared_layers"), - new LayerConfig(bike_monitoring_station,[], "shared_layers"), - new LayerConfig(birdhides,[], "shared_layers"), - new LayerConfig(nature_reserve,[], "shared_layers"), - new LayerConfig(bike_cafes,[], "shared_layers"), - new LayerConfig(bicycle_library, [], "bike_library"), - new LayerConfig(cycling_themed_objects,[], "shared_layers"), - new LayerConfig(bike_shops,[], "shared_layers"), - new LayerConfig(bike_cleaning,[], "shared_layers"), - new LayerConfig(maps,[], "shared_layers"), - new LayerConfig(direction,[], "shared_layers"), - new LayerConfig(information_boards,[], "shared_layers"), - new LayerConfig(toilets,[], "shared_layers"), - new LayerConfig(bookcases,[], "shared_layers"), - new LayerConfig(surveillance_camera,[], "shared_layers"), - new LayerConfig(tree_nodes,[], "shared_layers") - ]; + private static getSharedLayers(): Map { const sharedLayers = new Map(); - for (const layer of sharedLayersList) { + for (const layer of SharedLayers.sharedLayersListRaw) { + const parsed = new LayerConfig(layer, [], "shared_layers") + sharedLayers.set(layer.id, parsed); + sharedLayers[layer.id] = parsed; + } + return sharedLayers; + } + + private static getSharedLayersJson(): Map { + const sharedLayers = new Map(); + for (const layer of SharedLayers.sharedLayersListRaw) { sharedLayers.set(layer.id, layer); sharedLayers[layer.id] = layer; } return sharedLayers; } - - + + } diff --git a/Logic/Actors/UpdateFromOverpass.ts b/Logic/Actors/UpdateFromOverpass.ts index 63067b56f..ab4e95b10 100644 --- a/Logic/Actors/UpdateFromOverpass.ts +++ b/Logic/Actors/UpdateFromOverpass.ts @@ -38,7 +38,6 @@ export default class UpdateFromOverpass implements FeatureSource{ location: UIEventSource, layoutToUse: UIEventSource, leafletMap: UIEventSource) { - console.log("Crating overpass updater") this._location = location; this._layoutToUse = layoutToUse; this._leafletMap = leafletMap; diff --git a/Utils.ts b/Utils.ts index 177d528ec..d9d394098 100644 --- a/Utils.ts +++ b/Utils.ts @@ -175,6 +175,45 @@ export class Utils { 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)); + for (const key in source) { + const sourceV = source[key]; + const targetV = target[key] + if(typeof sourceV === "object"){ + if(targetV === undefined){ + target[key] = sourceV; + }else{ + Utils.Merge(sourceV, targetV); + } + + }else{ + target[key] = sourceV; + } + + } + 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/themes/cyclofix/cyclofix.json b/assets/themes/cyclofix/cyclofix.json index 1da6ad425..c90321a7b 100644 --- a/assets/themes/cyclofix/cyclofix.json +++ b/assets/themes/cyclofix/cyclofix.json @@ -25,6 +25,10 @@ "startZoom": 16, "widenFactor": 0.05, "socialImage": "./assets/themes/cyclofix/logo.svg", - "layers": ["bike_cafes", "bike_shops", "bicycle_library","bike_repair_station", "drinking_water", "bike_themed_object","bike_cleaning","bike_parking"], + "layers": ["bike_cafes", "bike_shops", {"builtin": "bicycle_library", + "override": { + "minzoom": 13 + } + },"bike_repair_station", "drinking_water", "bike_themed_object","bike_cleaning","bike_parking"], "roamingRenderings": [] } \ No newline at end of file From 489c6fc3c0935b5e6318599adc0ba4b9fa81794b Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jan 2021 13:18:33 +0100 Subject: [PATCH 08/25] Fix self-closing popup --- UI/ShowDataLayer.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 9968d76f5..b700f3785 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -69,6 +69,10 @@ export default class ShowDataLayer { } }); Hash.hash.addCallback(id => { + // This is a bit of an edge case: if the hash becomes an id to search, we have to show the corresponding popup + if(State.state.selectedElement !== undefined){ + return; // Something is already selected, we don't have to apply this fix + } const action = self._onSelectedTrigger[id]; if(action){ action(); @@ -144,6 +148,7 @@ export default class ShowDataLayer { } leafletLayer.openPopup(); uiElement.Activate(); + State.state.selectedElement.setData(feature); } this._onSelectedTrigger[feature.properties.id.replace("/","_")] = this._onSelectedTrigger[id]; From 701ce3e89a921b7cdc1a793a49a3d45a9fc9198b Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 7 Jan 2021 04:50:12 +0100 Subject: [PATCH 09/25] Css tweaks --- InitUiElements.ts | 4 +- Logic/Web/MangroveReviews.ts | 8 +- Models/Constants.ts | 2 +- Svg.ts | 42 ++- UI/Base/Ornament.ts | 34 ++ UI/BigComponents/FullWelcomePaneWithTabs.ts | 18 +- ...meMessage.ts => ThemeIntroductionPanel.ts} | 25 +- UI/FullScreenMessageBoxHandler.ts | 25 +- UI/OpeningHours/OhVisualization.ts | 2 +- UI/Popup/FeatureInfoBox.ts | 23 +- UI/ShowDataLayer.ts | 1 - assets/svg/Ornament-Horiz-0.svg | 91 ++++++ assets/svg/Ornament-Horiz-1.svg | 159 +++++++++ assets/svg/Ornament-Horiz-2.svg | 96 ++++++ assets/svg/Ornament-Horiz-3.svg | 91 ++++++ assets/svg/Ornament-Horiz-4.svg | 66 ++++ assets/svg/Ornament-Horiz-5.svg | 81 +++++ assets/svg/Ornament-Horiz-6.svg | 301 ++++++++++++++++++ assets/svg/back.svg | 77 +++++ css/fullscreenmessagebox.css | 30 +- css/mobile.css | 4 +- css/tagrendering.css | 88 ++++- index.css | 66 ++-- 23 files changed, 1220 insertions(+), 114 deletions(-) create mode 100644 UI/Base/Ornament.ts rename UI/BigComponents/{WelcomeMessage.ts => ThemeIntroductionPanel.ts} (69%) create mode 100644 assets/svg/Ornament-Horiz-0.svg create mode 100644 assets/svg/Ornament-Horiz-1.svg create mode 100644 assets/svg/Ornament-Horiz-2.svg create mode 100644 assets/svg/Ornament-Horiz-3.svg create mode 100644 assets/svg/Ornament-Horiz-4.svg create mode 100644 assets/svg/Ornament-Horiz-5.svg create mode 100644 assets/svg/Ornament-Horiz-6.svg create mode 100644 assets/svg/back.svg diff --git a/InitUiElements.ts b/InitUiElements.ts index de9fe3f88..6c7811716 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -174,9 +174,7 @@ export class InitUiElements { }); - new FullScreenMessageBox(() => { - State.state.selectedElement.setData(undefined) - }).AttachTo("messagesboxmobile"); + new FullScreenMessageBox().AttachTo("messagesboxmobile"); InitUiElements.OnlyIf(State.state.featureSwitchWelcomeMessage, () => { diff --git a/Logic/Web/MangroveReviews.ts b/Logic/Web/MangroveReviews.ts index 453aa7148..12c0247e8 100644 --- a/Logic/Web/MangroveReviews.ts +++ b/Logic/Web/MangroveReviews.ts @@ -22,8 +22,12 @@ export class MangroveIdentity { }) }) }) - if ((mangroveIdentity.data ?? "") === "") { - this.CreateIdentity(); + try { + if ((mangroveIdentity.data ?? "") === "") { + this.CreateIdentity(); + } + }catch(e){ + console.error("Could not create identity: ", e) } } diff --git a/Models/Constants.ts b/Models/Constants.ts index 2262323c9..ae7cd4431 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.1 "; + public static vNumber = "0.3.2"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/Svg.ts b/Svg.ts index e37eccb0b..3809edf38 100644 --- a/Svg.ts +++ b/Svg.ts @@ -4,6 +4,41 @@ import {FixedUiElement} from "./UI/Base/FixedUiElement"; export default class Svg { + public static Ornament_Horiz_0 = " image/svg+xml " + public static Ornament_Horiz_0_img = Img.AsImageElement(Svg.Ornament_Horiz_0) + public static Ornament_Horiz_0_svg() { return new FixedUiElement(Svg.Ornament_Horiz_0);} + public static Ornament_Horiz_0_ui() { return new FixedUiElement(Svg.Ornament_Horiz_0_img);} + + public static Ornament_Horiz_1 = " image/svg+xml " + public static Ornament_Horiz_1_img = Img.AsImageElement(Svg.Ornament_Horiz_1) + public static Ornament_Horiz_1_svg() { return new FixedUiElement(Svg.Ornament_Horiz_1);} + public static Ornament_Horiz_1_ui() { return new FixedUiElement(Svg.Ornament_Horiz_1_img);} + + public static Ornament_Horiz_2 = " image/svg+xml " + public static Ornament_Horiz_2_img = Img.AsImageElement(Svg.Ornament_Horiz_2) + public static Ornament_Horiz_2_svg() { return new FixedUiElement(Svg.Ornament_Horiz_2);} + public static Ornament_Horiz_2_ui() { return new FixedUiElement(Svg.Ornament_Horiz_2_img);} + + public static Ornament_Horiz_3 = " image/svg+xml " + public static Ornament_Horiz_3_img = Img.AsImageElement(Svg.Ornament_Horiz_3) + public static Ornament_Horiz_3_svg() { return new FixedUiElement(Svg.Ornament_Horiz_3);} + public static Ornament_Horiz_3_ui() { return new FixedUiElement(Svg.Ornament_Horiz_3_img);} + + public static Ornament_Horiz_4 = " image/svg+xml " + public static Ornament_Horiz_4_img = Img.AsImageElement(Svg.Ornament_Horiz_4) + public static Ornament_Horiz_4_svg() { return new FixedUiElement(Svg.Ornament_Horiz_4);} + public static Ornament_Horiz_4_ui() { return new FixedUiElement(Svg.Ornament_Horiz_4_img);} + + public static Ornament_Horiz_5 = " image/svg+xml " + public static Ornament_Horiz_5_img = Img.AsImageElement(Svg.Ornament_Horiz_5) + public static Ornament_Horiz_5_svg() { return new FixedUiElement(Svg.Ornament_Horiz_5);} + public static Ornament_Horiz_5_ui() { return new FixedUiElement(Svg.Ornament_Horiz_5_img);} + + public static Ornament_Horiz_6 = " image/svg+xml " + public static Ornament_Horiz_6_img = Img.AsImageElement(Svg.Ornament_Horiz_6) + public static Ornament_Horiz_6_svg() { return new FixedUiElement(Svg.Ornament_Horiz_6);} + public static Ornament_Horiz_6_ui() { return new FixedUiElement(Svg.Ornament_Horiz_6_img);} + public static add = " image/svg+xml " public static add_img = Img.AsImageElement(Svg.add) public static add_svg() { return new FixedUiElement(Svg.add);} @@ -29,6 +64,11 @@ export default class Svg { public static arrow_right_smooth_svg() { return new FixedUiElement(Svg.arrow_right_smooth);} public static arrow_right_smooth_ui() { return new FixedUiElement(Svg.arrow_right_smooth_img);} + public static back = " image/svg+xml " + public static back_img = Img.AsImageElement(Svg.back) + public static back_svg() { return new FixedUiElement(Svg.back);} + public static back_ui() { return new FixedUiElement(Svg.back_img);} + public static bug = " " public static bug_img = Img.AsImageElement(Svg.bug) public static bug_svg() { return new FixedUiElement(Svg.bug);} @@ -279,4 +319,4 @@ export default class Svg { public static wikipedia_svg() { return new FixedUiElement(Svg.wikipedia);} public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);} -public static All = {"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapillary.svg": Svg.mapillary,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} +public static All = {"Ornament-Horiz-0.svg": Svg.Ornament_Horiz_0,"Ornament-Horiz-1.svg": Svg.Ornament_Horiz_1,"Ornament-Horiz-2.svg": Svg.Ornament_Horiz_2,"Ornament-Horiz-3.svg": Svg.Ornament_Horiz_3,"Ornament-Horiz-4.svg": Svg.Ornament_Horiz_4,"Ornament-Horiz-5.svg": Svg.Ornament_Horiz_5,"Ornament-Horiz-6.svg": Svg.Ornament_Horiz_6,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapillary.svg": Svg.mapillary,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} diff --git a/UI/Base/Ornament.ts b/UI/Base/Ornament.ts new file mode 100644 index 000000000..e4057bf81 --- /dev/null +++ b/UI/Base/Ornament.ts @@ -0,0 +1,34 @@ +import {UIElement} from "../UIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Svg from "../../Svg"; + +export default class Ornament extends UIElement { + private static readonly ornamentsCount = Ornament.countOrnaments(); + private readonly _index = new UIEventSource(0) + + constructor(index = new UIEventSource(0)) { + super(index); + this._index = index; + this.SetClass("ornament") + const self = this; + this.onClick(() => { + self._index.setData((self._index.data + 1) % Ornament.ornamentsCount); + + }) + } + + private static countOrnaments() { + let ornamentCount = 0; + for (const key in Svg.All) { + if (key.startsWith("Ornament-Horiz-")) { + ornamentCount++; + } + } + return ornamentCount; + } + + InnerRender(): string { + return Svg.All[`Ornament-Horiz-${this._index.data}.svg`] + } + +} \ No newline at end of file diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index 8dd0d806a..b53a59e79 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -1,6 +1,6 @@ import {UIElement} from "../UIElement"; import State from "../../State"; -import WelcomeMessage from "./WelcomeMessage"; +import ThemeIntroductionPanel from "./ThemeIntroductionPanel"; import * as personal from "../../assets/themes/personalLayout/personalLayout.json"; import PersonalLayersPanel from "./PersonalLayersPanel"; import Svg from "../../Svg"; @@ -15,6 +15,8 @@ import {TabbedComponent} from "../Base/TabbedComponent"; import {UIEventSource} from "../../Logic/UIEventSource"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import UserDetails from "../../Logic/Osm/OsmConnection"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import CombinedInputElement from "../Input/CombinedInputElement"; export default class FullWelcomePaneWithTabs extends UIElement { private readonly _layoutToUse: UIEventSource; @@ -27,9 +29,9 @@ export default class FullWelcomePaneWithTabs extends UIElement { this._layoutToUse = State.state.layoutToUse; this._userDetails = State.state.osmConnection.userDetails; - + const layoutToUse = this._layoutToUse.data; - let welcome: UIElement = new WelcomeMessage(); + let welcome: UIElement = new ThemeIntroductionPanel(); if (layoutToUse.id === personal.id) { welcome = new PersonalLayersPanel(); } @@ -66,10 +68,18 @@ export default class FullWelcomePaneWithTabs extends UIElement { } ); - this._component = new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab) + const tabbedPart = new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab) .ListenTo(this._userDetails); + const backButton = new Combine([ + new Combine([Translations.t.general.returnToTheMap.Clone().SetClass("to-the-map")]) + .SetClass("to-the-map-inner") + + ]).SetClass("only-on-mobile") + .onClick(() => State.state.fullScreenMessage.setData(undefined)); + tabbedPart.SetStyle("overflow-y: auto; max-height: calc( 100vh - 4em);display:block;") + this._component = new Combine([tabbedPart, backButton]).SetStyle("width:100%;"); } InnerRender(): string { diff --git a/UI/BigComponents/WelcomeMessage.ts b/UI/BigComponents/ThemeIntroductionPanel.ts similarity index 69% rename from UI/BigComponents/WelcomeMessage.ts rename to UI/BigComponents/ThemeIntroductionPanel.ts index de1179c4b..3c68816ec 100644 --- a/UI/BigComponents/WelcomeMessage.ts +++ b/UI/BigComponents/ThemeIntroductionPanel.ts @@ -4,15 +4,16 @@ import State from "../../State"; import Combine from "../Base/Combine"; import LanguagePicker from "../LanguagePicker"; import Translations from "../i18n/Translations"; +import {VariableUiElement} from "../Base/VariableUIElement"; - -export default class WelcomeMessage extends UIElement { +export default class ThemeIntroductionPanel extends UIElement { private languagePicker: UIElement; private readonly description: UIElement; private readonly plzLogIn: UIElement; private readonly welcomeBack: UIElement; private readonly tail: UIElement; + private readonly loginStatus: UIElement; constructor() { @@ -32,20 +33,24 @@ export default class WelcomeMessage extends UIElement { }); this.welcomeBack = Translations.t.general.welcomeBack; this.tail = layout.descriptionTail; + this.loginStatus = new VariableUiElement( + State.state.osmConnection.userDetails.map( + userdetails => { + if (State.state.featureSwitchUserbadge.data) { + return ""; + } + return (userdetails.loggedIn ? this.welcomeBack : this.plzLogIn).Render(); + } + ) + + ) } InnerRender(): string { - - let loginStatus = undefined; - if (State.state.featureSwitchUserbadge.data) { - loginStatus = (State.state.osmConnection.userDetails.data.loggedIn ? this.welcomeBack : - this.plzLogIn); - } - return new Combine([ this.description, "

", - loginStatus, + this.loginStatus, this.tail, "
", this.languagePicker diff --git a/UI/FullScreenMessageBoxHandler.ts b/UI/FullScreenMessageBoxHandler.ts index 4b1950852..0a67765a3 100644 --- a/UI/FullScreenMessageBoxHandler.ts +++ b/UI/FullScreenMessageBoxHandler.ts @@ -1,5 +1,4 @@ import {UIElement} from "./UIElement"; -import Translations from "./i18n/Translations"; import State from "../State"; import Combine from "./Base/Combine"; @@ -8,24 +7,11 @@ import Combine from "./Base/Combine"; */ export default class FullScreenMessageBox extends UIElement { - private readonly returnToTheMap: UIElement; private _content: UIElement; - constructor(onClear: (() => void)) { + constructor() { super(State.state.fullScreenMessage); this.HideOnEmpty(true); - - this.returnToTheMap = - new Combine([ - // Wrapped another time to prevent the value of 'em' to fluctuate - Translations.t.general.returnToTheMap.Clone() - ]) - .onClick(() => { - State.state.fullScreenMessage.setData(undefined); - onClear(); - }) - .SetClass("to-the-map") - } @@ -34,17 +20,14 @@ export default class FullScreenMessageBox extends UIElement { return ""; } this._content = State.state.fullScreenMessage.data; - const innerWrap = new Combine([this._content]).SetClass("fullscreenmessage-content") - - return new Combine([innerWrap, this.returnToTheMap]) - .SetStyle("display:block; height: 100%;") - .Render(); + return new Combine([this._content]).SetClass("fullscreenmessage-content").Render(); } protected InnerUpdate(htmlElement: HTMLElement) { super.InnerUpdate(htmlElement); + // This is a bit out of place, and it is a fix specifically for the featureinfobox-titlebar const height = htmlElement.getElementsByClassName("featureinfobox-titlebar")[0]?.clientHeight ?? 0; - htmlElement.style.setProperty("--variable-title-height", height+"px") + htmlElement.style.setProperty("--variable-title-height", height + "px") } diff --git a/UI/OpeningHours/OhVisualization.ts b/UI/OpeningHours/OhVisualization.ts index 8f49b9c11..8b3638c8c 100644 --- a/UI/OpeningHours/OhVisualization.ts +++ b/UI/OpeningHours/OhVisualization.ts @@ -197,7 +197,7 @@ export default class OpeningHoursVisualization extends UIElement { const opensAtDate = oh.getNextChange(); if(opensAtDate === undefined){ const comm = oh.getComment() ?? oh.getUnknown(); - if(comm !== undefined){ + if(!!comm){ return new FixedUiElement(comm).SetClass("ohviz-closed").Render(); } diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 7a58c9393..f97e97a27 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -7,6 +7,9 @@ import Combine from "../Base/Combine"; import TagRenderingAnswer from "./TagRenderingAnswer"; import State from "../../State"; import {FixedUiElement} from "../Base/FixedUiElement"; +import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; +import Svg from "../../Svg"; +import Ornament from "../Base/Ornament"; export default class FeatureInfoBox extends UIElement { private _tags: UIEventSource; @@ -16,6 +19,8 @@ export default class FeatureInfoBox extends UIElement { private _titleIcons: UIElement; private _renderings: UIElement[]; private _questionBox: UIElement; + private _returnToTheMap: UIElement; + private _tail: UIElement; constructor( tags: UIEventSource, @@ -28,10 +33,14 @@ export default class FeatureInfoBox extends UIElement { this._tags = tags; this._layerConfig = layerConfig; + this._returnToTheMap = Svg.back_svg().onClick(() => { + State.state.fullScreenMessage.setData(undefined); + State.state.selectedElement.setData(undefined); + }).SetClass("only-on-mobile") + .SetClass("featureinfobox-back-to-the-map") - this._title = layerConfig.title === undefined ? undefined : - new TagRenderingAnswer(tags, layerConfig.title) - .SetClass("featureinfobox-title"); + this._title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI")) + .SetClass("featureinfobox-title"); this._titleIcons = new Combine( layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon))) .SetClass("featureinfobox-icons"); @@ -54,16 +63,18 @@ export default class FeatureInfoBox extends UIElement { if (!questionBoxIsUsed) { this._renderings.push(questionBox); } + this._tail = new Combine([new Ornament()]).SetClass("only-on-mobile"); } InnerRender(): string { return new Combine([ - new Combine([this._title, this._titleIcons]) - .SetClass("featureinfobox-titlebar"), + new Combine([ + this._returnToTheMap, new Combine([this._title, this._titleIcons]).SetClass("featureinfobox-titlebar-title") + ]).SetClass("featureinfobox-titlebar"), new Combine([ ...this._renderings, this._questionBox, - new FixedUiElement("").SetClass("featureinfobox-tail") + this._tail.SetClass("featureinfobox-tail") ] ).SetClass("featureinfobox-content"), ]).SetClass("featureinfobox") diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index b700f3785..ea0571e3f 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -151,7 +151,6 @@ export default class ShowDataLayer { State.state.selectedElement.setData(feature); } this._onSelectedTrigger[feature.properties.id.replace("/","_")] = this._onSelectedTrigger[id]; - if (feature.properties.id.replace(/\//g, "_") === Hash.hash.data) { // This element is in the URL, so this is a share link // We already open it diff --git a/assets/svg/Ornament-Horiz-0.svg b/assets/svg/Ornament-Horiz-0.svg new file mode 100644 index 000000000..83fabd389 --- /dev/null +++ b/assets/svg/Ornament-Horiz-0.svg @@ -0,0 +1,91 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/assets/svg/Ornament-Horiz-1.svg b/assets/svg/Ornament-Horiz-1.svg new file mode 100644 index 000000000..1475dd7b8 --- /dev/null +++ b/assets/svg/Ornament-Horiz-1.svg @@ -0,0 +1,159 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/Ornament-Horiz-2.svg b/assets/svg/Ornament-Horiz-2.svg new file mode 100644 index 000000000..a5f18fec2 --- /dev/null +++ b/assets/svg/Ornament-Horiz-2.svg @@ -0,0 +1,96 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/assets/svg/Ornament-Horiz-3.svg b/assets/svg/Ornament-Horiz-3.svg new file mode 100644 index 000000000..c9a335625 --- /dev/null +++ b/assets/svg/Ornament-Horiz-3.svg @@ -0,0 +1,91 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/assets/svg/Ornament-Horiz-4.svg b/assets/svg/Ornament-Horiz-4.svg new file mode 100644 index 000000000..ade8558e6 --- /dev/null +++ b/assets/svg/Ornament-Horiz-4.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/assets/svg/Ornament-Horiz-5.svg b/assets/svg/Ornament-Horiz-5.svg new file mode 100644 index 000000000..9367c3e59 --- /dev/null +++ b/assets/svg/Ornament-Horiz-5.svg @@ -0,0 +1,81 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/assets/svg/Ornament-Horiz-6.svg b/assets/svg/Ornament-Horiz-6.svg new file mode 100644 index 000000000..b2f2e4e96 --- /dev/null +++ b/assets/svg/Ornament-Horiz-6.svg @@ -0,0 +1,301 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/back.svg b/assets/svg/back.svg new file mode 100644 index 000000000..ce2f36fe4 --- /dev/null +++ b/assets/svg/back.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/css/fullscreenmessagebox.css b/css/fullscreenmessagebox.css index 4f67377a6..e6d558bdd 100644 --- a/css/fullscreenmessagebox.css +++ b/css/fullscreenmessagebox.css @@ -1,5 +1,5 @@ .fullscreenmessage-content { - max-height: calc(100vh - var(--return-to-the-map-height)); + max-height: calc(100vh); height: 100%; overflow-y: auto; overflow-x: hidden; @@ -16,10 +16,11 @@ padding: 1em; top: var(--variable-title-height); /* 2em extra: padding from the title */ - max-height: calc(100vh - var(--variable-title-height) - var(--return-to-the-map-height) - 2em) !important; + max-height: calc(100vh - var(--variable-title-height)) !important; display: block; position: absolute; overflow-y: auto; + box-sizing: border-box; } .fullscreenmessage-content .featureinfobox-titlebar { @@ -35,29 +36,6 @@ } .fullscreenmessage-content .featureinfobox-tail { - display: block; - height: 1em; + /*THe ornament to give the URL bar some room...*/ } - -.to-the-map span { - font-size: xx-large; -} - -.to-the-map { - background: var(--catch-detail-color); - height: var(--return-to-the-map-height); - position: fixed; - z-index: 10000; - bottom: 0; - left: 0; - width: 100vw; - color: var(--catch-detail-color-contrast); - font-weight: bold; - pointer-events: all; - cursor: pointer; - padding-top: 1.2em; - text-align: center; - padding-bottom: 1.2em; - box-sizing: border-box; -} diff --git a/css/mobile.css b/css/mobile.css index 148f22779..f2986b7bb 100644 --- a/css/mobile.css +++ b/css/mobile.css @@ -3,7 +3,7 @@ Contains tweaks for small screens */ .only-on-mobile { - display: none; + display: none !important; background-color: var(--background-color); color: var(--foreground-color); } @@ -11,7 +11,7 @@ Contains tweaks for small screens @media only screen and (max-width: 600px), only screen and (max-height: 600px) { .only-on-mobile { - display: unset; + display: unset !important; background-color: var(--background-color); color: var(--foreground-color); } diff --git a/css/tagrendering.css b/css/tagrendering.css index acba2ae98..5a83eb010 100644 --- a/css/tagrendering.css +++ b/css/tagrendering.css @@ -1,17 +1,21 @@ .featureinfobox { display: flex; - flex-direction: column; + flex-direction: column; } .featureinfobox-title { - font-size: xx-large; + font-size: xx-large; + word-break: break-word; } -.featureinfobox-icons img{ + +.featureinfobox-icons img { max-height: 1.5em; width: 1.5em; } + .featureinfobox-icons { + margin-left: auto; } .featureinfobox-icons span { @@ -19,14 +23,45 @@ padding-right: 0.1em; } -.featureinfobox-titlebar{ +.featureinfobox-titlebar { + border-bottom: 2px solid var(--foreground-color); + box-shadow: 0 10px 10px -10px lightgray; + display: flex; + justify-content: space-between; +} + +.featureinfobox-titlebar-title { font-size: large; font-weight: bold; display: flex; justify-content: space-between; - border-bottom: 2px solid var(--foreground-color); + flex-wrap: wrap; + flex-grow: 2; + word-break: break-all; } +.featureinfobox-back-to-the-map { + padding: 0.5em; + border-radius: 999em; + margin-right: 0.4em; + width: 2em; + height: 2em; + background: var(--subtle-detail-color); + flex-shrink: 0; +} + +.featureinfobox-back-to-the-map svg { + width: 1.75em; + height: 1.75em; + margin-left: 0.15em; + margin-top: 0.15em +} + +.featureinfobox-back-to-the-map svg path{ + stroke: var(--subtle-detail-color-contrast) !important; +} + + .featureinfobox-content { display: block; max-height: 75vh; @@ -34,15 +69,16 @@ overflow-x: hidden; padding-top: 1em; } + @media only screen and (max-width: 600px), only screen and (max-height: 600px) { .featureinfobox-content { - display:block; + display: block; max-height: unset !important; overflow-y: auto; } } -@media only screen and (max-height: 600px) and (min-width: 600px){ +@media only screen and (max-height: 600px) and (min-width: 600px) { /* landscape mode: the first tagrendering of the infobox gets a special treatment and is placed on the right*/ .featureinfobox-content { position: relative; @@ -51,7 +87,7 @@ max-height: unset !important; height: 100vh; } - + .answer { max-width: 48% !important; padding-right: 0.3em; @@ -63,8 +99,8 @@ padding-right: 0.3em; box-sizing: border-box; } - - .first-rendering{ + + .first-rendering { position: absolute; left: 50%; width: 94%; @@ -129,7 +165,7 @@ border-radius: 0.5em; display: inline-block; width: 100%; - margin:0; + margin: 0; margin-left: -2em; box-sizing: border-box; padding: 0.5em; @@ -161,7 +197,7 @@ input:checked + label .question-option-with-border { .login-button-friendly { display: inline-block; - background-color:var(--catch-detail-color); + background-color: var(--catch-detail-color); color: var(--catch-detail-color-contrast); border: solid var(--catch-detail-color-contrast) 2px; padding: 0.2em 0.6em; @@ -204,7 +240,31 @@ input:checked + label .question-option-with-border { float: right; } -.edit-button svg path{ +.edit-button svg path { stroke: var(--foreground-color) !important; fill: var(--foreground-color) !important; -} \ No newline at end of file +} + + + +.to-the-map span { + font-size: xx-large; +} + +.to-the-map { + background: var(--catch-detail-color); + height: var(--return-to-the-map-height); + color: var(--catch-detail-color-contrast); + font-weight: bold; + pointer-events: all; + cursor: pointer; + padding-top: 0.4em; + text-align: center; + box-sizing: border-box; + display: block; + max-height: 2em; +} + +.to-the-map-inner{ + font-size: xx-large; +} diff --git a/index.css b/index.css index b7b7406c7..c82e5ea1e 100644 --- a/index.css +++ b/index.css @@ -2,7 +2,7 @@ --subtle-detail-color: #e5f5ff; --subtle-detail-color-contrast: black; --subtle-detail-color-light-contrast: lightgrey; - + --catch-detail-color: #3a3aeb; --catch-detail-color-contrast: white; --alert-color: #fee4d1; @@ -10,9 +10,8 @@ --foreground-color: black; --popup-border: white; --shadow-color: #00000066; - + --variable-title-height: 0px; /* Set by javascript */ --return-to-the-map-height: 5em; - --variable-title-height: 0px; /*Set by javascript dynamically*/ } html, body { @@ -40,8 +39,8 @@ a { stroke: var(--foreground-color) !important; } -.direction-svg svg path{ - fill: var(--catch-detail-color) !important; +.direction-svg svg path { + fill: var(--catch-detail-color) !important; } @@ -100,37 +99,38 @@ a { box-shadow: 0 0 10px var(--shadow-color); } -.single-layer-selection-toggle{ +.single-layer-selection-toggle { position: relative; - width: 2em; + width: 2em; height: 2em; } -.single-layer-selection-toggle img{ - max-height: 2em !important; - max-width: 2em !important; -} -.single-layer-selection-toggle svg{ - max-height:2em !important; +.single-layer-selection-toggle img { + max-height: 2em !important; max-width: 2em !important; } -.simple-add-ui-icon{ +.single-layer-selection-toggle svg { + max-height: 2em !important; + max-width: 2em !important; +} + +.simple-add-ui-icon { position: relative; display: block; - width: 3.5em; + width: 3.5em; height: 3.5em; padding-right: 0.3em; padding-left: 0.3em; } -.simple-add-ui-icon img{ - max-height:3.5em !important; +.simple-add-ui-icon img { + max-height: 3.5em !important; max-width: 3.5em !important; } -.simple-add-ui-icon svg{ - max-height:3.5em !important; +.simple-add-ui-icon svg { + max-height: 3.5em !important; max-width: 3.5em !important; } @@ -551,7 +551,7 @@ a { height: 2.5em; width: 2.5em; box-sizing: border-box; - padding:0; + padding: 0; } .share-button svg { @@ -563,12 +563,34 @@ a { stroke: var(--subtle-detail-color-contrast) !important; } -.share-button svg path{ +.share-button svg path { fill: var(--subtle-detail-color-contrast) !important; stroke: var(--subtle-detail-color-contrast) !important; } -.share-button svg circle{ +.share-button svg circle { fill: var(--subtle-detail-color-contrast) !important; stroke: var(--subtle-detail-color-contrast) !important; } + + +.ornament { + padding-top: 1em; + padding-bottom: 1em; + display: flex; + justify-content: center; + box-sizing: border-box; +} + +.ornament svg { + height: 2.5em; + width: 100%; +} + +.ornament svg path { + stroke: var(--subtle-detail-color-light-contrast); +} + +.ornament svg polygon { + fill: var(--subtle-detail-color-light-contrast); +} \ No newline at end of file From 3f2608524e0640b8af45cd80f80e2b6a9acd6fd0 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 7 Jan 2021 05:10:27 +0100 Subject: [PATCH 10/25] Last css tweaks --- UI/Base/Ornament.ts | 7 +++++-- css/tagrendering.css | 5 ++++- index.css | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/UI/Base/Ornament.ts b/UI/Base/Ornament.ts index e4057bf81..2a4ac175b 100644 --- a/UI/Base/Ornament.ts +++ b/UI/Base/Ornament.ts @@ -12,7 +12,7 @@ export default class Ornament extends UIElement { this.SetClass("ornament") const self = this; this.onClick(() => { - self._index.setData((self._index.data + 1) % Ornament.ornamentsCount); + self._index.setData((self._index.data + 1) % (Ornament.ornamentsCount + 1)); }) } @@ -28,7 +28,10 @@ export default class Ornament extends UIElement { } InnerRender(): string { - return Svg.All[`Ornament-Horiz-${this._index.data}.svg`] + if(this._index.data == 0){ + return "" + } + return Svg.All[`Ornament-Horiz-${this._index.data - 1}.svg`] } } \ No newline at end of file diff --git a/css/tagrendering.css b/css/tagrendering.css index 5a83eb010..ffab21548 100644 --- a/css/tagrendering.css +++ b/css/tagrendering.css @@ -262,7 +262,10 @@ input:checked + label .question-option-with-border { text-align: center; box-sizing: border-box; display: block; - max-height: 2em; + max-height: var(--return-to-the-map-height); + position: fixed; + width: 100vw; + bottom: 0; } .to-the-map-inner{ diff --git a/index.css b/index.css index c82e5ea1e..74584f272 100644 --- a/index.css +++ b/index.css @@ -11,7 +11,7 @@ --popup-border: white; --shadow-color: #00000066; --variable-title-height: 0px; /* Set by javascript */ - --return-to-the-map-height: 5em; + --return-to-the-map-height: 2em; } html, body { From 8e55799c4f27367b103e1dbcad8662e571bd9da1 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 7 Jan 2021 05:11:07 +0100 Subject: [PATCH 11/25] Version bump --- Models/Constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Models/Constants.ts b/Models/Constants.ts index ae7cd4431..a7d326ea9 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.2"; + public static vNumber = "0.4.0"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { From 7dc7cc283d9b3fb4f172a09ea9a2699f5ecf0224 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 7 Jan 2021 15:19:50 +0100 Subject: [PATCH 12/25] Add return to map button on layer panel --- Svg.ts | 10 +++++----- UI/BigComponents/LayerControlPanel.ts | 12 ++++++++++++ UI/BigComponents/LayerSelection.ts | 3 +++ css/tagrendering.css | 1 - 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Svg.ts b/Svg.ts index 3809edf38..246569d6d 100644 --- a/Svg.ts +++ b/Svg.ts @@ -4,7 +4,7 @@ import {FixedUiElement} from "./UI/Base/FixedUiElement"; export default class Svg { - public static Ornament_Horiz_0 = " image/svg+xml " + public static Ornament_Horiz_0 = " image/svg+xml " public static Ornament_Horiz_0_img = Img.AsImageElement(Svg.Ornament_Horiz_0) public static Ornament_Horiz_0_svg() { return new FixedUiElement(Svg.Ornament_Horiz_0);} public static Ornament_Horiz_0_ui() { return new FixedUiElement(Svg.Ornament_Horiz_0_img);} @@ -14,17 +14,17 @@ export default class Svg { public static Ornament_Horiz_1_svg() { return new FixedUiElement(Svg.Ornament_Horiz_1);} public static Ornament_Horiz_1_ui() { return new FixedUiElement(Svg.Ornament_Horiz_1_img);} - public static Ornament_Horiz_2 = " image/svg+xml " + public static Ornament_Horiz_2 = " image/svg+xml " public static Ornament_Horiz_2_img = Img.AsImageElement(Svg.Ornament_Horiz_2) public static Ornament_Horiz_2_svg() { return new FixedUiElement(Svg.Ornament_Horiz_2);} public static Ornament_Horiz_2_ui() { return new FixedUiElement(Svg.Ornament_Horiz_2_img);} - public static Ornament_Horiz_3 = " image/svg+xml " + public static Ornament_Horiz_3 = " image/svg+xml " public static Ornament_Horiz_3_img = Img.AsImageElement(Svg.Ornament_Horiz_3) public static Ornament_Horiz_3_svg() { return new FixedUiElement(Svg.Ornament_Horiz_3);} public static Ornament_Horiz_3_ui() { return new FixedUiElement(Svg.Ornament_Horiz_3_img);} - public static Ornament_Horiz_4 = " image/svg+xml " + public static Ornament_Horiz_4 = " image/svg+xml " public static Ornament_Horiz_4_img = Img.AsImageElement(Svg.Ornament_Horiz_4) public static Ornament_Horiz_4_svg() { return new FixedUiElement(Svg.Ornament_Horiz_4);} public static Ornament_Horiz_4_ui() { return new FixedUiElement(Svg.Ornament_Horiz_4_img);} @@ -34,7 +34,7 @@ export default class Svg { public static Ornament_Horiz_5_svg() { return new FixedUiElement(Svg.Ornament_Horiz_5);} public static Ornament_Horiz_5_ui() { return new FixedUiElement(Svg.Ornament_Horiz_5_img);} - public static Ornament_Horiz_6 = " image/svg+xml " + public static Ornament_Horiz_6 = " image/svg+xml " public static Ornament_Horiz_6_img = Img.AsImageElement(Svg.Ornament_Horiz_6) public static Ornament_Horiz_6_svg() { return new FixedUiElement(Svg.Ornament_Horiz_6);} public static Ornament_Horiz_6_ui() { return new FixedUiElement(Svg.Ornament_Horiz_6_img);} diff --git a/UI/BigComponents/LayerControlPanel.ts b/UI/BigComponents/LayerControlPanel.ts index 8b90df064..f52acc1bc 100644 --- a/UI/BigComponents/LayerControlPanel.ts +++ b/UI/BigComponents/LayerControlPanel.ts @@ -3,6 +3,7 @@ import State from "../../State"; import BackgroundSelector from "./BackgroundSelector"; import LayerSelection from "./LayerSelection"; import Combine from "../Base/Combine"; +import Translations from "../i18n/Translations"; export default class LayerControlPanel extends UIElement{ private readonly _panel: UIElement; @@ -23,6 +24,17 @@ export default class LayerControlPanel extends UIElement{ layerSelection.onClick(() => { }); layerControlPanel = new Combine([layerSelection, "
", layerControlPanel]); } + + + const backButton = new Combine([ + new Combine([Translations.t.general.returnToTheMap.Clone().SetClass("to-the-map")]) + .SetClass("to-the-map-inner") + + ]).SetClass("only-on-mobile") + .onClick(() => State.state.fullScreenMessage.setData(undefined)); + + layerControlPanel = new Combine([layerControlPanel, backButton]); + this._panel = layerControlPanel; } diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index 3766bdf09..f914a18bc 100644 --- a/UI/BigComponents/LayerSelection.ts +++ b/UI/BigComponents/LayerSelection.ts @@ -7,6 +7,9 @@ import Combine from "../Base/Combine"; import {FixedUiElement} from "../Base/FixedUiElement"; import Translations from "../i18n/Translations"; +/** + * Shows the panel with all layers and a toggle for each of them + */ export default class LayerSelection extends UIElement { private readonly _checkboxes: UIElement[]; diff --git a/css/tagrendering.css b/css/tagrendering.css index ffab21548..bab0741ca 100644 --- a/css/tagrendering.css +++ b/css/tagrendering.css @@ -35,7 +35,6 @@ font-weight: bold; display: flex; justify-content: space-between; - flex-wrap: wrap; flex-grow: 2; word-break: break-all; } From 9a412c6b7458031c3c0fd45f58be26204d95abac Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 7 Jan 2021 20:11:07 +0100 Subject: [PATCH 13/25] Workaround: popups move into zoom vertically too now on desktop --- Models/Constants.ts | 2 +- UI/Base/LazyElement.ts | 6 ++++-- UI/BigComponents/LayerControlPanel.ts | 19 ++++++++++--------- UI/ShowDataLayer.ts | 3 ++- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Models/Constants.ts b/Models/Constants.ts index a7d326ea9..24d7f72cb 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.4.0"; + public static vNumber = "0.4.2"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/UI/Base/LazyElement.ts b/UI/Base/LazyElement.ts index c153d80bd..9796a6c5c 100644 --- a/UI/Base/LazyElement.ts +++ b/UI/Base/LazyElement.ts @@ -6,9 +6,11 @@ export default class LazyElement extends UIElement { private _content: UIElement = undefined; public Activate: () => void; + private _loadingContent: string; - constructor(content: (() => UIElement)) { + constructor(content: (() => UIElement), loadingContent = "Rendering...") { super(); + this._loadingContent = loadingContent; this.dumbMode = false; const self = this; this.Activate = () => { @@ -21,7 +23,7 @@ export default class LazyElement extends UIElement { InnerRender(): string { if (this._content === undefined) { - return "Rendering..."; + return this._loadingContent; } return this._content.InnerRender(); } diff --git a/UI/BigComponents/LayerControlPanel.ts b/UI/BigComponents/LayerControlPanel.ts index f52acc1bc..2330beec2 100644 --- a/UI/BigComponents/LayerControlPanel.ts +++ b/UI/BigComponents/LayerControlPanel.ts @@ -5,10 +5,10 @@ import LayerSelection from "./LayerSelection"; import Combine from "../Base/Combine"; import Translations from "../i18n/Translations"; -export default class LayerControlPanel extends UIElement{ +export default class LayerControlPanel extends UIElement { private readonly _panel: UIElement; - - + + constructor() { super(); let layerControlPanel: UIElement = undefined; @@ -21,7 +21,8 @@ export default class LayerControlPanel extends UIElement{ if (State.state.filteredLayers.data.length > 1) { const layerSelection = new LayerSelection(); - layerSelection.onClick(() => { }); + layerSelection.onClick(() => { + }); layerControlPanel = new Combine([layerSelection, "
", layerControlPanel]); } @@ -32,14 +33,14 @@ export default class LayerControlPanel extends UIElement{ ]).SetClass("only-on-mobile") .onClick(() => State.state.fullScreenMessage.setData(undefined)); - - layerControlPanel = new Combine([layerControlPanel, backButton]); - + + layerControlPanel = new Combine([layerControlPanel , backButton]); + this._panel = layerControlPanel; } - + InnerRender(): string { return this._panel.Render(); } - + } \ No newline at end of file diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index ea0571e3f..0c9c39b44 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -124,7 +124,8 @@ export default class ShowDataLayer { const tags = State.state.allElements.getEventSourceFor(feature); - const uiElement: LazyElement = new LazyElement(() => new FeatureInfoBox(tags, layer)); + const uiElement: LazyElement = new LazyElement(() => new FeatureInfoBox(tags, layer), + "
Rendering
"); popup.setContent(uiElement.Render()); popup.on('remove', () => { State.state.selectedElement.setData(undefined); From 77ffdc093af6a8b89bbaac3ff9c6450e11d30d71 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 8 Jan 2021 02:13:44 +0100 Subject: [PATCH 14/25] More styling tweaks --- AllTranslationAssets.ts | 4 +- Models/Constants.ts | 2 +- UI/Base/ScrollableFullScreen.ts | 34 ++++++++++++++++ UI/BigComponents/LayerControlPanel.ts | 15 +++---- UI/BigComponents/LayerSelection.ts | 4 +- UI/Popup/FeatureInfoBox.ts | 56 ++++++++++----------------- assets/translations.json | 21 ++++++---- css/tagrendering.css | 7 +++- 8 files changed, 85 insertions(+), 58 deletions(-) create mode 100644 UI/Base/ScrollableFullScreen.ts diff --git a/AllTranslationAssets.ts b/AllTranslationAssets.ts index da4822734..d2b4dd4b1 100644 --- a/AllTranslationAssets.ts +++ b/AllTranslationAssets.ts @@ -92,7 +92,9 @@ export default class AllTranslationAssets { customThemeIntro: new Translation( {"en":"

Custom themes

These are previously visited user-generated themes.","nl":"

Onofficiële thema's

De onderstaande thema's heb je eerder bezocht en zijn gemaakt door andere OpenStreetMappers.","fr":"

Thèmes personnalisés

Vous avez déjà visité ces thèmes personnalisés.","gl":"

Temas personalizados

Estes son temas xerados por usuarios previamente visitados.","de":"

Kundenspezifische Themen

Dies sind zuvor besuchte benutzergenerierte Themen"} ), aboutMapcomplete: new Translation( {"en":"

About MapComplete

MapComplete is an OpenStreetMap editor that is meant to help everyone to easily add information on a single theme.

Only features relevant to a single theme are shown with a few predefined questions, in order to keep things simple and extremly user-friendly.The theme maintainer can also choose a language for the interface, choose to disable elements or even to embed it into a different website without any UI-element at all.

However, another important part of MapComplete is to always offer the next step to learn more about OpenStreetMap:

  • An iframe without UI-elements will link to a full-screen version
  • The fullscreen version offers information about OpenStreetMap
  • If you're not logged in, you're asked to log in
  • If you answered a single question, you are allowed to add points
  • At a certain point, the actual added tags appear which later get linked to the wiki...

Do you notice an issue with MapComplete? Do you have a feature request? Do you want to help translating? Head over to the source code or issue tracker. Follow the edit count on OsmCha

","nl":"

Over MapComplete

MapComplete is een OpenStreetMap-editor om eenvoudig informatie toe te voegen over één enkel onderwerp.

Om de editor zo simpel en gebruiksvriendelijk mogelijk te houden, worden enkel objecten relevant voor het thema getoond.Voor deze objecten kunnen dan vragen beantwoord worden, of men kan een nieuw punt van dit thema toevoegen.De maker van het thema kan er ook voor opteren om een aantal elementen van de gebruikersinterface uit te schakelen of de taal ervan in te stellen.

Een ander belangrijk aspect is om bezoekers stap voor stap meer te leren over OpenStreetMap:

  • Een iframe zonder verdere uitleg linkt naar de volledige versie van MapComplete
  • De volledige versie heeft uitleg over OpenStreetMap
  • Als je niet aangemeld bent, wordt er je gevraagd dit te doen
  • Als je minstens één vraag hebt beantwoord, kan je punten gaan toevoegen.
  • Heb je genoeg changesets, dan verschijnen de tags die wat later doorlinken naar de wiki

Merk je een bug of wil je een extra feature? Wil je helpen vertalen? Bezoek dan de broncode en issue tracker. Volg de edits op OsmCha

","de":"

Über MapComplete

MapComplete ist ein OpenStreetMap-Editor, der jedem helfen soll, auf einfache Weise Informationen zu einem Einzelthema hinzuzufügen.

Nur Merkmale, die für ein einzelnes Thema relevant sind, werden mit einigen vordefinierten Fragen gezeigt, um die Dinge einfach und extrem benutzerfreundlich zu halten.Der Themen-Betreuer kann auch eine Sprache für die Schnittstelle wählen, Elemente deaktivieren oder sogar in eine andere Website ohne jegliches UI-Element einbetten.

Ein weiterer wichtiger Teil von MapComplete ist jedoch, immer den nächsten Schritt anzubietenum mehr über OpenStreetMap zu erfahren:

  • Ein iframe ohne UI-Elemente verlinkt zu einer Vollbildversion
  • Die Vollbildversion bietet Informationen über OpenStreetMap
  • Wenn Sie nicht eingeloggt sind, werden Sie gebeten, sich einzuloggen
  • Wenn Sie eine einzige Frage beantwortet haben, dürfen Sie Punkte hinzufügen
  • An einem bestimmten Punkt erscheinen die tatsächlich hinzugefügten Tags, die später mit dem Wiki verlinkt werden...

Fällt Ihnen ein Problem mit MapComplete auf? Haben Sie einen Feature-Wunsch? Wollen Sie beim Übersetzen helfen? Gehen Sie zum Quellcode oder zur Problemverfolgung.

"} ), backgroundMap: new Translation( {"en":"Background map","ca":"Mapa de fons","es":"Mapa de fondo","nl":"Achtergrondkaart","fr":"Carte de fonds","de":"Hintergrundkarte"} ), - zoomInToSeeThisLayer: new Translation( {"en":"Zoom in to see this layer","ca":"Amplia per veure aquesta capa","es":"Amplía para ver esta capa","nl":"Vergroot de kaart om deze laag te zien","fr":"Aggrandissez la carte pour voir cette couche","de":"Vergrößern, um diese Ebene zu sehen"} ), + layerSelection: { zoomInToSeeThisLayer: new Translation( {"en":"Zoom in to see this layer","ca":"Amplia per veure aquesta capa","es":"Amplía para ver esta capa","nl":"Vergroot de kaart om deze laag te zien","fr":"Aggrandissez la carte pour voir cette couche","de":"Vergrößern, um diese Ebene zu sehen"} ), + title: new Translation( {"en":"Select layers","nl":"Selecteer lagen"} ), +}, weekdays: { abbreviations: { monday: new Translation( {"en":"Mon","ca":"Dil","es":"Lun","nl":"Maan","fr":"Lun"} ), tuesday: new Translation( {"en":"Tue","ca":"Dim","es":"Mar","nl":"Din","fr":"Mar"} ), wednesday: new Translation( {"en":"Wed","ca":"Dic","es":"Mie","nl":"Woe","fr":"Mer"} ), diff --git a/Models/Constants.ts b/Models/Constants.ts index 24d7f72cb..106bec851 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.4.2"; + public static vNumber = "0.4.3"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/UI/Base/ScrollableFullScreen.ts b/UI/Base/ScrollableFullScreen.ts new file mode 100644 index 000000000..f5770d70a --- /dev/null +++ b/UI/Base/ScrollableFullScreen.ts @@ -0,0 +1,34 @@ +import {UIElement} from "../UIElement"; +import Svg from "../../Svg"; +import State from "../../State"; +import Combine from "./Combine"; + +/** + * Wraps some contents into a panel that scrolls the content _under_ the title + */ +export default class ScrollableFullScreen extends UIElement{ + private _component: Combine; + + + constructor(title: UIElement, content: UIElement) { + super(); + const returnToTheMap = Svg.back_svg().onClick(() => { + State.state.fullScreenMessage.setData(undefined); + State.state.selectedElement.setData(undefined); + }).SetClass("only-on-mobile") + .SetClass("featureinfobox-back-to-the-map") + + this._component = new Combine([ + new Combine([returnToTheMap, title]).SetClass("featureinfobox-titlebar"), + new Combine([content]).SetClass("featureinfobox-content") + ]) + this.SetClass("featureinfobox"); + + } + + InnerRender(): string { + return this._component.Render(); + } + + +} \ No newline at end of file diff --git a/UI/BigComponents/LayerControlPanel.ts b/UI/BigComponents/LayerControlPanel.ts index 2330beec2..76f7ac4cf 100644 --- a/UI/BigComponents/LayerControlPanel.ts +++ b/UI/BigComponents/LayerControlPanel.ts @@ -3,6 +3,8 @@ import State from "../../State"; import BackgroundSelector from "./BackgroundSelector"; import LayerSelection from "./LayerSelection"; import Combine from "../Base/Combine"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import Translations from "../i18n/Translations"; export default class LayerControlPanel extends UIElement { @@ -11,7 +13,7 @@ export default class LayerControlPanel extends UIElement { constructor() { super(); - let layerControlPanel: UIElement = undefined; + let layerControlPanel: UIElement = new FixedUiElement(""); if (State.state.layoutToUse.data.enableBackgroundLayerSelection) { layerControlPanel = new BackgroundSelector(); layerControlPanel.SetStyle("margin:1em"); @@ -27,16 +29,9 @@ export default class LayerControlPanel extends UIElement { } - const backButton = new Combine([ - new Combine([Translations.t.general.returnToTheMap.Clone().SetClass("to-the-map")]) - .SetClass("to-the-map-inner") + const title =Translations.t.general.layerSelection.title.SetClass("featureinfobox-title") - ]).SetClass("only-on-mobile") - .onClick(() => State.state.fullScreenMessage.setData(undefined)); - - layerControlPanel = new Combine([layerControlPanel , backButton]); - - this._panel = layerControlPanel; + this._panel = new ScrollableFullScreen(title, layerControlPanel); } InnerRender(): string { diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index f914a18bc..f38b8ca5b 100644 --- a/UI/BigComponents/LayerSelection.ts +++ b/UI/BigComponents/LayerSelection.ts @@ -36,14 +36,14 @@ export default class LayerSelection extends UIElement { const zoomStatus = new VariableUiElement(State.state.locationControl.map(location => { if (location.zoom < layer.layerDef.minzoom) { - return Translations.t.general.zoomInToSeeThisLayer + return Translations.t.general.layerSelection.zoomInToSeeThisLayer .SetClass("alert") .SetStyle("display: block ruby;width:min-content;") .Render(); } return "" })) - const style = "display:flex;align-items:center;" + const style = "display:flex;align-items:center;flex-wrap:wrap;" this._checkboxes.push(new CheckBox( new Combine([icon, name, zoomStatus]).SetStyle(style), new Combine([iconUnselected, "", name, "", zoomStatus]).SetStyle(style), diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index f97e97a27..01c29e649 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -10,17 +10,10 @@ import {FixedUiElement} from "../Base/FixedUiElement"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import Svg from "../../Svg"; import Ornament from "../Base/Ornament"; +import ScrollableFullScreen from "../Base/ScrollableFullScreen"; export default class FeatureInfoBox extends UIElement { - private _tags: UIEventSource; - private _layerConfig: LayerConfig; - - private _title: UIElement; - private _titleIcons: UIElement; - private _renderings: UIElement[]; - private _questionBox: UIElement; - private _returnToTheMap: UIElement; - private _tail: UIElement; + private _component: UIElement; constructor( tags: UIEventSource, @@ -30,18 +23,11 @@ export default class FeatureInfoBox extends UIElement { if (layerConfig === undefined) { throw "Undefined layerconfig" } - this._tags = tags; - this._layerConfig = layerConfig; - this._returnToTheMap = Svg.back_svg().onClick(() => { - State.state.fullScreenMessage.setData(undefined); - State.state.selectedElement.setData(undefined); - }).SetClass("only-on-mobile") - .SetClass("featureinfobox-back-to-the-map") - this._title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI")) + const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI")) .SetClass("featureinfobox-title"); - this._titleIcons = new Combine( + const titleIcons = new Combine( layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon))) .SetClass("featureinfobox-icons"); @@ -51,7 +37,7 @@ export default class FeatureInfoBox extends UIElement { } let questionBoxIsUsed = false; - this._renderings = layerConfig.tagRenderings.map(tr => { + const renderings = layerConfig.tagRenderings.map(tr => { if (tr.question === null) { questionBoxIsUsed = true; // This is the question box! @@ -59,27 +45,27 @@ export default class FeatureInfoBox extends UIElement { } return new EditableTagRendering(tags, tr); }); - this._renderings[0]?.SetClass("first-rendering"); + renderings[0]?.SetClass("first-rendering"); if (!questionBoxIsUsed) { - this._renderings.push(questionBox); + renderings.push(questionBox); } - this._tail = new Combine([new Ornament()]).SetClass("only-on-mobile"); + const tail = new Combine([new Ornament()]).SetClass("only-on-mobile"); + + const content = new Combine([ + ...renderings, + questionBox, + tail.SetClass("featureinfobox-tail") + ] + ) + const titleBar = new Combine([ + new Combine([title, titleIcons]).SetClass("featureinfobox-titlebar-title") + ]) + + this._component = new ScrollableFullScreen(titleBar, content) } InnerRender(): string { - return new Combine([ - new Combine([ - this._returnToTheMap, new Combine([this._title, this._titleIcons]).SetClass("featureinfobox-titlebar-title") - ]).SetClass("featureinfobox-titlebar"), - new Combine([ - ...this._renderings, - this._questionBox, - this._tail.SetClass("featureinfobox-tail") - ] - ).SetClass("featureinfobox-content"), - ]).SetClass("featureinfobox") - .Render(); + return this._component.Render(); } - } diff --git a/assets/translations.json b/assets/translations.json index b5be39a60..65dc28304 100644 --- a/assets/translations.json +++ b/assets/translations.json @@ -744,14 +744,21 @@ "fr": "Carte de fonds", "de": "Hintergrundkarte" }, - "zoomInToSeeThisLayer": { - "en": "Zoom in to see this layer", - "ca": "Amplia per veure aquesta capa", - "es": "Amplía para ver esta capa", - "nl": "Vergroot de kaart om deze laag te zien", - "fr": "Aggrandissez la carte pour voir cette couche", - "de": "Vergrößern, um diese Ebene zu sehen" + "layerSelection": { + "zoomInToSeeThisLayer": { + "en": "Zoom in to see this layer", + "ca": "Amplia per veure aquesta capa", + "es": "Amplía para ver esta capa", + "nl": "Vergroot de kaart om deze laag te zien", + "fr": "Aggrandissez la carte pour voir cette couche", + "de": "Vergrößern, um diese Ebene zu sehen" + }, + "title": { + "en": "Select layers", + "nl": "Selecteer lagen" + } }, + "weekdays": { "abbreviations": { "monday": { diff --git a/css/tagrendering.css b/css/tagrendering.css index bab0741ca..f99be5789 100644 --- a/css/tagrendering.css +++ b/css/tagrendering.css @@ -25,9 +25,11 @@ .featureinfobox-titlebar { border-bottom: 2px solid var(--foreground-color); - box-shadow: 0 10px 10px -10px lightgray; + box-shadow: 0 10px 10px -10px var(--shadow-color); display: flex; justify-content: space-between; + max-width: 95vw; + overflow-x: hidden; } .featureinfobox-titlebar-title { @@ -65,8 +67,9 @@ display: block; max-height: 75vh; overflow-y: auto; - overflow-x: hidden; padding-top: 1em; + max-width: 95vw; + overflow-x: hidden; } @media only screen and (max-width: 600px), only screen and (max-height: 600px) { From 2a31badd3d8dbbf8ebfb065620c820a0d1303a40 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 8 Jan 2021 03:57:18 +0100 Subject: [PATCH 15/25] New roaming rendering system which allows layers to push questions, badges and title-icons to all the other layers, improve bike-clean-services --- Customizations/JSON/LayerConfig.ts | 139 +++++---- Customizations/JSON/LayoutConfig.ts | 18 +- Customizations/JSON/TagRenderingConfig.ts | 37 ++- Customizations/JSON/TagRenderingConfigJson.ts | 6 + Customizations/SharedLayers.ts | 2 +- UI/CustomGenerator/AllLayersPanel.ts | 2 +- UI/CustomGenerator/TagRenderingPanel.ts | 2 +- UI/CustomGenerator/TagRenderingPreview.ts | 2 +- UI/Popup/FeatureInfoBox.ts | 3 +- UI/Popup/TagRenderingQuestion.ts | 2 +- UI/UIElement.ts | 6 - .../layers/bike_cleaning/bike_cleaning.json | 81 ++++- .../bike_cleaning/bike_cleaning_icon.svg | 204 ++++++++++++ assets/layers/bike_shop/bike_cleaning.svg | 292 ------------------ assets/layers/bike_shop/bike_shop.json | 9 - test/Tag.spec.ts | 16 +- 16 files changed, 427 insertions(+), 394 deletions(-) create mode 100644 assets/layers/bike_cleaning/bike_cleaning_icon.svg delete mode 100644 assets/layers/bike_shop/bike_cleaning.svg diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 79891f166..53825613d 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -19,22 +19,18 @@ import {UIElement} from "../../UI/UIElement"; export default class LayerConfig { + static WAYHANDLING_DEFAULT = 0; + static WAYHANDLING_CENTER_ONLY = 1; + static WAYHANDLING_CENTER_AND_WAY = 2; id: string; - name: Translation - description: Translation; overpassTags: TagsFilter; doNotDownload: boolean; - passAllFeatures: boolean; - minzoom: number; - title?: TagRenderingConfig; - titleIcons: TagRenderingConfig[]; - icon: TagRenderingConfig; iconOverlays: { if: TagsFilter, then: TagRenderingConfig, badge: boolean }[] iconSize: TagRenderingConfig; @@ -42,14 +38,7 @@ export default class LayerConfig { color: TagRenderingConfig; width: TagRenderingConfig; dashArray: TagRenderingConfig; - - wayHandling: number; - - static WAYHANDLING_DEFAULT = 0; - static WAYHANDLING_CENTER_ONLY = 1; - static WAYHANDLING_CENTER_AND_WAY = 2; - hideUnderlayingFeaturesMinPercentage?: number; presets: { @@ -60,10 +49,10 @@ export default class LayerConfig { tagRenderings: TagRenderingConfig []; - constructor(json: LayerConfigJson, roamingRenderings: TagRenderingConfig[], + constructor(json: LayerConfigJson, context?: string) { context = context + "." + json.id; - + const self = this; this.id = json.id; this.name = Translations.T(json.name); this.description = Translations.T(json.description); @@ -81,6 +70,28 @@ export default class LayerConfig { })) + /** Given a key, gets the corresponding property from the json (or the default if not found + * + * The found value is interpreted as a tagrendering and fetched/parsed + * */ + function tr(key: string, deflt) { + const v = json[key]; + if (v === undefined || v === null) { + if (deflt === undefined) { + return undefined; + } + return new TagRenderingConfig(deflt, self.overpassTags, `${context}.${key}.default value`); + } + if (typeof v === "string") { + const shared = SharedTagRenderings.SharedTagRendering[v]; + if (shared) { + console.log("Got shared TR:", v, "-->", shared) + return shared; + } + } + return new TagRenderingConfig(v, self.overpassTags, `${context}.${key}`); + } + /** * Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig * A string is interpreted as a name to call @@ -90,26 +101,27 @@ export default class LayerConfig { if (tagRenderings === undefined) { return []; } + return tagRenderings.map( (renderingJson, i) => { if (typeof renderingJson === "string") { - - if(renderingJson === "questions"){ - return new TagRenderingConfig("questions") + + if (renderingJson === "questions") { + return new TagRenderingConfig("questions", undefined) } - - + + const shared = SharedTagRenderings.SharedTagRendering[renderingJson]; if (shared !== undefined) { return shared; } throw `Predefined tagRendering ${renderingJson} not found in ${context}`; } - return new TagRenderingConfig(renderingJson, `${context}.tagrendering[${i}]`); + return new TagRenderingConfig(renderingJson, self.overpassTags, `${context}.tagrendering[${i}]`); }); } - this.tagRenderings = trs(json.tagRenderings).concat(roamingRenderings); + this.tagRenderings = trs(json.tagRenderings); const titleIcons = []; @@ -125,29 +137,10 @@ export default class LayerConfig { this.titleIcons = trs(titleIcons); - function tr(key, deflt) { - const v = json[key]; - if (v === undefined || v === null) { - if (deflt === undefined) { - return undefined; - } - return new TagRenderingConfig(deflt); - } - if (typeof v === "string") { - const shared = SharedTagRenderings.SharedTagRendering[v]; - if (shared) { - console.log("Got shared TR:", v, "-->", shared) - return shared; - } - } - return new TagRenderingConfig(v, context + "." + key); - } - - this.title = tr("title", undefined); this.icon = tr("icon", Img.AsData(Svg.pin)); - this.iconOverlays = (json.iconOverlays ?? []).map(overlay => { - let tr = new TagRenderingConfig(overlay.then); + this.iconOverlays = (json.iconOverlays ?? []).map((overlay, i) => { + let tr = new TagRenderingConfig(overlay.then, self.overpassTags, `iconoverlays.${i}`); if (typeof overlay.then === "string" && SharedTagRenderings.SharedIcons[overlay.then] !== undefined) { tr = SharedTagRenderings.SharedIcons[overlay.then]; } @@ -175,18 +168,54 @@ export default class LayerConfig { } + public AddRoamingRenderings(addAll: { + tagRenderings: TagRenderingConfig[], + titleIcons: TagRenderingConfig[], + iconOverlays: { "if": TagsFilter, then: TagRenderingConfig, badge: boolean }[] + + }): LayerConfig { + this.tagRenderings.push(...addAll.tagRenderings); + this.iconOverlays.push(...addAll.iconOverlays); + for (const icon of addAll.titleIcons) { + console.log("Adding ",icon, "to", this.id) + this.titleIcons.splice(0,0, icon); + } + return this; + } + + public GetRoamingRenderings(): { + tagRenderings: TagRenderingConfig[], + titleIcons: TagRenderingConfig[], + iconOverlays: { "if": TagsFilter, then: TagRenderingConfig, badge: boolean }[] + + } { + + const tagRenderings = this.tagRenderings.filter(tr => tr.roaming); + const titleIcons = this.titleIcons.filter(tr => tr.roaming); + const iconOverlays = this.iconOverlays.filter(io => io.then.roaming) + + return { + tagRenderings: tagRenderings, + titleIcons: titleIcons, + iconOverlays: iconOverlays + } + + } + public GenerateLeafletStyle(tags: UIEventSource, clickable: boolean): { - color: string; - icon: { - iconUrl: string, - popupAnchor: [number, number]; - iconAnchor: [number, number]; - iconSize: [number, number]; - html: UIElement; - className?: string; - }; - weight: number; dashArray: number[] + icon: + { + html: UIElement, + iconSize: [number, number], + iconAnchor: [number, number], + popupAnchor: [number, number], + iconUrl: string, + className: string + }, + color: string, + weight: number, + dashArray: number[] } { function num(str, deflt = 40) { @@ -259,7 +288,7 @@ export default class LayerConfig { if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { html = new Combine([ (Svg.All[match[1] + ".svg"] as string) - .replace(/#000000/g, match[2]) + .replace(/#000000/g, match[2]) ]).SetStyle(style); } return html; diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts index 23a2264b5..81be65c8e 100644 --- a/Customizations/JSON/LayoutConfig.ts +++ b/Customizations/JSON/LayoutConfig.ts @@ -75,7 +75,7 @@ export default class LayoutConfig { return SharedTagRenderings.SharedTagRendering[tr]; } } - return new TagRenderingConfig(tr, `${this.id}.roaming_renderings[${i}]`); + return new TagRenderingConfig(tr, undefined,`${this.id}.roaming_renderings[${i}]`); } ); this.defaultBackgroundId = json.defaultBackgroundId; @@ -102,9 +102,23 @@ export default class LayoutConfig { } // @ts-ignore - return new LayerConfig(layer, this.roamingRenderings, `${this.id}.layers[${i}]`); + return new LayerConfig(layer, `${this.id}.layers[${i}]`) }); + + // ALl the layers are constructed, let them share tags in piece now! + const roaming : {r, source: LayerConfig}[] = [] + for (const layer of this.layers) { + roaming.push({r: layer.GetRoamingRenderings(), source:layer}); + } + for (const layer of this.layers) { + for (const r of roaming) { + if(r.source == layer){ + continue; + } + layer.AddRoamingRenderings(r.r); + } + } this.clustering = { maxZoom: 16, diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts index add0f72c2..5817bb5e2 100644 --- a/Customizations/JSON/TagRenderingConfig.ts +++ b/Customizations/JSON/TagRenderingConfig.ts @@ -1,4 +1,4 @@ -import {TagsFilter} from "../../Logic/Tags"; +import {And, TagsFilter} from "../../Logic/Tags"; import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; import Translations from "../../UI/i18n/Translations"; import {FromJSON} from "./FromJSON"; @@ -11,25 +11,26 @@ import {Translation} from "../../UI/i18n/Translation"; */ export default class TagRenderingConfig { - render?: Translation; - question?: Translation; - condition?: TagsFilter; + readonly render?: Translation; + readonly question?: Translation; + readonly condition?: TagsFilter; - freeform?: { - key: string, - type: string, - addExtraTags: TagsFilter[]; + readonly freeform?: { + readonly key: string, + readonly type: string, + readonly addExtraTags: TagsFilter[]; }; readonly multiAnswer: boolean; - mappings?: { - if: TagsFilter, - then: Translation - hideInAnswer: boolean | TagsFilter + readonly mappings?: { + readonly if: TagsFilter, + readonly then: Translation + readonly hideInAnswer: boolean | TagsFilter }[] + readonly roaming: boolean; - constructor(json: string | TagRenderingConfigJson, context?: string) { + constructor(json: string | TagRenderingConfigJson, conditionIfRoaming: TagsFilter, context?: string) { if (json === "questions") { // Very special value @@ -46,10 +47,16 @@ export default class TagRenderingConfig { this.multiAnswer = false; return; } - + this.render = Translations.T(json.render); this.question = Translations.T(json.question); - this.condition = FromJSON.Tag(json.condition ?? {"and": []}, `${context}.condition`); + this.roaming = json.roaming ?? false; + const condition = FromJSON.Tag(json.condition ?? {"and": []}, `${context}.condition`); + if (this.roaming && conditionIfRoaming !== undefined) { + this.condition = new And([condition, conditionIfRoaming]); + } else { + this.condition = condition; + } if (json.freeform) { this.freeform = { key: json.freeform.key, diff --git a/Customizations/JSON/TagRenderingConfigJson.ts b/Customizations/JSON/TagRenderingConfigJson.ts index 7f2541b2f..441530dfd 100644 --- a/Customizations/JSON/TagRenderingConfigJson.ts +++ b/Customizations/JSON/TagRenderingConfigJson.ts @@ -53,4 +53,10 @@ export interface TagRenderingConfigJson { then: string | any hideInAnswer?: boolean }[] + + /** + * If set to true, this tagRendering will escape the current layer and attach itself to all the other layers too. + * However, it will _only_ be shown if it matches the overpass-tags of the layer it was originally defined in. + */ + roaming?: boolean } \ No newline at end of file diff --git a/Customizations/SharedLayers.ts b/Customizations/SharedLayers.ts index 6aed204c7..72427998f 100644 --- a/Customizations/SharedLayers.ts +++ b/Customizations/SharedLayers.ts @@ -56,7 +56,7 @@ export default class SharedLayers { private static getSharedLayers(): Map { const sharedLayers = new Map(); for (const layer of SharedLayers.sharedLayersListRaw) { - const parsed = new LayerConfig(layer, [], "shared_layers") + const parsed = new LayerConfig(layer, "shared_layers") sharedLayers.set(layer.id, parsed); sharedLayers[layer.id] = parsed; } diff --git a/UI/CustomGenerator/AllLayersPanel.ts b/UI/CustomGenerator/AllLayersPanel.ts index 588af7f2e..dcf0c633c 100644 --- a/UI/CustomGenerator/AllLayersPanel.ts +++ b/UI/CustomGenerator/AllLayersPanel.ts @@ -68,7 +68,7 @@ export default class AllLayersPanel extends UIElement { const layer = config.layers[i]; if (typeof layer !== "string") { try { - const iconTagRendering = new TagRenderingConfig(layer.icon, "icon") + const iconTagRendering = new TagRenderingConfig(layer.icon, undefined, "icon") const icon = iconTagRendering.GetRenderValue({"id": "node/-1"}).txt; return `` } catch (e) { diff --git a/UI/CustomGenerator/TagRenderingPanel.ts b/UI/CustomGenerator/TagRenderingPanel.ts index 211400498..89b6cd3f2 100644 --- a/UI/CustomGenerator/TagRenderingPanel.ts +++ b/UI/CustomGenerator/TagRenderingPanel.ts @@ -111,7 +111,7 @@ export default class TagRenderingPanel extends InputElement { try{ - new TagRenderingConfig(json, options?.title ?? ""); + new TagRenderingConfig(json,undefined, options?.title ?? ""); return ""; }catch(e){ return ""+e+"" diff --git a/UI/CustomGenerator/TagRenderingPreview.ts b/UI/CustomGenerator/TagRenderingPreview.ts index a884b3493..d758682b1 100644 --- a/UI/CustomGenerator/TagRenderingPreview.ts +++ b/UI/CustomGenerator/TagRenderingPreview.ts @@ -40,7 +40,7 @@ export default class TagRenderingPreview extends UIElement { rendering = new VariableUiElement(es.map(tagRenderingConfig => { try { - const tr = new EditableTagRendering(self.previewTagValue, new TagRenderingConfig(tagRenderingConfig, "preview")); + const tr = new EditableTagRendering(self.previewTagValue, new TagRenderingConfig(tagRenderingConfig, undefined,"preview")); return tr.Render(); } catch (e) { return new Combine(["Could not show this tagrendering:", e.message]).Render(); diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 01c29e649..57e7e99da 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -25,7 +25,7 @@ export default class FeatureInfoBox extends UIElement { } - const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI")) + const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI", undefined)) .SetClass("featureinfobox-title"); const titleIcons = new Combine( layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon))) @@ -53,7 +53,6 @@ export default class FeatureInfoBox extends UIElement { const content = new Combine([ ...renderings, - questionBox, tail.SetClass("featureinfobox-tail") ] ) diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 7fd00a7b7..b6ba2c39d 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -67,7 +67,7 @@ export default class TagRenderingQuestion extends UIElement { } - this._saveButton = new SaveButton(this._inputElement.GetValue(), State.state.osmConnection) + this._saveButton = new SaveButton(this._inputElement.GetValue(), State.state?.osmConnection) .onClick(save) diff --git a/UI/UIElement.ts b/UI/UIElement.ts index d1fda3f87..947e616f3 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -4,12 +4,6 @@ import {Utils} from "../Utils"; export abstract class UIElement extends UIEventSource { - /** - * In the 'deploy'-step, some code needs to be run by ts-node. - * However, ts-node crashes when it sees 'document'. When running from console, we flag this and disable all code where document is needed. - * This is a workaround and yet another hack - */ - public static runningFromConsole = false; private static nextId: number = 0; public readonly id: string; public readonly _source: UIEventSource; diff --git a/assets/layers/bike_cleaning/bike_cleaning.json b/assets/layers/bike_cleaning/bike_cleaning.json index ea66f4dbe..2665ea102 100644 --- a/assets/layers/bike_cleaning/bike_cleaning.json +++ b/assets/layers/bike_cleaning/bike_cleaning.json @@ -26,6 +26,7 @@ "overpassTags": { "or": [ "service:bicycle:cleaning=yes", + "service:bicycle:cleaning=diy", "amenity=bicycle_wash" ] }, @@ -38,13 +39,87 @@ "nl": "Fietsschoonmaakpunt" }, "tags": [ - "amenity=bicycle_wash", - "service:bicycle:cleaning=yes" + "amenity=bicycle_wash" ] } ], "color": "#6bc4f7", + "iconOverlays": [ + { + "if": { + "and": [ + "service:bicycle:cleaning~*", + "amenity!=bike_wash" + ] + }, + "then": { + "render": "./assets/layers/bike_cleaning/bike_cleaning_icon.svg", + "roaming": true + }, + "badge": true + } + ], + "titleIcons": [ + { + "render": "", + "roaming": true + } + ], "tagRenderings": [ - "images" + "images", + { + "question": "How much does it cost to use the cleaning service?", + "render": "Using the cleaning service costs {charge}", + "condition": "amenity!=bike_wash", + "freeform": { + "key": "service:bicycle:cleaning:charge", + "addExtraTags": [ + "service:bicycle:cleaning:fee=yes" + ] + }, + "mappings": [ + { + "if": "service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge=", + "then": "The cleaning service is free to use" + }, + { + "if": "service:bicycle:cleaning:fee=no&", + "then": "Free to use", + "hideInAnswer": true + }, + { + "if": "service:bicycle:cleaning:fee=yes", + "then": "The cleaning service has a fee" + } + ], + "roaming": true + }, + { + "question": "How much does it cost to use the cleaning service?", + "render": "Using the cleaning service costs {charge}", + "condition": "amenity=bike_wash", + "freeform": { + "key": "charge", + "addExtraTags": [ + "fee=yes" + ] + }, + "mappings": [ + { + "if": "fee=no&charge=", + "then": "Free to use cleaning service" + }, + { + "if": "fee=no&", + "then": "Free to use", + "hideInAnswer": true + }, + { + "if": "fee=yes", + "then": "The cleaning service has a fee" + } + ], + "roaming": false + } ] } \ No newline at end of file diff --git a/assets/layers/bike_cleaning/bike_cleaning_icon.svg b/assets/layers/bike_cleaning/bike_cleaning_icon.svg new file mode 100644 index 000000000..e6b5c0169 --- /dev/null +++ b/assets/layers/bike_cleaning/bike_cleaning_icon.svg @@ -0,0 +1,204 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/layers/bike_shop/bike_cleaning.svg b/assets/layers/bike_shop/bike_cleaning.svg deleted file mode 100644 index 96c03eed4..000000000 --- a/assets/layers/bike_shop/bike_cleaning.svg +++ /dev/null @@ -1,292 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/layers/bike_shop/bike_shop.json b/assets/layers/bike_shop/bike_shop.json index 9a54711c6..f8676ce3a 100644 --- a/assets/layers/bike_shop/bike_shop.json +++ b/assets/layers/bike_shop/bike_shop.json @@ -186,15 +186,6 @@ "condition": "service:bicycle:diy=yes", "render": "" }, - { - "condition": { - "or": [ - "service:bicycle:cleaning=yes", - "service:bicycle:cleaning=diy" - ] - }, - "render": "" - }, "defaults" ], "description": { diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index 4ef6ddf7d..5c4f8af55 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -1,5 +1,5 @@ -import {UIElement} from "../UI/UIElement"; -UIElement.runningFromConsole = true; +import {Utils} from "../Utils"; +Utils.runningFromConsole = true; import {equal} from "assert"; import T from "./TestHelper"; import {FromJSON} from "../Customizations/JSON/FromJSON"; @@ -10,7 +10,6 @@ import {UIEventSource} from "../Logic/UIEventSource"; import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig"; import EditableTagRendering from "../UI/Popup/EditableTagRendering"; import {SubstitutedTranslation} from "../UI/SpecialVisualizations"; -import {Utils} from "../Utils"; import {Translation} from "../UI/i18n/Translation"; import {OH, OpeningHour} from "../UI/OpeningHours/OpeningHours"; import PublicHolidayInput from "../UI/OpeningHours/PublicHolidayInput"; @@ -40,6 +39,13 @@ new T([ equal(notReg.matches([{k:"x",v:""}]), true) equal(notReg.matches([]), true) + + + const noMatch = FromJSON.Tag("key!=value") as Tag; + equal(noMatch.matches([{k:"key",v:"value"}]), false) + equal(noMatch.matches([{k:"key",v:"otherValue"}]), true) + equal(noMatch.matches([{k:"key",v:""}]), true) + equal(noMatch.matches([{k:"otherKey",v:""}]), true) })], @@ -92,7 +98,7 @@ new T([ } ], condition: "x=" - }, ""); + }, undefined,""); equal(undefined, tr.GetRenderValue({"foo": "bar"})); equal("Has no name", tr.GetRenderValue({"noname": "yes"})?.txt); @@ -145,7 +151,7 @@ new T([ ] }; - const constr = new TagRenderingConfig(def, "test"); + const constr = new TagRenderingConfig(def, undefined,"test"); const uiEl = new EditableTagRendering(new UIEventSource( {leisure: "park", "access": "no"}), constr ); From 52f1d5511d6cd662905114f0f45cfe9d2e9306b0 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 8 Jan 2021 04:06:10 +0100 Subject: [PATCH 16/25] Layer filtering now allows items that would show up on another layer --- Logic/FeatureSource/FilteringFeatureSource.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Logic/FeatureSource/FilteringFeatureSource.ts b/Logic/FeatureSource/FilteringFeatureSource.ts index 49fd2004b..9d684248e 100644 --- a/Logic/FeatureSource/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/FilteringFeatureSource.ts @@ -32,7 +32,20 @@ export default class FilteringFeatureSource implements FeatureSource { if (layer === undefined) { throw "No layer found with id " + layerId; } - return layer.isDisplayed.data && (layer.layerDef.minzoom <= location.data.zoom); + if(layer.isDisplayed.data && (layer.layerDef.minzoom <= location.data.zoom)){ + return true; + } + // Does it match any other layer? + for (const toCheck of layers) { + if(!toCheck.isDisplayed.data){ + continue; + } + if(toCheck.layerDef.overpassTags.matchesProperties(f.feature.properties)){ + return true; + } + } + return false; + }); self.features.setData(newFeatures); } From ddea3d2fc1e348a3325856bd9ba7c05a69e02296 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 8 Jan 2021 14:23:12 +0100 Subject: [PATCH 17/25] Small css tweaks, fix layer filtering --- Logic/FeatureSource/FilteringFeatureSource.ts | 25 ++++++++++++------- UI/BigComponents/LayerSelection.ts | 9 ++++--- css/tagrendering.css | 4 +-- index.css | 1 + 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Logic/FeatureSource/FilteringFeatureSource.ts b/Logic/FeatureSource/FilteringFeatureSource.ts index 9d684248e..cfc9c9480 100644 --- a/Logic/FeatureSource/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/FilteringFeatureSource.ts @@ -32,20 +32,20 @@ export default class FilteringFeatureSource implements FeatureSource { if (layer === undefined) { throw "No layer found with id " + layerId; } - if(layer.isDisplayed.data && (layer.layerDef.minzoom <= location.data.zoom)){ + if (FilteringFeatureSource.showLayer(layer, location)) { return true; } // Does it match any other layer? for (const toCheck of layers) { - if(!toCheck.isDisplayed.data){ + if (!FilteringFeatureSource.showLayer(toCheck, location)) { continue; } - if(toCheck.layerDef.overpassTags.matchesProperties(f.feature.properties)){ + if (toCheck.layerDef.overpassTags.matchesProperties(f.feature.properties)) { return true; } } return false; - + }); self.features.setData(newFeatures); } @@ -54,15 +54,16 @@ export default class FilteringFeatureSource implements FeatureSource { layerDict[layer.layerDef.id] = layer; } upstream.features.addCallback(() => { - update()}); + update() + }); location.map(l => { // We want something that is stable for the shown layers const displayedLayerIndexes = []; for (let i = 0; i < layers.length; i++) { - if(l.zoom < layers[i].layerDef.minzoom){ + if (l.zoom < layers[i].layerDef.minzoom) { continue; } - if(!layers[i].isDisplayed.data){ + if (!layers[i].isDisplayed.data) { continue; } displayedLayerIndexes.push(i); @@ -70,10 +71,16 @@ export default class FilteringFeatureSource implements FeatureSource { return displayedLayerIndexes.join(",") }, layers.map(l => l.isDisplayed)) .addCallback(() => { - update();}); + update(); + }); } - + private static showLayer(layer: { + isDisplayed: UIEventSource, + layerDef: LayerConfig + }, location: UIEventSource) { + return layer.isDisplayed.data && (layer.layerDef.minzoom <= location.data.zoom) + } } \ No newline at end of file diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index f38b8ca5b..d812b6810 100644 --- a/UI/BigComponents/LayerSelection.ts +++ b/UI/BigComponents/LayerSelection.ts @@ -43,10 +43,13 @@ export default class LayerSelection extends UIElement { } return "" })) - const style = "display:flex;align-items:center;flex-wrap:wrap;" + const style = "display:flex;align-items:center;" + const styleWhole = "display:flex; flex-wrap: wrap" this._checkboxes.push(new CheckBox( - new Combine([icon, name, zoomStatus]).SetStyle(style), - new Combine([iconUnselected, "", name, "", zoomStatus]).SetStyle(style), + new Combine([new Combine([icon, name]).SetStyle(style), zoomStatus]) + .SetStyle(styleWhole), + new Combine([new Combine([iconUnselected, "", name, ""]).SetStyle(style), zoomStatus]) + .SetStyle(styleWhole), layer.isDisplayed) .SetStyle("margin:0.3em;") ); diff --git a/css/tagrendering.css b/css/tagrendering.css index f99be5789..eb3290bda 100644 --- a/css/tagrendering.css +++ b/css/tagrendering.css @@ -28,7 +28,7 @@ box-shadow: 0 10px 10px -10px var(--shadow-color); display: flex; justify-content: space-between; - max-width: 95vw; + width: 100%; overflow-x: hidden; } @@ -68,7 +68,7 @@ max-height: 75vh; overflow-y: auto; padding-top: 1em; - max-width: 95vw; + width:100%; overflow-x: hidden; } diff --git a/index.css b/index.css index 74584f272..486bdbd11 100644 --- a/index.css +++ b/index.css @@ -103,6 +103,7 @@ a { position: relative; width: 2em; height: 2em; + flex-shrink: 0; } .single-layer-selection-toggle img { From 6e775048541fc896da5a5589605aa67b5d6f5ad7 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 8 Jan 2021 16:49:42 +0100 Subject: [PATCH 18/25] Add back button on simpleAddUI --- AllTranslationAssets.ts | 3 +- Logic/Web/QueryParameters.ts | 1 + Models/Constants.ts | 2 +- UI/Base/ScrollableFullScreen.ts | 7 +- UI/BigComponents/SimpleAddUI.ts | 273 ++++++++++++++++++-------------- UI/Popup/FeatureInfoBox.ts | 5 +- assets/translations.json | 26 ++- 7 files changed, 178 insertions(+), 139 deletions(-) diff --git a/AllTranslationAssets.ts b/AllTranslationAssets.ts index d2b4dd4b1..ac65d3d9a 100644 --- a/AllTranslationAssets.ts +++ b/AllTranslationAssets.ts @@ -39,7 +39,8 @@ export default class AllTranslationAssets { number: new Translation( {"en":"number","ca":"nombre","es":"número","nl":"getal","fr":"nombre","gl":"número","de":"Zahl"} ), osmLinkTooltip: new Translation( {"en":"See this object on OpenStreetMap for history and more editing options","ca":"Mira aquest objecte a OpenStreetMap per veure historial i altres opcions d'edició","es":"Mira este objeto en OpenStreetMap para ver historial y otras opciones de edición","nl":"Bekijk dit object op OpenStreetMap waar geschiedenis en meer aanpasopties zijn","fr":"Voir l'historique de cet objet sur OpenStreetMap et plus d'options d'édition","gl":"Ollar este obxecto no OpenStreetMap para ollar o historial e outras opcións de edición","de":"Dieses Objekt auf OpenStreetMap anschauen für die Geschichte und weitere Bearbeitungsmöglichkeiten"} ), add: { addNew: new Translation( {"en":"Add a new {category} here","ca":"Afegir {category} aquí","es":"Añadir {category} aquí","nl":"Voeg hier een {category} toe","fr":"Ajouter un/une {category} ici","gl":"Engadir {category} aquí","de":"Hier eine neue {category} hinzufügen"} ), - header: new Translation( {"en":"

Add a point?

You clicked somewhere where no data is known yet.
","ca":"

Vols afegir un punt?

Has marcat un lloc on no coneixem les dades.
","es":"

Quieres añadir un punto?

Has marcado un lugar del que no conocemos los datos.
","nl":"

Punt toevoegen?

Je klikte ergens waar er nog geen data is. Kies hieronder welk punt je wilt toevoegen
","fr":"

Pas de données

Vous avez cliqué sur un endroit où il n'y a pas encore de données.
","gl":"

Queres engadir un punto?

Marcaches un lugar onde non coñecemos os datos.
","de":"

Punkt hinzufügen?

Sie haben irgendwo geklickt, wo noch keine Daten bekannt sind.
"} ), + title: new Translation( {"en":"Add a point?","ca":"Vols afegir un punt?","es":"Quieres añadir un punto?","nl":"Punt toevoegen?","fr":"Pas de données","gl":"Queres engadir un punto?","de":"Punkt hinzufügen?"} ), + intro: new Translation( {"en":"You clicked somewhere where no data is known yet.
","ca":"Has marcat un lloc on no coneixem les dades.
","es":"Has marcado un lugar del que no conocemos los datos.
","nl":"Je klikte ergens waar er nog geen data is. Kies hieronder welk punt je wilt toevoegen
","fr":"Vous avez cliqué sur un endroit où il n'y a pas encore de données.
","gl":"Marcaches un lugar onde non coñecemos os datos.
","de":"Sie haben irgendwo geklickt, wo noch keine Daten bekannt sind.
"} ), pleaseLogin: new Translation( {"en":"Please log in to add a new point","ca":"Entra per afegir un nou punt","es":"Entra para añadir un nuevo punto","nl":"Gelieve je aan te melden om een punt to te voegen","fr":"Vous devez vous connecter pour ajouter un point","gl":"Inicia a sesión para engadir un novo punto","de":"Bitte loggen Sie sich ein, um einen neuen Punkt hinzuzufügen"} ), zoomInFurther: new Translation( {"en":"Zoom in further to add a point.","ca":"Apropa per afegir un punt.","es":"Acerca para añadir un punto.","nl":"Gelieve verder in te zoomen om een punt toe te voegen.","fr":"Rapprochez vous pour ajouter un point.","gl":"Achégate para engadir un punto.","de":"Weiter einzoomen, um einen Punkt hinzuzufügen."} ), stillLoading: new Translation( {"en":"The data is still loading. Please wait a bit before you add a new point.","ca":"Les dades es segueixen carregant. Espera una mica abans d'afegir cap punt.","es":"Los datos se siguen cargando. Espera un poco antes de añadir ningún punto.","nl":"De data wordt nog geladen. Nog even geduld en dan kan je een punt toevoegen.","fr":"Chargement des données en cours. Patientez un instant avant d'ajouter un nouveau point.","gl":"Os datos seguen a cargarse. Agarda un intre antes de engadir ningún punto.","de":"Die Daten werden noch geladen. Bitte warten Sie etwas, bevor Sie einen neuen Punkt hinzufügen."} ), diff --git a/Logic/Web/QueryParameters.ts b/Logic/Web/QueryParameters.ts index 715ee32a8..7c3458325 100644 --- a/Logic/Web/QueryParameters.ts +++ b/Logic/Web/QueryParameters.ts @@ -62,6 +62,7 @@ export class QueryParameters { parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data)) } + // Don't pollute the history every time a parameter changes history.replaceState(null, "", "?" + parts.join("&") + "#" + Hash.hash.data); } diff --git a/Models/Constants.ts b/Models/Constants.ts index 106bec851..dcd75e0ee 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.4.3"; + public static vNumber = "0.4.4"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/UI/Base/ScrollableFullScreen.ts b/UI/Base/ScrollableFullScreen.ts index f5770d70a..6c27d1210 100644 --- a/UI/Base/ScrollableFullScreen.ts +++ b/UI/Base/ScrollableFullScreen.ts @@ -2,6 +2,7 @@ import {UIElement} from "../UIElement"; import Svg from "../../Svg"; import State from "../../State"; import Combine from "./Combine"; +import Ornament from "./Ornament"; /** * Wraps some contents into a panel that scrolls the content _under_ the title @@ -17,10 +18,12 @@ export default class ScrollableFullScreen extends UIElement{ State.state.selectedElement.setData(undefined); }).SetClass("only-on-mobile") .SetClass("featureinfobox-back-to-the-map") - + title.SetClass("featureinfobox-title") this._component = new Combine([ new Combine([returnToTheMap, title]).SetClass("featureinfobox-titlebar"), - new Combine([content]).SetClass("featureinfobox-content") + new Combine([content]).SetClass("featureinfobox-content"), + new Combine([ new Ornament().SetStyle("height:5em;")]).SetClass("only-on-mobile") + // We add an ornament which takes around 5em. This is in order to make sure the Web UI doesn't hide ]) this.SetClass("featureinfobox"); diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index c2c73aed6..cecafdb91 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -13,47 +13,175 @@ import {FixedUiElement} from "../Base/FixedUiElement"; import Translations from "../i18n/Translations"; import Constants from "../../Models/Constants"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; +import ScrollableFullScreen from "../Base/ScrollableFullScreen"; export default class SimpleAddUI extends UIElement { - private readonly _addButtons: UIElement[]; - - private _loginButton : UIElement; - - private _confirmPreset: UIEventSource<{ + private readonly _loginButton: UIElement; + + private readonly _confirmPreset: UIEventSource<{ description: string | UIElement, name: string | UIElement, icon: UIElement, tags: Tag[], layerToAddTo: { layerDef: LayerConfig, - isDisplayed: UIEventSource } + isDisplayed: UIEventSource + } }> = new UIEventSource(undefined); - private confirmButton: UIElement = undefined; - private _confirmDescription: UIElement = undefined; - private openLayerControl: UIElement; - private cancelButton: UIElement; - private goToInboxButton: UIElement = new SubtleButton(Svg.envelope_ui(), - Translations.t.general.goToInbox, {url:"https://www.openstreetmap.org/messages/inbox", newTab: false}); + + private _component: UIElement; + + private readonly openLayerControl: UIElement; + private readonly cancelButton: UIElement; + private readonly goToInboxButton: UIElement = new SubtleButton(Svg.envelope_ui(), + Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false}); constructor() { - super(State.state.locationControl); + super(State.state.locationControl.map(loc => loc.zoom)); + const self = this; this.ListenTo(Locale.language); this.ListenTo(State.state.osmConnection.userDetails); this.ListenTo(State.state.layerUpdater.runningQuery); this.ListenTo(this._confirmPreset); this.ListenTo(State.state.locationControl); - + State.state.filteredLayers.data?.map(layer => { + self.ListenTo(layer.isDisplayed) + }) + this._loginButton = Translations.t.general.add.pleaseLogin.Clone().onClick(() => State.state.osmConnection.AttemptLogin()); - - this._addButtons = []; + this.SetStyle("font-size:large"); - + this.cancelButton = new SubtleButton(Svg.close_ui(), + Translations.t.general.cancel + ).onClick(() => { + self._confirmPreset.setData(undefined); + }) + + this.openLayerControl = new SubtleButton(Svg.layers_ui(), + Translations.t.general.add.openLayerControl + ).onClick(() => { + State.state.fullScreenMessage.setData(undefined); + State.state.layerControlIsOpened.setData(true); + }) + } + + InnerRender(): string { + + this._component = new ScrollableFullScreen( + Translations.t.general.add.title, + this.CreateContent() + ) + return this._component.Render(); + + } + + private CreatePresetsPanel(): UIElement { + const userDetails = State.state.osmConnection.userDetails; + if (userDetails === undefined) { + return undefined; + } + + if (!userDetails.data.loggedIn) { + return this._loginButton; + } + + if (userDetails.data.unreadMessages > 0 && userDetails.data.csCount < Constants.userJourney.addNewPointWithUnreadMessagesUnlock) { + return new Combine([ + Translations.t.general.readYourMessages.Clone().SetClass("alert"), + this.goToInboxButton + ]); + + } + + if (userDetails.data.csCount < Constants.userJourney.addNewPointsUnlock) { + return new Combine(["", + Translations.t.general.fewChangesBefore, + ""]); + } + + if (State.state.locationControl.data.zoom < Constants.userJourney.minZoomLevelToAddNewPoints) { + return Translations.t.general.add.zoomInFurther.SetClass("alert") + } + + if (State.state.layerUpdater.runningQuery.data) { + return Translations.t.general.add.stillLoading + } + + const presetButtons = this.CreatePresetButtons() + return new Combine(presetButtons).SetClass("add-popup-all-buttons") + } + + + private CreateContent(): UIElement { + const confirmPanel = this.CreateConfirmPanel(); + if (confirmPanel !== undefined) { + return confirmPanel; + } + + let intro: UIElement = Translations.t.general.add.intro; + + let testMode: UIElement = undefined; + if (State.state.osmConnection?.userDetails?.data?.dryRun) { + testMode = new Combine([ + "", + "Test mode - changes won't be saved", + "" + ]); + } + + let presets = this.CreatePresetsPanel(); + return new Combine([intro, testMode, presets]) + + + } + + private CreateConfirmPanel(): UIElement { + const preset = this._confirmPreset.data; + if (preset === undefined) { + return undefined; + } + + const confirmButton = new SubtleButton(preset.icon, + new Combine([ + "", + Translations.t.general.add.confirmButton.Subs({category: preset.name}), + ""])); + confirmButton.onClick(this.CreatePoint(preset.tags)); + + if (!this._confirmPreset.data.layerToAddTo.isDisplayed.data) { + return new Combine([ + Translations.t.general.add.layerNotEnabled.Subs({layer: this._confirmPreset.data.layerToAddTo.layerDef.name}) + .SetClass("alert"), + this.openLayerControl, + + this.cancelButton + ]); + } + + let tagInfo = ""; + const csCount = State.state.osmConnection.userDetails.data.csCount; + if (csCount > Constants.userJourney.tagsVisibleAt) { + tagInfo = this._confirmPreset.data.tags.map(t => t.asHumanString(csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"); + tagInfo = `
More information about the preset: ${tagInfo}` + } + + return new Combine([ + Translations.t.general.add.confirmIntro.Subs({title: this._confirmPreset.data.name}), + State.state.osmConnection.userDetails.data.dryRun ? "TESTING - changes won't be saved" : "", + confirmButton, + this.cancelButton, + preset.description, + tagInfo + + ]) + + } + + private CreatePresetButtons() { + const allButtons = []; const self = this; for (const layer of State.state.filteredLayers.data) { - - this.ListenTo(layer.isDisplayed); - const presets = layer.layerDef.presets; for (const preset of presets) { const tags = TagUtils.KVtoProperties(preset.tags ?? []); @@ -77,13 +205,6 @@ export default class SimpleAddUI extends UIElement { ]) ).onClick( () => { - self.confirmButton = new SubtleButton(icon, - new Combine([ - "", - Translations.t.general.add.confirmButton.Subs({category: preset.title}), - ""])); - self.confirmButton.onClick(self.CreatePoint(preset.tags)); - self._confirmDescription = preset.description; self._confirmPreset.setData({ tags: preset.tags, layerToAddTo: layer, @@ -94,23 +215,10 @@ export default class SimpleAddUI extends UIElement { self.Update(); } ) - - - this._addButtons.push(button); + allButtons.push(button); } } - - this.cancelButton = new SubtleButton(Svg.close_ui(), - Translations.t.general.cancel - ).onClick(() => { - self._confirmPreset.setData(undefined); - }) - - this.openLayerControl = new SubtleButton(Svg.layers_ui(), - Translations.t.general.add.openLayerControl - ).onClick(() => { - State.state.layerControlIsOpened.setData(true); - }) + return allButtons; } private CreatePoint(tags: Tag[]) { @@ -121,86 +229,5 @@ export default class SimpleAddUI extends UIElement { } } - InnerRender(): string { - - const userDetails = State.state.osmConnection.userDetails; - - if (this._confirmPreset.data !== undefined) { - - if(!this._confirmPreset.data.layerToAddTo.isDisplayed.data){ - return new Combine([ - Translations.t.general.add.layerNotEnabled.Subs({layer: this._confirmPreset.data.layerToAddTo.layerDef.name}) - .SetClass("alert"), - this.openLayerControl, - - this.cancelButton - ]).Render(); - } - - let tagInfo = ""; - const csCount = State.state.osmConnection.userDetails.data.csCount; - if (csCount > Constants.userJourney.tagsVisibleAt) { - tagInfo = this._confirmPreset.data .tags.map(t => t.asHumanString(csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"); - tagInfo = `
More information about the preset: ${tagInfo}` - } - - return new Combine([ - Translations.t.general.add.confirmIntro.Subs({title: this._confirmPreset.data.name}), - userDetails.data.dryRun ? "TESTING - changes won't be saved" : "", - this.confirmButton, - this.cancelButton, - this._confirmDescription, - tagInfo - - ]).Render(); - - - } - - - let header: UIElement = Translations.t.general.add.header; - - - if (userDetails === undefined) { - return header.Render(); - } - - if (!userDetails.data.loggedIn) { - return new Combine([header, this._loginButton]).Render() - } - - if (userDetails.data.unreadMessages > 0 && userDetails.data.csCount < Constants.userJourney.addNewPointWithUnreadMessagesUnlock) { - return new Combine([header, - Translations.t.general.readYourMessages.Clone().SetClass("alert"), - this.goToInboxButton - ]).Render(); - - } - - if (userDetails.data.dryRun) { - header = new Combine([header, - "", - "Test mode - changes won't be saved", - "" - ]); - } - - if (userDetails.data.csCount < Constants.userJourney.addNewPointsUnlock) { - return new Combine([header, "", - Translations.t.general.fewChangesBefore, - ""]).Render(); - } - - if (State.state.locationControl.data.zoom < Constants.userJourney.minZoomLevelToAddNewPoints) { - return new Combine([header, Translations.t.general.add.zoomInFurther.SetClass("alert")]).Render() - } - - if (State.state.layerUpdater.runningQuery.data) { - return new Combine([header, Translations.t.general.add.stillLoading]).Render() - } - - return header.Render() + new Combine(this._addButtons).SetClass("add-popup-all-buttons").Render(); - } - } \ No newline at end of file diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 57e7e99da..9ad6f9dfc 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -6,10 +6,7 @@ import QuestionBox from "./QuestionBox"; import Combine from "../Base/Combine"; import TagRenderingAnswer from "./TagRenderingAnswer"; import State from "../../State"; -import {FixedUiElement} from "../Base/FixedUiElement"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; -import Svg from "../../Svg"; -import Ornament from "../Base/Ornament"; import ScrollableFullScreen from "../Base/ScrollableFullScreen"; export default class FeatureInfoBox extends UIElement { @@ -49,7 +46,7 @@ export default class FeatureInfoBox extends UIElement { if (!questionBoxIsUsed) { renderings.push(questionBox); } - const tail = new Combine([new Ornament()]).SetClass("only-on-mobile"); + const tail = new Combine([]).SetClass("only-on-mobile"); const content = new Combine([ ...renderings, diff --git a/assets/translations.json b/assets/translations.json index 65dc28304..77cbb72c1 100644 --- a/assets/translations.json +++ b/assets/translations.json @@ -309,14 +309,24 @@ "gl": "Engadir {category} aquí", "de": "Hier eine neue {category} hinzufügen" }, - "header": { - "en": "

Add a point?

You clicked somewhere where no data is known yet.
", - "ca": "

Vols afegir un punt?

Has marcat un lloc on no coneixem les dades.
", - "es": "

Quieres añadir un punto?

Has marcado un lugar del que no conocemos los datos.
", - "nl": "

Punt toevoegen?

Je klikte ergens waar er nog geen data is. Kies hieronder welk punt je wilt toevoegen
", - "fr": "

Pas de données

Vous avez cliqué sur un endroit où il n'y a pas encore de données.
", - "gl": "

Queres engadir un punto?

Marcaches un lugar onde non coñecemos os datos.
", - "de": "

Punkt hinzufügen?

Sie haben irgendwo geklickt, wo noch keine Daten bekannt sind.
" + "title": { + "en": "Add a new point?", + "ca": "Vols afegir un punt?", + "es": "Quieres añadir un punto?", + "nl": "Nieuw punt toevoegen?", + "fr": "Pas de données", + "gl": "Queres engadir un punto?", + "de": "Punkt hinzufügen?" + + }, + "intro": { + "en": "You clicked somewhere where no data is known yet.
", + "ca": "Has marcat un lloc on no coneixem les dades.
", + "es": "Has marcado un lugar del que no conocemos los datos.
", + "nl": "Je klikte ergens waar er nog geen data is. Kies hieronder welk punt je wilt toevoegen
", + "fr": "Vous avez cliqué sur un endroit où il n'y a pas encore de données.
", + "gl": "Marcaches un lugar onde non coñecemos os datos.
", + "de": "Sie haben irgendwo geklickt, wo noch keine Daten bekannt sind.
" }, "pleaseLogin": { "en": "Please log in to add a new point", From 2f570102022e34598a9f16ca579361ac4546fb60 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 8 Jan 2021 18:02:07 +0100 Subject: [PATCH 19/25] Fix back button; add title --- AllTranslationAssets.ts | 2 +- Customizations/JSON/LayerConfig.ts | 1 - Customizations/JSON/LayoutConfig.ts | 1 - InitUiElements.ts | 18 ++++++++---- Logic/Actors/HistoryHandling.ts | 19 +++++++++++++ Logic/Actors/StrayClickHandler.ts | 4 +-- Logic/Actors/TitleHandler.ts | 33 ++++++++++++++++++++++ Logic/Web/Hash.ts | 43 ++++++++++++++++++++++------- Logic/Web/QueryParameters.ts | 12 +++----- State.ts | 17 ++++-------- UI/FullScreenMessageBoxHandler.ts | 2 +- UI/Popup/FeatureInfoBox.ts | 3 ++ UI/ShowDataLayer.ts | 2 +- index.ts | 1 - 14 files changed, 115 insertions(+), 43 deletions(-) create mode 100644 Logic/Actors/HistoryHandling.ts create mode 100644 Logic/Actors/TitleHandler.ts diff --git a/AllTranslationAssets.ts b/AllTranslationAssets.ts index ac65d3d9a..f4ca2c3e1 100644 --- a/AllTranslationAssets.ts +++ b/AllTranslationAssets.ts @@ -39,7 +39,7 @@ export default class AllTranslationAssets { number: new Translation( {"en":"number","ca":"nombre","es":"número","nl":"getal","fr":"nombre","gl":"número","de":"Zahl"} ), osmLinkTooltip: new Translation( {"en":"See this object on OpenStreetMap for history and more editing options","ca":"Mira aquest objecte a OpenStreetMap per veure historial i altres opcions d'edició","es":"Mira este objeto en OpenStreetMap para ver historial y otras opciones de edición","nl":"Bekijk dit object op OpenStreetMap waar geschiedenis en meer aanpasopties zijn","fr":"Voir l'historique de cet objet sur OpenStreetMap et plus d'options d'édition","gl":"Ollar este obxecto no OpenStreetMap para ollar o historial e outras opcións de edición","de":"Dieses Objekt auf OpenStreetMap anschauen für die Geschichte und weitere Bearbeitungsmöglichkeiten"} ), add: { addNew: new Translation( {"en":"Add a new {category} here","ca":"Afegir {category} aquí","es":"Añadir {category} aquí","nl":"Voeg hier een {category} toe","fr":"Ajouter un/une {category} ici","gl":"Engadir {category} aquí","de":"Hier eine neue {category} hinzufügen"} ), - title: new Translation( {"en":"Add a point?","ca":"Vols afegir un punt?","es":"Quieres añadir un punto?","nl":"Punt toevoegen?","fr":"Pas de données","gl":"Queres engadir un punto?","de":"Punkt hinzufügen?"} ), + title: new Translation( {"en":"Add a new point?","ca":"Vols afegir un punt?","es":"Quieres añadir un punto?","nl":"Nieuw punt toevoegen?","fr":"Pas de données","gl":"Queres engadir un punto?","de":"Punkt hinzufügen?"} ), intro: new Translation( {"en":"You clicked somewhere where no data is known yet.
","ca":"Has marcat un lloc on no coneixem les dades.
","es":"Has marcado un lugar del que no conocemos los datos.
","nl":"Je klikte ergens waar er nog geen data is. Kies hieronder welk punt je wilt toevoegen
","fr":"Vous avez cliqué sur un endroit où il n'y a pas encore de données.
","gl":"Marcaches un lugar onde non coñecemos os datos.
","de":"Sie haben irgendwo geklickt, wo noch keine Daten bekannt sind.
"} ), pleaseLogin: new Translation( {"en":"Please log in to add a new point","ca":"Entra per afegir un nou punt","es":"Entra para añadir un nuevo punto","nl":"Gelieve je aan te melden om een punt to te voegen","fr":"Vous devez vous connecter pour ajouter un point","gl":"Inicia a sesión para engadir un novo punto","de":"Bitte loggen Sie sich ein, um einen neuen Punkt hinzuzufügen"} ), zoomInFurther: new Translation( {"en":"Zoom in further to add a point.","ca":"Apropa per afegir un punt.","es":"Acerca para añadir un punto.","nl":"Gelieve verder in te zoomen om een punt toe te voegen.","fr":"Rapprochez vous pour ajouter un point.","gl":"Achégate para engadir un punto.","de":"Weiter einzoomen, um einen Punkt hinzuzufügen."} ), diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 53825613d..6dc3f6382 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -177,7 +177,6 @@ export default class LayerConfig { this.tagRenderings.push(...addAll.tagRenderings); this.iconOverlays.push(...addAll.iconOverlays); for (const icon of addAll.titleIcons) { - console.log("Adding ",icon, "to", this.id) this.titleIcons.splice(0,0, icon); } return this; diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts index 81be65c8e..a1f58c7d6 100644 --- a/Customizations/JSON/LayoutConfig.ts +++ b/Customizations/JSON/LayoutConfig.ts @@ -96,7 +96,6 @@ export default class LayoutConfig { if (shared === undefined) { throw "Unkown fixed layer " + name; } - console.log("PREMERGE", layer, shared) // @ts-ignore layer = Utils.Merge(layer.override, shared); } diff --git a/InitUiElements.ts b/InitUiElements.ts index 6c7811716..62c6e59c6 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -41,6 +41,7 @@ import FeatureDuplicatorPerLayer from "./Logic/FeatureSource/FeatureDuplicatorPe import LayerConfig from "./Customizations/JSON/LayerConfig"; import ShowDataLayer from "./UI/ShowDataLayer"; import Hash from "./Logic/Web/Hash"; +import HistoryHandling from "./Logic/Actors/HistoryHandling"; export class InitUiElements { @@ -133,7 +134,6 @@ export class InitUiElements { if (feature === undefined) { State.state.fullScreenMessage.setData(undefined); - Hash.hash.setData(undefined); } if (feature?.properties === undefined) { return; @@ -159,12 +159,18 @@ export class InitUiElements { layer ); - State.state.fullScreenMessage.setData(featureBox); + State.state.fullScreenMessage.setData({ + content: featureBox, + hashText: feature.properties.id.replace("/", "_"), + titleText: featureBox.title + }); break; } } ); + new HistoryHandling(Hash.hash, State.state.fullScreenMessage); + InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => { new UserBadge().AttachTo('userbadge'); }); @@ -279,20 +285,20 @@ export class InitUiElements { }) State.state.selectedElement.addCallback(selected => { - if(selected !== undefined){ + if (selected !== undefined) { checkbox.isEnabled.setData(false); } }) const fullOptions2 = new FullWelcomePaneWithTabs(); - State.state.fullScreenMessage.setData(fullOptions2) + State.state.fullScreenMessage.setData({content: fullOptions2, hashText: "welcome"}) Svg.help_svg() .SetClass("open-welcome-button") .SetClass("shadow") .onClick(() => { - State.state.fullScreenMessage.setData(fullOptions2) + State.state.fullScreenMessage.setData({content: fullOptions2, hashText: "welcome"}) }).AttachTo("help-button-mobile"); @@ -326,7 +332,7 @@ export class InitUiElements { const fullScreen = new LayerControlPanel(); checkbox.isEnabled.addCallback(isEnabled => { if (isEnabled) { - State.state.fullScreenMessage.setData(fullScreen); + State.state.fullScreenMessage.setData({content: fullScreen, hashText: "layer-select"}); } }) State.state.fullScreenMessage.addCallback(latest => { diff --git a/Logic/Actors/HistoryHandling.ts b/Logic/Actors/HistoryHandling.ts new file mode 100644 index 000000000..6b317a286 --- /dev/null +++ b/Logic/Actors/HistoryHandling.ts @@ -0,0 +1,19 @@ +import {UIEventSource} from "../UIEventSource"; +import {UIElement} from "../../UI/UIElement"; + +export default class HistoryHandling { + + constructor(hash: UIEventSource, fullscreenMessage: UIEventSource<{ content: UIElement, hashText: string }>) { + hash.addCallback(h => { + if (h === undefined || h === "") { + fullscreenMessage.setData(undefined); + } + }) + + fullscreenMessage.addCallback(fs => { + hash.setData(fs?.hashText); + }) + + } + +} \ No newline at end of file diff --git a/Logic/Actors/StrayClickHandler.ts b/Logic/Actors/StrayClickHandler.ts index a7041d6dc..bc185130d 100644 --- a/Logic/Actors/StrayClickHandler.ts +++ b/Logic/Actors/StrayClickHandler.ts @@ -17,7 +17,7 @@ export default class StrayClickHandler { selectedElement: UIEventSource, filteredLayers: UIEventSource<{ readonly isDisplayed: UIEventSource }[]>, leafletMap: UIEventSource, - fullscreenMessage: UIEventSource, + fullscreenMessage: UIEventSource<{content: UIElement, hashText: string}>, uiToShow: (() => UIElement)) { this._uiToShow = uiToShow; const self = this; @@ -51,7 +51,7 @@ export default class StrayClickHandler { self._lastMarker.bindPopup(popup); self._lastMarker.on("click", () => { - fullscreenMessage.setData(self._uiToShow()); + fullscreenMessage.setData({content: self._uiToShow(), hashText: "new"}); uiElement.Update(); }); }); diff --git a/Logic/Actors/TitleHandler.ts b/Logic/Actors/TitleHandler.ts new file mode 100644 index 000000000..8093b64e0 --- /dev/null +++ b/Logic/Actors/TitleHandler.ts @@ -0,0 +1,33 @@ +import {UIEventSource} from "../UIEventSource"; +import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; +import Translations from "../../UI/i18n/Translations"; +import Locale from "../../UI/i18n/Locale"; +import {UIElement} from "../../UI/UIElement"; + +export default class TitleHandler{ + constructor(layoutToUse: UIEventSource, fullScreenMessage: UIEventSource<{ content: UIElement, hashText: string, titleText?: UIElement }>) { + + + layoutToUse.map((layoutToUse) => { + return Translations.WT(layoutToUse?.title)?.txt ?? "MapComplete" + }, [Locale.language] + ).addCallbackAndRun((title) => { + document.title = title + }); + + fullScreenMessage.addCallbackAndRun(selected => { + const title = Translations.WT(layoutToUse.data?.title)?.txt ?? "MapComplete" + if(selected?.titleText?.data === undefined){ + document.title = title + }else{ + selected.titleText.Update(); + var d = document.createElement('div'); + d.innerHTML = selected.titleText.InnerRender(); + const poi = (d.textContent || d.innerText) + document.title = title + " | " + poi; + } + }) + + + } +} \ No newline at end of file diff --git a/Logic/Web/Hash.ts b/Logic/Web/Hash.ts index 4ef26916b..07372f2ff 100644 --- a/Logic/Web/Hash.ts +++ b/Logic/Web/Hash.ts @@ -1,29 +1,52 @@ import {UIEventSource} from "../UIEventSource"; -import Constants from "../../Models/Constants"; import {Utils} from "../../Utils"; export default class Hash { - - public static hash : UIEventSource = Hash.Get(); - - private static Get() : UIEventSource{ - if(Utils.runningFromConsole){ + + public static hash: UIEventSource = Hash.Get(); + + /** + * Gets the current string, including the pound sign + * @constructor + */ + public static Current(): string { + if (Hash.hash.data === undefined || Hash.hash.data === "") { + return "" + } else { + return "#" + Hash.hash.data; + } + } + + private static Get(): UIEventSource { + if (Utils.runningFromConsole) { return new UIEventSource(undefined); } const hash = new UIEventSource(window.location.hash.substr(1)); hash.addCallback(h => { - if(h === undefined || h === ""){ + if (h === "undefined") { + console.warn("Got a literal 'undefined' as hash, ignoring") + h = undefined; + } + + if (h === undefined || h === "") { window.location.hash = ""; return; } + h = h.replace(/\//g, "_"); window.location.hash = "#" + h; }); + + window.onhashchange = () => { - hash.setData(window.location.hash.substr(1)) + let newValue = window.location.hash.substr(1); + if (newValue === "") { + newValue = undefined; + } + hash.setData(newValue) } - + return hash; } - + } \ No newline at end of file diff --git a/Logic/Web/QueryParameters.ts b/Logic/Web/QueryParameters.ts index 7c3458325..406afa214 100644 --- a/Logic/Web/QueryParameters.ts +++ b/Logic/Web/QueryParameters.ts @@ -43,27 +43,23 @@ export class QueryParameters { private static Serialize() { const parts = [] for (const key of QueryParameters.order) { - if (QueryParameters.knownSources[key] === undefined || QueryParameters.knownSources[key].data === undefined) { + if (QueryParameters.knownSources[key]?.data === undefined) { continue; } - if (QueryParameters.knownSources[key].data === undefined) { - continue; - } - if (QueryParameters.knownSources[key].data === "undefined") { continue; } - - if (QueryParameters.knownSources[key].data == QueryParameters.defaults[key]) { + if (QueryParameters.knownSources[key].data === QueryParameters.defaults[key]) { continue; } parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data)) } // Don't pollute the history every time a parameter changes - history.replaceState(null, "", "?" + parts.join("&") + "#" + Hash.hash.data); + + history.replaceState(null, "", "?" + parts.join("&") + Hash.Current()); } diff --git a/State.ts b/State.ts index 0f3e43d12..2be700e45 100644 --- a/State.ts +++ b/State.ts @@ -4,7 +4,6 @@ import {ElementStorage} from "./Logic/ElementStorage"; import {Changes} from "./Logic/Osm/Changes"; import {OsmConnection} from "./Logic/Osm/OsmConnection"; import Locale from "./UI/i18n/Locale"; -import Translations from "./UI/i18n/Translations"; import {UIEventSource} from "./Logic/UIEventSource"; import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; import {QueryParameters} from "./Logic/Web/QueryParameters"; @@ -18,6 +17,7 @@ import Constants from "./Models/Constants"; import UpdateFromOverpass from "./Logic/Actors/UpdateFromOverpass"; import LayerConfig from "./Customizations/JSON/LayerConfig"; +import TitleHandler from "./Logic/Actors/TitleHandler"; /** * Contains the global state: a bunch of UI-event sources @@ -75,7 +75,7 @@ export default class State { /** This message is shown full screen on mobile devices */ - public readonly fullScreenMessage = new UIEventSource(undefined) + public readonly fullScreenMessage = new UIEventSource<{ content: UIElement, hashText: string, titleText?: UIElement }>(undefined) /** The latest element that was selected - used to generate the right UI at the right place @@ -112,9 +112,9 @@ export default class State { public layoutDefinition: string; public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; - public layerControlIsOpened: UIEventSource = + public layerControlIsOpened: UIEventSource = QueryParameters.GetQueryParameter("layer-control-toggle", "false", "Whether or not the layer control is shown") - .map((str) => str !== "false", [], b => "" + b) + .map((str) => str !== "false", [], b => "" + b) public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0", `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`).map( str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n @@ -240,13 +240,8 @@ export default class State { } }).ping() - this.layoutToUse.map((layoutToUse) => { - return Translations.WT(layoutToUse?.title)?.txt ?? "MapComplete" - }, [Locale.language] - ).addCallbackAndRun((title) => { - document.title = title - }); - + new TitleHandler(this.layoutToUse, this.fullScreenMessage); + this.allElements = new ElementStorage(); this.changes = new Changes(); diff --git a/UI/FullScreenMessageBoxHandler.ts b/UI/FullScreenMessageBoxHandler.ts index 0a67765a3..9fa2493f8 100644 --- a/UI/FullScreenMessageBoxHandler.ts +++ b/UI/FullScreenMessageBoxHandler.ts @@ -19,7 +19,7 @@ export default class FullScreenMessageBox extends UIElement { if (State.state.fullScreenMessage.data === undefined) { return ""; } - this._content = State.state.fullScreenMessage.data; + this._content = State.state.fullScreenMessage.data.content; return new Combine([this._content]).SetClass("fullscreenmessage-content").Render(); } diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 9ad6f9dfc..4818e1563 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -12,6 +12,8 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen"; export default class FeatureInfoBox extends UIElement { private _component: UIElement; + public title: UIEventSource ; + constructor( tags: UIEventSource, layerConfig: LayerConfig @@ -24,6 +26,7 @@ export default class FeatureInfoBox extends UIElement { const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI", undefined)) .SetClass("featureinfobox-title"); + this.title = title; const titleIcons = new Combine( layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon))) .SetClass("featureinfobox-icons"); diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 0c9c39b44..1b03a562d 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -154,7 +154,7 @@ export default class ShowDataLayer { this._onSelectedTrigger[feature.properties.id.replace("/","_")] = this._onSelectedTrigger[id]; if (feature.properties.id.replace(/\//g, "_") === Hash.hash.data) { // This element is in the URL, so this is a share link - // We already open it + // We open the relevant popup straight away uiElement.Activate(); popup.setContent(uiElement.Render()); diff --git a/index.ts b/index.ts index 0efc118bc..eb291a977 100644 --- a/index.ts +++ b/index.ts @@ -6,7 +6,6 @@ import {UIEventSource} from "./Logic/UIEventSource"; import * as $ from "jquery"; import LayoutConfig from "./Customizations/JSON/LayoutConfig"; import {Utils} from "./Utils"; -import {Overpass} from "./Logic/Osm/Overpass"; let defaultLayout = "bookcases" // --------------------- Special actions based on the parameters ----------------- From bd6f325fc17df1c8aa0bcf3da92eac223259e685 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 8 Jan 2021 18:02:55 +0100 Subject: [PATCH 20/25] version bump --- Models/Constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Models/Constants.ts b/Models/Constants.ts index dcd75e0ee..662900774 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.4.4"; + public static vNumber = "0.4.5"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { From 641b59bca534f677b08a029c367210192c71eff5 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 9 Jan 2021 00:03:21 +0100 Subject: [PATCH 21/25] Add ornament --- UI/Base/Ornament.ts | 20 ++++++++++++++------ UI/Base/ScrollableFullScreen.ts | 13 +++++++------ css/fullscreenmessagebox.css | 9 ++++++--- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/UI/Base/Ornament.ts b/UI/Base/Ornament.ts index 2a4ac175b..2cbf75974 100644 --- a/UI/Base/Ornament.ts +++ b/UI/Base/Ornament.ts @@ -1,18 +1,25 @@ import {UIElement} from "../UIElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import Svg from "../../Svg"; +import State from "../../State"; export default class Ornament extends UIElement { private static readonly ornamentsCount = Ornament.countOrnaments(); - private readonly _index = new UIEventSource(0) + private readonly _index = new UIEventSource("0") - constructor(index = new UIEventSource(0)) { - super(index); + constructor(index = undefined) { + super(); + index = index ?? State.state.osmConnection.GetPreference("ornament"); + this.ListenTo(index); this._index = index; this.SetClass("ornament") const self = this; this.onClick(() => { - self._index.setData((self._index.data + 1) % (Ornament.ornamentsCount + 1)); + let c = Number(index.data); + if(isNaN(c)){ + c = 0; + } + self._index.setData(""+ ((c + 1) % (Ornament.ornamentsCount + 1))); }) } @@ -28,10 +35,11 @@ export default class Ornament extends UIElement { } InnerRender(): string { - if(this._index.data == 0){ + if(this._index.data == "0"){ return "" } - return Svg.All[`Ornament-Horiz-${this._index.data - 1}.svg`] + console.log(this._index.data) + return Svg.All[`Ornament-Horiz-${Number(this._index.data) - 1}.svg`] } } \ No newline at end of file diff --git a/UI/Base/ScrollableFullScreen.ts b/UI/Base/ScrollableFullScreen.ts index 6c27d1210..8fd910d8c 100644 --- a/UI/Base/ScrollableFullScreen.ts +++ b/UI/Base/ScrollableFullScreen.ts @@ -7,9 +7,9 @@ import Ornament from "./Ornament"; /** * Wraps some contents into a panel that scrolls the content _under_ the title */ -export default class ScrollableFullScreen extends UIElement{ +export default class ScrollableFullScreen extends UIElement { private _component: Combine; - + constructor(title: UIElement, content: UIElement) { super(); @@ -19,16 +19,17 @@ export default class ScrollableFullScreen extends UIElement{ }).SetClass("only-on-mobile") .SetClass("featureinfobox-back-to-the-map") title.SetClass("featureinfobox-title") + const ornament = new Combine([new Ornament().SetStyle("height:5em;")]).SetClass("only-on-mobile") + this._component = new Combine([ new Combine([returnToTheMap, title]).SetClass("featureinfobox-titlebar"), - new Combine([content]).SetClass("featureinfobox-content"), - new Combine([ new Ornament().SetStyle("height:5em;")]).SetClass("only-on-mobile") + new Combine(["",content,"", ornament]).SetClass("featureinfobox-content"), // We add an ornament which takes around 5em. This is in order to make sure the Web UI doesn't hide ]) this.SetClass("featureinfobox"); - + } - + InnerRender(): string { return this._component.Render(); } diff --git a/css/fullscreenmessagebox.css b/css/fullscreenmessagebox.css index e6d558bdd..2c681b919 100644 --- a/css/fullscreenmessagebox.css +++ b/css/fullscreenmessagebox.css @@ -15,12 +15,15 @@ .fullscreenmessage-content .featureinfobox-content { padding: 1em; top: var(--variable-title-height); - /* 2em extra: padding from the title */ max-height: calc(100vh - var(--variable-title-height)) !important; - display: block; + min-height: calc(100vh - var(--variable-title-height)) !important; position: absolute; overflow-y: auto; box-sizing: border-box; + + display: flex; + flex-direction: column; + justify-content: space-between } .fullscreenmessage-content .featureinfobox-titlebar { @@ -32,7 +35,7 @@ padding: 0.5em; width: 100%; box-sizing: border-box; - + } .fullscreenmessage-content .featureinfobox-tail { From a2bec35a43f812b10889f9836ecc8b8a46a40de8 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 9 Jan 2021 00:29:12 +0100 Subject: [PATCH 22/25] Fix wiki links on mobile --- index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index eb291a977..a62a0b616 100644 --- a/index.ts +++ b/index.ts @@ -76,8 +76,8 @@ if (layoutFromBase64.startsWith("wiki:")) { data = data.substr(start, data.indexOf("
") - start) data = data.substr(0, data.lastIndexOf("

")) - data = data.substr(startTrigger.length + 3); - + data = data.substr( data.indexOf("

") + 3) + console.log(data) try { const parsed = JSON.parse(data); parsed["id"] = layoutFromBase64 @@ -87,6 +87,7 @@ if (layoutFromBase64.startsWith("wiki:")) { new FixedUiElement(`${themeName} is invalid:
${e}`) .SetClass("clickable") .AttachTo("centermessage"); + console.error("Could not parse the text", data) throw e; } }, From b9bcf801ef0e3afdcded3742013e033ee9917b71 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 9 Jan 2021 00:29:38 +0100 Subject: [PATCH 23/25] Version bump --- Models/Constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Models/Constants.ts b/Models/Constants.ts index 662900774..99e609bca 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.4.5"; + public static vNumber = "0.4.6"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { From 303485df7ec8f21a7b878e3fe318527ad57e10b9 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 9 Jan 2021 02:11:43 +0100 Subject: [PATCH 24/25] Add bicycle tube vending machines --- Customizations/JSON/FromJSON.ts | 12 ++++++- Customizations/SharedLayers.ts | 3 +- InitUiElements.ts | 5 ++- UI/Popup/FeatureInfoBox.ts | 2 +- .../bike_repair_station.json | 3 +- assets/themes/cyclofix/cyclofix.json | 32 ++++++++++++++----- test/Tag.spec.ts | 6 ++++ 7 files changed, 50 insertions(+), 13 deletions(-) diff --git a/Customizations/JSON/FromJSON.ts b/Customizations/JSON/FromJSON.ts index ed662758b..7d2fbd012 100644 --- a/Customizations/JSON/FromJSON.ts +++ b/Customizations/JSON/FromJSON.ts @@ -11,7 +11,17 @@ export class FromJSON { } public static Tag(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter { - if (json === undefined) { + try{ + return this.TagUnsafe(json, context); + }catch(e){ + console.error("Could not parse tag", json,"in context",context,"due to ", e) + throw e; + } + } + + private static TagUnsafe(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter { + + if (json === undefined) { throw `Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression` } if (typeof (json) == "string") { diff --git a/Customizations/SharedLayers.ts b/Customizations/SharedLayers.ts index 72427998f..1bdc62029 100644 --- a/Customizations/SharedLayers.ts +++ b/Customizations/SharedLayers.ts @@ -11,7 +11,7 @@ import * as cycling_themed_objects from "../assets/layers/cycling_themed_object/ import * as bike_shops from "../assets/layers/bike_shop/bike_shop.json" import * as bike_cleaning from "../assets/layers/bike_cleaning/bike_cleaning.json" import * as bicycle_library from "../assets/layers/bicycle_library/bicycle_library.json" - +import * as bicycle_tube_vending_machine from "../assets/layers/bicycle_tube_vending_machine/bicycle_tube_vending_machine.json" import * as maps from "../assets/layers/maps/maps.json" import * as information_boards from "../assets/layers/information_board/information_board.json" import * as direction from "../assets/layers/direction/direction.json" @@ -39,6 +39,7 @@ export default class SharedLayers { cycling_themed_objects, bike_shops, bike_cleaning, + bicycle_tube_vending_machine, maps, direction, information_boards, diff --git a/InitUiElements.ts b/InitUiElements.ts index 62c6e59c6..e1c872604 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -292,7 +292,10 @@ export class InitUiElements { const fullOptions2 = new FullWelcomePaneWithTabs(); - State.state.fullScreenMessage.setData({content: fullOptions2, hashText: "welcome"}) + if (Hash.hash.data === undefined) { + State.state.fullScreenMessage.setData({content: fullOptions2, hashText: "welcome"}) + + } Svg.help_svg() .SetClass("open-welcome-button") diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 4818e1563..b0b59d18f 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -12,7 +12,7 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen"; export default class FeatureInfoBox extends UIElement { private _component: UIElement; - public title: UIEventSource ; + public title: UIElement ; constructor( tags: UIEventSource, diff --git a/assets/layers/bike_repair_station/bike_repair_station.json b/assets/layers/bike_repair_station/bike_repair_station.json index d71665f1c..18a66380f 100644 --- a/assets/layers/bike_repair_station/bike_repair_station.json +++ b/assets/layers/bike_repair_station/bike_repair_station.json @@ -107,7 +107,8 @@ "titleIcons": [ { "render": "", - "condition": "operator=De Fietsambassade Gent" + "condition": "operator=De Fietsambassade Gent", + "roaming": true }, "defaults" ], diff --git a/assets/themes/cyclofix/cyclofix.json b/assets/themes/cyclofix/cyclofix.json index c90321a7b..d8e6bf43f 100644 --- a/assets/themes/cyclofix/cyclofix.json +++ b/assets/themes/cyclofix/cyclofix.json @@ -13,22 +13,38 @@ "fr": "Le but de cette carte est de présenter aux cyclistes une solution facile à utiliser pour trouver l'infrastructure appropriée à leurs besoins.

Vous pouvez suivre votre localisation précise (mobile uniquement) et sélectionner les couches qui vous concernent dans le coin inférieur gauche. Vous pouvez également utiliser cet outil pour ajouter ou modifier des épingles (points d'intérêt) sur la carte et fournir plus de données en répondant aux questions.

Toutes les modifications que vous apportez seront automatiquement enregistrées dans la base de données mondiale d'OpenStreetMap et peuvent être librement réutilisées par d'autres.

Pour plus d'informations sur le projet cyclofix, rendez-vous sur cyclofix.osm.be.", "gl": "O obxectivo deste mapa é amosar ós ciclistas unha solución doada de empregar para atopar a infraestrutura axeitada para as súas necesidades.

Podes obter a túa localización precisa (só para dispositivos móbiles) e escoller as capas que sexan relevantes para ti na esquina inferior esquerda. Tamén podes empregar esta ferramenta para engadir ou editar puntos de interese ó mapa e fornecer máis datos respondendo as cuestións.

Todas as modificacións que fagas serán gardadas de xeito automático na base de datos global do OpenStreetMap e outros poderán reutilizalos libremente.

Para máis información sobre o proxecto cyclofix, vai a cyclofix.osm.be.", "de": "Das Ziel dieser Karte ist es, den Radfahrern eine einfach zu benutzende Lösung zu präsentieren, um die geeignete Infrastruktur für ihre Bedürfnisse zu finden.

Sie können Ihren genauen Standort verfolgen (nur mobil) und in der linken unteren Ecke die für Sie relevanten Ebenen auswählen. Sie können dieses Tool auch verwenden, um Pins (Points of Interest/Interessante Orte) zur Karte hinzuzufügen oder zu bearbeiten und mehr Daten durch Beantwortung der Fragen bereitstellen.

Alle Änderungen, die Sie vornehmen, werden automatisch in der globalen Datenbank von OpenStreetMap gespeichert und können von anderen frei wiederverwendet werden.

Weitere Informationen über das Projekt Cyclofix finden Sie unter cyclofix.osm.be." - }, - "language": ["en", "nl", "fr", "gl","de"], + "language": [ + "en", + "nl", + "fr", + "gl", + "de" + ], "maintainer": "MapComplete", "icon": "./assets/themes/cyclofix/logo.svg", "version": "0", "startLat": 50.8465573, "defaultBackgroundId": "CartoDB.Voyager", - "startLon": 4.3516970, + "startLon": 4.3516970, "startZoom": 16, "widenFactor": 0.05, "socialImage": "./assets/themes/cyclofix/logo.svg", - "layers": ["bike_cafes", "bike_shops", {"builtin": "bicycle_library", - "override": { - "minzoom": 13 - } - },"bike_repair_station", "drinking_water", "bike_themed_object","bike_cleaning","bike_parking"], + "layers": [ + "bike_cafes", + "bike_shops", + { + "builtin": "bicycle_library", + "override": { + "minzoom": 13 + } + }, + "bike_repair_station", + "bicycle_tube_vending_machine", + "drinking_water", + "bike_themed_object", + "bike_cleaning", + "bike_parking" + ], "roamingRenderings": [] } \ No newline at end of file diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index 5c4f8af55..d3a60f1d2 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -47,6 +47,12 @@ new T([ equal(noMatch.matches([{k:"key",v:""}]), true) equal(noMatch.matches([{k:"otherKey",v:""}]), true) + + const multiMatch = FromJSON.Tag("vending~.*bicycle_tube.*") as Tag; + equal(multiMatch.matches([{k:"vending",v:"bicycle_tube"}]), true) + equal(multiMatch.matches([{k:"vending",v:"something;bicycle_tube"}]), true) + equal(multiMatch.matches([{k:"vending",v:"bicycle_tube;something"}]), true) + equal(multiMatch.matches([{k:"vending",v:"xyz;bicycle_tube;something"}]), true) })], ["Is equivalent test", (() => { From c58655d2d512037f0add4150412bd11bd37e8fc0 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 9 Jan 2021 02:12:31 +0100 Subject: [PATCH 25/25] Actually add assets --- Models/Constants.ts | 2 +- .../bicycle_tube_vending_machine.json | 151 ++++++++++++++ .../bicycle_tube_vending_machine/pinIcon.svg | 194 ++++++++++++++++++ .../bicycle_tube_vending_machine/tube.svg | 127 ++++++++++++ 4 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 assets/layers/bicycle_tube_vending_machine/bicycle_tube_vending_machine.json create mode 100644 assets/layers/bicycle_tube_vending_machine/pinIcon.svg create mode 100644 assets/layers/bicycle_tube_vending_machine/tube.svg diff --git a/Models/Constants.ts b/Models/Constants.ts index 99e609bca..a24574025 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.4.6"; + public static vNumber = "0.4.7"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { 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 new file mode 100644 index 000000000..5267e94aa --- /dev/null +++ b/assets/layers/bicycle_tube_vending_machine/bicycle_tube_vending_machine.json @@ -0,0 +1,151 @@ +{ + "id": "bicycle_tube_vending_machine", + "name": { + "en": "Bicycle tube vending machine" + }, + "title": { + "render": { + "en": "Bicycle tube vending machine" + }, + "mappings": [{ + "if": "name~*", + "then": "Bicycle tube vending machine {name}" + }] + }, + "icon": { + "render": "pin:#ffffff;./assets/layers/bicycle_tube_vending_machine/pinIcon.svg" + }, + "iconOverlays": [ + { + "if": { + "or": [ + "operational_status=broken", + "operational_status=closed" + ] + }, + "then": "close:#c33", + "badge": true + } + ], + "iconSize": "50,50,bottom", + "overpassTags": { + "and": [ + "amenity=vending_machine", + "vending~.*bicycle_tube.*" + ] + }, + "minzoom": 13, + "wayHandling": 2, + "presets": [ + { + "title": { + "en": "Bicycle tube vending machine" + }, + "tags": [ + "amenity=vending_machine", + "vending=bicycle_tube", + "vending:bicycle_tube=yes" + ] + } + ], + "color": "#6bc4f7", + "tagRenderings": [ + "images", + { + "#": "Still in use?", + "question": { + "en": "Is this vending machine still operational?" + }, + "render": { + "en": "The operational status is {operational_status" + }, + "freeform": { + "key": "operational_status" + }, + "mappings": [ + { + "if": "operational_status=", + "then": { + "en": "This vending machine works" + } + }, + { + "if": "operational_status=broken", + "then": { + "en": "This vending machine is broken" + } + }, + { + "if": "operational_status=closed", + "then": { + "en": "This vending machine is closed" + } + } + ] + }, + { + "question": "How much does a bicycle tube cost?", + "render": "A bicycle tube costs {charge}", + "freeform": { + "key": "charge" + } + }, + { + "question": "How can one pay at this tube vending machine?", + "mappings": [ + { + "if": "payment:coins=yes", + "then": "Payment with coins is possible" + }, + { + "if": "payment:notes=yes", + "then": "Payment with notes is possible" + }, + { + "if": "payment:cards=yes", + "then": "Payment with cards is possible" + } + ], + "multiAnswer": true + }, + { + "question": "Which brand of tubes are sold here?", + "freeform": { + "key": "brand" + }, + "mappings": [ + { + "if": "brand=Continental", + "then": "Continental tubes are sold here" + }, + { + "if": "brand=Schwalbe", + "then": "Schwalbe tubes are sold here" + } + ], + "multiAnswer": true + }, + { + "question": "Are other bicycle bicycle accessories sold here?", + "mappings": [ + { + "if": "vending:bicycle_light=yes", + "then": "Bicycle lights are sold here" + }, + { + "if": "vending:gloves=yes", + "then": "Gloves are sold here" + }, + { + "if": "vending:bicycle_repair_kit=yes", + "then": "Bicycle repair kits are sold here" + }, + { + "if": "vending:bicycle_pump=yes", + "then": "Bicycle pumps are sold here" + } + ], + "multiAnswer": true + } + ] +} diff --git a/assets/layers/bicycle_tube_vending_machine/pinIcon.svg b/assets/layers/bicycle_tube_vending_machine/pinIcon.svg new file mode 100644 index 000000000..7ca9e6785 --- /dev/null +++ b/assets/layers/bicycle_tube_vending_machine/pinIcon.svg @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/assets/layers/bicycle_tube_vending_machine/tube.svg b/assets/layers/bicycle_tube_vending_machine/tube.svg new file mode 100644 index 000000000..c527b7554 --- /dev/null +++ b/assets/layers/bicycle_tube_vending_machine/tube.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + +