Merge branch 'master' of github.com:pietervdvn/MapComplete

This commit is contained in:
Pieter Vander Vennet 2020-07-21 16:02:24 +02:00
commit 0a9d0f1d45
91 changed files with 2119 additions and 1360 deletions

View file

@ -14,9 +14,9 @@ import {NatureReserves} from "./Layers/NatureReserves";
import {Natuurpunt} from "./Layouts/Natuurpunt"; import {Natuurpunt} from "./Layouts/Natuurpunt";
export class AllKnownLayouts { export class AllKnownLayouts {
public static allSets: any = AllKnownLayouts.AllLayouts(); public static allSets = AllKnownLayouts.AllLayouts();
private static AllLayouts(): any { private static AllLayouts(): Map<string, Layout> {
const all = new All(); const all = new All();
const layouts: Layout[] = [ const layouts: Layout[] = [
new Groen(), new Groen(),
@ -32,7 +32,7 @@ export class AllKnownLayouts {
new Statues(), new Statues(),
*/ */
]; ];
const allSets = {}; const allSets: Map<string, Layout> = new Map();
for (const layout of layouts) { for (const layout of layouts) {
allSets[layout.name] = layout; allSets[layout.name] = layout;
all.layers = all.layers.concat(layout.layers); all.layers = all.layers.concat(layout.layers);

View file

@ -88,7 +88,6 @@ export class LayerDefinition {
} }
} = undefined) { } = undefined) {
if (options === undefined) { if (options === undefined) {
console.log("No options!")
return; return;
} }
this.name = options.name; this.name = options.name;
@ -100,7 +99,6 @@ export class LayerDefinition {
this.title = options.title; this.title = options.title;
this.elementsToShow = options.elementsToShow; this.elementsToShow = options.elementsToShow;
this.style = options.style; this.style = options.style;
console.log(this)
} }
asLayer(basemap: Basemap, allElements: ElementStorage, changes: Changes, userDetails: UIEventSource<UserDetails>, selectedElement: UIEventSource<any>, asLayer(basemap: Basemap, allElements: ElementStorage, changes: Changes, userDetails: UIEventSource<UserDetails>, selectedElement: UIEventSource<any>,

View file

@ -5,12 +5,15 @@ import * as L from "leaflet";
import FixedText from "../Questions/FixedText"; import FixedText from "../Questions/FixedText";
import ParkingType from "../Questions/bike/ParkingType"; import ParkingType from "../Questions/bike/ParkingType";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; 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 { export default class BikeParkings extends LayerDefinition {
constructor() { constructor() {
super(); super();
this.name = "bike_parking"; this.name = Translations.t.cyclofix.parking.name.txt;
this.icon = "./assets/bike/parking.svg"; this.icon = "./assets/bike/parking.svg";
this.overpassFilter = new Tag("amenity", "bicycle_parking"); this.overpassFilter = new Tag("amenity", "bicycle_parking");
this.newElementTags = [ this.newElementTags = [
@ -20,10 +23,10 @@ export default class BikeParkings extends LayerDefinition {
this.minzoom = 13; this.minzoom = 13;
this.style = this.generateStyleFunction(); this.style = this.generateStyleFunction();
this.title = new FixedText("Fietsparking"); this.title = new FixedText(Translations.t.cyclofix.parking.title)
this.elementsToShow = [ this.elementsToShow = [
new ImageCarouselWithUploadConstructor(), new ImageCarouselWithUploadConstructor(),
new OperatorTag(), //new ParkingOperator(),
new ParkingType() new ParkingType()
]; ];
@ -36,7 +39,8 @@ export default class BikeParkings extends LayerDefinition {
color: "#00bb00", color: "#00bb00",
icon: L.icon({ icon: L.icon({
iconUrl: self.icon, iconUrl: self.icon,
iconSize: [50, 50] iconSize: [50, 50],
iconAnchor: [25,50]
}) })
}; };
}; };

View file

@ -0,0 +1,122 @@
import {TagRenderingOptions} from "../TagRendering";
import {LayerDefinition} from "../LayerDefinition";
import {And, Tag} from "../../Logic/TagsFilter";
import L from "leaflet";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import {NameQuestion} from "../Questions/NameQuestion";
export class BikeShop extends LayerDefinition {
private readonly sellsBikes = new Tag("service:bicycle:retail", "yes");
private readonly repairsBikes = new Tag("service:bicycle:repair", "yes");
constructor() {
super(
{
name: "bike shop or repair",
icon: "assets/bike/repair_shop.svg",
minzoom: 14,
overpassFilter: new Tag("shop", "bicycle"),
newElementTags: [new Tag("shop", "bicycle")]
}
);
this.title = new TagRenderingOptions({
mappings: [
{k: new And([new Tag("name", "*"), this.sellsBikes]), txt: "Bicycle shop {name}"},
{
k: new And([new Tag("name", "*"), new Tag("service:bicycle:retail", "no")]),
txt: "Bicycle repair {name}",
},
{
k: new And([new Tag("name", "*"), new Tag("service:bicycle:retail", "")]),
txt: "Bicycle repair {name}"
},
{k: this.sellsBikes, txt: "Bicycle shop"},
{k: new Tag("service:bicycle:retail", "no"), txt: "Bicycle repair"},
{k: new Tag("service:bicycle:retail", ""), txt: "Bicycle repair/shop"},
]
})
this.elementsToShow = [
new ImageCarouselWithUploadConstructor(),
new TagRenderingOptions({
question: "What is the name of this bicycle shop?",
freeform: {
key: "name",
renderTemplate: "The name of this bicycle shop is {name}",
template: "The name of this bicycle shop is $$$"
}
}),
new TagRenderingOptions({
question: "Can one buy a bike here?",
mappings: [
{k: this.sellsBikes, txt: "Bikes are sold here"},
{k: new Tag("service:bicycle:retail", "no"), txt: "No bikes are sold here"},
]
}),
new TagRenderingOptions({
question: "Can one buy a new bike here?",
mappings: [
{k: new Tag("service:bicycle:second_hand", "yes"), txt: "Second-hand bikes are sold here"},
{k: new Tag("service:bicycle:second_hand", "only"), txt: "All bicycles sold here are second-hand"},
{k: new Tag("service:bicycle:second_hand", "no"), txt: "Only brand new bikes are sold here"},
]
}).OnlyShowIf(this.sellsBikes),
new TagRenderingOptions({
question: "Does this shop repair bicycles?",
mappings: [
{k: this.repairsBikes, txt: "Bikes are repaired here, by the shop owner (for a fee)"},
{k: new Tag("service:bicycle:repair", "only_sold"), txt: "Only bikes that were bought here, are repaired"},
{k: new Tag("service:bicycle:repair", "brand"), txt: "Only bikes of a fixed brand are repaired here"},
{k: new Tag("service:bicycle:repair", "no"), txt: "Bikes are not repaired here"},
]
}),
new TagRenderingOptions({
question: "Can one hire a new bike here?",
mappings: [
{k: new Tag("service:bicycle:rental", "yes"), txt: "Bikes can be rented here"},
{k: new Tag("service:bicycle:rental", "no"), txt: "Bikes cannot be rented here"},
]
}).OnlyShowIf(this.sellsBikes),
new TagRenderingOptions({
question: "Are there tools here so that one can repair their own bike?",
mappings: [
{k: new Tag("service:bicycle:diy", "yes"), txt: "Tools for DIY are available here"},
{k: new Tag("service:bicycle:diy", "no"), txt: "No tools for DIY are available here"},
]
}),
]
this.style = (tags) => {
let icon = "assets/bike/repair_shop.svg";
if (this.sellsBikes.matchesProperties(tags)) {
icon = "assets/bike/shop.svg";
}
return {
color: "#ff0000",
icon: L.icon({
iconUrl: icon,
iconSize: [50, 50],
iconAnchor: [25, 50]
})
}
}
}
}

View file

@ -0,0 +1,73 @@
import { LayerDefinition } from "../LayerDefinition";
import Translations from "../../UI/i18n/Translations";
import { 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.txt
this.icon = "./assets/bike/repair_shop.svg"
this.overpassFilter = new Tag("shop", "bicycle");
this.newElementTags = [
new Tag("shop", "bicycle"),
]
this.maxAllowedOverlapPercentage = 10
this.minzoom = 13;
this.style = this.generateStyleFunction();
this.title = new TagRenderingOptions({
mappings: [
{k: this.sellsBikes, txt: "Bicycle shop"},
{k: new Tag("service:bicycle:retail", "no"), txt: Translations.t.cyclofix.shop.titleRepair},
{k: new Tag("service:bicycle:retail", ""), txt: Translations.t.cyclofix.shop.title},
]
})
this.elementsToShow = [
new ImageCarouselWithUploadConstructor(),
//new ParkingOperator(),
new ShopRetail(),
new ShopRental(),
new ShopRepair(),
new ShopPump(),
new ShopDiy(),
new ShopName(),
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]
})
}
}
}
}

View file

@ -12,6 +12,7 @@ import PumpManometer from "../Questions/bike/PumpManometer";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import PumpOperational from "../Questions/bike/PumpOperational"; import PumpOperational from "../Questions/bike/PumpOperational";
import PumpValves from "../Questions/bike/PumpValves"; import PumpValves from "../Questions/bike/PumpValves";
import Translations from "../../UI/i18n/Translations";
export default class BikeStations extends LayerDefinition { export default class BikeStations extends LayerDefinition {
@ -22,7 +23,7 @@ export default class BikeStations extends LayerDefinition {
constructor() { constructor() {
super(); super();
this.name = "bike station or pump"; this.name = Translations.t.cyclofix.station.name.txt;
this.icon = "./assets/wrench.svg"; this.icon = "./assets/wrench.svg";
this.overpassFilter = new And([ this.overpassFilter = new And([
@ -36,7 +37,7 @@ export default class BikeStations extends LayerDefinition {
this.minzoom = 13; this.minzoom = 13;
this.style = this.generateStyleFunction(); this.style = this.generateStyleFunction();
this.title = new FixedText("Bike station"); this.title = new FixedText(Translations.t.cyclofix.station.title.txt)
this.elementsToShow = [ this.elementsToShow = [
new ImageCarouselWithUploadConstructor(), new ImageCarouselWithUploadConstructor(),
@ -50,7 +51,7 @@ export default class BikeStations extends LayerDefinition {
new PumpValves().OnlyShowIf(this.pump), new PumpValves().OnlyShowIf(this.pump),
new PumpOperational().OnlyShowIf(this.pump), new PumpOperational().OnlyShowIf(this.pump),
new BikeStationOperator(), // new BikeStationOperator(),
// new BikeStationBrand() DISABLED // new BikeStationBrand() DISABLED
]; ];
} }
@ -73,14 +74,19 @@ export default class BikeStations extends LayerDefinition {
} }
} }
} else { } else {
iconName = "repair_station.svg" if (!self.pump.matchesProperties(properties)) {
iconName = "repair_station.svg"
} else {
iconName = "repair_station.svg"
}
} }
const iconUrl = `./assets/bike/${iconName}` const iconUrl = `./assets/bike/${iconName}`
return { return {
color: "#00bb00", color: "#00bb00",
icon: L.icon({ icon: L.icon({
iconUrl: iconUrl, iconUrl: iconUrl,
iconSize: [50, 50] iconSize: [50, 50],
iconAnchor: [25,50]
}) })
}; };
}; };

View file

@ -1,10 +1,8 @@
import {LayerDefinition} from "../LayerDefinition"; import {LayerDefinition} from "../LayerDefinition";
import L from "leaflet"; import L from "leaflet";
import {And, Or, Regex, Tag} from "../../Logic/TagsFilter"; import {And, Or, Tag} from "../../Logic/TagsFilter";
import {QuestionDefinition} from "../../Logic/Question";
import {TagRenderingOptions} from "../TagRendering"; import {TagRenderingOptions} from "../TagRendering";
import {NameInline} from "../Questions/NameInline"; import {NameInline} from "../Questions/NameInline";
import {NameQuestion} from "../Questions/NameQuestion";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
export class Bookcases extends LayerDefinition { export class Bookcases extends LayerDefinition {
@ -121,7 +119,10 @@ export class Bookcases extends LayerDefinition {
key: "ref", key: "ref",
template: "Het referentienummer is $$$", template: "Het referentienummer is $$$",
renderTemplate: "Gekend als {brand} <b>{ref}</b>" renderTemplate: "Gekend als {brand} <b>{ref}</b>"
} },
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","*")), }).OnlyShowIf(new Tag("brand","*")),
new TagRenderingOptions({ new TagRenderingOptions({

View file

@ -10,8 +10,8 @@ export class DrinkingWater extends LayerDefinition {
constructor() { constructor() {
super(); super();
this.name = "drinking_water"; this.name = "drinking water";
this.icon = "./assets/bug.svg"; this.icon = "./assets/bike/drinking_water.svg";
this.overpassFilter = new Or([ this.overpassFilter = new Or([
new And([ new And([
@ -52,7 +52,8 @@ export class DrinkingWater extends LayerDefinition {
color: "#00bb00", color: "#00bb00",
icon: new L.icon({ icon: new L.icon({
iconUrl: self.icon, iconUrl: self.icon,
iconSize: [40, 40] iconSize: [50, 50],
iconAnchor: [25,50]
}) })
}; };
}; };

View file

@ -1,20 +1,30 @@
import {LayerDefinition} from "./LayerDefinition"; 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). * A layout is a collection of settings of the global view (thus: welcome text, title, selection of layers).
*/ */
export class Layout { export class Layout {
public name: string; public name: string;
public title: string; public title: UIElement;
public layers: LayerDefinition[]; public layers: LayerDefinition[];
public welcomeMessage: string; public welcomeMessage: UIElement;
public gettingStartedPlzLogin: string; public gettingStartedPlzLogin: UIElement;
public welcomeBackMessage: string; public welcomeBackMessage: UIElement;
public welcomeTail: UIElement;
public startzoom: number; public startzoom: number;
public supportedLanguages: string[];
public startLon: number; public startLon: number;
public startLat: number; public startLat: number;
public welcomeTail: string;
public locationContains: string[]; public locationContains: string[];
@ -33,26 +43,79 @@ export class Layout {
*/ */
constructor( constructor(
name: string, name: string,
title: string, supportedLanguages: string[],
title: UIElement | string,
layers: LayerDefinition[], layers: LayerDefinition[],
startzoom: number, startzoom: number,
startLat: number, startLat: number,
startLon: number, startLon: number,
welcomeMessage: string, welcomeMessage: UIElement | string,
gettingStartedPlzLogin: string = "Please login to get started", gettingStartedPlzLogin: UIElement | string = Translations.t.general.getStarted,
welcomeBackMessage: string = "You are logged in. Welcome back!", welcomeBackMessage: UIElement | string = Translations.t.general.welcomeBack,
welcomeTail: string = "" welcomeTail: UIElement | string = ""
) { ) {
this.title = title; this.supportedLanguages = supportedLanguages;
this.title = typeof (title) === 'string' ? new FixedUiElement(title) : title;
this.startLon = startLon; this.startLon = startLon;
this.startLat = startLat; this.startLat = startLat;
this.startzoom = startzoom; this.startzoom = startzoom;
this.name = name; this.name = name;
this.layers = layers; this.layers = layers;
this.welcomeMessage = welcomeMessage; this.welcomeMessage = Translations.W(welcomeMessage)
this.gettingStartedPlzLogin = gettingStartedPlzLogin; this.gettingStartedPlzLogin = Translations.W(gettingStartedPlzLogin);
this.welcomeBackMessage = welcomeBackMessage; this.welcomeBackMessage = Translations.W(welcomeBackMessage);
this.welcomeTail = welcomeTail; this.welcomeTail = Translations.W(welcomeTail);
}
}
export class WelcomeMessage extends UIElement {
private readonly layout: Layout;
private readonly userDetails: UIEventSource<UserDetails>;
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 "<div id='welcomeMessage'>" +
this.description.Render() +
"<br/>"+
(this.userDetails.data.loggedIn ? this.welcomeBack : this.plzLogIn).Render() +
"<br/>"+
this.tail.Render() +
"</div>"
;
/*
return new VariableUiElement(
this.userDetails.map((userdetails) => {
}),
function () {
}).ListenTo(Locale.language);*/
}
protected InnerUpdate(htmlElement: HTMLElement) {
this.osmConnection.registerActivateOsmAUthenticationClass()
} }
} }

View file

@ -4,6 +4,7 @@ export class All extends Layout{
constructor() { constructor() {
super( super(
"all", "all",
["en"],
"All quest layers", "All quest layers",
[], [],
15, 15,

View file

@ -4,6 +4,7 @@ import * as Layer from "../Layers/Bookcases";
export class Bookcases extends Layout{ export class Bookcases extends Layout{
constructor() { constructor() {
super( "bookcases", super( "bookcases",
["nl"],
"Open Bookcase Map", "Open Bookcase Map",
[new Layer.Bookcases()], [new Layer.Bookcases()],
14, 14,

View file

@ -1,30 +1,32 @@
import {Layout} from "../Layout"; import {Layout} from "../Layout";
import BikeParkings from "../Layers/BikeParkings"; import BikeParkings from "../Layers/BikeParkings";
import BikeServices from "../Layers/BikeStations"; import BikeServices from "../Layers/BikeStations";
import BikeShops from "../Layers/BikeShops";
import {GhostBike} from "../Layers/GhostBike"; import {GhostBike} from "../Layers/GhostBike";
import {DrinkingWater, DrinkingWaterLayer} from "../Layers/DrinkingWater"; import Translations from "../../UI/i18n/Translations";
import {DrinkingWater} from "../Layers/DrinkingWater";
import {BikeShop} from "../Layers/BikeShop"
import Combine from "../../UI/Base/Combine";
export default class Cyclofix extends Layout { export default class Cyclofix extends Layout {
constructor() { constructor() {
super( super(
"pomp", "pomp",
"Cyclofix bicycle infrastructure", ["en", "nl", "fr"],
[new GhostBike(), new BikeServices(), new BikeParkings(), new DrinkingWater()], Translations.t.cyclofix.title,
[new BikeServices(), new BikeShop(), new DrinkingWater(), new BikeParkings()],
16, 16,
50.8465573, 50.8465573,
4.3516970, 4.3516970,
/* Translations.t.cyclofix.title/*/
new Combine([
"<h3>Cyclofix bicycle infrastructure</h3>\n" + "<h3>",
"\n" + Translations.t.cyclofix.title,
"<p><b>EN&gt;</b> On this map we want to collect data about the whereabouts of bicycle pumps and public racks in Brussels." + "</h3><br/><p>",
"As a result, cyclists will be able to quickly find the nearest infrastructure for their needs.</p>" + Translations.t.cyclofix.description,
"<p><b>NL&gt;</b> Op deze kaart willen we gegevens verzamelen over de locatie van fietspompen en openbare stelplaatsen in Brussel." + "</p>"
"Hierdoor kunnen fietsers snel de dichtstbijzijnde infrastructuur vinden die voldoet aan hun behoeften.</p>" + ])//*/
"<p><b>FR&gt;</b> 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.</p>"
,
"", "");
} }
} }

View file

@ -4,6 +4,7 @@ import {GrbToFix} from "../Layers/GrbToFix";
export class GRB extends Layout { export class GRB extends Layout {
constructor() { constructor() {
super("grb", super("grb",
["en"],
"Grb import fix tool", "Grb import fix tool",
[new GrbToFix()], [new GrbToFix()],
15, 15,

View file

@ -7,6 +7,7 @@ export class Groen extends Layout {
constructor() { constructor() {
super("buurtnatuur", super("buurtnatuur",
["nl"],
"Buurtnatuur", "Buurtnatuur",
[new NatureReserves(), new Park(), new Bos()], [new NatureReserves(), new Park(), new Bos()],
10, 10,

View file

@ -5,6 +5,7 @@ import {Map} from "../Layers/Map";
export class MetaMap extends Layout{ export class MetaMap extends Layout{
constructor() { constructor() {
super( "metamap", super( "metamap",
["en"],
"Open Map Map", "Open Map Map",
[new Map()], [new Map()],
1, 1,

View file

@ -7,6 +7,7 @@ export class Natuurpunt extends Layout{
constructor() { constructor() {
super( super(
"natuurpunt", "natuurpunt",
["nl"],
"De natuur in", "De natuur in",
[new Birdhide(), new InformationBoard(), new NatureReserves(true)], [new Birdhide(), new InformationBoard(), new NatureReserves(true)],
12, 12,

View file

@ -5,6 +5,7 @@ export class Statues extends Layout{
constructor() { constructor() {
super( "statues", super( "statues",
"Open Artwork Map", "Open Artwork Map",
["en"],
[new Artwork()], [new Artwork()],
10, 10,
50.8435, 50.8435,

View file

@ -7,6 +7,7 @@ export class StreetWidth extends Layout{
constructor() { constructor() {
super( "width", super( "width",
["nl"],
"Straatbreedtes in Brugge", "Straatbreedtes in Brugge",
[new Widths( [new Widths(
2, 2,

View file

@ -4,6 +4,7 @@ import * as Layer from "../Layers/Toilets";
export class Toilets extends Layout{ export class Toilets extends Layout{
constructor() { constructor() {
super( "toilets", super( "toilets",
["en"],
"Open Toilet Map", "Open Toilet Map",
[new Layer.Toilets()], [new Layer.Toilets()],
12, 12,

View file

@ -6,6 +6,7 @@ import { Park } from "../Layers/Park";
export class WalkByBrussels extends Layout { export class WalkByBrussels extends Layout {
constructor() { constructor() {
super("walkbybrussels", super("walkbybrussels",
["en","fr","nl"],
"Drinking Water Spots", "Drinking Water Spots",
[new DrinkingWater(), new Park(), new NatureReserves()], [new DrinkingWater(), new Park(), new NatureReserves()],
10, 10,

View file

@ -1,7 +1,8 @@
import { TagRenderingOptions } from "../TagRendering"; import { TagRenderingOptions } from "../TagRendering";
import {UIElement} from "../../UI/UIElement";
export default class FixedText extends TagRenderingOptions { export default class FixedText extends TagRenderingOptions {
constructor(category: string) { constructor(category: string | UIElement) {
super({ super({
mappings: [ mappings: [
{ {

View file

@ -1,5 +1,6 @@
import {TagRenderingOptions} from "../TagRendering"; import {TagRenderingOptions} from "../TagRendering";
import {And, Tag} from "../../Logic/TagsFilter"; import {And, Tag} from "../../Logic/TagsFilter";
import {UIElement} from "../../UI/UIElement";
export class NameInline extends TagRenderingOptions{ export class NameInline extends TagRenderingOptions{
@ -8,7 +9,7 @@ export class NameInline extends TagRenderingOptions{
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
} }
constructor(category: string) { constructor(category: string ) {
super({ super({
question: "", question: "",

View file

@ -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.txt,
renderTemplate: to.render.txt,
placeholder: Translations.t.cyclofix.freeFormPlaceholder.txt
},
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()}
]
});
}
}

View file

@ -1,38 +1,61 @@
import {TagRenderingOptions} from "../../TagRendering"; import {TagRenderingOptions} from "../../TagRendering";
import {Tag} from "../../../Logic/TagsFilter"; 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) => `<img src=${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 { 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 `<img src=${url}>`
}
constructor() { constructor() {
const to = Translations.t.cyclofix.parking.type
super({ super({
priority: 5, priority: 5,
question: "Van welk type is deze fietsenparking?", question: to.question,
freeform: { freeform: {
key: "bicycle_parking", key: "bicycle_parking",
extraTags: new Tag("fixme", "Freeform bicycle_parking= tag used: possibly a wrong value"), extraTags: new Tag("fixme", "Freeform bicycle_parking= tag used: possibly a wrong value"),
template: "Iets anders: $$$", template: to.template.txt,
renderTemplate: "Dit is een fietsenparking van het type: {bicycle_parking}", renderTemplate: to.render.txt,
placeholder: "Specifieer" placeholder: Translations.t.cyclofix.freeFormPlaceholder,
}, },
mappings: [ mappings: ParkingTypeHelper.GenerateMappings()
{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"])}
]
}); });
} }
} }

View file

@ -1,16 +1,18 @@
import {TagRenderingOptions} from "../../TagRendering"; import {TagRenderingOptions} from "../../TagRendering";
import {Tag} from "../../../Logic/TagsFilter"; import {Tag} from "../../../Logic/TagsFilter";
import Translations from "../../../UI/i18n/Translations";
export default class PumpManometer extends TagRenderingOptions { export default class PumpManometer extends TagRenderingOptions {
constructor() { constructor() {
const to = Translations.t.cyclofix.station.manometer
super({ super({
question: "Does the pump have a pressure indicator or manometer?", question: to.question,
mappings: [ mappings: [
{k: new Tag("manometer", "yes"), txt: "Yes, there is a manometer"}, {k: new Tag("manometer", "yes"), txt: to.yes},
{k: new Tag("manometer","broken"), txt: "Yes, but it is broken"}, {k: new Tag("manometer", "no"), txt: to.no},
{k: new Tag("manometer", "yes"), txt: "No"} {k: new Tag("manometer", "broken"), txt: to.broken}
] ]
}); });
} }
} }

View file

@ -1,15 +1,17 @@
import {TagRenderingOptions} from "../../TagRendering"; import {TagRenderingOptions} from "../../TagRendering";
import {Tag} from "../../../Logic/TagsFilter"; import {Tag} from "../../../Logic/TagsFilter";
import Translations from "../../../UI/i18n/Translations";
export default class PumpManual extends TagRenderingOptions { export default class PumpManual extends TagRenderingOptions {
constructor() { constructor() {
const to = Translations.t.cyclofix.station.electric
super({ super({
priority: 5, priority: 5,
question: "Is this an electric bike pump?", question: to.question.Render(),
mappings: [ mappings: [
{k: new Tag("manual", "yes"), txt: "Manual pump"}, {k: new Tag("manual", "yes"), txt: to.manual.Render()},
{k: new Tag("manual", "no"), txt: "Electric pump"} {k: new Tag("manual", "no"), txt: to.electric.Render()}
] ]
}); });
} }

View file

@ -1,14 +1,16 @@
import {TagRenderingOptions} from "../../TagRendering"; import {TagRenderingOptions} from "../../TagRendering";
import {Tag} from "../../../Logic/TagsFilter"; import {Tag} from "../../../Logic/TagsFilter";
import Translations from "../../../UI/i18n/Translations";
export default class PumpOperational extends TagRenderingOptions { export default class PumpOperational extends TagRenderingOptions {
constructor() { constructor() {
const to = Translations.t.cyclofix.station.operational
super({ super({
question: "Is the bicycle pump still operational?", question: to.question.Render(),
mappings: [ mappings: [
{k: new Tag("service:bicycle:pump:operational_status","broken"), txt: "This pump is broken"}, {k: new Tag("service:bicycle:pump:operational_status","broken"), txt: to.broken.txt},
{k: new Tag("service:bicycle:pump:operational_status",""), txt: "This pump is operational"} {k: new Tag("service:bicycle:pump:operational_status",""), txt: to.operational.txt}
] ]
}); });
} }

View file

@ -1,24 +1,27 @@
import {TagRenderingOptions} from "../../TagRendering"; import {TagRenderingOptions} from "../../TagRendering";
import {Tag} from "../../../Logic/TagsFilter"; import {Tag} from "../../../Logic/TagsFilter";
import Translations from "../../../UI/i18n/Translations";
export default class PumpValves extends TagRenderingOptions{ export default class PumpValves extends TagRenderingOptions{
constructor() { constructor() {
const to = Translations.t.cyclofix.station.valves
super({ super({
question: "What valves are supported?", question: to.question.Render(),
mappings: [ mappings: [
{ {
k: new Tag("valves", " sclaverand;schrader;dunlop"), k: new Tag("valves", " sclaverand;schrader;dunlop"),
txt: "There is a default head, so Presta, Dunlop and Auto" txt: to.default.Render()
}, },
{k: new Tag("valves", "dunlop"), txt: "Only dunlop"}, {k: new Tag("valves", "dunlop"), txt: to.dunlop.Render()},
{k: new Tag("valves", "sclaverand"), txt: "Only Sclaverand (also known as Dunlop)"}, {k: new Tag("valves", "sclaverand"), txt: to.sclaverand.Render()},
{k: new Tag("valves", "auto"), txt: "Only auto"}, {k: new Tag("valves", "auto"), txt: to.auto.Render()},
], ],
freeform: { freeform: {
extraTags: new Tag("fixme", "Freeform valves= tag used: possibly a wrong value"),
key: "valves", key: "valves",
template: "Supported valves are $$$", template: to.template.txt,
renderTemplate: "Supported valves are {valves}" renderTemplate: to.render.txt
} }
}); });
} }

View file

@ -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.cylofix.shop.diy
super({
priority: 5,
question: to.question.Render(),
mappings: [
{k: new Tag(key, "yes"), txt: to.yes.Render()},
{k: new Tag(key, "no"), txt: to.no.Render()},
]
});
}
}

View file

@ -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.cylofix.shop.qName
super({
priority: 5,
question: to.question.Render(),
freeform: {
key: "name",
renderTemplate: to.render.txt,
template: to.template.txt
}
})
}
}

View file

@ -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.Render(),
mappings: [
{k: new Tag(key, "yes"), txt: to.yes.Render()},
{k: new Tag(key, "no"), txt: to.no.Render()},
]
});
}
}

View file

@ -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.Render(),
mappings: [
{k: new Tag(key, "yes"), txt: to.yes.Render()},
{k: new Tag(key, "no"), txt: to.no.Render()},
]
});
}
}

View file

@ -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.Render(),
mappings: [
{k: new Tag(key, "yes"), txt: to.yes.Render()},
{k: new Tag(key, "only_sold"), txt: to.sold.Render()},
{k: new Tag(key, "brand"), txt: to.brand.Render()},
{k: new Tag(key, "no"), txt: to.no.Render()},
]
});
}
}

View file

@ -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},
]
});
}
}

View file

@ -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.cylofix.shop.secondHand
super({
priority: 5,
question: to.question.Render(),
mappings: [
{k: new Tag(key, "yes"), txt: to.yes.Render()},
{k: new Tag(key, "no"), txt: to.no.Render()},
{k: new Tag(key, "only"), txt: to.only.Render()},
]
});
}
}

View file

@ -4,6 +4,8 @@ import {Tag} from "../../../Logic/TagsFilter";
/** /**
* Currently not used in Cyclofix because it's a little vague * Currently not used in Cyclofix because it's a little vague
*
* TODO: Translations
*/ */
export default class BikeStationBrand extends TagRenderingOptions { export default class BikeStationBrand extends TagRenderingOptions {
private static options = { private static options = {

View file

@ -1,15 +1,17 @@
import {TagRenderingOptions} from "../../TagRendering"; import {TagRenderingOptions} from "../../TagRendering";
import {Tag} from "../../../Logic/TagsFilter"; import {Tag} from "../../../Logic/TagsFilter";
import Translations from "../../../UI/i18n/Translations";
export default class StationChain extends TagRenderingOptions { export default class StationChain extends TagRenderingOptions {
constructor() { constructor() {
const to = Translations.t.cyclofix.station.chain
super({ super({
priority: 5, priority: 5,
question: "Does this bike station have a special tool to repair your bike chain?", question: to.question.Render(),
mappings: [ mappings: [
{k: new Tag("service:bicycle:chain_tool", "yes"), txt: "There is a chain tool."}, {k: new Tag("service:bicycle:chain_tool", "yes"), txt: to.yes.Render()},
{k: new Tag("service:bicycle:chain_tool", "no"), txt: "There is no chain tool."}, {k: new Tag("service:bicycle:chain_tool", "no"), txt: to.no.Render()},
] ]
}); });
} }

View file

@ -1,25 +1,27 @@
import {TagRenderingOptions} from "../../TagRendering"; import {TagRenderingOptions} from "../../TagRendering";
import {Tag} from "../../../Logic/TagsFilter"; import {Tag} from "../../../Logic/TagsFilter";
import Translations from "../../../UI/i18n/Translations";
export default class BikeStationOperator extends TagRenderingOptions { export default class BikeStationOperator extends TagRenderingOptions {
constructor() { constructor() {
const to = Translations.t.cyclofix.station.operator
super({ super({
priority: 15, priority: 15,
question: "Who operates this bike station (name of university, shop, city...)?", question: to.question,
freeform: {
key: "operator",
template: "This bike station is operated by $$$",
renderTemplate: "This bike station is operated by {operator}",
placeholder: "organisatie"
},
mappings: [ mappings: [
{k: new Tag("operator", "KU Leuven"), txt: "KU Leuven"}, {k: new Tag("operator", "KU Leuven"), txt: "KU Leuven"},
{k: new Tag("operator", "Stad Halle"), txt: "Stad Halle"}, {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", "Saint Gilles - Sint Gillis"), txt: "Saint Gilles - Sint Gillis"},
{k: new Tag("operator", "Jette"), txt: "Jette"}, {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.txt,
renderTemplate: to.render.txt,
placeholder: "organisatie"
}
}); });
} }
} }

View file

@ -1,16 +1,18 @@
import {TagRenderingOptions} from "../../TagRendering"; import {TagRenderingOptions} from "../../TagRendering";
import {Tag, And} from "../../../Logic/TagsFilter"; import {Tag, And} from "../../../Logic/TagsFilter";
import Translations from "../../../UI/i18n/Translations";
export default class BikeStationPumpTools extends TagRenderingOptions { export default class BikeStationPumpTools extends TagRenderingOptions {
constructor() { constructor() {
const to = Translations.t.cyclofix.station.services
super({ super({
priority: 15, priority: 15,
question: "Which services are available at this bike station?", question: to.question,
mappings: [ 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", "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: "There are only tools (screwdrivers, pliers...) available."}, {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: "There are both tools and a pump available."} {k: new And([new Tag("service:bicycle:tools", "yes"), new Tag("service:bicycle:pump", "yes")]), txt: to.both}
] ]
}); });
} }

View file

@ -1,14 +1,16 @@
import {TagRenderingOptions} from "../../TagRendering"; import {TagRenderingOptions} from "../../TagRendering";
import {Tag} from "../../../Logic/TagsFilter"; import {Tag} from "../../../Logic/TagsFilter";
import Translations from "../../../UI/i18n/Translations";
export default class BikeStationStand extends TagRenderingOptions { export default class BikeStationStand extends TagRenderingOptions {
constructor() { constructor() {
const to = Translations
super({ super({
priority: 10, priority: 10,
question: "Does this bike station have a hook to suspend your bike with or a stand to elevate it?", question: "Does this bike station have a hook to suspend your bike with or a stand to elevate it?",
mappings: [ 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"}, {k: new Tag("service:bicycle:stand", "no"), txt: "There is no hook or stand"},
] ]
}); });

View file

@ -1,17 +1,20 @@
import {UIElement} from "../UI/UIElement"; import {UIElement} from "../UI/UIElement";
import {UIEventSource} from "../UI/UIEventSource"; import {UIEventSource} from "../UI/UIEventSource";
import {And, Tag, TagsFilter, TagUtils} from "../Logic/TagsFilter"; import {And, Tag, TagsFilter, TagUtils} from "../Logic/TagsFilter";
import {UIRadioButton} from "../UI/Base/UIRadioButton";
import {FixedUiElement} from "../UI/Base/FixedUiElement"; import {FixedUiElement} from "../UI/Base/FixedUiElement";
import {SaveButton} from "../UI/SaveButton"; import {SaveButton} from "../UI/SaveButton";
import {Changes} from "../Logic/Changes"; 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 {VariableUiElement} from "../UI/Base/VariableUIElement";
import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor"; import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor";
import {OnlyShowIfConstructor} from "./OnlyShowIf"; import {OnlyShowIfConstructor} from "./OnlyShowIf";
import {UserDetails} from "../Logic/OsmConnection"; 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 { export class TagRenderingOptions implements TagDependantUIElementConstructor {
@ -20,8 +23,17 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
*/ */
public options: { public options: {
priority?: number; question?: string; primer?: string; priority?: number;
freeform?: { key: string; tagsPreprocessor?: (tags: any) => any; template: string; renderTemplate: string; placeholder?: string; extraTags?: TagsFilter }; mappings?: { k: TagsFilter; txt: string; priority?: number, substitute?: boolean }[] question?: string | UIElement;
freeform?: {
key: string;
tagsPreprocessor?: (tags: any) => any;
template: string;
renderTemplate: string;
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 'question' is undefined, then the question is never asked at all
* If the question is "" (empty string) then the question is * If the question is "" (empty string) then the question is
*/ */
question?: string, question?: UIElement | string,
/** /**
* What is the priority of the question. * 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 }[],
/** /**
@ -67,17 +79,11 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
freeform?: { freeform?: {
key: string, template: string, key: string, template: string,
renderTemplate: string renderTemplate: string
placeholder?: string, placeholder?: string | UIElement,
extraTags?: TagsFilter, 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 * In some very rare cases, tags have to be rewritten before displaying
* This function can be used for that. * This function can be used for that.
@ -85,6 +91,7 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
*/ */
tagsPreprocessor?: ((tags: any) => void) tagsPreprocessor?: ((tags: any) => void)
}) { }) {
this.options = options; this.options = options;
} }
@ -129,29 +136,25 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
class TagRendering extends UIElement implements TagDependantUIElement { class TagRendering extends UIElement implements TagDependantUIElement {
private _priority: number;
private _userDetails: UIEventSource<UserDetails>; private _userDetails: UIEventSource<UserDetails>;
private _priority: number;
Priority(): number {
return this._priority;
}
private _question: string; private _question: UIElement;
private _primer: string; private _mapping: { k: TagsFilter, txt: UIElement, priority?: number }[];
private _mapping: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[]; private _renderMapping: { k: TagsFilter, txt: UIElement, priority?: number }[];
private _renderMapping: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[];
private _tagsPreprocessor?: ((tags: any) => any); private _tagsPreprocessor?: ((tags: any) => any);
private _freeform: { private _freeform: {
key: string, template: string, key: string, template: string,
renderTemplate: string, renderTemplate: string,
placeholder?: string, placeholder?: string | UIElement,
extraTags?: TagsFilter extraTags?: TagsFilter
}; };
private readonly _questionElement: UIElement;
private readonly _textField: TextField<TagsFilter>; // Only here to update private readonly _questionElement: InputElement<TagsFilter>;
private readonly _saveButton: UIElement; private readonly _saveButton: UIElement;
private readonly _skipButton: UIElement; private readonly _skipButton: UIElement;
@ -165,19 +168,19 @@ class TagRendering extends UIElement implements TagDependantUIElement {
constructor(tags: UIEventSource<any>, changes: Changes, options: { constructor(tags: UIEventSource<any>, changes: Changes, options: {
priority?: number priority?: number
question?: string, question?: string | UIElement,
primer?: string,
freeform?: { freeform?: {
key: string, template: string, key: string, template: string,
renderTemplate: string renderTemplate: string
placeholder?: string, placeholder?: string | UIElement,
extraTags?: TagsFilter, extraTags?: TagsFilter,
}, },
tagsPreprocessor?: ((tags: any) => any), 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); super(tags);
this.ListenTo(Locale.language);
const self = this; const self = this;
this.ListenTo(this._questionSkipped); this.ListenTo(this._questionSkipped);
this.ListenTo(this._editMode); this.ListenTo(this._editMode);
@ -185,9 +188,10 @@ class TagRendering extends UIElement implements TagDependantUIElement {
this._userDetails = changes.login.userDetails; this._userDetails = changes.login.userDetails;
this.ListenTo(this._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._priority = options.priority ?? 0;
this._primer = options.primer ?? "";
this._tagsPreprocessor = function (properties) { this._tagsPreprocessor = function (properties) {
if (options.tagsPreprocessor === undefined) { if (options.tagsPreprocessor === undefined) {
return properties; return properties;
@ -204,94 +208,36 @@ class TagRendering extends UIElement implements TagDependantUIElement {
this._renderMapping = []; this._renderMapping = [];
this._freeform = options.freeform; this._freeform = options.freeform;
// Prepare the choices for the Radio buttons
const choices: UIElement[] = [];
const usedChoices: string [] = [];
for (const choice of options.mappings ?? []) { for (const choice of options.mappings ?? []) {
if (choice.k === null) { let choiceSubbed = {
this._mapping.push(choice); k: choice.k,
continue; txt: this.ApplyTemplate(choice.txt),
} priority: choice.priority
let choiceSubbed = choice; };
if (choice.substitute) { if (choice.substitute) {
choiceSubbed = { choiceSubbed = {
k: choice.k.substituteValues( k: choice.k.substituteValues(
options.tagsPreprocessor(this._source.data)), options.tagsPreprocessor(this._source.data)),
txt: this.ApplyTemplate(choice.txt), txt: this.ApplyTemplate(choice.txt),
substitute: false,
priority: choice.priority priority: choice.priority
} }
} }
const txt = choiceSubbed.txt this._mapping.push({
// Choices is what is shown in the radio buttons k: choiceSubbed.k,
if (usedChoices.indexOf(txt) < 0) { txt: choiceSubbed.txt
});
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
}
} }
// 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 // Prepare the actual input element -> pick an appropriate implementation
let inputElement: UIInputElement<TagsFilter>;
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<string>(this._freeform.placeholder), pickString);
inputElement = this._textField;
this._questionElement = new FixedUiElement(
"<div>" + this._freeform.template.replace("$$$", inputElement.Render()) + "</div>")
} else {
throw "Invalid questionRendering, expected at least choices or a freeform"
}
this._questionElement = this.InputElementFor(options);
const save = () => { const save = () => {
const selection = inputElement.GetValue().data; const selection = self._questionElement.GetValue().data;
if (selection) { if (selection) {
changes.addTag(tags.data.id, selection); changes.addTag(tags.data.id, selection);
} }
@ -305,42 +251,128 @@ class TagRendering extends UIElement implements TagDependantUIElement {
} }
// Setup the save button and it's action // Setup the save button and it's action
this._saveButton = new SaveButton(inputElement.GetValue()) this._saveButton = new SaveButton(this._questionElement.GetValue())
.onClick(save); .onClick(save);
this._editButton = new FixedUiElement("");
if (this._question !== undefined) { if (this._question !== undefined) {
this._editButton = new FixedUiElement("<img class='editbutton' src='./assets/pencil.svg' alt='edit'>") this._editButton = new FixedUiElement("<img class='editbutton' src='./assets/pencil.svg' alt='edit'>")
.onClick(() => { .onClick(() => {
console.log("Click", self._editButton);
if (self._textField) {
self._textField.value.setData(self._source.data["name"] ?? "");
}
self._editMode.setData(true); self._editMode.setData(true);
self._questionElement.GetValue().setData(self.CurrentValue());
}); });
} else {
this._editButton = new FixedUiElement("");
} }
const cancelContents = this._editMode.map((isEditing) => { const cancelContents = this._editMode.map((isEditing) => {
if (isEditing) { if (isEditing) {
return "<span class='skip-button'>Annuleren</span>"; return "<span class='skip-button'>"+Translations.t.general.cancel.R()+"</span>";
} else { } else {
return "<span class='skip-button'>Overslaan (Ik weet het niet zeker...)</span>"; return "<span class='skip-button'>"+Translations.t.general.skip.R()+"</span>";
} }
}); });
// And at last, set up the skip button // 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,
renderTemplate: string
placeholder?: string | UIElement,
extraTags?: TagsFilter,
},
mappings?: { k: TagsFilter, txt: string | UIElement, priority?: number, substitute?: boolean }[]
}):
InputElement<TagsFilter> {
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); private InputElementForMapping(mapping: { k: TagsFilter, txt: string | UIElement }) {
return TagUtils.ApplyTemplate(template, tags); return new FixedInputElement(mapping.txt, mapping.k);
} }
private InputForFreeForm(freeform): InputElement<TagsFilter> {
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<TagsFilter>;
const textField = new TextField({
placeholder: this._freeform.placeholder,
fromString: pickString,
toString: toString
});
const prepost = freeform.template.split("$$$");
return new InputElementWrapper(prepost[0], textField, prepost[1]);
}
IsKnown(): boolean { IsKnown(): boolean {
const tags = TagUtils.proprtiesToKV(this._source.data); const tags = TagUtils.proprtiesToKV(this._source.data);
@ -349,10 +381,28 @@ class TagRendering extends UIElement implements TagDependantUIElement {
return true; return true;
} }
} }
return this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined; return this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined;
} }
private CurrentValue(): TagsFilter {
console.log("Creating a current value...")
const tags = TagUtils.proprtiesToKV(this._source.data);
for (const oneOnOneElement of this._mapping.concat(this._renderMapping)) {
if (oneOnOneElement.k !== null && oneOnOneElement.k.matches(tags)) {
return oneOnOneElement.k;
}
}
if (this._freeform === undefined) {
return undefined;
}
console.log("Got a freeform tag:", new Tag(this._freeform.key, this._source.data[this._freeform.key]))
return new Tag(this._freeform.key, this._source.data[this._freeform.key]);
}
IsQuestioning(): boolean { IsQuestioning(): boolean {
if (this.IsKnown()) { if (this.IsKnown()) {
return false; return false;
@ -368,10 +418,10 @@ class TagRendering extends UIElement implements TagDependantUIElement {
return true; return true;
} }
private RenderAnwser(): string { private RenderAnwser(): UIElement {
const tags = TagUtils.proprtiesToKV(this._source.data); const tags = TagUtils.proprtiesToKV(this._source.data);
let freeform = ""; let freeform: UIElement = new FixedUiElement("");
let freeformScore = -10; let freeformScore = -10;
if (this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined) { if (this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined) {
freeform = this.ApplyTemplate(this._freeform.renderTemplate); freeform = this.ApplyTemplate(this._freeform.renderTemplate);
@ -379,58 +429,60 @@ class TagRendering extends UIElement implements TagDependantUIElement {
} }
let highestScore = -100; let highestScore = -100;
let highestTemplate = undefined; let highestTemplate = undefined;
for (const oneOnOneElement of this._mapping.concat(this._renderMapping)) { for (const oneOnOneElement of this._mapping.concat(this._renderMapping)) {
if (oneOnOneElement.k == null || if (oneOnOneElement.k == null ||
oneOnOneElement.k.matches(tags)) { oneOnOneElement.k.matches(tags)) {
// We have found a matching key -> we use the template, but only if it scores better // We have found a matching key -> we use the template, but only if it scores better
let score = oneOnOneElement.priority ?? let score = oneOnOneElement.priority ??
(oneOnOneElement.k === null ? -1 : 0); (oneOnOneElement.k === null ? -1 : 0);
if (score > highestScore) { if (score > highestScore) {
highestScore = score; highestScore = score;
highestTemplate = oneOnOneElement.txt highestTemplate = oneOnOneElement.txt
}
} }
} }
}
if (freeformScore > highestScore) { if (freeformScore > highestScore) {
return freeform; 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) { if (this.IsQuestioning() || this._editMode.data) {
// Not yet known or questioning, we have to ask a question // Not yet known or questioning, we have to ask a question
const question = this._question.Render();
return "<div class='question'>" + return "<div class='question'>" +
"<span class='question-text'>" + this._question + "</span>" + "<span class='question-text'>" + question + "</span>" +
(this._question !== "" ? "<br/>" : "") + (this._question.IsEmpty() ? "" : "<br/>") +
this._questionElement.Render() + "<div>" + this._questionElement.Render() + "</div>" +
this._skipButton.Render() + this._skipButton.Render() +
this._saveButton.Render() + this._saveButton.Render() +
"</div>" "</div>"
} }
if (this.IsKnown()) { if (this.IsKnown()) {
const html = this.RenderAnwser(); const answer = this.RenderAnwser()
if (html == "") { if (answer.IsEmpty()) {
return ""; return "";
} }
const html = answer.Render();
let editButton = ""; let editButton = "";
if(this._userDetails.data.loggedIn){ if (this._userDetails.data.loggedIn && this._question !== undefined) {
editButton = this._editButton.Render(); editButton = this._editButton.Render();
} }
return "<span class='answer'>" + return "<span class='answer'>" +
"<span class='answer-text'>" + html + "</span>" + "<span class='answer-text'>" + html + "</span>" +
editButton + editButton +
@ -441,13 +493,23 @@ class TagRendering extends UIElement implements TagDependantUIElement {
} }
Priority(): number {
return this._priority;
}
private ApplyTemplate(template: string | UIElement): UIElement {
if (template instanceof UIElement) {
return template;
}
const tags = this._tagsPreprocessor(this._source.data);
return new FixedUiElement(TagUtils.ApplyTemplate(template, tags));
}
InnerUpdate(htmlElement: HTMLElement) { InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement); super.InnerUpdate(htmlElement);
this._questionElement.Update(); this._questionElement.Update(); // Another manual update for them
this._saveButton.Update();
this._skipButton.Update();
this._textField?.Update();
this._editButton.Update();
} }
} }

View file

@ -6,7 +6,6 @@ import {OsmConnection} from "./OsmConnection";
import {OsmNode, OsmObject} from "./OsmObject"; import {OsmNode, OsmObject} from "./OsmObject";
import {ElementStorage} from "./ElementStorage"; import {ElementStorage} from "./ElementStorage";
import {UIEventSource} from "../UI/UIEventSource"; import {UIEventSource} from "../UI/UIEventSource";
import {Question, QuestionDefinition} from "./Question";
import {And, Tag, TagsFilter} from "./TagsFilter"; import {And, Tag, TagsFilter} from "./TagsFilter";
export class Changes { export class Changes {

View file

@ -60,7 +60,6 @@ export class Imgur {
} }
console.log(data);
const licenseInfo = new LicenseInfo(); const licenseInfo = new LicenseInfo();
licenseInfo.licenseShortName = data.license; licenseInfo.licenseShortName = data.license;

View file

@ -57,8 +57,7 @@ export class LayerUpdater {
} }
private handleFail(reason: any) { private handleFail(reason: any) {
console.log("QUERY FAILED", reason); console.log("QUERY FAILED (retrying in 1 sec)", reason);
console.log("Retrying in 1s")
this.previousBounds = undefined; this.previousBounds = undefined;
const self = this; const self = this;
window.setTimeout( window.setTimeout(
@ -73,7 +72,6 @@ export class LayerUpdater {
} }
console.log("Zoom level: ",this._map.map.getZoom(), "Least needed zoom:", this._minzoom) 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) { if (this._map.map.getZoom() < this._minzoom || this._map.Location.data.zoom < this._minzoom) {
console.log("Not running query: zoom not sufficient");
return; return;
} }

View file

@ -123,6 +123,7 @@ export class OsmConnection {
public preferenceSources : any = {} public preferenceSources : any = {}
public GetPreference(key: string) : UIEventSource<string>{ public GetPreference(key: string) : UIEventSource<string>{
key = "mapcomplete-"+key;
if (this.preferenceSources[key] !== undefined) { if (this.preferenceSources[key] !== undefined) {
return this.preferenceSources[key]; return this.preferenceSources[key];
} }

View file

@ -32,7 +32,7 @@ export class Overpass {
queryGeoJson(bbox: string, continuation: ((any) => void), onFail: ((reason) => void)): void { queryGeoJson(bbox: string, continuation: ((any) => void), onFail: ((reason) => void)): void {
let query = this.buildQuery(bbox); let query = this.buildQuery(bbox);
if(Overpass.testUrl !== null){ if(Overpass.testUrl !== null){
console.log("Using testing URL") console.log("Using testing URL")
query = Overpass.testUrl; query = Overpass.testUrl;
@ -44,7 +44,7 @@ export class Overpass {
console.log("Query failed") console.log("Query failed")
onFail(status); onFail(status);
} }
if(json.elements === [] && json.remarks.indexOf("runtime error") > 0){ if(json.elements === [] && json.remarks.indexOf("runtime error") > 0){
console.log("Timeout or other runtime error"); console.log("Timeout or other runtime error");
return; return;

View file

@ -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<any>;
/**
* The ID of the calling question - used to trigger it's onsave
*/
private readonly _qid;
constructor(q: Question, qid: number, tags: UIEventSource<any>) {
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 +=
"<input type='radio' name='q" + this._qid + "' id='" + ansId + "' value='" + c + "' />" +
"<label for='" + ansId + "'>" + human + "</label>" +
"<br />";
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 +=
"<input type='radio' name='q" + this._qid + "' id='" + ansId + "' value='" + c + "' />" +
"<label for='" + ansId + "'>" + human + "</label>" +
"<br />";
c++;
}
const ansId = "q" + this._qid + "-answer" + c;
radios +=
"<input type='radio' name='q" + this._qid + "' id='" + ansId + "' value='" + c + "' />" +
"<label for='" + ansId + "'><input type='text' id='q-" + this._qid + "-textbox' onclick='checkRadioButton(\"" + ansId + "\")'/></label>" +
"<br />";
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 += "<input type='text' id='q-" + this._qid + "-textbox'/><br/>"
} 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 = "<input class='save-button' type='button' onclick='" + embeddedScriptSave + "' value='Opslaan' />";
const skip = "<input class='skip-button' type='button' onclick='" + embeddedScriptSkip + "' value='Ik ben niet zeker (vraag overslaan)' />";
return q.question + "<br/> " + 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<any>): 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();
}
}

View file

@ -10,16 +10,16 @@ import {UIElement} from "../UI/UIElement";
export class StrayClickHandler { export class StrayClickHandler {
private _basemap: Basemap; private _basemap: Basemap;
private _lastMarker; private _lastMarker;
private _leftMessage: UIEventSource<() => UIElement>; private _fullScreenMessage: UIEventSource<UIElement>;
private _uiToShow: (() => UIElement); private _uiToShow: (() => UIElement);
constructor( constructor(
basemap: Basemap, basemap: Basemap,
selectElement: UIEventSource<any>, selectElement: UIEventSource<any>,
leftMessage: UIEventSource<() => UIElement>, fullScreenMessage: UIEventSource<UIElement>,
uiToShow: (() => UIElement)) { uiToShow: (() => UIElement)) {
this._basemap = basemap; this._basemap = basemap;
this._leftMessage = leftMessage; this._fullScreenMessage = fullScreenMessage;
this._uiToShow = uiToShow; this._uiToShow = uiToShow;
const self = this; const self = this;
const map = basemap.map; const map = basemap.map;
@ -32,15 +32,16 @@ export class StrayClickHandler {
self._lastMarker = L.marker([lastClick.lat, lastClick.lon]); self._lastMarker = L.marker([lastClick.lat, lastClick.lon]);
const uiElement = uiToShow(); const uiElement = uiToShow();
const popup = L.popup().setContent(uiElement.Render()); const popup = L.popup().setContent(uiElement.Render());
uiElement.Activate();
uiElement.Update(); uiElement.Update();
uiElement.Activate();
self._lastMarker.addTo(map); self._lastMarker.addTo(map);
self._lastMarker.bindPopup(popup).openPopup(); self._lastMarker.bindPopup(popup).openPopup();
self._lastMarker.on("click", () => { self._lastMarker.on("click", () => {
leftMessage.setData(self._uiToShow); fullScreenMessage.setData(self._uiToShow());
}); });
uiElement.Update();
uiElement.Activate();
}); });
selectElement.addCallback(() => { selectElement.addCallback(() => {

View file

@ -123,6 +123,7 @@ export class AddButton extends UIElement {
const self = this; const self = this;
htmlElement.onclick = function (event) { htmlElement.onclick = function (event) {
// @ts-ignore
if(event.consumed){ if(event.consumed){
return; return;
} }

View file

@ -18,7 +18,7 @@ export class Button extends UIElement {
} }
protected InnerRender(): string { InnerRender(): string {
return "<form>" + return "<form>" +
"<button id='button-"+this.id+"' type='button' "+this._clss+">" + this._text.Render() + "</button>" + "<button id='button-"+this.id+"' type='button' "+this._clss+">" + this._text.Render() + "</button>" +

View file

@ -3,15 +3,17 @@ import {UIEventSource} from "../UIEventSource";
export class CheckBox extends UIElement{ export class CheckBox extends UIElement{
private data: UIEventSource<boolean>;
constructor(data: UIEventSource<boolean>) { constructor(data: UIEventSource<boolean>) {
super(data); super(data);
this.data = data;
} }
protected InnerRender(): string { protected InnerRender(): string {
return ""; return "Current val: "+this.data.data;
} }
} }

31
UI/Base/Combine.ts Normal file
View file

@ -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();
}
}
}
}

View file

@ -1,61 +0,0 @@
import {UIEventSource} from "../UIEventSource";
import {UIElement} from "../UIElement";
export class DropDownUI extends UIElement {
selectedElement: UIEventSource<string>
private _label: string;
private _values: { value: string; shown: string }[];
constructor(label: string, values: { value: string, shown: string }[],
selectedElement: UIEventSource<string> = undefined) {
super(undefined);
this._label = label;
this._values = values;
this.selectedElement = selectedElement ?? new UIEventSource<string>(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 += "<option value='" + value.value + "'>" + value.shown + "</option>"
}
return "<form>" +
"<label for='dropdown-" + this.id + "'>" + this._label + "</label>" +
"<select name='dropdown-" + this.id + "' id='dropdown-" + this.id + "'>" +
options +
"</select>" +
"</form>";
}
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);
}
}
}

View file

@ -5,10 +5,10 @@ export class FixedUiElement extends UIElement {
constructor(html: string) { constructor(html: string) {
super(undefined); super(undefined);
this._html = html; this._html = html ?? "";
} }
protected InnerRender(): string { InnerRender(): string {
return this._html; return this._html;
} }

View file

@ -1,59 +0,0 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../UIEventSource";
import {UIInputElement} from "./UIInputElement";
export class TextField<T> extends UIInputElement<T> {
public value: UIEventSource<string> = new UIEventSource<string>("");
/**
* Pings and has the value data
*/
public enterPressed = new UIEventSource<string>(undefined);
private _placeholder: UIEventSource<string>;
private _mapping: (string) => T;
constructor(placeholder: UIEventSource<string>,
mapping: ((string) => T)) {
super(placeholder);
this._placeholder = placeholder;
this._mapping = mapping;
}
GetValue(): UIEventSource<T> {
return this.value.map(this._mapping);
}
protected InnerRender(): string {
return "<form onSubmit='return false' class='form-text-field'>" +
"<input type='text' placeholder='" + (this._placeholder.data ?? "") + "' id='text-" + this.id + "'>" +
"</form>";
}
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 = "";
}
}
}

View file

@ -1,8 +0,0 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../UIEventSource";
export abstract class UIInputElement<T> extends UIElement{
abstract GetValue() : UIEventSource<T>;
}

View file

@ -1,109 +0,0 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../UIEventSource";
import {UIInputElement} from "./UIInputElement";
export class UIRadioButton<T> extends UIInputElement<T> {
public readonly SelectedElementIndex: UIEventSource<number>
= new UIEventSource<number>(null);
private readonly _elements: UIEventSource<UIElement[]>
private _selectFirstAsDefault: boolean;
private _valueMapping: (i: number) => T;
constructor(elements: UIEventSource<UIElement[]>,
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<T> {
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 =
'<input type="radio" id="' + this.IdFor(i) + '" name="radiogroup-' + this.id + '">' +
'<label for="' + this.IdFor(i) + '">' + el.Render() + '</label>' +
'<br>';
body += htmlElement;
i++;
}
return "<form id='" + this.id + "-form'>" + body + "</form>";
}
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;
}
}
}
}
}

View file

@ -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<T> extends UIInputElement<T> {
private readonly _radioSelector: UIRadioButton<T>;
private readonly _freeformText: TextField<T>;
private readonly _value: UIEventSource<T> = new UIEventSource<T>(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<string>(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<T> {
return this._value;
}
protected InnerRender(): string {
return this._radioSelector.Render();
}
InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
this._radioSelector.Update();
this._freeformText.Update();
}
}

View file

@ -12,16 +12,8 @@ export class VariableUiElement extends UIElement {
} }
protected InnerRender(): string { InnerRender(): string {
return this._html.data; return this._html.data;
} }
InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
if(this._innerUpdate !== undefined){
this._innerUpdate(htmlElement);
}
}
} }

View file

@ -1,6 +1,7 @@
import {UIElement} from "./UIElement"; import {UIElement} from "./UIElement";
import {UIEventSource} from "./UIEventSource"; import {UIEventSource} from "./UIEventSource";
import {OsmConnection} from "../Logic/OsmConnection"; import {OsmConnection} from "../Logic/OsmConnection";
import Translations from "./i18n/Translations";
export class CenterMessageBox extends UIElement { export class CenterMessageBox extends UIElement {
@ -34,17 +35,17 @@ export class CenterMessageBox extends UIElement {
} }
protected InnerRender(): string { InnerRender(): string {
if (this._centermessage.data != "") { if (this._centermessage.data != "") {
return this._centermessage.data; return this._centermessage.data;
} }
if (this._queryRunning.data) { if (this._queryRunning.data) {
return "Data wordt geladen..."; return Translations.t.centerMessage.loadingData.txt;
} else if (this._zoomInMore.data) { } else if (this._zoomInMore.data) {
return "Zoom in om de data te zien en te bewerken"; return Translations.t.centerMessage.zoomIn.txt;
} }
return "Klaar!"; return Translations.t.centerMessage.ready.txt;
} }

View file

@ -10,7 +10,7 @@ import {TagRenderingOptions} from "../Customizations/TagRendering";
import {OsmLink} from "../Customizations/Questions/OsmLink"; import {OsmLink} from "../Customizations/Questions/OsmLink";
import {WikipediaLink} from "../Customizations/Questions/WikipediaLink"; import {WikipediaLink} from "../Customizations/Questions/WikipediaLink";
import {And} from "../Logic/TagsFilter"; import {And} from "../Logic/TagsFilter";
import {TagDependantUIElement} from "../Customizations/UIElementConstructor"; import {TagDependantUIElement, TagDependantUIElementConstructor} from "../Customizations/UIElementConstructor";
export class FeatureInfoBox extends UIElement { export class FeatureInfoBox extends UIElement {
@ -30,8 +30,8 @@ export class FeatureInfoBox extends UIElement {
constructor( constructor(
tagsES: UIEventSource<any>, tagsES: UIEventSource<any>,
title: TagRenderingOptions, title: TagRenderingOptions | UIElement,
elementsToShow: TagRenderingOptions[], elementsToShow: TagDependantUIElementConstructor[],
changes: Changes, changes: Changes,
userDetails: UIEventSource<UserDetails> userDetails: UIEventSource<UserDetails>
) { ) {
@ -56,10 +56,14 @@ export class FeatureInfoBox extends UIElement {
} }
) )
this._title = new TagRenderingOptions(title.options).construct(deps); if (title instanceof UIElement) {
this._osmLink =new OsmLink().construct(deps); this._title = title;
} else {
this._title = new TagRenderingOptions(title.options).construct(deps);
}
this._osmLink = new OsmLink().construct(deps);
this._wikipedialink = new WikipediaLink().construct(deps); this._wikipedialink = new WikipediaLink().construct(deps);
} }
@ -115,4 +119,6 @@ export class FeatureInfoBox extends UIElement {
"</div>"; "</div>";
} }
} }

View file

@ -1,14 +1,16 @@
/**
* Keeps 'messagebox' and 'messageboxmobile' in sync, shows a 'close' button on the latter one
*/
import {UIEventSource} from "./UIEventSource"; import {UIEventSource} from "./UIEventSource";
import {UIElement} from "./UIElement"; import {UIElement} from "./UIElement";
import {VariableUiElement} from "./Base/VariableUIElement"; 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<UIElement>;
constructor(uielement: UIEventSource<() => UIElement>, constructor(uielement: UIEventSource<UIElement>,
onClear: (() => void)) { onClear: (() => void)) {
this._uielement = uielement; this._uielement = uielement;
this.listenTo(uielement); this.listenTo(uielement);
@ -22,14 +24,13 @@ export class MessageBoxHandler {
} }
} }
new VariableUiElement(new UIEventSource<string>("<h2>Naar de kaart</h2>"), Translations.t.general.returnToTheMap
() => { .onClick(() => {
document.getElementById("to-the-map").onclick = function () { console.log("Clicked 'return to the map'")
uielement.setData(undefined); uielement.setData(undefined);
onClear(); onClear();
} })
} .AttachTo("to-the-map-h2");
).AttachTo("to-the-map");
} }
@ -45,7 +46,6 @@ export class MessageBoxHandler {
update() { update() {
const wrapper = document.getElementById("messagesboxmobilewrapper"); const wrapper = document.getElementById("messagesboxmobilewrapper");
const gen = this._uielement.data; const gen = this._uielement.data;
console.log("Generator: ", gen);
if (gen === undefined) { if (gen === undefined) {
wrapper.classList.add("hidden") wrapper.classList.add("hidden")
if (location.hash !== "") { if (location.hash !== "") {
@ -55,12 +55,8 @@ export class MessageBoxHandler {
} }
location.hash = "#element" location.hash = "#element"
wrapper.classList.remove("hidden"); wrapper.classList.remove("hidden");
/* gen()
?.HideOnEmpty(true)
?.AttachTo("messagesbox")
?.Activate();*/
gen() gen
?.HideOnEmpty(true) ?.HideOnEmpty(true)
?.AttachTo("messagesboxmobile") ?.AttachTo("messagesboxmobile")
?.Activate(); ?.Activate();

View file

@ -23,10 +23,10 @@ export class ImageCarouselConstructor implements TagDependantUIElementConstructo
return 0; return 0;
} }
construct(tags: UIEventSource<any>, changes: Changes): TagDependantUIElement { construct(dependencies: { tags: UIEventSource<any>, changes: Changes }): TagDependantUIElement {
return new ImageCarousel(tags, changes); return new ImageCarousel(dependencies.tags, dependencies.changes);
} }
} }
export class ImageCarousel extends TagDependantUIElement { export class ImageCarousel extends TagDependantUIElement {

View file

@ -34,14 +34,14 @@ class ImageCarouselWithUpload extends TagDependantUIElement {
const changes = dependencies.changes; const changes = dependencies.changes;
this._imageElement = new ImageCarousel(tags, changes); this._imageElement = new ImageCarousel(tags, changes);
const userDetails = changes.login.userDetails; 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, this._pictureUploader = new OsmImageUploadHandler(tags,
userDetails, license, userDetails, license,
changes, this._imageElement.slideshow).getUI(); changes, this._imageElement.slideshow).getUI();
} }
protected InnerRender(): string { InnerRender(): string {
return this._imageElement.Render() + return this._imageElement.Render() +
this._pictureUploader.Render(); this._pictureUploader.Render();
} }

View file

@ -3,13 +3,13 @@ import {UIEventSource} from "./UIEventSource";
import $ from "jquery" import $ from "jquery"
import {Imgur} from "../Logic/Imgur"; import {Imgur} from "../Logic/Imgur";
import {UserDetails} from "../Logic/OsmConnection"; import {UserDetails} from "../Logic/OsmConnection";
import {DropDownUI} from "./Base/DropDownUI"; import {DropDown} from "./Input/DropDown";
import {VariableUiElement} from "./Base/VariableUIElement"; import {VariableUiElement} from "./Base/VariableUIElement";
import Translations from "./i18n/Translations";
export class ImageUploadFlow extends UIElement { export class ImageUploadFlow extends UIElement {
private _licensePicker: UIElement; private _licensePicker: UIElement;
private _selectedLicence: UIEventSource<string>; private _selectedLicence: UIEventSource<string>;
private _licenseExplanation: UIElement;
private _isUploading: UIEventSource<number> = new UIEventSource<number>(0) private _isUploading: UIEventSource<number> = new UIEventSource<number>(0)
private _uploadOptions: (license: string) => { title: string; description: string; handleURL: (url: string) => void; allDone: (() => void) }; private _uploadOptions: (license: string) => { title: string; description: string; handleURL: (url: string) => void; allDone: (() => void) };
private _userdetails: UIEventSource<UserDetails>; private _userdetails: UIEventSource<UserDetails>;
@ -31,61 +31,49 @@ export class ImageUploadFlow extends UIElement {
this._uploadOptions = uploadOptions; this._uploadOptions = uploadOptions;
this.ListenTo(this._isUploading); this.ListenTo(this._isUploading);
const licensePicker = new DropDownUI("Jouw foto wordt gepubliceerd ", const licensePicker = new DropDown(Translations.t.image.willBePublished,
[ [
{value: "CC0", shown: "in het publiek domein"}, {value: "CC0", shown: Translations.t.image.cco},
{value: "CC-BY-SA 4.0", shown: "onder een CC-BY-SA-licentie"}, {value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs},
{value: "CC-BY 4.0", shown: "onder een CC-BY-licentie"} {value: "CC-BY 4.0", shown: Translations.t.image.ccb}
], ],
preferedLicense preferedLicense
); );
this._licensePicker = licensePicker; this._licensePicker = licensePicker;
this._selectedLicence = licensePicker.selectedElement; this._selectedLicence = licensePicker.GetValue();
const licenseExplanations = {
"CC-BY-SA 4.0":
"<b>Creative Commonse met naamsvermelding en gelijk delen</b><br/>" +
"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":
"<b>Creative Commonse met naamsvermelding</b> <br/>" +
"Je foto mag door iedereen gratis gebruikt worden, als ze je naam vermelden",
"CC0":
"<b>Geen copyright</b><br/> 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) { if (!this._userdetails.data.loggedIn) {
return "<div class='activate-osm-authentication'>Gelieve je aan te melden om een foto toe te voegen of vragen te beantwoorden</div>"; return `<div class='activate-osm-authentication'>${Translations.t.image.pleaseLogin.Render()}</div>`;
} }
let uploadingMessage = "";
if (this._isUploading.data == 1) { if (this._isUploading.data == 1) {
return "<b>Bezig met een foto te uploaden...</b>" return `<b>${Translations.t.image.uploadingPicture.Render()}</b>`
} }
if (this._isUploading.data > 0) { if (this._isUploading.data > 0) {
return "<b>Bezig met uploaden, nog " + this._isUploading.data + " foto's te gaan...</b>" uploadingMessage = "<b>Uploading multiple pictures, " + this._isUploading.data + " left...</b>"
} }
return "" + return "" +
"<div class='imageflow'>" + "<div class='imageflow'>" +
"<label for='fileselector-" + this.id + "'>" + "<label for='fileselector-" + this.id + "'>" +
"<div class='imageflow-file-input-wrapper'>" + "<div class='imageflow-file-input-wrapper'>" +
"<img src='./assets/camera-plus.svg' alt='upload image'/> " + "<img src='./assets/camera-plus.svg' alt='upload image'/> " +
"<span class='imageflow-add-picture'>Voeg foto toe</span>" + `<span class='imageflow-add-picture'>${Translations.t.image.addPicture.R()}</span>` +
"<div class='break'></div>"+ "<div class='break'></div>"+
"</div>" + "</div>" +
this._licensePicker.Render() +
this._licensePicker.Render() + "<br/>" +
uploadingMessage +
"</label>" + "</label>" +
"<input id='fileselector-" + this.id + "' " + "<input id='fileselector-" + this.id + "' " +

100
UI/Input/DropDown.ts Normal file
View file

@ -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<T> extends InputElement<T> {
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<T> = undefined) {
super(undefined);
this._value = value ?? new UIEventSource<T>(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<T> {
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 += "<option value='" + i + "'>" + this._values[i].shown.InnerRender() + "</option>"
}
return "<form>" +
"<label for='dropdown-" + this.id + "'>" + this._label.Render() + "</label>" +
"<select name='dropdown-" + this.id + "' id='dropdown-" + this.id + "'>" +
options +
"</select>" +
"</form>";
}
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;
}
}
}
}

View file

@ -0,0 +1,35 @@
import {InputElement} from "./InputElement";
import {UIEventSource} from "../UIEventSource";
import {UIElement} from "../UIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
export class FixedInputElement<T> extends InputElement<T> {
private rendering: UIElement;
private value: UIEventSource<T>;
constructor(rendering: UIElement | string, value: T) {
super(undefined);
this.value = new UIEventSource<T>(value);
this.rendering = typeof (rendering) === 'string' ? new FixedUiElement(rendering) : rendering;
}
GetValue(): UIEventSource<T> {
return this.value;
}
ShowValue(t: T): boolean {
return false;
}
InnerRender(): string {
return this.rendering.Render();
}
IsValid(t: T): boolean {
return t == this.value.data;
}
}

11
UI/Input/InputElement.ts Normal file
View file

@ -0,0 +1,11 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../UIEventSource";
import {FixedUiElement} from "../Base/FixedUiElement";
export abstract class InputElement<T> extends UIElement{
abstract GetValue() : UIEventSource<T>;
abstract IsValid(t: T) : boolean;
}

View file

@ -0,0 +1,41 @@
import {InputElement} from "./InputElement";
import {UIEventSource} from "../UIEventSource";
import {UIElement} from "../UIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
export class InputElementWrapper<T> extends InputElement<T>{
private pre: UIElement ;
private input: InputElement<T>;
private post: UIElement ;
constructor(
pre: UIElement | string,
input: InputElement<T>,
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<T> {
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);
}
}

146
UI/Input/RadioButton.ts Normal file
View file

@ -0,0 +1,146 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../UIEventSource";
import {InputElement} from "./InputElement";
export class RadioButton<T> extends InputElement<T> {
private readonly _selectedElementIndex: UIEventSource<number>
= new UIEventSource<number>(null);
private value: UIEventSource<T>;
private readonly _elements: InputElement<T>[]
private _selectFirstAsDefault: boolean;
constructor(elements: InputElement<T>[],
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<T> {
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 =
'<input type="radio" id="' + this.IdFor(i) + '" name="radiogroup-' + this.id + '">' +
'<label for="' + this.IdFor(i) + '">' + el.Render() + '</label>' +
'<br>';
body += htmlElement;
i++;
}
return "<form id='" + this.id + "-form'>" + body + "</form>";
}
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();
}
}
}
};
}

119
UI/Input/TextField.ts Normal file
View file

@ -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<T> extends InputElement<T> {
private value: UIEventSource<string>;
private mappedValue: UIEventSource<T>;
/**
* Pings and has the value data
*/
public enterPressed = new UIEventSource<string>(undefined);
private _placeholder: UIElement;
private _fromString?: (string: string) => T;
private _toString: (t: T) => string;
constructor(options: {
placeholder?: string | UIElement,
toString: (t: T) => string,
fromString: (string: string) => T,
value?: UIEventSource<T>
}) {
super(undefined);
const self = this;
this.value = new UIEventSource<string>("");
this.mappedValue = options?.value ?? new UIEventSource<T>(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<T> {
return this.mappedValue;
}
ShowValue(t: T): boolean {
if (!this.IsValid(t)) {
return false;
}
this.mappedValue.setData(t);
}
InnerRender(): string {
return "<form onSubmit='return false' class='form-text-field'>" +
"<input type='text' placeholder='" + this._placeholder.InnerRender() + "' id='text-" + this.id + "'>" +
"</form>";
}
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 = "";
}
}
}

View file

@ -21,7 +21,7 @@ export class PendingChanges extends UIElement {
}) })
} }
protected InnerRender(): string { InnerRender(): string {
if (this._isSaving.data) { if (this._isSaving.data) {
return "<span class='alert'>Saving</span>"; return "<span class='alert'>Saving</span>";
} }

View file

@ -1,5 +1,6 @@
import {UIEventSource} from "./UIEventSource"; import {UIEventSource} from "./UIEventSource";
import {UIElement} from "./UIElement"; import {UIElement} from "./UIElement";
import Translations from "./i18n/Translations";
export class SaveButton extends UIElement { export class SaveButton extends UIElement {
private _value: UIEventSource<any>; private _value: UIEventSource<any>;
@ -12,14 +13,14 @@ export class SaveButton extends UIElement {
this._value = value; this._value = value;
} }
protected InnerRender(): string { InnerRender(): string {
if (this._value.data === undefined || if (this._value.data === undefined ||
this._value.data === null this._value.data === null
|| this._value.data === "" || this._value.data === ""
) { ) {
return "<span class='save-non-active'>Opslaan</span>" return "<span class='save-non-active'>"+Translations.t.general.save.Render()+"</span>"
} }
return "<span class='save'>Opslaan</span>"; return "<span class='save'>"+Translations.t.general.save.Render()+"</span>";
} }
} }

View file

@ -1,15 +1,26 @@
import {UIElement} from "./UIElement"; import {UIElement} from "./UIElement";
import {TextField} from "./Base/TextField"; import {TextField} from "./Input/TextField";
import {UIEventSource} from "./UIEventSource"; import {UIEventSource} from "./UIEventSource";
import {FixedUiElement} from "./Base/FixedUiElement"; import {FixedUiElement} from "./Base/FixedUiElement";
import {Geocoding} from "../Logic/Geocoding"; import {Geocoding} from "../Logic/Geocoding";
import {Basemap} from "../Logic/Basemap"; 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 { export class SearchAndGo extends UIElement {
private _placeholder = new UIEventSource("Zoek naar een locatie...") private _placeholder = new UIEventSource<Translation>(Translations.t.general.search.search)
private _searchField = new TextField(this._placeholder); private _searchField = new TextField<string>({
placeholder: new VariableUiElement(
this._placeholder.map(uiElement => uiElement.InnerRender(), [Locale.language])
),
fromString: str => str,
toString: str => str
}
);
private _foundEntries = new UIEventSource([]); private _foundEntries = new UIEventSource([]);
private _map: Basemap; private _map: Basemap;
@ -33,14 +44,14 @@ export class SearchAndGo extends UIElement {
// Triggered by 'enter' or onclick // Triggered by 'enter' or onclick
private RunSearch() { private RunSearch() {
const searchString = this._searchField.value.data; const searchString = this._searchField.GetValue().data;
this._searchField.Clear(); this._searchField.Clear();
this._placeholder.setData("Bezig met zoeken..."); this._placeholder.setData(Translations.t.general.search.searching);
const self = this; const self = this;
Geocoding.Search(searchString, this._map, (result) => { Geocoding.Search(searchString, this._map, (result) => {
if (result.length == 0) { if (result.length == 0) {
this._placeholder.setData("Niets gevonden"); this._placeholder.setData(Translations.t.general.search.nothing);
return; return;
} }
@ -50,16 +61,15 @@ export class SearchAndGo extends UIElement {
[bb[1], bb[3]] [bb[1], bb[3]]
] ]
self._map.map.fitBounds(bounds); 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 { InnerRender(): string {
// "<img class='search' src='./assets/search.svg' alt='Search'> " +
return this._searchField.Render() + return this._searchField.Render() +
this._goButton.Render(); this._goButton.Render();

View file

@ -42,7 +42,7 @@ export class SimpleAddUI extends UIElement {
// <button type='button'> looks SO retarded // <button type='button'> looks SO retarded
// the default type of button is 'submit', which performs a POST and page reload // the default type of button is 'submit', which performs a POST and page reload
const button = const button =
new Button(new FixedUiElement("Voeg hier een " + option.name + " toe"), new Button(new FixedUiElement("Add a " + option.name + " here"),
this.CreatePoint(option)); this.CreatePoint(option));
this._addButtons.push(button); this._addButtons.push(button);
} }
@ -60,19 +60,19 @@ export class SimpleAddUI extends UIElement {
} }
} }
protected InnerRender(): string { InnerRender(): string {
const header = "<h2>Geen selectie</h2>" + const header = "<h2>No data here</h2>" +
"Je klikte ergens waar er nog geen gezochte data is.<br/>"; "You clicked somewhere where no data is known yet.<br/>";
if (!this._userDetails.data.loggedIn) { if (!this._userDetails.data.loggedIn) {
return header + "<a class='activate-osm-authentication'>Gelieve je aan te melden om een nieuw punt toe te voegen</a>" return header + "<a class='activate-osm-authentication'>Please log in to add a new point</a>"
} }
if (this._zoomlevel.data.zoom < 19) { if (this._zoomlevel.data.zoom < 19) {
return header + "Zoom verder in om een element toe te voegen."; return header + "Zoom in further to add a point.";
} }
if (this._dataIsLoading.data) { if (this._dataIsLoading.data) {
return header + "De data is nog aan het laden. Nog even geduld, dan kan je een punt toevoegen"; return header + "The data is still loading. Please wait a bit before you add a new point";
} }
var html = ""; var html = "";
@ -83,10 +83,6 @@ export class SimpleAddUI extends UIElement {
} }
InnerUpdate(htmlElement: HTMLElement) { InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
for (const button of this._addButtons) {
button.Update();
}
this._userDetails.data.osmConnection.registerActivateOsmAUthenticationClass(); this._userDetails.data.osmConnection.registerActivateOsmAUthenticationClass();
} }

View file

@ -1,8 +1,7 @@
import {UIEventSource} from "./UIEventSource"; import {UIEventSource} from "./UIEventSource";
import instantiate = WebAssembly.instantiate;
export abstract class UIElement { export abstract class UIElement {
private static nextId: number = 0; private static nextId: number = 0;
public readonly id: string; public readonly id: string;
@ -20,12 +19,13 @@ export abstract class UIElement {
public ListenTo(source: UIEventSource<any>) { public ListenTo(source: UIEventSource<any>) {
if (source === undefined) { if (source === undefined) {
return; return this;
} }
const self = this; const self = this;
source.addCallback(() => { source.addCallback(() => {
self.Update(); self.Update();
}) })
return this;
} }
private _onClick: () => void; private _onClick: () => void;
@ -35,14 +35,13 @@ export abstract class UIElement {
this.Update(); this.Update();
return this; return this;
} }
Update(): void { Update(): void {
let element = document.getElementById(this.id); let element = document.getElementById(this.id);
if (element === null || element === undefined) { if (element === undefined || element === null) {
// The element is not painted // The element is not painted
return; return;
} }
element.innerHTML = this.InnerRender(); element.innerHTML = this.InnerRender();
if (this._hideIfEmpty) { if (this._hideIfEmpty) {
if (element.innerHTML === "") { if (element.innerHTML === "") {
@ -84,7 +83,8 @@ export abstract class UIElement {
} }
// Called after the HTML has been replaced. Can be used for css tricks // Called after the HTML has been replaced. Can be used for css tricks
InnerUpdate(htmlElement : HTMLElement){} protected InnerUpdate(htmlElement: HTMLElement) {
}
Render(): string { Render(): string {
return "<span class='uielement' id='" + this.id + "'>" + this.InnerRender() + "</span>" return "<span class='uielement' id='" + this.id + "'>" + this.InnerRender() + "</span>"
@ -93,15 +93,14 @@ export abstract class UIElement {
AttachTo(divId: string) { AttachTo(divId: string) {
let element = document.getElementById(divId); let element = document.getElementById(divId);
if (element === null) { if (element === null) {
console.log("SEVERE: could not attach UIElement to ", divId); throw "SEVERE: could not attach UIElement to " + divId;
return;
} }
element.innerHTML = this.Render(); element.innerHTML = this.Render();
this.Update(); this.Update();
return this; return this;
} }
protected abstract InnerRender(): string; public abstract InnerRender(): string;
public Activate(): void { public Activate(): void {
for (const i in this) { for (const i in this) {
@ -121,5 +120,6 @@ export abstract class UIElement {
public IsEmpty(): boolean { public IsEmpty(): boolean {
return this.InnerRender() === ""; return this.InnerRender() === "";
} }
}
}

View file

@ -1,6 +1,6 @@
export class UIEventSource<T>{ export class UIEventSource<T>{
public data : T; public data: T;
private _callbacks = []; private _callbacks = [];
constructor(data: T) { constructor(data: T) {
@ -27,15 +27,32 @@ export class UIEventSource<T>{
} }
} }
public map<J>(f: ((T) => J), public static flatten<X>(source: UIEventSource<UIEventSource<X>>, possibleSources: UIEventSource<any>[]): UIEventSource<X> {
extraSources : UIEventSource<any>[] = []): UIEventSource<J> { const sink = new UIEventSource<X>(source.data?.data);
const self = this;
source.addCallback((latestData) => {
sink.setData(latestData?.data);
});
for (const possibleSource of possibleSources) {
possibleSource.addCallback(() => {
sink.setData(source.data?.data);
})
}
return sink;
}
public map<J>(f: ((T) => J),
extraSources: UIEventSource<any>[] = []): UIEventSource<J> {
const self = this;
const update = function () { const update = function () {
newSource.setData(f(self.data)); newSource.setData(f(self.data));
newSource.ping(); newSource.ping();
} }
this.addCallback(update); this.addCallback(update);
for (const extraSource of extraSources) { for (const extraSource of extraSources) {
extraSource.addCallback(update); extraSource.addCallback(update);
@ -49,5 +66,16 @@ export class UIEventSource<T>{
} }
public syncWith(otherSource: UIEventSource<T>){
this.addCallback((latest) => otherSource.setData(latest));
const self = this;
otherSource.addCallback((latest) => self.setData(latest));
if(this.data === undefined){
this.setData(otherSource.data);
}else{
otherSource.setData(this.data);
}
}
} }

View file

@ -5,6 +5,7 @@ import {Basemap} from "../Logic/Basemap";
import L from "leaflet"; import L from "leaflet";
import {FixedUiElement} from "./Base/FixedUiElement"; import {FixedUiElement} from "./Base/FixedUiElement";
import {VariableUiElement} from "./Base/VariableUIElement"; import {VariableUiElement} from "./Base/VariableUIElement";
import Translations from "./i18n/Translations";
/** /**
* Handles and updates the user badge * Handles and updates the user badge
@ -15,12 +16,15 @@ export class UserBadge extends UIElement {
private _logout: UIElement; private _logout: UIElement;
private _basemap: Basemap; private _basemap: Basemap;
private _homeButton: UIElement; private _homeButton: UIElement;
private _languagePicker: UIElement;
constructor(userDetails: UIEventSource<UserDetails>, constructor(userDetails: UIEventSource<UserDetails>,
pendingChanges: UIElement, pendingChanges: UIElement,
languagePicker: UIElement,
basemap: Basemap) { basemap: Basemap) {
super(userDetails); super(userDetails);
this._languagePicker = languagePicker;
this._userDetails = userDetails; this._userDetails = userDetails;
this._pendingChanges = pendingChanges; this._pendingChanges = pendingChanges;
this._basemap = basemap; this._basemap = basemap;
@ -57,10 +61,10 @@ export class UserBadge extends UIElement {
} }
protected InnerRender(): string { InnerRender(): string {
const user = this._userDetails.data; const user = this._userDetails.data;
if (!user.loggedIn) { if (!user.loggedIn) {
return "<div class='activate-osm-authentication'>Klik hier om aan te melden bij OSM</div>"; return "<div class='activate-osm-authentication'>" + Translations.t.general.loginWithOpenStreetMap.R()+ "</div>";
} }
@ -113,6 +117,7 @@ export class UserBadge extends UIElement {
" <a href='https://www.openstreetmap.org/user/" + user.name + "/history' target='_blank'><img class='small-userbadge-icon' src='./assets/star.svg' alt='star'/> " + user.csCount + " <a href='https://www.openstreetmap.org/user/" + user.name + "/history' target='_blank'><img class='small-userbadge-icon' src='./assets/star.svg' alt='star'/> " + user.csCount +
"</a></span> " + "</a></span> " +
this._logout.Render() + this._logout.Render() +
this._languagePicker.Render() +
this._pendingChanges.Render() + this._pendingChanges.Render() +
"</p>" + "</p>" +

24
UI/i18n/Locale.ts Normal file
View file

@ -0,0 +1,24 @@
import {UIEventSource} from "../UIEventSource";
import {OsmConnection} from "../../Logic/OsmConnection";
export default class Locale {
public static language: UIEventSource<string> = Locale.getInitialLanguage();
private static getInitialLanguage() {
// The key to save in local storage
const LANGUAGE_KEY = 'language'
const lng = new UIEventSource("en");
const saved = localStorage.getItem(LANGUAGE_KEY);
lng.setData(saved);
lng.addCallback(data => {
console.log("Selected language", data);
localStorage.setItem(LANGUAGE_KEY, data)
});
return lng;
}
}

39
UI/i18n/Translation.ts Normal file
View file

@ -0,0 +1,39 @@
import { UIElement } from "../UIElement"
import Locale from "./Locale"
import {FixedUiElement} from "../Base/FixedUiElement";
export default class Translation extends UIElement {
get txt(): string {
const txt = this.translations[Locale.language.data];
if (txt !== undefined) {
return txt;
}
const en = this.translations["en"];
console.warn("No translation for language ", Locale.language.data, "for", en);
if (en !== undefined) {
return en;
}
for (const i in this.translations) {
return this.translations[i]; // Return a random language
}
return "Missing translation"
}
InnerRender(): string {
return this.txt
}
public readonly translations: object
constructor(translations: object) {
super(Locale.language)
this.translations = translations
}
public R(): string {
return new Translation(this.translations).Render();
}
}

371
UI/i18n/Translations.ts Normal file
View file

@ -0,0 +1,371 @@
import Translation from "./Translation";
import T from "./Translation";
import {UIElement} from "../UIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
export default class Translations {
static t = {
cyclofix: {
title: new T({
en: 'Cyclofix bicycle infrastructure',
nl: 'Cyclofix fietsinfrastructuur',
fr: 'TODO: FRENCH TRANSLATION'
}),
description: new T({
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."
}),
freeFormPlaceholder: new T({en: 'specify', nl: 'specifieer', fr: 'TODO: fr'}),
parking: {
name: new T({en: 'bike parking', nl: 'fietsparking', fr: 'TODO: fr'}),
title: new T({en: 'Bike parking', nl: 'Fietsparking', fr: 'TODO: fr'}),
type: {
render: new T({
en: 'This is a bicycle parking of the type: {bicycle_parking}',
nl: 'Dit is een fietsenparking van het type: {bicycle_parking}',
fr: 'TODO: fr'
}),
template: new T({en: 'Some other type: $$$', nl: 'Een ander type: $$$', fr: 'TODO: fr'}),
question: new T({
en: 'What is the type of this bicycle parking?',
nl: 'Van welk type is deze fietsenparking?',
fr: 'TODO: fr'
}),
eg: new T({en: ", for example", nl: ", bijvoorbeeld"}),
stands: new T({en: 'Staple racks', nl: 'Nietjes', fr: 'TODO: fr'}),
wall_loops: new T({en: 'Wheel rack/loops', nl: 'Wielrek/lussen', fr: 'TODO: fr'}),
handlebar_holder: new T({en: 'Handlebar holder', nl: 'Stuurhouder', fr: 'TODO: fr'}),
shed: new T({en: 'Shed', nl: 'Schuur', fr: 'TODO: fr'}),
rack: new T({en: 'Rack', nl: 'Rek', fr: 'TODO: fr'}),
"two-tier": new T({en: 'Two-tiered', nl: 'Dubbel (twee verdiepingen)', fr: 'TODO: fr'}),
},
operator: {
render: new T({
en: 'This bike parking is operated by {operator}',
nl: 'Deze fietsenparking wordt beheerd door {operator}',
fr: 'TODO: fr'
}),
template: new T({en: 'A different operator: $$$', nl: 'Een andere beheerder: $$$', fr: 'TODO: fr'}),
question: new T({
en: 'Who operates this bike station (name of university, shop, city...)?',
nl: 'Wie beheert deze fietsenparking (naam universiteit, winkel, stad...)?',
fr: 'TODO: fr'
}),
private: new T({
en: 'Operated by a private person',
nl: 'Wordt beheerd door een privépersoon',
fr: 'TODO: fr'
}),
}
},
station: {
name: new T({
en: 'bike station (repair, pump or both)',
nl: 'fietsstation (herstel, pomp of allebei)',
fr: 'TODO: fr'
}),
title: new T({en: 'Bike station', nl: 'Fietsstation', fr: 'TODO: fr'}),
manometer: {
question: new T({
en: 'Does the pump have a pressure indicator or manometer?',
nl: 'Heeft deze pomp een luchtdrukmeter?',
fr: 'TODO: fr'
}),
yes: new T({en: 'There is a manometer', nl: 'Er is een luchtdrukmeter', fr: 'TODO: fr'}),
no: new T({en: 'There is no manometer', nl: 'Er is geen luchtdrukmeter', fr: 'TODO: fr'}),
broken: new T({
en: 'There is manometer but it is broken',
nl: 'Er is een luchtdrukmeter maar die is momenteel defect',
fr: 'TODO: fr'
})
},
electric: {
question: new T({
en: 'Is this an electric bike pump?',
nl: 'Is dit een electrische fietspomp?',
fr: 'TODO: fr'
}),
manual: new T({en: 'Manual pump', nl: 'Manuele pomp', fr: 'TODO: fr'}),
electric: new T({en: 'Electrical pump', nl: 'Electrische pomp', fr: 'TODO: fr'})
},
operational: {
question: new T({
en: 'Is the bike pump still operational?',
nl: 'Werkt de fietspomp nog?',
fr: 'TODO: fr'
}),
operational: new T({
en: 'The bike pump is operational',
nl: 'De fietspomp werkt nog',
fr: 'TODO: fr'
}),
broken: new T({en: 'The bike pump is broken', nl: 'De fietspomp is kapot', fr: 'TODO: fr'})
},
valves: {
question: new T({
en: 'What valves are supported?',
nl: 'Welke ventielen werken er met de pomp?',
fr: 'TODO: fr'
}),
default: new T({
en: 'There is a default head, so Dunlop, Sclaverand and auto',
nl: 'Er is een standaard aansluiting, die dus voor Dunlop, Sclaverand en auto\'s werkt',
fr: 'TODO: fr'
}),
dunlop: new T({en: 'Only Dunlop', nl: 'Enkel Dunlop', fr: 'TODO: fr'}),
sclaverand: new T({
en: 'Only Sclaverand (also known as Presta)',
nl: 'Enkel Sclaverand (ook gekend als Presta)',
fr: 'TODO: fr'
}),
auto: new T({en: 'Only for cars', nl: 'Enkel voor auto\'s', fr: 'TODO: fr'}),
render: new T({
en: 'This pump supports the following valves: {valves}',
nl: 'Deze pomp werkt met de volgende ventielen: {valves}',
fr: 'TODO: fr'
}),
template: new T({
en: 'Some other valve(s): $$$',
nl: 'Een ander type ventiel(en): $$$',
fr: 'TODO: fr'
})
},
chain: {
question: new T({
en: 'Does this bike station have a special tool to repair your bike chain?',
nl: 'Heeft dit fietsstation een speciale reparatieset voor je ketting?',
fr: 'TODO: fr'
}),
yes: new T({
en: 'There is a chain tool',
nl: 'Er is een reparatieset voor je ketting',
fr: 'TODO: fr'
}),
no: new T({
en: 'There is no chain tool',
nl: 'Er is geen reparatieset voor je ketting',
fr: 'TODO: fr'
}),
},
operator: {
render: new T({
en: 'This bike station is operated by {operator}',
nl: 'Dit fietsstation wordt beheerd door {operator}',
fr: 'TODO: fr'
}),
template: new T({en: 'A different operator: $$$', nl: 'Een andere beheerder: $$$', fr: 'TODO: fr'}),
question: new T({
en: 'Who operates this bike station (name of university, shop, city...)?',
nl: 'Wie beheert dit fietsstation (naam universiteit, winkel, stad...)?',
fr: 'TODO: fr'
}),
private: new T({
en: 'Operated by a private person',
nl: 'Wordt beheerd door een privépersoon',
fr: 'TODO: fr'
}),
},
services: {
question: new T({
en: 'Which services are available at this bike station?',
nl: 'Welke functies biedt dit fietsstation?',
fr: 'TODO: fr'
}),
pump: new T({
en: 'There is only a pump available',
nl: 'Er is enkel een pomp beschikbaar',
fr: 'TODO: fr'
}),
tools: new T({
en: 'There are only tools (screwdrivers, pliers...) available',
nl: 'Er is enkel gereedschap beschikbaar (schroevendraaier, tang...)',
fr: 'TODO: fr'
}),
both: new T({
en: 'There are both tools and a pump available',
nl: 'Er is zowel een pomp als gereedschap beschikbaar',
fr: 'TODO: fr'
}),
},
stand: {
question: new T({
en: 'Does this bike station have a hook to suspend your bike with or a stand to elevate it?',
nl: 'Heeft dit fietsstation een haak of standaard om je fiets op te hangen/zetten?',
fr: 'TODO: fr'
}),
yes: new T({en: 'There is a hook or stand', nl: 'Er is een haak of standaard', fr: 'TODO: fr'}),
no: new T({en: 'There is no hook or stand', nl: 'Er is geen haak of standaard', fr: 'TODO: fr'}),
}
},
shop: {
name: new T({en: 'bike shop', nl: 'fietswinkel', fr: 'TODO: fr'}),
title: new T({en: 'Bike repair/shop', nl: 'Fietswinkel/herstelling', fr: 'TODO: fr'}),
titleRepair: new T({en: 'Bike shop', nl: 'Fietswinkel', fr: 'TODO: fr'}),
retail: {
question: new T({
en: 'Does this shop sell bikes?',
nl: 'Verkoopt deze winkel fietsen?',
fr: 'TODO: fr'
}),
yes: new T({en: 'This shop sells bikes', nl: 'Deze winkel verkoopt fietsen', fr: 'TODO: fr'}),
no: new T({
en: 'This shop doesn\'t sell bikes',
nl: 'Deze winkel verkoopt geen fietsen',
fr: 'TODO: fr'
}),
},
repair: {
question: new T({
en: 'Does this shop repair bikes?',
nl: 'Verkoopt deze winkel fietsen?',
fr: 'TODO: fr'
}),
yes: new T({en: 'This shop repairs bikes', nl: 'Deze winkel herstelt fietsen', fr: 'TODO: fr'}),
no: new T({
en: 'This shop doesn\'t repair bikes',
nl: 'Deze winkel herstelt geen fietsen',
fr: 'TODO: fr'
}),
sold: new T({en: 'This shop only repairs bikes bought here', nl: 'Deze winkel herstelt enkel fietsen die hier werden gekocht', fr: 'TODO: fr'}),
brand: new T({en: 'This shop only repairs bikes of a certain brand', nl: 'Deze winkel herstelt enkel fietsen van een bepaald merk', fr: 'TODO: fr'}),
},
rental: {
question: new T({
en: 'Does this shop rent out bikes?',
nl: 'Verhuurt deze winkel fietsen?',
fr: 'TODO: fr'
}),
yes: new T({en: 'This shop rents out bikes', nl: 'Deze winkel verhuurt fietsen', fr: 'TODO: fr'}),
no: new T({
en: 'This shop doesn\'t rent out bikes',
nl: 'Deze winkel verhuurt geen fietsen',
fr: 'TODO: fr'
}),
},
pump: {
question: new T({
en: 'Does this shop offer a bike pump for use by anyone?',
nl: 'Biedt deze winkel een fietspomp aan voor iedereen?',
fr: 'TODO: fr'
}),
yes: new T({
en: 'This shop offers a bike pump for anyone',
nl: 'Deze winkel biedt geen fietspomp aan voor eender wie',
fr: 'TODO: fr'
}),
no: new T({
en: 'This shop doesn\'t offer a bike pump for anyone',
nl: 'Deze winkel biedt een fietspomp aan voor iedereen',
fr: 'TODO: fr'
})
},
qName: {
question: new T({en: 'What is the name of this bicycle shop?', nl: 'Wat is de naam van deze fietswinkel?', fr: 'TODO: fr'}),
render: new T({en: 'This bicycle shop is called {name}', nl: 'Deze fietswinkel heet {name}', fr: 'TODO: fr'}),
template: new T({en: 'This bicycle shop is called: $$$', nl: 'Deze fietswinkel heet: $$$', fr: 'TODO: fr'})
},
secondHand: {
question: new T({en: 'Does this shop sell second-hand bikes?', nl: 'Verkoopt deze winkel tweedehands fietsen?', fr: 'TODO: fr'}),
yes: new T({en: 'This shop sells second-hand bikes', nl: 'Deze winkel verkoopt tweedehands fietsen', fr: 'TODO: fr'}),
no: new T({en: 'This shop doesn\'t sell second-hand bikes', nl: 'Deze winkel verkoopt geen tweedehands fietsen', fr: 'TODO: fr'}),
only: new T({en: 'This shop only sells second-hand bikes', nl: 'Deze winkel verkoopt enkel tweedehands fietsen', fr: 'TODO: fr'}),
},
diy: {
question: new T({en: 'Are there tools here to repair your own bike?', nl: 'Biedt deze winkel gereedschap aan om je fiets zelf te herstellen?', fr: 'TODO: fr'}),
yes: new T({en: 'This shop offers tools for DIY repair', nl: 'Deze winkel biedt gereedschap aan om je fiets zelf te herstellen', fr: 'TODO: fr'}),
no: new T({en: 'This shop doesn\'t offer tools for DIY repair', nl: 'Deze winkel biedt geen gereedschap aan om je fiets zelf te herstellen', fr: 'TODO: fr'}),
}
}
},
image: {
addPicture: new T({en: 'Add picture', nl: 'Voeg foto toe', fr: 'TODO: fr'}),
uploadingPicture: new T({
en: 'Uploading your picture...',
nl: 'Bezig met een foto te uploaden...',
fr: 'TODO: fr'
}),
pleaseLogin: new T({
en: 'Please login to add a picure or to answer questions',
nl: 'Gelieve je aan te melden om een foto toe te voegen of vragen te beantwoorden',
fr: 'TODO: fr'
}),
willBePublished: new T({
en: 'Your picture will be published: ',
nl: 'Jouw foto wordt gepubliceerd: ',
fr: 'TODO: fr'
}),
cco: new T({en: 'in the public domain', nl: 'in het publiek domein', fr: 'TODO: fr'}),
ccbs: new T({en: 'under the CC-BY-SA-license', nl: 'onder de CC-BY-SA-licentie', fr: 'TODO: fr'}),
ccb: new T({en: 'under the CC-BY-license', nl: 'onder de CC-BY-licentie', fr: 'TODO: fr'})
},
centerMessage: {
loadingData: new T({en: 'Loading data...', nl: 'Data wordt geladen...', fr: 'TODO: fr'}),
zoomIn: new T({
en: 'Zoom in to view or edit the data',
nl: 'Zoom in om de data te zien en te bewerken',
fr: 'TODO: fr'
}),
ready: new T({en: 'Done!', nl: 'Klaar!', fr: 'TODO: fr'}),
},
general: {
loginWithOpenStreetMap: new T({en: "Login with OpenStreetMap", nl: "Aanmelden met OpenStreetMap"}),
getStarted: new T({
en: "<span class='activate-osm-authentication'>Login with OpenStreetMap</span> or <a href='https://www.openstreetmap.org/user/new' target='_blank'>make a free account to get started</a>",
nl: "<span class='activate-osm-authentication'>Meld je aan met je OpenStreetMap-account</span> of <a href='https://www.openstreetmap.org/user/new' target='_blank'>maak snel en gratis een account om te beginnen/a>",
}),
welcomeBack: new T({
en: "You are logged in, welcome back!",
nl: "Je bent aangemeld. Welkom terug!"
}),
search: {
search: new Translation({
en: "Search a location",
nl: "Zoek naar een locatie"
}),
searching: new Translation({
en: "Searching...",
nl: "Aan het zoeken..."
}),
nothing: new Translation({
en: "Nothing found...",
nl: "Niet gevonden..."
}),
error: new Translation({
en: "Something went wrong...",
nl: "Niet gelukt..."
})
},
returnToTheMap: new T({
en: "Return to the map",
nl: "Naar de kaart"
}),
save: new T({
en: "Save",
nl: "Opslaan"
}),
cancel: new T({
en: "Cancel",
nl: "Annuleren"
}),
skip: new T({
en: "Skip this question",
nl: "Vraag overslaan"
})
}
}
public static W(s: string | UIElement): UIElement {
if (s instanceof UIElement) {
return s;
}
return new FixedUiElement(s);
}
}

View file

@ -9,9 +9,10 @@
<rect x="34.3118" y="72.5698" width="28" height="5" fill="white"/> <rect x="34.3118" y="72.5698" width="28" height="5" fill="white"/>
<rect x="65.3118" y="60.5698" width="4" height="8" fill="white"/> <rect x="65.3118" y="60.5698" width="4" height="8" fill="white"/>
<rect x="68.3118" y="64.5699" width="3" height="4" fill="white"/> <rect x="68.3118" y="64.5699" width="3" height="4" fill="white"/>
<rect x="66" y="72.2385" width="21.8167" height="3.68967" transform="rotate(-44.3049 66 72.2385)" fill="#F00D0D"/> <rect x="56" y="42" width="5" height="10" fill="#171615"/>
</g> </g>
<rect x="68.4303" y="56.8712" width="22.1163" height="3.52552" transform="rotate(43.5782 68.4303 56.8712)" fill="#F00D0D"/> <rect x="72.4167" y="53.8615" width="29.0549" height="7.85787" transform="rotate(43.5782 72.4167 53.8615)" fill="#F00D0D"/>
<path d="M68.2141 73.9097L88.6128 54L94.0003 59.5199L73.6016 79.4295L68.2141 73.9097Z" fill="#F00D0D"/>
<defs> <defs>
<filter id="filter0_d" x="-3" y="2" width="102.479" height="102.479" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <filter id="filter0_d" x="-3" y="2" width="102.479" height="102.479" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/> <feFlood flood-opacity="0" result="BackgroundImageFix"/>

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
assets/bike/cyclofix.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View file

@ -0,0 +1,18 @@
<svg width="98" height="124" viewBox="0 0 98 124" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M55.0445 114.094C53.2614 117.981 47.7386 117.981 45.9555 114.094L15.2124 47.085C13.6928 43.7729 16.1129 40 19.7569 40L81.2431 40C84.8871 40 87.3072 43.7729 85.7876 47.085L55.0445 114.094Z" fill="#6BC4F7"/>
<circle cx="49" cy="49" r="49" fill="#6BC4F7"/>
<g filter="url(#filter0_d)">
<path d="M79 41.8705C79 39.3976 77.4079 35.6726 74.1327 30.4823C71.8064 26.7956 69.5171 23.8005 69.4208 23.6747C69.0964 23.2513 68.5726 23 68.0144 23C67.4562 23 66.9322 23.2514 66.6078 23.6748C66.3787 23.9738 61.828 29.939 59.0942 35.3688C54.8849 28.8794 50.9494 23.7307 50.9067 23.6748C50.5821 23.2513 50.0582 23 49.5 23C48.9418 23 48.4179 23.2513 48.0934 23.6747C48.0468 23.7357 43.367 29.8581 38.753 37.1701C38.2587 37.9536 37.7856 38.7192 37.3295 39.4706C34.9483 35.6504 32.5162 32.4661 32.3922 32.3043C32.0677 31.8809 31.5438 31.6296 30.9856 31.6296C30.4274 31.6296 29.9036 31.8809 29.5791 32.3043C29.4828 32.43 27.1935 35.4252 24.8672 39.1119C21.5921 44.3021 20 48.0271 20 50.5C20 55.6193 24.0504 59.8732 29.3205 60.6234C30.1402 70.3329 38.8775 78 49.5 78C60.6619 78 69.7429 69.5347 69.7429 59.1295C69.7429 57.2539 69.2077 54.9089 68.1449 52.1097C74.1423 52.0443 79 47.4766 79 41.8705V41.8705ZM23.457 50.5C23.457 49.457 24.0257 46.8053 27.8344 40.7656C28.9755 38.9562 30.126 37.2947 30.9856 36.0929C32.1576 37.7318 33.8718 40.2278 35.3741 42.8008C31.9223 48.9002 29.9183 53.7824 29.3954 57.3607C26.0053 56.6784 23.457 53.8613 23.457 50.5V50.5ZM49.5 74.7773C40.2443 74.7773 32.7142 67.7578 32.7142 59.1295C32.7142 56.3792 34.2766 50.6239 41.7201 38.8241C44.7375 34.0407 47.784 29.7745 49.5 27.4354C51.213 29.7702 54.2519 34.0258 57.2673 38.8042C64.7212 50.6167 66.2857 56.3773 66.2857 59.1296C66.2858 67.7578 58.7557 74.7773 49.5 74.7773V74.7773ZM68.0144 48.8887C67.5836 48.8887 67.156 48.8549 66.7362 48.7879C65.3713 45.8533 63.5372 42.506 61.2717 38.8163C62.8077 35.0043 66.1199 30.1147 68.0149 27.4638C68.8707 28.6601 70.0148 30.3125 71.153 32.1162C74.9727 38.1694 75.543 40.8257 75.543 41.8705C75.543 45.7402 72.1656 48.8887 68.0144 48.8887V48.8887Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d" x="16" y="23" width="67" height="63" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -2,85 +2,10 @@
<path d="M52.1412 111.419C50.4633 115.605 44.5366 115.605 42.8588 111.419L24.7014 66.1099C23.385 62.8252 25.8039 59.25 29.3426 59.25L65.6574 59.25C69.1962 59.25 71.615 62.8252 70.2986 66.11L52.1412 111.419Z" fill="#5675DF"/> <path d="M52.1412 111.419C50.4633 115.605 44.5366 115.605 42.8588 111.419L24.7014 66.1099C23.385 62.8252 25.8039 59.25 29.3426 59.25L65.6574 59.25C69.1962 59.25 71.615 62.8252 70.2986 66.11L52.1412 111.419Z" fill="#5675DF"/>
<ellipse cx="48.5" cy="47.5" rx="48.5" ry="47.5" fill="#5675DF"/> <ellipse cx="48.5" cy="47.5" rx="48.5" ry="47.5" fill="#5675DF"/>
<g filter="url(#filter0_d)"> <g filter="url(#filter0_d)">
<circle cx="39" cy="66" r="2" stroke="white" stroke-width="2"/> <path d="M42.2812 53.1875V71H36.2812V25.5H53.0625C58.0417 25.5 61.9375 26.7708 64.75 29.3125C67.5833 31.8542 69 35.2188 69 39.4062C69 43.8229 67.6146 47.2292 64.8438 49.625C62.0938 52 58.1458 53.1875 53 53.1875H42.2812ZM42.2812 48.2812H53.0625C56.2708 48.2812 58.7292 47.5312 60.4375 46.0312C62.1458 44.5104 63 42.3229 63 39.4688C63 36.7604 62.1458 34.5938 60.4375 32.9688C58.7292 31.3438 56.3854 30.5 53.4062 30.4375H42.2812V48.2812Z" fill="white"/>
</g> </g>
<g filter="url(#filter1_d)">
<path d="M37.375 67H25L31.75 51.4H38.5H43.375H48.625M48.625 51.4L46.75 47H49.375H52M48.625 51.4L49.375 53.4L50.875 56.6L55 67M48.625 51.4L45.0625 57.4L42.925 61M41.5 63.4L42.925 61M34.375 55.8L38.125 64.6L30.625 47L32.875 51.8M40.375 65.4L42.925 61" stroke="white" stroke-width="2"/>
</g>
<g filter="url(#filter2_d)">
<circle cx="23" cy="67" r="9" stroke="white" stroke-width="2"/>
</g>
<g filter="url(#filter3_d)">
<circle cx="55" cy="67" r="9" stroke="white" stroke-width="2"/>
</g>
<path d="M61 77V59.0476V55.9524L62.4814 54.4851C64.4301 52.5549 67.5699 52.5549 69.5186 54.4851L71 55.9524V58.4286V77" stroke="white" stroke-width="2"/>
<line x1="66" y1="53" x2="66" y2="16" stroke="white" stroke-width="2"/>
<g filter="url(#filter4_d)">
<circle cx="66" cy="23" r="13" fill="white"/>
</g>
<g filter="url(#filter5_d)">
<circle cx="66" cy="23" r="11" fill="#496DEB"/>
</g>
<g filter="url(#filter6_d)">
<path d="M64.1729 24.9902V30H62.4854V17.2031H67.2051C68.6055 17.2031 69.7012 17.5605 70.4922 18.2754C71.2891 18.9902 71.6875 19.9365 71.6875 21.1143C71.6875 22.3564 71.2979 23.3145 70.5186 23.9883C69.7451 24.6562 68.6348 24.9902 67.1875 24.9902H64.1729ZM64.1729 23.6104H67.2051C68.1074 23.6104 68.7988 23.3994 69.2793 22.9775C69.7598 22.5498 70 21.9346 70 21.1318C70 20.3701 69.7598 19.7607 69.2793 19.3037C68.7988 18.8467 68.1396 18.6094 67.3018 18.5918H64.1729V23.6104Z" fill="white"/>
</g>
<line x1="43" y1="78" x2="72" y2="78" stroke="white" stroke-width="2"/>
<defs> <defs>
<filter id="filter0_d" x="32" y="63" width="14" height="14" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <filter id="filter0_d" x="32.2812" y="25.5" width="40.7188" height="53.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter1_d" x="19.4777" y="46" width="40.4518" height="30" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter2_d" x="9" y="57" width="28" height="28" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter3_d" x="41" y="57" width="28" height="28" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter4_d" x="49" y="10" width="34" height="34" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter5_d" x="51" y="12" width="30" height="30" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter6_d" x="58.4854" y="17.2031" width="17.2021" height="20.7969" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/> <feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/> <feOffset dy="4"/>

Before

Width:  |  Height:  |  Size: 6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,93 @@
<svg width="97" height="123" viewBox="0 0 97 123" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M52.1412 111.419C50.4633 115.605 44.5366 115.605 42.8588 111.419L24.7014 66.1099C23.385 62.8252 25.8039 59.25 29.3426 59.25L65.6574 59.25C69.1962 59.25 71.615 62.8252 70.2986 66.11L52.1412 111.419Z" fill="#5675DF"/>
<ellipse cx="48.5" cy="47.5" rx="48.5" ry="47.5" fill="#5675DF"/>
<g filter="url(#filter0_d)">
<circle cx="39" cy="66" r="2" stroke="white" stroke-width="2"/>
</g>
<g filter="url(#filter1_d)">
<path d="M37.375 67H25L31.75 51.4H38.5H43.375H48.625M48.625 51.4L46.75 47H49.375H52M48.625 51.4L49.375 53.4L50.875 56.6L55 67M48.625 51.4L45.0625 57.4L42.925 61M41.5 63.4L42.925 61M34.375 55.8L38.125 64.6L30.625 47L32.875 51.8M40.375 65.4L42.925 61" stroke="white" stroke-width="2"/>
</g>
<g filter="url(#filter2_d)">
<circle cx="23" cy="67" r="9" stroke="white" stroke-width="2"/>
</g>
<g filter="url(#filter3_d)">
<circle cx="55" cy="67" r="9" stroke="white" stroke-width="2"/>
</g>
<path d="M61 77V59.0476V55.9524L62.4814 54.4851C64.4301 52.5549 67.5699 52.5549 69.5186 54.4851L71 55.9524V58.4286V77" stroke="white" stroke-width="2"/>
<line x1="66" y1="53" x2="66" y2="16" stroke="white" stroke-width="2"/>
<g filter="url(#filter4_d)">
<circle cx="66" cy="23" r="13" fill="white"/>
</g>
<g filter="url(#filter5_d)">
<circle cx="66" cy="23" r="11" fill="#496DEB"/>
</g>
<g filter="url(#filter6_d)">
<path d="M64.1729 24.9902V30H62.4854V17.2031H67.2051C68.6055 17.2031 69.7012 17.5605 70.4922 18.2754C71.2891 18.9902 71.6875 19.9365 71.6875 21.1143C71.6875 22.3564 71.2979 23.3145 70.5186 23.9883C69.7451 24.6562 68.6348 24.9902 67.1875 24.9902H64.1729ZM64.1729 23.6104H67.2051C68.1074 23.6104 68.7988 23.3994 69.2793 22.9775C69.7598 22.5498 70 21.9346 70 21.1318C70 20.3701 69.7598 19.7607 69.2793 19.3037C68.7988 18.8467 68.1396 18.6094 67.3018 18.5918H64.1729V23.6104Z" fill="white"/>
</g>
<line x1="43" y1="78" x2="72" y2="78" stroke="white" stroke-width="2"/>
<defs>
<filter id="filter0_d" x="32" y="63" width="14" height="14" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter1_d" x="19.4777" y="46" width="40.4518" height="30" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter2_d" x="9" y="57" width="28" height="28" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter3_d" x="41" y="57" width="28" height="28" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter4_d" x="49" y="10" width="34" height="34" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter5_d" x="51" y="12" width="30" height="30" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter6_d" x="58.4854" y="17.2031" width="17.2021" height="20.7969" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6 KiB

12
clean.sh Executable file
View file

@ -0,0 +1,12 @@
#! /bin/bash
rm *.js
rm Logic/*.js
rm Logic/*.js
rm Logic/*/*.js
rm Logic/*/*/*.js
rm UI/*.js
rm UI/*/*.js
rm UI/*/*/*.js
rm Customizations/*.js
rm Customizations/*/*.js
rm Customizations/*/*/*.js

View file

@ -238,6 +238,13 @@ form {
height: 1em; height: 1em;
} }
#language-select {
pointer-events: all;
cursor: pointer;
position: absolute;
margin-left: 2em;
margin-top: 3em;
}
#messagesbox-wrapper { #messagesbox-wrapper {
} }
@ -303,23 +310,26 @@ form {
} }
} }
#to-the-map { #to-the-map {
position: relative;
}
#to-the-map h2{
position: absolute;
height: 4em; height: 4em;
padding: 0.5em; padding: 0.5em;
margin: 0; margin: 0;
position: absolute;
bottom: 0;
right: 0;
padding-right: 2em; padding-right: 2em;
padding-top: 1em;
text-align: center;
width: 100%; width: 100%;
text-align: right;
color: white; color: white;
background-color: #7ebc6f; background-color: #7ebc6f;
cursor: pointer; cursor: pointer;
} }

View file

@ -9,17 +9,22 @@
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossorigin=""/> crossorigin=""/>
<link rel="stylesheet" href="./index.css"/> <link rel="stylesheet" href="./index.css"/>
<meta property="og:image" content="https://buurtnatuur.be/assets/BuurtnatuurFront.jpg" /> <meta property="og:image" content="/assets/bike/cyclofix.jpeg" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="Buurtnatuur.be - samen natuur in kaart brengen" /> <meta property="og:title" content="Cyclofix - a map for and by cyclists" />
<meta property="og:description" content="Met deze tool kan je natuur in je buurt in kaart brengen en meer informatie geven over je favoriete plekje" /> <meta property="og:description" content="With this tool, cyclists can contribute to a map of cycling infrastructure" />
</head> </head>
<body> <body>
<div id="messagesboxmobilewrapper"> <div id="messagesboxmobilewrapper">
<div id="messagesboxmobile-scroll"> <div id="messagesboxmobile-scroll">
<div id="messagesboxmobile"> </div> <div id="messagesboxmobile"></div>
</div>
<div id="to-the-map">
<h2 id="to-the-map-h2">
Loading... If this message persists, check if javascript is enabled and if no extension (uMatrix) is
blocking it.
</h2>
</div> </div>
<div id="to-the-map">Loading... If this message persists, check if javascript is enabled and if no extension (uMatrix) is blocking it.</div>
</div> </div>
<div id="topleft-tools"> <div id="topleft-tools">
@ -28,6 +33,7 @@
Loading... If this message persists, check if javascript is enabled and if no extension (uMatrix) is Loading... If this message persists, check if javascript is enabled and if no extension (uMatrix) is
blocking it. blocking it.
</div> </div>
<div id="language-select"></div>
<br/> <br/>
<div id="searchbox"></div> <div id="searchbox"></div>
</div> </div>

106
index.ts
View file

@ -11,7 +11,7 @@ import {Tag, TagUtils} from "./Logic/TagsFilter";
import {FilteredLayer} from "./Logic/FilteredLayer"; import {FilteredLayer} from "./Logic/FilteredLayer";
import {LayerUpdater} from "./Logic/LayerUpdater"; import {LayerUpdater} from "./Logic/LayerUpdater";
import {UIElement} from "./UI/UIElement"; import {UIElement} from "./UI/UIElement";
import {MessageBoxHandler} from "./UI/MessageBoxHandler"; import {FullScreenMessageBoxHandler} from "./UI/FullScreenMessageBoxHandler";
import {Overpass} from "./Logic/Overpass"; import {Overpass} from "./Logic/Overpass";
import {FeatureInfoBox} from "./UI/FeatureInfoBox"; import {FeatureInfoBox} from "./UI/FeatureInfoBox";
import {GeoLocationHandler} from "./Logic/GeoLocationHandler"; import {GeoLocationHandler} from "./Logic/GeoLocationHandler";
@ -22,6 +22,14 @@ import {SearchAndGo} from "./UI/SearchAndGo";
import {CollapseButton} from "./UI/Base/CollapseButton"; import {CollapseButton} from "./UI/Base/CollapseButton";
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
import {All} from "./Customizations/Layouts/All"; import {All} from "./Customizations/Layouts/All";
import Translations from "./UI/i18n/Translations";
import Translation from "./UI/i18n/Translation";
import Locale from "./UI/i18n/Locale";
import {Layout, WelcomeMessage} from "./Customizations/Layout";
import {DropDown} from "./UI/Input/DropDown";
import {FixedInputElement} from "./UI/Input/FixedInputElement";
import {FixedUiElement} from "./UI/Base/FixedUiElement";
import ParkingType from "./Customizations/Questions/bike/ParkingType";
// --------------------- Read the URL parameters ----------------- // --------------------- Read the URL parameters -----------------
@ -83,22 +91,32 @@ if (paramDict.test) {
dryRun = paramDict.test === "true"; dryRun = paramDict.test === "true";
} }
const layoutToUse = AllKnownLayouts.allSets[defaultLayout]; const layoutToUse: Layout = AllKnownLayouts.allSets[defaultLayout];
console.log("Using layout: ", layoutToUse.name); console.log("Using layout: ", layoutToUse.name);
document.title = layoutToUse.title; document.title = layoutToUse.title.InnerRender();
Locale.language.addCallback(e => {
document.title = layoutToUse.title.InnerRender();
})
// ----------------- Setup a few event sources ------------- // ----------------- Setup a few event sources -------------
// const LanguageSelect = document.getElementById('language-select') as HTMLOptionElement
// eLanguageSelect.addEventListener('selectionchange')
// The message that should be shown at the center of the screen // The message that should be shown at the center of the screen
const centerMessage = new UIEventSource<string>(""); const centerMessage = new UIEventSource<string>("");
// The countdown: if set to e.g. ten, it'll start counting down. When reaching zero, changes will be saved. NB: this is implemented later, not in the eventSource // The countdown: if set to e.g. ten, it'll start counting down. When reaching zero, changes will be saved. NB: this is implemented later, not in the eventSource
const secondsTillChangesAreSaved = new UIEventSource<number>(0); const secondsTillChangesAreSaved = new UIEventSource<number>(0);
const leftMessage = new UIEventSource<() => UIElement>(undefined); // const leftMessage = new UIEventSource<() => UIElement>(undefined);
// This message is shown full screen on mobile devices
const fullScreenMessage = new UIEventSource<UIElement>(undefined);
const selectedElement = new UIEventSource<any>(undefined); const selectedElement = new UIEventSource<any>(undefined);
@ -112,9 +130,19 @@ const locationControl = new UIEventSource<{ lat: number, lon: number, zoom: numb
// ----------------- Prepare the important objects ----------------- // ----------------- Prepare the important objects -----------------
const osmConnection = new OsmConnection(dryRun);
Locale.language.syncWith(osmConnection.GetPreference("language"));
// @ts-ignore
window.setLanguage = function (language: string) {
Locale.language.setData(language)
}
const saveTimeout = 30000; // After this many milliseconds without changes, saves are sent of to OSM const saveTimeout = 30000; // After this many milliseconds without changes, saves are sent of to OSM
const allElements = new ElementStorage(); const allElements = new ElementStorage();
const osmConnection = new OsmConnection(dryRun);
const changes = new Changes( const changes = new Changes(
"Beantwoorden van vragen met #MapComplete voor vragenset #" + layoutToUse.name, "Beantwoorden van vragen met #MapComplete voor vragenset #" + layoutToUse.name,
osmConnection, allElements); osmConnection, allElements);
@ -137,7 +165,6 @@ const bm = new Basemap("leafletDiv", locationControl, new VariableUiElement(
// ------------- Setup the layers ------------------------------- // ------------- Setup the layers -------------------------------
const controls = {};
const addButtons: { const addButtons: {
name: string, name: string,
icon: string, icon: string,
@ -167,8 +194,6 @@ for (const layer of layoutToUse.layers) {
const flayer = layer.asLayer(bm, allElements, changes, osmConnection.userDetails, selectedElement, generateInfo); const flayer = layer.asLayer(bm, allElements, changes, osmConnection.userDetails, selectedElement, generateInfo);
controls[layer.name] = flayer.isDisplayed;
const addButton = { const addButton = {
name: layer.name, name: layer.name,
icon: layer.icon, icon: layer.icon,
@ -184,8 +209,13 @@ const layerUpdater = new LayerUpdater(bm, minZoom, flayers);
// ------------------ Setup various UI elements ------------ // ------------------ Setup various UI elements ------------
let languagePicker = new DropDown(" ", layoutToUse.supportedLanguages.map(lang => {
return {value: lang, shown: lang}
}
), Locale.language).AttachTo("language-select");
new StrayClickHandler(bm, selectedElement, leftMessage, () => {
new StrayClickHandler(bm, selectedElement, fullScreenMessage, () => {
return new SimpleAddUI(bm.Location, return new SimpleAddUI(bm.Location,
bm.LastClickLocation, bm.LastClickLocation,
changes, changes,
@ -197,7 +227,7 @@ new StrayClickHandler(bm, selectedElement, leftMessage, () => {
); );
/** /**
* Show the questions and information for the selected element on the leftMessage * Show the questions and information for the selected element on the fullScreen
*/ */
selectedElement.addCallback((data) => { selectedElement.addCallback((data) => {
// Which is the applicable set? // Which is the applicable set?
@ -206,14 +236,16 @@ selectedElement.addCallback((data) => {
const applicable = layer.overpassFilter.matches(TagUtils.proprtiesToKV(data)); const applicable = layer.overpassFilter.matches(TagUtils.proprtiesToKV(data));
if (applicable) { if (applicable) {
// This layer is the layer that gives the questions // This layer is the layer that gives the questions
leftMessage.setData(() =>
new FeatureInfoBox( const featureBox = new FeatureInfoBox(
allElements.getElement(data.id), allElements.getElement(data.id),
layer.title, layer.title,
layer.elementsToShow, layer.elementsToShow,
changes, changes,
osmConnection.userDetails osmConnection.userDetails
)); );
fullScreenMessage.setData(featureBox);
break; break;
} }
} }
@ -224,36 +256,28 @@ selectedElement.addCallback((data) => {
const pendingChanges = new PendingChanges( const pendingChanges = new PendingChanges(
changes, secondsTillChangesAreSaved,); changes, secondsTillChangesAreSaved,);
new UserBadge(osmConnection.userDetails, pendingChanges, bm) new UserBadge(osmConnection.userDetails,
pendingChanges,
new FixedUiElement(""),
bm)
.AttachTo('userbadge'); .AttachTo('userbadge');
new SearchAndGo(bm).AttachTo("searchbox"); new SearchAndGo(bm).AttachTo("searchbox");
new CollapseButton("messagesbox") new CollapseButton("messagesbox")
.AttachTo("collapseButton"); .AttachTo("collapseButton");
new WelcomeMessage(layoutToUse, osmConnection).AttachTo("messagesbox");
var welcomeMessage = () => { fullScreenMessage.setData(
return new VariableUiElement( new WelcomeMessage(layoutToUse, osmConnection)
osmConnection.userDetails.map((userdetails) => { );
var login = layoutToUse.gettingStartedPlzLogin;
if (userdetails.loggedIn) {
login = layoutToUse.welcomeBackMessage;
}
return "<div id='welcomeMessage'>" +
layoutToUse.welcomeMessage + login + layoutToUse.welcomeTail +
"</div>";
}),
function () {
osmConnection.registerActivateOsmAUthenticationClass()
});
}
leftMessage.setData(welcomeMessage);
welcomeMessage().AttachTo("messagesbox");
var messageBox = new MessageBoxHandler(leftMessage, () => { new FullScreenMessageBoxHandler(fullScreenMessage, () => {
selectedElement.setData(undefined) selectedElement.setData(undefined)
}); }).update();
// fullScreenMessage.setData(generateWelcomeMessage());
new CenterMessageBox( new CenterMessageBox(
minZoom, minZoom,
@ -276,4 +300,6 @@ new GeoLocationHandler(bm).AttachTo("geolocate-button");
// --------------- Send a ping to start various action -------- // --------------- Send a ping to start various action --------
locationControl.ping(); locationControl.ping();
messageBox.update();
window.setTimeout(() => {Locale.language.setData("nl")}, 5000)

24
test.ts
View file

@ -1,17 +1,13 @@
import {UIEventSource} from "./UI/UIEventSource"; import {DropDown} from "./UI/Input/DropDown";
import {Changes} from "./Logic/Changes"; import Locale from "./UI/i18n/Locale";
import {OsmConnection} from "./Logic/OsmConnection"; import Combine from "./UI/Base/Combine";
import {ElementStorage} from "./Logic/ElementStorage"; import Translations from "./UI/i18n/Translations";
import {WikipediaLink} from "./Customizations/Questions/WikipediaLink";
import {OsmLink} from "./Customizations/Questions/OsmLink";
import {ConfirmDialog} from "./UI/ConfirmDialog";
import {Imgur} from "./Logic/Imgur";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
console.log("Hello world")
const html = new UIEventSource<string>("Some text"); let languagePicker = new DropDown("", ["en", "nl"].map(lang => {
return {value: lang, shown: lang}
}
), Locale.language).AttachTo("maindiv");
const uielement = new VariableUiElement(html); new Combine(["abc",Translations.t.cyclofix.title, Translations.t.cyclofix.title]).AttachTo("extradiv");
uielement.AttachTo("maindiv")
window.setTimeout(() => {html.setData("Different text")}, 1000)