diff --git a/AllTranslationAssets.ts b/AllTranslationAssets.ts index da48227..f4ca2c3 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 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."} ), 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."} ), @@ -92,7 +93,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:

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:

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:

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/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts index 3195daa..2a3467e 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/Customizations/JSON/FromJSON.ts b/Customizations/JSON/FromJSON.ts index ed66275..7d2fbd0 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/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 79891f1..6dc3f63 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,53 @@ 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) { + 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 +287,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 85f366c..a1f58c7 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; @@ -74,34 +75,62 @@ 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; 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; } } - return new LayerConfig(layer, this.roamingRenderings, `${this.id}.layers[${i}]`); - }); + // @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; + } + // @ts-ignore + layer = Utils.Merge(layer.override, shared); + } + // @ts-ignore + 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, 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 fd58ebb..9f56ac4 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 53ff1dc..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[]; }; - multiAnswer: boolean; + 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 @@ -49,7 +50,13 @@ export default class TagRenderingConfig { 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, @@ -69,7 +76,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 +118,8 @@ export default class TagRenderingConfig { } } - if (this.freeform?.key === undefined){ + + if (this.freeform?.key === undefined) { return this.render; } 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 379ee94..1bdc620 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" @@ -12,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" @@ -21,45 +20,58 @@ 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, + bicycle_tube_vending_machine, + 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/InitUiElements.ts b/InitUiElements.ts index c12ca08..e1c8726 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -40,6 +40,8 @@ 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"; +import HistoryHandling from "./Logic/Actors/HistoryHandling"; export class InitUiElements { @@ -157,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'); }); @@ -172,9 +180,7 @@ export class InitUiElements { }); - new FullScreenMessageBox(() => { - State.state.selectedElement.setData(undefined) - }).AttachTo("messagesboxmobile"); + new FullScreenMessageBox().AttachTo("messagesboxmobile"); InitUiElements.OnlyIf(State.state.featureSwitchWelcomeMessage, () => { @@ -279,20 +285,23 @@ 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) + if (Hash.hash.data === undefined) { + 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 +335,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 0000000..6b317a2 --- /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 a7041d6..bc18513 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 0000000..8093b64 --- /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/Actors/UpdateFromOverpass.ts b/Logic/Actors/UpdateFromOverpass.ts index 63067b5..ab4e95b 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/Logic/FeatureSource/FilteringFeatureSource.ts b/Logic/FeatureSource/FilteringFeatureSource.ts index 9ab1097..cfc9c94 100644 --- a/Logic/FeatureSource/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/FilteringFeatureSource.ts @@ -32,23 +32,55 @@ 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 (FilteringFeatureSource.showLayer(layer, location)) { + return true; + } + // Does it match any other layer? + for (const toCheck of layers) { + if (!FilteringFeatureSource.showLayer(toCheck, location)) { + continue; + } + if (toCheck.layerDef.overpassTags.matchesProperties(f.feature.properties)) { + return true; + } + } + return false; + }); self.features.setData(newFeatures); } for (const layer of layers) { layerDict[layer.layerDef.id] = layer; - layer.isDisplayed.addCallback(() => { - update()}) } upstream.features.addCallback(() => { - update()}); - location.map(l => l.zoom).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) { + continue; + } + if (!layers[i].isDisplayed.data) { + continue; + } + displayedLayerIndexes.push(i); + } + return displayedLayerIndexes.join(",") + }, layers.map(l => l.isDisplayed)) + .addCallback(() => { + 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/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index e4eb43c..bfad13e 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -101,6 +101,14 @@ export default class MetaTagging { // AUtomatically triggered on the next change const updateTags = () => { const oldValueIsOpen = tags["_isOpen"]; + const oldNextChange =tags["_isOpen:nextTrigger"] ?? 0; + + if(oldNextChange > (new Date()).getTime() && + tags["_isOpen:oldvalue"] === tags["opening_hours"]){ + // Already calculated and should not yet be triggered + return; + } + tags["_isOpen"] = oh.getState() ? "yes" : "no"; const comment = oh.getComment(); if (comment) { @@ -113,10 +121,16 @@ export default class MetaTagging { const nextChange = oh.getNextChange(); if (nextChange !== undefined) { + const timeout = nextChange.getTime() - (new Date()).getTime(); + tags["_isOpen:nextTrigger"] = nextChange.getTime(); + tags["_isOpen:oldvalue"] = tags.opening_hours window.setTimeout( - updateTags, - (nextChange.getTime() - (new Date()).getTime()) - ) + () => { + console.log("Updating the _isOpen tag for ", tags.id); + updateTags(); + }, + timeout + ) } } updateTags(); diff --git a/Logic/Osm/Geocoding.ts b/Logic/Osm/Geocoding.ts index 85f5c5e..f2ee260 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 031b1e1..1b84951 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]); + 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) { + if (actualValue.indexOf(neededValue) < 0) { + return false; + } + } + } + return true; + } +} \ No newline at end of file diff --git a/Logic/Web/Hash.ts b/Logic/Web/Hash.ts index 6ca2d99..07372f2 100644 --- a/Logic/Web/Hash.ts +++ b/Logic/Web/Hash.ts @@ -1,18 +1,52 @@ import {UIEventSource} from "../UIEventSource"; +import {Utils} from "../../Utils"; export default class Hash { - - public static Get() : UIEventSource{ + + 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") { + console.warn("Got a literal 'undefined' as hash, ignoring") + h = undefined; + } + + 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)) + 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/MangroveReviews.ts b/Logic/Web/MangroveReviews.ts index 453aa71..12c0247 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/Logic/Web/QueryParameters.ts b/Logic/Web/QueryParameters.ts index 08cd37a..406afa2 100644 --- a/Logic/Web/QueryParameters.ts +++ b/Logic/Web/QueryParameters.ts @@ -43,26 +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)) } - history.replaceState(null, "", "?" + parts.join("&") + "#" + Hash.Get().data); + // Don't pollute the history every time a parameter changes + + history.replaceState(null, "", "?" + parts.join("&") + Hash.Current()); } diff --git a/Models/Constants.ts b/Models/Constants.ts index cb9f6b7..290c686 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -1,8 +1,8 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.3.0" + - "a"; + + public static vNumber = "0.4.7"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { @@ -15,7 +15,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/State.ts b/State.ts index 7b92cf8..2be700e 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 @@ -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(""); @@ -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/Svg.ts b/Svg.ts index e37eccb..246569d 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/Img.ts b/UI/Base/Img.ts index 66b4e79..1186ae9 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/Base/LazyElement.ts b/UI/Base/LazyElement.ts index c153d80..9796a6c 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/Base/Ornament.ts b/UI/Base/Ornament.ts new file mode 100644 index 0000000..2cbf759 --- /dev/null +++ b/UI/Base/Ornament.ts @@ -0,0 +1,45 @@ +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") + + 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(() => { + let c = Number(index.data); + if(isNaN(c)){ + c = 0; + } + self._index.setData(""+ ((c + 1) % (Ornament.ornamentsCount + 1))); + + }) + } + + private static countOrnaments() { + let ornamentCount = 0; + for (const key in Svg.All) { + if (key.startsWith("Ornament-Horiz-")) { + ornamentCount++; + } + } + return ornamentCount; + } + + InnerRender(): string { + if(this._index.data == "0"){ + return "" + } + 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 new file mode 100644 index 0000000..8fd910d --- /dev/null +++ b/UI/Base/ScrollableFullScreen.ts @@ -0,0 +1,38 @@ +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 + */ +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") + 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,"", 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(); + } + + +} \ No newline at end of file diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index 8dd0d80..b53a59e 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/LayerControlPanel.ts b/UI/BigComponents/LayerControlPanel.ts index 8b90df0..76f7ac4 100644 --- a/UI/BigComponents/LayerControlPanel.ts +++ b/UI/BigComponents/LayerControlPanel.ts @@ -3,14 +3,17 @@ 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{ +export default class LayerControlPanel extends UIElement { private readonly _panel: 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"); @@ -20,14 +23,19 @@ 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]); } - this._panel = layerControlPanel; + + + const title =Translations.t.general.layerSelection.title.SetClass("featureinfobox-title") + + this._panel = new ScrollableFullScreen(title, layerControlPanel); } - + InnerRender(): string { return this._panel.Render(); } - + } \ No newline at end of file diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index 3766bdf..d812b68 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[]; @@ -33,7 +36,7 @@ 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(); @@ -41,9 +44,12 @@ export default class LayerSelection extends UIElement { return "" })) 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/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index 7fe81eb..e0f1ffe 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -40,15 +40,22 @@ export default class MoreScreen extends UIElement { } const currentLocation = State.state.locationControl.data; + let path = window.location.pathname; + // Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html' + path = path.substr(0, path.lastIndexOf("/")); + // Path will now contain '/dir/dir', or empty string in case of nothing + if(path === ""){ + path = "." + } let linkText = - `./${layout.id.toLowerCase()}.html?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` + `${path}/${layout.id.toLowerCase()}?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { - linkText = `./index.html?layout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` + linkText = `${path}/index.html?layout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` } if (customThemeDefinition) { - linkText = `./index.html?userlayout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}` + linkText = `${path}/?userlayout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}` } diff --git a/UI/BigComponents/SearchAndGo.ts b/UI/BigComponents/SearchAndGo.ts index 6603ef8..9d945a6 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/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts index 3f61e43..4301894 100644 --- a/UI/BigComponents/ShareScreen.ts +++ b/UI/BigComponents/ShareScreen.ts @@ -147,13 +147,15 @@ export default class ShareScreen extends UIElement { const url = (currentLocation ?? new UIEventSource(undefined)).map(() => { const host = window.location.host; - let literalText = `https://${host}/${layout.id.toLowerCase()}.html` + let path = window.location.pathname; + path = path.substr(0, path.lastIndexOf("/")); + let literalText = `https://${host}${path}/${layout.id.toLowerCase()}` const parts = Utils.NoEmpty(Utils.NoNull(optionParts.map((eventSource) => eventSource.data))); let hash = ""; if (layoutDefinition !== undefined) { - literalText = `https://${host}/index.html` + literalText = `https://${host}${path}/` if (layout.id.startsWith("wiki:")) { parts.push("userlayout=" + encodeURIComponent(layout.id)) } else { diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index c2c73ae..cecafdb 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/BigComponents/WelcomeMessage.ts b/UI/BigComponents/ThemeIntroductionPanel.ts similarity index 69% rename from UI/BigComponents/WelcomeMessage.ts rename to UI/BigComponents/ThemeIntroductionPanel.ts index de1179c..3c68816 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/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/FullScreenMessageBoxHandler.ts b/UI/FullScreenMessageBoxHandler.ts index 4b19508..9fa2493 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") - } @@ -33,18 +19,15 @@ export default class FullScreenMessageBox extends UIElement { if (State.state.fullScreenMessage.data === undefined) { 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(); + this._content = State.state.fullScreenMessage.data.content; + 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 6e36879..8b3638c 100644 --- a/UI/OpeningHours/OhVisualization.ts +++ b/UI/OpeningHours/OhVisualization.ts @@ -196,8 +196,8 @@ export default class OpeningHoursVisualization extends UIElement { // Closed! const opensAtDate = oh.getNextChange(); if(opensAtDate === undefined){ - const comm = oh.getComment(); - if(comm !== undefined){ + const comm = oh.getComment() ?? oh.getUnknown(); + if(!!comm){ return new FixedUiElement(comm).SetClass("ohviz-closed").Render(); } diff --git a/UI/OpeningHours/OpeningHoursInput.ts b/UI/OpeningHours/OpeningHoursInput.ts index 7a9ca26..8e9a7c9 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/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index 68d1452..11af718 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -7,6 +7,7 @@ import Combine from "../Base/Combine"; import TagRenderingAnswer from "./TagRenderingAnswer"; import State from "../../State"; import Svg from "../../Svg"; +import {TagUtils} from "../../Logic/Tags"; export default class EditableTagRendering extends UIElement { private readonly _tags: UIEventSource; @@ -45,6 +46,29 @@ export default class EditableTagRendering extends UIElement { } } + InnerRender(): string { + if (!this._configuration?.condition?.matchesProperties(this._tags.data)) { + return ""; + } + if (this._editMode.data) { + return this._question.Render(); + } + if (this._configuration.multiAnswer) { + const atLeastOneMatch = this._configuration.mappings.some(mp =>TagUtils.MatchesMultiAnswer(mp.if, this._tags.data)); + console.log("SOME MATCH?", atLeastOneMatch) + if (!atLeastOneMatch) { + return ""; + } + } else if (this._configuration.GetRenderValue(this._tags.data) === undefined) { + return ""; + } + + return new Combine([this._answer, + (State.state?.osmConnection?.userDetails?.data?.loggedIn ?? true) ? this._editButton : undefined + ]).SetClass("answer") + .Render(); + } + private GenerateQuestion() { const self = this; if (this._configuration.question !== undefined) { @@ -64,25 +88,4 @@ export default class EditableTagRendering extends UIElement { } } - - InnerRender(): string { - - if (this._editMode.data) { - return this._question.Render(); - } - - if(this._configuration.GetRenderValue(this._tags.data)=== undefined){ - return ""; - } - - if(!this._configuration?.condition?.matchesProperties(this._tags.data)){ - return ""; - } - - return new Combine([this._answer, - (State.state?.osmConnection?.userDetails?.data?.loggedIn ?? true) ? this._editButton : undefined - ]).SetClass("answer") - .Render(); - } - } \ No newline at end of file diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 7a58c93..b0b59d1 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -6,17 +6,14 @@ 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 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 _component: UIElement; + public title: UIElement ; + constructor( tags: UIEventSource, layerConfig: LayerConfig @@ -25,14 +22,12 @@ export default class FeatureInfoBox extends UIElement { if (layerConfig === undefined) { throw "Undefined layerconfig" } - this._tags = tags; - this._layerConfig = layerConfig; - this._title = layerConfig.title === undefined ? undefined : - new TagRenderingAnswer(tags, layerConfig.title) - .SetClass("featureinfobox-title"); - this._titleIcons = new Combine( + 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"); @@ -42,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! @@ -50,25 +45,26 @@ 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); } + const tail = new Combine([]).SetClass("only-on-mobile"); + + const content = new Combine([ + ...renderings, + 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._title, this._titleIcons]) - .SetClass("featureinfobox-titlebar"), - new Combine([ - ...this._renderings, - this._questionBox, - new FixedUiElement("").SetClass("featureinfobox-tail") - ] - ).SetClass("featureinfobox-content"), - ]).SetClass("featureinfobox") - .Render(); + return this._component.Render(); } - } diff --git a/UI/Popup/QuestionBox.ts b/UI/Popup/QuestionBox.ts index 991d11a..0b7e85b 100644 --- a/UI/Popup/QuestionBox.ts +++ b/UI/Popup/QuestionBox.ts @@ -3,6 +3,7 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import TagRenderingQuestion from "./TagRenderingQuestion"; import Translations from "../i18n/Translations"; +import {TagUtils} from "../../Logic/Tags"; /** @@ -46,20 +47,39 @@ export default class QuestionBox extends UIElement { }) } + /** + * Returns true if it is known or not shown, false if the question should be asked + * @constructor + */ + IsKnown(tagRendering: TagRenderingConfig): boolean { + if (tagRendering.condition && + !tagRendering.condition.matchesProperties(this._tags.data)) { + // Filtered away by the condition + return true; + } + if(tagRendering.multiAnswer){ + for (const m of tagRendering.mappings) { + if(TagUtils.MatchesMultiAnswer(m.if, this._tags.data)){ + return true; + } + } + } + + if (tagRendering.GetRenderValue(this._tags.data) !== undefined) { + // This value is known and can be rendered + return true; + } + + return false; + } + InnerRender(): string { for (let i = 0; i < this._tagRenderingQuestions.length; i++) { let tagRendering = this._tagRenderings[i]; - if(tagRendering.condition && - !tagRendering.condition.matchesProperties(this._tags.data)){ - // Filtered away by the condition + + if(this.IsKnown(tagRendering)){ continue; } - - if (tagRendering.GetRenderValue(this._tags.data) !== undefined) { - // This value is known - continue; - } - if (this._skippedQuestions.data.indexOf(i) >= 0) { continue; diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 57ddf68..f02bcd8 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -2,6 +2,9 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import {UIElement} from "../UIElement"; import {SubstitutedTranslation} from "../SpecialVisualizations"; +import {Utils} from "../../Utils"; +import Combine from "../Base/Combine"; +import {TagUtils} from "../../Logic/Tags"; /*** * Displays the correct value for a known tagrendering @@ -15,7 +18,7 @@ export default class TagRenderingAnswer extends UIElement { super(tags); this._tags = tags; this._configuration = configuration; - if(configuration === undefined){ + if (configuration === undefined) { throw "Trying to generate a tagRenderingAnswer without configuration..." } } @@ -32,12 +35,38 @@ export default class TagRenderingAnswer extends UIElement { return ""; } const tr = this._configuration.GetRenderValue(tags); - if (tr === undefined) { - return ""; + if (tr !== undefined) { + this._content = new SubstitutedTranslation(tr, this._tags); + return this._content.Render(); } - // Bit of a hack; remember that the fields are updated - this._content = new SubstitutedTranslation(tr, this._tags); - return this._content.Render(); + + // The render value doesn't work well with multi-answers (checkboxes), so we have to check for them manually + if (this._configuration.multiAnswer) { + const applicableThens = Utils.NoNull(this._configuration.mappings.map(mapping => { + if (mapping.if === undefined) { + return mapping.then; + } + if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) { + return mapping.then; + } + return undefined; + })) + if (applicableThens.length >= 0) { + if (applicableThens.length === 1) { + this._content = applicableThens[0]; + } else { + this._content = new Combine(["
    ", + ...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 bfeb82e..b6ba2c3 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); @@ -65,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/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 6a8317a..1b03a56 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; @@ -68,6 +68,16 @@ export default class ShowDataLayer { action(); } }); + 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(); + } + }) } @@ -114,9 +124,10 @@ 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('popupclose', () => { + popup.on('remove', () => { State.state.selectedElement.setData(undefined); }); leafletLayer.bindPopup(popup); @@ -126,21 +137,24 @@ 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(); + State.state.selectedElement.setData(feature); } - - - if (feature.properties.id.replace(/\//g, "_") === Hash.Get().data) { + 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/UI/UIElement.ts b/UI/UIElement.ts index 6c91ad5..947e616 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -1,27 +1,19 @@ 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 clss: string[] = [] + private style: string; + private _hideIfEmpty = 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 _onClick: () => void; + private _onHover: UIEventSource; protected constructor(source: UIEventSource = undefined) { super(""); @@ -32,7 +24,6 @@ export abstract class UIElement extends UIEventSource { this.ListenTo(source); } - public ListenTo(source: UIEventSource) { if (source === undefined) { return this; @@ -46,8 +37,6 @@ export abstract class UIElement extends UIEventSource { return this; } - private _onClick: () => void; - public onClick(f: (() => void)) { this.dumbMode = false; this._onClick = f; @@ -56,8 +45,6 @@ export abstract class UIElement extends UIEventSource { return this; } - private _onHover: UIEventSource; - public IsHovered(): UIEventSource { this.dumbMode = false; if (this._onHover !== undefined) { @@ -69,10 +56,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 +88,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 +110,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 +160,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 +170,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 9482766..55365ce 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 7866df2..d9d3940 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, @@ -176,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/layers/bicycle_library/bicycle_library.svg b/assets/layers/bicycle_library/bicycle_library.svg index 3692a1b..a4d017c 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/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 0000000..5267e94 --- /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 0000000..7ca9e67 --- /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 0000000..c527b75 --- /dev/null +++ b/assets/layers/bicycle_tube_vending_machine/tube.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + 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_repair_station/bike_repair_station.json b/assets/layers/bike_repair_station/bike_repair_station.json index d71665f..18a6638 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/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/assets/svg/Ornament-Horiz-0.svg b/assets/svg/Ornament-Horiz-0.svg new file mode 100644 index 0000000..83fabd3 --- /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 0000000..1475dd7 --- /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 0000000..a5f18fe --- /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 0000000..c9a3356 --- /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 0000000..ade8558 --- /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 0000000..9367c3e --- /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 0000000..b2f2e4e --- /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 0000000..ce2f36f --- /dev/null +++ b/assets/svg/back.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index 16265f6..e9aeefa 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 0000000..93c8d01 --- /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 0000000..7256b88 --- /dev/null +++ b/assets/themes/bicycle_library/logo.svg @@ -0,0 +1,166 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/themes/cyclofix/cyclofix.json b/assets/themes/cyclofix/cyclofix.json index 1da6ad4..d8e6bf4 100644 --- a/assets/themes/cyclofix/cyclofix.json +++ b/assets/themes/cyclofix/cyclofix.json @@ -13,18 +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", "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", + "bicycle_tube_vending_machine", + "drinking_water", + "bike_themed_object", + "bike_cleaning", + "bike_parking" + ], "roamingRenderings": [] } \ No newline at end of file diff --git a/assets/translations.json b/assets/translations.json index b5be39a..77cbb72 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", @@ -744,14 +754,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/fullscreenmessagebox.css b/css/fullscreenmessagebox.css index 4f67377..2c681b9 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; @@ -15,11 +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) - var(--return-to-the-map-height) - 2em) !important; - display: block; + max-height: calc(100vh - var(--variable-title-height)) !important; + 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 { @@ -31,33 +35,10 @@ padding: 0.5em; width: 100%; box-sizing: border-box; - + } .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 148f227..f2986b7 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 acba2ae..eb3290b 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,30 +23,64 @@ padding-right: 0.1em; } -.featureinfobox-titlebar{ +.featureinfobox-titlebar { + border-bottom: 2px solid var(--foreground-color); + box-shadow: 0 10px 10px -10px var(--shadow-color); + display: flex; + justify-content: space-between; + width: 100%; + overflow-x: hidden; +} + +.featureinfobox-titlebar-title { font-size: large; font-weight: bold; display: flex; justify-content: space-between; - border-bottom: 2px solid var(--foreground-color); + 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; overflow-y: auto; - overflow-x: hidden; padding-top: 1em; + width:100%; + overflow-x: hidden; } + @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 +89,7 @@ max-height: unset !important; height: 100vh; } - + .answer { max-width: 48% !important; padding-right: 0.3em; @@ -63,8 +101,8 @@ padding-right: 0.3em; box-sizing: border-box; } - - .first-rendering{ + + .first-rendering { position: absolute; left: 50%; width: 94%; @@ -129,7 +167,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 +199,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 +242,34 @@ 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: var(--return-to-the-map-height); + position: fixed; + width: 100vw; + bottom: 0; +} + +.to-the-map-inner{ + font-size: xx-large; +} diff --git a/index.css b/index.css index b7b7406..486bdbd 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; - - --return-to-the-map-height: 5em; - --variable-title-height: 0px; /*Set by javascript dynamically*/ + --variable-title-height: 0px; /* Set by javascript */ + --return-to-the-map-height: 2em; } 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,39 @@ 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; + flex-shrink: 0; } -.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 +552,7 @@ a { height: 2.5em; width: 2.5em; box-sizing: border-box; - padding:0; + padding: 0; } .share-button svg { @@ -563,12 +564,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 diff --git a/index.ts b/index.ts index 0efc118..a62a0b6 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 ----------------- @@ -77,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 @@ -88,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; } }, diff --git a/scripts/createLayouts.ts b/scripts/createLayouts.ts index 38d3970..46ebd91 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 3bc9c9b..d3a60f1 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 {Utils} from "../Utils"; +Utils.runningFromConsole = true; import {equal} from "assert"; import T from "./TestHelper"; import {FromJSON} from "../Customizations/JSON/FromJSON"; @@ -12,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"; @@ -42,7 +39,20 @@ 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) + + 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", (() => { @@ -94,7 +104,7 @@ new T([ } ], condition: "x=" - }, ""); + }, undefined,""); equal(undefined, tr.GetRenderValue({"foo": "bar"})); equal("Has no name", tr.GetRenderValue({"noname": "yes"})?.txt); @@ -147,7 +157,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 );