diff --git a/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts index b0dd76a79..b5165c4ef 100644 --- a/Customizations/AllKnownLayouts.ts +++ b/Customizations/AllKnownLayouts.ts @@ -14,9 +14,9 @@ import {NatureReserves} from "./Layers/NatureReserves"; import {Natuurpunt} from "./Layouts/Natuurpunt"; export class AllKnownLayouts { - public static allSets: any = AllKnownLayouts.AllLayouts(); + public static allSets = AllKnownLayouts.AllLayouts(); - private static AllLayouts(): any { + private static AllLayouts(): Map { const all = new All(); const layouts: Layout[] = [ new Groen(), @@ -32,7 +32,7 @@ export class AllKnownLayouts { new Statues(), */ ]; - const allSets = {}; + const allSets: Map = new Map(); for (const layout of layouts) { allSets[layout.name] = layout; all.layers = all.layers.concat(layout.layers); diff --git a/Customizations/LayerDefinition.ts b/Customizations/LayerDefinition.ts index 41ef0095e..ce3fbe6ab 100644 --- a/Customizations/LayerDefinition.ts +++ b/Customizations/LayerDefinition.ts @@ -15,7 +15,7 @@ export class LayerDefinition { /** * This name is shown in the 'add XXX button' */ - name: string; + name: string | UIElement; /** * These tags are added whenever a new point is added by the user on the map. * This is the ideal place to add extra info, such as "fixme=added by MapComplete, geometry should be checked" @@ -72,7 +72,15 @@ export class LayerDefinition { */ maxAllowedOverlapPercentage: number = undefined; - + /** + * If true, then ways (and polygons) will be converted to a 'point' at the center instead before further processing + */ + wayHandling: number = 0; + + static WAYHANDLING_DEFAULT = 0; + static WAYHANDLING_CENTER_ONLY = 1; + static WAYHANDLING_CENTER_AND_WAY = 2; + constructor(options: { name: string, newElementTags: Tag[], @@ -82,13 +90,13 @@ export class LayerDefinition { title?: TagRenderingOptions, elementsToShow?: TagDependantUIElementConstructor[], maxAllowedOverlapPercentage?: number, + wayHandling?: number, style?: (tags: any) => { color: string, icon: any } } = undefined) { if (options === undefined) { - console.log("No options!") return; } this.name = options.name; @@ -100,11 +108,12 @@ export class LayerDefinition { this.title = options.title; this.elementsToShow = options.elementsToShow; this.style = options.style; - console.log(this) + this.wayHandling = options.wayHandling ?? LayerDefinition.WAYHANDLING_DEFAULT; } - asLayer(basemap: Basemap, allElements: ElementStorage, changes: Changes, userDetails: UIEventSource, selectedElement: UIEventSource, - showOnPopup: (tags: UIEventSource<(any)>) => UIElement): + asLayer(basemap: Basemap, allElements: ElementStorage, changes: Changes, userDetails: UIEventSource, + selectedElement: UIEventSource<{feature: any}>, + showOnPopup: (tags: UIEventSource<(any)>, feature: any) => UIElement): FilteredLayer { return new FilteredLayer( this, diff --git a/Customizations/Layers/BikeParkings.ts b/Customizations/Layers/BikeParkings.ts index ccd737edb..3f07869fb 100644 --- a/Customizations/Layers/BikeParkings.ts +++ b/Customizations/Layers/BikeParkings.ts @@ -5,12 +5,15 @@ import * as L from "leaflet"; import FixedText from "../Questions/FixedText"; import ParkingType from "../Questions/bike/ParkingType"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; +import BikeStationOperator from "../Questions/bike/StationOperator"; +import Translations from "../../UI/i18n/Translations"; +import ParkingOperator from "../Questions/bike/ParkingOperator"; export default class BikeParkings extends LayerDefinition { constructor() { super(); - this.name = "bike_parking"; + this.name = Translations.t.cyclofix.parking.name; this.icon = "./assets/bike/parking.svg"; this.overpassFilter = new Tag("amenity", "bicycle_parking"); this.newElementTags = [ @@ -20,12 +23,13 @@ export default class BikeParkings extends LayerDefinition { this.minzoom = 13; this.style = this.generateStyleFunction(); - this.title = new FixedText("Fietsparking"); + this.title = new FixedText(Translations.t.cyclofix.parking.title) this.elementsToShow = [ new ImageCarouselWithUploadConstructor(), - new OperatorTag(), + //new ParkingOperator(), new ParkingType() ]; + this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY; } @@ -36,7 +40,8 @@ export default class BikeParkings extends LayerDefinition { color: "#00bb00", icon: L.icon({ iconUrl: self.icon, - iconSize: [50, 50] + iconSize: [50, 50], + iconAnchor: [25,50] }) }; }; diff --git a/Customizations/Layers/BikeShops.ts b/Customizations/Layers/BikeShops.ts new file mode 100644 index 000000000..0cec27fe0 --- /dev/null +++ b/Customizations/Layers/BikeShops.ts @@ -0,0 +1,82 @@ +import { LayerDefinition } from "../LayerDefinition"; +import Translations from "../../UI/i18n/Translations"; +import {And, Tag} from "../../Logic/TagsFilter"; +import FixedText from "../Questions/FixedText"; +import { ImageCarouselWithUploadConstructor } from "../../UI/Image/ImageCarouselWithUpload"; +import * as L from "leaflet"; +import ShopRetail from "../Questions/bike/ShopRetail"; +import ShopPump from "../Questions/bike/ShopPump"; +import ShopRental from "../Questions/bike/ShopRental"; +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 { TagRenderingOptions } from "../TagRendering"; + + +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(); + this.name = Translations.t.cyclofix.shop.name + this.icon = "./assets/bike/repair_shop.svg" + this.overpassFilter = new Tag("shop", "bicycle"); + this.newElementTags = [ + new Tag("shop", "bicycle"), + ] + this.maxAllowedOverlapPercentage = 10 + this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY + + this.minzoom = 13; + this.style = this.generateStyleFunction(); + this.title = new TagRenderingOptions({ + mappings: [ + {k: new And([new Tag("name", "*"), this.sellsBikes]), txt: Translations.t.cyclofix.shop.titleShopNamed}, + { + k: new And([new Tag("name", "*"), new Tag("service:bicycle:retail", "")]), + txt: Translations.t.cyclofix.shop.titleShop + }, + { + k: new And([new Tag("name", "*"), new Tag("service:bicycle:retail", "no")]), + txt: Translations.t.cyclofix.shop.titleRepairNamed + }, + {k: this.sellsBikes, txt: Translations.t.cyclofix.shop.titleShop}, + {k: new Tag("service:bicycle:retail", " "), txt: Translations.t.cyclofix.shop.title}, + {k: new Tag("service:bicycle:retail", "no"), txt: Translations.t.cyclofix.shop.titleRepair}, + ] + }) + + this.elementsToShow = [ + new ImageCarouselWithUploadConstructor(), + new ShopName(), + new ShopRetail(), + new ShopRental(), + new ShopRepair(), + new ShopPump(), + new ShopDiy(), + new ShopSecondHand() + ] + } + + private generateStyleFunction() { + const self = this; + return function (tags: any) { + let icon = "assets/bike/repair_shop.svg"; + + if (self.sellsBikes.matchesProperties(tags)) { + icon = "assets/bike/shop.svg"; + } + + return { + color: "#00bb00", + icon: L.icon({ + iconUrl: self.icon, + iconSize: [50, 50], + iconAnchor: [25, 50] + }) + } + } + } +} diff --git a/Customizations/Layers/BikeStations.ts b/Customizations/Layers/BikeStations.ts index 89f79b372..2fd80c2b5 100644 --- a/Customizations/Layers/BikeStations.ts +++ b/Customizations/Layers/BikeStations.ts @@ -12,6 +12,7 @@ import PumpManometer from "../Questions/bike/PumpManometer"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import PumpOperational from "../Questions/bike/PumpOperational"; import PumpValves from "../Questions/bike/PumpValves"; +import Translations from "../../UI/i18n/Translations"; export default class BikeStations extends LayerDefinition { @@ -22,7 +23,7 @@ export default class BikeStations extends LayerDefinition { constructor() { super(); - this.name = "bike station or pump"; + this.name = Translations.t.cyclofix.station.name; this.icon = "./assets/wrench.svg"; this.overpassFilter = new And([ @@ -36,7 +37,8 @@ export default class BikeStations extends LayerDefinition { this.minzoom = 13; this.style = this.generateStyleFunction(); - this.title = new FixedText("Bike station"); + this.title = new FixedText(Translations.t.cyclofix.station.title) + this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY this.elementsToShow = [ new ImageCarouselWithUploadConstructor(), @@ -50,7 +52,7 @@ export default class BikeStations extends LayerDefinition { new PumpValves().OnlyShowIf(this.pump), new PumpOperational().OnlyShowIf(this.pump), - new BikeStationOperator(), + // new BikeStationOperator(), // new BikeStationBrand() DISABLED ]; } @@ -61,26 +63,26 @@ export default class BikeStations extends LayerDefinition { const hasPump = self.pump.matchesProperties(properties) const isOperational = self.pumpOperationalOk.matchesProperties(properties) const hasTools = self.tools.matchesProperties(properties) - let iconName = "" - if (hasPump) { - if (hasTools) { - iconName = "repair_station_pump.svg" + let iconName = "repair_station.svg"; + if (hasTools && hasPump && isOperational) { + iconName = "repair_station_pump.svg" + }else if(hasTools){ + iconName = "repair_station.svg" + }else if(hasPump){ + if (isOperational) { + iconName = "pump.svg" } else { - if (isOperational) { - iconName = "pump.svg" - } else { - iconName = "pump_broken.svg" - } + iconName = "broken_pump.svg" } - } else { - iconName = "repair_station.svg" } + const iconUrl = `./assets/bike/${iconName}` return { color: "#00bb00", icon: L.icon({ iconUrl: iconUrl, - iconSize: [50, 50] + iconSize: [50, 50], + iconAnchor: [25,50] }) }; }; diff --git a/Customizations/Layers/Bookcases.ts b/Customizations/Layers/Bookcases.ts index 25953f44c..7a2311c7a 100644 --- a/Customizations/Layers/Bookcases.ts +++ b/Customizations/Layers/Bookcases.ts @@ -1,10 +1,8 @@ import {LayerDefinition} from "../LayerDefinition"; import L from "leaflet"; -import {And, Or, Regex, Tag} from "../../Logic/TagsFilter"; -import {QuestionDefinition} from "../../Logic/Question"; +import {And, Or, Tag} from "../../Logic/TagsFilter"; import {TagRenderingOptions} from "../TagRendering"; import {NameInline} from "../Questions/NameInline"; -import {NameQuestion} from "../Questions/NameQuestion"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; export class Bookcases extends LayerDefinition { @@ -121,7 +119,10 @@ export class Bookcases extends LayerDefinition { key: "ref", template: "Het referentienummer is $$$", renderTemplate: "Gekend als {brand} {ref}" - } + }, + mappings: [ + {k: new And([new Tag("brand",""), new Tag("nobrand","yes"), new Tag("ref", "")]), txt: "Maakt geen deel uit van een groter netwerk"} + ] }).OnlyShowIf(new Tag("brand","*")), new TagRenderingOptions({ diff --git a/Customizations/Layers/DrinkingWater.ts b/Customizations/Layers/DrinkingWater.ts index 92e633894..1dd0c8906 100644 --- a/Customizations/Layers/DrinkingWater.ts +++ b/Customizations/Layers/DrinkingWater.ts @@ -10,8 +10,8 @@ export class DrinkingWater extends LayerDefinition { constructor() { super(); - this.name = "drinking_water"; - this.icon = "./assets/bug.svg"; + this.name = "drinking water"; + this.icon = "./assets/bike/drinking_water.svg"; this.overpassFilter = new Or([ new And([ @@ -24,6 +24,7 @@ export class DrinkingWater extends LayerDefinition { new Tag("amenity", "drinking_water"), ]; this.maxAllowedOverlapPercentage = 10; + this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY this.minzoom = 13; this.style = this.generateStyleFunction(); @@ -52,7 +53,8 @@ export class DrinkingWater extends LayerDefinition { color: "#00bb00", icon: new L.icon({ iconUrl: self.icon, - iconSize: [40, 40] + iconSize: [50, 50], + iconAnchor: [25,50] }) }; }; diff --git a/Customizations/Layers/NatureReserves.ts b/Customizations/Layers/NatureReserves.ts index 6adde3134..a0a1d94dd 100644 --- a/Customizations/Layers/NatureReserves.ts +++ b/Customizations/Layers/NatureReserves.ts @@ -25,6 +25,13 @@ export class NatureReserves extends LayerDefinition { this.style = this.generateStyleFunction(); this.elementsToShow = [ new ImageCarouselWithUploadConstructor(), + new TagRenderingOptions({ + freeform: { + key: "_surface", + renderTemplate: "{_surface}m²", + template: "$$$" + } + }), new NameQuestion(), new AccessTag(), new OperatorTag(), diff --git a/Customizations/Layout.ts b/Customizations/Layout.ts index 7c7292790..eb13be3da 100644 --- a/Customizations/Layout.ts +++ b/Customizations/Layout.ts @@ -1,20 +1,30 @@ import {LayerDefinition} from "./LayerDefinition"; +import {UIElement} from "../UI/UIElement"; +import {FixedUiElement} from "../UI/Base/FixedUiElement"; +import Translation from "../UI/i18n/Translation"; +import Translations from "../UI/i18n/Translations"; +import Locale from "../UI/i18n/Locale"; +import {VariableUiElement} from "../UI/Base/VariableUIElement"; +import {OsmConnection, UserDetails} from "../Logic/OsmConnection"; +import {UIEventSource} from "../UI/UIEventSource"; /** * A layout is a collection of settings of the global view (thus: welcome text, title, selection of layers). */ export class Layout { + public name: string; - public title: string; + public title: UIElement; public layers: LayerDefinition[]; - public welcomeMessage: string; - public gettingStartedPlzLogin: string; - public welcomeBackMessage: string; + public welcomeMessage: UIElement; + public gettingStartedPlzLogin: UIElement; + public welcomeBackMessage: UIElement; + public welcomeTail: UIElement; public startzoom: number; + public supportedLanguages: string[]; public startLon: number; public startLat: number; - public welcomeTail: string; public locationContains: string[]; @@ -33,26 +43,79 @@ export class Layout { */ constructor( name: string, - title: string, + supportedLanguages: string[], + title: UIElement | string, layers: LayerDefinition[], startzoom: number, startLat: number, startLon: number, - welcomeMessage: string, - gettingStartedPlzLogin: string = "Please login to get started", - welcomeBackMessage: string = "You are logged in. Welcome back!", - welcomeTail: string = "" + welcomeMessage: UIElement | string, + gettingStartedPlzLogin: UIElement | string = Translations.t.general.getStarted, + welcomeBackMessage: UIElement | string = Translations.t.general.welcomeBack, + welcomeTail: UIElement | string = "" ) { - this.title = title; + this.supportedLanguages = supportedLanguages; + this.title = typeof (title) === 'string' ? new FixedUiElement(title) : title; this.startLon = startLon; this.startLat = startLat; this.startzoom = startzoom; this.name = name; this.layers = layers; - this.welcomeMessage = welcomeMessage; - this.gettingStartedPlzLogin = gettingStartedPlzLogin; - this.welcomeBackMessage = welcomeBackMessage; - this.welcomeTail = welcomeTail; + this.welcomeMessage = Translations.W(welcomeMessage) + this.gettingStartedPlzLogin = Translations.W(gettingStartedPlzLogin); + this.welcomeBackMessage = Translations.W(welcomeBackMessage); + this.welcomeTail = Translations.W(welcomeTail); + } + + +} + +export class WelcomeMessage extends UIElement { + private readonly layout: Layout; + private readonly userDetails: UIEventSource; + private osmConnection: OsmConnection; + + private readonly description: UIElement; + private readonly plzLogIn: UIElement; + private readonly welcomeBack: UIElement; + private readonly tail: UIElement; + + + constructor(layout: Layout, osmConnection: OsmConnection) { + super(osmConnection.userDetails); + this.ListenTo(Locale.language); + this.osmConnection = osmConnection; + this.layout = layout; + this.userDetails = osmConnection.userDetails; + + this.description = layout.welcomeMessage; + console.log(" >>>>",this.description, "DESCR ") + this.plzLogIn = layout.gettingStartedPlzLogin; + this.welcomeBack = layout.welcomeBackMessage; + this.tail = layout.welcomeTail; + } + + InnerRender(): string { + return "
" + + this.description.Render() + + "
"+ + (this.userDetails.data.loggedIn ? this.welcomeBack : this.plzLogIn).Render() + + "
"+ + this.tail.Render() + + "
" + + ; + /* + return new VariableUiElement( + this.userDetails.map((userdetails) => { + }), + function () { + + }).ListenTo(Locale.language);*/ + } + + protected InnerUpdate(htmlElement: HTMLElement) { + this.osmConnection.registerActivateOsmAUthenticationClass() } } diff --git a/Customizations/Layouts/All.ts b/Customizations/Layouts/All.ts index a415c2ea0..54065b4b6 100644 --- a/Customizations/Layouts/All.ts +++ b/Customizations/Layouts/All.ts @@ -4,6 +4,7 @@ export class All extends Layout{ constructor() { super( "all", + ["en"], "All quest layers", [], 15, diff --git a/Customizations/Layouts/Bookcases.ts b/Customizations/Layouts/Bookcases.ts index 833150b30..d833d6edd 100644 --- a/Customizations/Layouts/Bookcases.ts +++ b/Customizations/Layouts/Bookcases.ts @@ -4,6 +4,7 @@ import * as Layer from "../Layers/Bookcases"; export class Bookcases extends Layout{ constructor() { super( "bookcases", + ["nl"], "Open Bookcase Map", [new Layer.Bookcases()], 14, diff --git a/Customizations/Layouts/Cyclofix.ts b/Customizations/Layouts/Cyclofix.ts index 548981ba4..82d56dde8 100644 --- a/Customizations/Layouts/Cyclofix.ts +++ b/Customizations/Layouts/Cyclofix.ts @@ -1,30 +1,30 @@ import {Layout} from "../Layout"; import BikeParkings from "../Layers/BikeParkings"; import BikeServices from "../Layers/BikeStations"; -import {GhostBike} from "../Layers/GhostBike"; -import {DrinkingWater, DrinkingWaterLayer} from "../Layers/DrinkingWater"; +import BikeShops from "../Layers/BikeShops"; +import Translations from "../../UI/i18n/Translations"; +import {DrinkingWater} from "../Layers/DrinkingWater"; +import Combine from "../../UI/Base/Combine"; export default class Cyclofix extends Layout { constructor() { super( "pomp", - "Cyclofix bicycle infrastructure", - [new GhostBike(), new BikeServices(), new BikeParkings(), new DrinkingWater()], + ["en", "nl", "fr"], + Translations.t.cyclofix.title, + [new BikeServices(), new BikeShops(), new DrinkingWater(), new BikeParkings()], 16, 50.8465573, 4.3516970, - - - "

Cyclofix bicycle infrastructure

\n" + - "\n" + - "

EN> On this map we want to collect data about the whereabouts of bicycle pumps and public racks in Brussels." + - "As a result, cyclists will be able to quickly find the nearest infrastructure for their needs.

" + - "

NL> Op deze kaart willen we gegevens verzamelen over de locatie van fietspompen en openbare stelplaatsen in Brussel." + - "Hierdoor kunnen fietsers snel de dichtstbijzijnde infrastructuur vinden die voldoet aan hun behoeften.

" + - "

FR> Sur cette carte, nous voulons collecter des données sur la localisation des pompes à vélo et des supports publics à Bruxelles." + - "Les cyclistes pourront ainsi trouver rapidement l'infrastructure la plus proche de leurs besoins.

" - , - "", ""); + /* Translations.t.cyclofix.title/*/ + new Combine([ + "

", + Translations.t.cyclofix.title, + "


", + Translations.t.cyclofix.description, + "

" + ])//*/ + ); } } diff --git a/Customizations/Layouts/GRB.ts b/Customizations/Layouts/GRB.ts index 93514a1de..21ba2ab4b 100644 --- a/Customizations/Layouts/GRB.ts +++ b/Customizations/Layouts/GRB.ts @@ -4,6 +4,7 @@ import {GrbToFix} from "../Layers/GrbToFix"; export class GRB extends Layout { constructor() { super("grb", + ["en"], "Grb import fix tool", [new GrbToFix()], 15, diff --git a/Customizations/Layouts/Groen.ts b/Customizations/Layouts/Groen.ts index 7f3d0f934..991a61893 100644 --- a/Customizations/Layouts/Groen.ts +++ b/Customizations/Layouts/Groen.ts @@ -7,6 +7,7 @@ export class Groen extends Layout { constructor() { super("buurtnatuur", + ["nl"], "Buurtnatuur", [new NatureReserves(), new Park(), new Bos()], 10, diff --git a/Customizations/Layouts/MetaMap.ts b/Customizations/Layouts/MetaMap.ts index 8302f985d..c174bc6db 100644 --- a/Customizations/Layouts/MetaMap.ts +++ b/Customizations/Layouts/MetaMap.ts @@ -5,6 +5,7 @@ import {Map} from "../Layers/Map"; export class MetaMap extends Layout{ constructor() { super( "metamap", + ["en"], "Open Map Map", [new Map()], 1, diff --git a/Customizations/Layouts/Natuurpunt.ts b/Customizations/Layouts/Natuurpunt.ts index 8216fcb7e..78c0c7a0c 100644 --- a/Customizations/Layouts/Natuurpunt.ts +++ b/Customizations/Layouts/Natuurpunt.ts @@ -7,6 +7,7 @@ export class Natuurpunt extends Layout{ constructor() { super( "natuurpunt", + ["nl"], "De natuur in", [new Birdhide(), new InformationBoard(), new NatureReserves(true)], 12, diff --git a/Customizations/Layouts/Statues.ts b/Customizations/Layouts/Statues.ts index 76a46dd26..d355fa4cf 100644 --- a/Customizations/Layouts/Statues.ts +++ b/Customizations/Layouts/Statues.ts @@ -5,6 +5,7 @@ export class Statues extends Layout{ constructor() { super( "statues", "Open Artwork Map", + ["en"], [new Artwork()], 10, 50.8435, diff --git a/Customizations/Layouts/StreetWidth.ts b/Customizations/Layouts/StreetWidth.ts index f93e8a1ad..4d72b6e3a 100644 --- a/Customizations/Layouts/StreetWidth.ts +++ b/Customizations/Layouts/StreetWidth.ts @@ -7,6 +7,7 @@ export class StreetWidth extends Layout{ constructor() { super( "width", + ["nl"], "Straatbreedtes in Brugge", [new Widths( 2, diff --git a/Customizations/Layouts/Toilets.ts b/Customizations/Layouts/Toilets.ts index c6c585e90..1598c0f5a 100644 --- a/Customizations/Layouts/Toilets.ts +++ b/Customizations/Layouts/Toilets.ts @@ -4,6 +4,7 @@ import * as Layer from "../Layers/Toilets"; export class Toilets extends Layout{ constructor() { super( "toilets", + ["en"], "Open Toilet Map", [new Layer.Toilets()], 12, diff --git a/Customizations/Layouts/WalkByBrussels.ts b/Customizations/Layouts/WalkByBrussels.ts index ca8fdc4b4..499b7740f 100644 --- a/Customizations/Layouts/WalkByBrussels.ts +++ b/Customizations/Layouts/WalkByBrussels.ts @@ -6,6 +6,7 @@ import { Park } from "../Layers/Park"; export class WalkByBrussels extends Layout { constructor() { super("walkbybrussels", + ["en","fr","nl"], "Drinking Water Spots", [new DrinkingWater(), new Park(), new NatureReserves()], 10, diff --git a/Customizations/Questions/FixedText.ts b/Customizations/Questions/FixedText.ts index 178fb53f4..d3f51001d 100644 --- a/Customizations/Questions/FixedText.ts +++ b/Customizations/Questions/FixedText.ts @@ -1,7 +1,8 @@ import { TagRenderingOptions } from "../TagRendering"; +import {UIElement} from "../../UI/UIElement"; export default class FixedText extends TagRenderingOptions { - constructor(category: string) { + constructor(category: string | UIElement) { super({ mappings: [ { diff --git a/Customizations/Questions/NameInline.ts b/Customizations/Questions/NameInline.ts index 83458a5b7..5c9d4e69a 100644 --- a/Customizations/Questions/NameInline.ts +++ b/Customizations/Questions/NameInline.ts @@ -1,5 +1,6 @@ import {TagRenderingOptions} from "../TagRendering"; import {And, Tag} from "../../Logic/TagsFilter"; +import {UIElement} from "../../UI/UIElement"; export class NameInline extends TagRenderingOptions{ @@ -8,7 +9,7 @@ export class NameInline extends TagRenderingOptions{ return string.charAt(0).toUpperCase() + string.slice(1); } - constructor(category: string) { + constructor(category: string ) { super({ question: "", diff --git a/Customizations/Questions/bike/ParkingOperator.ts b/Customizations/Questions/bike/ParkingOperator.ts new file mode 100644 index 000000000..251bcb502 --- /dev/null +++ b/Customizations/Questions/bike/ParkingOperator.ts @@ -0,0 +1,27 @@ +import {TagRenderingOptions} from "../../TagRendering"; +import {Tag, And} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; + + +export default class ParkingOperator extends TagRenderingOptions { + constructor() { + const to = Translations.t.cyclofix.parking.operator + super({ + priority: 15, + question: to.question.Render(), + freeform: { + key: "operator", + template: to.template, + renderTemplate: to.render, + placeholder: Translations.t.cyclofix.freeFormPlaceholder + }, + mappings: [ + {k: new Tag("operator", "KU Leuven"), txt: "KU Leuven"}, + {k: new Tag("operator", "Stad Halle"), txt: "Stad Halle"}, + {k: new Tag("operator", "Saint Gilles - Sint Gillis"), txt: "Saint Gilles - Sint Gillis"}, + {k: new Tag("operator", "Jette"), txt: "Jette"}, + {k: new And([new Tag("operator", ""), new Tag("operator:type", "private")]), txt: to.private.Render()} + ] + }); + } +} diff --git a/Customizations/Questions/bike/ParkingType.ts b/Customizations/Questions/bike/ParkingType.ts index 4b5074ff1..fa8ab508a 100644 --- a/Customizations/Questions/bike/ParkingType.ts +++ b/Customizations/Questions/bike/ParkingType.ts @@ -1,38 +1,61 @@ import {TagRenderingOptions} from "../../TagRendering"; import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; +import Combine from "../../../UI/Base/Combine"; +class ParkingTypeHelper { + static GenerateMappings() { + const images = { + stands: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dc/Bike_racks_at_north-west_of_Westfield_-_geograph.org.uk_-_1041057.jpg/100px-Bike_racks_at_north-west_of_Westfield_-_geograph.org.uk_-_1041057.jpg", + wall_loops: "https://wiki.openstreetmap.org/w/images/thumb/c/c2/Bike-parking-wheelbender.jpg/100px-Bike-parking-wheelbender.jpg", + handlebar_holder: "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Bicycle_parking_handlebar_holder.jpg/100px-Bicycle_parking_handlebar_holder.jpg", + shed: "https://wiki.openstreetmap.org/w/images/thumb/b/b2/Bike-shelter.jpg/100px-Bike-shelter.jpg", + rack: "https://wiki.openstreetmap.org/w/images/thumb/4/41/Triton_Bike_Rack.png/100px-Triton_Bike_Rack.png", + "two-tier": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Bicis_a_l%27estaci%C3%B3_de_Leiden.JPG/100px-Bicis_a_l%27estaci%C3%B3_de_Leiden.JPG" + }; + + + const toImg = (url) => `` + const mappings = []; + const to = Translations.t.cyclofix.parking.type + + for (const imagesKey in images) { + const mapping = + { + k: new Tag("bicycle_parking", imagesKey), + txt: new Combine([ + to[imagesKey], + to.eg, + toImg(images[imagesKey]) + ]) + }; + + mappings.push(mapping); + + } + + return mappings; + } +} export default class ParkingType extends TagRenderingOptions { - private static images = { - stands: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dc/Bike_racks_at_north-west_of_Westfield_-_geograph.org.uk_-_1041057.jpg/100px-Bike_racks_at_north-west_of_Westfield_-_geograph.org.uk_-_1041057.jpg", - wall_loops: "https://wiki.openstreetmap.org/w/images/thumb/c/c2/Bike-parking-wheelbender.jpg/100px-Bike-parking-wheelbender.jpg", - handlebar_holder: "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Bicycle_parking_handlebar_holder.jpg/100px-Bicycle_parking_handlebar_holder.jpg", - shed: "https://wiki.openstreetmap.org/w/images/thumb/b/b2/Bike-shelter.jpg/100px-Bike-shelter.jpg", - "two-tier": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Bicis_a_l%27estaci%C3%B3_de_Leiden.JPG/100px-Bicis_a_l%27estaci%C3%B3_de_Leiden.JPG" - } - - private static toImgTxt(url: string) { - return `` - } - constructor() { + + const to = Translations.t.cyclofix.parking.type + + super({ priority: 5, - question: "Van welk type is deze fietsenparking?", + question: to.question, freeform: { key: "bicycle_parking", extraTags: new Tag("fixme", "Freeform bicycle_parking= tag used: possibly a wrong value"), - template: "Iets anders: $$$", - renderTemplate: "Dit is een fietsenparking van het type: {bicycle_parking}", - placeholder: "Specifieer" + template: to.template.txt, + renderTemplate: to.render.txt, + placeholder: Translations.t.cyclofix.freeFormPlaceholder, }, - mappings: [ - {k: new Tag("bicycle_parking", "stands"), txt: ParkingType.toImgTxt(ParkingType.images.stands)}, - {k: new Tag("bicycle_parking", "wall_loops"), txt: ParkingType.toImgTxt(ParkingType.images.wall_loops)}, - {k: new Tag("bicycle_parking", "handlebar_holder"), txt: ParkingType.toImgTxt(ParkingType.images.handlebar_holder)}, - {k: new Tag("bicycle_parking", "shed"), txt: ParkingType.toImgTxt(ParkingType.images.shed)}, - {k: new Tag("bicycle_parking", "two-tier"), txt: ParkingType.toImgTxt(ParkingType.images["two-tier"])} - ] + mappings: ParkingTypeHelper.GenerateMappings() + }); } } diff --git a/Customizations/Questions/bike/PumpManometer.ts b/Customizations/Questions/bike/PumpManometer.ts index bc97a032b..1ae416efd 100644 --- a/Customizations/Questions/bike/PumpManometer.ts +++ b/Customizations/Questions/bike/PumpManometer.ts @@ -1,16 +1,18 @@ import {TagRenderingOptions} from "../../TagRendering"; import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; export default class PumpManometer extends TagRenderingOptions { constructor() { + const to = Translations.t.cyclofix.station.manometer super({ - question: "Does the pump have a pressure indicator or manometer?", + question: to.question, mappings: [ - {k: new Tag("manometer", "yes"), txt: "Yes, there is a manometer"}, - {k: new Tag("manometer","broken"), txt: "Yes, but it is broken"}, - {k: new Tag("manometer", "yes"), txt: "No"} + {k: new Tag("manometer", "yes"), txt: to.yes}, + {k: new Tag("manometer", "no"), txt: to.no}, + {k: new Tag("manometer", "broken"), txt: to.broken} ] }); - } + } } diff --git a/Customizations/Questions/bike/PumpManual.ts b/Customizations/Questions/bike/PumpManual.ts index 18b79e68e..44aa4ee7d 100644 --- a/Customizations/Questions/bike/PumpManual.ts +++ b/Customizations/Questions/bike/PumpManual.ts @@ -1,15 +1,17 @@ import {TagRenderingOptions} from "../../TagRendering"; import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; export default class PumpManual extends TagRenderingOptions { constructor() { + const to = Translations.t.cyclofix.station.electric super({ priority: 5, - question: "Is this an electric bike pump?", + question: to.question, mappings: [ - {k: new Tag("manual", "yes"), txt: "Manual pump"}, - {k: new Tag("manual", "no"), txt: "Electric pump"} + {k: new Tag("manual", "yes"), txt: to.manual}, + {k: new Tag("manual", "no"), txt: to.electric} ] }); } diff --git a/Customizations/Questions/bike/PumpOperational.ts b/Customizations/Questions/bike/PumpOperational.ts index d7abe0f16..ac84f0c80 100644 --- a/Customizations/Questions/bike/PumpOperational.ts +++ b/Customizations/Questions/bike/PumpOperational.ts @@ -1,14 +1,16 @@ import {TagRenderingOptions} from "../../TagRendering"; import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; export default class PumpOperational extends TagRenderingOptions { constructor() { + const to = Translations.t.cyclofix.station.operational super({ - question: "Is the bicycle pump still operational?", + question: to.question, mappings: [ - {k: new Tag("service:bicycle:pump:operational_status","broken"), txt: "This pump is broken"}, - {k: new Tag("service:bicycle:pump:operational_status",""), txt: "This pump is operational"} + {k: new Tag("service:bicycle:pump:operational_status","broken"), txt: to.broken}, + {k: new Tag("service:bicycle:pump:operational_status",""), txt: to.operational} ] }); } diff --git a/Customizations/Questions/bike/PumpValves.ts b/Customizations/Questions/bike/PumpValves.ts index ae9ebb4bf..51247d052 100644 --- a/Customizations/Questions/bike/PumpValves.ts +++ b/Customizations/Questions/bike/PumpValves.ts @@ -1,24 +1,27 @@ import {TagRenderingOptions} from "../../TagRendering"; import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; export default class PumpValves extends TagRenderingOptions{ constructor() { + const to = Translations.t.cyclofix.station.valves super({ - question: "What valves are supported?", + question: to.question, mappings: [ { k: new Tag("valves", " sclaverand;schrader;dunlop"), - txt: "There is a default head, so Presta, Dunlop and Auto" + txt: to.default }, - {k: new Tag("valves", "dunlop"), txt: "Only dunlop"}, - {k: new Tag("valves", "sclaverand"), txt: "Only Sclaverand (also known as Dunlop)"}, - {k: new Tag("valves", "auto"), txt: "Only auto"}, + {k: new Tag("valves", "dunlop"), txt: to.dunlop}, + {k: new Tag("valves", "sclaverand"), txt: to.sclaverand}, + {k: new Tag("valves", "auto"), txt: to.auto}, ], freeform: { + extraTags: new Tag("fixme", "Freeform valves= tag used: possibly a wrong value"), key: "valves", - template: "Supported valves are $$$", - renderTemplate: "Supported valves are {valves}" + template: to.template, + renderTemplate: to.render } }); } diff --git a/Customizations/Questions/bike/ShopDiy.ts b/Customizations/Questions/bike/ShopDiy.ts new file mode 100644 index 000000000..48217314a --- /dev/null +++ b/Customizations/Questions/bike/ShopDiy.ts @@ -0,0 +1,19 @@ +import {TagRenderingOptions} from "../../TagRendering"; +import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; + + +export default class ShopPump extends TagRenderingOptions { + constructor() { + const key = 'service:bicycle:diy' + const to = Translations.t.cyclofix.shop.diy + super({ + priority: 5, + question: to.question, + mappings: [ + {k: new Tag(key, "yes"), txt: to.yes}, + {k: new Tag(key, "no"), txt: to.no}, + ] + }); + } +} diff --git a/Customizations/Questions/bike/ShopName.ts b/Customizations/Questions/bike/ShopName.ts new file mode 100644 index 000000000..f270268ab --- /dev/null +++ b/Customizations/Questions/bike/ShopName.ts @@ -0,0 +1,18 @@ +import {TagRenderingOptions} from "../../TagRendering"; +import Translations from "../../../UI/i18n/Translations"; + + +export default class ShopPump extends TagRenderingOptions { + constructor() { + const to = Translations.t.cyclofix.shop.qName + super({ + priority: 5, + question: to.question, + freeform: { + key: "name", + renderTemplate: to.render, + template: to.template + } + }) + } +} diff --git a/Customizations/Questions/bike/ShopPump.ts b/Customizations/Questions/bike/ShopPump.ts new file mode 100644 index 000000000..6ed14dabb --- /dev/null +++ b/Customizations/Questions/bike/ShopPump.ts @@ -0,0 +1,19 @@ +import {TagRenderingOptions} from "../../TagRendering"; +import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; + + +export default class ShopPump extends TagRenderingOptions { + constructor() { + const key = 'service:bicycle:pump' + const to = Translations.t.cyclofix.shop.pump + super({ + priority: 5, + question: to.question, + mappings: [ + {k: new Tag(key, "yes"), txt: to.yes}, + {k: new Tag(key, "no"), txt: to.no}, + ] + }); + } +} diff --git a/Customizations/Questions/bike/ShopRental.ts b/Customizations/Questions/bike/ShopRental.ts new file mode 100644 index 000000000..eac267774 --- /dev/null +++ b/Customizations/Questions/bike/ShopRental.ts @@ -0,0 +1,19 @@ +import {TagRenderingOptions} from "../../TagRendering"; +import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; + + +export default class ShopRental extends TagRenderingOptions { + constructor() { + const key = 'service:bicycle:rental' + const to = Translations.t.cyclofix.shop.rental + super({ + priority: 5, + question: to.question, + mappings: [ + {k: new Tag(key, "yes"), txt: to.yes}, + {k: new Tag(key, "no"), txt: to.no}, + ] + }); + } +} diff --git a/Customizations/Questions/bike/ShopRepair.ts b/Customizations/Questions/bike/ShopRepair.ts new file mode 100644 index 000000000..2a1a3759d --- /dev/null +++ b/Customizations/Questions/bike/ShopRepair.ts @@ -0,0 +1,21 @@ +import {TagRenderingOptions} from "../../TagRendering"; +import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; + + +export default class ShopRepair extends TagRenderingOptions { + constructor() { + const key = 'service:bicycle:repair' + const to = Translations.t.cyclofix.shop.repair + super({ + priority: 5, + question: to.question, + mappings: [ + {k: new Tag(key, "yes"), txt: to.yes}, + {k: new Tag(key, "only_sold"), txt: to.sold}, + {k: new Tag(key, "brand"), txt: to.brand}, + {k: new Tag(key, "no"), txt: to.no}, + ] + }); + } +} diff --git a/Customizations/Questions/bike/ShopRetail.ts b/Customizations/Questions/bike/ShopRetail.ts new file mode 100644 index 000000000..3ad3ea396 --- /dev/null +++ b/Customizations/Questions/bike/ShopRetail.ts @@ -0,0 +1,19 @@ +import {TagRenderingOptions} from "../../TagRendering"; +import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; + + +export default class ShopRetail extends TagRenderingOptions { + constructor() { + const key = 'service:bicycle:retail' + const to = Translations.t.cyclofix.shop.retail + super({ + priority: 5, + question: to.question, + mappings: [ + {k: new Tag(key, "yes"), txt: to.yes}, + {k: new Tag(key, "no"), txt: to.no}, + ] + }); + } +} diff --git a/Customizations/Questions/bike/ShopSecondHand.ts b/Customizations/Questions/bike/ShopSecondHand.ts new file mode 100644 index 000000000..b95da4929 --- /dev/null +++ b/Customizations/Questions/bike/ShopSecondHand.ts @@ -0,0 +1,20 @@ +import {TagRenderingOptions} from "../../TagRendering"; +import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; + + +export default class ShopPump extends TagRenderingOptions { + constructor() { + const key = 'service:bicycle:second_hand' + const to = Translations.t.cyclofix.shop.secondHand + super({ + priority: 5, + question: to.question, + mappings: [ + {k: new Tag(key, "yes"), txt: to.yes}, + {k: new Tag(key, "no"), txt: to.no}, + {k: new Tag(key, "only"), txt: to.only}, + ] + }); + } +} diff --git a/Customizations/Questions/bike/StationBrand.ts b/Customizations/Questions/bike/StationBrand.ts index 61dbc44db..b763bf650 100644 --- a/Customizations/Questions/bike/StationBrand.ts +++ b/Customizations/Questions/bike/StationBrand.ts @@ -4,6 +4,8 @@ import {Tag} from "../../../Logic/TagsFilter"; /** * Currently not used in Cyclofix because it's a little vague + * + * TODO: Translations */ export default class BikeStationBrand extends TagRenderingOptions { private static options = { diff --git a/Customizations/Questions/bike/StationChain.ts b/Customizations/Questions/bike/StationChain.ts index eb5c36ade..7930d3bc6 100644 --- a/Customizations/Questions/bike/StationChain.ts +++ b/Customizations/Questions/bike/StationChain.ts @@ -1,15 +1,17 @@ import {TagRenderingOptions} from "../../TagRendering"; import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; export default class StationChain extends TagRenderingOptions { constructor() { + const to = Translations.t.cyclofix.station.chain super({ priority: 5, - question: "Does this bike station have a special tool to repair your bike chain?", + question: to.question, mappings: [ - {k: new Tag("service:bicycle:chain_tool", "yes"), txt: "There is a chain tool."}, - {k: new Tag("service:bicycle:chain_tool", "no"), txt: "There is no chain tool."}, + {k: new Tag("service:bicycle:chain_tool", "yes"), txt: to.yes}, + {k: new Tag("service:bicycle:chain_tool", "no"), txt: to.no}, ] }); } diff --git a/Customizations/Questions/bike/StationOperator.ts b/Customizations/Questions/bike/StationOperator.ts index 001f920cf..5d09434e0 100644 --- a/Customizations/Questions/bike/StationOperator.ts +++ b/Customizations/Questions/bike/StationOperator.ts @@ -1,25 +1,27 @@ import {TagRenderingOptions} from "../../TagRendering"; import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; export default class BikeStationOperator extends TagRenderingOptions { constructor() { + const to = Translations.t.cyclofix.station.operator super({ priority: 15, - question: "Who operates this bike station (name of university, shop, city...)?", - freeform: { - key: "operator", - template: "This bike station is operated by $$$", - renderTemplate: "This bike station is operated by {operator}", - placeholder: "organisatie" - }, + question: to.question, mappings: [ {k: new Tag("operator", "KU Leuven"), txt: "KU Leuven"}, {k: new Tag("operator", "Stad Halle"), txt: "Stad Halle"}, {k: new Tag("operator", "Saint Gilles - Sint Gillis"), txt: "Saint Gilles - Sint Gillis"}, {k: new Tag("operator", "Jette"), txt: "Jette"}, - {k: new Tag("operator", "private"), txt: "Beheer door een privépersoon"} - ] + {k: new Tag("operator", "private"), txt: to.private} + ], + freeform: { + key: "operator", + template: to.template, + renderTemplate: to.render, + placeholder: "organisatie" + } }); } } diff --git a/Customizations/Questions/bike/StationPumpTools.ts b/Customizations/Questions/bike/StationPumpTools.ts index be81c8b27..5442eba51 100644 --- a/Customizations/Questions/bike/StationPumpTools.ts +++ b/Customizations/Questions/bike/StationPumpTools.ts @@ -1,16 +1,18 @@ import {TagRenderingOptions} from "../../TagRendering"; import {Tag, And} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; export default class BikeStationPumpTools extends TagRenderingOptions { constructor() { + const to = Translations.t.cyclofix.station.services super({ priority: 15, - question: "Which services are available at this bike station?", + question: to.question, mappings: [ - {k: new And([new Tag("service:bicycle:tools", "no"), new Tag("service:bicycle:pump", "yes")]), txt: "There is only a pump available."}, - {k: new And([new Tag("service:bicycle:tools", "yes"), new Tag("service:bicycle:pump", "no")]), txt: "There are only tools (screwdrivers, pliers...) available."}, - {k: new And([new Tag("service:bicycle:tools", "yes"), new Tag("service:bicycle:pump", "yes")]), txt: "There are both tools and a pump available."} + {k: new And([new Tag("service:bicycle:tools", "no"), new Tag("service:bicycle:pump", "yes")]), txt: to.pump}, + {k: new And([new Tag("service:bicycle:tools", "yes"), new Tag("service:bicycle:pump", "no")]), txt: to.tools}, + {k: new And([new Tag("service:bicycle:tools", "yes"), new Tag("service:bicycle:pump", "yes")]), txt: to.both} ] }); } diff --git a/Customizations/Questions/bike/StationStand.ts b/Customizations/Questions/bike/StationStand.ts index 0b6fce196..be6ac4a1e 100644 --- a/Customizations/Questions/bike/StationStand.ts +++ b/Customizations/Questions/bike/StationStand.ts @@ -1,14 +1,16 @@ import {TagRenderingOptions} from "../../TagRendering"; import {Tag} from "../../../Logic/TagsFilter"; +import Translations from "../../../UI/i18n/Translations"; export default class BikeStationStand extends TagRenderingOptions { constructor() { + const to = Translations super({ priority: 10, question: "Does this bike station have a hook to suspend your bike with or a stand to elevate it?", mappings: [ - {k: new Tag("service:bicycle:stand", "yes"), txt: "There is a hook or stand."}, + {k: new Tag("service:bicycle:stand", "yes"), txt: "There is a hook or stand"}, {k: new Tag("service:bicycle:stand", "no"), txt: "There is no hook or stand"}, ] }); diff --git a/Customizations/TagRendering.ts b/Customizations/TagRendering.ts index ea8ca3290..a426fa326 100644 --- a/Customizations/TagRendering.ts +++ b/Customizations/TagRendering.ts @@ -1,17 +1,20 @@ import {UIElement} from "../UI/UIElement"; import {UIEventSource} from "../UI/UIEventSource"; import {And, Tag, TagsFilter, TagUtils} from "../Logic/TagsFilter"; -import {UIRadioButton} from "../UI/Base/UIRadioButton"; import {FixedUiElement} from "../UI/Base/FixedUiElement"; import {SaveButton} from "../UI/SaveButton"; import {Changes} from "../Logic/Changes"; -import {TextField} from "../UI/Base/TextField"; -import {UIInputElement} from "../UI/Base/UIInputElement"; -import {UIRadioButtonWithOther} from "../UI/Base/UIRadioButtonWithOther"; import {VariableUiElement} from "../UI/Base/VariableUIElement"; import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor"; import {OnlyShowIfConstructor} from "./OnlyShowIf"; import {UserDetails} from "../Logic/OsmConnection"; +import {TextField} from "../UI/Input/TextField"; +import {InputElement} from "../UI/Input/InputElement"; +import {InputElementWrapper} from "../UI/Input/InputElementWrapper"; +import {FixedInputElement} from "../UI/Input/FixedInputElement"; +import {RadioButton} from "../UI/Input/RadioButton"; +import Translations from "../UI/i18n/Translations"; +import Locale from "../UI/i18n/Locale"; export class TagRenderingOptions implements TagDependantUIElementConstructor { @@ -20,8 +23,17 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { */ public options: { - priority?: number; question?: string; primer?: string; - freeform?: { key: string; tagsPreprocessor?: (tags: any) => any; template: string; renderTemplate: string; placeholder?: string; extraTags?: TagsFilter }; mappings?: { k: TagsFilter; txt: string; priority?: number, substitute?: boolean }[] + priority?: number; + question?: string | UIElement; + freeform?: { + key: string; + tagsPreprocessor?: (tags: any) => any; + template: string | UIElement; + renderTemplate: string | UIElement; + placeholder?: string | UIElement; + extraTags?: TagsFilter + }; + mappings?: { k: TagsFilter; txt: string | UIElement; priority?: number, substitute?: boolean }[] }; @@ -35,7 +47,7 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { * If 'question' is undefined, then the question is never asked at all * If the question is "" (empty string) then the question is */ - question?: string, + question?: UIElement | string, /** * What is the priority of the question. @@ -56,7 +68,7 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { * * */ - mappings?: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[], + mappings?: { k: TagsFilter, txt: UIElement | string, priority?: number, substitute?: boolean }[], /** @@ -65,19 +77,14 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { * In the question, it'll offer a textfield */ freeform?: { - key: string, template: string, - renderTemplate: string - placeholder?: string, + key: string, + template: string | UIElement, + renderTemplate: string | UIElement + placeholder?: string | UIElement, extraTags?: TagsFilter, }, - /** - * Optional: - * if defined, this a common piece of tag that is shown in front of every mapping (except freeform) - */ - primer?: string, - /** * In some very rare cases, tags have to be rewritten before displaying * This function can be used for that. @@ -85,6 +92,7 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { */ tagsPreprocessor?: ((tags: any) => void) }) { + this.options = options; } @@ -129,29 +137,24 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor { class TagRendering extends UIElement implements TagDependantUIElement { - private _priority: number; private _userDetails: UIEventSource; + private _priority: number; - Priority(): number { - return this._priority; - } - private _question: string; - private _primer: string; - private _mapping: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[]; - private _renderMapping: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[]; + private _question: UIElement; + private _mapping: { k: TagsFilter, txt: string | UIElement, priority?: number }[]; private _tagsPreprocessor?: ((tags: any) => any); private _freeform: { - key: string, template: string, - renderTemplate: string, - - placeholder?: string, + key: string, + template: string | UIElement, + renderTemplate: string | UIElement, + placeholder?: string | UIElement, extraTags?: TagsFilter }; - private readonly _questionElement: UIElement; - private readonly _textField: TextField; // Only here to update + + private readonly _questionElement: InputElement; private readonly _saveButton: UIElement; private readonly _skipButton: UIElement; @@ -165,19 +168,20 @@ class TagRendering extends UIElement implements TagDependantUIElement { constructor(tags: UIEventSource, changes: Changes, options: { priority?: number - question?: string, - primer?: string, + question?: string | UIElement, freeform?: { - key: string, template: string, - renderTemplate: string - placeholder?: string, + key: string, + template: string | UIElement, + renderTemplate: string | UIElement, + placeholder?: string | UIElement, extraTags?: TagsFilter, }, tagsPreprocessor?: ((tags: any) => any), - mappings?: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[] + mappings?: { k: TagsFilter, txt: string | UIElement, priority?: number, substitute?: boolean }[] }) { super(tags); + this.ListenTo(Locale.language); const self = this; this.ListenTo(this._questionSkipped); this.ListenTo(this._editMode); @@ -185,9 +189,10 @@ class TagRendering extends UIElement implements TagDependantUIElement { this._userDetails = changes.login.userDetails; this.ListenTo(this._userDetails); - this._question = options.question; + if (options.question !== undefined) { + this._question = Translations.W(options.question); + } this._priority = options.priority ?? 0; - this._primer = options.primer ?? ""; this._tagsPreprocessor = function (properties) { if (options.tagsPreprocessor === undefined) { return properties; @@ -201,97 +206,38 @@ class TagRendering extends UIElement implements TagDependantUIElement { }; this._mapping = []; - this._renderMapping = []; this._freeform = options.freeform; - // Prepare the choices for the Radio buttons - const choices: UIElement[] = []; - const usedChoices: string [] = []; for (const choice of options.mappings ?? []) { - if (choice.k === null) { - this._mapping.push(choice); - continue; - } - let choiceSubbed = choice; + let choiceSubbed = { + k: choice.k, + txt: choice.txt, + priority: choice.priority + }; + if (choice.substitute) { choiceSubbed = { k: choice.k.substituteValues( options.tagsPreprocessor(this._source.data)), - txt: this.ApplyTemplate(choice.txt), - substitute: false, + txt: choice.txt, priority: choice.priority } } - const txt = choiceSubbed.txt - // Choices is what is shown in the radio buttons - if (usedChoices.indexOf(txt) < 0) { - - choices.push(new FixedUiElement(txt)); - usedChoices.push(txt); - // This is used to convert the radio button index into tags needed to add - this._mapping.push(choiceSubbed); - } else { - this._renderMapping.push(choiceSubbed); // only used while rendering - } + this._mapping.push({ + k: choiceSubbed.k, + txt: choiceSubbed.txt + }); } - // Map radiobutton choice and textfield answer onto tagfilter. That tagfilter will be pushed into the changes later on - const pickChoice = (i => { - if (i === undefined || i === null) { - return undefined - } - return self._mapping[i].k - }); - const pickString = - (string) => { - if (string === "" || string === undefined) { - return undefined; - } - const tag = new Tag(self._freeform.key, string); - if (self._freeform.extraTags === undefined) { - return tag; - } - return new And([ - self._freeform.extraTags, - tag - ] - ); - }; - // Prepare the actual input element -> pick an appropriate implementation - let inputElement: UIInputElement; - - - if (this._freeform !== undefined && this._mapping !== undefined) { - // Radio buttons with 'other' - inputElement = new UIRadioButtonWithOther( - choices, - this._freeform.template, - this._freeform.placeholder, - pickChoice, - pickString - ); - this._questionElement = inputElement; - } else if (this._mapping !== [] && this._mapping.length > 0) { - // This is a classic radio selection element - inputElement = new UIRadioButton(new UIEventSource(choices), pickChoice, false) - this._questionElement = inputElement; - } else if (this._freeform !== undefined) { - this._textField = new TextField(new UIEventSource(this._freeform.placeholder), pickString); - inputElement = this._textField; - this._questionElement = new FixedUiElement( - "
" + this._freeform.template.replace("$$$", inputElement.Render()) + "
") - } else { - throw "Invalid questionRendering, expected at least choices or a freeform" - } - + this._questionElement = this.InputElementFor(options); const save = () => { - const selection = inputElement.GetValue().data; + const selection = self._questionElement.GetValue().data; if (selection) { changes.addTag(tags.data.id, selection); } @@ -305,54 +251,157 @@ class TagRendering extends UIElement implements TagDependantUIElement { } // Setup the save button and it's action - this._saveButton = new SaveButton(inputElement.GetValue()) + this._saveButton = new SaveButton(this._questionElement.GetValue()) .onClick(save); + this._editButton = new FixedUiElement(""); if (this._question !== undefined) { this._editButton = new FixedUiElement("edit") .onClick(() => { - console.log("Click", self._editButton); - if (self._textField) { - self._textField.value.setData(self._source.data["name"] ?? ""); - } - self._editMode.setData(true); + self._questionElement.GetValue().setData(self.CurrentValue()); }); - } else { - this._editButton = new FixedUiElement(""); } const cancelContents = this._editMode.map((isEditing) => { if (isEditing) { - return "Annuleren"; + return ""+Translations.t.general.cancel.R()+""; } else { - return "Overslaan (Ik weet het niet zeker...)"; + return ""+Translations.t.general.skip.R()+""; } - }); + }, [Locale.language]); // And at last, set up the skip button - this._skipButton = new VariableUiElement(cancelContents).onClick(cancel); + this._skipButton = new VariableUiElement(cancelContents).onClick(cancel) ; + } + private InputElementFor(options: { + freeform?: { + key: string, + template: string | UIElement, + renderTemplate: string | UIElement, + placeholder?: string | UIElement, + extraTags?: TagsFilter, + }, + mappings?: { k: TagsFilter, txt: string | UIElement, priority?: number, substitute?: boolean }[] + }): + InputElement { + + const elements = []; + + if (options.mappings !== undefined) { + + const previousTexts= []; + for (const mapping of options.mappings) { + if(mapping.k === null){ + continue; + } + if(previousTexts.indexOf(mapping.txt) >= 0){ + continue; + } + previousTexts.push(mapping.txt); + + elements.push(this.InputElementForMapping(mapping)); + } + } + + if (options.freeform !== undefined) { + elements.push(this.InputForFreeForm(options.freeform)); + } + + + if (elements.length == 0) { + console.warn("WARNING: no tagrendering with following options:", options); + return new FixedInputElement("This should not happen: no tag renderings defined", undefined); + } + if (elements.length == 1) { + return elements[0]; + } + + return new RadioButton(elements, false); + } - private ApplyTemplate(template: string): string { - const tags = this._tagsPreprocessor(this._source.data); - return TagUtils.ApplyTemplate(template, tags); + + private InputElementForMapping(mapping: { k: TagsFilter, txt: string | UIElement }) { + return new FixedInputElement(mapping.txt, mapping.k); } + + private InputForFreeForm(freeform): InputElement { + if (freeform === undefined) { + return undefined; + } + + + const pickString = + (string) => { + if (string === "" || string === undefined) { + return undefined; + } + const tag = new Tag(freeform.key, string); + if (freeform.extraTags === undefined) { + return tag; + } + return new And([ + tag, + freeform.extraTags + ] + ); + }; + + const toString = + (tag) => { + if (tag instanceof And) { + return toString(tag.and[0]) + } else if (tag instanceof Tag) { + return tag.value + } + return undefined; + } + + + let inputElement: InputElement; + const textField = new TextField({ + placeholder: this._freeform.placeholder, + fromString: pickString, + toString: toString + }); + + const prepost = Translations.W(freeform.template).InnerRender().split("$$$"); + return new InputElementWrapper(prepost[0], textField, prepost[1]); + } + + IsKnown(): boolean { const tags = TagUtils.proprtiesToKV(this._source.data); - for (const oneOnOneElement of this._mapping.concat(this._renderMapping)) { + for (const oneOnOneElement of this._mapping) { if (oneOnOneElement.k === null || oneOnOneElement.k.matches(tags)) { return true; } } - + return this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined; } + private CurrentValue(): TagsFilter { + const tags = TagUtils.proprtiesToKV(this._source.data); + + for (const oneOnOneElement of this._mapping) { + if (oneOnOneElement.k !== null && oneOnOneElement.k.matches(tags)) { + return oneOnOneElement.k; + } + } + if (this._freeform === undefined) { + return undefined; + } + + return new Tag(this._freeform.key, this._source.data[this._freeform.key]); + } + + IsQuestioning(): boolean { if (this.IsKnown()) { return false; @@ -368,10 +417,10 @@ class TagRendering extends UIElement implements TagDependantUIElement { return true; } - private RenderAnwser(): string { + private RenderAnwser(): UIElement { const tags = TagUtils.proprtiesToKV(this._source.data); - let freeform = ""; + let freeform: UIElement = new FixedUiElement(""); let freeformScore = -10; if (this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined) { freeform = this.ApplyTemplate(this._freeform.renderTemplate); @@ -379,58 +428,59 @@ class TagRendering extends UIElement implements TagDependantUIElement { } - let highestScore = -100; - let highestTemplate = undefined; - for (const oneOnOneElement of this._mapping.concat(this._renderMapping)) { - if (oneOnOneElement.k == null || - oneOnOneElement.k.matches(tags)) { - // We have found a matching key -> we use the template, but only if it scores better - let score = oneOnOneElement.priority ?? - (oneOnOneElement.k === null ? -1 : 0); - if (score > highestScore) { - highestScore = score; - highestTemplate = oneOnOneElement.txt - } + let highestScore = -100; + let highestTemplate = undefined; + for (const oneOnOneElement of this._mapping) { + if (oneOnOneElement.k == null || + oneOnOneElement.k.matches(tags)) { + // We have found a matching key -> we use the template, but only if it scores better + let score = oneOnOneElement.priority ?? + (oneOnOneElement.k === null ? -1 : 0); + if (score > highestScore) { + highestScore = score; + highestTemplate = oneOnOneElement.txt } } + } - if (freeformScore > highestScore) { - return freeform; - } + if (freeformScore > highestScore) { + return freeform; + } + + if (highestTemplate !== undefined) { + // we render the found template + return this.ApplyTemplate(highestTemplate); + } - if (highestTemplate !== undefined) { - // we render the found template - return this._primer + this.ApplyTemplate(highestTemplate); - } - } - protected InnerRender(): string { - + InnerRender(): string { if (this.IsQuestioning() || this._editMode.data) { // Not yet known or questioning, we have to ask a question + const question = this._question.Render(); return "
" + - "" + this._question + "" + - (this._question !== "" ? "
" : "") + - this._questionElement.Render() + + "" + question + "" + + (this._question.IsEmpty() ? "" : "
") + + "
" + this._questionElement.Render() + "
" + this._skipButton.Render() + this._saveButton.Render() + "
" } if (this.IsKnown()) { - const html = this.RenderAnwser(); - if (html == "") { + const answer = this.RenderAnwser() + if (answer.IsEmpty()) { return ""; } + const html = answer.Render(); let editButton = ""; - if(this._userDetails.data.loggedIn){ + if (this._userDetails.data.loggedIn && this._question !== undefined) { editButton = this._editButton.Render(); } - + return "" + "" + html + "" + editButton + @@ -441,13 +491,26 @@ class TagRendering extends UIElement implements TagDependantUIElement { } + + Priority(): number { + return this._priority; + } + + private ApplyTemplate(template: string | UIElement): UIElement { + if(template === undefined || template === null){ + throw "Trying to apply a template, but the template is null/undefined" + } + const tags = this._tagsPreprocessor(this._source.data); + if (template instanceof UIElement) { + template = template.Render(); + } + return new FixedUiElement(TagUtils.ApplyTemplate(template, tags)); + } + + InnerUpdate(htmlElement: HTMLElement) { super.InnerUpdate(htmlElement); - this._questionElement.Update(); - this._saveButton.Update(); - this._skipButton.Update(); - this._textField?.Update(); - this._editButton.Update(); + this._questionElement.Update(); // Another manual update for them } } \ No newline at end of file diff --git a/Logic/Changes.ts b/Logic/Changes.ts index e0dd9ce8d..488ea50d5 100644 --- a/Logic/Changes.ts +++ b/Logic/Changes.ts @@ -6,7 +6,6 @@ import {OsmConnection} from "./OsmConnection"; import {OsmNode, OsmObject} from "./OsmObject"; import {ElementStorage} from "./ElementStorage"; import {UIEventSource} from "../UI/UIEventSource"; -import {Question, QuestionDefinition} from "./Question"; import {And, Tag, TagsFilter} from "./TagsFilter"; export class Changes { diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts index 2bf8e1ac5..cbfdc0c2c 100644 --- a/Logic/FilteredLayer.ts +++ b/Logic/FilteredLayer.ts @@ -19,7 +19,7 @@ import { LayerDefinition } from "../Customizations/LayerDefinition"; */ export class FilteredLayer { - public readonly name: string; + public readonly name: string | UIElement; public readonly filters: TagsFilter; public readonly isDisplayed: UIEventSource = new UIEventSource(true); public readonly layerDef: LayerDefinition; @@ -33,6 +33,7 @@ export class FilteredLayer { /** The featurecollection from overpass */ private _dataFromOverpass; + private _wayHandling: number; /** List of new elements, geojson features */ private _newElements = []; @@ -40,8 +41,8 @@ export class FilteredLayer { * The leaflet layer object which should be removed on rerendering */ private _geolayer; - private _selectedElement: UIEventSource; - private _showOnPopup: (tags: UIEventSource) => UIElement; + private _selectedElement: UIEventSource<{ feature: any }>; + private _showOnPopup: (tags: UIEventSource, feature: any) => UIElement; constructor( layerDef: LayerDefinition, @@ -51,6 +52,8 @@ export class FilteredLayer { showOnPopup: ((tags: UIEventSource) => UIElement) ) { this.layerDef = layerDef; + + this._wayHandling = layerDef.wayHandling; this._selectedElement = selectedElement; this._showOnPopup = showOnPopup; this._style = layerDef.style; @@ -84,10 +87,18 @@ export class FilteredLayer { public SetApplicableData(geojson: any): any { const leftoverFeatures = []; const selfFeatures = []; - for (const feature of geojson.features) { + for (let feature of geojson.features) { // feature.properties contains all the properties var tags = TagUtils.proprtiesToKV(feature.properties); if (this.filters.matches(tags)) { + feature.properties["_surface"] = GeoOperations.surfaceAreaInSqMeters(feature); + if (feature.geometry.type !== "Point") { + if (this._wayHandling === LayerDefinition.WAYHANDLING_CENTER_AND_WAY) { + selfFeatures.push(GeoOperations.centerpoint(feature)); + } else if (this._wayHandling === LayerDefinition.WAYHANDLING_CENTER_ONLY) { + feature = GeoOperations.centerpoint(feature); + } + } selfFeatures.push(feature); } else { leftoverFeatures.push(feature); @@ -199,8 +210,8 @@ export class FilteredLayer { layer.on("click", function (e) { console.log("Selected ", feature) - self._selectedElement.setData(feature.properties); - const uiElement = self._showOnPopup(eventSource); + self._selectedElement.setData({feature: feature}); + const uiElement = self._showOnPopup(eventSource, feature); const popup = L.popup() .setContent(uiElement.Render()) .setLatLng(e.latlng) diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 91687e950..4ed677ebc 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -6,6 +6,15 @@ export class GeoOperations { return turf.area(feature); } + static centerpoint(feature: any) + { + const newFeature= turf.center(feature); + newFeature.properties = feature.properties; + newFeature.id = feature.id; + + return newFeature; + } + static featureIsContainedInAny(feature: any, shouldNotContain: any[], maxOverlapPercentage: number): boolean { diff --git a/Logic/Imgur.ts b/Logic/Imgur.ts index bfba380cd..7d1b0ed49 100644 --- a/Logic/Imgur.ts +++ b/Logic/Imgur.ts @@ -8,6 +8,7 @@ export class Imgur { title: string, description: string, blobs: FileList, handleSuccessfullUpload: ((imageURL: string) => void), allDone: (() => void), + onFail: ((reason: string) => void), offset:number = 0) { if (blobs.length == offset) { @@ -24,7 +25,8 @@ export class Imgur { handleSuccessfullUpload, allDone, offset + 1); - } + }, + onFail ); @@ -60,7 +62,6 @@ export class Imgur { } - console.log(data); const licenseInfo = new LicenseInfo(); licenseInfo.licenseShortName = data.license; @@ -75,7 +76,8 @@ export class Imgur { } static uploadImage(title: string, description: string, blob, - handleSuccessfullUpload: ((imageURL: string) => void)) { + handleSuccessfullUpload: ((imageURL: string) => void), + onFail: (reason:string) => void) { const apiUrl = 'https://api.imgur.com/3/image'; const apiKey = '7070e7167f0a25a'; @@ -106,7 +108,8 @@ export class Imgur { response = JSON.parse(response); handleSuccessfullUpload(response.data.link); }).fail((reason) => { - console.log("Uploading to IMGUR failed", reason) + console.log("Uploading to IMGUR failed", reason); + onFail(reason) }); } diff --git a/Logic/LayerUpdater.ts b/Logic/LayerUpdater.ts index 408d75623..5478e8944 100644 --- a/Logic/LayerUpdater.ts +++ b/Logic/LayerUpdater.ts @@ -57,8 +57,7 @@ export class LayerUpdater { } private handleFail(reason: any) { - console.log("QUERY FAILED", reason); - console.log("Retrying in 1s") + console.log("QUERY FAILED (retrying in 1 sec)", reason); this.previousBounds = undefined; const self = this; window.setTimeout( @@ -73,7 +72,6 @@ export class LayerUpdater { } console.log("Zoom level: ",this._map.map.getZoom(), "Least needed zoom:", this._minzoom) if (this._map.map.getZoom() < this._minzoom || this._map.Location.data.zoom < this._minzoom) { - console.log("Not running query: zoom not sufficient"); return; } diff --git a/Logic/OsmConnection.ts b/Logic/OsmConnection.ts index 648d9bbb5..e07751d33 100644 --- a/Logic/OsmConnection.ts +++ b/Logic/OsmConnection.ts @@ -17,7 +17,6 @@ export class UserDetails { export class OsmConnection { - private auth = new osmAuth({ oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem', oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI', @@ -123,6 +122,7 @@ export class OsmConnection { public preferenceSources : any = {} public GetPreference(key: string) : UIEventSource{ + key = "mapcomplete-"+key; if (this.preferenceSources[key] !== undefined) { return this.preferenceSources[key]; } diff --git a/Logic/OsmImageUploadHandler.ts b/Logic/OsmImageUploadHandler.ts index 3cabdbc40..cd242c777 100644 --- a/Logic/OsmImageUploadHandler.ts +++ b/Logic/OsmImageUploadHandler.ts @@ -48,12 +48,18 @@ export class OsmImageUploadHandler { title: title, description: description, handleURL: function (url) { - let freeIndex = 0; - while (tags["image:" + freeIndex] !== undefined) { - freeIndex++; + + let key = "image"; + if (tags["image"] !== undefined) { + + let freeIndex = 0; + while (tags["image:" + freeIndex] !== undefined) { + freeIndex++; + } + key = "image:" + freeIndex; } - console.log("Adding image:" + freeIndex, url); - changes.addChange(tags.id, "image:" + freeIndex, url); + console.log("Adding image:" + key, url); + changes.addChange(tags.id, key, url); self._slideShow.MoveTo(-1); // set the last (thus newly added) image) to view }, allDone: function () { diff --git a/Logic/Overpass.ts b/Logic/Overpass.ts index 28efa0b62..f9eafaf8e 100644 --- a/Logic/Overpass.ts +++ b/Logic/Overpass.ts @@ -32,7 +32,7 @@ export class Overpass { queryGeoJson(bbox: string, continuation: ((any) => void), onFail: ((reason) => void)): void { let query = this.buildQuery(bbox); - + if(Overpass.testUrl !== null){ console.log("Using testing URL") query = Overpass.testUrl; @@ -44,7 +44,7 @@ export class Overpass { console.log("Query failed") onFail(status); } - + if(json.elements === [] && json.remarks.indexOf("runtime error") > 0){ console.log("Timeout or other runtime error"); return; diff --git a/Logic/Question.ts b/Logic/Question.ts deleted file mode 100644 index 3ffd6469a..000000000 --- a/Logic/Question.ts +++ /dev/null @@ -1,508 +0,0 @@ -import {Changes} from "./Changes"; -import {UIElement} from "../UI/UIElement"; -import {UIEventSource} from "../UI/UIEventSource"; - -export class QuestionUI extends UIElement { - private readonly _q: Question; - private readonly _tags: UIEventSource; - /** - * The ID of the calling question - used to trigger it's onsave - */ - private readonly _qid; - - constructor(q: Question, qid: number, tags: UIEventSource) { - super(tags); - this._q = q; - this._tags = tags; - this._qid = qid; - } - - - private RenderRadio() { - let radios = ""; - let c = 0; - for (let answer of this._q.question.answers) { - const human = answer.text; - const ansId = "q" + this._qid + "-answer" + c; - radios += - "" + - "" + - "
"; - c++; - } - return radios; - } - - private RenderRadioText() { - let radios = ""; - let c = 0; - for (let answer of this._q.question.answers) { - const human = answer.text; - const ansId = "q" + this._qid + "-answer" + c; - radios += - "" + - "" + - "
"; - c++; - } - const ansId = "q" + this._qid + "-answer" + c; - - radios += - "" + - "" + - "
"; - - return radios; - } - - - InnerRender(): string { - - if (!this._q.Applicable(this._tags.data)) { - return ""; - } - - - const q = this._q.question; - - - let answers = ""; - if (q.type == "radio") { - answers += this.RenderRadio(); - } else if (q.type == "text") { - answers += "
" - } else if (q.type == "radio+text") { - answers += this.RenderRadioText(); - } else { - alert("PLZ RENDER TYPE " + q.type); - } - - - const embeddedScriptSave = 'questionAnswered(' + this._qid + ', "' + this._tags.data.id + '", false )'; - const embeddedScriptSkip = 'questionAnswered(' + this._qid + ', "' + this._tags.data.id + '", true )'; - const saveButton = ""; - const skip = ""; - return q.question + "
" + answers + saveButton + skip; - } - - InnerUpdate(htmlElement: HTMLElement) { - } -} - - -export class QuestionDefinition { - - - static noNameOrNameQuestion(question: string, noExplicitName : string, severity : number) : QuestionDefinition{ - const q = new QuestionDefinition(question); - - q.type = 'radio+text'; - q.addAnwser(noExplicitName, "noname","yes"); - q.addUnrequiredTag("name", "*"); - q.addUnrequiredTag("noname", "yes"); - - q.key = "name"; - q.severity = severity; - return q; - } - - static textQuestion( - question: string, - key: string, - severity: number - ): QuestionDefinition { - const q = new QuestionDefinition(question); - q.type = 'text'; - q.key = key; - q.severity = severity; - q.addUnrequiredTag(key, '*'); - return q; - } - - static radioQuestionSimple( - question: string, - severity: number, - key: string, - answers: { text: string, value: string }[]) { - - - const answers0: { - text: string, - tags: { k: string, v: string }[], - }[] = []; - for (const i in answers) { - const answer = answers[i]; - answers0.push({text: answer.text, tags: [{k: key, v: answer.value}]}) - } - - var q = this.radioQuestion(question, severity, answers0); - q.key = key; - q.addUnrequiredTag(key, '*'); - return q; - } - - static radioAndTextQuestion( - question: string, - severity: number, - key: string, - answers: { text: string, value: string }[]) { - - const q = this.radioQuestionSimple(question, severity, key, answers); - q.type = 'radio+text'; - return q; - - } - - static radioQuestion( - question: string, - severity: number, - answers: - { - text: string, - tags: { k: string, v: string }[], - }[] - ): QuestionDefinition { - - - const q = new QuestionDefinition(question); - q.severity = severity; - q.type = 'radio'; - q.answers = answers; - for (const i in answers) { - const answer = answers[i]; - for (const j in answer.tags) { - const tag = answer.tags[j]; - q.addUnrequiredTag(tag.k, tag.v); - } - } - - return q; - } - - - static GrbNoNumberQuestion() : QuestionDefinition{ - const q = new QuestionDefinition("Heeft dit gebouw een huisnummer?"); - q.type = "radio"; - q.severity = 10; - q.answers = [{ - text: "Ja, het OSM-huisnummer is correct", - tags: [{k: "fixme", v: ""}] - }, { - - text: "Nee, het is een enkele garage", - tags: [{k: "building", v: "garage"}, {k: "fixme", v: ""}] - }, { - - text: "Nee, het zijn meerdere garages", - tags: [{k: "building", v: "garages"}, {k: "fixme", v: ""}] - } - - - ]; - q.addRequiredTag("fixme", "GRB thinks that this has number no number") - return q; - } - - static GrbHouseNumberQuestion() : QuestionDefinition{ - - - const q = new QuestionDefinition("Wat is het huisnummer?"); - q.type = "radio+text"; - q.severity = 10; - - q.answers = [{ - text: "Het OSM-huisnummer is correct", - tags: [{k: "fixme", v: ""}], - }] - q.key = "addr:housenumber"; - - - q.addRequiredTag("fixme", "*"); - - return q; - } - - - private constructor(question: string) { - this.question = question; - } - - /** - * Question for humans - */ - public question: string; - - /** - * 'type' indicates how the answers are rendered and must be one of: - * 'text' for a free to fill text field - * 'radio' for radiobuttons - * 'radio+text' for radiobuttons and a freefill text field - * 'dropdown' for a dropdown menu - * 'number' for a number field - * - * If 'text' or 'number' is specified, 'key' is used as tag for the answer. - * If 'radio' or 'dropdown' is specified, the answers are used from 'tags' - * - */ - public type: string = 'radio'; - /** - * Only used for 'text' or 'number' questions - */ - public key: string = null; - - public answers: { - text: string, - tags: { k: string, v: string }[] - }[]; - - /** - * Indicates that the element must have _all_ the tags defined below - * Dictionary 'key' => [values]; empty list is wildcard - */ - private mustHaveAllTags = []; - - /** - * Indicates that the element must _not_ have any of the tags defined below. - * Dictionary 'key' => [values] - */ - private mustNotHaveTags = []; - - /** - * Severity: how important the question is - * The higher, the sooner it'll be shown - */ - public severity: number = 0; - - addRequiredTag(key: string, value: string) { - if (this.mustHaveAllTags[key] === undefined) { - this.mustHaveAllTags[key] = [value]; - } else { - if(this.mustHaveAllTags[key] === []){ - // Wildcard - return; - } - this.mustHaveAllTags[key].push(value); - } - - if (value === '*') { - this.mustHaveAllTags[key] = []; - } - return this; - } - - addUnrequiredTag(key: string, value: string) { - let valueList = this.mustNotHaveTags[key]; - - if (valueList === undefined) { - valueList = [value]; - this.mustNotHaveTags[key] = valueList; - } else { - if (valueList === []) { - return; - } - valueList.push(value); - } - - if (value === '*') { - this.mustNotHaveTags[key] = []; - } - return this; - } - - private addAnwser(anwser: string, key: string, value: string) { - if (this.answers === undefined) { - this.answers = [{text: anwser, tags: [{k: key, v: value}]}]; - } else { - this.answers.push({text: anwser, tags: [{k: key, v: value}]}); - } - this.addUnrequiredTag(key, value); - } - - public isApplicable(alreadyExistingTags): boolean { - for (let k in this.mustHaveAllTags) { - - var actual = alreadyExistingTags[k]; - if (actual === undefined) { - return false; - } - - let possibleVals = this.mustHaveAllTags[k]; - if (possibleVals.length == 0) { - // Wildcard - continue; - } - - let index = possibleVals.indexOf(actual); - if (index < 0) { - return false - } - } - - for (var k in this.mustNotHaveTags) { - var actual = alreadyExistingTags[k]; - if (actual === undefined) { - continue; - } - let impossibleVals = this.mustNotHaveTags[k]; - if (impossibleVals.length == 0) { - // Wildcard - return false; - } - - let index = impossibleVals.indexOf(actual); - if (index >= 0) { - return false - } - } - - return true; - - } -} - - -export class Question { - - - // All the questions are stored in here, to be able to retrieve them globaly. This is a workaround, see below - static questions = Question.InitCallbackFunction(); - - static InitCallbackFunction(): Question[] { - - // This needs some explanation, as it is a workaround - Question.questions = []; - // The html in a popup is only created when the user actually clicks to open it - // This means that we can not bind code to an HTML-element (as it doesn't exist yet) - // We work around this, by letting the 'save' button just call the function 'questionAnswered' with the ID of the question - // THis defines and registers this global function - - - /** - * Calls back to the question with either the answer or 'skip' - * @param questionId - * @param elementId - */ - function questionAnswered(questionId, elementId, dontKnow) { - if (dontKnow) { - Question.questions[questionId].Skip(elementId); - } else { - Question.questions[questionId].OnSave(elementId); - } - } - - - function checkRadioButton(id) { - // @ts-ignore - document.getElementById(id).checked = true; - } - - // must cast as any to set property on window - // @ts-ignore - const _global = (window /* browser */ || global /* node */) as any; - _global.questionAnswered = questionAnswered; - _global.checkRadioButton = checkRadioButton; - return []; - } - - - public readonly question: QuestionDefinition; - private _changeHandler: Changes; - private readonly _qId; - public skippedElements: string[] = []; - - constructor( - changeHandler: Changes, - question: QuestionDefinition) { - - this.question = question; - - this._qId = Question.questions.length; - this._changeHandler = changeHandler; - Question.questions.push(this); - } - - /** - * SHould this question be asked? - * Returns false if question is already there or if a premise is missing - */ - public Applicable(tags): boolean { - - if (this.skippedElements.indexOf(tags.id) >= 0) { - return false; - } - - return this.question.isApplicable(tags); - } - - /** - * - * @param elementId: the OSM-id of the element to perform the change on, format 'way/123', 'node/456' or 'relation/789' - * @constructor - */ - protected OnSave(elementId: string) { - let tagsToApply: { k: string, v: string }[] = []; - const q: QuestionDefinition = this.question; - let tp = this.question.type; - if (tp === "radio") { - const selected = document.querySelector('input[name="q' + this._qId + '"]:checked'); - if (selected === null) { - console.log("No answer selected"); - return - } - let index = (selected as any).value; - tagsToApply = q.answers[index].tags; - } else if (tp === "text") { - // @ts-ignore - let value = document.getElementById("q-" + this._qId + "-textbox").value; - if (value === undefined || value.length == 0) { - console.log("Answer too short"); - return; - } - tagsToApply = [{k: q.key, v: value}]; - } else if (tp === "radio+text") { - const selected = document.querySelector('input[name="q' + this._qId + '"]:checked'); - if (selected === null) { - console.log("No answer selected"); - return - } - let index = (selected as any).value; - if (index < q.answers.length) { - // A 'proper' answer was selected - tagsToApply = q.answers[index].tags; - } else { - // The textfield was selected - // @ts-ignore - let value = document.getElementById("q-" + this._qId + "-textbox").value; - if (value === undefined || value.length < 3) { - console.log("Answer too short"); - return; - } - tagsToApply = [{k: q.key, v: value}]; - } - - } - - console.log("Question.ts: Applying tags",tagsToApply," to element ", elementId); - - for (const toApply of tagsToApply) { - this._changeHandler.addChange(elementId, toApply.k, toApply.v); - } - - } - - /** - * Creates the HTML question for this tag collection - */ - public CreateHtml(tags: UIEventSource): UIElement { - return new QuestionUI(this, this._qId, tags); - } - - - private Skip(elementId: any) { - this.skippedElements.push(elementId); - console.log("SKIP"); - // Yeah, this is cheating below - // It is an easy way to notify the UIElement that something has changed - this._changeHandler._allElements.getElement(elementId).ping(); - } -} \ No newline at end of file diff --git a/Logic/StrayClickHandler.ts b/Logic/StrayClickHandler.ts index 85ccc9d7b..59c50de96 100644 --- a/Logic/StrayClickHandler.ts +++ b/Logic/StrayClickHandler.ts @@ -10,16 +10,16 @@ import {UIElement} from "../UI/UIElement"; export class StrayClickHandler { private _basemap: Basemap; private _lastMarker; - private _leftMessage: UIEventSource<() => UIElement>; + private _fullScreenMessage: UIEventSource; private _uiToShow: (() => UIElement); constructor( basemap: Basemap, - selectElement: UIEventSource, - leftMessage: UIEventSource<() => UIElement>, + selectElement: UIEventSource<{ feature: any }>, + fullScreenMessage: UIEventSource, uiToShow: (() => UIElement)) { this._basemap = basemap; - this._leftMessage = leftMessage; + this._fullScreenMessage = fullScreenMessage; this._uiToShow = uiToShow; const self = this; const map = basemap.map; @@ -32,15 +32,16 @@ export class StrayClickHandler { self._lastMarker = L.marker([lastClick.lat, lastClick.lon]); const uiElement = uiToShow(); const popup = L.popup().setContent(uiElement.Render()); - uiElement.Activate(); uiElement.Update(); + uiElement.Activate(); self._lastMarker.addTo(map); self._lastMarker.bindPopup(popup).openPopup(); self._lastMarker.on("click", () => { - leftMessage.setData(self._uiToShow); + fullScreenMessage.setData(self._uiToShow()); }); - + uiElement.Update(); + uiElement.Activate(); }); selectElement.addCallback(() => { diff --git a/README.md b/README.md index 4ca29f4ce..4a48c3a36 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,9 @@ When a map feature is clicked, a popup shows the information, images and questio The answers given by the user are sent (after a few seconds) to OpenStreetMap directly - if the user is logged in. If not logged in, the user is prompted to do so. +The UI-event-source is a class where the entire system is built upon, it acts as an observable object: another object can register for changes to update when needed. + + ### Searching images Images are fetched from: diff --git a/UI/AddButton.ts b/UI/AddButton.ts index ac0a3434b..3b493a949 100644 --- a/UI/AddButton.ts +++ b/UI/AddButton.ts @@ -123,6 +123,7 @@ export class AddButton extends UIElement { const self = this; htmlElement.onclick = function (event) { + // @ts-ignore if(event.consumed){ return; } diff --git a/UI/Base/Button.ts b/UI/Base/Button.ts index 3026729c3..817de1158 100644 --- a/UI/Base/Button.ts +++ b/UI/Base/Button.ts @@ -18,7 +18,7 @@ export class Button extends UIElement { } - protected InnerRender(): string { + InnerRender(): string { return "
" + "" + diff --git a/UI/Base/CheckBox.ts b/UI/Base/CheckBox.ts index ccc3759d8..fcf52cd69 100644 --- a/UI/Base/CheckBox.ts +++ b/UI/Base/CheckBox.ts @@ -4,6 +4,7 @@ import { FilteredLayer } from "../../Logic/FilteredLayer"; export class CheckBox extends UIElement{ + private data: UIEventSource; private readonly _data: UIEventSource; private readonly _showEnabled: string|UIElement; @@ -21,7 +22,7 @@ export class CheckBox extends UIElement{ } - protected InnerRender(): string { + InnerRender(): string { if (this._data.data) { return this._showEnabled; } else { diff --git a/UI/Base/Combine.ts b/UI/Base/Combine.ts new file mode 100644 index 000000000..12b433867 --- /dev/null +++ b/UI/Base/Combine.ts @@ -0,0 +1,31 @@ +import {UIElement} from "../UIElement"; +import Translations from "../i18n/Translations"; + +export default class Combine extends UIElement { + private uiElements: (string | UIElement)[]; + + constructor(uiElements: (string | UIElement)[]) { + super(undefined); + this.uiElements = uiElements; + } + + InnerRender(): string { + let elements = ""; + for (const element of this.uiElements) { + if (element instanceof UIElement) { + elements += element.Render(); + } else { + elements += element; + } + } + return elements; + } + + protected InnerUpdate(htmlElement: HTMLElement) { + for (const element of this.uiElements) { + if (element instanceof UIElement) { + element.Update(); + } + } + } +} \ No newline at end of file diff --git a/UI/Base/DropDownUI.ts b/UI/Base/DropDownUI.ts deleted file mode 100644 index 208ea857e..000000000 --- a/UI/Base/DropDownUI.ts +++ /dev/null @@ -1,61 +0,0 @@ -import {UIEventSource} from "../UIEventSource"; -import {UIElement} from "../UIElement"; - -export class DropDownUI extends UIElement { - - selectedElement: UIEventSource - private _label: string; - private _values: { value: string; shown: string }[]; - - constructor(label: string, values: { value: string, shown: string }[], - selectedElement: UIEventSource = undefined) { - super(undefined); - this._label = label; - this._values = values; - this.selectedElement = selectedElement ?? new UIEventSource(values[0].value); - if(selectedElement.data === undefined){ - this.selectedElement.setData(values[0].value) - } - const self = this; - this.selectedElement.addCallback(() => { - self.InnerUpdate(); - }); - } - - - protected InnerRender(): string { - - let options = ""; - for (const value of this._values) { - options += "" - } - - return "" + - "" + - "" + - ""; - } - - InnerUpdate() { - const self = this; - const e = document.getElementById("dropdown-" + this.id); - if(e === null){ - return; - } - // @ts-ignore - if (this.selectedElement.data !== e.value) { - // @ts-ignore - e.value = this.selectedElement.data; - } - e.onchange = function () { - // @ts-ignore - const selectedValue = e.options[e.selectedIndex].value; - console.log("Putting data", selectedValue) - self.selectedElement.setData(selectedValue); - } - - } - -} \ No newline at end of file diff --git a/UI/Base/FixedUiElement.ts b/UI/Base/FixedUiElement.ts index 39fee94dc..05790929d 100644 --- a/UI/Base/FixedUiElement.ts +++ b/UI/Base/FixedUiElement.ts @@ -5,10 +5,10 @@ export class FixedUiElement extends UIElement { constructor(html: string) { super(undefined); - this._html = html; + this._html = html ?? ""; } - protected InnerRender(): string { + InnerRender(): string { return this._html; } diff --git a/UI/Base/TextField.ts b/UI/Base/TextField.ts deleted file mode 100644 index ca0c4ead3..000000000 --- a/UI/Base/TextField.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../UIEventSource"; -import {UIInputElement} from "./UIInputElement"; - - -export class TextField extends UIInputElement { - - public value: UIEventSource = new UIEventSource(""); - /** - * Pings and has the value data - */ - public enterPressed = new UIEventSource(undefined); - private _placeholder: UIEventSource; - private _mapping: (string) => T; - - constructor(placeholder: UIEventSource, - mapping: ((string) => T)) { - super(placeholder); - this._placeholder = placeholder; - this._mapping = mapping; - } - - GetValue(): UIEventSource { - return this.value.map(this._mapping); - } - - protected InnerRender(): string { - return "
" + - "" + - "
"; - } - - InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); - const field = document.getElementById('text-' + this.id); - const self = this; - field.oninput = () => { - // @ts-ignore - self.value.setData(field.value); - }; - - field.addEventListener("keyup", function (event) { - if (event.key === "Enter") { - // @ts-ignore - self.enterPressed.setData(field.value); - } - }); - - - } - - Clear() { - const field = document.getElementById('text-' + this.id); - if (field !== undefined) { - // @ts-ignore - field.value = ""; - } - } -} \ No newline at end of file diff --git a/UI/Base/UIInputElement.ts b/UI/Base/UIInputElement.ts deleted file mode 100644 index fd4a24215..000000000 --- a/UI/Base/UIInputElement.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../UIEventSource"; - -export abstract class UIInputElement extends UIElement{ - - abstract GetValue() : UIEventSource; - -} \ No newline at end of file diff --git a/UI/Base/UIRadioButton.ts b/UI/Base/UIRadioButton.ts deleted file mode 100644 index 2413073f0..000000000 --- a/UI/Base/UIRadioButton.ts +++ /dev/null @@ -1,109 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../UIEventSource"; -import {UIInputElement} from "./UIInputElement"; - -export class UIRadioButton extends UIInputElement { - - public readonly SelectedElementIndex: UIEventSource - = new UIEventSource(null); - - private readonly _elements: UIEventSource - private _selectFirstAsDefault: boolean; - private _valueMapping: (i: number) => T; - - constructor(elements: UIEventSource, - valueMapping: ((i: number) => T), - selectFirstAsDefault = true) { - super(elements); - this._elements = elements; - this._selectFirstAsDefault = selectFirstAsDefault; - const self = this; - this._valueMapping = valueMapping; - this.SelectedElementIndex.addCallback(() => { - self.InnerUpdate(undefined); - }) - } - - GetValue(): UIEventSource { - return this.SelectedElementIndex.map(this._valueMapping); - } - - - private IdFor(i) { - return 'radio-' + this.id + '-' + i; - } - - protected InnerRender(): string { - - let body = ""; - let i = 0; - for (const el of this._elements.data) { - const htmlElement = - '' + - '' + - '
'; - body += htmlElement; - - i++; - } - - return "
" + body + "
"; - } - - InnerUpdate(htmlElement: HTMLElement) { - const self = this; - - function checkButtons() { - for (let i = 0; i < self._elements.data.length; i++) { - const el = document.getElementById(self.IdFor(i)); - // @ts-ignore - if (el.checked) { - self.SelectedElementIndex.setData(i); - } - } - } - - - const el = document.getElementById(this.id); - el.addEventListener("change", - function () { - checkButtons(); - } - ); - - if (this.SelectedElementIndex.data == null) { - if (this._selectFirstAsDefault) { - const el = document.getElementById(this.IdFor(0)); - if (el) { - // @ts-ignore - el.checked = true; - checkButtons(); - } - } - } else { - - // We check that what is selected matches the previous rendering - var checked = -1; - var expected = this.SelectedElementIndex.data; - if (expected) { - - for (let i = 0; i < self._elements.data.length; i++) { - const el = document.getElementById(self.IdFor(i)); - // @ts-ignore - if (el.checked) { - checked = i; - } - } - if (expected != checked) { - const el = document.getElementById(this.IdFor(expected)); - // @ts-ignore - el.checked = true; - } - } - } - - - } - - -} \ No newline at end of file diff --git a/UI/Base/UIRadioButtonWithOther.ts b/UI/Base/UIRadioButtonWithOther.ts deleted file mode 100644 index 806764d20..000000000 --- a/UI/Base/UIRadioButtonWithOther.ts +++ /dev/null @@ -1,72 +0,0 @@ -import {UIInputElement} from "./UIInputElement"; -import {UIEventSource} from "../UIEventSource"; -import {UIRadioButton} from "./UIRadioButton"; -import {UIElement} from "../UIElement"; -import {TextField} from "./TextField"; -import {FixedUiElement} from "./FixedUiElement"; - - -export class UIRadioButtonWithOther extends UIInputElement { - private readonly _radioSelector: UIRadioButton; - private readonly _freeformText: TextField; - private readonly _value: UIEventSource = new UIEventSource(undefined) - - constructor(choices: UIElement[], - otherChoiceTemplate: string, - placeholder: string, - choiceToValue: ((i: number) => T), - stringToValue: ((string: string) => T)) { - super(undefined); - const self = this; - - this._freeformText = new TextField( - new UIEventSource(placeholder), - stringToValue); - - - const otherChoiceElement = new FixedUiElement( - otherChoiceTemplate.replace("$$$", this._freeformText.Render())); - choices.push(otherChoiceElement); - - this._radioSelector = new UIRadioButton(new UIEventSource(choices), - (i) => { - if (i === undefined || i === null) { - return undefined; - } - if (i + 1 >= choices.length) { - return this._freeformText.GetValue().data - } - return choiceToValue(i); - }, - false); - - this._radioSelector.GetValue().addCallback( - (i) => { - self._value.setData(i); - }); - this._freeformText.GetValue().addCallback((str) => { - self._value.setData(str); - } - ); - this._freeformText.onClick(() => { - self._radioSelector.SelectedElementIndex.setData(choices.length - 1); - }) - - - } - - GetValue(): UIEventSource { - return this._value; - } - - protected InnerRender(): string { - return this._radioSelector.Render(); - } - - InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); - this._radioSelector.Update(); - this._freeformText.Update(); - } - -} \ No newline at end of file diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index b1f7f05c0..8afab13b2 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -12,16 +12,8 @@ export class VariableUiElement extends UIElement { } - protected InnerRender(): string { + InnerRender(): string { return this._html.data; } - InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); - if(this._innerUpdate !== undefined){ - this._innerUpdate(htmlElement); - } - } - - } \ No newline at end of file diff --git a/UI/CenterMessageBox.ts b/UI/CenterMessageBox.ts index 8bbb7a019..25ca95c66 100644 --- a/UI/CenterMessageBox.ts +++ b/UI/CenterMessageBox.ts @@ -1,6 +1,7 @@ import {UIElement} from "./UIElement"; import {UIEventSource} from "./UIEventSource"; import {OsmConnection} from "../Logic/OsmConnection"; +import Translations from "./i18n/Translations"; export class CenterMessageBox extends UIElement { @@ -34,17 +35,17 @@ export class CenterMessageBox extends UIElement { } - protected InnerRender(): string { + InnerRender(): string { if (this._centermessage.data != "") { return this._centermessage.data; } if (this._queryRunning.data) { - return "Data wordt geladen..."; + return Translations.t.centerMessage.loadingData.Render(); } else if (this._zoomInMore.data) { - return "Zoom in om de data te zien en te bewerken"; + return Translations.t.centerMessage.zoomIn.Render(); } - return "Klaar!"; + return Translations.t.centerMessage.ready.Render(); } diff --git a/UI/FeatureInfoBox.ts b/UI/FeatureInfoBox.ts index 441c63a1c..c9477efad 100644 --- a/UI/FeatureInfoBox.ts +++ b/UI/FeatureInfoBox.ts @@ -10,10 +10,18 @@ import {TagRenderingOptions} from "../Customizations/TagRendering"; import {OsmLink} from "../Customizations/Questions/OsmLink"; import {WikipediaLink} from "../Customizations/Questions/WikipediaLink"; import {And} from "../Logic/TagsFilter"; -import {TagDependantUIElement} from "../Customizations/UIElementConstructor"; +import {TagDependantUIElement, TagDependantUIElementConstructor} from "../Customizations/UIElementConstructor"; +import Translations from "./i18n/Translations"; export class FeatureInfoBox extends UIElement { + /** + * The actual GEOJSON-object, with geometry and stuff + */ + private _feature: any; + /** + * The tags, wrapped in a global event source + */ private _tagsES: UIEventSource; private _changes: Changes; private _userDetails: UIEventSource; @@ -24,31 +32,49 @@ export class FeatureInfoBox extends UIElement { private _wikipedialink: UIElement; - private _infoboxes: TagDependantUIElement[]; private _questions: QuestionPicker; + private _oneSkipped = Translations.t.general.oneSkippedQuestion.Clone(); + private _someSkipped = Translations.t.general.skippedQuestions.Clone(); + constructor( + feature: any, tagsES: UIEventSource, - title: TagRenderingOptions, - elementsToShow: TagRenderingOptions[], + title: TagRenderingOptions | UIElement, + elementsToShow: TagDependantUIElementConstructor[], changes: Changes, userDetails: UIEventSource ) { super(tagsES); + this._feature = feature; this._tagsES = tagsES; this._changes = changes; this._userDetails = userDetails; this.ListenTo(userDetails); - const deps = {tags:this._tagsES , changes:this._changes} - + const deps = {tags: this._tagsES, changes: this._changes} + this._infoboxes = []; elementsToShow = elementsToShow ?? [] + + const self = this; for (const tagRenderingOption of elementsToShow) { - this._infoboxes.push( + self._infoboxes.push( tagRenderingOption.construct(deps)); } + function initTags() { + self._infoboxes = [] + for (const tagRenderingOption of elementsToShow) { + self._infoboxes.push( + tagRenderingOption.construct(deps)); + } + self.Update(); + } + + this._someSkipped.onClick(initTags) + this._oneSkipped.onClick(initTags) + title = title ?? new TagRenderingOptions( { @@ -56,10 +82,14 @@ export class FeatureInfoBox extends UIElement { } ) - this._title = new TagRenderingOptions(title.options).construct(deps); - this._osmLink =new OsmLink().construct(deps); + if (title instanceof UIElement) { + this._title = title; + } else { + this._title = new TagRenderingOptions(title.options).construct(deps); + } + this._osmLink = new OsmLink().construct(deps); this._wikipedialink = new WikipediaLink().construct(deps); - + } @@ -67,13 +97,16 @@ export class FeatureInfoBox extends UIElement { const info = []; - const questions : TagDependantUIElement[] = []; - + const questions: TagDependantUIElement[] = []; + let skippedQuestions = 0; for (const infobox of this._infoboxes) { if (infobox.IsKnown()) { info.push(infobox); } else if (infobox.IsQuestioning()) { questions.push(infobox); + } else { + // This question is neither known nor questioning -> it was skipped + skippedQuestions++; } } @@ -93,6 +126,10 @@ export class FeatureInfoBox extends UIElement { } questionsHtml = mostImportantQuestion.Render(); + } else if (skippedQuestions == 1) { + questionsHtml = this._oneSkipped.Render(); + } else if (skippedQuestions > 0) { + questionsHtml = this._someSkipped.Render(); } return "
" + @@ -115,4 +152,6 @@ export class FeatureInfoBox extends UIElement { "
"; } + + } diff --git a/UI/MessageBoxHandler.ts b/UI/FullScreenMessageBoxHandler.ts similarity index 62% rename from UI/MessageBoxHandler.ts rename to UI/FullScreenMessageBoxHandler.ts index 5ce7bdbd5..88f687144 100644 --- a/UI/MessageBoxHandler.ts +++ b/UI/FullScreenMessageBoxHandler.ts @@ -1,14 +1,16 @@ -/** - * Keeps 'messagebox' and 'messageboxmobile' in sync, shows a 'close' button on the latter one - */ import {UIEventSource} from "./UIEventSource"; import {UIElement} from "./UIElement"; import {VariableUiElement} from "./Base/VariableUIElement"; +import Translations from "./i18n/Translations"; -export class MessageBoxHandler { - private _uielement: UIEventSource<() => UIElement>; +/** + * Handles the full screen popup on mobile + */ +export class FullScreenMessageBoxHandler { + + private _uielement: UIEventSource; - constructor(uielement: UIEventSource<() => UIElement>, + constructor(uielement: UIEventSource, onClear: (() => void)) { this._uielement = uielement; this.listenTo(uielement); @@ -22,14 +24,13 @@ export class MessageBoxHandler { } } - new VariableUiElement(new UIEventSource("

Naar de kaart

"), - () => { - document.getElementById("to-the-map").onclick = function () { - uielement.setData(undefined); - onClear(); - } - } - ).AttachTo("to-the-map"); + Translations.t.general.returnToTheMap + .onClick(() => { + console.log("Clicked 'return to the map'") + uielement.setData(undefined); + onClear(); + }) + .AttachTo("to-the-map-h2"); } @@ -45,7 +46,6 @@ export class MessageBoxHandler { update() { const wrapper = document.getElementById("messagesboxmobilewrapper"); const gen = this._uielement.data; - console.log("Generator: ", gen); if (gen === undefined) { wrapper.classList.add("hidden") if (location.hash !== "") { @@ -55,12 +55,8 @@ export class MessageBoxHandler { } location.hash = "#element" wrapper.classList.remove("hidden"); - /* gen() - ?.HideOnEmpty(true) - ?.AttachTo("messagesbox") - ?.Activate();*/ - gen() + gen ?.HideOnEmpty(true) ?.AttachTo("messagesboxmobile") ?.Activate(); diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 9d4e1fd00..b3f40d8c5 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -23,10 +23,10 @@ export class ImageCarouselConstructor implements TagDependantUIElementConstructo return 0; } - construct(tags: UIEventSource, changes: Changes): TagDependantUIElement { - return new ImageCarousel(tags, changes); + construct(dependencies: { tags: UIEventSource, changes: Changes }): TagDependantUIElement { + return new ImageCarousel(dependencies.tags, dependencies.changes); } - + } export class ImageCarousel extends TagDependantUIElement { diff --git a/UI/Image/ImageCarouselWithUpload.ts b/UI/Image/ImageCarouselWithUpload.ts index 38ebe206f..8ce0225f8 100644 --- a/UI/Image/ImageCarouselWithUpload.ts +++ b/UI/Image/ImageCarouselWithUpload.ts @@ -34,14 +34,14 @@ class ImageCarouselWithUpload extends TagDependantUIElement { const changes = dependencies.changes; this._imageElement = new ImageCarousel(tags, changes); const userDetails = changes.login.userDetails; - const license = changes.login.GetPreference( "mapcomplete-pictures-license"); + const license = changes.login.GetPreference( "pictures-license"); this._pictureUploader = new OsmImageUploadHandler(tags, userDetails, license, changes, this._imageElement.slideshow).getUI(); } - protected InnerRender(): string { + InnerRender(): string { return this._imageElement.Render() + this._pictureUploader.Render(); } diff --git a/UI/ImageUploadFlow.ts b/UI/ImageUploadFlow.ts index 6542824c6..048b32ac7 100644 --- a/UI/ImageUploadFlow.ts +++ b/UI/ImageUploadFlow.ts @@ -3,20 +3,21 @@ import {UIEventSource} from "./UIEventSource"; import $ from "jquery" import {Imgur} from "../Logic/Imgur"; import {UserDetails} from "../Logic/OsmConnection"; -import {DropDownUI} from "./Base/DropDownUI"; +import {DropDown} from "./Input/DropDown"; import {VariableUiElement} from "./Base/VariableUIElement"; +import Translations from "./i18n/Translations"; export class ImageUploadFlow extends UIElement { private _licensePicker: UIElement; private _selectedLicence: UIEventSource; - private _licenseExplanation: UIElement; private _isUploading: UIEventSource = new UIEventSource(0) + private _didFail: UIEventSource = new UIEventSource(false); private _uploadOptions: (license: string) => { title: string; description: string; handleURL: (url: string) => void; allDone: (() => void) }; private _userdetails: UIEventSource; constructor( userInfo: UIEventSource, - preferedLicense : UIEventSource, + preferedLicense: UIEventSource, uploadOptions: ((license: string) => { title: string, @@ -30,70 +31,63 @@ export class ImageUploadFlow extends UIElement { this.ListenTo(userInfo); this._uploadOptions = uploadOptions; this.ListenTo(this._isUploading); + this.ListenTo(this._didFail); - const licensePicker = new DropDownUI("Jouw foto wordt gepubliceerd ", - + const licensePicker = new DropDown(Translations.t.image.willBePublished, [ - {value: "CC0", shown: "in het publiek domein"}, - {value: "CC-BY-SA 4.0", shown: "onder een CC-BY-SA-licentie"}, - {value: "CC-BY 4.0", shown: "onder een CC-BY-licentie"} + {value: "CC0", shown: Translations.t.image.cco}, + {value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs}, + {value: "CC-BY 4.0", shown: Translations.t.image.ccb} ], preferedLicense ); this._licensePicker = licensePicker; - this._selectedLicence = licensePicker.selectedElement; + this._selectedLicence = licensePicker.GetValue(); - const licenseExplanations = { - "CC-BY-SA 4.0": - "Creative Commonse met naamsvermelding en gelijk delen
" + - "Je foto mag door iedereen gratis gebruikt worden, als ze je naam vermelden én ze afgeleide werken met deze licentie en attributie delen.", - "CC-BY 4.0": - "Creative Commonse met naamsvermelding
" + - "Je foto mag door iedereen gratis gebruikt worden, als ze je naam vermelden", - "CC0": - "Geen copyright
Je foto mag door iedereen voor alles gebruikt worden" - } - this._licenseExplanation = new VariableUiElement( - this._selectedLicence.map((license) => { - return licenseExplanations[license] - }) - ); } - protected InnerRender(): string { + InnerRender(): string { if (!this._userdetails.data.loggedIn) { - return "
Gelieve je aan te melden om een foto toe te voegen of vragen te beantwoorden
"; + return `
${Translations.t.image.pleaseLogin.Render()}
`; } + + let uploadingMessage = ""; if (this._isUploading.data == 1) { - return "Bezig met een foto te uploaden..." + return `${Translations.t.image.uploadingPicture.Render()}` } if (this._isUploading.data > 0) { - return "Bezig met uploaden, nog " + this._isUploading.data + " foto's te gaan..." + uploadingMessage = "Uploading multiple pictures, " + this._isUploading.data + " left..." + } + + if(this._didFail.data){ + uploadingMessage += "Some images failed to upload. Imgur migth be down or you might block third-party API's (e.g. by using Brave or UMatrix)
" } return "" + "
" + - + "" + - + "" + - + "
" ; } @@ -128,11 +122,12 @@ export class ImageUploadFlow extends UIElement { function () { console.log("All uploads completed") opts.allDone(); + }, + function(failReason) { + } ) } } } - - } \ No newline at end of file diff --git a/UI/Input/DropDown.ts b/UI/Input/DropDown.ts new file mode 100644 index 000000000..fc7012abc --- /dev/null +++ b/UI/Input/DropDown.ts @@ -0,0 +1,100 @@ +import {UIEventSource} from "../UIEventSource"; +import {UIElement} from "../UIElement"; +import {InputElement} from "./InputElement"; +import instantiate = WebAssembly.instantiate; +import {FixedUiElement} from "../Base/FixedUiElement"; +import Translations from "../i18n/Translations"; + +export class DropDown extends InputElement { + + private readonly _label: UIElement; + private readonly _values: { value: T; shown: UIElement }[]; + + private readonly _value; + + constructor(label: string | UIElement, + values: { value: T, shown: string | UIElement }[], + value: UIEventSource = undefined) { + super(undefined); + this._value = value ?? new UIEventSource(undefined); + this._label = Translations.W(label); + this._values = values.map(v => { + return { + value: v.value, + shown: Translations.W(v.shown) + } + } + ); + for (const v of this._values) { + this.ListenTo(v.shown._source); + } + this.ListenTo(this._value) + + } + + 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) { + return true; + } + } + return false + } + + + InnerRender(): string { + if(this._values.length <=1){ + return ""; + } + + let options = ""; + for (let i = 0; i < this._values.length; i++) { + options += "" + + } + return "
" + + "" + + "" + + "
"; + } + + protected InnerUpdate(element) { + + + var e = document.getElementById("dropdown-" + this.id); + if(e === null){ + return; + } + const self = this; + e.onchange = (() => { + // @ts-ignore + var index = parseInt(e.selectedIndex); + self._value.setData(self._values[index].value); + + }); + + var t = this._value.data; + for (let i = 0; i < this._values.length ; i++) { + const value = this._values[i]; + if (value.value == t) { + // @ts-ignore + e.selectedIndex = i; + } + } + + } + +} \ No newline at end of file diff --git a/UI/Input/FixedInputElement.ts b/UI/Input/FixedInputElement.ts new file mode 100644 index 000000000..0cada259e --- /dev/null +++ b/UI/Input/FixedInputElement.ts @@ -0,0 +1,35 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../UIEventSource"; +import {UIElement} from "../UIElement"; +import {FixedUiElement} from "../Base/FixedUiElement"; + + +export class FixedInputElement extends InputElement { + private rendering: UIElement; + private value: UIEventSource; + + constructor(rendering: UIElement | string, value: T) { + super(undefined); + this.value = new UIEventSource(value); + this.rendering = typeof (rendering) === 'string' ? new FixedUiElement(rendering) : rendering; + } + + GetValue(): UIEventSource { + return this.value; + } + + ShowValue(t: T): boolean { + return false; + } + + InnerRender(): string { + return this.rendering.Render(); + } + + IsValid(t: T): boolean { + return t == this.value.data; + } + + + +} \ No newline at end of file diff --git a/UI/Input/InputElement.ts b/UI/Input/InputElement.ts new file mode 100644 index 000000000..8278314f8 --- /dev/null +++ b/UI/Input/InputElement.ts @@ -0,0 +1,11 @@ +import {UIElement} from "../UIElement"; +import {UIEventSource} from "../UIEventSource"; +import {FixedUiElement} from "../Base/FixedUiElement"; + +export abstract class InputElement extends UIElement{ + + abstract GetValue() : UIEventSource; + + abstract IsValid(t: T) : boolean; + +} \ No newline at end of file diff --git a/UI/Input/InputElementWrapper.ts b/UI/Input/InputElementWrapper.ts new file mode 100644 index 000000000..905a9481c --- /dev/null +++ b/UI/Input/InputElementWrapper.ts @@ -0,0 +1,41 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../UIEventSource"; +import {UIElement} from "../UIElement"; +import {FixedUiElement} from "../Base/FixedUiElement"; + + +export class InputElementWrapper extends InputElement{ + private pre: UIElement ; + private input: InputElement; + private post: UIElement ; + + constructor( + pre: UIElement | string, + input: InputElement, + post: UIElement | string + + ) { + super(undefined); + this.pre = typeof(pre) === 'string' ? new FixedUiElement(pre) : pre + this.input = input; + this.post =typeof(post) === 'string' ? new FixedUiElement(post) : post + } + + + GetValue(): UIEventSource { + return this.input.GetValue(); + } + + ShowValue(t: T) { + return this.input.ShowValue(t); + } + + InnerRender(): string { + return this.pre.Render() + this.input.Render() + this.post.Render(); + } + + IsValid(t: T): boolean { + return this.input.IsValid(t); + } + +} \ No newline at end of file diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts new file mode 100644 index 000000000..8d8b5cc5e --- /dev/null +++ b/UI/Input/RadioButton.ts @@ -0,0 +1,146 @@ +import {UIElement} from "../UIElement"; +import {UIEventSource} from "../UIEventSource"; +import {InputElement} from "./InputElement"; + +export class RadioButton extends InputElement { + + private readonly _selectedElementIndex: UIEventSource + = new UIEventSource(null); + + private value: UIEventSource; + private readonly _elements: InputElement[] + private _selectFirstAsDefault: boolean; + + + constructor(elements: InputElement[], + selectFirstAsDefault = true) { + super(undefined); + this._elements = elements; + this._selectFirstAsDefault = selectFirstAsDefault; + const self = this; + + + this.value = + UIEventSource.flatten(this._selectedElementIndex.map( + (selectedIndex) => { + if (selectedIndex !== undefined && selectedIndex !== null) { + return elements[selectedIndex].GetValue() + } + } + ), elements.map(e => e.GetValue())); + + this.value.addCallback((t) => { + self.ShowValue(t); + }) + + + for (let i = 0; i < elements.length; i++) { + // If an element is clicked, the radio button corresponding with it should be selected as well + elements[i].onClick(() => { + self._selectedElementIndex.setData(i); + }); + } + + } + + IsValid(t: T): boolean { + for (const inputElement of this._elements) { + if (inputElement.IsValid(t)) { + return true; + } + } + return false; + } + + GetValue(): UIEventSource { + return this.value; + } + + + private IdFor(i) { + return 'radio-' + this.id + '-' + i; + } + + InnerRender(): string { + + let body = ""; + let i = 0; + for (const el of this._elements) { + const htmlElement = + '' + + '' + + '
'; + body += htmlElement; + + i++; + } + + return "
" + body + "
"; + } + + public ShowValue(t: T): boolean { + if (t === undefined) { + return false; + } + if (!this.IsValid(t)) { + return false; + } + // We check that what is selected matches the previous rendering + for (let i = 0; i < this._elements.length; i++) { + const e = this._elements[i]; + if (e.IsValid(t)) { + this._selectedElementIndex.setData(i); + e.GetValue().setData(t); + const radio = document.getElementById(this.IdFor(i)); + // @ts-ignore + radio?.checked = true; + return; + } + + } + } + + InnerUpdate(htmlElement: HTMLElement) { + const self = this; + + function checkButtons() { + for (let i = 0; i < self._elements.length; i++) { + const el = document.getElementById(self.IdFor(i)); + // @ts-ignore + if (el.checked) { + self._selectedElementIndex.setData(i); + } + } + } + + + const el = document.getElementById(this.id); + el.addEventListener("change", + function () { + checkButtons(); + } + ); + if (this._selectedElementIndex.data !== null) { + const el = document.getElementById(this.IdFor(this._selectedElementIndex.data)); + if (el) { + // @ts-ignore + el.checked = true; + checkButtons(); + } + } else if (this._selectFirstAsDefault) { + this.ShowValue(this.value.data); + if (this._selectedElementIndex.data === null || this._selectedElementIndex.data === undefined) { + const el = document.getElementById(this.IdFor(0)); + if (el) { + // @ts-ignore + el.checked = true; + checkButtons(); + } + } + } + + + }; + + +} \ No newline at end of file diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts new file mode 100644 index 000000000..f025b28be --- /dev/null +++ b/UI/Input/TextField.ts @@ -0,0 +1,119 @@ +import {UIElement} from "../UIElement"; +import {UIEventSource} from "../UIEventSource"; +import {InputElement} from "./InputElement"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import Translations from "../i18n/Translations"; + + +export class TextField extends InputElement { + + private value: UIEventSource; + private mappedValue: UIEventSource; + /** + * Pings and has the value data + */ + public enterPressed = new UIEventSource(undefined); + private _placeholder: UIElement; + private _fromString?: (string: string) => T; + private _toString: (t: T) => string; + + constructor(options: { + placeholder?: string | UIElement, + toString: (t: T) => string, + fromString: (string: string) => T, + value?: UIEventSource + }) { + super(undefined); + const self = this; + this.value = new UIEventSource(""); + + this.mappedValue = options?.value ?? new UIEventSource(undefined); + this.mappedValue.addCallback(() => self.InnerUpdate()); + + // @ts-ignore + this._fromString = options.fromString ?? ((str) => (str)) + this.value.addCallback((str) => this.mappedValue.setData(options.fromString(str))); + this.mappedValue.addCallback((t) => this.value.setData(options.toString(t))); + + + this._placeholder = Translations.W(options.placeholder ?? ""); + this.ListenTo(this._placeholder._source); + this._toString = options.toString ?? ((t) => ("" + t)); + + + this.mappedValue.addCallback((t) => { + if (t === undefined || t === null) { + return; + } + const field = document.getElementById('text-' + this.id); + if (field === undefined || field === null) { + return; + } + // @ts-ignore + field.value = options.toString(t); + }) + } + + GetValue(): UIEventSource { + return this.mappedValue; + } + + ShowValue(t: T): boolean { + if (!this.IsValid(t)) { + return false; + } + this.mappedValue.setData(t); + } + + InnerRender(): string { + return "
" + + "" + + "
"; + } + + InnerUpdate() { + const field = document.getElementById('text-' + this.id); + if (field === null) { + return; + } + const self = this; + field.oninput = () => { + // @ts-ignore + self.value.setData(field.value); + }; + + field.addEventListener("keyup", function (event) { + if (event.key === "Enter") { + // @ts-ignore + self.enterPressed.setData(field.value); + } + }); + + if (this.IsValid(this.mappedValue.data)) { + const expected = this._toString(this.mappedValue.data); + // @ts-ignore + if (field.value !== expected) { + // @ts-ignore + field.value = expected; + } + } + + + } + + IsValid(t: T): boolean { + if(t === undefined || t === null){ + return false; + } + const result = this._toString(t); + return result !== undefined && result !== null; + } + + Clear() { + const field = document.getElementById('text-' + this.id); + if (field !== undefined) { + // @ts-ignore + field.value = ""; + } + } +} \ No newline at end of file diff --git a/UI/PendingChanges.ts b/UI/PendingChanges.ts index e0411a538..cda62141d 100644 --- a/UI/PendingChanges.ts +++ b/UI/PendingChanges.ts @@ -21,7 +21,7 @@ export class PendingChanges extends UIElement { }) } - protected InnerRender(): string { + InnerRender(): string { if (this._isSaving.data) { return "Saving"; } diff --git a/UI/SaveButton.ts b/UI/SaveButton.ts index 3bba390a7..a2728c41f 100644 --- a/UI/SaveButton.ts +++ b/UI/SaveButton.ts @@ -1,5 +1,6 @@ import {UIEventSource} from "./UIEventSource"; import {UIElement} from "./UIElement"; +import Translations from "./i18n/Translations"; export class SaveButton extends UIElement { private _value: UIEventSource; @@ -12,14 +13,14 @@ export class SaveButton extends UIElement { this._value = value; } - protected InnerRender(): string { + InnerRender(): string { if (this._value.data === undefined || this._value.data === null || this._value.data === "" ) { - return "Opslaan" + return ""+Translations.t.general.save.Render()+"" } - return "Opslaan"; + return ""+Translations.t.general.save.Render()+""; } } \ No newline at end of file diff --git a/UI/SearchAndGo.ts b/UI/SearchAndGo.ts index 3916168a3..8e51f93bb 100644 --- a/UI/SearchAndGo.ts +++ b/UI/SearchAndGo.ts @@ -1,15 +1,26 @@ import {UIElement} from "./UIElement"; -import {TextField} from "./Base/TextField"; +import {TextField} from "./Input/TextField"; import {UIEventSource} from "./UIEventSource"; import {FixedUiElement} from "./Base/FixedUiElement"; import {Geocoding} from "../Logic/Geocoding"; import {Basemap} from "../Logic/Basemap"; +import {VariableUiElement} from "./Base/VariableUIElement"; +import Translation from "./i18n/Translation"; +import Locale from "./i18n/Locale"; +import Translations from "./i18n/Translations"; export class SearchAndGo extends UIElement { - private _placeholder = new UIEventSource("Zoek naar een locatie...") - private _searchField = new TextField(this._placeholder); + private _placeholder = new UIEventSource(Translations.t.general.search.search) + private _searchField = new TextField({ + placeholder: new VariableUiElement( + this._placeholder.map(uiElement => uiElement.InnerRender(), [Locale.language]) + ), + fromString: str => str, + toString: str => str + } + ); private _foundEntries = new UIEventSource([]); private _map: Basemap; @@ -33,14 +44,14 @@ export class SearchAndGo extends UIElement { // Triggered by 'enter' or onclick private RunSearch() { - const searchString = this._searchField.value.data; + const searchString = this._searchField.GetValue().data; this._searchField.Clear(); - this._placeholder.setData("Bezig met zoeken..."); + this._placeholder.setData(Translations.t.general.search.searching); const self = this; Geocoding.Search(searchString, this._map, (result) => { if (result.length == 0) { - this._placeholder.setData("Niets gevonden"); + this._placeholder.setData(Translations.t.general.search.nothing); return; } @@ -50,16 +61,15 @@ export class SearchAndGo extends UIElement { [bb[1], bb[3]] ] self._map.map.fitBounds(bounds); - this._placeholder.setData("Zoek naar een locatie..."); + this._placeholder.setData(Translations.t.general.search.search); }, () => { - this._placeholder.setData("Niets gevonden: er ging iets mis"); + this._placeholder.setData(Translations.t.general.search.error); }); } - protected InnerRender(): string { - // "Search " + + InnerRender(): string { return this._searchField.Render() + this._goButton.Render(); diff --git a/UI/SimpleAddUI.ts b/UI/SimpleAddUI.ts index b23543755..5d0bc6e79 100644 --- a/UI/SimpleAddUI.ts +++ b/UI/SimpleAddUI.ts @@ -15,17 +15,17 @@ export class SimpleAddUI extends UIElement { private _addButtons: UIElement[]; private _lastClickLocation: UIEventSource<{ lat: number; lon: number }>; private _changes: Changes; - private _selectedElement: UIEventSource; + private _selectedElement: UIEventSource<{feature: any}>; private _dataIsLoading: UIEventSource; private _userDetails: UIEventSource; constructor(zoomlevel: UIEventSource<{ zoom: number }>, lastClickLocation: UIEventSource<{ lat: number, lon: number }>, changes: Changes, - selectedElement: UIEventSource, + selectedElement: UIEventSource<{feature: any}>, dataIsLoading: UIEventSource, userDetails: UIEventSource, - addButtons: { name: string; icon: string; tags: Tag[]; layerToAddTo: FilteredLayer }[], + addButtons: { name: UIElement; icon: string; tags: Tag[]; layerToAddTo: FilteredLayer }[], ) { super(zoomlevel); this._zoomlevel = zoomlevel; @@ -42,37 +42,36 @@ export class SimpleAddUI extends UIElement { //