From a57b7d93fa7ae30f27ee47e5fe5ca26cd6f58606 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 30 Aug 2020 01:13:18 +0200 Subject: [PATCH] Huge refactorings of JSON-parsing and Tagsfilter, other cleanups, warning cleanups and lots of small subtle bugfixes --- Customizations/AllKnownLayouts.ts | 23 +- Customizations/JSON/CustomLayoutFromJSON.ts | 306 -------- Customizations/JSON/FromJSON.ts | 259 +++++++ Customizations/JSON/LayerConfigJson.ts | 79 ++ Customizations/JSON/LayoutConfigJson.ts | 98 +++ Customizations/JSON/TagConfig.ts | 14 + Customizations/JSON/TagConfigJson.ts | 8 + Customizations/JSON/TagRenderingConfigJson.ts | 51 ++ Customizations/LayerDefinition.ts | 8 +- Customizations/Layers/BikeCafes.ts | 34 +- Customizations/Layers/BikeOtherShops.ts | 12 +- Customizations/Layers/BikeParkings.ts | 31 +- Customizations/Layers/BikeShops.ts | 8 +- Customizations/Layers/BikeStations.ts | 6 +- Customizations/Layers/Birdhide.ts | 5 +- Customizations/Layers/Bos.ts | 2 +- Customizations/Layers/ClimbingTree.ts | 2 +- Customizations/Layers/DrinkingWater.ts | 11 +- Customizations/Layers/GhostBike.ts | 2 +- Customizations/Layers/GrbToFix.ts | 6 +- Customizations/Layers/InformationBoard.ts | 2 +- Customizations/Layers/Map.ts | 2 +- Customizations/Layers/NatureReserves.ts | 2 +- Customizations/Layers/Park.ts | 4 +- Customizations/Layers/Viewpoint.ts | 5 +- Customizations/Layers/Widths.ts | 23 +- Customizations/Layout.ts | 17 +- Customizations/Layouts/All.ts | 20 - Customizations/Layouts/ClimbingTrees.ts | 1 - Customizations/Layouts/Cyclofix.ts | 3 +- Customizations/Layouts/GhostBikes.ts | 1 - Customizations/Layouts/Smoothness.ts | 2 +- Customizations/OnlyShowIf.ts | 44 +- Customizations/Questions/AccessTag.ts | 3 +- Customizations/Questions/FixedText.ts | 1 - Customizations/Questions/NameInline.ts | 3 +- Customizations/Questions/NameQuestion.ts | 2 +- Customizations/Questions/OperatorTag.ts | 3 +- Customizations/Questions/OsmLink.ts | 2 +- Customizations/Questions/bike/CafeDiy.ts | 2 +- Customizations/Questions/bike/CafePump.ts | 2 +- Customizations/Questions/bike/CafeRepair.ts | 2 +- .../Questions/bike/ParkingAccessCargo.ts | 2 +- .../Questions/bike/ParkingCapacityCargo.ts | 1 - .../Questions/bike/ParkingCovered.ts | 2 +- .../Questions/bike/ParkingOperator.ts | 2 +- Customizations/Questions/bike/ParkingType.ts | 2 +- .../Questions/bike/PumpManometer.ts | 2 +- Customizations/Questions/bike/PumpManual.ts | 2 +- .../Questions/bike/PumpOperational.ts | 2 +- Customizations/Questions/bike/PumpValves.ts | 2 +- Customizations/Questions/bike/ShopDiy.ts | 2 +- Customizations/Questions/bike/ShopPump.ts | 2 +- Customizations/Questions/bike/ShopRental.ts | 2 +- Customizations/Questions/bike/ShopRepair.ts | 2 +- Customizations/Questions/bike/ShopRetail.ts | 2 +- .../Questions/bike/ShopSecondHand.ts | 2 +- Customizations/Questions/bike/StationBrand.ts | 29 - Customizations/Questions/bike/StationChain.ts | 2 +- .../Questions/bike/StationOperator.ts | 2 +- .../Questions/bike/StationPumpTools.ts | 2 +- Customizations/Questions/bike/StationStand.ts | 2 +- Customizations/TagRendering.ts | 34 +- Customizations/TagRenderingOptions.ts | 45 +- Customizations/UIElementConstructor.ts | 3 +- InitUiElements.ts | 19 +- Logic/ElementStorage.ts | 5 - Logic/FilteredLayer.ts | 32 +- Logic/GeoOperations.ts | 43 +- Logic/ImageSearcher.ts | 4 +- Logic/LayerUpdater.ts | 50 +- Logic/Leaflet/Basemap.ts | 10 +- Logic/Leaflet/GeoLocationHandler.ts | 12 +- Logic/Leaflet/StrayClickHandler.ts | 2 - Logic/Osm/Changes.ts | 12 +- Logic/Osm/ChangesetHandler.ts | 38 +- Logic/Osm/OsmConnection.ts | 19 +- Logic/Osm/OsmImageUploadHandler.ts | 10 +- Logic/Osm/OsmObject.ts | 24 +- Logic/Osm/OsmPreferences.ts | 7 +- Logic/Osm/Overpass.ts | 8 +- Logic/PersonalLayersPanel.ts | 14 +- Logic/{TagsFilter.ts => Tags.ts} | 179 ++--- Logic/Web/Imgur.ts | 6 +- Logic/Web/Wikimedia.ts | 2 +- State.ts | 17 +- UI/Base/TabbedComponent.ts | 4 +- UI/CustomThemeGenerator/Preview.ts | 57 -- UI/CustomThemeGenerator/ThemeGenerator.ts | 712 ------------------ UI/FeatureInfoBox.ts | 16 +- UI/Image/ImageCarousel.ts | 5 +- UI/Image/ImageCarouselWithUpload.ts | 6 +- UI/Input/TextField.ts | 92 +-- UI/MoreScreen.ts | 31 +- UI/ShareScreen.ts | 41 +- UI/SimpleAddUI.ts | 21 +- UI/WelcomeMessage.ts | 5 - UI/i18n/Translation.ts | 10 +- UI/i18n/Translations.ts | 18 +- assets/themes/aed/aed.json | 183 +++-- assets/themes/artwork/artwork.json | 306 ++++---- assets/themes/bookcases/Bookcases.json | 250 +++--- assets/themes/buurtnatuur/buurtnatuur.be.json | 2 + assets/themes/cyclestreets/cyclestreets.json | 179 ++--- assets/themes/spanishResidential.json | 120 --- assets/themes/toilets/toilets.json | 125 +-- customGenerator.ts | 81 +- deploy.sh | 8 +- index.css | 7 +- index.ts | 15 +- preferences.ts | 2 +- test.ts | 19 - test/Tag.spec.ts | 72 +- 113 files changed, 1565 insertions(+), 2594 deletions(-) delete mode 100644 Customizations/JSON/CustomLayoutFromJSON.ts create mode 100644 Customizations/JSON/FromJSON.ts create mode 100644 Customizations/JSON/LayerConfigJson.ts create mode 100644 Customizations/JSON/LayoutConfigJson.ts create mode 100644 Customizations/JSON/TagConfig.ts create mode 100644 Customizations/JSON/TagConfigJson.ts create mode 100644 Customizations/JSON/TagRenderingConfigJson.ts delete mode 100644 Customizations/Layouts/All.ts delete mode 100644 Customizations/Questions/bike/StationBrand.ts rename Logic/{TagsFilter.ts => Tags.ts} (52%) delete mode 100644 UI/CustomThemeGenerator/Preview.ts delete mode 100644 UI/CustomThemeGenerator/ThemeGenerator.ts create mode 100644 assets/themes/buurtnatuur/buurtnatuur.be.json delete mode 100644 assets/themes/spanishResidential.json diff --git a/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts index 6c40605..639c0d7 100644 --- a/Customizations/AllKnownLayouts.ts +++ b/Customizations/AllKnownLayouts.ts @@ -1,6 +1,5 @@ import {LayerDefinition} from "./LayerDefinition"; import {Layout} from "./Layout"; -import {All} from "./Layouts/All"; import {Groen} from "./Layouts/Groen"; import Cyclofix from "./Layouts/Cyclofix"; import {StreetWidth} from "./Layouts/StreetWidth"; @@ -10,12 +9,14 @@ import {Smoothness} from "./Layouts/Smoothness"; import {MetaMap} from "./Layouts/MetaMap"; import {Natuurpunt} from "./Layouts/Natuurpunt"; import {GhostBikes} from "./Layouts/GhostBikes"; -import {CustomLayoutFromJSON} from "./JSON/CustomLayoutFromJSON"; +import {FromJSON} from "./JSON/FromJSON"; import * as bookcases from "../assets/themes/bookcases/Bookcases.json"; import * as aed from "../assets/themes/aed/aed.json"; import * as toilets from "../assets/themes/toilets/toilets.json"; import * as artworks from "../assets/themes/artwork/artwork.json"; import * as cyclestreets from "../assets/themes/cyclestreets/cyclestreets.json"; + + import {PersonalLayout} from "../Logic/PersonalLayout"; export class AllKnownLayouts { @@ -28,11 +29,11 @@ export class AllKnownLayouts { new GRB(), new Cyclofix(), new GhostBikes(), - CustomLayoutFromJSON.LayoutFromJSON(bookcases), - CustomLayoutFromJSON.LayoutFromJSON(aed), - CustomLayoutFromJSON.LayoutFromJSON(toilets), - CustomLayoutFromJSON.LayoutFromJSON(artworks), - CustomLayoutFromJSON.LayoutFromJSON(cyclestreets), + FromJSON.LayoutFromJSON(bookcases), + // FromJSON.LayoutFromJSON(aed), + // FromJSON.LayoutFromJSON(toilets), + // FromJSON.LayoutFromJSON(artworks), + // FromJSON.LayoutFromJSON(cyclestreets), new MetaMap(), new StreetWidth(), @@ -48,26 +49,22 @@ export class AllKnownLayouts { private static AllLayouts(): Map { - const all = new All(); this.allLayers = new Map(); for (const layout of this.layoutsList) { for (const layer of layout.layers) { - const key = layer.id; if (this.allLayers[layer.id] !== undefined) { continue; } this.allLayers[layer.id] = layer; this.allLayers[layer.id.toLowerCase()] = layer; - all.layers.push(layer); } } const allSets: Map = new Map(); for (const layout of this.layoutsList) { - allSets[layout.name] = layout; - allSets[layout.name.toLowerCase()] = layout; + allSets[layout.id] = layout; + allSets[layout.id.toLowerCase()] = layout; } - allSets[all.name] = all; return allSets; } diff --git a/Customizations/JSON/CustomLayoutFromJSON.ts b/Customizations/JSON/CustomLayoutFromJSON.ts deleted file mode 100644 index 6a6e415..0000000 --- a/Customizations/JSON/CustomLayoutFromJSON.ts +++ /dev/null @@ -1,306 +0,0 @@ -import {TagRenderingOptions} from "../TagRenderingOptions"; -import {LayerDefinition, Preset} from "../LayerDefinition"; -import {Layout} from "../Layout"; -import Translation from "../../UI/i18n/Translation"; -import Combine from "../../UI/Base/Combine"; -import {And, Tag} from "../../Logic/TagsFilter"; -import FixedText from "../Questions/FixedText"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {TagDependantUIElementConstructor} from "../UIElementConstructor"; -import {Map} from "../Layers/Map"; -import {UIElement} from "../../UI/UIElement"; -import Translations from "../../UI/i18n/Translations"; - - -export interface TagRenderingConfigJson { - // If this key is present, then... - key?: string, - // Use this string to render - render?: string | any, - // One of string, int, nat, float, pfloat, email, phone. Default: string - type?: string, - // If it is not known (and no mapping below matches), this question is asked; a textfield is inserted in the rendering above - question?: string | any, - // If a value is added with the textfield, this extra tag is addded. Optional field - addExtraTags?: string | { k: string, v: string }[]; - // Extra tags: rendering is only shown/asked if these tags are present - condition?: string; - // Alternatively, these tags are shown if they match - even if the key above is not there - // If unknown, these become a radio button - mappings?: - { - if: string, - then: string | any - }[] -} - -export interface LayerConfigJson { - name: string; - title: string | any | TagRenderingConfigJson; - description: string | any; - minzoom: number | string, - icon?: TagRenderingConfigJson; - color?: TagRenderingConfigJson; - width?: TagRenderingConfigJson; - overpassTags: string | { k: string, v: string }[]; - wayHandling?: number, - widenFactor?: number, - presets: { - tags: string, - title: string | any, - description?: string | any, - icon?: string - }[], - tagRenderings: TagRenderingConfigJson [] -} - -export interface LayoutConfigJson { - widenFactor?: number; - name: string; - title: string | any; - description: string | any; - maintainer: string; - language: string | string[]; - layers: LayerConfigJson[], - startZoom: string | number; - startLat: string | number; - startLon: string | number; - /** - * Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64,' - */ - icon: string; -} - -export class CustomLayoutFromJSON { - - - public static FromQueryParam(layoutFromBase64: string): Layout { - return CustomLayoutFromJSON.LayoutFromJSON(JSON.parse(atob(layoutFromBase64))); - } - - public static TagRenderingFromJson(json: TagRenderingConfigJson): TagDependantUIElementConstructor { - - if(json === undefined){ - return undefined; - } - - if (typeof (json) === "string") { - return new FixedText(json); - } - - let freeform = undefined; - if (json.render !== undefined) { - const type = json.type ?? "text"; - let renderTemplate = CustomLayoutFromJSON.MaybeTranslation(json.render);; - const template = renderTemplate.replace("{" + json.key + "}", "$" + type + "$"); - if(type === "url"){ - renderTemplate = json.render.replace("{" + json.key + "}", - `{${json.key}}` - ); - } - - freeform = { - key: json.key, - template: template, - renderTemplate: renderTemplate, - extraTags: CustomLayoutFromJSON.TagsFromJson(json.addExtraTags), - } - if (freeform.key === "*") { - freeform.key = "id"; // Id is always there -> always take the rendering. Used for 'icon' and 'stroke' - } - } - - let mappings = undefined; - if (json.mappings !== undefined) { - mappings = []; - for (const mapping of json.mappings) { - mappings.push({ - k: new And(CustomLayoutFromJSON.TagsFromJson(mapping.if)), - txt: CustomLayoutFromJSON.MaybeTranslation(mapping.then) - }) - } - } - - const rendering = new TagRenderingOptions({ - question: CustomLayoutFromJSON.MaybeTranslation(json.question), - freeform: freeform, - mappings: mappings - }); - - if (json.condition) { - const conditionTags: Tag[] = CustomLayoutFromJSON.TagsFromJson(json.condition); - return rendering.OnlyShowIf(new And(conditionTags)); - } - return rendering; - } - - private static PresetFromJson(layout: any, preset: any): Preset { - const t = CustomLayoutFromJSON.MaybeTranslation; - const tags = CustomLayoutFromJSON.TagsFromJson; - return { - icon: preset.icon ?? CustomLayoutFromJSON.TagRenderingFromJson(layout.icon), - tags: tags(preset.tags) ?? tags(layout.overpassTags), - title: t(preset.title) ?? t(layout.title), - description: t(preset.description) ?? t(layout.description) - } - } - - private static StyleFromJson(layout: LayerConfigJson): ((tags: any) => { - color: string, - weight?: number, - icon: { - iconUrl: string, - iconSize: number[], - }, - }) { - const iconRendering: TagDependantUIElementConstructor = CustomLayoutFromJSON.TagRenderingFromJson(layout.icon); - const colourRendering = CustomLayoutFromJSON.TagRenderingFromJson(layout.color); - let thickness = CustomLayoutFromJSON.TagRenderingFromJson(layout.width); - - - return (tags) => { - const iconUrl = iconRendering.GetContent(tags); - const stroke = colourRendering.GetContent(tags) ?? "#00f"; - let weight = parseInt(thickness?.GetContent(tags)) ?? 10; - if(isNaN(weight)){ - weight = 10; - } - return { - color: stroke, - weight: weight, - icon: { - iconUrl: iconUrl, - iconSize: [40, 40], - }, - } - }; - } - - private static TagFromJson(json: string | { k: string, v: string }): Tag { - if (json === undefined) { - return undefined; - } - if (typeof (json) !== "string") { - return new Tag(json.k.trim(), json.v.trim()) - } - - let kv: string[] = undefined; - let invert = false; - let regex = false; - if (json.indexOf("!=") >= 0) { - kv = json.split("!="); - invert = true; - } else if (json.indexOf("~=") >= 0) { - kv = json.split("~="); - regex = true; - } else { - kv = json.split("="); - } - - if (kv.length !== 2) { - return undefined; - } - if (kv[0].trim() === "") { - return undefined; - } - let v = kv[1].trim(); - if(v.startsWith("/") && v.endsWith("/")){ - v = v.substr(1, v.length - 2); - regex = true; - } - return new Tag(kv[0].trim(), regex ? new RegExp(v): v, invert); - } - - public static TagsFromJson(json: string | { k: string, v: string }[]): Tag[] { - if (json === undefined) { - return undefined; - } - if (json === "") { - return []; - } - let tags = []; - if (typeof (json) === "string") { - tags = json.split("&").map(CustomLayoutFromJSON.TagFromJson); - } else { - tags = json.map(x => {CustomLayoutFromJSON.TagFromJson(x)}); - } - for (const tag of tags) { - if (tag === undefined) { - return undefined; - } - } - return tags; - } - - private static LayerFromJson(json: LayerConfigJson): LayerDefinition { - const t = CustomLayoutFromJSON.MaybeTranslation; - const tr = CustomLayoutFromJSON.TagRenderingFromJson; - const tags = CustomLayoutFromJSON.TagsFromJson(json.overpassTags); - // We run the icon rendering with the bare minimum of tags (the overpass tags) to get the actual icon - const icon = CustomLayoutFromJSON.TagRenderingFromJson(json.icon).GetContent({id:"node/-1"}); - - // @ts-ignore - const id = json.name?.replace(/[^a-zA-Z0-9_-]/g,'') ?? json.id; - return new LayerDefinition( - id, - { - description: t(json.description), - name: Translations.WT(t(json.name)), - icon: icon, - minzoom: parseInt(""+json.minzoom), - title: tr(json.title), - presets: json.presets.map((preset) => { - return CustomLayoutFromJSON.PresetFromJson(json, preset) - }), - elementsToShow: - [new ImageCarouselWithUploadConstructor()].concat(json.tagRenderings.map(tr)), - overpassFilter: new And(tags), - wayHandling: parseInt(""+json.wayHandling) ?? LayerDefinition.WAYHANDLING_CENTER_AND_WAY, - maxAllowedOverlapPercentage: 0, - style: CustomLayoutFromJSON.StyleFromJson(json) - } - ) - } - - - private static MaybeTranslation(json: any): Translation | string { - if (json === undefined) { - return undefined; - } - if (typeof (json) === "string") { - return json; - } - return new Translation(json); - } - - public static LayoutFromJSON(json: LayoutConfigJson) { - const t = CustomLayoutFromJSON.MaybeTranslation; - let languages : string[] ; - if(typeof (json.language) === "string"){ - languages = [json.language]; - }else{ - languages = json.language - } - const layout = new Layout(json.name, - languages, - t(json.title), - json.layers.map(CustomLayoutFromJSON.LayerFromJson), - parseInt(""+json.startZoom), - parseFloat(""+json.startLat), - parseFloat(""+json.startLon), - new Combine(['

', t(json.title), '


', t(json.description)]) - ); - layout.icon = json.icon; - layout.maintainer = json.maintainer; - layout.widenFactor = parseFloat(""+json.widenFactor) ?? 0.03; - if(isNaN(layout.widenFactor)){ - layout.widenFactor = 0.03; - } - if (layout.widenFactor > 0.1) { - layout.widenFactor = 0.1; - } - return layout; - } - -} \ No newline at end of file diff --git a/Customizations/JSON/FromJSON.ts b/Customizations/JSON/FromJSON.ts new file mode 100644 index 0000000..5dc94be --- /dev/null +++ b/Customizations/JSON/FromJSON.ts @@ -0,0 +1,259 @@ +import {Layout} from "../Layout"; +import {LayoutConfigJson} from "./LayoutConfigJson"; +import {AndOrTagConfigJson} from "./TagConfigJson"; +import {And, RegexTag, Tag, TagsFilter} from "../../Logic/Tags"; +import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; +import {TagRenderingOptions} from "../TagRenderingOptions"; +import Translation from "../../UI/i18n/Translation"; +import {LayerConfigJson} from "./LayerConfigJson"; +import {LayerDefinition, Preset} from "../LayerDefinition"; +import {TagDependantUIElementConstructor} from "../UIElementConstructor"; +import FixedText from "../Questions/FixedText"; +import Translations from "../../UI/i18n/Translations"; +import Combine from "../../UI/Base/Combine"; +import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; +import {ImageCarouselConstructor} from "../../UI/Image/ImageCarousel"; + + +export class FromJSON { + + + public static FromBase64(layoutFromBase64: string): Layout { + return FromJSON.LayoutFromJSON(JSON.parse(atob(layoutFromBase64))); + } + + public static LayoutFromJSON(json: LayoutConfigJson): Layout { + console.log("Parsing ", json.id) + const tr = FromJSON.Translation; + + const layers = json.layers.map(FromJSON.Layer); + const roaming: TagDependantUIElementConstructor[] = json.roamingRenderings?.map(FromJSON.TagRendering) ?? []; + for (const layer of layers) { + layer.elementsToShow.push(...roaming); + } + + const layout = new Layout( + json.id, + typeof (json.language) === "string" ? [json.language] : json.language, + tr(json.title), + layers, + json.startZoom, + json.startLat, + json.startLon, + new Combine(["

", tr(json.title), "

", tr(json.description)]), + ); + + layout.widenFactor = json.widenFactor ?? 0.07; + layout.icon = json.icon; + layout.maintainer = json.maintainer; + layout.version = json.version; + layout.socialImage = json.socialImage; + layout.changesetMessage = json.changesetmessage; + return layout; + } + + public static Translation(json: string | any): string | Translation { + if (json === undefined) { + return undefined; + } + if (typeof (json) === "string") { + return json; + } + const tr = {}; + for (let key in json) { + tr[key] = json[key]; // I'm doing this wrong, I know + } + return new Translation(tr); + } + + public static TagRendering(json: TagRenderingConfigJson | string): TagDependantUIElementConstructor { + return FromJSON.TagRenderingWithDefault(json, "", undefined); + } + + public static TagRenderingWithDefault(json: TagRenderingConfigJson | string, propertyName, defaultValue: string): TagDependantUIElementConstructor { + if (json === undefined) { + if(defaultValue !== undefined){ + console.warn(`Using default value ${defaultValue} for ${propertyName}`) + return FromJSON.TagRendering(defaultValue); + } + throw `Tagrendering ${propertyName} is undefined...` + } + + if (typeof json === "string") { + + switch (json) { + case "picture": { + return new ImageCarouselWithUploadConstructor() + } + case "pictures": { + return new ImageCarouselWithUploadConstructor() + } + case "image": { + return new ImageCarouselWithUploadConstructor() + } + case "images": { + return new ImageCarouselWithUploadConstructor() + } + case "picturesNoUpload": { + return new ImageCarouselConstructor() + } + } + + + return new TagRenderingOptions({ + freeform: { + key: "id", + renderTemplate: json, + template: "$$$" + } + }); + } + + let template = FromJSON.Translation(json.render); + + let freeform = undefined; + if (json.freeform) { + + if(json.render === undefined){ + console.error("Freeform is defined, but render is not. This is not allowed.", json) + throw "Freeform is defined, but render is not. This is not allowed." + } + + freeform = { + template: `$${json.freeform.type ?? "string"}$`, + renderTemplate: template, + key: json.freeform.key + }; + if (json.freeform.addExtraTags) { + freeform["extraTags"] = FromJSON.Tag(json.freeform.addExtraTags); + } + } else if (json.render) { + freeform = { + template: `$string$`, + renderTemplate: template, + key: "id" + } + } + + const mappings = json.mappings?.map(mapping => ( + { + k: FromJSON.Tag(mapping.if), + txt: FromJSON.Translation(mapping.then), + hideInAnswer: mapping.hideInAnswer + }) + ); + + return new TagRenderingOptions({ + question: FromJSON.Translation(json.question), + freeform: freeform, + mappings: mappings + }); + } + + public static SimpleTag(json: string): Tag { + const tag = json.split("="); + return new Tag(tag[0], tag[1]); + } + + public static Tag(json: AndOrTagConfigJson | string): TagsFilter { + if (typeof (json) == "string") { + const tag = json as string; + if (tag.indexOf("!~") >= 0) { + const split = tag.split("!~"); + if(split[1] == "*"){ + split[1] = ".*" + } + return new RegexTag( + new RegExp(split[0]), + new RegExp(split[1]), + true + ); + } + if (tag.indexOf("!=") >= 0) { + const split = tag.split("!="); + return new RegexTag( + new RegExp(split[0]), + new RegExp(split[1]), + true + ); + } + if (tag.indexOf("~") >= 0) { + const split = tag.split("~"); + if(split[1] == "*"){ + split[1] = ".*" + } + return new RegexTag( + new RegExp("^"+split[0]+"$"), + new RegExp("^"+split[1]+"$") + ); + } + const split = tag.split("="); + return new Tag(split[0], split[1]) + } + if (json.and !== undefined) { + return new And(json.and.map(FromJSON.Tag)); + } + if (json.or !== undefined) { + return new And(json.or.map(FromJSON.Tag)); + } + } + + private static Title(json: string | Map | TagRenderingConfigJson): TagDependantUIElementConstructor { + if ((json as TagRenderingConfigJson).render !== undefined) { + return FromJSON.TagRendering((json as TagRenderingConfigJson)); + } else if (typeof (json) === "string") { + return new FixedText(Translations.WT(json)); + } else { + return new FixedText(FromJSON.Translation(json as Map)); + } + } + + public static Layer(json: LayerConfigJson): LayerDefinition { + console.log("Parsing ",json.name); + const tr = FromJSON.Translation; + const overpassTags = FromJSON.Tag(json.overpassTags); + const icon = FromJSON.TagRenderingWithDefault(json.icon, "layericon", "./assets/bug.svg"); + const color = FromJSON.TagRenderingWithDefault(json.color, "layercolor", "#0000ff"); + const width = FromJSON.TagRenderingWithDefault(json.width, "layerwidth", "10"); + const renderTags = {"id": "node/-1"} + const presets: Preset[] = json?.presets?.map(preset => { + return ({ + title: tr(preset.title), + description: tr(preset.description), + tags: preset.tags.map(FromJSON.SimpleTag) + }); + }) ?? []; + + function style(tags) { + return { + color: color.GetContent(tags).txt, + weight: width.GetContent(tags).txt, + icon: { + iconUrl: icon.GetContent(tags).txt + }, + } + } + + const layer = new LayerDefinition( + json.id, + { + name: tr(json.name), + description: tr(json.description), + icon: icon.GetContent(renderTags).txt, + overpassFilter: overpassTags, + + title: FromJSON.Title(json.title), + minzoom: json.minzoom, + presets: presets, + elementsToShow: json.tagRenderings?.map(FromJSON.TagRendering) ?? [], + style: style, + wayHandling: json.wayHandling + + } + ); + return layer; + + } + + +} \ No newline at end of file diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts new file mode 100644 index 0000000..01aa5fa --- /dev/null +++ b/Customizations/JSON/LayerConfigJson.ts @@ -0,0 +1,79 @@ +import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; +import {AndOrTagConfigJson} from "./TagConfigJson"; + +/** + * Configuration for a single layer + */ +export interface LayerConfigJson { + /** + * The id of this layer. + * This should be a simple, lowercase, human readable string that is used to identify the layer. + */ + id: string; + + /** + * The name of this layer + * Used in the layer control panel and the 'Personal theme' + */ + name: string | any + + /** + * A description for this layer. + * Shown in the layer selections and in the personal theme + */ + description?: string | any; + + + /** + * The tags to load from overpass. Either a simple 'key=value'-string, otherwise an advanced configuration + */ + overpassTags: AndOrTagConfigJson | string; + + /** + * The zoomlevel at which point the data is shown and loaded. + */ + minzoom: number; + + + + /** + * The title shown in a popup for elements of this layer + */ + title: string | any | TagRenderingConfigJson; + + /** + * The icon for an element. + * Note that this also doubles as the icon for this layer (rendered with the overpass-tags) ánd the icon in the presets. + */ + icon?: string | TagRenderingConfigJson; + /** + * The color for way-elements + */ + color?: string | TagRenderingConfigJson; + /** + * The stroke-width for way-elements + */ + width?: string | TagRenderingConfigJson; + + /** + * Wayhandling: should a way/area be displayed as: + * 0) The way itself + * 1) The centerpoint and the way + * 2) Only the centerpoint? + */ + wayHandling?: number; + + /** + * Presets for this layer + */ + presets?: { + tags: string[], + title: string | any, + description?: string | any, + }[], + + /** + * All the tag renderings. + */ + tagRenderings?: (string | TagRenderingConfigJson) [] +} \ No newline at end of file diff --git a/Customizations/JSON/LayoutConfigJson.ts b/Customizations/JSON/LayoutConfigJson.ts new file mode 100644 index 0000000..b7404e0 --- /dev/null +++ b/Customizations/JSON/LayoutConfigJson.ts @@ -0,0 +1,98 @@ +import {LayerConfigJson} from "./LayerConfigJson"; +import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; + +/** + * Defines what a JSON-segment defining a layout should look like. + * + * General remark: a type (string | any) indicates either a fixed or a translatable string + */ +export interface LayoutConfigJson { + /** + * The id of this layout. + * This should be a simple, lowercase string which is used to create the html-page, e.g. + * 'cyclestreets' which become 'cyclestreets.html' + */ + id: string; + /** + * Who does maintian this preset? + */ + maintainer: string; + /** + * Extra piece of text that can be added to the changeset + */ + changesetmessage?: string; + /** + * A version number, either semantically or by date. + * Should be sortable, where the higher value is the later version + */ + version: string; + /** + * The supported language(s). + * This should be a two-letter, lowercase code which identifies the language, e.g. "en", "nl", ... + * If the theme supports multiple languages, use a list: `["en","nl","fr"]` to allow the user to pick any of them + */ + language: string | string[]; + + /** + * The title, as shown in the welcome message and the more-screen + */ + title: string | any; + /** + * The description, as shown in the welcome message and the more-screen + */ + description: string | any; + + + /** + * The icon representing this theme. + * Used as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ... + * Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64) + */ + icon: string; + + /** + * Link to a 'social image' which is included as og:image-tag on official themes. + * Usefull to share the theme on social media + */ + socialImage?: string; + + /** + * Default location and zoom to start. + * Note that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used + */ + startZoom: number; + startLat: number; + startLon: number; + + /** + * When a query is run, the data within bounds of the visible map is loaded. + * However, users tend to pan and zoom a lot. It is pretty annoying if every single pan means a reloading of the data. + * For this, the bounds are widened in order to make a small pan still within bounds of the loaded data. + * + * IF widenfactor is 0, this feature is disabled. A recommended value is between 0.5 and 0.01 (the latter for very dense queries) + */ + widenFactor?: number; + + /** + * A tagrendering depicts how to show some tags or how to show a question for it. + * + * These tagrenderings are applied to _all_ the loaded layers and are a way to reuse tagrenderings. + * Note that if multiple themes are loaded (e.g. via the personal theme) + * that these roamingRenderings are applied to the layers of the OTHER themes too! + * + * In order to prevent them to do too much damage, all the overpass-tags of the layers are taken and combined as OR. + * These tag renderings will only show up if the object matches this filter. + */ + roamingRenderings?: TagRenderingConfigJson[], + + /** + * The layers to display + */ + layers: LayerConfigJson[], + + + + + + +} \ No newline at end of file diff --git a/Customizations/JSON/TagConfig.ts b/Customizations/JSON/TagConfig.ts new file mode 100644 index 0000000..bc12272 --- /dev/null +++ b/Customizations/JSON/TagConfig.ts @@ -0,0 +1,14 @@ +/** + * Read a tagconfig and converts it into a TagsFilter value + */ +import {AndOrTagConfigJson} from "./TagConfigJson"; + +export default class TagConfig { + + public static fromJson(json: any): TagConfig { + const config: AndOrTagConfigJson = json; + return config; + } + +} + diff --git a/Customizations/JSON/TagConfigJson.ts b/Customizations/JSON/TagConfigJson.ts new file mode 100644 index 0000000..e32cd4f --- /dev/null +++ b/Customizations/JSON/TagConfigJson.ts @@ -0,0 +1,8 @@ + +export interface AndOrTagConfigJson { + + and?: (string | AndOrTagConfigJson)[] + or?: (string | AndOrTagConfigJson)[] + + +} \ No newline at end of file diff --git a/Customizations/JSON/TagRenderingConfigJson.ts b/Customizations/JSON/TagRenderingConfigJson.ts new file mode 100644 index 0000000..ffcb925 --- /dev/null +++ b/Customizations/JSON/TagRenderingConfigJson.ts @@ -0,0 +1,51 @@ +import {AndOrTagConfigJson} from "./TagConfigJson"; + +export interface TagRenderingConfigJson { + /** + * Renders this value. Note that "{key}"-parts are substituted by the corresponding values of the element. + * If neither 'textFieldQuestion' nor 'mappings' are defined, this text is simply shown as default value. + */ + render?: string | any, + + /** + * If it turns out that this tagRendering doesn't match _any_ value, then we show this question. + * If undefined, the question is never asked and this tagrendering is read-only + */ + question?: string | any, + + /** + * Only show this question if the object also matches the following tags. + * + * This is useful to ask a follow-up question. E.g. if there is a diaper table, then ask a follow-up question on diaper tables... + * */ + condition?: AndOrTagConfigJson | string; + + /** + * Allow freeform text input from the user + */ + freeform?: { + /** + * If this key is present, then 'render' is used to display the value. + * If this is undefined, the rendering is _always_ shown + */ + key: string, + /** + * The type of the text-field, e.g. 'string', 'nat', 'float', 'date',... + */ + type?: string, + /** + * If a value is added with the textfield, these extra tag is addded. + * Usefull to add a 'fixme=freeform textfield used - to be checked' + **/ + addExtraTags?: AndOrTagConfigJson | string; + } + + /** + * Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes + */ + mappings?: { + if: AndOrTagConfigJson | string, + then: string | any + hideInAnswer?: boolean + }[] +} \ No newline at end of file diff --git a/Customizations/LayerDefinition.ts b/Customizations/LayerDefinition.ts index bf876ab..feb1004 100644 --- a/Customizations/LayerDefinition.ts +++ b/Customizations/LayerDefinition.ts @@ -1,9 +1,8 @@ -import {Tag, TagsFilter} from "../Logic/TagsFilter"; +import {Tag, TagsFilter} from "../Logic/Tags"; import {UIElement} from "../UI/UIElement"; import {TagDependantUIElementConstructor} from "./UIElementConstructor"; import {TagRenderingOptions} from "./TagRenderingOptions"; import Translation from "../UI/i18n/Translation"; -import {LayerConfigJson, TagRenderingConfigJson} from "./JSON/CustomLayoutFromJSON"; export interface Preset { tags: Tag[], @@ -75,9 +74,8 @@ export class LayerDefinition { style: (tags: any) => { color: string, weight?: number, - icon: { - iconUrl: string, - iconSize: number[], + icon: { + iconUrl: string, iconSize?: number[], popupAnchor?: number[], iconAnchor?: number[] }, }; diff --git a/Customizations/Layers/BikeCafes.ts b/Customizations/Layers/BikeCafes.ts index 19a65cb..daa3270 100644 --- a/Customizations/Layers/BikeCafes.ts +++ b/Customizations/Layers/BikeCafes.ts @@ -3,8 +3,8 @@ import FixedText from "../Questions/FixedText"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import Translations from "../../UI/i18n/Translations"; import CafeName from "../Questions/bike/CafeName"; -import { Or, And, Tag, anyValueExcept, Regex } from "../../Logic/TagsFilter"; -import { PhoneNumberQuestion } from "../Questions/PhoneNumberQuestion"; +import {And, Or, RegexTag, Tag} from "../../Logic/Tags"; +import {PhoneNumberQuestion} from "../Questions/PhoneNumberQuestion"; import Website from "../Questions/Website"; import CafeRepair from "../Questions/bike/CafeRepair"; import CafeDiy from "../Questions/bike/CafeDiy"; @@ -20,10 +20,11 @@ export default class BikeCafes extends LayerDefinition { this.name = this.to.name this.icon = "./assets/bike/cafe.svg" this.overpassFilter = new And([ - new Tag("amenity", /^pub|bar|cafe$/), + new RegexTag(/^amenity$/, /^pub|bar|cafe$/), new Or([ - new Tag(/^service:bicycle:/, "*"), - new Tag("pub", "cycling") + new RegexTag(/^service:bicycle:/, /.*/), + new RegexTag(/^pub$/, /^cycling|bicycle$/), + new RegexTag(/^theme$/, /^cycling|bicycle$/), ]) ]) @@ -40,7 +41,14 @@ export default class BikeCafes extends LayerDefinition { this.maxAllowedOverlapPercentage = 10; this.minzoom = 13 - this.style = this.generateStyleFunction() + this.style = () => ({ + color: "#00bb00", + icon: { + iconUrl: "./assets/bike/cafe.svg", + iconSize: [50, 50], + iconAnchor: [25, 50] + } + }); this.title = new FixedText(this.to.title) this.elementsToShow = [ new ImageCarouselWithUploadConstructor(), @@ -54,18 +62,4 @@ export default class BikeCafes extends LayerDefinition { ] this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY } - - private generateStyleFunction() { - const self = this - return function (properties: any) { - return { - color: "#00bb00", - icon: { - iconUrl: "./assets/bike/cafe.svg", - iconSize: [50, 50], - iconAnchor: [25,50] - } - } - } - } } \ No newline at end of file diff --git a/Customizations/Layers/BikeOtherShops.ts b/Customizations/Layers/BikeOtherShops.ts index 1565dd4..38b8ab0 100644 --- a/Customizations/Layers/BikeOtherShops.ts +++ b/Customizations/Layers/BikeOtherShops.ts @@ -1,7 +1,7 @@ -import { LayerDefinition } from "../LayerDefinition"; +import {LayerDefinition} from "../LayerDefinition"; import Translations from "../../UI/i18n/Translations"; -import {And, Tag, Or, anyValueExcept} from "../../Logic/TagsFilter"; -import { ImageCarouselWithUploadConstructor } from "../../UI/Image/ImageCarouselWithUpload"; +import {And, RegexTag, Tag} from "../../Logic/Tags"; +import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import ShopRetail from "../Questions/bike/ShopRetail"; import ShopPump from "../Questions/bike/ShopPump"; import ShopRental from "../Questions/bike/ShopRental"; @@ -9,7 +9,7 @@ import ShopRepair from "../Questions/bike/ShopRepair"; import ShopDiy from "../Questions/bike/ShopDiy"; import ShopName from "../Questions/bike/ShopName"; import ShopSecondHand from "../Questions/bike/ShopSecondHand"; -import { PhoneNumberQuestion } from "../Questions/PhoneNumberQuestion"; +import {PhoneNumberQuestion} from "../Questions/PhoneNumberQuestion"; import Website from "../Questions/Website"; import {TagRenderingOptions} from "../TagRenderingOptions"; @@ -24,8 +24,8 @@ export default class BikeOtherShops extends LayerDefinition { this.name = this.to.name this.icon = "./assets/bike/non_bike_repair_shop.svg" this.overpassFilter = new And([ - anyValueExcept("shop", "bicycle"), - new Tag(/^service:bicycle:/, "*"), + new RegexTag(/^shop$/, /^bicycle$/, true), + new RegexTag(/^service:bicycle:/, /.*/), ]) this.presets = [] this.maxAllowedOverlapPercentage = 10 diff --git a/Customizations/Layers/BikeParkings.ts b/Customizations/Layers/BikeParkings.ts index b5c7d28..6fbe754 100644 --- a/Customizations/Layers/BikeParkings.ts +++ b/Customizations/Layers/BikeParkings.ts @@ -1,12 +1,10 @@ import {LayerDefinition} from "../LayerDefinition"; -import {And, Or, Tag, TagsFilter} from "../../Logic/TagsFilter"; -import {OperatorTag} from "../Questions/OperatorTag"; +import {Tag} from "../../Logic/Tags"; import FixedText from "../Questions/FixedText"; import ParkingType from "../Questions/bike/ParkingType"; import ParkingCapacity from "../Questions/bike/ParkingCapacity"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import Translations from "../../UI/i18n/Translations"; -import ParkingOperator from "../Questions/bike/ParkingOperator"; import ParkingAccessCargo from "../Questions/bike/ParkingAccessCargo"; import ParkingCapacityCargo from "../Questions/bike/ParkingCapacityCargo"; @@ -28,8 +26,17 @@ export default class BikeParkings extends LayerDefinition { this.maxAllowedOverlapPercentage = 10; - this.minzoom = 13; - this.style = this.generateStyleFunction(); + this.minzoom = 17; + this.style = function () { + return { + color: "#00bb00", + icon: { + iconUrl: "./assets/bike/parking.svg", + iconSize: [50, 50], + iconAnchor: [25, 50] + } + }; + }; this.title = new FixedText(Translations.t.cyclofix.parking.title) this.elementsToShow = [ new ImageCarouselWithUploadConstructor(), @@ -42,18 +49,4 @@ export default class BikeParkings extends LayerDefinition { this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY; } - - private generateStyleFunction() { - const self = this; - return function (properties: any) { - return { - color: "#00bb00", - icon: { - iconUrl: "./assets/bike/parking.svg", - iconSize: [50, 50], - iconAnchor: [25,50] - } - }; - }; - } } diff --git a/Customizations/Layers/BikeShops.ts b/Customizations/Layers/BikeShops.ts index ef52f1d..3c6640e 100644 --- a/Customizations/Layers/BikeShops.ts +++ b/Customizations/Layers/BikeShops.ts @@ -1,8 +1,7 @@ -import { LayerDefinition } from "../LayerDefinition"; +import {LayerDefinition} from "../LayerDefinition"; import Translations from "../../UI/i18n/Translations"; -import {And, Tag, Or} from "../../Logic/TagsFilter"; -import FixedText from "../Questions/FixedText"; -import { ImageCarouselWithUploadConstructor } from "../../UI/Image/ImageCarouselWithUpload"; +import {And, Tag} from "../../Logic/Tags"; +import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import ShopRetail from "../Questions/bike/ShopRetail"; import ShopPump from "../Questions/bike/ShopPump"; import ShopRental from "../Questions/bike/ShopRental"; @@ -18,7 +17,6 @@ import {TagRenderingOptions} from "../TagRenderingOptions"; export default class BikeShops extends LayerDefinition { private readonly sellsBikes = new Tag("service:bicycle:retail", "yes") - private readonly repairsBikes = new Tag("service:bicycle:repair", "yes") constructor() { super("bikeshop"); diff --git a/Customizations/Layers/BikeStations.ts b/Customizations/Layers/BikeStations.ts index 3b2158d..13a36ab 100644 --- a/Customizations/Layers/BikeStations.ts +++ b/Customizations/Layers/BikeStations.ts @@ -1,12 +1,9 @@ import {LayerDefinition} from "../LayerDefinition"; -import {And, Tag, TagsFilter, Or, Not} from "../../Logic/TagsFilter"; +import {And, Or, Tag} from "../../Logic/Tags"; import BikeStationChain from "../Questions/bike/StationChain"; import BikeStationPumpTools from "../Questions/bike/StationPumpTools"; import BikeStationStand from "../Questions/bike/StationStand"; import PumpManual from "../Questions/bike/PumpManual"; -import BikeStationOperator from "../Questions/bike/StationOperator"; -import BikeStationBrand from "../Questions/bike/StationBrand"; -import FixedText from "../Questions/FixedText"; import PumpManometer from "../Questions/bike/PumpManometer"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import PumpOperational from "../Questions/bike/PumpOperational"; @@ -19,7 +16,6 @@ export default class BikeStations extends LayerDefinition { private readonly repairStation = new Tag("amenity", "bicycle_repair_station"); private readonly pump = new Tag("service:bicycle:pump", "yes"); private readonly nopump = new Tag("service:bicycle:pump", "no"); - private readonly pumpOperationalAny = new Tag("service:bicycle:pump:operational_status", "yes"); private readonly pumpOperationalOk = new Or([new Tag("service:bicycle:pump:operational_status", "yes"), new Tag("service:bicycle:pump:operational_status", "operational"), new Tag("service:bicycle:pump:operational_status", "ok"), new Tag("service:bicycle:pump:operational_status", "")]); private readonly tools = new Tag("service:bicycle:tools", "yes"); private readonly notools = new Tag("service:bicycle:tools", "no"); diff --git a/Customizations/Layers/Birdhide.ts b/Customizations/Layers/Birdhide.ts index c7dc527..fa561ee 100644 --- a/Customizations/Layers/Birdhide.ts +++ b/Customizations/Layers/Birdhide.ts @@ -1,6 +1,5 @@ import {LayerDefinition} from "../LayerDefinition"; -import {And, Or, Tag} from "../../Logic/TagsFilter"; -import FixedText from "../Questions/FixedText"; +import {And, Or, Tag} from "../../Logic/Tags"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import {TagRenderingOptions} from "../TagRenderingOptions"; @@ -24,7 +23,7 @@ export class Birdhide extends LayerDefinition { tags: [Birdhide.birdhide] } ], - style(tags: any): { color: string; icon: any } { + style(): { color: string; icon: any } { return {color: "", icon: undefined}; }, }); diff --git a/Customizations/Layers/Bos.ts b/Customizations/Layers/Bos.ts index 906d609..28a4184 100644 --- a/Customizations/Layers/Bos.ts +++ b/Customizations/Layers/Bos.ts @@ -1,5 +1,5 @@ import {LayerDefinition} from "../LayerDefinition"; -import {Or, Tag} from "../../Logic/TagsFilter"; +import {Or, Tag} from "../../Logic/Tags"; import {AccessTag} from "../Questions/AccessTag"; import {OperatorTag} from "../Questions/OperatorTag"; import {NameQuestion} from "../Questions/NameQuestion"; diff --git a/Customizations/Layers/ClimbingTree.ts b/Customizations/Layers/ClimbingTree.ts index f423f1a..d5009bd 100644 --- a/Customizations/Layers/ClimbingTree.ts +++ b/Customizations/Layers/ClimbingTree.ts @@ -1,7 +1,7 @@ import {LayerDefinition} from "../LayerDefinition"; import Translations from "../../UI/i18n/Translations"; import FixedText from "../Questions/FixedText"; -import {And, Tag} from "../../Logic/TagsFilter"; +import {And, Tag} from "../../Logic/Tags"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; export class ClimbingTree extends LayerDefinition { diff --git a/Customizations/Layers/DrinkingWater.ts b/Customizations/Layers/DrinkingWater.ts index e7fd086..50a5376 100644 --- a/Customizations/Layers/DrinkingWater.ts +++ b/Customizations/Layers/DrinkingWater.ts @@ -1,7 +1,6 @@ import {LayerDefinition} from "../LayerDefinition"; -import {And, Or, Tag} from "../../Logic/TagsFilter"; +import {And, Or, Tag} from "../../Logic/Tags"; import {OperatorTag} from "../Questions/OperatorTag"; -import * as L from "leaflet"; import FixedText from "../Questions/FixedText"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import Translations from "../../UI/i18n/Translations"; @@ -29,7 +28,7 @@ export class DrinkingWater extends LayerDefinition { this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY this.minzoom = 13; - this.style = this.generateStyleFunction(); + this.style = DrinkingWater.generateStyleFunction(); this.title = new FixedText("Drinking water"); this.elementsToShow = [ new OperatorTag(), @@ -47,10 +46,8 @@ export class DrinkingWater extends LayerDefinition { } - private generateStyleFunction() { - const self = this; - return function (properties: any) { - + private static generateStyleFunction() { + return function () { return { color: "#00bb00", icon: { diff --git a/Customizations/Layers/GhostBike.ts b/Customizations/Layers/GhostBike.ts index 89013b8..c47b0f8 100644 --- a/Customizations/Layers/GhostBike.ts +++ b/Customizations/Layers/GhostBike.ts @@ -1,5 +1,5 @@ import {LayerDefinition} from "../LayerDefinition"; -import {Tag} from "../../Logic/TagsFilter"; +import {Tag} from "../../Logic/Tags"; import FixedText from "../Questions/FixedText"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import {TagRenderingOptions} from "../TagRenderingOptions"; diff --git a/Customizations/Layers/GrbToFix.ts b/Customizations/Layers/GrbToFix.ts index 84b277f..3ca5bdc 100644 --- a/Customizations/Layers/GrbToFix.ts +++ b/Customizations/Layers/GrbToFix.ts @@ -1,5 +1,5 @@ import {LayerDefinition} from "../LayerDefinition"; -import {And, Regex, Tag} from "../../Logic/TagsFilter"; +import {And, RegexTag, Tag} from "../../Logic/Tags"; import {TagRenderingOptions} from "../TagRenderingOptions"; export class GrbToFix extends LayerDefinition { @@ -10,12 +10,12 @@ export class GrbToFix extends LayerDefinition { this.name = "grb"; this.presets = []; this.icon = "./assets/star.svg"; - this.overpassFilter = new Regex("fixme", "GRB"); + this.overpassFilter = new RegexTag(/fixme/, /.*GRB.*/); this.minzoom = 13; - this.style = function (tags) { + this.style = function () { return { icon: { iconUrl: "assets/star.svg", diff --git a/Customizations/Layers/InformationBoard.ts b/Customizations/Layers/InformationBoard.ts index 0f2a4da..c918168 100644 --- a/Customizations/Layers/InformationBoard.ts +++ b/Customizations/Layers/InformationBoard.ts @@ -1,7 +1,7 @@ import {LayerDefinition} from "../LayerDefinition"; import FixedText from "../Questions/FixedText"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; -import {And, Tag} from "../../Logic/TagsFilter"; +import {And, Tag} from "../../Logic/Tags"; import {TagRenderingOptions} from "../TagRenderingOptions"; export class InformationBoard extends LayerDefinition { diff --git a/Customizations/Layers/Map.ts b/Customizations/Layers/Map.ts index 75cb991..230cb8b 100644 --- a/Customizations/Layers/Map.ts +++ b/Customizations/Layers/Map.ts @@ -1,7 +1,7 @@ import {LayerDefinition} from "../LayerDefinition"; import FixedText from "../Questions/FixedText"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; -import {And, Tag} from "../../Logic/TagsFilter"; +import {Tag} from "../../Logic/Tags"; import {TagRenderingOptions} from "../TagRenderingOptions"; export class Map extends LayerDefinition { diff --git a/Customizations/Layers/NatureReserves.ts b/Customizations/Layers/NatureReserves.ts index 1f3c11e..6beb53a 100644 --- a/Customizations/Layers/NatureReserves.ts +++ b/Customizations/Layers/NatureReserves.ts @@ -1,5 +1,5 @@ import {LayerDefinition} from "../LayerDefinition"; -import {Or, Tag} from "../../Logic/TagsFilter"; +import {Or, Tag} from "../../Logic/Tags"; import {AccessTag} from "../Questions/AccessTag"; import {OperatorTag} from "../Questions/OperatorTag"; import {NameQuestion} from "../Questions/NameQuestion"; diff --git a/Customizations/Layers/Park.ts b/Customizations/Layers/Park.ts index cfb8692..ff1f7a6 100644 --- a/Customizations/Layers/Park.ts +++ b/Customizations/Layers/Park.ts @@ -1,7 +1,5 @@ import {LayerDefinition} from "../LayerDefinition"; -import {And, Or, Tag} from "../../Logic/TagsFilter"; -import {AccessTag} from "../Questions/AccessTag"; -import {OperatorTag} from "../Questions/OperatorTag"; +import {Or, Tag} from "../../Logic/Tags"; import {NameQuestion} from "../Questions/NameQuestion"; import {NameInline} from "../Questions/NameInline"; import {DescriptionQuestion} from "../Questions/DescriptionQuestion"; diff --git a/Customizations/Layers/Viewpoint.ts b/Customizations/Layers/Viewpoint.ts index 05122b4..ec3e8a2 100644 --- a/Customizations/Layers/Viewpoint.ts +++ b/Customizations/Layers/Viewpoint.ts @@ -1,7 +1,6 @@ import {LayerDefinition} from "../LayerDefinition"; -import {FixedUiElement} from "../../UI/Base/FixedUiElement"; import FixedText from "../Questions/FixedText"; -import {Tag} from "../../Logic/TagsFilter"; +import {Tag} from "../../Logic/Tags"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import {TagRenderingOptions} from "../TagRenderingOptions"; @@ -19,7 +18,7 @@ export class Viewpoint extends LayerDefinition { }], icon: "assets/viewpoint.svg", wayHandling: LayerDefinition.WAYHANDLING_CENTER_ONLY, - style: tags => { + style: _ => { return { color: undefined, icon: { iconUrl: "assets/viewpoint.svg", diff --git a/Customizations/Layers/Widths.ts b/Customizations/Layers/Widths.ts index 3a196f6..201eb06 100644 --- a/Customizations/Layers/Widths.ts +++ b/Customizations/Layers/Widths.ts @@ -1,13 +1,12 @@ import {LayerDefinition} from "../LayerDefinition"; -import {And, Not, Or, Tag} from "../../Logic/TagsFilter"; -import {Park} from "./Park"; +import {And, Or, Tag} from "../../Logic/Tags"; import {TagRenderingOptions} from "../TagRenderingOptions"; export class Widths extends LayerDefinition { - private cyclistWidth: number; - private carWidth: number; - private pedestrianWidth: number; + private readonly cyclistWidth: number; + private readonly carWidth: number; + private readonly pedestrianWidth: number; private readonly _bothSideParking = new Tag("parking:lane:both", "parallel"); private readonly _noSideParking = new Tag("parking:lane:both", "no_parking"); @@ -36,10 +35,9 @@ export class Widths extends LayerDefinition { private readonly _oneSideParking = new Or([this._leftSideParking, this._rightSideParking]); - private readonly _carfree = new Or( + private readonly _carfree = new And( [new Tag("highway", "pedestrian"), new Tag("highway", "living_street"), new Tag("access","destination"), new Tag("motor_vehicle", "destination")]) - private readonly _notCarFree = new Not(this._carfree); private calcProps(properties) { let parkingStateKnown = true; @@ -59,8 +57,7 @@ export class Widths extends LayerDefinition { } - let pedestrianFlowNeeded = 0; - + let pedestrianFlowNeeded; if (this._sidewalkBoth.matchesProperties(properties)) { pedestrianFlowNeeded = 0; } else if (this._sidewalkNone.matchesProperties(properties)) { @@ -198,7 +195,7 @@ export class Widths extends LayerDefinition { renderTemplate: "{note:width:carriageway}", template: "$$$", } - }).OnlyShowIf(this._notCarFree), + }).OnlyShowIf(this._carfree, true), new TagRenderingOptions({ @@ -218,7 +215,7 @@ export class Widths extends LayerDefinition { renderTemplate: "{note:width:carriageway}", template: "$$$", } - }).OnlyShowIf(this._notCarFree), + }).OnlyShowIf(this._carfree, true), new TagRenderingOptions({ @@ -248,7 +245,7 @@ export class Widths extends LayerDefinition { txt: "Tweerichtingsverkeer voor iedereen. Dit gebruikt " + r(2 * this.carWidth + 2 * this.cyclistWidth) + "m" } ] - }).OnlyShowIf(this._notCarFree), + }).OnlyShowIf(this._carfree, true), new TagRenderingOptions( { @@ -266,7 +263,7 @@ export class Widths extends LayerDefinition { {k: new Tag("short",""), txt: "De totale nodige ruimte voor vlot en veilig verkeer is dus {targetWidth}m"} ] } - ).OnlyShowIf(this._notCarFree), + ).OnlyShowIf(this._carfree, true), new TagRenderingOptions({ diff --git a/Customizations/Layout.ts b/Customizations/Layout.ts index 555cb8a..8cf2f7f 100644 --- a/Customizations/Layout.ts +++ b/Customizations/Layout.ts @@ -2,7 +2,6 @@ import {LayerDefinition} from "./LayerDefinition"; import {UIElement} from "../UI/UIElement"; import Translations from "../UI/i18n/Translations"; import Combine from "../UI/Base/Combine"; -import {FixedUiElement} from "../UI/Base/FixedUiElement"; import {State} from "../State"; /** @@ -10,7 +9,7 @@ import {State} from "../State"; */ export class Layout { - public name: string; + public id: string; public icon: string = "./assets/logo.svg"; public title: UIElement; public maintainer: string; @@ -25,20 +24,19 @@ export class Layout { public welcomeBackMessage: UIElement; public welcomeTail: UIElement; - public startzoom: number; public supportedLanguages: string[]; + + public startzoom: number; public startLon: number; public startLat: number; - public locationContains: string[]; - public enableAdd: boolean = true; public enableUserBadge: boolean = true; public enableSearch: boolean = true; public enableLayers: boolean = true; public enableMoreQuests: boolean = true; public enableShareScreen: boolean = true; - + public enableGeolocation: boolean = true; public hideFromOverview: boolean = false; /** @@ -47,11 +45,10 @@ export class Layout { */ public widenFactor: number = 0.07; public defaultBackground: string = "osm"; - public enableGeolocation: boolean = true; /** * - * @param name: The name used in the query string. If in the query "quests=" is defined, it will select this layout + * @param id: The name used in the query string. If in the query "quests=" is defined, it will select this layout * @param title: Will be used in the of the page * @param layers: The layers to show, a list of LayerDefinitions * @param startzoom: The initial starting zoom of the map @@ -63,7 +60,7 @@ export class Layout { * @param welcomeTail: This text is shown below the login message. It is ideal for extra help */ constructor( - name: string, + id: string, supportedLanguages: string[], title: UIElement | string, layers: LayerDefinition[], @@ -85,7 +82,7 @@ export class Layout { this.startLon = startLon; this.startLat = startLat; this.startzoom = startzoom; - this.name = name; + this.id = id; this.layers = layers; this.welcomeMessage = Translations.W(welcomeMessage) this.gettingStartedPlzLogin = Translations.W(gettingStartedPlzLogin); diff --git a/Customizations/Layouts/All.ts b/Customizations/Layouts/All.ts deleted file mode 100644 index a3e0aeb..0000000 --- a/Customizations/Layouts/All.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {Layout} from "../Layout"; - -export class All extends Layout{ - constructor() { - super( - "all", - ["en"], - "All quest layers", - [], - 15, - 51.2, - 3.2, - "<h3>All quests of MapComplete</h3>" + - "This is a mixed bag. Some quests might be hard or for experts to answer only", - "Please log in", - "" - ); - this.hideFromOverview = true; - } -} \ No newline at end of file diff --git a/Customizations/Layouts/ClimbingTrees.ts b/Customizations/Layouts/ClimbingTrees.ts index 8618b29..068b4e6 100644 --- a/Customizations/Layouts/ClimbingTrees.ts +++ b/Customizations/Layouts/ClimbingTrees.ts @@ -1,4 +1,3 @@ -import {LayerDefinition} from "../LayerDefinition"; import Translations from "../../UI/i18n/Translations"; import {Layout} from "../Layout"; import {ClimbingTree} from "../Layers/ClimbingTree"; diff --git a/Customizations/Layouts/Cyclofix.ts b/Customizations/Layouts/Cyclofix.ts index 4e5afb6..d2a4dec 100644 --- a/Customizations/Layouts/Cyclofix.ts +++ b/Customizations/Layouts/Cyclofix.ts @@ -29,6 +29,7 @@ export default class Cyclofix extends Layout { ); this.icon = "./assets/bike/logo.svg" this.description = "Easily search and contribute bicycle data nearby"; - this.socialImage = "./assets/bike/cyclofix.jpeg" + this.socialImage = "./assets/bike/cyclofix.jpeg"; + this.widenFactor = 0.5; } } diff --git a/Customizations/Layouts/GhostBikes.ts b/Customizations/Layouts/GhostBikes.ts index 22ed372..25eda11 100644 --- a/Customizations/Layouts/GhostBikes.ts +++ b/Customizations/Layouts/GhostBikes.ts @@ -1,7 +1,6 @@ import {Layout} from "../Layout"; import {GhostBike} from "../Layers/GhostBike"; import Combine from "../../UI/Base/Combine"; -import Translations from "../../UI/i18n/Translations"; export class GhostBikes extends Layout { constructor() { diff --git a/Customizations/Layouts/Smoothness.ts b/Customizations/Layouts/Smoothness.ts index 11d697c..9f2cd1c 100644 --- a/Customizations/Layouts/Smoothness.ts +++ b/Customizations/Layouts/Smoothness.ts @@ -1,6 +1,6 @@ import {Layout} from "../Layout"; import {LayerDefinition} from "../LayerDefinition"; -import {Or, Tag} from "../../Logic/TagsFilter"; +import {Or, Tag} from "../../Logic/Tags"; import {TagRenderingOptions} from "../TagRenderingOptions"; diff --git a/Customizations/OnlyShowIf.ts b/Customizations/OnlyShowIf.ts index 5e1b473..2b80242 100644 --- a/Customizations/OnlyShowIf.ts +++ b/Customizations/OnlyShowIf.ts @@ -1,26 +1,28 @@ +import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor"; +import {TagsFilter, TagUtils} from "../Logic/Tags"; +import {UIElement} from "../UI/UIElement"; +import {UIEventSource} from "../Logic/UIEventSource"; +import Translation from "../UI/i18n/Translation"; + /** * Wrapper around another TagDependandElement, which only shows if the filters match */ -import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor"; -import {TagsFilter, TagUtils} from "../Logic/TagsFilter"; -import {UIElement} from "../UI/UIElement"; -import {UIEventSource} from "../Logic/UIEventSource"; -import {Changes} from "../Logic/Osm/Changes"; - - export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{ - private _tagsFilter: TagsFilter; - private _embedded: TagDependantUIElementConstructor; - - constructor(tagsFilter : TagsFilter, embedded: TagDependantUIElementConstructor) { + private readonly _tagsFilter: TagsFilter; + private readonly _embedded: TagDependantUIElementConstructor; + private readonly _invert: boolean; + + constructor(tagsFilter: TagsFilter, embedded: TagDependantUIElementConstructor, invert: boolean = false) { this._tagsFilter = tagsFilter; this._embedded = embedded; + this._invert = invert; } construct(dependencies): TagDependantUIElement { return new OnlyShowIf(dependencies.tags, this._embedded.construct(dependencies), - this._tagsFilter); + this._tagsFilter, + this._invert); } IsKnown(properties: any): boolean { @@ -41,34 +43,38 @@ export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{ return this._embedded.Priority(); } - GetContent(tags: any): string { - if(!this.IsKnown(tags)){ + GetContent(tags: any): Translation { + if(this.IsKnown(tags)){ return undefined; } return this._embedded.GetContent(tags); } private Matches(properties: any) : boolean{ - return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties)); + return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties)) != this._invert; } } class OnlyShowIf extends UIElement implements TagDependantUIElement { - private _embedded: TagDependantUIElement; - private _filter: TagsFilter; + private readonly _embedded: TagDependantUIElement; + private readonly _filter: TagsFilter; + private readonly _invert: boolean; constructor( tags: UIEventSource<any>, - embedded: TagDependantUIElement, filter: TagsFilter) { + embedded: TagDependantUIElement, + filter: TagsFilter, + invert: boolean) { super(tags); this._filter = filter; this._embedded = embedded; + this._invert = invert; } private Matches() : boolean{ - return this._filter.matches(TagUtils.proprtiesToKV(this._source.data)); + return this._filter.matches(TagUtils.proprtiesToKV(this._source.data)) != this._invert; } InnerRender(): string { diff --git a/Customizations/Questions/AccessTag.ts b/Customizations/Questions/AccessTag.ts index 866b36f..8190cdc 100644 --- a/Customizations/Questions/AccessTag.ts +++ b/Customizations/Questions/AccessTag.ts @@ -1,5 +1,4 @@ -import {Changes} from "../../Logic/Osm/Changes"; -import {And, Tag} from "../../Logic/TagsFilter"; +import {And, Tag} from "../../Logic/Tags"; import {TagRenderingOptions} from "../TagRenderingOptions"; export class AccessTag extends TagRenderingOptions { diff --git a/Customizations/Questions/FixedText.ts b/Customizations/Questions/FixedText.ts index dfa7079..fd4fa79 100644 --- a/Customizations/Questions/FixedText.ts +++ b/Customizations/Questions/FixedText.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../../UI/UIElement"; import {TagRenderingOptions} from "../TagRenderingOptions"; import Translation from "../../UI/i18n/Translation"; diff --git a/Customizations/Questions/NameInline.ts b/Customizations/Questions/NameInline.ts index 12c6097..6d53508 100644 --- a/Customizations/Questions/NameInline.ts +++ b/Customizations/Questions/NameInline.ts @@ -1,5 +1,4 @@ -import {And, Tag} from "../../Logic/TagsFilter"; -import {UIElement} from "../../UI/UIElement"; +import {Tag} from "../../Logic/Tags"; import Translations from "../../UI/i18n/Translations"; import {TagRenderingOptions} from "../TagRenderingOptions"; import Translation from "../../UI/i18n/Translation"; diff --git a/Customizations/Questions/NameQuestion.ts b/Customizations/Questions/NameQuestion.ts index 810578b..7b54f8c 100644 --- a/Customizations/Questions/NameQuestion.ts +++ b/Customizations/Questions/NameQuestion.ts @@ -3,7 +3,7 @@ * One is a big 'name-question', the other is the 'edit name' in the title. * THis one is the big question */ -import {Tag} from "../../Logic/TagsFilter"; +import {Tag} from "../../Logic/Tags"; import {TagRenderingOptions} from "../TagRenderingOptions"; export class NameQuestion extends TagRenderingOptions{ diff --git a/Customizations/Questions/OperatorTag.ts b/Customizations/Questions/OperatorTag.ts index e26440b..dc9290a 100644 --- a/Customizations/Questions/OperatorTag.ts +++ b/Customizations/Questions/OperatorTag.ts @@ -1,5 +1,4 @@ -import {Changes} from "../../Logic/Osm/Changes"; -import {Tag} from "../../Logic/TagsFilter"; +import {Tag} from "../../Logic/Tags"; import {TagRenderingOptions} from "../TagRenderingOptions"; diff --git a/Customizations/Questions/OsmLink.ts b/Customizations/Questions/OsmLink.ts index ef50fd0..97d8821 100644 --- a/Customizations/Questions/OsmLink.ts +++ b/Customizations/Questions/OsmLink.ts @@ -1,5 +1,5 @@ import {Img} from "../../UI/Img"; -import {Tag} from "../../Logic/TagsFilter"; +import {Tag} from "../../Logic/Tags"; import {TagRenderingOptions} from "../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/CafeDiy.ts b/Customizations/Questions/bike/CafeDiy.ts index d090ab9..d82d525 100644 --- a/Customizations/Questions/bike/CafeDiy.ts +++ b/Customizations/Questions/bike/CafeDiy.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/CafePump.ts b/Customizations/Questions/bike/CafePump.ts index 088ed54..fd748bf 100644 --- a/Customizations/Questions/bike/CafePump.ts +++ b/Customizations/Questions/bike/CafePump.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/CafeRepair.ts b/Customizations/Questions/bike/CafeRepair.ts index d191956..74df83c 100644 --- a/Customizations/Questions/bike/CafeRepair.ts +++ b/Customizations/Questions/bike/CafeRepair.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/ParkingAccessCargo.ts b/Customizations/Questions/bike/ParkingAccessCargo.ts index 00a2983..9a188a5 100644 --- a/Customizations/Questions/bike/ParkingAccessCargo.ts +++ b/Customizations/Questions/bike/ParkingAccessCargo.ts @@ -1,4 +1,4 @@ -import { Tag } from "../../../Logic/TagsFilter"; +import { Tag } from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/ParkingCapacityCargo.ts b/Customizations/Questions/bike/ParkingCapacityCargo.ts index a43bbc0..7841259 100644 --- a/Customizations/Questions/bike/ParkingCapacityCargo.ts +++ b/Customizations/Questions/bike/ParkingCapacityCargo.ts @@ -1,5 +1,4 @@ import Translations from "../../../UI/i18n/Translations"; -import Combine from "../../../UI/Base/Combine"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/ParkingCovered.ts b/Customizations/Questions/bike/ParkingCovered.ts index 56adaff..abbf64f 100644 --- a/Customizations/Questions/bike/ParkingCovered.ts +++ b/Customizations/Questions/bike/ParkingCovered.ts @@ -1,4 +1,4 @@ -import { Tag } from "../../../Logic/TagsFilter"; +import { Tag } from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/ParkingOperator.ts b/Customizations/Questions/bike/ParkingOperator.ts index 29cd907..ed89887 100644 --- a/Customizations/Questions/bike/ParkingOperator.ts +++ b/Customizations/Questions/bike/ParkingOperator.ts @@ -1,4 +1,4 @@ -import {Tag, And} from "../../../Logic/TagsFilter"; +import {Tag, And} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/ParkingType.ts b/Customizations/Questions/bike/ParkingType.ts index 2ecb217..d0c9b0f 100644 --- a/Customizations/Questions/bike/ParkingType.ts +++ b/Customizations/Questions/bike/ParkingType.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import Combine from "../../../UI/Base/Combine"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/PumpManometer.ts b/Customizations/Questions/bike/PumpManometer.ts index 3d52d58..4b5db8f 100644 --- a/Customizations/Questions/bike/PumpManometer.ts +++ b/Customizations/Questions/bike/PumpManometer.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/PumpManual.ts b/Customizations/Questions/bike/PumpManual.ts index 92ad165..26ba4e6 100644 --- a/Customizations/Questions/bike/PumpManual.ts +++ b/Customizations/Questions/bike/PumpManual.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/PumpOperational.ts b/Customizations/Questions/bike/PumpOperational.ts index fcd6797..045e8bc 100644 --- a/Customizations/Questions/bike/PumpOperational.ts +++ b/Customizations/Questions/bike/PumpOperational.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/PumpValves.ts b/Customizations/Questions/bike/PumpValves.ts index e8f63e8..c41d415 100644 --- a/Customizations/Questions/bike/PumpValves.ts +++ b/Customizations/Questions/bike/PumpValves.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/ShopDiy.ts b/Customizations/Questions/bike/ShopDiy.ts index b074ccf..2a643a7 100644 --- a/Customizations/Questions/bike/ShopDiy.ts +++ b/Customizations/Questions/bike/ShopDiy.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/ShopPump.ts b/Customizations/Questions/bike/ShopPump.ts index 83acdf8..efa2365 100644 --- a/Customizations/Questions/bike/ShopPump.ts +++ b/Customizations/Questions/bike/ShopPump.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/ShopRental.ts b/Customizations/Questions/bike/ShopRental.ts index f4659c8..412b923 100644 --- a/Customizations/Questions/bike/ShopRental.ts +++ b/Customizations/Questions/bike/ShopRental.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/ShopRepair.ts b/Customizations/Questions/bike/ShopRepair.ts index 8193ba4..1196820 100644 --- a/Customizations/Questions/bike/ShopRepair.ts +++ b/Customizations/Questions/bike/ShopRepair.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/ShopRetail.ts b/Customizations/Questions/bike/ShopRetail.ts index 6b98f4c..9f268c1 100644 --- a/Customizations/Questions/bike/ShopRetail.ts +++ b/Customizations/Questions/bike/ShopRetail.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/ShopSecondHand.ts b/Customizations/Questions/bike/ShopSecondHand.ts index 064b603..c8f3379 100644 --- a/Customizations/Questions/bike/ShopSecondHand.ts +++ b/Customizations/Questions/bike/ShopSecondHand.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/StationBrand.ts b/Customizations/Questions/bike/StationBrand.ts deleted file mode 100644 index 17211b7..0000000 --- a/Customizations/Questions/bike/StationBrand.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {Tag} from "../../../Logic/TagsFilter"; -import {TagRenderingOptions} from "../../TagRenderingOptions"; - - -/** - * Currently not used in Cyclofix because it's a little vague - * - * TODO: Translations - */ -export default class BikeStationBrand extends TagRenderingOptions { - private static options = { - priority: 15, - question: "What is the brand of this bike station (name of university, shop, city...)?", - freeform: { - key: "brand", - template: "The brand of this bike station is $$$", - renderTemplate: "The brand of this bike station is {operator}", - placeholder: "brand" - }, - mappings: [ - {k: new Tag("brand", "Velo Fix Station"), txt: "Velo Fix Station"} - ] - } - - constructor() { - throw Error('BikeStationBrand disabled') - super(BikeStationBrand.options); - } -} diff --git a/Customizations/Questions/bike/StationChain.ts b/Customizations/Questions/bike/StationChain.ts index c1d616e..730a907 100644 --- a/Customizations/Questions/bike/StationChain.ts +++ b/Customizations/Questions/bike/StationChain.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/StationOperator.ts b/Customizations/Questions/bike/StationOperator.ts index 39380a7..43de41b 100644 --- a/Customizations/Questions/bike/StationOperator.ts +++ b/Customizations/Questions/bike/StationOperator.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/StationPumpTools.ts b/Customizations/Questions/bike/StationPumpTools.ts index 43876f6..8919912 100644 --- a/Customizations/Questions/bike/StationPumpTools.ts +++ b/Customizations/Questions/bike/StationPumpTools.ts @@ -1,4 +1,4 @@ -import {Tag, And} from "../../../Logic/TagsFilter"; +import {Tag, And} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/Questions/bike/StationStand.ts b/Customizations/Questions/bike/StationStand.ts index ca343e2..cb38e90 100644 --- a/Customizations/Questions/bike/StationStand.ts +++ b/Customizations/Questions/bike/StationStand.ts @@ -1,4 +1,4 @@ -import {Tag} from "../../../Logic/TagsFilter"; +import {Tag} from "../../../Logic/Tags"; import Translations from "../../../UI/i18n/Translations"; import {TagRenderingOptions} from "../../TagRenderingOptions"; diff --git a/Customizations/TagRendering.ts b/Customizations/TagRendering.ts index 1e635d2..4993337 100644 --- a/Customizations/TagRendering.ts +++ b/Customizations/TagRendering.ts @@ -1,6 +1,6 @@ import {UIElement} from "../UI/UIElement"; import {UIEventSource} from "../Logic/UIEventSource"; -import {And, Tag, TagsFilter, TagUtils} from "../Logic/TagsFilter"; +import {And, Tag, TagsFilter, TagUtils} from "../Logic/Tags"; import {FixedUiElement} from "../UI/Base/FixedUiElement"; import {SaveButton} from "../UI/SaveButton"; import {VariableUiElement} from "../UI/Base/VariableUIElement"; @@ -15,23 +15,21 @@ import Locale from "../UI/i18n/Locale"; import {State} from "../State"; import {TagRenderingOptions} from "./TagRenderingOptions"; import Translation from "../UI/i18n/Translation"; -import {SubtleButton} from "../UI/Base/SubtleButton"; import Combine from "../UI/Base/Combine"; -export class TagRendering extends UIElement implements TagDependantUIElement { +export class +TagRendering extends UIElement implements TagDependantUIElement { - private _priority: number; - - - private _question: string | Translation; - private _mapping: { k: TagsFilter, txt: string | UIElement, priority?: number }[]; + private readonly _priority: number; + private readonly _question: string | Translation; + private readonly _mapping: { k: TagsFilter, txt: string | UIElement, priority?: number }[]; private currentTags : UIEventSource<any> ; - private _freeform: { + private readonly _freeform: { key: string, template: string | UIElement, renderTemplate: string | Translation, @@ -53,10 +51,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { private readonly _questionSkipped: UIEventSource<boolean> = new UIEventSource<boolean>(false); private readonly _editMode: UIEventSource<boolean> = new UIEventSource<boolean>(false); - - - private static injected = TagRendering.injectFunction(); - + static injectFunction() { // This is a workaround as not to import tagrendering into TagREnderingOptions TagRenderingOptions.tagRendering = (tags, options) => new TagRendering(tags, options); @@ -76,7 +71,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { extraTags?: TagsFilter, }, tagsPreprocessor?: ((tags: any) => any), - mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean }[] + mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[] }) { super(tags); this.ListenTo(Locale.language); @@ -204,7 +199,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { placeholder?: string | Translation, extraTags?: TagsFilter, }, - mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean }[] + mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[] }): InputElement<TagsFilter> { @@ -217,7 +212,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { if(mapping.k === null){ continue; } - if(previousTexts.indexOf(mapping.txt) >= 0){ + if(mapping.hideInAnswer){ continue; } previousTexts.push(this.ApplyTemplate(mapping.txt)); @@ -262,7 +257,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { let isValid = ValidatedTextField.inputValidation[type]; if (isValid === undefined) { - isValid = (str) => true; + isValid = () => true; } let formatter = ValidatedTextField.formatting[type] ?? ((str) => str); @@ -297,7 +292,6 @@ export class TagRendering extends UIElement implements TagDependantUIElement { } - let inputElement: InputElement<TagsFilter>; const textField = new TextField({ placeholder: this._freeform.placeholder, fromString: pickString, @@ -455,9 +449,9 @@ export class TagRendering extends UIElement implements TagDependantUIElement { private ApplyTemplate(template: string | Translation): UIElement { if (template === undefined || template === null) { - throw "Trying to apply a template, but the template is null/undefined" + console.warn("Applying template which is undefined by ",this); // TODO THis error msg can probably be removed + return undefined; } - const self = this; return new VariableUiElement(this.currentTags.map(tags => { const tr = Translations.WT(template); if (tr.Subs === undefined) { diff --git a/Customizations/TagRenderingOptions.ts b/Customizations/TagRenderingOptions.ts index 643e599..0d02343 100644 --- a/Customizations/TagRenderingOptions.ts +++ b/Customizations/TagRenderingOptions.ts @@ -1,21 +1,15 @@ import {Dependencies, TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor"; -import * as EmailValidator from "email-validator"; -import {parsePhoneNumberFromString} from "libphonenumber-js"; -import {UIElement} from "../UI/UIElement"; -import {TagsFilter, TagUtils} from "../Logic/TagsFilter"; +import {TagsFilter, TagUtils} from "../Logic/Tags"; import {OnlyShowIfConstructor} from "./OnlyShowIf"; import {UIEventSource} from "../Logic/UIEventSource"; import Translation from "../UI/i18n/Translation"; +import Translations from "../UI/i18n/Translations"; export class TagRenderingOptions implements TagDependantUIElementConstructor { - - - /** * Notes: by not giving a 'question', one disables the question form alltogether */ - public options: { priority?: number; question?: string | Translation; @@ -27,10 +21,9 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { placeholder?: string | Translation; extraTags?: TagsFilter }; - mappings?: { k: TagsFilter; txt: string | Translation; priority?: number, substitute?: boolean }[] + mappings?: { k: TagsFilter; txt: string | Translation; priority?: number, substitute?: boolean, hideInAnwser?: boolean }[] }; - constructor(options: { @@ -61,7 +54,7 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { * * */ - mappings?: { k: TagsFilter, txt: Translation | string, priority?: number, substitute?: boolean }[], + mappings?: { k: TagsFilter, txt: Translation | string, priority?: number, substitute?: boolean , hideInAnswer?:boolean}[], /** @@ -85,12 +78,11 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { */ tagsPreprocessor?: ((tags: any) => void) }) { - this.options = options; } - OnlyShowIf(tagsFilter: TagsFilter): TagDependantUIElementConstructor { - return new OnlyShowIfConstructor(tagsFilter, this); + OnlyShowIf(tagsFilter: TagsFilter, invert: boolean = false): TagDependantUIElementConstructor { + return new OnlyShowIfConstructor(tagsFilter, this, invert); } @@ -105,39 +97,28 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { if (this.options.freeform !== undefined && tags[this.options.freeform.key] !== undefined) { return false; } - if (this.options.question === undefined) { - return false; - } - - return true; + return this.options.question !== undefined; } - GetContent(tags: any): string { + GetContent(tags: any): Translation { const tagsKV = TagUtils.proprtiesToKV(tags); for (const oneOnOneElement of this.options.mappings ?? []) { if (oneOnOneElement.k === null || oneOnOneElement.k.matches(tagsKV)) { - const mapping = oneOnOneElement.txt; - if (typeof (mapping) === "string") { - return mapping; - } else { - return mapping.InnerRender(); - } + return Translations.WT(oneOnOneElement.txt); } } if (this.options.freeform !== undefined) { - let template = this.options.freeform.renderTemplate; - if (typeof (template) !== "string") { - template = template.InnerRender(); - } - return TagUtils.ApplyTemplate(template, tags); + let template = Translations.WT(this.options.freeform.renderTemplate); + return template.Subs(tags); } + console.warn("No content defined for",tags," with mapping",this); return undefined; } - public static tagRendering: (tags: UIEventSource<any>, options: { priority?: number; question?: string | Translation; freeform?: { key: string; tagsPreprocessor?: (tags: any) => any; template: string | Translation; renderTemplate: string | Translation; placeholder?: string | Translation; extraTags?: TagsFilter }; mappings?: { k: TagsFilter; txt: string | Translation; priority?: number; substitute?: boolean }[] }) => TagDependantUIElement; + public static tagRendering: (tags: UIEventSource<any>, options: { priority?: number; question?: string | Translation; freeform?: { key: string; tagsPreprocessor?: (tags: any) => any; template: string | Translation; renderTemplate: string | Translation; placeholder?: string | Translation; extraTags?: TagsFilter }; mappings?: { k: TagsFilter; txt: string | Translation; priority?: number; substitute?: boolean, hideInAnswer?: boolean }[] }) => TagDependantUIElement; construct(dependencies: Dependencies): TagDependantUIElement { return TagRenderingOptions.tagRendering(dependencies.tags, this.options); diff --git a/Customizations/UIElementConstructor.ts b/Customizations/UIElementConstructor.ts index 13de70b..b17fb8b 100644 --- a/Customizations/UIElementConstructor.ts +++ b/Customizations/UIElementConstructor.ts @@ -1,5 +1,6 @@ import {UIElement} from "../UI/UIElement"; import {UIEventSource} from "../Logic/UIEventSource"; +import Translation from "../UI/i18n/Translation"; export interface Dependencies { @@ -12,7 +13,7 @@ export interface TagDependantUIElementConstructor { IsKnown(properties: any): boolean; IsQuestioning(properties: any): boolean; Priority(): number; - GetContent(tags: any): string; + GetContent(tags: any): Translation; } diff --git a/InitUiElements.ts b/InitUiElements.ts index 61f09ce..ecfcdeb 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -1,5 +1,3 @@ -import {Layout} from "./Customizations/Layout"; -import Locale from "./UI/i18n/Locale"; import Translations from "./UI/i18n/Translations"; import {TabbedComponent} from "./UI/Base/TabbedComponent"; import {ShareScreen} from "./UI/ShareScreen"; @@ -8,12 +6,8 @@ import {CheckBox} from "./UI/Input/CheckBox"; import Combine from "./UI/Base/Combine"; import {UIElement} from "./UI/UIElement"; import {MoreScreen} from "./UI/MoreScreen"; -import {Tag} from "./Logic/TagsFilter"; import {FilteredLayer} from "./Logic/FilteredLayer"; import {FeatureInfoBox} from "./UI/FeatureInfoBox"; -import {ElementStorage} from "./Logic/ElementStorage"; -import {Changes} from "./Logic/Osm/Changes"; -import {OsmConnection} from "./Logic/Osm/OsmConnection"; import {BaseLayers, Basemap} from "./Logic/Leaflet/Basemap"; import {State} from "./State"; import {WelcomeMessage} from "./UI/WelcomeMessage"; @@ -50,30 +44,30 @@ export class InitUiElements { const layoutToUse = State.state.layoutToUse.data; let welcome: UIElement = new WelcomeMessage(); - if (layoutToUse.name === PersonalLayout.NAME) { + if (layoutToUse.id === PersonalLayout.NAME) { welcome = new PersonalLayersPanel(); } const tabs = [ {header: Img.AsImageElement(layoutToUse.icon), content: welcome}, - {header: `<img src='${'./assets/osm-logo.svg'}'>`, content: Translations.t.general.openStreetMapIntro}, + {header: `<img src='./assets/osm-logo.svg'>`, content: Translations.t.general.openStreetMapIntro}, ] if (State.state.featureSwitchShareScreen.data) { - tabs.push({header: `<img src='${'./assets/share.svg'}'>`, content: new ShareScreen()}); + tabs.push({header: `<img src='./assets/share.svg'>`, content: new ShareScreen()}); } if (State.state.featureSwitchMoreQuests.data){ tabs.push({ - header: `<img src='${'./assets/add.svg'}'>` + header: `<img src='./assets/add.svg'>` , content: new MoreScreen() }); } - const fullOptions = new TabbedComponent(tabs); + const fullOptions = new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab); return fullOptions; @@ -94,7 +88,6 @@ export class InitUiElements { new Combine(["<span class='open-button'>", help, "</span>"]) , true ).AttachTo("messagesbox"); - let dontCloseYet = true; const openedTime = new Date().getTime(); State.state.locationControl.addCallback(() => { if (new Date().getTime() - openedTime < 15 * 1000) { @@ -107,7 +100,7 @@ export class InitUiElements { const fullOptions2 = this.CreateWelcomePane(); State.state.fullScreenMessage.setData(fullOptions2) - new FixedUiElement(`<div class='collapse-button-img' class="shadow"><img src='assets/help.svg' alt='help'></div>`).onClick(() => { + new FixedUiElement(`<div class='collapse-button-img shadow'><img src='assets/help.svg' alt='help'></div>`).onClick(() => { State.state.fullScreenMessage.setData(fullOptions2) }).AttachTo("help-button-mobile"); diff --git a/Logic/ElementStorage.ts b/Logic/ElementStorage.ts index a8e52f1..444209b 100644 --- a/Logic/ElementStorage.ts +++ b/Logic/ElementStorage.ts @@ -48,9 +48,4 @@ export class ElementStorage { } console.log("Can not find eventsource with id ", elementId); } - - - removeId(oldId: string) { - delete this._elements[oldId]; - } } \ No newline at end of file diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts index d5e5181..32510cd 100644 --- a/Logic/FilteredLayer.ts +++ b/Logic/FilteredLayer.ts @@ -1,4 +1,4 @@ -import {TagsFilter, TagUtils} from "./TagsFilter"; +import {TagsFilter, TagUtils} from "./Tags"; import {UIEventSource} from "./UIEventSource"; import L from "leaflet" import {GeoOperations} from "./GeoOperations"; @@ -21,6 +21,7 @@ export class FilteredLayer { public readonly name: string | UIElement; public readonly filters: TagsFilter; public readonly isDisplayed: UIEventSource<boolean> = new UIEventSource(true); + private readonly combinedIsDisplayed : UIEventSource<boolean>; public readonly layerDef: LayerDefinition; private readonly _maxAllowedOverlap: number; @@ -29,8 +30,8 @@ export class FilteredLayer { /** The featurecollection from overpass */ - private _dataFromOverpass : any[]; - private _wayHandling: number; + private _dataFromOverpass: any[]; + private readonly _wayHandling: number; /** List of new elements, geojson features */ private _newElements = []; @@ -60,7 +61,12 @@ export class FilteredLayer { this.filters = layerDef.overpassFilter; this._maxAllowedOverlap = layerDef.maxAllowedOverlapPercentage; const self = this; - this.isDisplayed.addCallback(function (isDisplayed) { + this.combinedIsDisplayed = this.isDisplayed.map<boolean>(isDisplayed => { + return isDisplayed && State.state.locationControl.data.zoom >= self.layerDef.minzoom + }, + [State.state.locationControl] + ); + this.combinedIsDisplayed.addCallback(function (isDisplayed) { const map = State.state.bm.map; if (self._geolayer !== undefined && self._geolayer !== null) { if (isDisplayed) { @@ -91,7 +97,8 @@ export class FilteredLayer { const selfFeatures = []; for (let feature of geojson.features) { // feature.properties contains all the properties - var tags = TagUtils.proprtiesToKV(feature.properties); + const tags = TagUtils.proprtiesToKV(feature.properties); + if (this.filters.matches(tags)) { const centerPoint = GeoOperations.centerpoint(feature); feature.properties["_surface"] = ""+GeoOperations.surfaceAreaInSqMeters(feature); @@ -204,7 +211,6 @@ export class FilteredLayer { style: function (feature) { return self._style(feature.properties); }, - pointToLayer: function (feature, latLng) { const style = self._style(feature.properties); let marker; @@ -231,7 +237,7 @@ export class FilteredLayer { const uiElement = self._showOnPopup(eventSource, feature); const popup = L.popup({}, marker).setContent(uiElement.Render()); marker.bindPopup(popup) - .on("popupopen", (popup) => { + .on("popupopen", () => { uiElement.Activate(); uiElement.Update(); }); @@ -264,7 +270,7 @@ export class FilteredLayer { eventSource.addCallback(feature.updateStyle); layer.on("click", function (e) { - const prevSelectedElement = State.state.selectedElement.data?.feature.updateStyle(); + State.state.selectedElement.data?.feature.updateStyle(); State.state.selectedElement.setData({feature: feature}); feature.updateStyle() if (feature.geometry.type === "Point") { @@ -272,13 +278,13 @@ export class FilteredLayer { } const uiElement = self._showOnPopup(eventSource, feature); - - const popup = L.popup({ + + L.popup({ autoPan: true, - }) - .setContent(uiElement.Render()) + }).setContent(uiElement.Render()) .setLatLng(e.latlng) .openOn(State.state.bm.map); + uiElement.Update(); uiElement.Activate(); L.DomEvent.stop(e); // Marks the event as consumed @@ -286,7 +292,7 @@ export class FilteredLayer { } }); - if (this.isDisplayed.data) { + if (this.combinedIsDisplayed.data) { this._geolayer.addTo(State.state.bm.map); } } diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 4ed677e..096d927 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -77,37 +77,6 @@ export class GeoOperations { return false; } - - - /** - * Simple check: that every point of the polygon is inside the container - * @param polygon - * @param container - */ - private static isPolygonInside(polygon, container) { - for (const coor of polygon.geometry.coordinates[0]) { - if (!GeoOperations.inside(coor, container)) { - return false; - } - } - return true; - } - - /** - * Simple check: one point of the polygon is inside the container - * @param polygon - * @param container - */ - private static isPolygonTouching(polygon, container) { - for (const coor of polygon.geometry.coordinates[0]) { - if (GeoOperations.inside(coor, container)) { - return true; - } - } - return false; - } - - private static inside(pointCoordinate, feature): boolean { // ray-casting algorithm based on // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html @@ -124,7 +93,7 @@ export class GeoOperations { let poly = feature.geometry.coordinates[0]; var inside = false; - for (var i = 0, j = poly.length - 1; i < poly.length; j = i++) { + for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { const coori = poly[i]; const coorj = poly[j]; @@ -133,7 +102,7 @@ export class GeoOperations { const xj = coorj[0]; const yj = coorj[1]; - var intersect = ((yi > y) != (yj > y)) + const intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); if (intersect) { inside = !inside; @@ -146,7 +115,7 @@ export class GeoOperations { } -class BBox { +class BBox{ readonly maxLat: number; readonly maxLon: number; @@ -188,10 +157,8 @@ class BBox { if (this.minLon > other.maxLon) { return false; } - if (this.minLat > other.maxLat) { - return false; - } - return true; + return this.minLat <= other.maxLat; + } static get(feature) { diff --git a/Logic/ImageSearcher.ts b/Logic/ImageSearcher.ts index f79b31d..2efe9f1 100644 --- a/Logic/ImageSearcher.ts +++ b/Logic/ImageSearcher.ts @@ -1,12 +1,11 @@ import {WikimediaImage} from "../UI/Image/WikimediaImage"; import {SimpleImageElement} from "../UI/Image/SimpleImageElement"; import {UIElement} from "../UI/UIElement"; -import {Changes} from "./Osm/Changes"; import {ImgurImage} from "../UI/Image/ImgurImage"; import {State} from "../State"; import {ImagesInCategory, Wikidata, Wikimedia} from "./Web/Wikimedia"; import {UIEventSource} from "./UIEventSource"; -import {Tag} from "./TagsFilter"; +import {Tag} from "./Tags"; /** * There are multiple way to fetch images for an object @@ -150,7 +149,6 @@ export class ImageSearcher extends UIEventSource<string[]> { } for (const key in this._tags.data) { - // @ts-ignore if (key.startsWith("image:")) { const url = this._tags.data[key] this.AddImage(url); diff --git a/Logic/LayerUpdater.ts b/Logic/LayerUpdater.ts index a89ff4e..6472118 100644 --- a/Logic/LayerUpdater.ts +++ b/Logic/LayerUpdater.ts @@ -1,9 +1,8 @@ -import {Or, TagsFilter} from "./TagsFilter"; +import {Or, TagsFilter} from "./Tags"; import {UIEventSource} from "./UIEventSource"; import {FilteredLayer} from "./FilteredLayer"; import {Bounds} from "./Bounds"; import {Overpass} from "./Osm/Overpass"; -import {Basemap} from "./Leaflet/Basemap"; import {State} from "../State"; export class LayerUpdater { @@ -18,20 +17,20 @@ export class LayerUpdater { * If the map location changes, we check for each layer if it is loaded: * we start checking the bounds at the first zoom level the layer might operate. If in bounds - no reload needed, otherwise we continue walking down */ - private previousBounds: Map<number, Bounds[]> = new Map<number, Bounds[]>(); + private readonly previousBounds: Map<number, Bounds[]> = new Map<number, Bounds[]>(); /** * The most important layer should go first, as that one gets first pick for the questions - * @param map - * @param minzoom - * @param layers */ constructor(state: State) { const self = this; - let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom)); - this.sufficentlyZoomed = State.state.locationControl.map(location => location.zoom >= minzoom); + this.sufficentlyZoomed = State.state.locationControl.map(location => { + let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom ?? 18)); + return location.zoom >= minzoom; + }, [state.layoutToUse] + ); for (let i = 0; i < 25; i++) { // This update removes all data on all layers -> erase the map on lower levels too this.previousBounds.set(i, []); @@ -47,7 +46,7 @@ export class LayerUpdater { } private GetFilter(state: State) { - var filters: TagsFilter[] = []; + const filters: TagsFilter[] = []; state = state ?? State.state; for (const layer of state.layoutToUse.data.layers) { if (state.locationControl.data.zoom < layer.minzoom) { @@ -142,15 +141,14 @@ export class LayerUpdater { const w = Math.max(-180, bounds.getWest() - diff); const queryBounds = {north: n, east: e, south: s, west: w}; - const z = state.locationControl.data.zoom; - - this.previousBounds.get(z).push(queryBounds); + const z = Math.floor(state.locationControl.data.zoom); this.runningQuery.setData(true); const self = this; const overpass = new Overpass(filter); overpass.queryGeoJson(queryBounds, function (data) { + self.previousBounds.get(z).push(queryBounds); self.handleData(data) }, function (reason) { @@ -162,33 +160,21 @@ export class LayerUpdater { private IsInBounds(state: State, bounds: Bounds): boolean { - if (this.previousBounds === undefined) { return false; } - const b = state.bm.map.getBounds(); - if (b.getSouth() < bounds.south) { - return false; - } - - if (b.getNorth() > bounds.north) { - return false; - } - - if (b.getEast() > bounds.east) { - return false; - } - if (b.getWest() < bounds.west) { - return false; - } - - return true; + return b.getSouth() >= bounds.south && + b.getNorth() <= bounds.north && + b.getEast() <= bounds.east && + b.getWest() >= bounds.west; } - public ForceRefresh(){ - this.previousBounds = undefined; + public ForceRefresh() { + for (let i = 0; i < 25; i++) { + this.previousBounds.set(i, []); + } } } \ No newline at end of file diff --git a/Logic/Leaflet/Basemap.ts b/Logic/Leaflet/Basemap.ts index 88cf722..6d38dce 100644 --- a/Logic/Leaflet/Basemap.ts +++ b/Logic/Leaflet/Basemap.ts @@ -60,12 +60,12 @@ export class Basemap { // @ts-ignore - public map: Map; + public readonly map: Map; - public Location: UIEventSource<{ zoom: number, lat: number, lon: number }>; - public LastClickLocation: UIEventSource<{ lat: number, lon: number }> = new UIEventSource<{ lat: number, lon: number }>(undefined) - private _previousLayer: L.tileLayer = undefined; - public CurrentLayer: UIEventSource<{ + public readonly Location: UIEventSource<{ zoom: number, lat: number, lon: number }>; + public readonly LastClickLocation: UIEventSource<{ lat: number, lon: number }> = new UIEventSource<{ lat: number, lon: number }>(undefined) + private _previousLayer: L.tileLayer = undefined; + public readonly CurrentLayer: UIEventSource<{ id: string, name: string, layer: L.tileLayer diff --git a/Logic/Leaflet/GeoLocationHandler.ts b/Logic/Leaflet/GeoLocationHandler.ts index 745b3a5..53cc678 100644 --- a/Logic/Leaflet/GeoLocationHandler.ts +++ b/Logic/Leaflet/GeoLocationHandler.ts @@ -7,10 +7,10 @@ import {Basemap} from "./Basemap"; export class GeoLocationHandler extends UIElement { - private _isActive: UIEventSource<boolean> = new UIEventSource<boolean>(false); - private _permission: UIEventSource<string> = new UIEventSource<string>(""); + private readonly _isActive: UIEventSource<boolean> = new UIEventSource<boolean>(false); + private readonly _permission: UIEventSource<string> = new UIEventSource<string>(""); private _marker: any; - private _hasLocation: UIEventSource<boolean>; + private readonly _hasLocation: UIEventSource<boolean>; constructor() { super(undefined); @@ -84,13 +84,13 @@ export class GeoLocationHandler extends UIElement { } if (this._hasLocation.data) { - return "<img src='assets/crosshair-blue.svg' alt='locate me'>"; + return "<img src='./assets/crosshair-blue.svg' alt='locate me'>"; } if (this._isActive.data) { - return "<img src='assets/crosshair-blue-center.svg' alt='locate me'>"; + return "<img src='./assets/crosshair-blue-center.svg' alt='locate me'>"; } - return "<img src='assets/crosshair.svg' alt='locate me'>"; + return "<img src='./assets/crosshair.svg' alt='locate me'>"; } diff --git a/Logic/Leaflet/StrayClickHandler.ts b/Logic/Leaflet/StrayClickHandler.ts index 576c08a..30f0b16 100644 --- a/Logic/Leaflet/StrayClickHandler.ts +++ b/Logic/Leaflet/StrayClickHandler.ts @@ -1,6 +1,4 @@ -import {Basemap} from "./Basemap"; import L from "leaflet"; -import {UIEventSource} from "../UIEventSource"; import {UIElement} from "../../UI/UIElement"; import {State} from "../../State"; diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index 2a41386..22ef888 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -2,11 +2,8 @@ * Handles all changes made to OSM. * Needs an authenticator via OsmConnection */ -import {UIEventSource} from "../UIEventSource"; -import {OsmConnection} from "./OsmConnection"; import {OsmNode, OsmObject} from "./OsmObject"; -import {And, Tag, TagsFilter} from "../TagsFilter"; -import {ElementStorage} from "../ElementStorage"; +import {And, Tag, TagsFilter} from "../Tags"; import {State} from "../../State"; import {Utils} from "../../Utils"; @@ -163,6 +160,8 @@ export class Changes { console.log("Beginning upload..."); // At last, we build the changeset and upload State.state.osmConnection.UploadChangeset( + State.state.layoutToUse.data, + State.state.allElements, function (csId) { let modifications = ""; @@ -190,11 +189,10 @@ export class Changes { } if (modifications.length > 0) { - changes += - "<modify>" + + "<modify>\n" + modifications + - "</modify>"; + "\n</modify>"; } changes += "</osmChange>"; diff --git a/Logic/Osm/ChangesetHandler.ts b/Logic/Osm/ChangesetHandler.ts index a235d60..fb16944 100644 --- a/Logic/Osm/ChangesetHandler.ts +++ b/Logic/Osm/ChangesetHandler.ts @@ -1,6 +1,8 @@ -import {State} from "../../State"; import {OsmConnection, UserDetails} from "./OsmConnection"; import {UIEventSource} from "../UIEventSource"; +import {ElementStorage} from "../ElementStorage"; +import {Layout} from "../../Customizations/Layout"; +import {State} from "../../State"; export class ChangesetHandler { @@ -22,11 +24,14 @@ export class ChangesetHandler { } - public UploadChangeset(generateChangeXML: (csid: string) => string, - continuation: () => void) { + public UploadChangeset( + layout: Layout, + allElements: ElementStorage, + generateChangeXML: (csid: string) => string, + continuation: () => void) { if (this._dryRun) { - var changesetXML = generateChangeXML("123456"); + const changesetXML = generateChangeXML("123456"); console.log(changesetXML); continuation(); return; @@ -34,14 +39,14 @@ export class ChangesetHandler { const self = this; - if (this.currentChangeset.data === undefined || this.currentChangeset.data === "") { // We have to open a new changeset - this.OpenChangeset((csId) => { + this.OpenChangeset(layout,(csId) => { this.currentChangeset.setData(csId); const changeset = generateChangeXML(csId); console.log(changeset); self.AddChange(csId, changeset, + allElements, () => { }, (e) => { @@ -55,6 +60,7 @@ export class ChangesetHandler { self.AddChange( csId, generateChangeXML(csId), + allElements, () => { }, (e) => { @@ -62,7 +68,7 @@ export class ChangesetHandler { // Mark the CS as closed... this.currentChangeset.setData(""); // ... and try again. As the cs is closed, no recursive loop can exist - self.UploadChangeset(generateChangeXML, continuation); + self.UploadChangeset(layout, allElements, generateChangeXML, continuation); } ) @@ -71,9 +77,10 @@ export class ChangesetHandler { } - private OpenChangeset(continuation: (changesetId: string) => void) { + private OpenChangeset( + layout : Layout, + continuation: (changesetId: string) => void) { - const layout = State.state.layoutToUse.data; const commentExtra = layout.changesetMessage !== undefined? " - "+layout.changesetMessage : ""; this.auth.xhr({ method: 'PUT', @@ -81,8 +88,8 @@ export class ChangesetHandler { options: {header: {'Content-Type': 'text/xml'}}, content: [`<osm><changeset>`, `<tag k="created_by" v="MapComplete ${State.vNumber}" />`, - `<tag k="comment" v="Adding data with #MapComplete for theme #${layout.name}${commentExtra}"/>`, - `<tag k="theme" v="${layout.name}"/>`, + `<tag k="comment" v="Adding data with #MapComplete for theme #${layout.id}${commentExtra}"/>`, + `<tag k="theme" v="${layout.id}"/>`, layout.maintainer !== undefined ? `<tag k="theme-creator" v="${layout.maintainer}"/>` : "", `</changeset></osm>`].join("") }, function (err, response) { @@ -98,6 +105,7 @@ export class ChangesetHandler { private AddChange(changesetId: string, changesetXML: string, + allElements: ElementStorage, continuation: ((changesetId: string, idMapping: any) => void), onFail: ((changesetId: string) => void) = undefined) { this.auth.xhr({ @@ -113,7 +121,7 @@ export class ChangesetHandler { } return; } - const mapping = ChangesetHandler.parseUploadChangesetResponse(response); + const mapping = ChangesetHandler.parseUploadChangesetResponse(response, allElements); console.log("Uploaded changeset ", changesetId); continuation(changesetId, mapping); }); @@ -145,7 +153,7 @@ export class ChangesetHandler { }); } - public static parseUploadChangesetResponse(response: XMLDocument) { + private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) { const nodes = response.getElementsByTagName("node"); // @ts-ignore for (const node of nodes) { @@ -157,9 +165,9 @@ export class ChangesetHandler { continue; } console.log("Rewriting id: ", oldId, "-->", newId); - const element = State.state.allElements.getElement("node/" + oldId); + const element = allElements.getElement("node/" + oldId); element.data.id = "node/" + newId; - State.state.allElements.addElementById("node/" + newId, element); + allElements.addElementById("node/" + newId, element); element.ping(); } diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index b6f5f6e..525eca4 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -1,10 +1,10 @@ // @ts-ignore import osmAuth from "osm-auth"; import {UIEventSource} from "../UIEventSource"; -import {State} from "../../State"; -import {All} from "../../Customizations/Layouts/All"; import {OsmPreferences} from "./OsmPreferences"; import {ChangesetHandler} from "./ChangesetHandler"; +import {Layout} from "../../Customizations/Layout"; +import {ElementStorage} from "../ElementStorage"; export class UserDetails { @@ -32,8 +32,7 @@ export class OsmConnection { constructor(dryRun: boolean, oauth_token: UIEventSource<string>, // Used to keep multiple changesets open and to write to the correct changeset layoutName: string, - singlePage: boolean = true, - useDevServer:boolean = false) { + singlePage: boolean = true) { let pwaStandAloneMode = false; try { @@ -55,7 +54,6 @@ export class OsmConnection { oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI', singlepage: false, auto: true, - url: useDevServer ? "https://master.apis.dev.openstreetmap.org" : undefined }); } else { @@ -65,7 +63,6 @@ export class OsmConnection { singlepage: true, landing: window.location.href, auto: true, - url: useDevServer ? "https://master.apis.dev.openstreetmap.org" : undefined }); } @@ -97,9 +94,12 @@ export class OsmConnection { } - public UploadChangeset(generateChangeXML: (csid: string) => string, + public UploadChangeset( + layout: Layout, + allElements: ElementStorage, + generateChangeXML: (csid: string) => string, continuation: () => void = () => {}) { - this.changesetHandler.UploadChangeset(generateChangeXML, continuation); + this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, continuation); } public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { @@ -168,11 +168,12 @@ export class OsmConnection { data.unreadMessages = parseInt(messages.getAttribute("unread")); data.totalMessages = parseInt(messages.getAttribute("count")); + self.userDetails.ping(); for (const action of self._onLoggedIn) { action(self.userDetails.data); } + self._onLoggedIn = []; - self.userDetails.ping(); }); } diff --git a/Logic/Osm/OsmImageUploadHandler.ts b/Logic/Osm/OsmImageUploadHandler.ts index f7c73da..cce2947 100644 --- a/Logic/Osm/OsmImageUploadHandler.ts +++ b/Logic/Osm/OsmImageUploadHandler.ts @@ -1,18 +1,16 @@ /** * Helps in uplaoding, by generating the rigth title, decription and by adding the tag to the changeset */ -import {Changes} from "./Changes"; import {UIEventSource} from "../UIEventSource"; import {ImageUploadFlow} from "../../UI/ImageUploadFlow"; -import {UserDetails} from "./OsmConnection"; import {SlideShow} from "../../UI/SlideShow"; import {State} from "../../State"; -import {Tag} from "../TagsFilter"; +import {Tag} from "../Tags"; export class OsmImageUploadHandler { - private _tags: UIEventSource<any>; - private _slideShow: SlideShow; - private _preferedLicense: UIEventSource<string>; + private readonly _tags: UIEventSource<any>; + private readonly _slideShow: SlideShow; + private readonly _preferedLicense: UIEventSource<string>; constructor(tags: UIEventSource<any>, preferedLicense: UIEventSource<string>, diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts index 2b2147c..baee083 100644 --- a/Logic/Osm/OsmObject.ts +++ b/Logic/Osm/OsmObject.ts @@ -18,13 +18,21 @@ export abstract class OsmObject { const splitted = id.split("/"); const type = splitted[0]; const idN = splitted[1]; + + const newContinuation = (element: OsmObject) => { + + console.log("Received: ",element); + + continuation(element); + } + switch (type) { case("node"): - return new OsmNode(idN).Download(continuation); + return new OsmNode(idN).Download(newContinuation); case("way"): - return new OsmWay(idN).Download(continuation); + return new OsmWay(idN).Download(newContinuation); case("relation"): - return new OsmRelation(idN).Download(continuation); + return new OsmRelation(idN).Download(newContinuation); } } @@ -38,11 +46,9 @@ export abstract class OsmObject { * @param string * @constructor */ - private Escape(string: string) { - while (string.indexOf('"') >= 0) { - string = string.replace('"', '"'); - } - return string; + private static Escape(string: string) { + return string.replace(/"/g, '"') + .replace(/&/g, "&"); } /** @@ -54,7 +60,7 @@ export abstract class OsmObject { for (const key in this.tags) { const v = this.tags[key]; if (v !== "") { - tags += ' <tag k="' + this.Escape(key) + '" v="' + this.Escape(this.tags[key]) + '"/>\n' + tags += ' <tag k="' + OsmObject.Escape(key) + '" v="' + OsmObject.Escape(this.tags[key]) + '"/>\n' } } return tags; diff --git a/Logic/Osm/OsmPreferences.ts b/Logic/Osm/OsmPreferences.ts index f866f9b..dc7a36b 100644 --- a/Logic/Osm/OsmPreferences.ts +++ b/Logic/Osm/OsmPreferences.ts @@ -1,6 +1,5 @@ import {UIEventSource} from "../UIEventSource"; import {OsmConnection, UserDetails} from "./OsmConnection"; -import {All} from "../../Customizations/Layouts/All"; import {Utils} from "../../Utils"; export class OsmPreferences { @@ -68,8 +67,6 @@ export class OsmPreferences { } if (l > 25) { throw "Length to long"; - source.setData(undefined); - return; } const prefsCount = Number(l); let str = ""; @@ -154,7 +151,7 @@ export class OsmPreferences { method: 'DELETE', path: '/api/0.6/user/preferences/' + k, options: {header: {'Content-Type': 'text/plain'}}, - }, function (error, result) { + }, function (error) { if (error) { console.log("Could not remove preference", error); return; @@ -172,7 +169,7 @@ export class OsmPreferences { path: '/api/0.6/user/preferences/' + k, options: {header: {'Content-Type': 'text/plain'}}, content: v - }, function (error, result) { + }, function (error) { if (error) { console.log(`Could not set preference "${k}"'`, error); return; diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts index f6974ce..2e5e3c3 100644 --- a/Logic/Osm/Overpass.ts +++ b/Logic/Osm/Overpass.ts @@ -1,11 +1,11 @@ -/** - * Interfaces overpass to get all the latest data - */ import {Bounds} from "../Bounds"; -import {TagsFilter} from "../TagsFilter"; +import {TagsFilter} from "../Tags"; import $ from "jquery" import * as OsmToGeoJson from "osmtogeojson"; +/** + * Interfaces overpass to get all the latest data + */ export class Overpass { private _filter: TagsFilter public static testUrl: string = null diff --git a/Logic/PersonalLayersPanel.ts b/Logic/PersonalLayersPanel.ts index 3b9a680..7a97f5a 100644 --- a/Logic/PersonalLayersPanel.ts +++ b/Logic/PersonalLayersPanel.ts @@ -6,14 +6,8 @@ import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; import Combine from "../UI/Base/Combine"; import {Img} from "../UI/Img"; import {CheckBox} from "../UI/Input/CheckBox"; -import {VerticalCombine} from "../UI/Base/VerticalCombine"; -import {FixedUiElement} from "../UI/Base/FixedUiElement"; -import {SubtleButton} from "../UI/Base/SubtleButton"; import {PersonalLayout} from "./PersonalLayout"; -import {All} from "../Customizations/Layouts/All"; import {Layout} from "../Customizations/Layout"; -import {TagDependantUIElement} from "../Customizations/UIElementConstructor"; -import {TagRendering} from "../Customizations/TagRendering"; export class PersonalLayersPanel extends UIElement { private checkboxes: UIElement[] = []; @@ -22,7 +16,6 @@ export class PersonalLayersPanel extends UIElement { super(State.state.favouriteLayers); this.ListenTo(State.state.osmConnection.userDetails); - const t = Translations.t.favourite; this.UpdateView([]); const self = this; @@ -38,9 +31,9 @@ export class PersonalLayersPanel extends UIElement { const favs = State.state.favouriteLayers.data ?? []; const controls = new Map<string, UIEventSource<boolean>>(); const allLayouts = AllKnownLayouts.layoutsList.concat(extraThemes); + console.log("ALL LAYOUTS", allLayouts) for (const layout of allLayouts) { - - if (layout.name === PersonalLayout.NAME) { + if (layout.id === PersonalLayout.NAME) { continue; } if (layout.hideFromOverview && @@ -60,10 +53,9 @@ export class PersonalLayersPanel extends UIElement { this.checkboxes.push(header); for (const layer of layout.layers) { - let icon = layer.icon; if (icon !== undefined && typeof (icon) !== "string") { - icon = icon.GetContent({"id": "node/-1"}) ?? "./assets/bug.svg"; + icon = icon.GetContent({"id": "node/-1"}).txt ?? "./assets/bug.svg"; } const image = (layer.icon ? `<img src='${layer.icon}'>` : Img.checkmark); const noimage = (layer.icon ? `<img src='${layer.icon}'>` : Img.no_checkmark); diff --git a/Logic/TagsFilter.ts b/Logic/Tags.ts similarity index 52% rename from Logic/TagsFilter.ts rename to Logic/Tags.ts index 6d4124a..116b7d7 100644 --- a/Logic/TagsFilter.ts +++ b/Logic/Tags.ts @@ -5,7 +5,7 @@ export abstract class TagsFilter { abstract asOverpass(): string[] abstract substituteValues(tags: any) : TagsFilter; - matchesProperties(properties: any) : boolean{ + matchesProperties(properties: Map<string, string>): boolean { return this.matches(TagUtils.proprtiesToKV(properties)); } @@ -13,80 +13,60 @@ export abstract class TagsFilter { } -export class Regex extends TagsFilter { - private _k: string; - private _r: string; +export class RegexTag extends TagsFilter { + private readonly key: RegExp; + private readonly value: RegExp; + private readonly invert: boolean; - constructor(k: string, r: string) { + constructor(key: RegExp, value: RegExp, invert: boolean = false) { super(); - this._k = k; - this._r = r; + this.key = key; + this.value = value; + this.invert = invert; } asOverpass(): string[] { - return ["['" + this._k + "'~'" + this._r + "']"]; + + return [`['${this.key.source}'${this.invert ? "!" : ""}~'${this.value.source}']`]; } matches(tags: { k: string; v: string }[]): boolean { - if(!(tags instanceof Array)){ - throw "You used 'matches' on something that is not a list. Did you mean to use 'matchesProperties'?" - } - for (const tag of tags) { - if (tag.k === this._k) { - if (tag.v === "") { - // This tag has been removed - return false; - } - if (this._r === "*") { - // Any is allowed - return true; - } - - - const matchCount =tag.v.match(this._r)?.length; - return (matchCount ?? 0) > 0; + if (tag.k.match(this.key)) { + return tag.v.match(this.value) !== null; } } return false; } substituteValues(tags: any) : TagsFilter{ - throw "Substituting values is not supported on regex tags" + console.warn("Not substituting values on regex tags"); + return this; } asHumanString() { - return this._k+"~="+this._r; + return `${this.key}${this.invert ? "!" : ""}~${this.value}`; } } export class Tag extends TagsFilter { public key: string - public value: string | RegExp - public invertValue: boolean - - constructor(key: string | RegExp, value: string | RegExp, invertValue = false) { - - if (value instanceof RegExp && invertValue) { - throw new Error("Unsupported combination: RegExp value and inverted value (use regex to invert the match)") - } + public value: string + constructor(key: string, value: string) { super() - // @ts-ignore this.key = key - // @ts-ignore this.value = value - this.invertValue = invertValue - } - - private static regexOrStrMatches(regexOrStr: string | RegExp, testStr: string) { - if (typeof regexOrStr === 'string') { - return regexOrStr === testStr - } else if (regexOrStr instanceof RegExp) { - return (regexOrStr as RegExp).test(testStr) + if(key === undefined || key === ""){ + throw "Invalid key"; + } + if(value === undefined){ + throw "Invalid value"; + } + if(value === undefined || value === "*"){ + console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}!~*`) } - throw new Error("<regexOrStr> must be of type RegExp or string") } matches(tags: { k: string; v: string }[]): boolean { @@ -95,70 +75,36 @@ export class Tag extends TagsFilter { } for (const tag of tags) { - - if (Tag.regexOrStrMatches(this.key, tag.k)) { + if (this.key == tag.k) { if (tag.v === "") { // This tag has been removed -> always matches false return false; } - if (this.value === "*") { - // Any is allowed (as long as the tag is not empty) + + if (this.value === tag.v) { return true; } - - if(this.value === tag.v){ - return !this.invertValue; - } - - return Tag.regexOrStrMatches(this.value, tag.v) !== this.invertValue } } - - return this.invertValue + + return false; } asOverpass(): string[] { - // @ts-ignore - const keyIsRegex = this.key instanceof RegExp - // @ts-ignore - const key = keyIsRegex ? (this.key as RegExp).source : this.key - - // @ts-ignore - const valIsRegex = this.value instanceof RegExp - // @ts-ignore - const val = valIsRegex ? (this.value as RegExp).source : this.value - - const regexKeyPrefix = keyIsRegex ? '~' : '' - const anyVal = this.value === "*" - - if (anyVal && !keyIsRegex) { - return [`[${regexKeyPrefix}"${key}"]`]; - } if (this.value === "") { // NOT having this key - return ['[!"' + key + '"]']; + return ['[!"' + this.key + '"]']; } - - const compareOperator = (valIsRegex || keyIsRegex) ? '~' : (this.invertValue ? '!=' : '=') - return [`[${regexKeyPrefix}"${key}"${compareOperator}"${keyIsRegex && anyVal ? '.' : val}"]`]; + return [`["${this.key}"="${this.value}"]`]; } substituteValues(tags: any) { - if (typeof this.value !== 'string') { - throw new Error("substituteValues() only possible with tag value of type string") - } return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags)); } asHumanString(linkToWiki: boolean, shorten: boolean) { - let v = "" - if (typeof (this.value) === "string") { - v = this.value; - } else { - // value is a regex - v = this.value.source; - } + let v = this.value; if (shorten) { v = Utils.EllipsesAfter(v, 25); } @@ -167,26 +113,11 @@ export class Tag extends TagsFilter { `=` + `<a href='https://wiki.openstreetmap.org/wiki/Tag:${this.key}%3D${this.value}' target='_blank'>${v}</a>` } - - if (typeof (this.value) === "string") { - return this.key + (this.invertValue ? "!=": "=") + v; - }else{ - // value is a regex - return this.key + "~=" + this.value.source; - } - + return this.key + "=" + v; } } -export function anyValueExcept(key: string, exceptValue: string) { - return new And([ - new Tag(key, "*"), - new Tag(key, exceptValue, true) - ]) -} - - export class Or extends TagsFilter { public or: TagsFilter[] @@ -248,8 +179,8 @@ export class And extends TagsFilter { return true; } - private combine(filter: string, choices: string[]): string[] { - var values = [] + private static combine(filter: string, choices: string[]): string[] { + const values = []; for (const or of choices) { values.push(filter + or); } @@ -257,19 +188,18 @@ export class And extends TagsFilter { } asOverpass(): string[] { - var allChoices: string[] = null; - + let allChoices: string[] = null; for (const andElement of this.and) { - var andElementFilter = andElement.asOverpass(); + const andElementFilter = andElement.asOverpass(); if (allChoices === null) { allChoices = andElementFilter; continue; } - var newChoices: string[] = [] - for (var choice of allChoices) { + const newChoices: string[] = []; + for (const choice of allChoices) { newChoices.push( - ...this.combine(choice, andElementFilter) + ...And.combine(choice, andElementFilter) ) } allChoices = newChoices; @@ -291,31 +221,6 @@ export class And extends TagsFilter { } -export class Not extends TagsFilter{ - private not: TagsFilter; - - constructor(not: TagsFilter) { - super(); - this.not = not; - } - - asOverpass(): string[] { - throw "Not supported yet" - } - - matches(tags: { k: string; v: string }[]): boolean { - return !this.not.matches(tags); - } - - substituteValues(tags: any): TagsFilter { - return new Not(this.not.substituteValues(tags)); - } - - asHumanString(linkToWiki: boolean, shorten: boolean) { - return "!" + this.not.asHumanString(linkToWiki, shorten); - } -} - export class TagUtils { static proprtiesToKV(properties: any): { k: string, v: string }[] { diff --git a/Logic/Web/Imgur.ts b/Logic/Web/Imgur.ts index 77837d8..ec47fc3 100644 --- a/Logic/Web/Imgur.ts +++ b/Logic/Web/Imgur.ts @@ -43,7 +43,7 @@ export class Imgur { const apiUrl = 'https://api.imgur.com/3/image/'+hash; const apiKey = '7070e7167f0a25a'; - var settings = { + const settings = { async: true, crossDomain: true, processData: false, @@ -86,7 +86,7 @@ export class Imgur { const apiUrl = 'https://api.imgur.com/3/image'; const apiKey = '7070e7167f0a25a'; - var settings = { + const settings = { async: true, crossDomain: true, processData: false, @@ -99,7 +99,7 @@ export class Imgur { }, mimeType: 'multipart/form-data', }; - var formData = new FormData(); + const formData = new FormData(); formData.append('image', blob); formData.append("title", title); formData.append("description", description) diff --git a/Logic/Web/Wikimedia.ts b/Logic/Web/Wikimedia.ts index a5f3ff1..37be85d 100644 --- a/Logic/Web/Wikimedia.ts +++ b/Logic/Web/Wikimedia.ts @@ -23,7 +23,7 @@ export class Wikimedia { "api.php?action=query&prop=imageinfo&iiprop=extmetadata&" + "titles=" + filename + "&format=json&origin=*"; - $.getJSON(url, function (data, status) { + $.getJSON(url, function (data) { const licenseInfo = new LicenseInfo(); const license = data.query.pages[-1].imageinfo[0].extmetadata; diff --git a/State.ts b/State.ts index d7fa4d8..cb06f51 100644 --- a/State.ts +++ b/State.ts @@ -1,19 +1,18 @@ import {UIElement} from "./UI/UIElement"; import {Layout} from "./Customizations/Layout"; import {Utils} from "./Utils"; -import {LayerDefinition, Preset} from "./Customizations/LayerDefinition"; +import {Preset} from "./Customizations/LayerDefinition"; import {ElementStorage} from "./Logic/ElementStorage"; import {Changes} from "./Logic/Osm/Changes"; import {OsmConnection} from "./Logic/Osm/OsmConnection"; import Locale from "./UI/i18n/Locale"; -import {VariableUiElement} from "./UI/Base/VariableUIElement"; import Translations from "./UI/i18n/Translations"; import {FilteredLayer} from "./Logic/FilteredLayer"; import {LayerUpdater} from "./Logic/LayerUpdater"; import {UIEventSource} from "./Logic/UIEventSource"; import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; import {QueryParameters} from "./Logic/Web/QueryParameters"; -import {CustomLayoutFromJSON} from "./Customizations/JSON/CustomLayoutFromJSON"; +import {FromJSON} from "./Customizations/JSON/FromJSON"; /** * Contains the global state: a bunch of UI-event sources @@ -24,7 +23,7 @@ export class State { // The singleton of the global state public static state: State; - public static vNumber = "0.0.7c mutlizoom"; + public static vNumber = "0.0.7d Refactored"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { @@ -124,6 +123,9 @@ export class State { public layoutDefinition: string; public installedThemes: UIEventSource<{ layout: Layout; definition: string }[]>; + public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab","0").map<number>( + str => isNaN(Number(str)) ? 0 : Number(str),[],n => ""+n + ); constructor(layoutToUse: Layout, useDevServer = false) { const self = this; @@ -175,9 +177,8 @@ export class State { this.osmConnection = new OsmConnection( testParam === "true", QueryParameters.GetQueryParameter("oauth_token", undefined), - layoutToUse.name, - true, - testParam === "dev" + layoutToUse.id, + true ); @@ -201,7 +202,7 @@ export class State { } try { installedThemes.push({ - layout: CustomLayoutFromJSON.FromQueryParam(customLayout.data), + layout: FromJSON.FromBase64(customLayout.data), definition: customLayout.data }); } catch (e) { diff --git a/UI/Base/TabbedComponent.ts b/UI/Base/TabbedComponent.ts index a37fa2d..ebbfc52 100644 --- a/UI/Base/TabbedComponent.ts +++ b/UI/Base/TabbedComponent.ts @@ -7,8 +7,8 @@ export class TabbedComponent extends UIElement { private headers: UIElement[] = []; private content: UIElement[] = []; - constructor(elements: { header: UIElement | string, content: UIElement | string }[]) { - super(new UIEventSource<number>(0)); + constructor(elements: { header: UIElement | string, content: UIElement | string }[], openedTab : UIEventSource<number> = new UIEventSource<number>(0)) { + super(openedTab); const self = this; for (let i = 0; i < elements.length; i++) { let element = elements[i]; diff --git a/UI/CustomThemeGenerator/Preview.ts b/UI/CustomThemeGenerator/Preview.ts deleted file mode 100644 index 3cd9016..0000000 --- a/UI/CustomThemeGenerator/Preview.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {LayoutConfigJson} from "../../Customizations/JSON/CustomLayoutFromJSON"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; -import Combine from "../Base/Combine"; -import {Button} from "../Base/Button"; -import {VariableUiElement} from "../Base/VariableUIElement"; - -export class Preview extends UIElement { - private url: UIEventSource<string>; - private config: UIEventSource<LayoutConfigJson>; - - private currentPreview = new UIEventSource<string>("") - private reloadButton: Button; - private otherPreviews: VariableUiElement; - - constructor(url: UIEventSource<string>, testurl: UIEventSource<string>, config: UIEventSource<LayoutConfigJson>) { - super(undefined); - this.config = config; - this.url = url; - this.reloadButton = new Button("Reload the preview", () => { - this.currentPreview.setData(`<iframe width="99%" height="70%" src="${this.url.data}"></iframe>` + - '<p class="alert">The above preview is in testmode. Changes will not be sent to OSM, so feel free to add points and answer questions</p> ', - ); - }); - this.ListenTo(this.currentPreview); - - - this.otherPreviews = new VariableUiElement(this.url.map(url => { - - return [ - `<h2>Your link</h2>`, - '<span class="alert">Bookmark the link below</span><br/>', - 'MapComplete has no backend. The <i>entire</i> theme configuration is saved in the following URL. This means that this URL is needed to revive and change your MapComplete instance.<br/>', - `<a target='_blank' href='${this.url.data}'>${this.url.data}</a><br/>`, - '<h2>JSON-configuration</h2>', - 'You can see the configuration in JSON format below.<br/>', - '<span class=\'literal-code iframe-code-block\' style="width:95%">', - JSON.stringify(this.config.data, null, 2).replace(/\n/g, "<br/>").replace(/ /g, " "), - '</span>' - - ].join("") - - })); - } - - InnerRender(): string { - const url = this.url.data; - return new Combine([ - new VariableUiElement(this.currentPreview), - this.reloadButton, - "<h2>Statistics</h2>", - "We track statistics with goatcounter. <a href='https://pietervdvn.goatcounter.com' target='_blank'>The statistics can be seen by anyone, so if you want to see where your theme ends up, click here</a>", - this.otherPreviews - ]).Render(); - } - -} \ No newline at end of file diff --git a/UI/CustomThemeGenerator/ThemeGenerator.ts b/UI/CustomThemeGenerator/ThemeGenerator.ts deleted file mode 100644 index fea4db1..0000000 --- a/UI/CustomThemeGenerator/ThemeGenerator.ts +++ /dev/null @@ -1,712 +0,0 @@ -import {UIElement} from "../UIElement"; -import {VerticalCombine} from "../Base/VerticalCombine"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Combine from "../Base/Combine"; -import { - CustomLayoutFromJSON, - LayerConfigJson, - LayoutConfigJson, - TagRenderingConfigJson -} from "../../Customizations/JSON/CustomLayoutFromJSON"; -import {TabbedComponent} from "../Base/TabbedComponent"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {OsmConnection, UserDetails} from "../../Logic/Osm/OsmConnection"; -import {Button} from "../Base/Button"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {TextField, ValidatedTextField} from "../Input/TextField"; -import {Tag} from "../../Logic/TagsFilter"; -import {DropDown} from "../Input/DropDown"; -import {TagRendering} from "../../Customizations/TagRendering"; -import {LayerDefinition} from "../../Customizations/LayerDefinition"; -import {State} from "../../State"; - - -TagRendering.injectFunction(); - -function TagsToString(tags: string | string [] | { k: string, v: string }[]) { - if (tags === undefined) { - return undefined; - } - if (typeof (tags) == "string") { - return tags; - } - const newTags = []; - console.log(tags) - for (const tag of tags) { - if (typeof (tag) == "string") { - newTags.push(tag) - } else { - newTags.push(tag.k + "=" + tag.v); - } - } - return newTags.join(","); -} - -// Defined below, as it needs some context/closure -let createFieldUI: (label: string, key: string, root: any, options: { deflt?: string, type?: string, description: string, emptyAllowed?: boolean }) => UIElement; -let pingThemeObject: () => void; - -class MappingGenerator extends UIElement { - - private elements: UIElement[]; - - constructor(tagRendering: TagRenderingConfigJson, - mapping: { if: string | string[] | { k: string, v: string }[] }) { - super(undefined); - this.CreateElements(tagRendering, mapping) - } - - private CreateElements(tagRendering: TagRenderingConfigJson, - mapping) { - { - const self = this; - this.elements = [ - new FixedUiElement("A mapping shows a specific piece of text if a specific tag is present. If no mapping is known and no key matches (and the question is defined), then the mappings show up as radio buttons to answer the question and to update OSM"), - createFieldUI("If these tags apply", "if", mapping, { - type: "tags", - description: "The tags that have to be present. Use <span class='literal-code'>key=</span> to indicate an implicit assumption. 'key=' can be used to indicate: 'if this key is missing'" - }), - createFieldUI("Then: show this text", "then", mapping, {description: "The text that is shown"}), - new Button("Remove this mapping", () => { - for (let i = 0; i < tagRendering.mappings.length; i++) { - if (tagRendering.mappings[i] === mapping) { - tagRendering.mappings.splice(i, 1); - self.elements = [ - new FixedUiElement("Tag mapping removed") - ] - self.Update(); - pingThemeObject(); - break; - } - } - }) - ]; - } - } - - InnerRender(): string { - const combine = new VerticalCombine(this.elements).SetClass("bordered"); - return combine.Render(); - } -} - -class TagRenderingGenerator - extends UIElement { - - private elements: UIElement[]; - - constructor(fullConfig: UIEventSource<LayoutConfigJson>, - layerConfig: LayerConfigJson, - tagRendering: TagRenderingConfigJson, - options: { header: string, description: string, removable: boolean, hideQuestion: boolean }) { - super(undefined); - this.CreateElements(fullConfig, layerConfig, tagRendering, options) - } - - private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson, tagRendering: TagRenderingConfigJson, - options: { header: string, description: string, removable: boolean, hideQuestion: boolean }) { - - - const self = this; - this.elements = [ - new FixedUiElement(`<h3>${options.header}</h3>`), - new FixedUiElement(options.description), - options.hideQuestion ? new FixedUiElement("") : createFieldUI("Key", "key", tagRendering, { - deflt: "name", - type: "key", - description: "Optional. A single key, such as <span class='literal-code'>name</span> &npbs, &npbs <span class='literal-code'>surface</span>. If the object contains a tag with the specified key, the rendering below will be shown. Use <span class='literal-code'>*</span> if you want to show the rendering by default. Note that a mapping overrides this" - }), - createFieldUI("Rendering", "render", tagRendering, { - deflt: "The name of this object is {name}", - description: "Optional. If the above key is present, this rendering will be used. Note that <span class='literal-code'>{key}</span> will be replaced by the value - if that key is present. This is _not_ limited to the given key above, it is allowed to use multiple subsitutions." + - "If the above key is _not_ present, the question will be shown and the rendering will be used as answer with {key} as textfield" - }), - options.hideQuestion ? new FixedUiElement("") : createFieldUI("Type", "type", tagRendering, { - deflt: "string", - description: "Input validation of this type", - type: "typeSelector", - - }), - options.hideQuestion ? new FixedUiElement("") : - createFieldUI("Question", "question", tagRendering, { - deflt: "", - description: "Optional. If 'key' is not present (or not given) and none of the mappings below match, then this will be shown as question. Users are then able to answer this question and save the data to OSM. If no question is given, values can still be shown but not answered", - type: "string" - }), - options.hideQuestion ? new FixedUiElement("") : - createFieldUI("Extra tags", "addExtraTags", tagRendering, - { - deflt: "", - type: "tags", - emptyAllowed: true, - description: "Optional. If the freeform text field is used to fill out the tag, these tags are applied as well. The main use case is to flag the object for review. (A prime example is access. A few predefined values are given and the option to fill out something. Here, one can add e.g. <span class='literal-code'>fixme=access was filled out by user, value might not be correct</span>" - }), - - options.hideQuestion ? new FixedUiElement("") : createFieldUI( - "Only show if", "condition", tagRendering, - { - deflt: "", - type: "tags", - emptyAllowed: true, - description: "Only show this question/rendering if the object also has the specified tag. This can be useful to ask a follow up question only if the prerequisite is met" - } - ), - - ...(tagRendering.mappings ?? []).map((mapping) => { - return new MappingGenerator(tagRendering, mapping) - }), - new Button("Add mapping", () => { - if (tagRendering.mappings === undefined) { - tagRendering.mappings = [] - } - tagRendering.mappings.push({if: "", then: ""}); - self.CreateElements(fullConfig, layerConfig, tagRendering, options); - self.Update(); - }) - - ] - - if (!!options.removable) { - const b = new Button("Remove this tag rendering", () => { - for (let i = 0; i < layerConfig.tagRenderings.length; i++) { - if (layerConfig.tagRenderings[i] === tagRendering) { - layerConfig.tagRenderings.splice(i, 1); - self.elements = [ - new FixedUiElement("Tag rendering removed") - ] - self.Update(); - break; - } - } - }); - this.elements.push(b); - } - - } - - InnerRender(): string { - const combine = new VerticalCombine(this.elements).SetClass("bordered"); - return combine.Render(); - } -} - -class PresetGenerator extends UIElement { - - private elements: UIElement[]; - - constructor(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson, - preset0: { title?: string, description?: string, icon?: string, tags?: string | string[] | { k: string, v: string }[] }) { - super(undefined); - const self = this; - this.elements = [ - new FixedUiElement("<h3>Preset</h3>"), - new FixedUiElement("A preset allows the user to add a new point at a location that was clicked. Note that one layer can have zero, one or multiple presets"), - createFieldUI("Title", "title", preset0, { - description: "The title of this preset, shown in the 'add new {Title} here'-dialog" - }), - createFieldUI("Description", "description", preset0, - { - deflt: layerConfig.description, - type: "string", - description: "A description shown alongside the 'add new'-button" - }), - createFieldUI("tags", "tags", preset0, - { - deflt: TagsToString(layerConfig.overpassTags), type: "tags", - description: "The tags that are added to the newly created point" - }), - new Button("Remove this preset", () => { - for (let i = 0; i < layerConfig.presets.length; i++) { - if (layerConfig.presets[i] === preset0) { - layerConfig.presets.splice(i, 1); - self.elements = [ - new FixedUiElement("Preset removed") - ] - self.Update(); - pingThemeObject(); - break; - } - } - }) - ] - - } - - InnerRender(): string { - const combine = new VerticalCombine(this.elements).SetClass("bordered"); - return combine.Render(); - } - -} - -class LayerGenerator extends UIElement { - private fullConfig: UIEventSource<LayoutConfigJson>; - private layerConfig: UIEventSource<LayerConfigJson>; - private generateField: ((label: string, key: string, root: any, deflt?: string) => UIElement); - private uielements: UIElement[]; - - constructor(fullConfig: UIEventSource<LayoutConfigJson>, - layerConfig: LayerConfigJson) { - super(undefined); - this.layerConfig = new UIEventSource<LayerConfigJson>(layerConfig); - this.fullConfig = fullConfig; - this.CreateElements(fullConfig, layerConfig) - - } - - private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson) { - - - // Init some defaults - layerConfig.title = layerConfig.title ?? { - key: "*", - addExtraTags: "", - mappings: [], - question: "", - render: "Title", - type: "text" - }; - layerConfig.title.key = "*"; - - layerConfig.icon = layerConfig.icon ?? { - key: "*", - addExtraTags: "", - mappings: [], - question: "", - render: "./assets/bug.svg", - type: "text" - }; - layerConfig.icon.key = "*"; - - - layerConfig.color = layerConfig.color ?? { - key: "*", - addExtraTags: "", - mappings: [], - question: "", - render: "#00f", - type: "text" - }; - layerConfig.color.key = "*"; - - layerConfig.width = layerConfig.width?? { - key: "*", - addExtraTags: "", - mappings: [], - question: "", - render: "10", - type: "nat" - }; - layerConfig.width.key = "*" - - - const self = this; - this.uielements = [ - - new FixedUiElement("<p>A layer is a collection of related objects which have the same or very similar tags renderings. In general, all objects of one layer have the same icon (or at least very similar icons)</p>"), - - createFieldUI("Name", "name", layerConfig, {description: "The name of this layer"}), - createFieldUI("A description of objects for this layer", "description", layerConfig, {description: "The description of this layer"}), - createFieldUI("Minimum zoom level", "minzoom", layerConfig, { - type: "nat", - deflt: "12", - description: "The minimum zoom level to start loading data. This is mainly limited by the expected number of objects: if there are a lot of objects, then pick something higher. A generous bounding box is put around the map, so some scrolling should be possible" - }), - createFieldUI("The tags to load from overpass", "overpassTags", layerConfig, { - type: "tags", - description: "Tags to load from overpass. " + - "The format is <span class='literal-code'>key=value&key0=value0&key1=value1</span>, e.g. <span class='literal-code'>amenity=public_bookcase</span> or <span class='literal-code'>amenity=compressed_air&bicycle=yes</span>." + - "Special values are:" + - "<ul>" + - "<li> <span class='literal-code'>key=*</span> to indicate that this key can be anything</li>. " + - "<li><span class='literal-code'>key=</span> means 'key is NOT present'</li>" + - "<li><span class='literal-code'>key!=value</span> means 'key does NOT have this value'</li>" + - "<li><span class='literal-code'>key~=regex</span> indicates a regex, e.g. <b>highway~=residential|tertiary</b></li>"+ - "</ul>"+ - ". E.g. something that is indoor, not private and has no name tag can be queried as <span class='literal-code'>indoor=yes&name=&access!=private</span>" - }), - - createFieldUI("Wayhandling","wayHandling", layerConfig, { - type:"wayhandling", - description: "Specifies how ways (lines and areas) are handled: either the way is shown, a center point is shown or both" - }), - - new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.title, { - header: "Title element", - description: "This element is shown in the title of the popup in a header-tag", - removable: false, - hideQuestion: true - }), - - - new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.icon , { - header: "Icon", - description: "This decides which icon is used to represent an element on the map. Leave blank if you don't want icons to pop up", - removable: false, - hideQuestion: true - }), - - - new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.color, { - header: "Colour", - description: "This decides which color is used to represent a way on the map. Note that if an icon is defined as well, the icon will be showed too", - removable: false, - hideQuestion: true - }), - - new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.width , { - header: "Line thickness", - description: "This decides the line thickness of ways (in pixels)", - removable: false, - hideQuestion: true - }), - - - ...layerConfig.tagRenderings.map(tr => new TagRenderingGenerator(fullConfig, layerConfig, tr, { - header: "Tag rendering", - description: "A single tag rendering", - removable: true, - hideQuestion: false - })), - new Button("Add a tag rendering", () => { - layerConfig.tagRenderings.push({ - key: undefined, - addExtraTags: undefined, - mappings: [], - question: undefined, - render: undefined, - type: "text" - }); - self.CreateElements(fullConfig, layerConfig); - self.Update(); - }), - - - ...layerConfig.presets.map(preset => new PresetGenerator(fullConfig, layerConfig, preset)), - new Button("Add a preset", () => { - layerConfig.presets.push({ - icon: undefined, - title: "", - description: "", - tags: TagsToString(layerConfig.overpassTags) - }); - self.CreateElements(fullConfig, layerConfig); - self.Update(); - }), - - new Button("Remove this layer", () => { - const layers = fullConfig.data.layers; - for (let i = 0; i < layers.length; i++) { - if(layers[i] === layerConfig){ - layers.splice(i, 1); - break; - } - } - self.Update(); - pingThemeObject(); - }) - - ] - } - - InnerRender(): string { - return new VerticalCombine(this.uielements).Render(); - } -} - - -class AllLayerComponent extends UIElement { - - private tabs: TabbedComponent; - private config: UIEventSource<LayoutConfigJson>; - - constructor(config: UIEventSource<LayoutConfigJson>) { - super(undefined); - this.config = config; - const self = this; - let previousLayerAmount = config.data.layers.length; - config.addCallback((data) => { - if (data.layers.length != previousLayerAmount) { - previousLayerAmount = data.layers.length; - self.UpdateTabs(); - self.Update(); - } - }); - this.UpdateTabs(); - - } - - private UpdateTabs() { - const layerPanes: { header: UIElement | string, content: UIElement | string }[] = []; - const config = this.config; - for (const layer of this.config.data.layers) { - const iconUrl = CustomLayoutFromJSON.TagRenderingFromJson(layer?.icon) - ?.GetContent({id: "node/-1"}); - const header = this.config.map(() => { - - return `<img src="${iconUrl ?? "./assets/help.svg"}">` - }); - layerPanes.push({ - header: new VariableUiElement(header), - content: new LayerGenerator(config, layer) - }) - } - - - layerPanes.push({ - header: "<img src='./assets/add.svg'>", - content: new Button("Add a new layer", () => { - config.data.layers.push({ - name: "", - title: { - key: "*", - render: "Title" - }, - icon: { - key: "*", - render: "./assets/bug.svg" - }, - color: { - key: "*", - render: "#0000ff" - }, - width: { - key:"*", - render: "10" - }, - description: "", - minzoom: 12, - overpassTags: "", - wayHandling: LayerDefinition.WAYHANDLING_CENTER_AND_WAY, - presets: [], - tagRenderings: [] - }); - - config.ping(); - }) - }) - - this.tabs = new TabbedComponent(layerPanes); - } - - InnerRender(): string { - return this.tabs.Render(); - } - -} - - -export class ThemeGenerator extends UIElement { - - private readonly userDetails: UIEventSource<UserDetails>; - - public readonly themeObject: UIEventSource<LayoutConfigJson>; - private readonly allQuestionFields: UIElement[]; - public url: UIEventSource<string>; - public testurl: UIEventSource<string>; - - private loginButton: Button - - constructor(connection: OsmConnection, windowHash) { - super(connection.userDetails); - this.userDetails = connection.userDetails; - this.loginButton = new Button("Log in with OSM", () => { - connection.AttemptLogin() - }) - - const defaultTheme = {layers: [], icon: "./assets/bug.svg"}; - let loadedTheme = undefined; - if (windowHash !== undefined && windowHash.length > 4) { - loadedTheme = JSON.parse(atob(windowHash)); - } - - this.themeObject = new UIEventSource<LayoutConfigJson>(loadedTheme ?? defaultTheme); - const jsonObjectRoot = this.themeObject.data; - connection.userDetails.addCallback((userDetails) => { - jsonObjectRoot.maintainer = userDetails.name; - }); - jsonObjectRoot.maintainer = connection.userDetails.data.name; - - - const base64 = this.themeObject.map(JSON.stringify).map(btoa); - let baseUrl = "https://pietervdvn.github.io/MapComplete"; - if (window.location.hostname === "127.0.0.1") { - baseUrl = "http://127.0.0.1:1234"; - } - this.url = base64.map((data) => `${baseUrl}/index.html?userlayout=${this.themeObject.data.name}#${data}`); - this.testurl = base64.map((data) => `${baseUrl}/index.html?test=true&userlayout=${this.themeObject.data.name}#${data}`); - - const self = this; - - pingThemeObject = () => {self.themeObject.ping()}; - createFieldUI = (label, key, root, options) => { - - options = options ?? {description: "?"}; - options.type = options.type ?? "string"; - - const value = new UIEventSource<string>(TagsToString(root[key]) ?? options?.deflt); - - let textField: UIElement; - if (options.type === "typeSelector") { - const options: { value: string, shown: string | UIElement }[] = []; - for (const possibleType in ValidatedTextField.inputValidation) { - if (possibleType !== "$") { - options.push({value: possibleType, shown: possibleType}); - } - } - - textField = new DropDown<string>("", - options, - value) - } else if (options.type === "wayhandling") { - const options: { value: string, shown: string | UIElement }[] = - [{value: "" + LayerDefinition.WAYHANDLING_DEFAULT, shown: "Show a line/area as line/area"}, - { - value: "" + LayerDefinition.WAYHANDLING_CENTER_AND_WAY, - shown: "Show a line/area as line/area AND show an icon at the center" - }, - { - value: "" + LayerDefinition.WAYHANDLING_CENTER_ONLY, - shown: "Only show the centerpoint of a way" - }]; - - textField = new DropDown<string>("", - options, - value) - - } else if (options.type === "key") { - textField = new TextField<string>({ - placeholder: "single key", - startValidated: false, - value:value, - toString: str => str, - fromString: str => { - if(str === undefined){ - return ""; - } - if (str === "*") { - return str; - } - str = str.trim(); - if (str.match("^_*[a-zA-Z]*[a-zA-Z0-9:_]*$") == null) { - return undefined; - } - return str; - } - }) - - } else if (options.type === "tags") { - textField = ValidatedTextField.TagTextField(value.map(CustomLayoutFromJSON.TagsFromJson, [], tags => { - if (tags === undefined) { - return undefined; - } - return tags.map((tag: Tag) => tag.key + "=" + tag.value).join("&"); - }), options?.emptyAllowed ?? false); - } else if (options.type === "img" || options.type === "colour") { - textField = new TextField<string>({ - placeholder: options.type, - fromString: (str) => str, - toString: (str) => str, - value: value, - startValidated: true - }); - } else if (options.type) { - textField = ValidatedTextField.ValidatedTextField(options.type, {value: value}); - } else { - textField = new TextField<string>({ - placeholder: options.type, - fromString: (str) => str, - toString: (str) => str, - value: value, - startValidated: true - }); - } - - let sendingPing = false; - value.addCallback((v) => { - if (v === undefined || v === "") { - delete root[key]; - } else { - root[key] = v; - } - if(!sendingPing){ - sendingPing = true; - self.themeObject.ping(); // We assume the root is a part of the themeObject - sendingPing = false; - } - }); - - self.themeObject.addCallback(() => { - value.setData(root[key]); - }) - - return new Combine([ - label, - textField, - "<br>", - "<span class='subtle'>" + options.description + "</span>" - ]); - } - - this.allQuestionFields = [ - createFieldUI("Name of this theme", "name", jsonObjectRoot, {description: "An identifier for this theme"}), - createFieldUI("Title", "title", jsonObjectRoot, { - deflt: "Title", - description: "The title of this theme, as shown in the welcome message and in the title bar of the browser" - }), - createFieldUI("icon", "icon", jsonObjectRoot, { - deflt: "./assets/bug.svg", - type: "img", - description: "The icon representing this MapComplete instance. It is shown in the welcome message and -if adopted as official theme- used as favicon and to browse themes" - }), - createFieldUI("Description", "description", jsonObjectRoot, { - description: "Shown in the welcome message", - deflt: "Description" - }), - createFieldUI("The supported language", "language", jsonObjectRoot, { - description: "The language of this mapcomplete instance. MapComplete can be translated, see <a href='https://github.com/pietervdvn/MapComplete#translating-mapcomplete' target='_blank'> here for more information</a>", - deflt: "en" - }), - createFieldUI("startLat", "startLat", jsonObjectRoot, { - type: "float", - deflt: "0", - description: "The latitude where this theme should start. Note that this is only for completely fresh users, as the last location is saved" - }), - createFieldUI("startLon", "startLon", jsonObjectRoot, { - type: "float", - deflt: "0", - description: "The longitude where this theme should start. Note that this is only for completely fresh users, as the last location is saved" - }), - createFieldUI("startzoom", "startZoom", jsonObjectRoot, { - type: "nat", - deflt: "12", - description: "The initial zoom level where the map is located" - }), - createFieldUI("Query widening factor", "widenFactor", jsonObjectRoot, { - type: "pfloat", - deflt: "0.05", - description: "When a query is run, the current map view is taken and a margin with a certain factor is added to allow panning and zooming. If you are running heavy queries (e.g. highway=residential), to much data is returned. In that case, lower the widenfactor, e.g. to 0.01-0.02" - }), - - - new AllLayerComponent(this.themeObject) - ] - - - } - - InnerRender(): string { - - if (!this.userDetails.data.loggedIn) { - return new Combine(["Not logged in. You need to be logged in to create a theme.", this.loginButton]).Render(); - } - if (this.userDetails.data.csCount < State.userJourney.themeGeneratorUnlock ) { - return `You need at least ${State.userJourney.themeGeneratorUnlock} changesets to create your own theme.`; - } - - - return new VerticalCombine([ - ...this.allQuestionFields, - ]).Render(); - } -} \ No newline at end of file diff --git a/UI/FeatureInfoBox.ts b/UI/FeatureInfoBox.ts index 8cadc42..ab94a6c 100644 --- a/UI/FeatureInfoBox.ts +++ b/UI/FeatureInfoBox.ts @@ -1,13 +1,11 @@ import {UIElement} from "./UIElement"; -import {ImageCarousel} from "./Image/ImageCarousel"; import {VerticalCombine} from "./Base/VerticalCombine"; import {OsmLink} from "../Customizations/Questions/OsmLink"; import {WikipediaLink} from "../Customizations/Questions/WikipediaLink"; -import {And} from "../Logic/TagsFilter"; +import {And} from "../Logic/Tags"; import {TagDependantUIElement, TagDependantUIElementConstructor} from "../Customizations/UIElementConstructor"; import Translations from "./i18n/Translations"; import {Changes} from "../Logic/Osm/Changes"; -import {UserDetails} from "../Logic/Osm/OsmConnection"; import {FixedUiElement} from "./Base/FixedUiElement"; import {State} from "../State"; import {TagRenderingOptions} from "../Customizations/TagRenderingOptions"; @@ -23,13 +21,11 @@ export class FeatureInfoBox extends UIElement { /** * The tags, wrapped in a global event source */ - private _tagsES: UIEventSource<any>; - private _changes: Changes; - - private _title: UIElement; - private _osmLink: UIElement; - private _wikipedialink: UIElement; - + private readonly _tagsES: UIEventSource<any>; + private readonly _changes: Changes; + private readonly _title: UIElement; + private readonly _osmLink: UIElement; + private readonly _wikipedialink: UIElement; private _infoboxes: TagDependantUIElement[]; private _oneSkipped = Translations.t.general.oneSkippedQuestion.Clone(); diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index e4cfcfb..bda22b2 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -11,6 +11,7 @@ import { TagDependantUIElementConstructor } from "../../Customizations/UIElementConstructor"; import {State} from "../../State"; +import Translation from "../i18n/Translation"; export class ImageCarouselConstructor implements TagDependantUIElementConstructor{ IsKnown(properties: any): boolean { @@ -29,8 +30,8 @@ export class ImageCarouselConstructor implements TagDependantUIElementConstructo return new ImageCarousel(dependencies.tags); } - GetContent(tags: any): string { - return undefined; + GetContent(tags: any): Translation { + return new Translation({"en":"Images without upload"}); } } diff --git a/UI/Image/ImageCarouselWithUpload.ts b/UI/Image/ImageCarouselWithUpload.ts index 8590368..c82dc09 100644 --- a/UI/Image/ImageCarouselWithUpload.ts +++ b/UI/Image/ImageCarouselWithUpload.ts @@ -7,6 +7,7 @@ import {ImageCarousel} from "./ImageCarousel"; import {ImageUploadFlow} from "../ImageUploadFlow"; import {OsmImageUploadHandler} from "../../Logic/Osm/OsmImageUploadHandler"; import {State} from "../../State"; +import Translation from "../i18n/Translation"; export class ImageCarouselWithUploadConstructor implements TagDependantUIElementConstructor{ IsKnown(properties: any): boolean { @@ -25,8 +26,8 @@ export class ImageCarouselWithUploadConstructor implements TagDependantUIElement return new ImageCarouselWithUpload(dependencies); } - GetContent(tags: any): string { - return undefined; + GetContent(tags: any): Translation { + return new Translation({"en":"Image carousel with uploader"}); } } @@ -38,7 +39,6 @@ class ImageCarouselWithUpload extends TagDependantUIElement { super(dependencies.tags); const tags = dependencies.tags; this._imageElement = new ImageCarousel(tags); - const userDetails = State.state.osmConnection.userDetails; const license = State.state.osmConnection.GetPreference( "pictures-license"); this._pictureUploader = new OsmImageUploadHandler(tags, license, this._imageElement.slideshow).getUI(); diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index b624225..4b6572f 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -4,16 +4,13 @@ import Translations from "../i18n/Translations"; import {UIEventSource} from "../../Logic/UIEventSource"; import * as EmailValidator from "email-validator"; import {parsePhoneNumberFromString} from "libphonenumber-js"; -import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions"; -import {CustomLayoutFromJSON} from "../../Customizations/JSON/CustomLayoutFromJSON"; -import {And, Tag} from "../../Logic/TagsFilter"; export class ValidatedTextField { public static inputValidation = { - "$": (str) => true, - "string": (str) => true, - "date": (str) => true, // TODO validate and add a date picker - "wikidata": (str) => true, // TODO validate wikidata IDS + "$": () => true, + "string": () => true, + "date": () => true, // TODO validate and add a date picker + "wikidata": () => true, // TODO validate wikidata IDS "int": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))}, "nat": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0}, "float": (str) => !isNaN(Number(str)), @@ -31,75 +28,18 @@ export class ValidatedTextField { return parsePhoneNumberFromString(str, country.toUpperCase()).formatInternational() } } - - public static TagTextField(value: UIEventSource<Tag[]> = undefined, allowEmpty: boolean) { - allowEmpty = allowEmpty ?? false; - return new TextField<Tag[]>({ - placeholder: "Tags", - fromString: str => { - const tags = CustomLayoutFromJSON.TagsFromJson(str); - console.log("Parsed",str," --> ",tags) - if (tags === []) { - if (allowEmpty) { - return [] - } else { - return undefined; - } - } - return tags; - } - , - toString: (tags: Tag[]) => { - if (tags === undefined || tags === []) { - if (allowEmpty) { - return ""; - } else { - return undefined; - } - } - return new And(tags).asHumanString(false, false); - }, - value: value, - startValidated: true - } - ) - } - - public static - - ValidatedTextField(type: string, options: { value?: UIEventSource<string>, country?: string }) - : TextField<string> { - let isValid = ValidatedTextField.inputValidation[type]; - if (isValid === undefined - ) { - throw "Invalid type for textfield: " + type - } - let formatter = ValidatedTextField.formatting[type] ?? ((str) => str); - return new TextField<string>({ - placeholder: type, - toString: str => str, - fromString: str => isValid(str, options?.country) ? formatter(str, options.country) : undefined, - value: options.value, - startValidated: true - }) - - } - } export class TextField<T> extends InputElement<T> { - private value: UIEventSource<string>; - private mappedValue: UIEventSource<T>; - /** - * Pings and has the value data - */ - public enterPressed = new UIEventSource<string>(undefined); - private _placeholder: UIElement; - private _fromString?: (string: string) => T; - private _toString: (t: T) => string; - private startValidated: boolean; + private readonly value: UIEventSource<string>; + private readonly mappedValue: UIEventSource<T>; + public readonly enterPressed = new UIEventSource<string>(undefined); + private readonly _placeholder: UIElement; + private readonly _fromString?: (string: string) => T; + private readonly _toString: (t: T) => string; + private readonly startValidated: boolean; constructor(options: { @@ -157,14 +97,6 @@ export class TextField<T> extends InputElement<T> { GetValue(): UIEventSource<T> { return this.mappedValue; } - - ShowValue(t: T): boolean { - if (!this.IsValid(t)) { - return false; - } - this.mappedValue.setData(t); - } - InnerRender(): string { return `<form onSubmit='return false' class='form-text-field'>` + `<input type='text' placeholder='${this._placeholder.InnerRender()}' id='text-${this.id}'>` + @@ -178,7 +110,7 @@ export class TextField<T> extends InputElement<T> { } this.mappedValue.addCallback((data) => { - field.className = this.mappedValue.data !== undefined ? "valid" : "invalid"; + field.className = data !== undefined ? "valid" : "invalid"; }); field.className = this.mappedValue.data !== undefined ? "valid" : "invalid"; diff --git a/UI/MoreScreen.ts b/UI/MoreScreen.ts index ae6a69d..96a51e5 100644 --- a/UI/MoreScreen.ts +++ b/UI/MoreScreen.ts @@ -7,10 +7,7 @@ import {SubtleButton} from "./Base/SubtleButton"; import {State} from "../State"; import {VariableUiElement} from "./Base/VariableUIElement"; import {PersonalLayout} from "../Logic/PersonalLayout"; -import {FixedUiElement} from "./Base/FixedUiElement"; import {Layout} from "../Customizations/Layout"; -import {CustomLayoutFromJSON} from "../Customizations/JSON/CustomLayoutFromJSON"; -import {All} from "../Customizations/Layouts/All"; export class MoreScreen extends UIElement { @@ -22,26 +19,29 @@ export class MoreScreen extends UIElement { } - private createLinkButton(layout: Layout, customThemeDefinition: string = undefined) { + private static createLinkButton(layout: Layout, customThemeDefinition: string = undefined) { + if (layout === undefined) { + return undefined; + } if (layout.hideFromOverview) { - if (State.state.osmConnection.GetPreference("hidden-theme-" + layout.name + "-enabled").data !== "true") { + if (State.state.osmConnection.GetPreference("hidden-theme-" + layout.id + "-enabled").data !== "true") { return undefined; } } - if (layout.name === State.state.layoutToUse.data.name) { + if (layout.id === State.state.layoutToUse.data.id) { return undefined; } const currentLocation = State.state.locationControl.data; let linkText = - `./${layout.name.toLowerCase()}.html?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` + `./${layout.id.toLowerCase()}.html?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { - linkText = `./index.html?layout=${layout.name}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` + linkText = `./index.html?layout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` } if (customThemeDefinition) { - linkText = `./index.html?userlayout=${layout.name}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}` + linkText = `./index.html?userlayout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}` } @@ -86,20 +86,21 @@ export class MoreScreen extends UIElement { continue; } } - if(layout.name !== k){ + if (layout.id !== k) { continue; // This layout was added multiple time due to an uppercase } - els.push(this.createLinkButton(layout)); + els.push(MoreScreen.createLinkButton(layout)); } const customThemesNames = State.state.installedThemes.data ?? []; - if (customThemesNames !== []) { + if (customThemesNames.length > 0) { + console.log(customThemesNames) els.push(Translations.t.general.customThemeIntro) - } - for (const installed of State.state.installedThemes.data) { - els.push(this.createLinkButton(installed.layout, installed.definition)); + for (const installed of State.state.installedThemes.data) { + els.push(MoreScreen.createLinkButton(installed.layout, installed.definition)); + } } diff --git a/UI/ShareScreen.ts b/UI/ShareScreen.ts index 2aa7f9e..5caa4b7 100644 --- a/UI/ShareScreen.ts +++ b/UI/ShareScreen.ts @@ -11,19 +11,15 @@ import {Basemap} from "../Logic/Leaflet/Basemap"; import {FilteredLayer} from "../Logic/FilteredLayer"; import {Utils} from "../Utils"; import {UIEventSource} from "../Logic/UIEventSource"; -import {UserDetails} from "../Logic/Osm/OsmConnection"; import Translation from "./i18n/Translation"; import {SubtleButton} from "./Base/SubtleButton"; export class ShareScreen extends UIElement { - - private _shareButton: UIElement; - - private _options: UIElement; - private _iframeCode: UIElement; - private _link: UIElement; - private _linkStatus: UIEventSource<string | UIElement>; - private _editLayout: UIElement; + private readonly _options: UIElement; + private readonly _iframeCode: UIElement; + private readonly _link: UIElement; + private readonly _linkStatus: UIEventSource<string | UIElement>; + private readonly _editLayout: UIElement; constructor() { super(undefined) @@ -33,8 +29,8 @@ export class ShareScreen extends UIElement { const optionParts: (UIEventSource<string>)[] = []; const includeLocation = new CheckBox( - new Combine([Img.checkmark, "Include current location"]), - new Combine([Img.no_checkmark, "Include current location"]), + new Combine([Img.checkmark, tr.fsIncludeCurrentLocation]), + new Combine([Img.no_checkmark, tr.fsIncludeCurrentLocation]), true ) optionCheckboxes.push(includeLocation); @@ -52,11 +48,7 @@ export class ShareScreen extends UIElement { const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = (State.state.bm as Basemap).CurrentLayer; - const currentBackground = new VariableUiElement( - currentLayer.map( - (layer) => `Include the current background choice <b>${layer.name}</b>` - ) - ); + const currentBackground = tr.fsIncludeCurrentBackgroundMap.Subs({name: layout.id}); const includeCurrentBackground = new CheckBox( new Combine([Img.checkmark, currentBackground]), new Combine([Img.no_checkmark, currentBackground]), @@ -73,8 +65,8 @@ export class ShareScreen extends UIElement { const includeLayerChoices = new CheckBox( - new Combine([Img.checkmark, "Include the current layer choices"]), - new Combine([Img.no_checkmark, "Include the current layer choices"]), + new Combine([Img.checkmark, tr.fsIncludeCurrentLayers]), + new Combine([Img.no_checkmark, tr.fsIncludeCurrentLayers]), true ) optionCheckboxes.push(includeLayerChoices); @@ -110,8 +102,7 @@ export class ShareScreen extends UIElement { const checkbox = new CheckBox( new Combine([Img.checkmark, Translations.W(swtch.human)]), - new Combine([Img.no_checkmark, Translations.W(swtch.human)]), - swtch.reverse ? false : true + new Combine([Img.no_checkmark, Translations.W(swtch.human)]), !swtch.reverse ); optionCheckboxes.push(checkbox); optionParts.push(checkbox.isEnabled.map((isEn) => { @@ -135,8 +126,8 @@ export class ShareScreen extends UIElement { this._options = new VerticalCombine(optionCheckboxes) const url = currentLocation.map(() => { - - let literalText = "https://pietervdvn.github.io/MapComplete/" + layout.name + ".html" + + let literalText = "https://pietervdvn.github.io/MapComplete/" + layout.id.toLowerCase() + ".html" const parts = Utils.NoEmpty(Utils.NoNull(optionParts.map((eventSource) => eventSource.data))); @@ -190,18 +181,18 @@ export class ShareScreen extends UIElement { const self = this; this._link = new VariableUiElement( url.map((url) => { - return `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%"readonly>` + return `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">` }) ).onClick(async () => { const shareData = { - title: Translations.W(layout.name)?.InnerRender() ?? "", + title: Translations.W(layout.id)?.InnerRender() ?? "", text: Translations.W(layout.description)?.InnerRender() ?? "", url: self._link.data, } function rejected() { - var copyText = document.getElementById("code-link--copyable"); + const copyText = document.getElementById("code-link--copyable"); // @ts-ignore copyText.select(); diff --git a/UI/SimpleAddUI.ts b/UI/SimpleAddUI.ts index b808aec..3d7cad1 100644 --- a/UI/SimpleAddUI.ts +++ b/UI/SimpleAddUI.ts @@ -1,5 +1,5 @@ import {UIElement} from "./UIElement"; -import {Tag, TagUtils} from "../Logic/TagsFilter"; +import {Tag, TagUtils} from "../Logic/Tags"; import {FilteredLayer} from "../Logic/FilteredLayer"; import Translations from "./i18n/Translations"; import Combine from "./Base/Combine"; @@ -8,8 +8,6 @@ import Locale from "./i18n/Locale"; import {State} from "../State"; import {UIEventSource} from "../Logic/UIEventSource"; -import {UserDetails} from "../Logic/Osm/OsmConnection"; -import {FixedUiElement} from "./Base/FixedUiElement"; import {Utils} from "../Utils"; /** @@ -44,8 +42,7 @@ export class SimpleAddUI extends UIElement { this._loginButton = Translations.t.general.add.pleaseLogin.Clone().onClick(() => State.state.osmConnection.AttemptLogin()); this._addButtons = []; - this.clss = "add-ui" - + this.SetClass("add-ui"); const self = this; for (const layer of State.state.filteredLayers.data) { @@ -67,7 +64,7 @@ export class SimpleAddUI extends UIElement { const csCount = State.state.osmConnection.userDetails.data.csCount; let tagInfo = ""; if (csCount > State.userJourney.tagsVisibleAt) { - tagInfo = preset.tags.map(t => t.asHumanString(false)).join("&"); + tagInfo = preset.tags.map(t => t.asHumanString(false, true)).join("&"); tagInfo = `<br/><span class='subtle'>${tagInfo}</span>` } const button: UIElement = @@ -115,7 +112,6 @@ export class SimpleAddUI extends UIElement { } private CreatePoint(tags: Tag[], layerToAddTo: FilteredLayer) { - const self = this; return () => { const loc = State.state.bm.LastClickLocation.data; @@ -139,7 +135,7 @@ export class SimpleAddUI extends UIElement { let tagInfo = ""; const csCount = State.state.osmConnection.userDetails.data.csCount; if (csCount > State.userJourney.tagsVisibleAt) { - tagInfo = this._confirmPreset.data .tags.map(t => t.asHumanString(csCount > State.userJourney.tagsVisibleAndWikiLinked)).join("&"); + tagInfo = this._confirmPreset.data .tags.map(t => t.asHumanString(csCount > State.userJourney.tagsVisibleAndWikiLinked, true)).join("&"); tagInfo = `<br/>More information about the preset: ${tagInfo}` } @@ -197,14 +193,7 @@ export class SimpleAddUI extends UIElement { return new Combine([header, Translations.t.general.add.stillLoading]).Render() } - - var html = ""; - for (const button of this._addButtons) { - html += button.Render(); - } - - - return header.Render() + new Combine([html], "add-popup-all-buttons").Render(); + return header.Render() + new Combine(this._addButtons, "add-popup-all-buttons").Render(); } diff --git a/UI/WelcomeMessage.ts b/UI/WelcomeMessage.ts index 93a132a..6f220c5 100644 --- a/UI/WelcomeMessage.ts +++ b/UI/WelcomeMessage.ts @@ -1,19 +1,14 @@ import {UIElement} from "../UI/UIElement"; -import {OsmConnection, UserDetails} from "../Logic/Osm/OsmConnection"; import Locale from "../UI/i18n/Locale"; import {State} from "../State"; import {Layout} from "../Customizations/Layout"; import Translations from "./i18n/Translations"; -import {VariableUiElement} from "./Base/VariableUIElement"; import {Utils} from "../Utils"; -import {UIEventSource} from "../Logic/UIEventSource"; import Combine from "./Base/Combine"; export class WelcomeMessage extends UIElement { - private readonly layout: Layout; private languagePicker: UIElement; - private osmConnection: OsmConnection; private readonly description: UIElement; private readonly plzLogIn: UIElement; diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index 505f0ad..15e2b65 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -1,7 +1,5 @@ import {UIElement} from "../UIElement" import Locale from "./Locale" -import {FixedUiElement} from "../Base/FixedUiElement"; -import {TagUtils} from "../../Logic/TagsFilter"; import Combine from "../Base/Combine"; @@ -17,6 +15,9 @@ export default class Translation extends UIElement { const combined = []; const parts = template.split("{" + k + "}"); const el: string | UIElement = text[k]; + if(el === undefined){ + continue; + } let rtext: string = ""; if (typeof (el) === "string") { rtext = el; @@ -51,6 +52,7 @@ export default class Translation extends UIElement { for (const i in this.translations) { return this.translations[i]; // Return a random language } + console.log("Missing language ",Locale.language.data,"for",this.translations) return "Missing translation" } @@ -62,6 +64,10 @@ export default class Translation extends UIElement { constructor(translations: object) { super(Locale.language) + let count = 0; + for (const translationsKey in translations) { + count++; + } this.translations = translations } diff --git a/UI/i18n/Translations.ts b/UI/i18n/Translations.ts index 3be2edf..3ea5f34 100644 --- a/UI/i18n/Translations.ts +++ b/UI/i18n/Translations.ts @@ -128,19 +128,16 @@ export default class Translations { question: new T({ en: 'Is this parking covered? Also select "covered" for indoor parkings.', nl: 'Is deze parking overdekt? Selecteer ook "overdekt" voor fietsparkings binnen een gebouw.', - fr: 'TODO: fr', gl: 'Este aparcadoiro está cuberto? Tamén escolle "cuberto" para aparcadoiros interiores.' }), yes: new T({ en: 'This parking is covered (it has a roof)', nl: 'Deze parking is overdekt (er is een afdak)', - fr: 'TODO: fr', gl: 'Este aparcadoiro está cuberto (ten un teito)' }), no: new T({ en: 'This parking is not covered', nl: 'Deze parking is niet overdekt', - fr: 'TODO: fr', gl: 'Este aparcadoiro non está cuberto' }) }, @@ -148,19 +145,16 @@ export default class Translations { question: new T({ en: "How many bicycles fit in this bicycle parking (including possible cargo bicycles)?", nl: "Voor hoeveel fietsen is er bij deze fietsparking plaats (inclusief potentiëel bakfietsen)?", - fr: "TODO: fr", gl: "Cantas bicicletas caben neste aparcadoiro de bicicletas (incluídas as posíbeis bicicletas de carga)?" }), template: new T({ en: "This parking fits $nat$ bikes", nl: "Deze parking heeft plaats voor $nat$ fietsen", - fr: "TODO: fr", gl: "Neste aparcadoiro caben $nat$ bicicletas" }), render: new T({ en: "Place for {capacity} bikes (in total)", nl: "Plaats voor {capacity} fietsen (in totaal)", - fr: "TODO: fr", gl: "Lugar para {capacity} bicicletas (en total)" }), }, @@ -1308,6 +1302,18 @@ export default class Translations { en: "Enable the 'geolocate-me' button (mobile only)", gl: "Activar o botón de 'xeolocalizarme' (só móbil)", nl: "Toon het knopje voor geolocalisatie (enkel op mobiel)" + }), + fsIncludeCurrentBackgroundMap: new T({ + en: "Include the current background choice <b>{name}</b>", + nl: "Gebruik de huidige achtergrond <b>{name}</b>" + }), + fsIncludeCurrentLayers: new T({ + en: "Include the current layer choices", + nl: "Toon enkel de huidig getoonde lagen" + }), + fsIncludeCurrentLocation: new T({ + en: "Include current location", + nl: "Start op de huidige locatie" }) }, morescreen: { diff --git a/assets/themes/aed/aed.json b/assets/themes/aed/aed.json index 142e484..b170cb2 100644 --- a/assets/themes/aed/aed.json +++ b/assets/themes/aed/aed.json @@ -1,134 +1,171 @@ { + "id": "aed", + "title": { + "en": "Open AED Map", + "fr": "Carte AED", + "nl": "Open AED-kaart" + }, + "maintainer": "MapComplete", + "icon": "./assets/themes/aed/aed.svg", + "description": { + "en": "On this map, one can find and mark nearby defibrillators", + "fr": "Sur cette carte, vous pouvez trouver et améliorer les informations sur les défibrillateurs", + "nl": "Op deze kaart kan je informatie over AEDs vinden en verbeteren" + }, "language": [ "en", - "fr" + "fr", + "nl" ], - "startLat": "0", - "startLon": "0", - "startZoom": "12", - "maintainer": "Pieter Vander Vennet", + "version": "2020-08-29", + "startLat": 0, + "startLon": 0, + "startZoom": 12, "layers": [ { - "name": "Defibrillator", + "id": "Defibrillator", + "name": { + "en": "Defibrillators", + "fr": "Défibrillateurs", + "nl": "Defibrillatoren" + }, + "overpassTags": "emergency=defibrillator", + "minzoom": 12, "title": { - "key": "*", "render": { "en": "Defibrillator", - "fr": "Défibrillateur" + "fr": "Défibrillateur", + "nl": "Defibrillator" } }, - "icon": { - "key": "*", - "render": "./assets/themes/aed/aed.svg" - }, - "color": { - "render": "#0000ff", - "key": "*" - }, - "description": { - "en": "A defibrillator", - "fr": "Un défibrillateur" - }, - "minzoom": "12", + "icon": "./assets/themes/aed/aed.svg", + "color": "#0000ff", "presets": [ { "title": { "en": "Defibrillator", - "fr": "Défibrillateur" + "fr": "Défibrillateur", + "nl": "Defibrillator" }, - "tags": "emergency=defibrillator", - "description": "A defibrillator" + "tags": [ + "emergency=defibrillator" + ] } ], "tagRenderings": [ + "pictures", { "mappings": [ { + "question": { + "en": "Is this defibrillator located indoors?", + "fr": "Ce défibrillateur est-il disposé en intérieur ?", + "nl": "Hangt deze defibrillator binnen of buiten?" + }, + "if": "indoor=yes", "then": { "en": "This defibrillator is located indoors", - "fr": "Ce défibrillateur est en intérieur (dans un batiment)" - }, - "if": "indoor=yes" + "fr": "Ce défibrillateur est en intérieur (dans un batiment)", + "nl": "Deze defibrillator bevindt zich in een gebouw" + } }, { + "if": "indoor=no", "then": { "en": "This defibrillator is located outdoors", - "fr": "Ce défibrillateur est situé en extérieur" - }, - "if": "indoor=no" + "fr": "Ce défibrillateur est situé en extérieur", + "nl": "Deze defibrillator hangt buiten" + } } - ], - "question": { - "en": "Is this defibrillator located indoors?", - "fr": "Ce défibrillateur est-il disposé en intérieur ?" - }, - "type": "text" + ] }, { + "question": { + "en": "Is this defibrillator freely accessible?", + "fr": "Ce défibrillateur est-il librement accessible?", + "nl": "Is deze defibrillator vrij toegankelijk?" + }, + "render": { + "en": "Access is {access}", + "nl": "Toegankelijkheid is {access}", + "fr": "{access} accessible" + }, + "condition": "indoor=yes", + "freeform": { + "key": "access", + "addExtraTags": "fixme=Freeform field used for access - doublecheck the value" + }, "mappings": [ { + "if": "access=yes", "then": { "en": "Publicly accessible", - "fr": "Librement accessible" - }, - "if": "access=yes" + "fr": "Librement accessible", + "nl": "Publiek toegankelijk" + } }, { + "if": "access=public", + "then": { + "en": "Publicly accessible", + "fr": "Librement accessible", + "nl": "Publiek toegankelijk" + }, + "hideInAnswer": true + }, + { + "if": "access=customers", "then": { "en": "Only accessible to customers", - "fr": "Réservé aux clients du lieu" - }, - "if": "access=customers" + "fr": "Réservé aux clients du lieu", + "nl": "Enkel toegankeleijk voor klanten" + } }, { "if": "access=private", "then": { "en": "Not accessible to the general public (e.g. only accesible to staff, the owners, ...)", - "fr": "Non accessible au public (par exemple réservé au personnel, au propriétaire, ...)" + "fr": "Non accessible au public (par exemple réservé au personnel, au propriétaire, ...)", + "nl": "Niet toegankelijk voor het publiek (bv. enkel voor personneel, de eigenaar, ...)" } } - ], - "question": { - "en": "Is this defibrillator freely accessible?", - "fr": "Ce défibrillateur est-il librement accessible ?" - }, - "type": "text", - "key": "access", - "condition": "indoor=yes" + ] }, { - "key": "level", - "mappings": [], "question": { "en": "On which floor is this defibrillator located?", - "fr": "À quel étage est situé ce défibrillateur ?" + "fr": "À quel étage est situé ce défibrillateur?", + "nl": "Op welke verdieping bevindt deze defibrillator zich?" + }, + "condition": { + "and": [ + "indoor=yes", + "access!~private" + ] + }, + "freeform": { + "key": "level", + "type": "int" }, - "type": "int", "render": { "en": "This defibrallator is on floor {level}", - "fr": "Ce défibrillateur est à l'étage {level}" - }, - "condition": "indoor=yes&access!=private" + "fr": "Ce défibrillateur est à l'étage {level}", + "nl": "De defibrillator bevindt zicht op verdieping {level}" + } }, { - "key": "defibrillator:location", - "mappings": [], + "render": "{defibrillator:location}", "question": { "en": "Please give some explanation on where the defibrillator can be found", - "fr": "Veuillez indiquez plus précisément où se situe le défibrillateur" + "fr": "Veuillez indiquez plus précisément où se situe le défibrillateur", + "nl": "Gelieve meer informatie te geven over de exacte locatie van de defibrillator" }, - "type": "text", - "render": "{defibrillator:location}" + "freeform": { + "type": "text", + "key": "defibrillator:location" + } } - ], - "overpassTags": "emergency=defibrillator" + ] } - ], - "title": "Open AED Map", - "icon": "./assets/themes/aed/aed.svg", - "name": "aed", - "description": { - "en": "On this map, one can find and mark nearby defibrillators", - "fr": "Sur cette carte, vous pouvez trouver et améliorer les informations sur les défibrillateurs" - } + ] } \ No newline at end of file diff --git a/assets/themes/artwork/artwork.json b/assets/themes/artwork/artwork.json index 2baacc5..d6eb1a1 100644 --- a/assets/themes/artwork/artwork.json +++ b/assets/themes/artwork/artwork.json @@ -1,28 +1,34 @@ { - "startLat": 0, - "startLon": 0, - "startZoom": 12, - "maintainer": "Not logged in", - "language": [ - "en", - "nl" - ], - "widenFactor": 0.07, - "name": "artworks", + "id": "artworks", + "version": "2020-08-30", "title": { "en": "Open Artwork Map", "nl": "Kunstwerkenkaart" }, - "icon": "./assets/themes/artwork/artwork.svg", "description": { "en": "Welcome to Open Artwork Map, a map of statues, busts, grafittis, ... all over the world", "nl": "Welkom op de Open Kunstwerken Kaart" }, + "language": [ + "en", + "nl", + "fr" + ], + "icon": "./assets/themes/artwork/artwork.svg", + "maintainer": "MapComplete", + "startZoom": 12, + "startLat": 0, + "startLon": 0, "layers": [ { - "name": "Artwork", + "id": "artwork", + "name": { + "en": "Artworks", + "nl": "Kunstwerken", + "fr": "Oeuvres d'art" + }, + "overpassTags": "tourism=artwork", "title": { - "key": "*", "render": { "en": "Artwork", "nl": "Kunstwerk", @@ -30,165 +36,195 @@ } }, "icon": { - "key": "*", "render": "./assets/themes/artwork/artwork.svg" }, "color": { - "key": "*", "render": "#0000ff" }, "width": { - "key": "*", "render": "10" }, "description": { - "en": "", - "nl": "" + "en": "Diverse pieces of artwork", + "nl": "Verschillende soorten kunstwerken" }, "minzoom": 12, "wayHandling": 2, "presets": [ { - "tags": "tourism=artwork", + "tags": [ + "tourism=artwork" + ], "title": { - "en": "Artwork" + "en": "Artwork", + "nl": "Kunstwerk" } } ], "tagRenderings": [ + "pictures", { - "mappings": [ - { - "if": "artwork_type=architecture", - "then": { - "en": "architecture", - "nl": "architectuur", - "fr": "architecture" - } - }, - { - "if": "artwork_type=mural", - "then": { - "en": "mural", - "nl": "muurschildering", - "fr": "mural" - } - }, - { - "if": "artwork_type=painting", - "then": { - "en": "painting", - "nl": "schilderij", - "fr": "peinture" - } - }, - { - "if": "artwork_type=sculpture", - "then": { - "en": "sculpture", - "nl": "beeldhouwwerk", - "fr": "sculpture" - } - }, - { - "if": "artwork_type=statue", - "then": { - "en": "statue", - "nl": "standbeeld", - "fr": "statue" - } - }, - { - "if": "artwork_type=bust", - "then": { - "en": "bust", - "nl": "buste", - "fr": "buste" - } - }, - { - "if": "artwork_type=stone", - "then": { - "en": "stone", - "nl": "steen", - "fr": "rocher" - } - }, - { - "if": "artwork_type=installation", - "then": { - "en": "installation", - "nl": "installatie", - "fr": "installation" - } - }, - { - "if": "artwork_type=graffiti", - "then": { - "en": "graffiti", - "nl": "graffiti", - "fr": "graffiti" - } - }, - { - "if": "artwork_type=relief", - "then": { - "en": "relief", - "nl": "verlichting", - "fr": "relief" - } - }, - { - "if": "artwork_type=azulejo", - "then": { - "en": "azulejo", - "nl": "azulejo", - "fr": "azulejo" - } - }, - { - "if": "artwork_type=tilework", - "then": { - "en": "tilework", - "nl": "tegelwerk", - "fr": "carrelage" - } - } - ], - "key": "artwork_type", "render": { "en": "This is a {artwork_type}", "nl": "Dit is een {artwork_type}", "fr": "{artwork_type}" }, - "type": "text", "question": { "en": "What is the type of this artwork?", "nl": "Wat voor soort kunstwerk is dit?", "fr": "Quel est le type de cette oeuvre d'art?" }, - "addExtraTags": "fixme=Artowrk type was added with the freeform, might need another check" + "freeform": { + "key": "artwork_type", + "addExtraTags": "fixme=Artowrk type was added with the freeform, might need another check" + }, + "mappings": [ + { + "if": "artwork_type=architecture", + "then": { + "en": "Architecture", + "nl": "Architectuur", + "fr": "Architecture" + } + }, + { + "if": "artwork_type=mural", + "then": { + "en": "Mural", + "nl": "Muurschildering", + "fr": "Mural" + } + }, + { + "if": "artwork_type=painting", + "then": { + "en": "Painting", + "nl": "Schilderij", + "fr": "Peinture" + } + }, + { + "if": "artwork_type=sculpture", + "then": { + "en": "Sculpture", + "nl": "Beeldhouwwerk", + "fr": "Sculpture" + } + }, + { + "if": "artwork_type=statue", + "then": { + "en": "Statue", + "nl": "Standbeeld", + "fr": "Statue" + } + }, + { + "if": "artwork_type=bust", + "then": { + "en": "Bust", + "nl": "Buste", + "fr": "Buste" + } + }, + { + "if": "artwork_type=stone", + "then": { + "en": "Stone", + "nl": "Steen", + "fr": "Rocher" + } + }, + { + "if": "artwork_type=installation", + "then": { + "en": "Installation", + "nl": "Installatie", + "fr": "Installation" + } + }, + { + "if": "artwork_type=graffiti", + "then": { + "en": "Graffiti", + "nl": "Graffiti", + "fr": "Graffiti" + } + }, + { + "if": "artwork_type=relief", + "then": { + "en": "Relief", + "nl": "Reliëf", + "fr": "Relief" + } + }, + { + "if": "artwork_type=azulejo", + "then": { + "en": "Azulejo (Spanish decorative tilework)", + "nl": "Azulejo (Spaanse siertegels)", + "fr": "Azulejo" + } + }, + { + "if": "artwork_type=tilework", + "then": { + "en": "Tilework", + "nl": "Tegelwerk", + "fr": "Carrelage" + } + } + ] }, { - "question": "Which wikidata-entry corresponds with <b>this artwork</b>?", - "key": "wikidata", - "type": "wikidata", - "render": "Corresponds with <a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'>{wikidata}</a>" + "question": { + "en": "Which wikidata-entry corresponds with <b>this artwork</b>?", + "fr": "Quelle entrée wikidata correspond à <b>cette œuvre d'art</b> ?", + "nl": "Welk wikidata-item beschrijft dit kunstwerk?" + }, + "render": { + "en": "Corresponds with <a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'>{wikidata}</a>", + "nl": "Komt overeen met <a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'>{wikidata}</a>", + "fr": "Correspond à <a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'>{wikidata}</a>" + }, + "freeform": { + "key": "wikidata", + "type": "wikidata" + } }, { - "question": "Which artist created this?", - "key": "artist_name", - "render": "Created by {artist_name}", - "condition": "wikidata=" + "question": { + "en": "Which artist created this?", + "nl": "Welke artist creëerde dit kunstwerk?", + "fr": "Quel artiste a créé cela?" + }, + "render": { + "en": "Created by {artist_name}", + "nl": "Gecreëerd door {artist_name}", + "fr": "Créé par {artist_name}" + }, + "freeform": { + "key": "artist_name" + } }, { - "question": "On which website is more information about this artwork?", - "key": "website", - "type": "url", - "render": "More information on {website}" + "question": { + "en": "On which website is more information about this artwork?", + "nl": "Op welke website kan men meer informatie vinden over dit kunstwerk?", + "fr": "Sur quel site web pouvons-nous trouver plus d'informations sur cette œuvre d'art?" + }, + "render": { + "en": "More information on <a href='{website}' target='_blank'>this website</a>", + "nl": "Meer informatie op <a href='{website}' target='_blank'>deze website</a>", + "fr": "Plus d'info <a href='{website}' target='_blank'>sûr ce site web</a>" + }, + "freeform": { + "key": "website", + "type": "url" + } } - ], - "overpassTags": "tourism=artwork" + ] } ] } \ No newline at end of file diff --git a/assets/themes/bookcases/Bookcases.json b/assets/themes/bookcases/Bookcases.json index 688d143..1adbf85 100644 --- a/assets/themes/bookcases/Bookcases.json +++ b/assets/themes/bookcases/Bookcases.json @@ -1,99 +1,122 @@ { - "name": "bookcases", - "title": { - "en": "Open Bookcase Map", - "nl": "Open Boekenruilkastenkaart" - }, - "maintainer": "Pieter Vander Vennet", - "icon": "./assets/themes/bookcases/bookcase.svg", - "description": { - "en": "A public bookcase is a small streetside cabinet, box, old phone boot or some other objects where books are stored. Everyone can place or take a book. This map aims to collect all these bookcases. You can discover new bookcases nearby and, with a free OpenStreetMap account, quickly add your favourite bookcases.", - "nl": "Een boekenruilkast is een kastje waar iedereen een boek kan nemen of achterlaten. Op deze kaart kan je deze boekenruilkasten terugvinden en met een gratis OpenStreetMap-account, ook boekenruilkasten toevoegen of informatie verbeteren" - }, - "widenFactor": 0.05, - "startLat": 0, - "startLon": 0, - "startZoom": 10, + "id": "bookcases", + "maintainer": "MapComplete", + "version": "2020-08-29", "language": [ "en", "nl" ], + "title": { + "en": "Open Bookcase Map", + "nl": "Open Boekenruilkastenkaart" + }, + "description": { + "en": "A public bookcase is a small streetside cabinet, box, old phone boot or some other objects where books are stored. Everyone can place or take a book. This map aims to collect all these bookcases. You can discover new bookcases nearby and, with a free OpenStreetMap account, quickly add your favourite bookcases.", + "nl": "Een boekenruilkast is een kastje waar iedereen een boek kan nemen of achterlaten. Op deze kaart kan je deze boekenruilkasten terugvinden en met een gratis OpenStreetMap-account, ook boekenruilkasten toevoegen of informatie verbeteren" + }, + "icon": "./assets/themes/bookcases/bookcase.svg", + "socialImage": null, + "startLat": 0, + "startLon": 0, + "startZoom": 10, + "widenFactor": 0.05, + "roamingRenderings": [], "layers": [ { - "name": "Bookcases", + "id": "bookcases", + "name": { + "en": "Bookcases", + "nl": "Boekenruilkastjes" + }, + "description": { + "en": "A streetside cabinet with books, accessible to anyone", + "nl": "Een straatkastje met boeken voor iedereen" + }, + "overpassTags": "amenity=public_bookcase", + "minzoom": 12, "title": { - "key": "*", "render": { "en": "Bookcase", "nl": "Boekenruilkast" }, "mappings": [ { - "if": "name=*", - "then": "{name}" + "if": "name~*", + "then": { + "en": "Public bookcase <i>{name}</i>", + "nl": "Boekenruilkast <i>{name}</i>" + } } ] }, "icon": { - "key": "*", - "render": "./assets/themes/bookcases/bookcase.svg", - "mappings": [] + "render": "./assets/themes/bookcases/bookcase.svg" }, "color": { - "key": "*", "render": "#0000ff" }, - "description": { - "en": "A streetside cabinet with books, accessible to anyone", - "nl": "Een straatkastje met boeken voor iedereen" + "width": { + "render": "8" }, - "minzoom": "12", - "overpassTags": "amenity=public_bookcase", "presets": [ { "title": { "en": "Bookcase", "nl": "Boekenruilkast" }, - "tags": "amenity=public_bookcase" + "tags": [ + "amenity=public_bookcase" + ] } ], "tagRenderings": [ + "images", { - "key": "name", - "mappings": [ - { - "then": { - "en": "This bookcase doesn't have a name", - "nl": "Dit boekenruilkastje heeft geen naam" - }, - "if": "noname=yes&name=" - } - ], - "question": { - "en": "What is the name of this public bookcase?", - "nl": "Wat is de naam van dit boekenuilkastje?" - }, "render": { "en": "The name of this bookcase is {name}", "nl": "De naam van dit boekenruilkastje is {name}" }, - "type": "text" + "question": { + "en": "What is the name of this public bookcase?", + "nl": "Wat is de naam van dit boekenuilkastje?" + }, + "freeform": { + "key": "name" + }, + "mappings": [ + { + "if": { + "and": [ + "noname=yes", + "name=" + ] + }, + "then": { + "en": "This bookcase doesn't have a name", + "nl": "Dit boekenruilkastje heeft geen naam" + } + } + ] }, { - "key": "capacity", - "mappings": [], - "question": { - "en": "How many books fit into this public bookcase?", - "nl": "Hoeveel boeken passen er in dit boekenruilkastje?" - }, "render": { "en": "{capacity} books fit in this bookcase", "nl": "Er passen {capacity} boeken" }, - "type": "nat" + "question": { + "en": "How many books fit into this public bookcase?", + "nl": "Hoeveel boeken passen er in dit boekenruilkastje?" + }, + "freeform": { + "key": "capacity", + "type": "nat" + } }, { + "question": { + "en": "What kind of books can be found in this public bookcase?", + "nl": "Voor welke doelgroep zijn de meeste boeken in dit boekenruilkastje?" + }, "mappings": [ { "if": "books=children", @@ -116,15 +139,13 @@ "nl": "Boeken voor zowel kinderen als volwassenen" } } - ], - "question": { - "en": "What kind of books can be found in this public bookcase?", - "nl": "Voor welke doelgroep zijn de meeste boeken in dit boekenruilkastje?" - }, - "type": "text" + ] }, { - "addExtraTags": "", + "question": { + "en": "Is this bookcase located outdoors?", + "nl": "Staat dit boekenruilkastje binnen of buiten?" + }, "mappings": [ { "then": { @@ -145,16 +166,17 @@ "en": "This bookcase is located outdoors", "nl": "Dit boekenruilkastje staat buiten" }, - "if": "indoor=" + "if": "indoor=", + "hideInAnswers": true } - ], - "question": { - "en": "Is this bookcase located outdoors?", - "nl": "Staat dit boekenruilkastje binnen of buiten?" - }, - "type": "text" + ] }, { + "question": { + "en": "Is this public bookcase freely accessible?", + "nl": "Is dit boekenruilkastje publiek toegankelijk?" + }, + "condition": "indoor=yes", "mappings": [ { "then": { @@ -170,29 +192,35 @@ }, "if": "access=customers" } - ], - "question": { - "en": "Is this public bookcase freely accessible?", - "nl": "Is dit boekenruilkastje publiek toegankelijk?" - }, - "type": "text", - "condition": "indoor=yes" + ] }, { - "key": "operator", - "mappings": [], "question": { "en": "Who maintains this public bookcase?", "nl": "Wie is verantwoordelijk voor dit boekenruilkastje?" }, - "type": "text", "render": { "en": "Operated by {operator}", "nl": "Onderhouden door {operator}" + }, + "freeform": { + "type": "text", + "key": "operator" } }, { - "key": "brand", + "question": { + "en": "Is this public bookcase part of a bigger network?", + "nl": "Is dit boekenruilkastje deel van een netwerk?" + }, + "render": { + "en": "This public bookcase is part of {brand}", + "nl": "Dit boekenruilkastje is deel van het netwerk {brand}" + }, + "condition": "ref=", + "freeform": { + "key": "brand" + }, "mappings": [ { "then": { @@ -202,68 +230,72 @@ "if": "brand=Little Free Library" }, { + "if": { + "and": [ + "nobrand=yes", + "brand=" + ] + }, "then": { "en": "This public bookcase is not part of a bigger network", "nl": "Dit boekenruilkastje maakt geen deel uit van een netwerk" - }, - "if": "nobrand=yes&brand=" + } } - ], - "question": { - "en": "Is this public bookcase part of a bigger network?", - "nl": "Is dit boekenruilkastje deel van een netwerk?" - }, - "render": { - "en": "This public bookcase is part of {brand}", - "nl": "Dit boekenruilkastje is deel van het netwerk {brand}" - }, - "type": "text", - "condition": "ref=" + ] }, { - "key": "ref", + "render": { + "en": "The reference number of this public bookcase within {brand} is {ref}", + "nl": "Het referentienummer binnen {brand} is {ref}" + }, + "question": { + "en": "What is the reference number of this public bookcase?", + "nl": "Wat is het referentienummer van dit boekenruilkastje?" + }, + "condition": "brand=*", + "freeform": { + "key": "ref" + }, "mappings": [ { "then": { "en": "This bookcase is not part of a bigger network", "nl": "Dit boekenruilkastje maakt geen deel uit van een netwerk" }, - "if": "nobrand=yes&brand=&ref=" + "if": { + "and": [ + "nobrand=yes", + "brand=", + "ref=" + ] + } } - ], - "question": { - "en": "What is the reference number of this public bookcase?", - "nl": "Wat is het referentienummer van dit boekenruilkastje?" - }, - "type": "text", - "render": { - "en": "The reference number of this public bookcase within {brand} is {ref}", - "nl": "Het referentienummer binnen {brand} is {ref}" - }, - "condition": "brand=*" + ] }, { - "key": "start_date", - "mappings": [], "question": { "en": "When was this public bookcase installed?", "nl": "Op welke dag werd dit boekenruilkastje geinstalleerd?" }, - "type": "date", "render": { "en": "Installed on {start_date}", "nl": "Geplaatst op {start_date}" + }, + "freeform": { + "key": "start_date", + "type": "date" } }, { - "key": "website", - "mappings": [], - "type": "url", + "render": "Meer info op <a href='{website}' target='_blank'>de website</a>", "question": { "en": "Is there a website with more information about this public bookcase?", "nl": "Is er een website over dit boekenruilkastje?" }, - "render": "{website}" + "freeform": { + "key": "website", + "type": "url" + } } ] } diff --git a/assets/themes/buurtnatuur/buurtnatuur.be.json b/assets/themes/buurtnatuur/buurtnatuur.be.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/assets/themes/buurtnatuur/buurtnatuur.be.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/assets/themes/cyclestreets/cyclestreets.json b/assets/themes/cyclestreets/cyclestreets.json index 0fa369e..e235228 100644 --- a/assets/themes/cyclestreets/cyclestreets.json +++ b/assets/themes/cyclestreets/cyclestreets.json @@ -1,90 +1,93 @@ { - "layers": [ + "id": "fietsstraten", + "version": "2020-08-30", + "title": "Fietsstraten", + "description": "Een fietsstraat is een straat waar <b>automobilisten geen fietsers mogen inhalen</b> en waar een maximumsnelheid van <b>30km/u</b> geldt.<br/><br/>Op deze open kaart kan je alle gekende fietsstraten zien en kan je ontbrekende fietsstraten aanduiden. Om de kaart aan te passen, moet je je aanmelden met OpenStreetMap en helemaal inzoomen tot straatniveau.", + "icon": "./assets/themes/cyclestreets/F111.svg", + "language": "nl", + "startLat": 51.2095, + "startZoom": 14, + "startLon": 3.2228, + "maintainer": "MapComlete", + "widenfactor": 0.05, + "roamingRenderings": [ { - "id": "Fietsstraat", - "title": { - "render": "{name}", - "key": "*" - }, - "icon": { - "key": "*", - "render": "./assets/themes/cyclestreets/F111.svg" - }, - "color": { - "key": "*", - "render": "#0000ff" - }, - "description": "Een fietsstraat is een straat waar gemotoriseerd verkeer een fietser niet mag inhalen.", - "minzoom": 9, - "presets": [], - "tagRenderings": [], - "overpassTags": "cyclestreet=yes", - "width": { - "key": "*", - "addExtraTags": "", - "mappings": [], - "question": "", - "render": "10", - "type": "nat" - }, - "name": "Fietsstraat" + "question": "Is deze straat een fietsstraat?", + "mappings": [ + { + "then": "Deze straat is een fietsstraat", + "if": "cyclestreet=yes&proposed:cyclestreet!~*" + }, + { + "then": "Deze straat wordt binnenkort een fietsstraat", + "if": "proposed:cyclestreet=yes&cyclestreet!~*" + }, + { + "if": "cyclestreet!~*&proposed:cyclestreet!~*", + "then": "Deze straat is geen fietsstraat" + } + ] }, { - "id": "", + "question": "Wanneer wordt deze straat een fietsstraat?", + "render": "Deze straat wordt fietsstraat op {cyclestreet:start_date}", + "freeform": { + "type": "date", + "key": "cyclestreet:start_date" + } + } + ], + "layers": [ + { + "id": "fietsstraat", + "name": "Fietsstraten", + "minzoom": 9, + "overpassTags": "cyclestreet=yes", + "description": "Een fietsstraat is een straat waar gemotoriseerd verkeer een fietser niet mag inhalen.", + "title": "{name}", + "icon": "./assets/themes/cyclestreets/F111.svg", + "color": "#0000ff", + "width": "10" + }, + { + "id": "toekomstige_fietsstraat", + "name": "Toekomstige fietsstraat", + "description": "Deze straat wordt binnenkort een fietsstraat", + "minzoom": 9, + "wayHandling": 0, + "overpassTags": "proposed:cyclestreet=yes", "title": { - "key": "*", "render": "Toekomstige fietsstraat", "mappings": [ { "then": "{name} wordt fietsstraat", - "if": "name=*" + "if": "name~*" } ] }, - "icon": { - "key": "*", - "render": "https://upload.wikimedia.org/wikipedia/commons/6/65/Belgian_road_sign_F113.svg" - }, - "color": { - "key": "*", - "render": "#09f9dd" - }, - "width": { - "key": "*", - "render": "5" - }, - "description": "Deze straat wordt binnenkort een fietsstraat", - "minzoom": "9", - "wayHandling": 0, - "presets": [], - "tagRenderings": [{ - "key": "cyclestreet:start_date", - "render": "Deze straat wordt fietsstraat op {cyclestreet:start_date}", - "type": "date", - "question": "Wanneer wordt deze straat een fietsstraat?" - }], - "name": "Toekomstige fietsstraat", - "overpassTags": "proposed:cyclestreet=yes" + "icon": "./assets/themes/cyclestreets/F113.svg", + "color": "#09f9dd", + "width": "5" }, - { + "id": "all_streets", "name": "Alle straten", + "description": "Laag waar je een straat als fietsstraat kan markeren", + "overpassTags": "highway~residential|tertiary|unclassified", + "minzoom": "18", + "wayHandling": 0, "title": { - "key": "*", "render": "Straat", "mappings": [ { "then": "{name}", - "if": "name=*" + "if": "name~*" } ] }, - "icon": { - "key": "*", - "render": "./assets/pencil.svg" - }, + "icon": "./assets/pencil.svg", + "width": "5", "color": { - "key": "*", "render": "#aaaaaa", "mappings": [ { @@ -96,53 +99,7 @@ "if": "proposed:cyclestreet=yes" } ] - }, - "width": { - "key": "*", - "render": "5" - }, - "description": "Laag waar je een straat als fietsstraat kan markeren", - "wayHandling": 0, - "presets": [], - "tagRenderings": [ - { - "mappings": [ - { - "then": "Deze straat is een fietsstraat", - "if": "cyclestreet=yes&proposed:cyclestreet=" - }, - { - "then": "Deze straat wordt binnenkort een fietsstraat", - "if": "proposed:cyclestreet=yes&cyclestreet=" - }, - { - "if": "cyclestreet=&proposed:cyclestreet=", - "then": "Deze straat is geen fietsstraat" - } - ], - "type": "text", - "question": "Is deze straat een fietsstraat?" - }, - { - "key": "cyclestreet:start_date", - "render": "Deze straat wordt fietsstraat op {cyclestreet:start_date}", - "type": "date", - "question": "Wanneer wordt deze straat een fietsstraat?", - "condition": "proposed:cyclestreet=yes" - } - ], - "overpassTags": "highway~=residential|tertiary|unclassified", - "minzoom": "18" + } } - ], - "language": "nl", - "startLat": "51.2095", - "startZoom": "14", - "maintainer": "Not logged in", - "name": "Fietsstraten", - "title": "Fietsstraten", - "startLon": "3.2228", - "icon": "./assets/themes/cyclestreets/F111.svg", - "description": "Een fietsstraat is een straat waar <b>automobilisten geen fietsers mogen inhalen</b> en waar een maximumsnelheid van <b>30km/u</b> geldt.<br/><br/>Op deze open kaart kan je alle gekende fietsstraten zien en kan je ontbrekende fietsstraten aanduiden. Om de kaart aan te passen, moet je je aanmelden met OpenStreetMap en helemaal inzoomen tot straatniveau.", - "widenfactor": 0.05 + ] } \ No newline at end of file diff --git a/assets/themes/spanishResidential.json b/assets/themes/spanishResidential.json deleted file mode 100644 index 69c7261..0000000 --- a/assets/themes/spanishResidential.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "layers": [ - { - "id": "Superficie", - "title": { - "render": "Calle sin nombre", - "key": "*", - "type": "text", - "question": "¿Cómo se llama esta calle?", - "mappings": [ - { - "if": "name=*", - "then": "Nombre de la calle: {name}" - } - ] - }, - "description": "Completar datos de superficie", - "minzoom": "16", - "overpassTags": "highway=/residential|tertiary|pedestrian|unclassified|secondary|primary/", - "presets": [], - "tagRenderings": [ - { - "key": "surface", - "addExtraTags": "", - "mappings": [ - { - "if": "surface=asphalt", - "then": "asfalto" - }, - { - "then": "cemento", - "if": "surface=concrete" - }, - { - "then": "pavimentado", - "if": "surface=paved" - }, - { - "then": "sin pavimentar", - "if": "surface=unpaved" - } - ], - "question": "Qué superficie tiene?", - "render": "Surface: {surface}", - "type": "text" - } - ], - "wayHandling": "0", - "icon": { - "key": "*", - "addExtraTags": "", - "mappings": [ - { - "then": "https://raw.githubusercontent.com/yopaseopor/beta_preset_josm/master/ES/traffic_signs/ES/ES_P26.png", - "if": "surface=asphalt" - } - ], - "question": "", - "render": "https://github.com/yopaseopor/beta_preset_josm/raw/master/ES/traffic_signs/ES/ES_P25.png", - "type": "text" - }, - "color": { - "key": "*", - "addExtraTags": "", - "mappings": [ - { - "then": "#000", - "if": "surface=asphalt" - }, - { - "then": "#ccc", - "if": "surface=concrete" - }, - { - "then": "#f3f", - "if": "surface=paving_stones" - }, - { - "then": "#b5721b", - "if": "surface=sett" - } - ], - "question": "", - "render": "#00f", - "type": "text" - }, - "width": { - "key": "*", - "addExtraTags": "", - "mappings": [ - { - "then": "12", - "if": "highway=tertiary" - }, - { - "then": "15", - "if": "highway=secondary" - }, - { - "then": "18", - "if": "highway=primary" - } - ], - "question": "", - "render": "6", - "type": "nat" - } - } - ], - "startLat": "41.39767", - "startLon": "2.17614", - "startZoom": "16", - "maintainer": "Pieter Vander Vennet", - "language": "es", - "icon": "https://github.com/yopaseopor/beta_preset_josm/raw/master/ES/traffic_signs/ES/ES_P28.png", - "name": "Superficie", - "title": "Test completar superficie", - "description": "Completar datos de superficie", - "widenFactor": "0.01" -} \ No newline at end of file diff --git a/assets/themes/toilets/toilets.json b/assets/themes/toilets/toilets.json index 3a3cd37..bea6bed 100644 --- a/assets/themes/toilets/toilets.json +++ b/assets/themes/toilets/toilets.json @@ -1,67 +1,84 @@ { + "id": "toilets", + "title": "Open Toilet Map", + "description": "A map of public toilets", + "maintainer": "MapComplete", + "version": "2020-08-29", + "language": [ + "en" + ], + "startZoom": 12, + "startLat": 51.2095, + "startLon": 3.2222, + "widenFactor": 0.05, + "icon": "./assets/themes/toilets/toilets.svg", "layers": [ { - "name": "Toilet", - "title": { - "key": "*", - "render": "Toilet" - }, + "id": "Toilet", + "name": "Toilets", + "overpassTags": "amenity=toilets", + "title": "Toilet", "icon": { - "key": "*", "render": "./assets/themes/toilets/toilets.svg", "mappings": [ { - "then": "./assets/themes/toilets/wheelchair.svg", - "if": "wheelchair=yes" + "if": "wheelchair=yes", + "then": "./assets/themes/toilets/wheelchair.svg" } ] }, "color": { - "key": "*", "render": "#0000ff" }, - "description": "A toilet", - "minzoom": "14", + "minzoom": 12, "wayHandling": 2, "presets": [ { "title": "Toilet", - "tags": "amenity=toilets", - "description": "Only add public toilets" + "tags": [ + "amenity=toilets" + ], + "description": "A publicly accessible toilet or restroom" }, { "title": "Toilets with wheelchair accessible toilet", - "tags": "amenity=toilets&wheelchair=yes", + "tags": [ + "amenity=toilets", + "wheelchair=yes" + ], "description": "A restroom which has at least one wheelchair-accessible toilet" } ], "tagRenderings": [ + "pictures", { - "key": "access", + "question": "Are these toilets publicly accessible?", + "render": "Access is {access}", + "freeform": { + "key": "access", + "addExtraTags": "fixme=the tag access was filled out by the user and might need refinement" + }, "mappings": [ { - "then": "Public access", - "if": "access=yes" + "if": "access=yes", + "then": "Public access" }, { - "then": "Only access to customers", - "if": "access=customers" + "if": "access=customers", + "then": "Only access to customers" }, { "if": "access=no", "then": "Not accessible" }, { - "then": "Accessible, but one has to ask a key to enter", - "if": "access=key" + "if": "access=key", + "then": "Accessible, but one has to ask a key to enter" } - ], - "question": "Are these toilets publicly accessible?", - "type": "text", - "addExtraTags": "fixme=the tag access was filled out by the user and might need refinement", - "render": "Access is {access}" + ] }, { + "question": "Are these toilets free to use?", "mappings": [ { "then": "These are paid toilets", @@ -71,19 +88,19 @@ "if": "fee=no", "then": "Free to use" } - ], - "type": "text", - "question": "Are these toilets free to use?" + ] }, { - "key": "charge", - "mappings": [], "question": "How much does one have to pay for these toilets?", - "type": "string", "render": "The fee is {charge}", - "condition": "fee=yes" + "condition": "fee=yes", + "freeform": { + "key": "charge", + "type": "string" + } }, { + "question": "Is there a dedicated toilet for wheelchair users", "mappings": [ { "then": "There is a dedicated toilet for wheelchair users", @@ -93,11 +110,10 @@ "if": "wheelchair=no", "then": "No wheelchair access" } - ], - "type": "text", - "question": "Is there a dedicated toilet for wheelchair users" + ] }, { + "question": "Which kind of toilets are this?", "mappings": [ { "if": "toilets:position=seated", @@ -115,11 +131,10 @@ "if": "toilets:position=seated;urinals", "then": "Both seated toilets and urinals are available here" } - ], - "question": "Which kind of toilets are this?", - "type": "text" + ] }, { + "question": "Is a changing table (to change diapers) available?", "mappings": [ { "then": "A changing table is available", @@ -129,12 +144,15 @@ "if": "changing_table=no", "then": "No changing table is available" } - ], - "question": "Is a changing table (to change diapers) available?", - "type": "text" + ] }, { - "key": "changing_table:location", + "question": "Where is the changing table located?", + "render": "The changing table is located at {changing_table:location}", + "condition": "changing_table=yes", + "freeform": { + "key": "changing_table:location" + }, "mappings": [ { "then": "The changing table is in the toilet for women. ", @@ -152,24 +170,9 @@ "if": "changing_table:location=dedicated_room", "then": "The changing table is in a dedicated room. " } - ], - "type": "text", - "question": "Where is the changing table located?", - "condition": "changing_table=yes", - "render": "The changing table is located at {changing_table:location}" + ] } - ], - "overpassTags": "amenity=toilets" + ] } - ], - "startLat": "51.2095", - "startZoom": "12", - "maintainer": "Pieter Vander Vennet", - "title": "Open Toilet Map", - "startLon": "3.2222", - "widenFactor": 0.05, - "icon": "./assets/themes/toilets/toilets.svg", - "description": "A map of public toilets", - "language": ["en"], - "name": "toilets" + ] } \ No newline at end of file diff --git a/customGenerator.ts b/customGenerator.ts index 4207f1e..0f6601e 100644 --- a/customGenerator.ts +++ b/customGenerator.ts @@ -1,81 +1,2 @@ -import {OsmConnection, UserDetails} from "./Logic/Osm/OsmConnection"; -import {VariableUiElement} from "./UI/Base/VariableUIElement"; -import {UIEventSource} from "./Logic/UIEventSource"; -import {ThemeGenerator} from "./UI/CustomThemeGenerator/ThemeGenerator"; -import {Preview} from "./UI/CustomThemeGenerator/Preview"; -import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; -import {createHash} from "crypto"; -import Combine from "./UI/Base/Combine"; -import {Button} from "./UI/Base/Button"; -import {FixedUiElement} from "./UI/Base/FixedUiElement"; -import {State} from "./State"; -import {TextField} from "./UI/Input/TextField"; -const connection = new OsmConnection(true, new UIEventSource<string>(undefined), "customThemeGenerator", false); -connection.AttemptLogin(); - -let hash = window.location.hash?.substr(1); -const localStorage = LocalStorageSource.Get("last-custom-save"); -console.log("hash", hash) -console.log("Saved: ", localStorage.data) - -if (hash === undefined || hash === "" && localStorage.data !== undefined) { - const previous = localStorage.data.split("#"); - hash = previous[1]; - console.log("Using previously saved data ", hash) -} - -const themeGenerator = new ThemeGenerator(connection, hash); -themeGenerator.AttachTo("layoutCreator"); -themeGenerator.url.syncWith(localStorage); - - - -function setDefault() { - themeGenerator.themeObject.data.title = undefined; - themeGenerator.themeObject.data.description = undefined; - themeGenerator.themeObject.data.icon = undefined; - themeGenerator.themeObject.data.language = ["en"]; - themeGenerator.themeObject.data.name = undefined; - themeGenerator.themeObject.data.startLat = 0; - themeGenerator.themeObject.data.startLon = 0; - themeGenerator.themeObject.data.startZoom = 12; - themeGenerator.themeObject.data.maintainer = connection.userDetails.data.name; - themeGenerator.themeObject.data.layers = []; - themeGenerator.themeObject.data.widenFactor = 0.07; - themeGenerator.themeObject.ping(); -} - - -var loadFrom = new TextField<string>( - { - placeholder: "Load from a previous JSON (paste the value here)", - toString: str => str, - fromString: str => str, - } -); -loadFrom.GetValue().setData(""); - -const loadFromTextField = new Button("Load", () => { - const parsed = JSON.parse(loadFrom.GetValue().data); - setDefault(); - for (const parsedKey in parsed) { - themeGenerator.themeObject.data[parsedKey] = parsed[parsedKey]; - } - themeGenerator.themeObject.ping(); - loadFrom.GetValue().setData(""); -}); - -new Combine([ - new Preview(themeGenerator.url, themeGenerator.testurl, themeGenerator.themeObject), - loadFrom, - loadFromTextField, - "<span class='alert'>Loading from the text field will erase the current theme</span>", - "<h2>Danger zone</h2>", - "<br/>", - new Button("Clear theme", setDefault), - "<br/>", - "Version: ", - State.vNumber - -]).AttachTo("preview"); +console.log("TODO") \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index d7c4fbc..789e889 100755 --- a/deploy.sh +++ b/deploy.sh @@ -8,11 +8,7 @@ rm -rf /home/pietervdvn/git/pietervdvn.github.io/MapComplete/* cp -r dist/* /home/pietervdvn/git/pietervdvn.github.io/MapComplete/ cd /home/pietervdvn/git/pietervdvn.github.io/MapComplete/ -git add . -git commit -m "New mapcomplete version" -git push - - +# git add . && git commit -m "New mapcomplete version" &&git push cd - # clean up the mess we made # rm *.js @@ -37,4 +33,4 @@ for f in ./*.html; do else rm $f fi -done \ No newline at end of file +done diff --git a/index.css b/index.css index 0335433..c2fec20 100644 --- a/index.css +++ b/index.css @@ -1,4 +1,4 @@ -html, body { + html, body { height: 100%; margin: 0; padding: 0; @@ -928,6 +928,11 @@ form { position: relative; } +.question .form-text-field > input{ + width: 100%; + box-sizing: border-box; +} + .osmlink { position: absolute; right: 0; diff --git a/index.ts b/index.ts index 6a33bc7..691f4ff 100644 --- a/index.ts +++ b/index.ts @@ -1,7 +1,7 @@ import {TagRendering} from "./Customizations/TagRendering"; import {UserBadge} from "./UI/UserBadge"; import {CenterMessageBox} from "./UI/CenterMessageBox"; -import {TagUtils} from "./Logic/TagsFilter"; +import {TagUtils} from "./Logic/Tags"; import {FullScreenMessageBoxHandler} from "./UI/FullScreenMessageBoxHandler"; import {FeatureInfoBox} from "./UI/FeatureInfoBox"; import {SimpleAddUI} from "./UI/SimpleAddUI"; @@ -13,11 +13,10 @@ import {InitUiElements} from "./InitUiElements"; import {StrayClickHandler} from "./Logic/Leaflet/StrayClickHandler"; import {GeoLocationHandler} from "./Logic/Leaflet/GeoLocationHandler"; import {State} from "./State"; -import {CustomLayoutFromJSON} from "./Customizations/JSON/CustomLayoutFromJSON"; import {QueryParameters} from "./Logic/Web/QueryParameters"; import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; import {PersonalLayout} from "./Logic/PersonalLayout"; -import {OsmConnection} from "./Logic/Osm/OsmConnection"; +import {FromJSON} from "./Customizations/JSON/FromJSON"; TagRendering.injectFunction(); @@ -88,8 +87,8 @@ if (layoutFromBase64 !== "false") { hashFromLocalStorage.setData(hash); dedicatedHashFromLocalStorage.setData(hash); } - layoutToUse = CustomLayoutFromJSON.FromQueryParam(hash.substr(1)); - userLayoutParam.setData(layoutToUse.name); + layoutToUse = FromJSON.FromBase64(hash.substr(1)); + userLayoutParam.setData(layoutToUse.id); } catch (e) { new FixedUiElement("Error: could not parse the custom layout:<br/> "+e).AttachTo("centermessage"); throw e; @@ -104,18 +103,18 @@ if (layoutToUse === undefined) { throw "Incorrect layout" } -console.log("Using layout: ", layoutToUse.name); +console.log("Using layout: ", layoutToUse.id); State.state = new State(layoutToUse); if (layoutToUse.hideFromOverview) { - State.state.osmConnection.GetPreference("hidden-theme-" + layoutToUse.name + "-enabled").setData("true"); + State.state.osmConnection.GetPreference("hidden-theme-" + layoutToUse.id + "-enabled").setData("true"); } if (layoutFromBase64 !== "false") { State.state.layoutDefinition = hash.substr(1); State.state.osmConnection.OnLoggedIn(() => { - State.state.osmConnection.GetLongPreference("installed-theme-" + layoutToUse.name).setData(State.state.layoutDefinition); + State.state.osmConnection.GetLongPreference("installed-theme-" + layoutToUse.id).setData(State.state.layoutDefinition); }) } InitUiElements.InitBaseMap(); diff --git a/preferences.ts b/preferences.ts index 7c8d5bd..6d9e4c3 100644 --- a/preferences.ts +++ b/preferences.ts @@ -11,7 +11,7 @@ import {UIElement} from "./UI/UIElement"; import {UIEventSource} from "./Logic/UIEventSource"; -const connection = new OsmConnection(false, new UIEventSource<string>(undefined)); +const connection = new OsmConnection(false, new UIEventSource<string>(undefined), ""); let rendered = false; diff --git a/test.ts b/test.ts index 01cd5e2..e69de29 100644 --- a/test.ts +++ b/test.ts @@ -1,19 +0,0 @@ -import {OsmConnection} from "./Logic/Osm/OsmConnection"; -import {UIEventSource} from "./Logic/UIEventSource"; -import {ChangesetHandler} from "./Logic/Osm/ChangesetHandler"; -import {State} from "./State"; -import {LayerDefinition} from "./Customizations/LayerDefinition"; -import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; -import {Tag} from "./Logic/TagsFilter"; -import {VariableUiElement} from "./UI/Base/VariableUIElement"; -const bookcases = AllKnownLayouts.layoutsList[5]; -State.state = new State(bookcases); - -new VariableUiElement( - State.state.osmConnection.changesetHandler.currentChangeset -).AttachTo("maindiv"); - -window.setTimeout(() => { - - State.state.osmConnection.changesetHandler.CloseChangeset("89995035") -}, 1000) \ No newline at end of file diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index 6395edc..bfec14f 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -1,29 +1,17 @@ -import {equal} from "assert"; import {UIElement} from "../UI/UIElement"; UIElement.runningFromConsole = true; -import {CustomLayoutFromJSON} from "../Customizations/JSON/CustomLayoutFromJSON"; -import {And} from "../Logic/TagsFilter"; + +import {equal} from "assert"; import Translation from "../UI/i18n/Translation"; import T from "./TestHelper"; +import {FromJSON} from "../Customizations/JSON/FromJSON"; +import {And, Tag} from "../Logic/Tags"; +import Locale from "../UI/i18n/Locale"; +import Translations from "../UI/i18n/Translations"; new T([ - ["Parse and match advanced tagging", () => { - const tags = CustomLayoutFromJSON.TagsFromJson("indoor=yes&access!=private"); - const m0 = new And(tags).matches([{k: "indoor", v: "yes"}, {k: "access", v: "yes"}]); - equal(m0, true); - const m1 = new And(tags).matches([{k: "indoor", v: "yes"}, {k: "access", v: "private"}]); - equal(m1, false); - } - ], - ["Parse tagging with regex", () => { - const tags = CustomLayoutFromJSON.TagsFromJson("highway~=residential|tertiary"); - equal(""+tags[0].value, ""+/residential|tertiary/); - console.log(tags[0].asOverpass()); - - } - ], ["Tag replacement works in translation", () => { const tr = new Translation({ "en": "Test {key} abc" @@ -31,6 +19,50 @@ new T([ equal(tr.txt, "Test value abc"); }], - ["Preset renders icon correctly", () => { - }] + ["Parse tag config", (() => { + const tag = FromJSON.Tag("key=value") as Tag; + equal(tag.key, "key"); + equal(tag.value, "value"); + + const and = FromJSON.Tag({"and": ["key=value", "x=y"]}) as And; + equal((and.and[0] as Tag).key, "key"); + equal((and.and[1] as Tag).value, "y"); + + })], + ["Parse translation map", (() => { + + const json: any = {"en": "English", "nl": "Nederlands"}; + const translation = Translations.WT(FromJSON.Translation(json)); + Locale.language.setData("en"); + equal(translation.txt, "English"); + Locale.language.setData("nl"); + equal(translation.txt, "Nederlands"); + })], + ["Parse tag rendering", (() => { + Locale.language.setData("nl"); + const tr = FromJSON.TagRendering({ + render: ({"en":"Name is {name}", "nl":"Ook een {name}"} as any), + question: "Wat is de naam van dit object?", + freeform: { + key: "name", + }, + fixedInputField: { + mappings: [ + { + if: "noname=yes", + "then": "Has no name" + } + ] + }, + condition: "x=" + }); + + equal(true, tr.IsKnown({"noname": "yes"})); + equal(true, tr.IsKnown({"name": "ABC"})); + equal(false, tr.IsKnown({"foo": "bar"})); + equal("Has no name", tr.GetContent({"noname": "yes"})); + equal("Ook een xyz", tr.GetContent({"name": "xyz"})); + equal("Ook een {name}", tr.GetContent({"foo": "bar"})); + + })] ]);