diff --git a/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts index 639c0d7..29e9789 100644 --- a/Customizations/AllKnownLayouts.ts +++ b/Customizations/AllKnownLayouts.ts @@ -4,41 +4,35 @@ import {Groen} from "./Layouts/Groen"; import Cyclofix from "./Layouts/Cyclofix"; import {StreetWidth} from "./Layouts/StreetWidth"; import {GRB} from "./Layouts/GRB"; -import {ClimbingTrees} from "./Layouts/ClimbingTrees"; -import {Smoothness} from "./Layouts/Smoothness"; import {MetaMap} from "./Layouts/MetaMap"; import {Natuurpunt} from "./Layouts/Natuurpunt"; -import {GhostBikes} from "./Layouts/GhostBikes"; 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 * as ghostbikes from "../assets/themes/ghostbikes/ghostbikes.json" import {PersonalLayout} from "../Logic/PersonalLayout"; export class AllKnownLayouts { public static allLayers: Map = undefined; - + public static layoutsList: Layout[] = [ new PersonalLayout(), new Natuurpunt(), new GRB(), new Cyclofix(), - new GhostBikes(), FromJSON.LayoutFromJSON(bookcases), - // FromJSON.LayoutFromJSON(aed), - // FromJSON.LayoutFromJSON(toilets), - // FromJSON.LayoutFromJSON(artworks), - // FromJSON.LayoutFromJSON(cyclestreets), - + FromJSON.LayoutFromJSON(aed), + FromJSON.LayoutFromJSON(toilets), + FromJSON.LayoutFromJSON(artworks), + FromJSON.LayoutFromJSON(cyclestreets), + FromJSON.LayoutFromJSON(ghostbikes), + new MetaMap(), new StreetWidth(), - new ClimbingTrees(), - new Smoothness(), new Groen(), ]; @@ -47,11 +41,15 @@ export class AllKnownLayouts { public static allSets: Map = AllKnownLayouts.AllLayouts(); private static AllLayouts(): Map { - - this.allLayers = new Map(); for (const layout of this.layoutsList) { - for (const layer of layout.layers) { + for (let i = 0; i < layout.layers.length; i++) { + let layer = layout.layers[i]; + if (typeof (layer) === "string") { + layer = layout.layers[i] = FromJSON.sharedLayers.get(layer); + } + + if (this.allLayers[layer.id] !== undefined) { continue; } diff --git a/Customizations/JSON/FromJSON.ts b/Customizations/JSON/FromJSON.ts index 5dc94be..895cd14 100644 --- a/Customizations/JSON/FromJSON.ts +++ b/Customizations/JSON/FromJSON.ts @@ -13,10 +13,31 @@ import Translations from "../../UI/i18n/Translations"; import Combine from "../../UI/Base/Combine"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import {ImageCarouselConstructor} from "../../UI/Image/ImageCarousel"; - +import * as drinkingWater from "../../assets/layers/drinking_water/drinking_water.json"; +import * as ghostbikes from "../../assets/layers/ghost_bike/ghost_bike.json" +import * as viewpoint from "../../assets/layers/viewpoint/viewpoint.json" +import {Utils} from "../../Utils"; export class FromJSON { + public static sharedLayers: Map = FromJSON.getSharedLayers(); + + private static getSharedLayers() { + const sharedLayers = new Map(); + + const sharedLayersList = [ + FromJSON.Layer(drinkingWater), + FromJSON.Layer(ghostbikes), + FromJSON.Layer(viewpoint), + + ]; + + for (const layer of sharedLayersList) { + sharedLayers.set(layer.id, layer); + } + + return sharedLayers; + } public static FromBase64(layoutFromBase64: string): Layout { return FromJSON.LayoutFromJSON(JSON.parse(atob(layoutFromBase64))); @@ -139,19 +160,27 @@ export class FromJSON { { k: FromJSON.Tag(mapping.if), txt: FromJSON.Translation(mapping.then), - hideInAnswer: mapping.hideInAnswer + hideInAnswer: mapping.hideInAnswer }) ); - return new TagRenderingOptions({ + + let rendering = new TagRenderingOptions({ question: FromJSON.Translation(json.question), freeform: freeform, mappings: mappings }); + + if (json.condition) { + console.log("Applying confition ", json.condition) + return rendering.OnlyShowIf(FromJSON.Tag(json.condition)); + } + + return rendering; } public static SimpleTag(json: string): Tag { - const tag = json.split("="); + const tag = Utils.SplitFirst(json, "="); return new Tag(tag[0], tag[1]); } @@ -159,35 +188,39 @@ export class FromJSON { if (typeof (json) == "string") { const tag = json as string; if (tag.indexOf("!~") >= 0) { - const split = tag.split("!~"); - if(split[1] == "*"){ + const split = Utils.SplitFirst(tag, "!~"); + if (split[1] === "*") { split[1] = ".*" } + console.log(split) return new RegexTag( - new RegExp(split[0]), - new RegExp(split[1]), + split[0], + new RegExp("^" + split[1] + "$"), true ); } if (tag.indexOf("!=") >= 0) { - const split = tag.split("!="); + const split = Utils.SplitFirst(tag, "!="); + if (split[1] === "*") { + split[1] = ".*" + } return new RegexTag( - new RegExp(split[0]), - new RegExp(split[1]), + split[0], + new RegExp("^" + split[1] + "$"), true ); } if (tag.indexOf("~") >= 0) { - const split = tag.split("~"); - if(split[1] == "*"){ + const split = Utils.SplitFirst(tag, "~"); + if (split[1] === "*") { split[1] = ".*" } return new RegexTag( - new RegExp("^"+split[0]+"$"), - new RegExp("^"+split[1]+"$") + split[0], + new RegExp("^" + split[1] + "$") ); } - const split = tag.split("="); + const split = Utils.SplitFirst(tag, "="); return new Tag(split[0], split[1]) } if (json.and !== undefined) { @@ -208,11 +241,23 @@ export class FromJSON { } } - public static Layer(json: LayerConfigJson): LayerDefinition { - console.log("Parsing ",json.name); + public static Layer(json: LayerConfigJson | string): LayerDefinition { + + if (typeof (json) === "string") { + const cached = FromJSON.sharedLayers.get(json); + if (cached) { + return cached; + } + + throw "Layer not yet loaded..." + } + + + 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 iconSize = FromJSON.TagRenderingWithDefault(json.iconSize, "iconSize", "40,40,center"); const color = FromJSON.TagRenderingWithDefault(json.color, "layercolor", "#0000ff"); const width = FromJSON.TagRenderingWithDefault(json.width, "layerwidth", "10"); const renderTags = {"id": "node/-1"} @@ -225,11 +270,38 @@ export class FromJSON { }) ?? []; function style(tags) { + const iconSizeStr = iconSize.GetContent(tags).txt.split(","); + const iconwidth = Number(iconSizeStr[0]); + const iconheight = Number(iconSizeStr[1]); + const iconmode = iconSizeStr[2]; + const iconAnchor = [iconwidth / 2, iconheight / 2] // x, y + // If iconAnchor is set to [0,0], then the top-left of the icon will be placed at the geographical location + if (iconmode.indexOf("left") >= 0) { + iconAnchor[0] = 0; + } + if (iconmode.indexOf("right") >= 0) { + iconAnchor[0] = iconwidth; + } + + if (iconmode.indexOf("top") >= 0) { + iconAnchor[1] = 0; + } + if (iconmode.indexOf("bottom") >= 0) { + iconAnchor[1] = iconheight; + } + + // the anchor is always set from the center of the point + // x, y with x going right and y going down if the values are bigger + const popupAnchor = [0, -iconAnchor[1]+3]; + return { color: color.GetContent(tags).txt, weight: width.GetContent(tags).txt, icon: { - iconUrl: icon.GetContent(tags).txt + iconUrl: icon.GetContent(tags).txt, + iconSize: [iconwidth, iconheight], + popupAnchor: popupAnchor, + iconAnchor: iconAnchor }, } } diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts index 01aa5fa..c69901c 100644 --- a/Customizations/JSON/LayerConfigJson.ts +++ b/Customizations/JSON/LayerConfigJson.ts @@ -46,6 +46,13 @@ export interface LayerConfigJson { * 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; + + /** + * A string containing "width,height" or "width,height,anchorpoint" where anchorpoint is any of 'center', 'top', 'bottom', 'left', 'right', 'bottomleft','topright', ... + * Default is '40,40,center' + */ + iconSize?: string | TagRenderingConfigJson; + /** * The color for way-elements */ @@ -67,8 +74,8 @@ export interface LayerConfigJson { * Presets for this layer */ presets?: { - tags: string[], title: string | any, + tags: string[], description?: string | any, }[], diff --git a/Customizations/JSON/LayoutConfigJson.ts b/Customizations/JSON/LayoutConfigJson.ts index b7404e0..274b3f5 100644 --- a/Customizations/JSON/LayoutConfigJson.ts +++ b/Customizations/JSON/LayoutConfigJson.ts @@ -83,12 +83,12 @@ export interface LayoutConfigJson { * 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[], + roamingRenderings?: (TagRenderingConfigJson | string)[], /** * The layers to display */ - layers: LayerConfigJson[], + layers: (LayerConfigJson | string)[], diff --git a/Customizations/JSON/TagConfig.ts b/Customizations/JSON/TagConfig.ts deleted file mode 100644 index bc12272..0000000 --- a/Customizations/JSON/TagConfig.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 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/LayerDefinition.ts b/Customizations/LayerDefinition.ts index feb1004..a17b7de 100644 --- a/Customizations/LayerDefinition.ts +++ b/Customizations/LayerDefinition.ts @@ -75,7 +75,10 @@ export class LayerDefinition { color: string, weight?: number, icon: { - iconUrl: string, iconSize?: number[], popupAnchor?: number[], iconAnchor?: number[] + iconUrl: string, + iconSize?: number[], + popupAnchor?: number[], + iconAnchor?: number[] }, }; diff --git a/Customizations/Layers/BikeOtherShops.ts b/Customizations/Layers/BikeOtherShops.ts index 38b8ab0..33386a5 100644 --- a/Customizations/Layers/BikeOtherShops.ts +++ b/Customizations/Layers/BikeOtherShops.ts @@ -24,7 +24,7 @@ export default class BikeOtherShops extends LayerDefinition { this.name = this.to.name this.icon = "./assets/bike/non_bike_repair_shop.svg" this.overpassFilter = new And([ - new RegexTag(/^shop$/, /^bicycle$/, true), + new RegexTag("shop", /^bicycle$/, true), new RegexTag(/^service:bicycle:/, /.*/), ]) this.presets = [] diff --git a/Customizations/Layers/ClimbingTree.ts b/Customizations/Layers/ClimbingTree.ts deleted file mode 100644 index d5009bd..0000000 --- a/Customizations/Layers/ClimbingTree.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {LayerDefinition} from "../LayerDefinition"; -import Translations from "../../UI/i18n/Translations"; -import FixedText from "../Questions/FixedText"; -import {And, Tag} from "../../Logic/Tags"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; - -export class ClimbingTree extends LayerDefinition { - - - constructor() { - super("climbingtree"); - const t = Translations.t.climbingTrees.layer; - this.title = new FixedText(t.title); - const icon = "./assets/themes/nature/tree.svg"; - this.icon = icon; - this.description = t.description; - this.style = (tags) => { - return { - color: "#00aa00", - icon: { - iconUrl: icon, - iconSize: [50, 50] - } - } - } - const tags = [new Tag("natural","tree"),new Tag("sport","climbing")]; - this.overpassFilter = new And(tags); - this.presets = [ - { - title: t.title, - description: t.description, - tags: tags - } - ] - this.minzoom = 12; - this.elementsToShow = [ - new ImageCarouselWithUploadConstructor() - ] - - - } - -} \ No newline at end of file diff --git a/Customizations/Layers/DrinkingWater.ts b/Customizations/Layers/DrinkingWater.ts deleted file mode 100644 index 50a5376..0000000 --- a/Customizations/Layers/DrinkingWater.ts +++ /dev/null @@ -1,62 +0,0 @@ -import {LayerDefinition} from "../LayerDefinition"; -import {And, Or, Tag} from "../../Logic/Tags"; -import {OperatorTag} from "../Questions/OperatorTag"; -import FixedText from "../Questions/FixedText"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; -import Translations from "../../UI/i18n/Translations"; -import {TagRenderingOptions} from "../TagRenderingOptions"; - -export class DrinkingWater extends LayerDefinition { - - constructor() { - super("drinkingwater"); - this.name = Translations.t.cyclofix.drinking_water.title; - this.icon = "./assets/bike/drinking_water.svg"; - - this.overpassFilter = new Or([ - new And([ - new Tag("amenity", "drinking_water") - ]) - ]); - - - this.presets = [{ - title: Translations.t.cyclofix.drinking_water.title, - tags: [new Tag("amenity", "drinking_water")] - }]; - this.maxAllowedOverlapPercentage = 10; - this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY - - this.minzoom = 13; - this.style = DrinkingWater.generateStyleFunction(); - this.title = new FixedText("Drinking water"); - this.elementsToShow = [ - new OperatorTag(), - ]; - this.elementsToShow = [ - new ImageCarouselWithUploadConstructor(), - new TagRenderingOptions({ - question: "How easy is it to fill water bottles?", - mappings: [ - { k: new Tag("bottle", "yes"), txt: "It is easy to refill water bottles" }, - { k: new Tag("bottle", "no"), txt: "Water bottles may not fit" } - ], - })]; - - } - - - private static generateStyleFunction() { - return function () { - return { - color: "#00bb00", - icon: { - iconUrl: "./assets/bike/drinking_water.svg", - iconSize: [50, 50], - iconAnchor: [25,50] - } - }; - }; - } - -} \ No newline at end of file diff --git a/Customizations/Layers/GhostBike.ts b/Customizations/Layers/GhostBike.ts deleted file mode 100644 index c47b0f8..0000000 --- a/Customizations/Layers/GhostBike.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {LayerDefinition} from "../LayerDefinition"; -import {Tag} from "../../Logic/Tags"; -import FixedText from "../Questions/FixedText"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; -import {TagRenderingOptions} from "../TagRenderingOptions"; - -export class GhostBike extends LayerDefinition { - constructor() { - super("ghost bike"); - this.name = "Ghost bike"; - this.overpassFilter = new Tag("memorial", "ghost_bike") - this.title = new FixedText("Ghost bike"); - this.description = "A ghost bike is a memorial for a cyclist who died in a traffic accident," + - " in the form of a white bicycle placed permanently near the accident location."; - - this.minzoom = 1; - this.icon = "./assets/bike/ghost.svg" - this.presets = [ - { - title: "Ghost bike", - description: "Add a missing ghost bike to the map", - tags: [new Tag("historic", "memorial"), new Tag("memorial", "ghost_bike")] - } - ] - - this.elementsToShow = [ - new FixedText(this.description), - new ImageCarouselWithUploadConstructor(), - - new TagRenderingOptions({ - question: "Whom is remembered by this ghost bike?" + - "" + - "
" + - "Please respect privacy - only fill out the name if it is widely published or marked on the cycle." + - "
", - mappings: [{k: new Tag("noname", "yes"), txt: "There is no name marked on the bike"},], - freeform: { - key: "name", - extraTags: new Tag("noname", ""), - template: "$$$", - renderTemplate: "In the remembrance of {name}", - } - }), - new TagRenderingOptions({ - question: "When was the ghost bike installed?", - freeform: { - key: "start_date", - template: "The ghost bike was placed on $$$", // TODO create a date picker - renderTemplate: "The ghost bike was placed on {start_date}", - } - }), - new TagRenderingOptions({ - question: "On what URL can more information be found?" + - "If available, add a link to a news report about the accident or about the placing of the ghost bike", - freeform: { - key: "source", - template: "More information available on $$$", - renderTemplate: "More information", - } - }), - - - - ]; - - this.style = (tags: any) => { - return { - color: "#000000", - icon: { - iconUrl: 'assets/bike/ghost.svg', - iconSize: [40, 40], - iconAnchor: [20, 20], - } - } - }; - - } -} diff --git a/Customizations/Layers/Viewpoint.ts b/Customizations/Layers/Viewpoint.ts deleted file mode 100644 index ec3e8a2..0000000 --- a/Customizations/Layers/Viewpoint.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {LayerDefinition} from "../LayerDefinition"; -import FixedText from "../Questions/FixedText"; -import {Tag} from "../../Logic/Tags"; -import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; -import {TagRenderingOptions} from "../TagRenderingOptions"; - -export class Viewpoint extends LayerDefinition { - - constructor() { - super("viewpoint",{ - name: "Bezienswaardigheid", - description: "Wil je een foto toevoegen van iets dat geen park, bos of natuurgebied is? Dit kan hiermee", - presets: [{ - title: "Bezienswaardigheid (andere)", - description: "Wens je een foto toe te voegen dat geen park, bos of (erkend) natuurreservaat is? Dit kan hiermee", - tags: [new Tag("tourism", "viewpoint"), - new Tag("fixme", "Added with mapcomplete. This viewpoint should probably me merged with some existing feature")] - }], - icon: "assets/viewpoint.svg", - wayHandling: LayerDefinition.WAYHANDLING_CENTER_ONLY, - style: _ => { - return { - color: undefined, icon: { - iconUrl: "assets/viewpoint.svg", - iconSize: [20, 20] - } - } - }, - maxAllowedOverlapPercentage: 0, - overpassFilter: new Tag("tourism", "viewpoint"), - minzoom: 13, - title: new FixedText("Bezienswaardigheid") - }); - - this.elementsToShow = [ - new FixedText(this.description), - new ImageCarouselWithUploadConstructor(), - new TagRenderingOptions({ - question: "Zijn er bijzonderheden die je wilt toevoegen?", - freeform:{ - key: "description:0", - template: "$$$", - renderTemplate: "

Bijzonderheden

{description:0}" - } - }) - ] - } - -} \ No newline at end of file diff --git a/Customizations/Layers/Widths.ts b/Customizations/Layers/Widths.ts index 201eb06..436c644 100644 --- a/Customizations/Layers/Widths.ts +++ b/Customizations/Layers/Widths.ts @@ -1,6 +1,7 @@ import {LayerDefinition} from "../LayerDefinition"; import {And, Or, Tag} from "../../Logic/Tags"; import {TagRenderingOptions} from "../TagRenderingOptions"; +import {FromJSON} from "../JSON/FromJSON"; export class Widths extends LayerDefinition { @@ -39,6 +40,13 @@ export class Widths extends LayerDefinition { [new Tag("highway", "pedestrian"), new Tag("highway", "living_street"), new Tag("access","destination"), new Tag("motor_vehicle", "destination")]) + private readonly _notCarfree = + FromJSON.Tag({"and":[ + "highway!~pedestrian|living_street", + "access!~destination", + "motor_vehicle!~destination|no" + ]}); + private calcProps(properties) { let parkingStateKnown = true; let parallelParkingCount = 0; @@ -195,7 +203,7 @@ export class Widths extends LayerDefinition { renderTemplate: "{note:width:carriageway}", template: "$$$", } - }).OnlyShowIf(this._carfree, true), + }).OnlyShowIf(this._notCarfree), new TagRenderingOptions({ @@ -215,7 +223,7 @@ export class Widths extends LayerDefinition { renderTemplate: "{note:width:carriageway}", template: "$$$", } - }).OnlyShowIf(this._carfree, true), + }).OnlyShowIf(this._notCarfree), new TagRenderingOptions({ @@ -245,7 +253,7 @@ export class Widths extends LayerDefinition { txt: "Tweerichtingsverkeer voor iedereen. Dit gebruikt " + r(2 * this.carWidth + 2 * this.cyclistWidth) + "m" } ] - }).OnlyShowIf(this._carfree, true), + }).OnlyShowIf(this._notCarfree), new TagRenderingOptions( { @@ -263,7 +271,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._carfree, true), + ).OnlyShowIf(this._notCarfree), new TagRenderingOptions({ diff --git a/Customizations/Layout.ts b/Customizations/Layout.ts index 8cf2f7f..8e1976d 100644 --- a/Customizations/Layout.ts +++ b/Customizations/Layout.ts @@ -18,7 +18,7 @@ export class Layout { public changesetMessage: string; public socialImage: string = ""; - public layers: LayerDefinition[]; + public layers: (LayerDefinition | string)[]; public welcomeMessage: UIElement; public gettingStartedPlzLogin: UIElement; public welcomeBackMessage: UIElement; @@ -63,7 +63,7 @@ export class Layout { id: string, supportedLanguages: string[], title: UIElement | string, - layers: LayerDefinition[], + layers: (LayerDefinition | string)[], startzoom: number, startLat: number, startLon: number, diff --git a/Customizations/Layouts/ClimbingTrees.ts b/Customizations/Layouts/ClimbingTrees.ts deleted file mode 100644 index 068b4e6..0000000 --- a/Customizations/Layouts/ClimbingTrees.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Translations from "../../UI/i18n/Translations"; -import {Layout} from "../Layout"; -import {ClimbingTree} from "../Layers/ClimbingTree"; - -export class ClimbingTrees extends Layout { - constructor() { - super( - "climbing_trees", - ["nl"], - Translations.t.climbingTrees.layout.title, - [new ClimbingTree()], - 12, - 50.8435, - 4.3688, - Translations.t.climbingTrees.layout.welcome - ); - this.icon = "./assets/themes/nature/tree.svg" - this.hideFromOverview = true; - } -} \ No newline at end of file diff --git a/Customizations/Layouts/Cyclofix.ts b/Customizations/Layouts/Cyclofix.ts index d2a4dec..f10d203 100644 --- a/Customizations/Layouts/Cyclofix.ts +++ b/Customizations/Layouts/Cyclofix.ts @@ -3,7 +3,6 @@ import BikeParkings from "../Layers/BikeParkings"; import BikeServices from "../Layers/BikeStations"; import BikeShops from "../Layers/BikeShops"; import Translations from "../../UI/i18n/Translations"; -import {DrinkingWater} from "../Layers/DrinkingWater"; import Combine from "../../UI/Base/Combine"; import BikeOtherShops from "../Layers/BikeOtherShops"; import BikeCafes from "../Layers/BikeCafes"; @@ -15,7 +14,7 @@ export default class Cyclofix extends Layout { "cyclofix", ["en", "nl", "fr","gl"], Translations.t.cyclofix.title, - [new BikeServices(), new BikeShops(), new DrinkingWater(), new BikeParkings(), new BikeOtherShops(), new BikeCafes()], + [new BikeServices(), new BikeShops(), "drinking_water", new BikeParkings(), new BikeOtherShops(), new BikeCafes()], 16, 50.8465573, 4.3516970, diff --git a/Customizations/Layouts/GhostBikes.ts b/Customizations/Layouts/GhostBikes.ts deleted file mode 100644 index 25eda11..0000000 --- a/Customizations/Layouts/GhostBikes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {Layout} from "../Layout"; -import {GhostBike} from "../Layers/GhostBike"; -import Combine from "../../UI/Base/Combine"; - -export class GhostBikes extends Layout { - constructor() { - super("ghostbikes", - ["en"], - "Ghost Bike Map", - [new GhostBike()], - 6, - 50.423, - 5.493, - new Combine(["

", "A map of Ghost Bikes", "

", - "A ghost bike is a memorial for a cyclist who died in a traffic accident," + - " in the form of a white bicycle placed permanently near the accident location.", - "On this map, one can see the location of known ghost bikes, and (with a free OpenStreetMap account) easily add missing and new Ghost Bikes"]) - ); - - this.icon = "./assets/bike/ghost.svg"; - } -} \ No newline at end of file diff --git a/Customizations/Layouts/Groen.ts b/Customizations/Layouts/Groen.ts index ba0d605..5f27bb1 100644 --- a/Customizations/Layouts/Groen.ts +++ b/Customizations/Layouts/Groen.ts @@ -2,7 +2,6 @@ import {NatureReserves} from "../Layers/NatureReserves"; import {Park} from "../Layers/Park"; import {Bos} from "../Layers/Bos"; import {Layout} from "../Layout"; -import {Viewpoint} from "../Layers/Viewpoint"; export class Groen extends Layout { @@ -10,12 +9,12 @@ export class Groen extends Layout { super("buurtnatuur", ["nl"], "Buurtnatuur.be", - [new NatureReserves(), new Park(), new Bos(), new Viewpoint()], + [new NatureReserves(), new Park(), new Bos(), "viewpoint"], 10, 50.8435, 4.3688, "\n" + - "
" + + "
" + "

Breng jouw buurtnatuur in kaart

" + "Natuur maakt gelukkig. Aan de hand van deze website willen we de natuur dicht bij ons beter inventariseren. Met als doel meer mensen te laten genieten van toegankelijke natuur én te strijden voor meer natuur in onze buurten. \n" + "
    " + @@ -52,9 +51,8 @@ export class Groen extends Layout { "" ); - this.icon = "./assets/groen.svg" - this.locationContains = ["buurtnatuur.be"] - this.socialImage = "assets/BuurtnatuurFront.jpg" + this.icon = "./assets/themes/buurtnatuur/groen_logo.svg" + this.socialImage = "assets/themes/buurtnatuur/social_image.jpg" this.description = "Met deze tool kan je natuur in je buurt in kaart brengen en meer informatie geven over je favoriete plekje" this.enableMoreQuests = false; this.enableShareScreen = false diff --git a/Customizations/Layouts/Natuurpunt.ts b/Customizations/Layouts/Natuurpunt.ts index 89fa5fd..df81eb6 100644 --- a/Customizations/Layouts/Natuurpunt.ts +++ b/Customizations/Layouts/Natuurpunt.ts @@ -2,7 +2,6 @@ import {Layout} from "../Layout"; import {Birdhide} from "../Layers/Birdhide"; import {InformationBoard} from "../Layers/InformationBoard"; import {NatureReserves} from "../Layers/NatureReserves"; -import {DrinkingWater} from "../Layers/DrinkingWater"; export class Natuurpunt extends Layout{ constructor() { @@ -10,7 +9,7 @@ export class Natuurpunt extends Layout{ "natuurpunt", ["nl"], "De natuur in", - [new Birdhide(), new InformationBoard(), new NatureReserves(true), new DrinkingWater()], + [new Birdhide(), new InformationBoard(), new NatureReserves(true), "drinking_water"], 12, 51.20875, 3.22435, diff --git a/Customizations/Layouts/Smoothness.ts b/Customizations/Layouts/Smoothness.ts deleted file mode 100644 index 9f2cd1c..0000000 --- a/Customizations/Layouts/Smoothness.ts +++ /dev/null @@ -1,82 +0,0 @@ -import {Layout} from "../Layout"; -import {LayerDefinition} from "../LayerDefinition"; -import {Or, Tag} from "../../Logic/Tags"; -import {TagRenderingOptions} from "../TagRenderingOptions"; - - -export class SmoothnessLayer extends LayerDefinition { - - constructor() { - super("smoothness"); - this.name = "smoothness"; - this.minzoom = 17; - this.overpassFilter = new Or([ - new Tag("highway","unclassified"), - new Tag("highway", "residential"), - new Tag("highway", "cycleway"), - new Tag("highway", "footway"), - new Tag("highway", "path"), - new Tag("highway", "tertiary") - ]); - - this.elementsToShow = [ - new TagRenderingOptions({ - question: "How smooth is this road to rollerskate on", - mappings: [ - {k: new Tag("smoothness","bad"), txt: "It's horrible"}, - {k: new Tag("smoothness","intermediate"), txt: "It is passable by rollerscate, but only if you have to"}, - {k: new Tag("smoothness","good"), txt: "Good, but it has some friction or holes"}, - {k: new Tag("smoothness","very_good"), txt: "Quite good and enjoyable"}, - {k: new Tag("smoothness","excellent"), txt: "Excellent - this is where you'd want to drive 24/7"}, - ] - }) - ] - - this.style = (properties) => { - let color = "#000000"; - if(new Tag("smoothness","bad").matchesProperties(properties)){ - color = "#ff0000"; - } - if(new Tag("smoothness","intermediate").matchesProperties(properties)){ - color = "#ffaa00"; - } - if(new Tag("smoothness","good").matchesProperties(properties)){ - color = "#ccff00"; - } - if(new Tag("smoothness","very_good").matchesProperties(properties)){ - color = "#00aa00"; - } - if(new Tag("smoothness","excellent").matchesProperties(properties)){ - color = "#00ff00"; - } - - return { - color: color, - icon: undefined, - weight: 8 - } - - } - - - } - -} - -export class Smoothness extends Layout { - constructor() { - super( - "smoothness", - ["en" ], - "Smoothness while rollerskating", - [new SmoothnessLayer()], - 17, - 51.2, - 3.2, - "Give smoothness feedback for rollerskating" - ); - this.widenFactor = 0.005 - this.hideFromOverview = true; - this.enableAdd = false; - } -} \ No newline at end of file diff --git a/Customizations/OnlyShowIf.ts b/Customizations/OnlyShowIf.ts index 2b80242..0552a38 100644 --- a/Customizations/OnlyShowIf.ts +++ b/Customizations/OnlyShowIf.ts @@ -10,19 +10,16 @@ import Translation from "../UI/i18n/Translation"; export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{ private readonly _tagsFilter: TagsFilter; private readonly _embedded: TagDependantUIElementConstructor; - private readonly _invert: boolean; - constructor(tagsFilter: TagsFilter, embedded: TagDependantUIElementConstructor, invert: boolean = false) { + constructor(tagsFilter: TagsFilter, embedded: TagDependantUIElementConstructor) { this._tagsFilter = tagsFilter; this._embedded = embedded; - this._invert = invert; } construct(dependencies): TagDependantUIElement { return new OnlyShowIf(dependencies.tags, this._embedded.construct(dependencies), - this._tagsFilter, - this._invert); + this._tagsFilter); } IsKnown(properties: any): boolean { @@ -51,7 +48,7 @@ export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{ } private Matches(properties: any) : boolean{ - return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties)) != this._invert; + return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties)); } } @@ -59,22 +56,18 @@ export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{ class OnlyShowIf extends UIElement implements TagDependantUIElement { private readonly _embedded: TagDependantUIElement; private readonly _filter: TagsFilter; - private readonly _invert: boolean; constructor( tags: UIEventSource, embedded: TagDependantUIElement, - filter: TagsFilter, - invert: boolean) { + filter: TagsFilter) { super(tags); this._filter = filter; this._embedded = embedded; - this._invert = invert; - } private Matches() : boolean{ - return this._filter.matches(TagUtils.proprtiesToKV(this._source.data)) != this._invert; + return this._filter.matches(TagUtils.proprtiesToKV(this._source.data)); } InnerRender(): string { diff --git a/Customizations/SharedLayers.ts b/Customizations/SharedLayers.ts new file mode 100644 index 0000000..080b2af --- /dev/null +++ b/Customizations/SharedLayers.ts @@ -0,0 +1,3 @@ +export default class SharedLayers { + +} \ No newline at end of file diff --git a/Customizations/TagRenderingOptions.ts b/Customizations/TagRenderingOptions.ts index 0d02343..3dab835 100644 --- a/Customizations/TagRenderingOptions.ts +++ b/Customizations/TagRenderingOptions.ts @@ -81,8 +81,8 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { this.options = options; } - OnlyShowIf(tagsFilter: TagsFilter, invert: boolean = false): TagDependantUIElementConstructor { - return new OnlyShowIfConstructor(tagsFilter, this, invert); + OnlyShowIf(tagsFilter: TagsFilter): TagDependantUIElementConstructor { + return new OnlyShowIfConstructor(tagsFilter, this); } diff --git a/InitUiElements.ts b/InitUiElements.ts index ecfcdeb..f3d9059 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -21,6 +21,7 @@ import {UIEventSource} from "./Logic/UIEventSource"; import {QueryParameters} from "./Logic/Web/QueryParameters"; import {PersonalLayout} from "./Logic/PersonalLayout"; import {PersonalLayersPanel} from "./Logic/PersonalLayersPanel"; +import Locale from "./UI/i18n/Locale"; export class InitUiElements { @@ -106,6 +107,14 @@ export class InitUiElements { } + + static CreateLanguagePicker(label: string | UIElement = "") { + + return new DropDown(label, State.state.layoutToUse.data.supportedLanguages.map(lang => { + return {value: lang, shown: lang} + } + ), Locale.language); + } static InitLayerSelection() { const closedFilterButton = ``; @@ -178,6 +187,10 @@ export class InitUiElements { const state = State.state; for (const layer of state.layoutToUse.data.layers) { + + if(typeof (layer) === "string"){ + throw "Layer "+layer+" was not substituted"; + } const generateInfo = (tagsES, feature) => { diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts index 32510cd..08f753b 100644 --- a/Logic/FilteredLayer.ts +++ b/Logic/FilteredLayer.ts @@ -223,12 +223,8 @@ export class FilteredLayer { } else { if(style.icon.iconSize === undefined){ style.icon.iconSize = [50,50] - }if(style.icon.iconAnchor === undefined){ - style.icon.iconAnchor = [style.icon.iconSize[0] / 2,style.icon.iconSize[1]] - } - if (style.icon.popupAnchor === undefined) { - style.icon.popupAnchor = [0, 8 - (style.icon.iconSize[1])] } + marker = L.marker(latLng, { icon: new L.icon(style.icon), }); diff --git a/Logic/LayerUpdater.ts b/Logic/LayerUpdater.ts index 6472118..00ee7c3 100644 --- a/Logic/LayerUpdater.ts +++ b/Logic/LayerUpdater.ts @@ -53,12 +53,11 @@ export class LayerUpdater { console.log("Not loading layer ", layer.id, " as it needs at least ", layer.minzoom, "zoom") continue; } - // Check if data for this layer has already been loaded let previouslyLoaded = false; for (let z = layer.minzoom; z < 25 && !previouslyLoaded; z++) { const previousLoadedBounds = this.previousBounds.get(z); - if (previousLoadedBounds == undefined) { + if (previousLoadedBounds === undefined) { continue; } for (const previousLoadedBound of previousLoadedBounds) { @@ -89,7 +88,7 @@ export class LayerUpdater { self.runningQuery.setData(false); if (geojson.features.length > 0) { - console.log("Got some leftovers: ", geojson) + console.warn("Got some leftovers: ", geojson) } return; } diff --git a/Logic/Tags.ts b/Logic/Tags.ts index 116b7d7..882e963 100644 --- a/Logic/Tags.ts +++ b/Logic/Tags.ts @@ -18,15 +18,14 @@ export class RegexTag extends TagsFilter { private readonly value: RegExp; private readonly invert: boolean; - constructor(key: RegExp, value: RegExp, invert: boolean = false) { + constructor(key: string | RegExp, value: RegExp, invert: boolean = false) { super(); - this.key = key; + this.key = typeof (key) === "string" ? new RegExp(key) : key; this.value = value; this.invert = invert; } asOverpass(): string[] { - return [`['${this.key.source}'${this.invert ? "!" : ""}~'${this.value.source}']`]; } @@ -45,7 +44,7 @@ export class RegexTag extends TagsFilter { } asHumanString() { - return `${this.key}${this.invert ? "!" : ""}~${this.value}`; + return `${this.key.source}${this.invert ? "!" : ""}~${this.value.source}`; } } @@ -64,7 +63,7 @@ export class Tag extends TagsFilter { if(value === undefined){ throw "Invalid value"; } - if(value === undefined || value === "*"){ + if(value === "*"){ console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}!~*`) } } diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index bffcb4a..23b999e 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -8,17 +8,18 @@ export class UIEventSource{ } - public addCallback(callback: ((latestData : T) => void)) { + public addCallback(callback: ((latestData : T) => void)) : UIEventSource{ this._callbacks.push(callback); return this; } - public setData(t: T): void { + public setData(t: T): UIEventSource { if (this.data === t) { return; } this.data = t; this.ping(); + return this; } public ping(): void { @@ -55,7 +56,6 @@ export class UIEventSource{ const update = function () { newSource.setData(f(self.data)); - newSource.ping(); } this.addCallback(update); diff --git a/State.ts b/State.ts index cb06f51..97225cb 100644 --- a/State.ts +++ b/State.ts @@ -201,8 +201,15 @@ export class State { continue; } try { + const layout = FromJSON.FromBase64(customLayout.data); + if(layout.id === undefined){ + // This is an old style theme + // We remove it + customLayout.setData(undefined); + continue; + } installedThemes.push({ - layout: FromJSON.FromBase64(customLayout.data), + layout: layout, definition: customLayout.data }); } catch (e) { diff --git a/UI/Base/SubtleButton.ts b/UI/Base/SubtleButton.ts index b13f1b8..ec0f09a 100644 --- a/UI/Base/SubtleButton.ts +++ b/UI/Base/SubtleButton.ts @@ -17,6 +17,10 @@ export class SubtleButton extends UIElement{ } InnerRender(): string { + + if(this.message.IsEmpty()){ + return ""; + } if(this.linkTo != undefined){ return new Combine([ diff --git a/UI/Base/TabbedComponent.ts b/UI/Base/TabbedComponent.ts index ebbfc52..2c45e85 100644 --- a/UI/Base/TabbedComponent.ts +++ b/UI/Base/TabbedComponent.ts @@ -15,13 +15,9 @@ export class TabbedComponent extends UIElement { this.headers.push(Translations.W(element.header).onClick(() => self._source.setData(i))); this.content.push(Translations.W(element.content)); } - - } InnerRender(): string { - let html = ""; - let headerBar = ""; for (let i = 0; i < this.headers.length; i++) { let header = this.headers[i]; @@ -36,10 +32,11 @@ export class TabbedComponent extends UIElement { headerBar = "
    " + headerBar + "
    " const content = this.content[this._source.data]; - return headerBar + "
    " + content.Render() + "
    "; + return headerBar + "
    " + (content?.Render() ?? "") + "
    "; } protected InnerUpdate(htmlElement: HTMLElement) { + super.InnerUpdate(htmlElement); this.content[this._source.data].Update(); } diff --git a/UI/CustomGenerator/AllLayersPanel.ts b/UI/CustomGenerator/AllLayersPanel.ts new file mode 100644 index 0000000..d83e59c --- /dev/null +++ b/UI/CustomGenerator/AllLayersPanel.ts @@ -0,0 +1,73 @@ +import {UIElement} from "../UIElement"; +import {TabbedComponent} from "../Base/TabbedComponent"; +import {SubtleButton} from "../Base/SubtleButton"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; +import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson"; +import LayerPanel from "./LayerPanel"; +import SingleSetting from "./SingleSetting"; + +export default class AllLayersPanel extends UIElement { + + + private panel: UIElement; + private _config: UIEventSource; + private _currentlySelected: UIEventSource>; + private languages: UIEventSource; + + private static createEmptyLayer(): LayerConfigJson { + return { + id: undefined, + name: undefined, + minzoom: 0, + overpassTags: undefined, + title: undefined, + description: {} + } + } + + constructor(config: UIEventSource, currentlySelected: UIEventSource>, + languages: UIEventSource) { + super(undefined); + this._config = config; + this._currentlySelected = currentlySelected; + this.languages = languages; + + this.createPanels(); + const self = this; + config.map(config => config.layers.length).addCallback(() => self.createPanels()); + + } + + + private createPanels() { + const self = this; + const tabs = []; + + const layers = this._config.data.layers; + for (let i = 0; i < layers.length; i++) { + tabs.push({ + header: "", + content: new LayerPanel(this._config, this.languages, i, this._currentlySelected) + }); + } + tabs.push({ + header: "", + content: new SubtleButton( + "./assets/add.svg", + "Add a new layer" + ).onClick(() => { + self._config.data.layers.push(AllLayersPanel.createEmptyLayer()) + self._config.ping(); + }) + }) + + this.panel = new TabbedComponent(tabs, new UIEventSource(Math.max(0, layers.length-1))); + this.Update(); + } + + InnerRender(): string { + return this.panel.Render(); + } + +} \ No newline at end of file diff --git a/UI/CustomGenerator/CustomGeneratorPanel.ts b/UI/CustomGenerator/CustomGeneratorPanel.ts new file mode 100644 index 0000000..e69de29 diff --git a/UI/CustomGenerator/GeneralSettings.ts b/UI/CustomGenerator/GeneralSettings.ts new file mode 100644 index 0000000..9f40bc4 --- /dev/null +++ b/UI/CustomGenerator/GeneralSettings.ts @@ -0,0 +1,84 @@ +import {UIElement} from "../UIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; +import Combine from "../Base/Combine"; +import SettingsTable from "./SettingsTable"; +import SingleSetting from "./SingleSetting"; +import {TextField} from "../Input/TextField"; +import MultiLingualTextFields from "../Input/MultiLingualTextFields"; + + +export default class GeneralSettingsPanel extends UIElement { + private panel: Combine; + + public languages : UIEventSource; + + constructor(configuration: UIEventSource, currentSetting: UIEventSource>) { + super(undefined); + + + const languagesField = new TextField( + { + fromString: str => str?.split(";")?.map(str => str.trim().toLowerCase()), + toString: languages => languages.join(";"), + } + ); + this.languages = languagesField.GetValue(); + + const version = TextField.StringInput(); + const current_datetime = new Date(); + let formatted_date = current_datetime.getFullYear() + "-" + (current_datetime.getMonth() + 1) + "-" + current_datetime.getDate() + " " + current_datetime.getHours() + ":" + current_datetime.getMinutes() + ":" + current_datetime.getSeconds() + version.GetValue().setData(formatted_date); + + + const locationRemark = "
    Note that, as soon as an URL-parameter sets the location or a location is known due to a previous visit, that the theme-set location is ignored" + + const settingsTable = new SettingsTable( + [ + new SingleSetting(configuration, TextField.StringInput(), "id", + "Identifier", "The identifier of this theme. This should be a lowercase, unique string"), + new SingleSetting(configuration, version, "version", "Version", + "A version to indicate the theme version. Ideal is the date you created or updated the theme"), + new SingleSetting(configuration, languagesField, "language", + "Supported languages", "Which languages do you want to support in this theme? Type the two letter code representing your language, seperated by ;. For example:en;nl "), + new SingleSetting(configuration, new MultiLingualTextFields(this.languages), "title", + "Title", "The title as shown in the welcome message, in the browser title bar, in the more screen, ..."), + new SingleSetting(configuration, new MultiLingualTextFields(this.languages, true), + "description", "Description", "The description is shown in the welcomemessage. It is a small text welcoming users"), + new SingleSetting(configuration, TextField.StringInput(), "icon", + "Icon", "A visual representation for your theme; used as logo in the welcomeMessage. If your theme is official, used as favicon and webapp logo", + { + showIconPreview: true + }), + + new SingleSetting(configuration, TextField.NumberInput("nat", n => n < 23), "startZoom","Initial zoom level", + "When a user first loads MapComplete, this zoomlevel is shown."+locationRemark), + new SingleSetting(configuration, TextField.NumberInput("float", n => (n < 90 && n > -90)), "startLat","Initial latitude", + "When a user first loads MapComplete, this latitude is shown as location."+locationRemark), + new SingleSetting(configuration, TextField.NumberInput("float", n => (n < 180 && n > -180)), "startLon","Initial longitude", + "When a user first loads MapComplete, this longitude is shown as location."+locationRemark), + + new SingleSetting(configuration, TextField.NumberInput("pfloat", n => (n < 0.5 )), "widenFactor","Query widening", + "When a query is run, the data within bounds of the visible map is loaded.\n" + + "However, users tend to pan and zoom a lot. It is pretty annoying if every single pan means a reloading of the data.\n" + + "For this, the bounds are widened in order to make a small pan still within bounds of the loaded data.\n" + + "IF widenfactor is 0, this feature is disabled. A recommended value is between 0.5 and 0.01 (the latter for very dense queries)"), + + new SingleSetting(configuration, TextField.StringInput(), "socialImage", + "og:image (aka Social Image)", "Only works on incorporated themes" + + "The Social Image is set as og:image for the HTML-site and helps social networks to show a preview", {showIconPreview: true}) + ], currentSetting); + + this.panel = new Combine([ + "

    General theme settings

    ", + settingsTable + ]); + } + + + InnerRender(): string { + return this.panel.Render(); + } + + +} \ No newline at end of file diff --git a/UI/CustomGenerator/LayerPanel.ts b/UI/CustomGenerator/LayerPanel.ts new file mode 100644 index 0000000..0db2305 --- /dev/null +++ b/UI/CustomGenerator/LayerPanel.ts @@ -0,0 +1,87 @@ +import {UIElement} from "../UIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; +import SettingsTable from "./SettingsTable"; +import SingleSetting from "./SingleSetting"; +import {SubtleButton} from "../Base/SubtleButton"; +import Combine from "../Base/Combine"; +import {TextField} from "../Input/TextField"; +import {InputElement} from "../Input/InputElement"; +import MultiLingualTextFields from "../Input/MultiLingualTextFields"; +import {CheckBox} from "../Input/CheckBox"; +import {MultiTagInput} from "../Input/MultiTagInput"; + +/** + * Shows the configuration for a single layer + */ +export default class LayerPanel extends UIElement { + private _config: UIEventSource; + + private settingsTable: UIElement; + + private deleteButton: UIElement; + + constructor(config: UIEventSource, + languages: UIEventSource, + index: number, + currentlySelected: UIEventSource>) { + super(undefined); + this._config = config; + + const actualDeleteButton = new SubtleButton( + "./assets/delete.svg", + "Yes, delete this layer" + ).onClick(() => { + config.data.layers.splice(index, 1); + config.ping(); + }); + + this.deleteButton = new CheckBox( + new Combine( + [ + "

    Confirm layer deletion

    ", + new SubtleButton( + "./assets/close.svg", + "No, don't delete" + ), + "Deleting a layer can not be undone!", + actualDeleteButton + ] + ), + new SubtleButton( + "./assets/delete.svg", + "Remove this layer" + ) + ) + + function setting(input: InputElement, path: string | string[], name: string, description: string | UIElement): SingleSetting { + let pathPre = ["layers", index]; + if (typeof (path) === "string") { + pathPre.push(path); + } else { + pathPre = pathPre.concat(path); + } + + return new SingleSetting(config, input, pathPre, name, description); + } + + + this.settingsTable = new SettingsTable([ + setting(TextField.StringInput(), "id", "Id", "An identifier for this layer
    This should be a simple, lowercase, human readable string that is used to identify the layer."), + setting(new MultiLingualTextFields(languages), "title", "Title", "The human-readable name of this layer
    Used in the layer control panel and the 'Personal theme'"), + setting(new MultiLingualTextFields(languages, true), "description", "Description", "A description for this layer.
    Shown in the layer selections and in the personal theme"), + setting(new MultiTagInput(), "overpassTags","Overpass query", + new Combine(["The tags to load from overpass. ", MultiTagInput.tagExplanation])) + ], + currentlySelected + ) + ; + } + + InnerRender(): string { + return new Combine([ + this.settingsTable, + this.deleteButton + ]).Render(); + } +} \ No newline at end of file diff --git a/UI/CustomGenerator/SettingsTable.ts b/UI/CustomGenerator/SettingsTable.ts new file mode 100644 index 0000000..1d0b4eb --- /dev/null +++ b/UI/CustomGenerator/SettingsTable.ts @@ -0,0 +1,46 @@ +import SingleSetting from "./SingleSetting"; +import {UIElement} from "../UIElement"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import {InputElement} from "../Input/InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Combine from "../Base/Combine"; +import {VariableUiElement} from "../Base/VariableUIElement"; + +export default class SettingsTable extends UIElement { + + private _col1: UIElement[] = []; + private _col2: InputElement[] = []; + + public selectedSetting: UIEventSource>; + + constructor(elements: SingleSetting[], + currentSelectedSetting: UIEventSource>) { + super(undefined); + const self = this; + this.selectedSetting = currentSelectedSetting ?? new UIEventSource>(undefined); + for (const element of elements) { + let title: UIElement = new FixedUiElement(element._name); + this._col1.push(title); + this._col2.push(element._value); + element._value.IsSelected.addCallback(isSelected => { + if (isSelected) { + self.selectedSetting.setData(element); + } else if (self.selectedSetting.data === element) { + self.selectedSetting.setData(undefined); + } + }) + } + + } + + InnerRender(): string { + let html = ""; + + for (let i = 0; i < this._col1.length; i++) { + html += `${this._col1[i].Render()}${this._col2[i].Render()}` + } + + return `${html}
    `; + } + +} \ No newline at end of file diff --git a/UI/CustomGenerator/SharePanel.ts b/UI/CustomGenerator/SharePanel.ts new file mode 100644 index 0000000..9837565 --- /dev/null +++ b/UI/CustomGenerator/SharePanel.ts @@ -0,0 +1,39 @@ +import {UIElement} from "../UIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; +import Combine from "../Base/Combine"; +import {VariableUiElement} from "../Base/VariableUIElement"; + +export default class SharePanel extends UIElement { + private _config: UIEventSource; + + private _panel: UIElement; + + constructor(config: UIEventSource, liveUrl: UIEventSource) { + super(undefined); + this._config = config; + + const json = new VariableUiElement(config.map(config => { + return JSON.stringify(config, null, 2) + .replace(/\n/g, "
    ") + .replace(/ /g, " "); + })); + + + this._panel = new Combine([ + "

    share

    ", + "Share the following link with friends:
    ", + new VariableUiElement(liveUrl.map(url => `${url}`)), + "

    Json

    ", + "The json configuration is included for debugging purposes", + "
    ", + json, + "
    " + ]); + } + + InnerRender(): string { + return this._panel.Render(); + } + +} \ No newline at end of file diff --git a/UI/CustomGenerator/SingleSetting.ts b/UI/CustomGenerator/SingleSetting.ts new file mode 100644 index 0000000..3c4b684 --- /dev/null +++ b/UI/CustomGenerator/SingleSetting.ts @@ -0,0 +1,84 @@ +import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {InputElement} from "../Input/InputElement"; +import {UIElement} from "../UIElement"; +import Translations from "../i18n/Translations"; +import Combine from "../Base/Combine"; +import {VariableUiElement} from "../Base/VariableUIElement"; + +export default class SingleSetting { + public _value: InputElement; + public _name: string; + public _description: UIElement; + public _options: { showIconPreview?: boolean }; + + constructor(config: UIEventSource, + value: InputElement, + path: string | (string | number)[], + name: string, + description: string | UIElement, + options?: { + showIconPreview?: boolean + } + ) { + this._value = value; + this._name = name; + this._description = Translations.W(description); + + this._options = options ?? {}; + if (this._options.showIconPreview) { + this._description = new Combine([ + this._description, + "

    Icon preview

    ", + new VariableUiElement(this._value.GetValue().map(url => ``)) + ]); + } + + if(typeof (path) === "string"){ + path = [path]; + } + const lastPart = path[path.length - 1]; + path.splice(path.length - 1, 1); + + function assignValue(value) { + if (value === undefined) { + return; + } + // We have to rewalk every time as parts might be new + let configPart = config.data; + for (const pathPart of path) { + configPart = configPart[pathPart]; + if (configPart === undefined) { + console.warn("Lost the way for path ", path) + return; + } + } + configPart[lastPart] = value; + config.ping(); + } + + function loadValue() { + let configPart = config.data; + for (const pathPart of path) { + configPart = configPart[pathPart]; + if (configPart === undefined) { + return; + } + } + const loadedValue = configPart[lastPart]; + + if (loadedValue !== undefined) { + value.GetValue().setData(loadedValue); + } + } + loadValue(); + config.addCallback(() => loadValue()); + + value.GetValue().addCallback(assignValue); + assignValue(this._value.GetValue().data); + + + } + + +} \ No newline at end of file diff --git a/UI/Img.ts b/UI/Img.ts index 8a08a1b..beb8b57 100644 --- a/UI/Img.ts +++ b/UI/Img.ts @@ -37,35 +37,5 @@ export class Img { static openFilterButton: string = ` ` - - - static ornament = "\n" + - "image/svg+xml\n" + - " \n" + - " \n" + - " \n" + - " \n" + - "" - } diff --git a/UI/Input/DropDown.ts b/UI/Input/DropDown.ts index 3d71c0c..cc919bd 100644 --- a/UI/Input/DropDown.ts +++ b/UI/Input/DropDown.ts @@ -8,7 +8,9 @@ export class DropDown extends InputElement { private readonly _label: UIElement; private readonly _values: { value: T; shown: UIElement }[]; - private readonly _value : UIEventSource; + private readonly _value: UIEventSource; + + public IsSelected: UIEventSource = new UIEventSource(false); constructor(label: string | UIElement, values: { value: T, shown: string | UIElement }[], @@ -17,8 +19,8 @@ export class DropDown extends InputElement { this._value = value ?? new UIEventSource(undefined); this._label = Translations.W(label); this._values = values.map(v => { - return { - value: v.value, + return { + value: v.value, shown: Translations.W(v.shown) } } @@ -36,14 +38,6 @@ export class DropDown extends InputElement { GetValue(): UIEventSource { return this._value; } - - ShowValue(t: T): boolean { - if (!this.IsValid(t)) { - return false; - } - this._value.setData(t); - } - IsValid(t: T): boolean { for (const value of this._values) { if (value.value === t) { diff --git a/UI/Input/FixedInputElement.ts b/UI/Input/FixedInputElement.ts index 8ce4657..6a19a1a 100644 --- a/UI/Input/FixedInputElement.ts +++ b/UI/Input/FixedInputElement.ts @@ -4,8 +4,9 @@ import {FixedUiElement} from "../Base/FixedUiElement"; import {UIEventSource} from "../../Logic/UIEventSource"; export class FixedInputElement extends InputElement { - private rendering: UIElement; - private value: UIEventSource; + private readonly rendering: UIElement; + private readonly value: UIEventSource; + public readonly IsSelected : UIEventSource = new UIEventSource(false); constructor(rendering: UIElement | string, value: T) { super(undefined); @@ -16,11 +17,6 @@ export class FixedInputElement extends InputElement { GetValue(): UIEventSource { return this.value; } - - ShowValue(t: T): boolean { - return false; - } - InnerRender(): string { return this.rendering.Render(); } @@ -28,7 +24,14 @@ export class FixedInputElement extends InputElement { IsValid(t: T): boolean { return t == this.value.data; } - - + + protected InnerUpdate(htmlElement: HTMLElement) { + super.InnerUpdate(htmlElement); + const self = this; + htmlElement.addEventListener("mouseenter", () => self.IsSelected.setData(true)); + htmlElement.addEventListener("mouseout", () => self.IsSelected.setData(false)) + + } + } \ No newline at end of file diff --git a/UI/Input/InputElement.ts b/UI/Input/InputElement.ts index e9f7507..0131688 100644 --- a/UI/Input/InputElement.ts +++ b/UI/Input/InputElement.ts @@ -1,10 +1,10 @@ import {UIElement} from "../UIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; import {UIEventSource} from "../../Logic/UIEventSource"; + export abstract class InputElement extends UIElement{ abstract GetValue() : UIEventSource; - + abstract IsSelected: UIEventSource; abstract IsValid(t: T) : boolean; } diff --git a/UI/Input/MultiLingualTextFields.ts b/UI/Input/MultiLingualTextFields.ts new file mode 100644 index 0000000..244ed6f --- /dev/null +++ b/UI/Input/MultiLingualTextFields.ts @@ -0,0 +1,93 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {TextField} from "./TextField"; + +export default class MultiLingualTextFields extends InputElement { + private _fields: Map> = new Map>(); + private _value: UIEventSource; + IsSelected: UIEventSource = new UIEventSource(false); + + constructor(languages: UIEventSource, + textArea: boolean = false, + value: UIEventSource>> = undefined) { + super(undefined); + this._value = value ?? new UIEventSource({}); + const self = this; + + function setup(languages: string[]) { + if(languages === undefined){ + return; + } + const newFields = new Map>(); + for (const language of languages) { + if(language.length != 2){ + continue; + } + + let oldField = self._fields.get(language); + if (oldField === undefined) { + oldField = TextField.StringInput(textArea); + oldField.GetValue().addCallback(str => { + self._value.data[language] = str; + self._value.ping(); + }); + oldField.GetValue().setData(self._value.data[language]); + + oldField.IsSelected.addCallback(() => { + let selected = false; + self._fields.forEach(value => {selected = selected || value.IsSelected.data}); + self.IsSelected.setData(selected); + }) + + } + newFields.set(language, oldField); + } + self._fields = newFields; + self.Update(); + + + } + + setup(languages.data); + languages.addCallback(setup); + + + function load(latest: any){ + if(latest === undefined){ + return; + } + for (const lang in latest) { + self._fields.get(lang)?.GetValue().setData(latest[lang]); + } + } + this._value.addCallback(load); + load(this._value.data); + } + + protected InnerUpdate(htmlElement: HTMLElement) { + super.InnerUpdate(htmlElement); + this._fields.forEach(value => value.Update()); + } + + GetValue(): UIEventSource>> { + return this._value; + } + + InnerRender(): string { + let html = ""; + this._fields.forEach((field, lang) => { + html += `${lang}${field.Render()}` + }) + if(html === ""){ + return "Please define one or more languages" + } + + return `${html}
    `; + } + + + IsValid(t: any): boolean { + return true; + } + +} \ No newline at end of file diff --git a/UI/Input/MultiTagInput.ts b/UI/Input/MultiTagInput.ts new file mode 100644 index 0000000..af9095c --- /dev/null +++ b/UI/Input/MultiTagInput.ts @@ -0,0 +1,92 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {UIElement} from "../UIElement"; +import Combine from "../Base/Combine"; +import {SubtleButton} from "../Base/SubtleButton"; +import TagInput from "./TagInput"; +import {FixedUiElement} from "../Base/FixedUiElement"; + +export class MultiTagInput extends InputElement { + + public static tagExplanation: UIElement = + new FixedUiElement("

    How to use the tag-element

    ") + + private readonly _value: UIEventSource; + IsSelected: UIEventSource; + private elements: UIElement[] = []; + private inputELements: InputElement[] = []; + private addTag: UIElement; + + constructor(value: UIEventSource = new UIEventSource([])) { + super(undefined); + this._value = value; + + this.addTag = new SubtleButton("./assets/addSmall.svg", "Add a tag") + .SetClass("small-button") + .onClick(() => { + this.IsSelected.setData(true); + value.data.push(""); + value.ping(); + }); + const self = this; + value.map((tags: string[]) => tags.length).addCallback(() => self.createElements()); + this.createElements(); + + + this._value.addCallback(tags => self.load(tags)); + this.IsSelected = new UIEventSource(false); + } + + private load(tags: string[]) { + if (tags === undefined) { + return; + } + for (let i = 0; i < tags.length; i++) { + console.log("Setting tag ", i) + this.inputELements[i].GetValue().setData(tags[i]); + } + } + + private UpdateIsSelected(){ + this.IsSelected.setData(this.inputELements.map(input => input.IsSelected.data).reduce((a,b) => a && b)) + } + + private createElements() { + this.inputELements = []; + this.elements = []; + for (let i = 0; i < this._value.data.length; i++) { + let tag = this._value.data[i]; + const input = new TagInput(new UIEventSource(tag)); + input.GetValue().addCallback(tag => { + console.log("Writing ", tag) + this._value.data[i] = tag; + this._value.ping(); + } + ); + this.inputELements.push(input); + input.IsSelected.addCallback(() => this.UpdateIsSelected()); + const deleteBtn = new FixedUiElement("") + .onClick(() => { + this._value.data.splice(i, 1); + this._value.ping(); + }); + this.elements.push(new Combine([input, deleteBtn, "
    "]).SetClass("tag-input-row")) + } + + this.Update(); + } + + InnerRender(): string { + return new Combine([...this.elements, this.addTag]).SetClass("bordered").Render(); + } + + + IsValid(t: string[]): boolean { + return false; + } + + GetValue(): UIEventSource { + return this._value; + } + +} \ No newline at end of file diff --git a/UI/Input/TagInput.ts b/UI/Input/TagInput.ts new file mode 100644 index 0000000..f70b97e --- /dev/null +++ b/UI/Input/TagInput.ts @@ -0,0 +1,107 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {DropDown} from "./DropDown"; +import {TextField} from "./TextField"; +import Combine from "../Base/Combine"; +import {Utils} from "../../Utils"; + +export default class SingleTagInput extends InputElement { + + private readonly _value: UIEventSource; + IsSelected: UIEventSource; + + private key: InputElement; + private value: InputElement; + private operator: DropDown + + constructor(value: UIEventSource = undefined) { + super(undefined); + this._value = value ?? new UIEventSource(undefined); + + this.key = new TextField({ + placeholder: "key", + fromString: str => { + if (str?.match(/^[a-zA-Z][a-zA-Z0-9:]*$/)) { + return str; + } + return undefined + }, + toString: str => str + }); + + this.value = new TextField({ + placeholder: "value - if blank, matches if key is NOT present", + fromString: str => str, + toString: str => str + } + ); + this.operator = new DropDown("", [ + {value: "=", shown: "="}, + {value: "~", shown: "~"}, + {value: "!~", shown: "!~"} + ]); + this.operator.GetValue().setData("="); + + const self = this; + + function updateValue() { + if (self.key.GetValue().data === undefined || + self.value.GetValue().data === undefined || + self.operator.GetValue().data === undefined) { + return undefined; + } + self._value.setData(self.key.GetValue().data + self.operator.GetValue().data + self.value.GetValue().data); + } + + this.key.GetValue().addCallback(() => updateValue()); + this.operator.GetValue().addCallback(() => updateValue()); + this.value.GetValue().addCallback(() => updateValue()); + + + function loadValue(value: string) { + if (value === undefined) { + return; + } + let parts: string[]; + if (value.indexOf("=") >= 0) { + parts = Utils.SplitFirst(value, "="); + self.operator.GetValue().setData("="); + } else if (value.indexOf("!~") > 0) { + parts = Utils.SplitFirst(value, "!~"); + self.operator.GetValue().setData("!~"); + + } else if (value.indexOf("~") > 0) { + parts = Utils.SplitFirst(value, "~"); + self.operator.GetValue().setData("~"); + } else { + console.warn("Invalid value for tag: ", value) + return; + } + self.key.GetValue().setData(parts[0]); + self.value.GetValue().setData(parts[1]); + } + + self._value.addCallback(loadValue); + loadValue(self._value.data); + this.IsSelected = this.key.IsSelected.map( + isSelected => isSelected || this.value.IsSelected.data, [this.value.IsSelected] + ) + } + + IsValid(t: string): boolean { + return false; + } + + InnerRender(): string { + return new Combine([ + this.key, this.operator, this.value + ]).Render(); + } + + + GetValue(): UIEventSource { + return this._value; + } + + +} \ No newline at end of file diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index 4b6572f..6cc6eec 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -14,7 +14,7 @@ export class ValidatedTextField { "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)), - "pfloat": (str) => !isNaN(Number(str)) && Number(str) > 0, + "pfloat": (str) => !isNaN(Number(str)) && Number(str) >= 0, "email": (str) => EmailValidator.validate(str), "url": (str) => str, "phone": (str, country) => { @@ -32,6 +32,33 @@ export class ValidatedTextField { export class TextField extends InputElement { + public static StringInput(textArea: boolean = false): TextField { + return new TextField({ + toString: str => str, + fromString: str => str, + textArea: textArea + }); + } + + public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined) : TextField{ + const isValid = ValidatedTextField.inputValidation[type]; + extraValidation = extraValidation ?? (() => true) + return new TextField({ + fromString: str => { + if(!isValid(str)){ + return undefined; + } + const n = Number(str); + if(!extraValidation(n)){ + return undefined; + } + return n; + }, + toString: num => ""+num, + placeholder: type + }); + } + private readonly value: UIEventSource; private readonly mappedValue: UIEventSource; @@ -40,7 +67,8 @@ export class TextField extends InputElement { private readonly _fromString?: (string: string) => T; private readonly _toString: (t: T) => string; private readonly startValidated: boolean; - + public readonly IsSelected: UIEventSource = new UIEventSource(false); + private readonly _isArea: boolean; constructor(options: { /** @@ -61,11 +89,12 @@ export class TextField extends InputElement { fromString: (string: string) => T, value?: UIEventSource, startValidated?: boolean, + textArea?: boolean }) { super(undefined); const self = this; this.value = new UIEventSource(""); - + this._isArea = options.textArea ?? false; this.mappedValue = options?.value ?? new UIEventSource(undefined); this.mappedValue.addCallback(() => self.InnerUpdate()); @@ -98,6 +127,11 @@ export class TextField extends InputElement { return this.mappedValue; } InnerRender(): string { + + if(this._isArea){ + return `` + } + return `
    ` + `` + `
    `; @@ -112,7 +146,7 @@ export class TextField extends InputElement { this.mappedValue.addCallback((data) => { field.className = data !== undefined ? "valid" : "invalid"; }); - + field.className = this.mappedValue.data !== undefined ? "valid" : "invalid"; const self = this; @@ -121,6 +155,9 @@ export class TextField extends InputElement { self.value.setData(field.value); }; + field.addEventListener("focusin", () => self.IsSelected.setData(true)); + field.addEventListener("focusout", () => self.IsSelected.setData(false)); + field.addEventListener("keyup", function (event) { if (event.key === "Enter") { // @ts-ignore diff --git a/UI/MoreScreen.ts b/UI/MoreScreen.ts index 96a51e5..440f385 100644 --- a/UI/MoreScreen.ts +++ b/UI/MoreScreen.ts @@ -23,6 +23,10 @@ export class MoreScreen extends UIElement { if (layout === undefined) { return undefined; } + if(layout.id === undefined){ + console.error("ID is undefined for layout",layout); + return undefined; + } if (layout.hideFromOverview) { if (State.state.osmConnection.GetPreference("hidden-theme-" + layout.id + "-enabled").data !== "true") { return undefined; diff --git a/UI/UIElement.ts b/UI/UIElement.ts index 83a6ab6..88037cb 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -17,7 +17,7 @@ export abstract class UIElement extends UIEventSource{ */ public static runningFromConsole = false; - protected constructor(source: UIEventSource) { + protected constructor(source: UIEventSource = undefined) { super(""); this.id = "ui-element-" + UIElement.nextId; this._source = source; @@ -146,6 +146,15 @@ export abstract class UIElement extends UIEventSource{ if (this.clss.indexOf(clss) < 0) { this.clss.push(clss); } + this.Update(); + return this; + } + + public RemoveClass(clss: string): UIElement { + if (this.clss.indexOf(clss) >= 0) { + this.clss = this.clss.splice(this.clss.indexOf(clss), 1); + } + this.Update(); return this; } diff --git a/UI/UserBadge.ts b/UI/UserBadge.ts index a30a55d..b73e267 100644 --- a/UI/UserBadge.ts +++ b/UI/UserBadge.ts @@ -8,6 +8,7 @@ import {State} from "../State"; import {Utils} from "../Utils"; import {UIEventSource} from "../Logic/UIEventSource"; import {SubtleButton} from "./Base/SubtleButton"; +import {InitUiElements} from "../InitUiElements"; /** * Handles and updates the user badge @@ -23,7 +24,7 @@ export class UserBadge extends UIElement { constructor() { super(State.state.osmConnection.userDetails); this._userDetails = State.state.osmConnection.userDetails; - this._languagePicker = Utils.CreateLanguagePicker(); + this._languagePicker = InitUiElements.CreateLanguagePicker(); this._loginButton = Translations.t.general.loginWithOpenStreetMap .Clone() .SetClass("userbadge-login") diff --git a/UI/WelcomeMessage.ts b/UI/WelcomeMessage.ts index 6f220c5..a60c20a 100644 --- a/UI/WelcomeMessage.ts +++ b/UI/WelcomeMessage.ts @@ -1,10 +1,10 @@ -import {UIElement} from "../UI/UIElement"; +import {UIElement} from "./UIElement"; import Locale from "../UI/i18n/Locale"; import {State} from "../State"; import {Layout} from "../Customizations/Layout"; import Translations from "./i18n/Translations"; -import {Utils} from "../Utils"; import Combine from "./Base/Combine"; +import {InitUiElements} from "../InitUiElements"; export class WelcomeMessage extends UIElement { @@ -19,7 +19,7 @@ export class WelcomeMessage extends UIElement { constructor() { super(State.state.osmConnection.userDetails); this.ListenTo(Locale.language); - this.languagePicker = Utils.CreateLanguagePicker(Translations.t.general.pickLanguage); + this.languagePicker = InitUiElements.CreateLanguagePicker(Translations.t.general.pickLanguage); function fromLayout(f: (layout: Layout) => (string | UIElement)): UIElement { return Translations.W(f(State.state.layoutToUse.data)); diff --git a/UI/i18n/Translations.ts b/UI/i18n/Translations.ts index 3ea5f34..a0218e1 100644 --- a/UI/i18n/Translations.ts +++ b/UI/i18n/Translations.ts @@ -760,14 +760,6 @@ export default class Translations { fr: "{name} (vend des vélos)", gl: "{name} (vende bicicletas)" }), - }, - drinking_water: { - title: new T({ - en: 'Drinking water', - nl: "Drinkbaar water", - fr: "Eau potable", - gl: "Auga potábel" - }) } }, diff --git a/Utils.ts b/Utils.ts index d7830ee..d090c54 100644 --- a/Utils.ts +++ b/Utils.ts @@ -1,7 +1,4 @@ import {UIElement} from "./UI/UIElement"; -import {DropDown} from "./UI/Input/DropDown"; -import {State} from "./State"; -import Locale from "./UI/i18n/Locale"; export class Utils { @@ -25,7 +22,7 @@ export class Utils { } static DoEvery(millis: number, f: (() => void)) { - if (State.runningFromConsole) { + if (UIElement.runningFromConsole) { return; } window.setTimeout( @@ -58,14 +55,6 @@ export class Utils { return ls; } - public static CreateLanguagePicker(label: string | UIElement = "") { - - return new DropDown(label, State.state.layoutToUse.data.supportedLanguages.map(lang => { - return {value: lang, shown: lang} - } - ), Locale.language); - } - public static EllipsesAfter(str : string, l : number = 100){ if(str.length <= l){ return str; @@ -96,5 +85,13 @@ export class Utils { } return t; } + + public static SplitFirst(a: string, sep: string):string[]{ + const index = a.indexOf(sep); + if(index < 0){ + return [a]; + } + return [a.substr(0, index), a.substr(index+sep.length)]; + } } diff --git a/assets/addSmall.svg b/assets/addSmall.svg new file mode 100644 index 0000000..59051c8 --- /dev/null +++ b/assets/addSmall.svg @@ -0,0 +1,288 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/arrow-down.svg b/assets/arrow-down.svg deleted file mode 100644 index 66a60a1..0000000 --- a/assets/arrow-down.svg +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/assets/arrow-left-smooth.svg b/assets/arrow-left-smooth.svg deleted file mode 100644 index 1139447..0000000 --- a/assets/arrow-left-smooth.svg +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/assets/arrow-right-both.svg b/assets/arrow-right-both.svg deleted file mode 100644 index 39a3fa9..0000000 --- a/assets/arrow-right-both.svg +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - diff --git a/assets/arrow-right-go-black.svg b/assets/arrow-right-go-black.svg deleted file mode 100644 index b91e3ba..0000000 --- a/assets/arrow-right-go-black.svg +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - diff --git a/assets/arrow-right-sharp.svg b/assets/arrow-right-sharp.svg deleted file mode 100644 index 49d1810..0000000 --- a/assets/arrow-right-sharp.svg +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/assets/arrow-right-smooth.svg b/assets/arrow-right-smooth.svg deleted file mode 100644 index 7cc17c6..0000000 --- a/assets/arrow-right-smooth.svg +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/assets/arrow-up.svg b/assets/arrow-up.svg deleted file mode 100644 index 14094c7..0000000 --- a/assets/arrow-up.svg +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/assets/floppy.svg b/assets/floppy.svg new file mode 100644 index 0000000..9eae121 --- /dev/null +++ b/assets/floppy.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/layers.svg b/assets/layers.svg new file mode 100644 index 0000000..8af7fc7 --- /dev/null +++ b/assets/layers.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/layers/drinking_water/drinking_water.json b/assets/layers/drinking_water/drinking_water.json new file mode 100644 index 0000000..2221d72 --- /dev/null +++ b/assets/layers/drinking_water/drinking_water.json @@ -0,0 +1,58 @@ +{ + "id": "drinking_water", + "name": { + "en": "Drinking water", + "nl": "Drinkbaar water", + "fr": "Eau potable", + "gl": "Auga potábel" + }, + "title": { + "en": "Drinking water", + "nl": "Drinkbaar water", + "fr": "Eau potable", + "gl": "Auga potábel" + }, + "icon": "./assets/layers/drinking_water/drinking_water.svg", + "iconSize": "40,40,bottom", + "overpassTags": "amenity=drinking_water", + "minzoom": 13, + "wayHandling": 1, + "presets": [ + { + "title": { + "en": "Drinking water", + "nl": "Drinkbaar water", + "fr": "Eau potable", + "gl": "Auga potábel" + }, + "tags": ["amenity=drinking_water"] + } + ], + "color": "#00bb00", + "tagRenderings": [ + "images", + { + "question": { + "en": "How easy is it to fill water bottles?", + "nl": "Hoe gemakkelijk is het om drinkbussen bij te vullen?" + }, + "mappings": [ + { + "if": "bottle=yes", + "then": { + "en": "It is easy to refill water bottles", + "nl": "Een drinkbus bijvullen gaat makkelijk" + } + }, + { + "if": "bottle=no", + "then": { + "en": "Water bottles may not fit", + "nl": "Een drinkbus past moeilijk" + } + } + ] + } + ] +} + diff --git a/assets/bike/drinking_water.svg b/assets/layers/drinking_water/drinking_water.svg similarity index 100% rename from assets/bike/drinking_water.svg rename to assets/layers/drinking_water/drinking_water.svg diff --git a/assets/layers/ghost_bike/ghost_bike.json b/assets/layers/ghost_bike/ghost_bike.json new file mode 100644 index 0000000..63fa857 --- /dev/null +++ b/assets/layers/ghost_bike/ghost_bike.json @@ -0,0 +1,83 @@ +{ + "id": "ghost_bike", + "name": { + "en": "Ghost bikes", + "nl": "Witte Fietsen" + }, + "overpassTags": "memorial=ghost_bike", + "minzoom": 0, + "title": { + "render": { + "en": "Ghost bike", + "nl": "Witte Fiets" + }, + "mappings": [ + { + "if": "name~*", + "then": { + "en": "Ghost bike in the remembrance of {name}", + "nl": "Witte fiets ter nagedachtenis van {name}" + } + } + ] + }, + "icon": "./assets/layers/ghost_bike/ghost_bike.svg", + "iconSize": "40,40,bottom", + "width": "5", + "color": "#000", + "wayHandling": 1, + "presets": [ + { + "title": { + "en": "Ghost bike", + "nl": "Witte fiets" + }, + "tags": [ + "historic=memorial", + "memorial=ghost_bike" + ] + } + ], + "tagRenderings": [ + { + "render": { + "en": "A ghost bike is a memorial for a cyclist who died in a traffic accident, in the form of a white bicycle placed permanently near the accident location.", + "nl": "Een Witte Fiets (of Spookfiets) is een aandenken aan een fietser die bij een verkeersongeval om het leven kwam. Het gaat over een witgeschilderde fiets die geplaatst werd in de buurt van het ongeval." + } + }, + "images", + { + "question": { + "en": "Whom is remembered by this ghost bike?
    Please respect privacy - only fill out the name if it is widely published or marked on the cycle. Opt to leave out the family name.
    ", + "nl": "Aan wie is deze witte fiets een eerbetoon??
    Respecteer privacy - voeg enkel een naam toe indien die op de fiets staat of gepubliceerd is. Eventueel voeg je enkel de voornaam toe.
    " + }, + "render": { + "en": "In remembrance of {name}", + "nl": "Ter nagedachtenis van {name}" + }, + "mappings": [ + { + "if": "noname=yes", + "then": { + "en": "No name is marked on the bike", + "nl": "De naam is niet aangeduid op de fiets" + } + } + ] + }, + { + "question": { + "en": "On what webpage can one find more information about the Ghost bike or the accident?", + "nl": "Op welke website kan men meer informatie vinden over de Witte fiets of over het ongeval?" + }, + "render": { + "en": "More information is available", + "nl": "Meer informatie" + }, + "freeform": { + "type": "url", + "key": "source" + } + } + ] +} \ No newline at end of file diff --git a/assets/layers/ghost_bike/ghost_bike.svg b/assets/layers/ghost_bike/ghost_bike.svg new file mode 100644 index 0000000..93a4b2a --- /dev/null +++ b/assets/layers/ghost_bike/ghost_bike.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/layers/viewpoint/viewpoint.json b/assets/layers/viewpoint/viewpoint.json new file mode 100644 index 0000000..fd41dac --- /dev/null +++ b/assets/layers/viewpoint/viewpoint.json @@ -0,0 +1,46 @@ +{ + "id": "viewpoint", + "name": { + "en": "Viewpoint", + "nl": "Uitzicht" + }, + "description": { + "en": "A nice viewpoint or nice view. Ideal to add an image if no other category fits", + "nl": "Een mooi uitzicht - ideaal om een foto toe te voegen wanneer iets niet in een andere categorie past" + }, + "overpassTags": "tourism=viewpoint", + "minzoom": 14, + "icon": "./assets/layers/viewpoint/viewpoint.svg", + "iconSize": "20,20,center", + "color": "#ffffff", + "width": "5", + "wayhandling": 2, + "presets": [ + { + "title": { + "en": "Viewpoint", + "nl": "Uitzicht" + }, + "tags": [ + "tourism=viewpoint" + ] + } + ], + "title": { + "en": "Viewpoint", + "nl": "Uitzicht" + }, + "tagRenderings": [ + "images", + { + "question": { + "nl": "Zijn er bijzonderheden die je wilt toevoegen?", + "en": "Do you want to add a description?" + }, + "render": "{description}", + "freeform": { + "key": "description" + } + } + ] +} \ No newline at end of file diff --git a/assets/viewpoint.svg b/assets/layers/viewpoint/viewpoint.svg similarity index 100% rename from assets/viewpoint.svg rename to assets/layers/viewpoint/viewpoint.svg diff --git a/assets/parking.svg b/assets/parking.svg deleted file mode 100644 index 07cb1be..0000000 --- a/assets/parking.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - Svg Vector Icons : http://www.onlinewebfonts.com/icon - - \ No newline at end of file diff --git a/assets/statue.svg b/assets/statue.svg deleted file mode 100644 index ddf4019..0000000 --- a/assets/statue.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - diff --git a/assets/Buurtnatuur-Welkom.txt b/assets/themes/buurtnatuur/Buurtnatuur-Welkom.txt similarity index 100% rename from assets/Buurtnatuur-Welkom.txt rename to assets/themes/buurtnatuur/Buurtnatuur-Welkom.txt diff --git a/assets/themes/buurtnatuur/buurtnatuur.be.json b/assets/themes/buurtnatuur/buurtnatuur.be.json deleted file mode 100644 index 7a73a41..0000000 --- a/assets/themes/buurtnatuur/buurtnatuur.be.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/assets/groen.svg b/assets/themes/buurtnatuur/groen_logo.svg similarity index 100% rename from assets/groen.svg rename to assets/themes/buurtnatuur/groen_logo.svg diff --git a/assets/BuurtnatuurFront.jpg b/assets/themes/buurtnatuur/social_image.jpg similarity index 100% rename from assets/BuurtnatuurFront.jpg rename to assets/themes/buurtnatuur/social_image.jpg diff --git a/assets/themes/cyclestreets/cyclestreets.json b/assets/themes/cyclestreets/cyclestreets.json index e235228..d7f151c 100644 --- a/assets/themes/cyclestreets/cyclestreets.json +++ b/assets/themes/cyclestreets/cyclestreets.json @@ -11,19 +11,35 @@ "maintainer": "MapComlete", "widenfactor": 0.05, "roamingRenderings": [ + "pictures", { "question": "Is deze straat een fietsstraat?", "mappings": [ { - "then": "Deze straat is een fietsstraat", - "if": "cyclestreet=yes&proposed:cyclestreet!~*" + "if": { + "and": [ + "cyclestreet=yes", + "proposed:cyclestreet=" + ] + }, + "then": "Deze straat is een fietsstraat" }, { - "then": "Deze straat wordt binnenkort een fietsstraat", - "if": "proposed:cyclestreet=yes&cyclestreet!~*" + "if": { + "and": [ + "cyclestreet=", + "proposed:cyclestreet=yes" + ] + }, + "then": "Deze straat wordt binnenkort een fietsstraat" }, { - "if": "cyclestreet!~*&proposed:cyclestreet!~*", + "if": { + "and": [ + "cyclestreet=", + "proposed:cyclestreet=" + ] + }, "then": "Deze straat is geen fietsstraat" } ] @@ -31,6 +47,7 @@ { "question": "Wanneer wordt deze straat een fietsstraat?", "render": "Deze straat wordt fietsstraat op {cyclestreet:start_date}", + "condition": "proposed:cyclestreet=yes", "freeform": { "type": "date", "key": "cyclestreet:start_date" @@ -42,7 +59,12 @@ "id": "fietsstraat", "name": "Fietsstraten", "minzoom": 9, - "overpassTags": "cyclestreet=yes", + "overpassTags": { + "and": [ + "cyclestreet=yes", + "traffic_sign=" + ] + }, "description": "Een fietsstraat is een straat waar gemotoriseerd verkeer een fietser niet mag inhalen.", "title": "{name}", "icon": "./assets/themes/cyclestreets/F111.svg", @@ -74,14 +96,14 @@ "name": "Alle straten", "description": "Laag waar je een straat als fietsstraat kan markeren", "overpassTags": "highway~residential|tertiary|unclassified", - "minzoom": "18", + "minzoom": 18, "wayHandling": 0, "title": { "render": "Straat", "mappings": [ { - "then": "{name}", - "if": "name~*" + "if": "name~*", + "then": "{name}" } ] }, diff --git a/assets/themes/ghostbikes/ghostbikes.json b/assets/themes/ghostbikes/ghostbikes.json new file mode 100644 index 0000000..1f10e53 --- /dev/null +++ b/assets/themes/ghostbikes/ghostbikes.json @@ -0,0 +1,20 @@ +{ + "id": "ghostbikes", + "maintainer":"MapComplete", + "version": "2020-08-30", + "language": ["en","nl"], + "title": { + "en": "Ghost bikes", + "nl": "Witte Fietsen" + }, + "description": { + "en": "A ghost bike is a memorial for a cyclist who died in a traffic accident, in the form of a white bicycle placed permanently near the accident location.

    On this map, one can see all the ghost bikes which are known by OpenStreetMap. Is a ghost bike missing? Everyone can add or update information here - you only need to have a (free) OpenStreetMap account.", + "nl": "Een Witte Fiets of Spookfiets is een aandenken aan een fietser die bij een verkeersongeval om het leven kwam. Het gaat om een fiets die volledig wit is geschilderd en in de buurt van het ongeval werd geinstalleerd.

    Op deze kaart zie je alle witte fietsen die door OpenStreetMap gekend zijn. Ontbreekt er een Witte Fiets of wens je informatie aan te passen? Meld je dan aan met een (gratis) OpenStreetMap account." + }, + "icon": "./assets/layers/ghost_bike/ghost_bike.svg", + "startZoom": 1, + "startLat": 0, + "startLon": 0, + "widenFactor": 0.1, + "layers": ["ghost_bike"] +} \ No newline at end of file diff --git a/assets/wrench.svg b/assets/wrench.svg deleted file mode 100644 index dd729c2..0000000 --- a/assets/wrench.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/customGenerator.html b/customGenerator.html index 65daad9..d7fe549 100644 --- a/customGenerator.html +++ b/customGenerator.html @@ -1,11 +1,11 @@ - + Custom Theme Generator for Mapcomplete