Merge branches

This commit is contained in:
Pieter Vander Vennet 2020-07-22 11:05:04 +02:00
commit 00fb99defe
117 changed files with 3104 additions and 1424 deletions

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import { FilteredLayer } from "../../Logic/FilteredLayer";
export class CheckBox extends UIElement{
private data: UIEventSource<boolean>;
private readonly _data: UIEventSource<boolean>;
private readonly _showEnabled: string|UIElement;
@ -21,7 +22,7 @@ export class CheckBox extends UIElement{
}
protected InnerRender(): string {
InnerRender(): string {
if (this._data.data) {
return this._showEnabled;
} else {

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) {
super(undefined);
this._html = html;
this._html = html ?? "";
}
protected InnerRender(): string {
InnerRender(): string {
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;
}
InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
if(this._innerUpdate !== undefined){
this._innerUpdate(htmlElement);
}
}
}

View file

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

View file

@ -10,10 +10,18 @@ import {TagRenderingOptions} from "../Customizations/TagRendering";
import {OsmLink} from "../Customizations/Questions/OsmLink";
import {WikipediaLink} from "../Customizations/Questions/WikipediaLink";
import {And} from "../Logic/TagsFilter";
import {TagDependantUIElement} from "../Customizations/UIElementConstructor";
import {TagDependantUIElement, TagDependantUIElementConstructor} from "../Customizations/UIElementConstructor";
import Translations from "./i18n/Translations";
export class FeatureInfoBox extends UIElement {
/**
* The actual GEOJSON-object, with geometry and stuff
*/
private _feature: any;
/**
* The tags, wrapped in a global event source
*/
private _tagsES: UIEventSource<any>;
private _changes: Changes;
private _userDetails: UIEventSource<UserDetails>;
@ -24,31 +32,49 @@ export class FeatureInfoBox extends UIElement {
private _wikipedialink: UIElement;
private _infoboxes: TagDependantUIElement[];
private _questions: QuestionPicker;
private _oneSkipped = Translations.t.general.oneSkippedQuestion.Clone();
private _someSkipped = Translations.t.general.skippedQuestions.Clone();
constructor(
feature: any,
tagsES: UIEventSource<any>,
title: TagRenderingOptions,
elementsToShow: TagRenderingOptions[],
title: TagRenderingOptions | UIElement,
elementsToShow: TagDependantUIElementConstructor[],
changes: Changes,
userDetails: UIEventSource<UserDetails>
) {
super(tagsES);
this._feature = feature;
this._tagsES = tagsES;
this._changes = changes;
this._userDetails = userDetails;
this.ListenTo(userDetails);
const deps = {tags:this._tagsES , changes:this._changes}
const deps = {tags: this._tagsES, changes: this._changes}
this._infoboxes = [];
elementsToShow = elementsToShow ?? []
const self = this;
for (const tagRenderingOption of elementsToShow) {
this._infoboxes.push(
self._infoboxes.push(
tagRenderingOption.construct(deps));
}
function initTags() {
self._infoboxes = []
for (const tagRenderingOption of elementsToShow) {
self._infoboxes.push(
tagRenderingOption.construct(deps));
}
self.Update();
}
this._someSkipped.onClick(initTags)
this._oneSkipped.onClick(initTags)
title = title ?? new TagRenderingOptions(
{
@ -56,10 +82,14 @@ export class FeatureInfoBox extends UIElement {
}
)
this._title = new TagRenderingOptions(title.options).construct(deps);
this._osmLink =new OsmLink().construct(deps);
if (title instanceof UIElement) {
this._title = title;
} else {
this._title = new TagRenderingOptions(title.options).construct(deps);
}
this._osmLink = new OsmLink().construct(deps);
this._wikipedialink = new WikipediaLink().construct(deps);
}
@ -67,13 +97,16 @@ export class FeatureInfoBox extends UIElement {
const info = [];
const questions : TagDependantUIElement[] = [];
const questions: TagDependantUIElement[] = [];
let skippedQuestions = 0;
for (const infobox of this._infoboxes) {
if (infobox.IsKnown()) {
info.push(infobox);
} else if (infobox.IsQuestioning()) {
questions.push(infobox);
} else {
// This question is neither known nor questioning -> it was skipped
skippedQuestions++;
}
}
@ -93,6 +126,10 @@ export class FeatureInfoBox extends UIElement {
}
questionsHtml = mostImportantQuestion.Render();
} else if (skippedQuestions == 1) {
questionsHtml = this._oneSkipped.Render();
} else if (skippedQuestions > 0) {
questionsHtml = this._someSkipped.Render();
}
return "<div class='featureinfobox'>" +
@ -115,4 +152,6 @@ export class FeatureInfoBox extends UIElement {
"</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 {UIElement} from "./UIElement";
import {VariableUiElement} from "./Base/VariableUIElement";
import Translations from "./i18n/Translations";
export class MessageBoxHandler {
private _uielement: UIEventSource<() => UIElement>;
/**
* Handles the full screen popup on mobile
*/
export class FullScreenMessageBoxHandler {
private _uielement: UIEventSource<UIElement>;
constructor(uielement: UIEventSource<() => UIElement>,
constructor(uielement: UIEventSource<UIElement>,
onClear: (() => void)) {
this._uielement = uielement;
this.listenTo(uielement);
@ -22,14 +24,13 @@ export class MessageBoxHandler {
}
}
new VariableUiElement(new UIEventSource<string>("<h2>Naar de kaart</h2>"),
() => {
document.getElementById("to-the-map").onclick = function () {
uielement.setData(undefined);
onClear();
}
}
).AttachTo("to-the-map");
Translations.t.general.returnToTheMap
.onClick(() => {
console.log("Clicked 'return to the map'")
uielement.setData(undefined);
onClear();
})
.AttachTo("to-the-map-h2");
}
@ -45,7 +46,6 @@ export class MessageBoxHandler {
update() {
const wrapper = document.getElementById("messagesboxmobilewrapper");
const gen = this._uielement.data;
console.log("Generator: ", gen);
if (gen === undefined) {
wrapper.classList.add("hidden")
if (location.hash !== "") {
@ -55,12 +55,8 @@ export class MessageBoxHandler {
}
location.hash = "#element"
wrapper.classList.remove("hidden");
/* gen()
?.HideOnEmpty(true)
?.AttachTo("messagesbox")
?.Activate();*/
gen()
gen
?.HideOnEmpty(true)
?.AttachTo("messagesboxmobile")
?.Activate();

View file

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

View file

@ -34,14 +34,14 @@ class ImageCarouselWithUpload extends TagDependantUIElement {
const changes = dependencies.changes;
this._imageElement = new ImageCarousel(tags, changes);
const userDetails = changes.login.userDetails;
const license = changes.login.GetPreference( "mapcomplete-pictures-license");
const license = changes.login.GetPreference( "pictures-license");
this._pictureUploader = new OsmImageUploadHandler(tags,
userDetails, license,
changes, this._imageElement.slideshow).getUI();
}
protected InnerRender(): string {
InnerRender(): string {
return this._imageElement.Render() +
this._pictureUploader.Render();
}

View file

@ -3,20 +3,21 @@ import {UIEventSource} from "./UIEventSource";
import $ from "jquery"
import {Imgur} from "../Logic/Imgur";
import {UserDetails} from "../Logic/OsmConnection";
import {DropDownUI} from "./Base/DropDownUI";
import {DropDown} from "./Input/DropDown";
import {VariableUiElement} from "./Base/VariableUIElement";
import Translations from "./i18n/Translations";
export class ImageUploadFlow extends UIElement {
private _licensePicker: UIElement;
private _selectedLicence: UIEventSource<string>;
private _licenseExplanation: UIElement;
private _isUploading: UIEventSource<number> = new UIEventSource<number>(0)
private _didFail: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private _uploadOptions: (license: string) => { title: string; description: string; handleURL: (url: string) => void; allDone: (() => void) };
private _userdetails: UIEventSource<UserDetails>;
constructor(
userInfo: UIEventSource<UserDetails>,
preferedLicense : UIEventSource<string>,
preferedLicense: UIEventSource<string>,
uploadOptions: ((license: string) =>
{
title: string,
@ -30,70 +31,63 @@ export class ImageUploadFlow extends UIElement {
this.ListenTo(userInfo);
this._uploadOptions = uploadOptions;
this.ListenTo(this._isUploading);
this.ListenTo(this._didFail);
const licensePicker = new DropDownUI("Jouw foto wordt gepubliceerd ",
const licensePicker = new DropDown(Translations.t.image.willBePublished,
[
{value: "CC0", shown: "in het publiek domein"},
{value: "CC-BY-SA 4.0", shown: "onder een CC-BY-SA-licentie"},
{value: "CC-BY 4.0", shown: "onder een CC-BY-licentie"}
{value: "CC0", shown: Translations.t.image.cco},
{value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs},
{value: "CC-BY 4.0", shown: Translations.t.image.ccb}
],
preferedLicense
);
this._licensePicker = licensePicker;
this._selectedLicence = licensePicker.selectedElement;
this._selectedLicence = licensePicker.GetValue();
const licenseExplanations = {
"CC-BY-SA 4.0":
"<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) {
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) {
return "<b>Bezig met een foto te uploaden...</b>"
return `<b>${Translations.t.image.uploadingPicture.Render()}</b>`
}
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>"
}
if(this._didFail.data){
uploadingMessage += "<b>Some images failed to upload. Imgur migth be down or you might block third-party API's (e.g. by using Brave or UMatrix)</b><br/>"
}
return "" +
"<div class='imageflow'>" +
"<label for='fileselector-" + this.id + "'>" +
"<div class='imageflow-file-input-wrapper'>" +
"<img src='./assets/camera-plus.svg' alt='upload image'/> " +
"<span class='imageflow-add-picture'>Voeg foto toe</span>" +
"<div class='break'></div>"+
`<span class='imageflow-add-picture'>${Translations.t.image.addPicture.R()}</span>` +
"<div class='break'></div>" +
"</div>" +
this._licensePicker.Render() +
this._licensePicker.Render() + "<br/>" +
uploadingMessage +
"</label>" +
"<input id='fileselector-" + this.id + "' " +
"type='file' " +
"class='imageflow-file-input' " +
"accept='image/*' name='picField' size='24' multiple='multiple' alt=''" +
"/>" +
"</div>"
;
}
@ -128,11 +122,12 @@ export class ImageUploadFlow extends UIElement {
function () {
console.log("All uploads completed")
opts.allDone();
},
function(failReason) {
}
)
}
}
}
}

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) {
return "<span class='alert'>Saving</span>";
}

View file

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

View file

@ -15,17 +15,17 @@ export class SimpleAddUI extends UIElement {
private _addButtons: UIElement[];
private _lastClickLocation: UIEventSource<{ lat: number; lon: number }>;
private _changes: Changes;
private _selectedElement: UIEventSource<any>;
private _selectedElement: UIEventSource<{feature: any}>;
private _dataIsLoading: UIEventSource<boolean>;
private _userDetails: UIEventSource<UserDetails>;
constructor(zoomlevel: UIEventSource<{ zoom: number }>,
lastClickLocation: UIEventSource<{ lat: number, lon: number }>,
changes: Changes,
selectedElement: UIEventSource<any>,
selectedElement: UIEventSource<{feature: any}>,
dataIsLoading: UIEventSource<boolean>,
userDetails: UIEventSource<UserDetails>,
addButtons: { name: string; icon: string; tags: Tag[]; layerToAddTo: FilteredLayer }[],
addButtons: { name: UIElement; icon: string; tags: Tag[]; layerToAddTo: FilteredLayer }[],
) {
super(zoomlevel);
this._zoomlevel = zoomlevel;
@ -42,37 +42,36 @@ export class SimpleAddUI extends UIElement {
// <button type='button'> looks SO retarded
// the default type of button is 'submit', which performs a POST and page reload
const button =
new Button(new FixedUiElement("Voeg hier een " + option.name + " toe"),
new Button(new FixedUiElement("Add a " + option.name.Render() + " here"),
this.CreatePoint(option));
this._addButtons.push(button);
}
}
private CreatePoint(option: { name: string; icon: string; tags: Tag[]; layerToAddTo: FilteredLayer }) {
private CreatePoint(option: {icon: string; tags: Tag[]; layerToAddTo: FilteredLayer }) {
const self = this;
return () => {
console.log("Creating a new ", option.name, " at last click location");
const loc = self._lastClickLocation.data;
let feature = self._changes.createElement(option.tags, loc.lat, loc.lon);
option.layerToAddTo.AddNewElement(feature);
self._selectedElement.setData(feature.properties);
self._selectedElement.setData({feature: feature});
}
}
protected InnerRender(): string {
const header = "<h2>Geen selectie</h2>" +
"Je klikte ergens waar er nog geen gezochte data is.<br/>";
InnerRender(): string {
const header = "<h2>No data here</h2>" +
"You clicked somewhere where no data is known yet.<br/>";
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) {
return header + "Zoom verder in om een element toe te voegen.";
return header + "Zoom in further to add a point.";
}
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 = "";
@ -83,10 +82,6 @@ export class SimpleAddUI extends UIElement {
}
InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
for (const button of this._addButtons) {
button.Update();
}
this._userDetails.data.osmConnection.registerActivateOsmAUthenticationClass();
}

View file

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

View file

@ -1,6 +1,6 @@
export class UIEventSource<T>{
public data : T;
public data: T;
private _callbacks = [];
constructor(data: T) {
@ -27,15 +27,32 @@ export class UIEventSource<T>{
}
}
public map<J>(f: ((T) => J),
extraSources : UIEventSource<any>[] = []): UIEventSource<J> {
const self = this;
public static flatten<X>(source: UIEventSource<UIEventSource<X>>, possibleSources: UIEventSource<any>[]): UIEventSource<X> {
const sink = new UIEventSource<X>(source.data?.data);
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 () {
newSource.setData(f(self.data));
newSource.ping();
}
this.addCallback(update);
for (const extraSource of extraSources) {
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 {FixedUiElement} from "./Base/FixedUiElement";
import {VariableUiElement} from "./Base/VariableUIElement";
import Translations from "./i18n/Translations";
/**
* Handles and updates the user badge
@ -15,12 +16,15 @@ export class UserBadge extends UIElement {
private _logout: UIElement;
private _basemap: Basemap;
private _homeButton: UIElement;
private _languagePicker: UIElement;
constructor(userDetails: UIEventSource<UserDetails>,
pendingChanges: UIElement,
languagePicker: UIElement,
basemap: Basemap) {
super(userDetails);
this._languagePicker = languagePicker;
this._userDetails = userDetails;
this._pendingChanges = pendingChanges;
this._basemap = basemap;
@ -57,10 +61,10 @@ export class UserBadge extends UIElement {
}
protected InnerRender(): string {
InnerRender(): string {
const user = this._userDetails.data;
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></span> " +
this._logout.Render() +
this._languagePicker.Render() +
this._pendingChanges.Render() +
"</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;
}
}

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

@ -0,0 +1,43 @@
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();
}
public Clone(){
return new Translation(this.translations)
}
}

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

@ -0,0 +1,394 @@
import Translation from "./Translation";
import T from "./Translation";
import {UIElement} from "../UIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
export default class Translations {
constructor() {
throw "Translations is static. If you want to intitialize a new translation, use the singular form"
}
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 shop', nl: 'Fietszaak', fr: 'TODO: fr'}),
titleRepair: new T({en: 'Bike repair', nl: 'Fietsenmaker', fr: 'TODO: fr'}),
titleShop: new T({en: 'Bike repair/shop', nl: 'Fietswinkel', fr: 'TODO: fr'}),
titleNamed: new T({en: 'Bike repair/shop', nl: 'Fietszaak {name}', fr: 'TODO: fr'}),
titleRepairNamed: new T({en: 'Bike shop', nl: 'Fietsenmaker {name}', fr: 'TODO: fr'}),
titleShopNamed: new T({en: 'Bike repair/shop', nl: 'Fietswinkel {name}', 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 fietszaak?', fr: 'TODO: fr'}),
render: new T({en: 'This bicycle shop is called {name}', nl: 'Deze fietszaak heet <b>{name}</b>', fr: 'TODO: fr'}),
template: new T({en: 'This bicycle shop is called: $$$', nl: 'Deze fietszaak heet: <b>$$$</b>', 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"
}),
oneSkippedQuestion: new T({
en: "One question is skipped",
nl: "Een vraag is overgeslaan"
}),
skippedQuestions: new T({
en: "Some questions are skipped",
nl: "Sommige vragen zijn overgeslaan"
})
}
}
public static W(s: string | UIElement): UIElement {
if (s instanceof UIElement) {
return s;
}
return new FixedUiElement(s);
}
}