From 2a31badd3d8dbbf8ebfb065620c820a0d1303a40 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 8 Jan 2021 03:57:18 +0100 Subject: [PATCH] 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 79891f1..5382561 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 23a2264..81be65c 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 add0f72..5817bb5 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 7f2541b..441530d 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 6aed204..7242799 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 588af7f..dcf0c63 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 2114004..89b6cd3 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 a884b34..d758682 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 01c29e6..57e7e99 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 7fd00a7..b6ba2c3 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 d1fda3f..947e616 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 ea66f4d..2665ea1 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 0000000..e6b5c01 --- /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 96c03ee..0000000 --- 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 9a54711..f8676ce 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 4ef6ddf..5c4f8af 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 );