New question system

This commit is contained in:
Pieter Vander Vennet 2020-07-05 18:59:47 +02:00
parent 1738fc4252
commit d1f8080c24
45 changed files with 1391 additions and 689 deletions

View file

@ -0,0 +1,24 @@
import {Groen} from "./Layouts/Groen";
import {Toilets} from "./Layouts/Toilets";
import {GRB} from "./Layouts/GRB";
import {Statues} from "./Layouts/Statues";
import {Bookcases} from "./Layouts/Bookcases";
export class AllKnownLayouts {
public static allSets: any = AllKnownLayouts.AllLayouts();
private static AllLayouts() {
const layouts = [
new Groen(),
new GRB(),
/*new Toilets(),
new Statues(),
new Bookcases()*/
];
const allSets = {};
for (const layout of layouts) {
allSets[layout.name] = layout;
}
return allSets;
}
}

View file

@ -1,14 +1,12 @@
import {Basemap} from "./Logic/Basemap"; import {Tag, TagsFilter} from "../Logic/TagsFilter";
import {ElementStorage} from "./Logic/ElementStorage"; import {UIElement} from "../UI/UIElement";
import {Changes} from "./Logic/Changes"; import {Basemap} from "../Logic/Basemap";
import {QuestionDefinition} from "./Logic/Question"; import {ElementStorage} from "../Logic/ElementStorage";
import {TagMappingOptions} from "./UI/TagMapping"; import {UIEventSource} from "../UI/UIEventSource";
import {UIEventSource} from "./UI/UIEventSource"; import {FilteredLayer} from "../Logic/FilteredLayer";
import {UIElement} from "./UI/UIElement"; import {Changes} from "../Logic/Changes";
import {Tag, TagsFilter} from "./Logic/TagsFilter"; import {UserDetails} from "../Logic/OsmConnection";
import {FilteredLayer} from "./Logic/FilteredLayer"; import {TagRenderingOptions} from "./TagRendering";
import {UserDetails} from "./Logic/OsmConnection";
export class LayerDefinition { export class LayerDefinition {
@ -19,8 +17,8 @@ export class LayerDefinition {
minzoom: number; minzoom: number;
overpassFilter: TagsFilter; overpassFilter: TagsFilter;
elementsToShow: (TagMappingOptions | QuestionDefinition | UIElement)[]; title: TagRenderingOptions;
questions: QuestionDefinition[]; // Questions are shown below elementsToShow in a questionPicker elementsToShow: TagRenderingOptions[];
style: (tags: any) => { color: string, icon: any }; style: (tags: any) => { color: string, icon: any };

View file

@ -1,10 +1,7 @@
import {LayerDefinition} from "../LayerDefinition"; import {LayerDefinition} from "../LayerDefinition";
import {FixedUiElement} from "../UI/Base/FixedUiElement"; import {QuestionDefinition} from "../../Logic/Question";
import {Tag} from "../../Logic/TagsFilter";
import L from "leaflet"; import L from "leaflet";
import {CommonTagMappings} from "./CommonTagMappings";
import {TagMappingOptions} from "../UI/TagMapping";
import {QuestionDefinition} from "../Logic/Question";
import {Tag} from "../Logic/TagsFilter";
export class Artwork extends LayerDefinition { export class Artwork extends LayerDefinition {
@ -40,7 +37,8 @@ export class Artwork extends LayerDefinition {
iconUrl: "assets/statue.svg", iconUrl: "assets/statue.svg",
iconSize: [40, 40], iconSize: [40, 40],
text: "hi" text: "hi"
}) }),
color: "#0000ff"
}; };
} }
@ -80,8 +78,7 @@ export class Artwork extends LayerDefinition {
new TagMappingOptions({key: "image", template: "<img class='popupImg' alt='image' src='{image}' />"}), new TagMappingOptions({key: "image", template: "<img class='popupImg' alt='image' src='{image}' />"})
CommonTagMappings.osmLink
]; ];
} }

View file

@ -1,8 +1,8 @@
import {LayerDefinition} from "../LayerDefinition"; import {LayerDefinition} from "../LayerDefinition";
import {TagMappingOptions} from "../UI/TagMapping";
import L from "leaflet"; import L from "leaflet";
import {QuestionDefinition} from "../Logic/Question"; import {Tag} from "../../Logic/TagsFilter";
import {Tag} from "../Logic/TagsFilter"; import {QuestionDefinition} from "../../Logic/Question";
import {TagRenderingOptions} from "../TagRendering";
export class Bookcases extends LayerDefinition { export class Bookcases extends LayerDefinition {
@ -31,13 +31,15 @@ export class Bookcases extends LayerDefinition {
icon: new L.icon({ icon: new L.icon({
iconUrl: "assets/bookcase.svg", iconUrl: "assets/bookcase.svg",
iconSize: [40, 40] iconSize: [40, 40]
}) }),
color: "#0000ff"
}; };
} }
this.elementsToShow = [ this.elementsToShow = [
new TagMappingOptions({ new TagMappingOptions({
key: "name", key: "name",
template: "{name}", template: "{name}",

View file

@ -1,9 +1,11 @@
import {LayerDefinition} from "../LayerDefinition"; import {LayerDefinition} from "../LayerDefinition";
import {Quests} from "../Quests"; import {Quests} from "../../Quests";
import {TagMappingOptions} from "../UI/TagMapping"; import {And, Or, Tag} from "../../Logic/TagsFilter";
import L from "leaflet" import {AccessTag} from "../Questions/AccessTag";
import {CommonTagMappings} from "./CommonTagMappings"; import {OperatorTag} from "../Questions/OperatorTag";
import {Or, Tag} from "../Logic/TagsFilter"; import {TagRenderingOptions} from "../TagRendering";
import {NameQuestion} from "../Questions/NameQuestion";
import {NameInline} from "../Questions/NameInline";
export class Bos extends LayerDefinition { export class Bos extends LayerDefinition {
@ -27,17 +29,12 @@ export class Bos extends LayerDefinition {
this.maxAllowedOverlapPercentage = 10; this.maxAllowedOverlapPercentage = 10;
this.minzoom = 13; this.minzoom = 13;
this.questions = [Quests.nameOf(this.name), Quests.accessNatureReserve, Quests.operator];
this.style = this.generateStyleFunction(); this.style = this.generateStyleFunction();
this.title = new NameInline("bos");
this.elementsToShow = [ this.elementsToShow = [
new TagMappingOptions({ new NameQuestion(),
key: "name", new AccessTag(),
template: "{name}", new OperatorTag()
missing: "Naamloos bos"
}),
CommonTagMappings.access,
CommonTagMappings.operator,
]; ];
} }
@ -47,9 +44,9 @@ export class Bos extends LayerDefinition {
const self = this; const self = this;
return function (properties: any) { return function (properties: any) {
let questionSeverity = 0; let questionSeverity = 0;
for (const qd of self.questions) { for (const qd of self.elementsToShow) {
if (qd.isApplicable(properties)) { if (qd.IsQuestioning(properties)) {
questionSeverity = Math.max(questionSeverity, qd.severity); questionSeverity = Math.max(questionSeverity, qd.options.priority ?? 0);
} }
} }
@ -63,7 +60,7 @@ export class Bos extends LayerDefinition {
let colour = colormapping[questionSeverity]; let colour = colormapping[questionSeverity];
while (colour == undefined) { while (colour == undefined) {
questionSeverity--; questionSeverity--;
colormapping[questionSeverity]; colour = colormapping[questionSeverity];
} }
return { return {

View file

@ -0,0 +1,88 @@
import {LayerDefinition} from "../LayerDefinition";
import L from "leaflet"
import {And, Regex, Tag} from "../../Logic/TagsFilter";
import {TagRenderingOptions} from "../TagRendering";
export class GrbToFix extends LayerDefinition {
constructor() {
super();
this.name = "grb";
this.newElementTags = undefined;
this.icon = "./assets/star.svg";
this.overpassFilter = new Regex("fixme", "GRB");
this.minzoom = 13;
this.style = function (tags) {
return {
icon: new L.icon({
iconUrl: "assets/star.svg",
iconSize: [40, 40],
text: "hi"
}),
color: "#ff0000"
};
}
this.title = new TagRenderingOptions({
freeform: {
key: "fixme",
renderTemplate: "{fixme}",
template: "Fixme $$$"
}
})
this.elementsToShow = [
new TagRenderingOptions(
{
freeform: {
key: "addr:street",
renderTemplate: "Het adres is {addr:street} <b>{addr:housenumber}</b>",
template: "Straat? $$$"
}
}
),
new TagRenderingOptions({
question: "Wat is het huisnummer?",
tagsPreprocessor: tags => {
const newTags = {};
newTags["addr:housenumber"] = tags["addr:housenumber"]
newTags["addr:street"] = tags["addr:street"]
const telltale = "GRB thinks that this has number ";
const index = tags.fixme.indexOf(telltale);
if (index >= 0) {
const housenumber = tags.fixme.slice(index + telltale.length);
newTags["grb:housenumber:human"] = housenumber;
newTags["grb:housenumber"] = housenumber == "no number" ? "" : housenumber;
}
return newTags;
},
mappings: [
{
k: new And([new Tag("addr:housenumber", "{grb:housenumber}"), new Tag("fixme", "")]),
txt: "Volg GRB: <b>{grb:housenumber:human}</b>",
substitute: true
},
{
k: new And([new Tag("addr:housenumber", "{addr:housenumber}"), new Tag("fixme", "")]),
txt: "Volg OSM: <b>{addr:housenumber}</b>",
substitute: true
}
]
})
];
}
}

View file

@ -1,8 +1,10 @@
import {LayerDefinition} from "../LayerDefinition"; import {LayerDefinition} from "../LayerDefinition";
import {Quests} from "../Quests"; import {Or, Tag} from "../../Logic/TagsFilter";
import {TagMappingOptions} from "../UI/TagMapping"; import {TagRenderingOptions} from "../TagRendering";
import {CommonTagMappings} from "./CommonTagMappings"; import {AccessTag} from "../Questions/AccessTag";
import {Or, Tag} from "../Logic/TagsFilter"; import {OperatorTag} from "../Questions/OperatorTag";
import {NameQuestion} from "../Questions/NameQuestion";
import {NameInline} from "../Questions/NameInline";
export class NatureReserves extends LayerDefinition { export class NatureReserves extends LayerDefinition {
@ -17,16 +19,12 @@ export class NatureReserves extends LayerDefinition {
this.newElementTags = [new Tag("leisure", "nature_reserve"), this.newElementTags = [new Tag("leisure", "nature_reserve"),
new Tag("fixme", "Toegevoegd met MapComplete, geometry nog uit te tekenen")] new Tag("fixme", "Toegevoegd met MapComplete, geometry nog uit te tekenen")]
this.minzoom = 13; this.minzoom = 13;
this.questions = [Quests.nameOf(this.name), Quests.accessNatureReserve, Quests.operator]; this.title = new NameInline("natuurreservaat");
this.style = this.generateStyleFunction(); this.style = this.generateStyleFunction();
this.elementsToShow = [ this.elementsToShow = [
new TagMappingOptions({ new NameQuestion(),
key: "name", new AccessTag(),
template: "{name}", new OperatorTag(),
missing: "Naamloos gebied"
}),
CommonTagMappings.access,
CommonTagMappings.operator,
]; ];
} }
@ -35,9 +33,9 @@ export class NatureReserves extends LayerDefinition {
const self = this; const self = this;
return function (properties: any) { return function (properties: any) {
let questionSeverity = 0; let questionSeverity = 0;
for (const qd of self.questions) { for (const qd of self.elementsToShow) {
if (qd.isApplicable(properties)) { if (qd.IsQuestioning(properties)) {
questionSeverity = Math.max(questionSeverity, qd.severity); questionSeverity = Math.max(questionSeverity, qd.options.priority ?? 0);
} }
} }
@ -51,7 +49,7 @@ export class NatureReserves extends LayerDefinition {
let colour = colormapping[questionSeverity]; let colour = colormapping[questionSeverity];
while (colour == undefined) { while (colour == undefined) {
questionSeverity--; questionSeverity--;
colormapping[questionSeverity]; colour = colormapping[questionSeverity];
} }
return { return {

View file

@ -1,8 +1,11 @@
import {LayerDefinition} from "../LayerDefinition"; import {LayerDefinition} from "../LayerDefinition";
import {Quests} from "../Quests"; import {Quests} from "../../Quests";
import {TagMappingOptions} from "../UI/TagMapping"; import {And, Or, Tag} from "../../Logic/TagsFilter";
import {CommonTagMappings} from "./CommonTagMappings"; import {AccessTag} from "../Questions/AccessTag";
import {Or, Tag} from "../Logic/TagsFilter"; import {OperatorTag} from "../Questions/OperatorTag";
import {TagRenderingOptions} from "../TagRendering";
import {NameQuestion} from "../Questions/NameQuestion";
import {NameInline} from "../Questions/NameInline";
export class Park extends LayerDefinition { export class Park extends LayerDefinition {
@ -10,25 +13,16 @@ export class Park extends LayerDefinition {
super(); super();
this.name = "park"; this.name = "park";
this.icon = "./assets/tree_white_background.svg"; this.icon = "./assets/tree_white_background.svg";
this.overpassFilter = this.overpassFilter =
new Or([new Tag("leisure","park"), new Tag("landuse","village_green")]); new Or([new Tag("leisure", "park"), new Tag("landuse", "village_green")]);
this.newElementTags = [new Tag("leisure", "park"), this.newElementTags = [new Tag("leisure", "park"),
new Tag("fixme", "Toegevoegd met MapComplete, geometry nog uit te tekenen")]; new Tag("fixme", "Toegevoegd met MapComplete, geometry nog uit te tekenen")];
this.maxAllowedOverlapPercentage = 25; this.maxAllowedOverlapPercentage = 25;
this.minzoom = 13; this.minzoom = 13;
this.questions = [Quests.nameOf("park")];
this.style = this.generateStyleFunction(); this.style = this.generateStyleFunction();
this.elementsToShow = [ this.title = new NameInline("park");
new TagMappingOptions({ this.elementsToShow = [new NameQuestion()];
key: "name",
template: "{name}",
missing: "Naamloos park"
}),
CommonTagMappings.access,
CommonTagMappings.operator,
];
} }
@ -39,9 +33,9 @@ export class Park extends LayerDefinition {
const self = this; const self = this;
return function (properties: any) { return function (properties: any) {
let questionSeverity = 0; let questionSeverity = 0;
for (const qd of self.questions) { for (const qd of self.elementsToShow) {
if (qd.isApplicable(properties)) { if (qd.IsQuestioning(properties)) {
questionSeverity = Math.max(questionSeverity, qd.severity); questionSeverity = Math.max(questionSeverity, qd.options.priority ?? 0);
} }
} }
@ -55,7 +49,7 @@ export class Park extends LayerDefinition {
let colour = colormapping[questionSeverity]; let colour = colormapping[questionSeverity];
while (colour == undefined) { while (colour == undefined) {
questionSeverity--; questionSeverity--;
colormapping[questionSeverity]; colour = colormapping[questionSeverity];
} }
return { return {

View file

@ -1,9 +1,8 @@
import {LayerDefinition} from "../LayerDefinition"; import {LayerDefinition} from "../LayerDefinition";
import {Quests} from "../Quests"; import {Quests} from "../../Quests";
import {FixedUiElement} from "../UI/Base/FixedUiElement"; import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import {TagMappingOptions} from "../UI/TagMapping";
import L from "leaflet"; import L from "leaflet";
import {Tag} from "../Logic/TagsFilter"; import {Tag} from "../../Logic/TagsFilter";
export class Toilets extends LayerDefinition{ export class Toilets extends LayerDefinition{

53
Customizations/Layout.ts Normal file
View file

@ -0,0 +1,53 @@
import {LayerDefinition} from "./LayerDefinition";
/**
* A layout is a collection of settings of the global view (thus: welcome text, title, selection of layers).
*/
export class Layout {
public name: string;
public title: string;
public layers: LayerDefinition[];
public welcomeMessage: string;
public gettingStartedPlzLogin: string;
public welcomeBackMessage: string;
public startzoom: number;
public startLon: number;
public startLat: number;
public welcomeTail: string;
constructor(
name: string,
title: string,
layers: LayerDefinition[],
startzoom: number,
startLat: number,
startLon: number,
welcomeMessage: string,
gettingStartedPlzLogin: string,
welcomeBackMessage: string,
welcomeTail: string = ""
) {
this.title = title;
this.startLon = startLon;
this.startLat = startLat;
this.startzoom = startzoom;
this.name = name;
this.layers = layers;
this.welcomeMessage = welcomeMessage;
this.gettingStartedPlzLogin = gettingStartedPlzLogin;
this.welcomeBackMessage = welcomeBackMessage;
this.welcomeTail = welcomeTail;
}
/*
static statues = new Layout(
);
*/
}

View file

@ -0,0 +1,27 @@
import {Layout} from "../Layout";
import * as Layer from "../Layers/Bookcases";
export class Bookcases extends Layout{
constructor() {
super( "bookcases",
"Open Bookcase Map",
[new Layer.Bookcases()],
14,
51.2,
3.2,
" <h3>Open BoekenkastjesKaart</h3>\n" +
"\n" +
"<p>" +
"Help mee met het creëeren van een volledige kaart met alle boekenruilkastjes!" +
"Een boekenruilkastje is een vaste plaats in publieke ruimte waar iedereen een boek in kan zetten of uit kan meenemen." +
"Meestal een klein kastje of doosje dat op straat staat, maar ook een oude telefooncellen of een schap in een station valt hieronder."+
"</p>"
,
" <p>Begin met <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">het aanmaken van een account\n" +
" </a> of door je " +
" <span onclick=\"authOsm()\" class=\"activate-osm-authentication\">aan te melden</span>.</p>",
"Klik op een boekenruilkastje om vragen te beantwoorden");
}
}

View file

@ -0,0 +1,21 @@
import {Layout} from "../Layout";
import {GrbToFix} from "../Layers/GrbToFix";
export class GRB extends Layout {
constructor() {
super("grb",
"Grb import fix tool",
[new GrbToFix()],
15,
51.2083,
3.2279,
"<h3>GRB Fix tool</h3>\n" +
"\n" +
"Expert use only"
,
"", "");
}
}

View file

@ -0,0 +1,50 @@
import {NatureReserves} from "../Layers/NatureReserves";
import {Park} from "../Layers/Park";
import {Bos} from "../Layers/Bos";
import {Layout} from "../Layout";
export class Groen extends Layout {
constructor() {
super("groen",
"Buurtnatuur",
[new NatureReserves(), new Park(), new Bos()],
10,
50.8435,
4.3688,
"\n" +
"<img src='assets/groen.svg' alt='logo-groen' class='logo'> <br />" +
"<h3>Breng jouw buurtnatuur in kaart</h3>" +
"<b>Natuur maakt gelukkig.</b> Aan de hand van deze website willen we de natuur dicht bij ons beter inventariseren. Met als doel meer mensen te laten genieten van toegankelijke natuur én te strijden voor meer natuur in onze buurten. \n" +
"<ul>" +
"<li>In welke natuurgebieden kan jij terecht? Hoe toegankelijk zijn ze?</li>" +
"<li>In welke bossen kan een gezin in jouw gemeente opnieuw op adem komen?</li>" +
"<li>Op welke onbekende plekjes is het zalig spelen?</li>" +
"</ul>" +
"<p>Samen kleuren we heel Vlaanderen en Brussel groen.</p>" +
"<p>Blijf op de hoogte van de resultaten van buurtnatuur.be: <a href=\"https://www.groen.be/buurtnatuur\" target='_blank'>meld je aan voor e-mailupdates</a>.</p> \n"
,
"<b>Begin meteen door <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">een account te maken\n" +
" te maken</a> of\n" +
" <span onclick=\"authOsm()\" class=\"activate-osm-authentication\">in te loggen</span>.</b>",
"",
"<h4>Tips</h4>" +
"<ul>" +
"<li>Over groen ingekleurde gebieden weten we alles wat we willen weten.</li>" +
"<li>Bij rood ingekleurde gebieden ontbreekt nog heel wat info: klik een gebied aan en beantwoord de vragen.</li>" +
"<li>Je kan altijd een foto toevoegen</li>" +
"<li>Je kan ook zelf een gebied toevoegen door op de kaart te klikken</li>" +
"</ul>" +
"<small>" +
"<p>" +
"De oorspronkelijke data komt van <b>OpenStreetMap</b> en je antwoorden worden daar bewaard.<br/> Omdat iedereen vrij kan meewerken aan dit project, kunnen we niet garanderen dat er geen fouten opduiken." +
"</p>" +
"Je privacy is belangrijk. We tellen wel hoeveel gebruikers deze website bezoeken. We plaatsen een cookie waar geen persoonlijke informatie in bewaard wordt. " +
"Als je inlogt, komt er een tweede cookie bij met je inloggegevens." +
"</small>"
);
}
}

View file

@ -0,0 +1,26 @@
import {Layout} from "../Layout";
import {Artwork} from "../Layers/Artwork";
export class Statues extends Layout{
constructor() {
super( "statues",
"Open Artwork Map",
[new Artwork()],
10,
50.8435,
4.3688,
" <h3>Open Statue Map</h3>\n" +
"\n" +
"<p>" +
"Help with creating a map of all statues all over the world!"
,
" <p>Start by <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">creating an account\n" +
" </a> or by " +
" <span onclick=\"authOsm()\" class=\"activate-osm-authentication\">logging in</span>.</p>",
"Start by clicking a pin and answering the questions");
}
}

View file

@ -0,0 +1,24 @@
import {Layout} from "../Layout";
import * as Layer from "../Layers/Toilets";
export class Toilets extends Layout{
constructor() {
super( "toilets",
"Open Toilet Map",
[new Layer.Toilets()],
12,
51.2,
3.2,
" <h3>Open Toilet Map</h3>\n" +
"\n" +
"<p>Help us to create the most complete map about <i>all</i> the toilets in the world, based on openStreetMap." +
"One can answer questions here, which help users all over the world to find an accessible toilet, close to them.</p>"
,
" <p>Start by <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">creating an account\n" +
" </a> or by " +
" <span onclick=\"authOsm()\" class=\"activate-osm-authentication\">logging in</span>.</p>",
"Start by clicking a pin and answering the questions");
}
}

View file

@ -0,0 +1,39 @@
import {TagRendering, TagRenderingOptions} from "../TagRendering";
import {UIEventSource} from "../../UI/UIEventSource";
import {Changes} from "../../Logic/Changes";
import {And, Tag} from "../../Logic/TagsFilter";
export class AccessTag extends TagRenderingOptions {
private static options = {
priority: 10,
question: "Is dit gebied toegankelijk?",
primer: "Dit gebied is ",
freeform: {
key: "access",
extraTags: new Tag("fixme", "Freeform access tag used: possibly a wrong value"),
template: "Iets anders: $$$",
renderTemplate: "De toegangekelijkheid van dit gebied is: {access}",
placeholder: "Specifieer"
},
mappings: [
{k: new And([new Tag("access", "yes"), new Tag("fee", "")]), txt: "publiek toegankelijk"},
{k: new And([new Tag("access", "no"), new Tag("fee", "")]), txt: "niet toegankelijk"},
{k: new And([new Tag("access", "private"), new Tag("fee", "")]), txt: "niet toegankelijk, want privegebied"},
{k: new And([new Tag("access", "permissive"), new Tag("fee", "")]), txt: "toegankelijk, maar het is privegebied"},
{k: new And([new Tag("access", "guided"), new Tag("fee", "")]), txt: "enkel met gids of op activiteit"},
{
k: new And([new Tag("access", "yes"),
new Tag("fee", "yes")]),
txt: "toegankelijk mits betaling",
priority: 10
},
]
}
constructor() {
super(AccessTag.options);
}
}

View file

@ -0,0 +1,29 @@
import {TagRenderingOptions} from "../TagRendering";
import {And, Tag} from "../../Logic/TagsFilter";
export class NameInline extends TagRenderingOptions{
static Upper(string){
return string.charAt(0).toUpperCase() + string.slice(1);
}
constructor(category: string) {
super({
question: "",
freeform: {
renderTemplate: "{name}",
template: "De naam van dit "+category+" is $$$",
key: "name",
extraTags: new Tag("noname", "") // Remove 'noname=yes'
},
mappings: [
{k: new Tag("noname","yes"), txt: NameInline.Upper(category)+" zonder naam"},
{k: null, txt: NameInline.Upper(category)}
]
});
}
}

View file

@ -0,0 +1,30 @@
/**
* There are two ways to ask for names:
* One is a big 'name-question', the other is the 'edit name' in the title.
* THis one is the big question
*/
import {TagRenderingOptions} from "../TagRendering";
import {Tag} from "../../Logic/TagsFilter";
export class NameQuestion extends TagRenderingOptions{
static options = {
priority: 20,
question: "Wat is de <i>officiële</i> naam van dit gebied?",
freeform: {
key: "name",
template: "De naam is $$$",
renderTemplate: "", // We don't actually render it, only ask
placeholder: "",
extraTags: new Tag("noname","")
},
mappings: [
{k: new Tag("noname", "yes"), txt: "Dit gebied heeft geen naam"},
]
}
constructor() {
super(NameQuestion.options);
}
}

View file

@ -0,0 +1,30 @@
import {TagRenderingOptions} from "../TagRendering";
import {UIEventSource} from "../../UI/UIEventSource";
import {Changes} from "../../Logic/Changes";
import {Tag} from "../../Logic/TagsFilter";
export class OperatorTag extends TagRenderingOptions {
private static options = {
priority: 5,
question: "Wie beheert dit gebied?",
freeform: {
key: "operator",
template: "Dit gebied wordt beheerd door $$$",
renderTemplate: "Dit gebied wordt beheerd door {operator}",
placeholder: "organisatie"
},
mappings: [
{k: new Tag("operator", "Natuurpunt"), txt: "Natuurpunt"},
{k: new Tag("operator", "Agentschap Natuur en Bos"), txt: "het Agentschap Natuur en Bos (ANB)"},
{k: new Tag("operator", "private"), txt: "Beheer door een privépersoon"}
]
}
constructor() {
super(OperatorTag.options);
}
}

View file

@ -0,0 +1,31 @@
import {TagRenderingOptions} from "../TagRendering";
import {Img} from "../../UI/Img";
import {Tag} from "../../Logic/TagsFilter";
export class OsmLink extends TagRenderingOptions {
static options = {
freeform: {
key: "id",
template: "$$$",
renderTemplate:
"<span class='osmlink'><a href='https://osm.org/{id}' target='_blank'>" +
Img.osmAbstractLogo +
"</a></span>",
placeholder: "",
},
mappings: [
{k: new Tag("id", "node/-1"), txt: "<span class='alert'>Uploading</span>"}
]
}
constructor() {
super(OsmLink.options);
}
}

View file

@ -0,0 +1,53 @@
import {TagRenderingOptions} from "../TagRendering";
export class WikipediaLink extends TagRenderingOptions {
private static FixLink(value: string): string {
// @ts-ignore
if (value.startsWith("https")) {
return value;
} else {
const splitted = value.split(":");
const language = splitted[0];
splitted.shift();
const page = splitted.join(":");
return 'https://' + language + '.wikipedia.org/wiki/' + page;
}
}
static options = {
priority: 10,
// question: "Wat is het overeenstemmende wkipedia-artikel?",
freeform: {
key: "wikipedia",
template: "$$$",
renderTemplate:
"<span class='wikipedialink'>" +
"<a href='{wikipedia}' target='_blank'>" +
"<img width='64px' src='./assets/wikipedia.svg' alt='wikipedia'>" +
"</a></span>",
placeholder: "",
tagsPreprocessor: (tags) => {
const newTags = {};
for (const k in tags) {
if (k === "wikipedia") {
newTags["wikipedia"] = WikipediaLink.FixLink(tags[k]);
} else {
newTags[k] = tags[k];
}
}
return newTags;
}
},
}
constructor() {
super(WikipediaLink.options);
}
}

View file

@ -0,0 +1,358 @@
import {UIElement} from "../UI/UIElement";
import {UIEventSource} from "../UI/UIEventSource";
import {And, Tag, TagsFilter, TagUtils} from "../Logic/TagsFilter";
import {UIRadioButton} from "../UI/Base/UIRadioButton";
import {FixedUiElement} from "../UI/Base/FixedUiElement";
import {SaveButton} from "../UI/SaveButton";
import {Changes} from "../Logic/Changes";
import {TextField} from "../UI/Base/TextField";
import {UIInputElement} from "../UI/Base/UIInputElement";
import {UIRadioButtonWithOther} from "../UI/Base/UIRadioButtonWithOther";
import {VariableUiElement} from "../UI/Base/VariableUIElement";
export class TagRenderingOptions {
/**
* Notes: by not giving a 'question', one disables the question form alltogether
*/
public options: {
priority?: number; question?: string; primer?: string;
freeform?: { key: string; tagsPreprocessor?: (tags: any) => any; template: string; renderTemplate: string; placeholder?: string; extraTags?: TagsFilter }; mappings?: { k: TagsFilter; txt: string; priority?: number, substitute?: boolean }[]
};
constructor(options: {
priority?: number
question?: string,
primer?: string,
tagsPreprocessor?: ((tags: any) => any),
freeform?: {
key: string, template: string,
renderTemplate: string
placeholder?: string,
extraTags?: TagsFilter,
},
mappings?: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[]
}) {
this.options = options;
}
IsQuestioning(tags: any): boolean {
const tagsKV = TagUtils.proprtiesToKV(tags);
for (const oneOnOneElement of this.options.mappings) {
if (oneOnOneElement.k.matches(tagsKV)) {
return false;
}
}
if (this.options.freeform !== undefined && tags[this.options.freeform.key] !== undefined) {
return false;
}
if (this.options.question === undefined) {
return false;
}
return true;
}
}
export class TagRendering extends UIElement {
public elementPriority: number;
private _question: string;
private _primer: string;
private _mapping: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[];
private _tagsPreprocessor?: ((tags: any) => any);
private _freeform: {
key: string, template: string,
renderTemplate: string,
placeholder?: string,
extraTags?: TagsFilter
};
private readonly _questionElement: UIElement;
private readonly _textField: TextField<TagsFilter>; // Only here to update
private readonly _saveButton: UIElement;
private readonly _skipButton: UIElement;
private readonly _editButton: UIElement;
private readonly _questionSkipped: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _editMode: UIEventSource<boolean> = new UIEventSource<boolean>(false);
constructor(tags: UIEventSource<any>, changes: Changes, options: {
priority?: number
question?: string,
primer?: string,
freeform?: {
key: string, template: string,
renderTemplate: string
placeholder?: string,
extraTags?: TagsFilter,
},
tagsPreprocessor?: ((tags: any) => any),
mappings?: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[]
}) {
super(tags);
const self = this;
this.ListenTo(this._questionSkipped);
this.ListenTo(this._editMode);
this._question = options.question;
this._primer = options.primer ?? "";
this._tagsPreprocessor = options.tagsPreprocessor;
this._mapping = [];
this._freeform = options.freeform;
this.elementPriority = options.priority ?? 0;
// Prepare the choices for the Radio buttons
let i = 0;
const choices: UIElement[] = [];
for (const choice of options.mappings ?? []) {
if (choice.k === null) {
this._mapping.push(choice);
continue;
}
let choiceSubbed = choice;
if (choice.substitute) {
choiceSubbed = {
k : choice.k.substituteValues(
options.tagsPreprocessor(this._source.data)),
txt : this.ApplyTemplate(choice.txt),
substitute: false,
priority: choice.priority
}
}
choices.push(new FixedUiElement(choiceSubbed.txt));
this._mapping.push(choiceSubbed);
i++;
}
// Map radiobutton choice and textfield answer onto tagfilter. That tagfilter will be pushed into the changes later on
const pickChoice = (i => {
if (i === undefined || i === null) {
return undefined
}
return self._mapping[i].k
});
const pickString =
(string) => {
if (string === "" || string === undefined) {
return undefined;
}
const tag = new Tag(self._freeform.key, string);
if (self._freeform.extraTags === undefined) {
return tag;
}
return new And([
self._freeform.extraTags,
tag
]
);
};
// Prepare the actual input element -> pick an appropriate implementation
let inputElement: UIInputElement<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 !== undefined) {
// This is a classic radio selection element
inputElement = new UIRadioButton(new UIEventSource(choices), pickChoice)
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(this._freeform.template.replace("$$$", inputElement.Render()))
} else {
throw "Invalid questionRendering, expected at least choices or a freeform"
}
const save = () => {
const selection = inputElement.GetValue().data;
if (selection) {
changes.addTag(tags.data.id, selection);
}
self._editMode.setData(false);
}
const cancel = () => {
self._questionSkipped.setData(true);
self._editMode.setData(false);
}
// Setup the save button and it's action
this._saveButton = new SaveButton(inputElement.GetValue())
.onClick(save);
if (this._question !== undefined) {
this._editButton = new FixedUiElement("<img class='editbutton' src='./assets/pencil.svg' alt='edit'>")
.onClick(() => {
console.log("Click", self._editButton);
if (self._textField) {
self._textField.value.setData(self._source.data["name"] ?? "");
}
self._editMode.setData(true);
});
} else {
this._editButton = new FixedUiElement("");
}
const cancelContents = this._editMode.map((isEditing) => {
if (isEditing) {
return "<span class='skip-button'>Annuleren</span>";
} else {
return "<span class='skip-button'>Ik weet het niet zeker...</span>";
}
});
// And at last, set up the skip button
this._skipButton = new VariableUiElement(cancelContents).onClick(cancel);
}
private ApplyTemplate(template: string): string {
let tags = this._source.data;
if (this._tagsPreprocessor !== undefined) {
tags = this._tagsPreprocessor(tags);
}
return TagUtils.ApplyTemplate(template, tags);
}
IsKnown(): boolean {
const tags = TagUtils.proprtiesToKV(this._source.data);
for (const oneOnOneElement of this._mapping) {
if (oneOnOneElement.k === null || oneOnOneElement.k.matches(tags)) {
return true;
}
}
return this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined;
}
IsQuestioning(): boolean {
if (this.IsKnown()) {
return false;
}
if (this._question === undefined) {
// We don't ask this question in the first place
return false;
}
if (this._questionSkipped.data) {
// We don't ask for this question anymore, skipped by user
return false;
}
return true;
}
private RenderAnwser(): string {
const tags = TagUtils.proprtiesToKV(this._source.data);
let freeform = "";
let freeformScore = -10;
if (this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined) {
freeform = this.ApplyTemplate(this._freeform.renderTemplate);
freeformScore = 0;
}
if (this._mapping !== undefined) {
let highestScore = -100;
let highestTemplate = undefined;
for (const oneOnOneElement of this._mapping) {
if (oneOnOneElement.k == null ||
oneOnOneElement.k.matches(tags)) {
// We have found a matching key -> we use the template, but only if it scores better
let score = oneOnOneElement.priority ??
(oneOnOneElement.k === null ? -1 : 0);
if (score > highestScore) {
highestScore = score;
highestTemplate = oneOnOneElement.txt
}
}
}
if (freeformScore > highestScore) {
return freeform;
}
if (highestTemplate !== undefined) {
// we render the found template
return this._primer + this.ApplyTemplate(highestTemplate);
}
} else {
return freeform;
}
}
protected InnerRender(): string {
if (this.IsQuestioning() || this._editMode.data) {
// Not yet known or questioning, we have to ask a question
return "<div class='question'>" +
this._question +
(this._question !== "" ? "<br/>" : "") +
this._questionElement.Render() +
this._skipButton.Render() +
this._saveButton.Render() +
"</div>"
}
if (this.IsKnown()) {
const html = this.RenderAnwser();
if (html == "") {
return "";
}
return "<span class='answer'>" +
"<span class='answer-text'>" + html + "</span>" + this._editButton.Render() +
"</span>";
}
return "";
}
InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
this._questionElement.Update();
this._saveButton.Update();
this._skipButton.Update();
this._textField?.Update();
this._editButton.Update();
}
}

View file

@ -1,60 +0,0 @@
import {TagMappingOptions} from "../UI/TagMapping";
import {Img} from "../UI/Img";
export class CommonTagMappings {
public static access = new TagMappingOptions({
key: "access",
mapping: {
yes: "Vrij toegankelijk (op de paden)",
no: "Niet toegankelijk",
private: "Niet toegankelijk, want privegebied",
permissive: "Toegankelijk, maar het is privegebied",
guided: "Enkel met gids of op activiteit"
}
});
public static operator = new TagMappingOptions({
key: "operator",
template: "Beheer door {operator}",
mapping: {
private: 'Beheer door een privepersoon of organisatie'
}
});
public static osmLink = new TagMappingOptions({
key: "id",
mapping: {
"node/-1": ""
},
template: "<span class='osmlink'><a href='https://osm.org/{id}' target='_blank'>" +
Img.osmAbstractLogo +
"</a></span>"
});
public static wikipediaLink = new TagMappingOptions({
key: "wikipedia",
missing: "",
freeform: (value: string) => {
let link = "";
// @ts-ignore
if (value.startsWith("https")) {
link = value;
} else {
const splitted = value.split(":");
const language = splitted[0];
splitted.shift();
const page = splitted.join(":");
link = 'https://' + language + '.wikipedia.org/wiki/' + page;
}
return "<span class='wikipedialink'>" +
"<a href='" + link + "' target='_blank'>" +
"<img width='64px' src='./assets/wikipedia.svg' alt='wikipedia'" +
"</a></span>";
}
});
}

View file

@ -1,58 +0,0 @@
import {LayerDefinition} from "../LayerDefinition";
import {QuestionDefinition} from "../Logic/Question";
import {TagMappingOptions} from "../UI/TagMapping";
import {CommonTagMappings} from "./CommonTagMappings";
import L from "leaflet"
import {Regex} from "../Logic/TagsFilter";
export class GrbToFix extends LayerDefinition {
constructor() {
super();
this.name = "grb";
this.newElementTags = undefined;
this.icon = "./assets/star.svg";
this.overpassFilter = new Regex("fixme","GRB");
this.minzoom = 13;
this.questions = [
QuestionDefinition.GrbNoNumberQuestion(),
QuestionDefinition.GrbHouseNumberQuestion()
];
this.style = function (tags) {
return {
icon: new L.icon({
iconUrl: "assets/star.svg",
iconSize: [40, 40],
text: "hi"
})
};
}
this.elementsToShow = [
new TagMappingOptions(
{
key: "fixme",
template: "<h2>Fixme</h2>{fixme}",
}),
new TagMappingOptions({
key: "addr:street",
template: "Straat: <b>{addr:street}</b>",
missing: "<b>Geen straat bekend</b>"
}),
new TagMappingOptions({
key: "addr:housenumber",
template: "Nummer: <b>{addr:housenumber}</b>",
missing: "<b>Geen huisnummer bekend</b>"
}),
CommonTagMappings.osmLink
];
}
}

View file

@ -1,175 +0,0 @@
import {LayerDefinition} from "../LayerDefinition";
import {NatureReserves} from "./NatureReserves";
import {Toilets} from "./Toilets";
import {Bos} from "./Bos";
import {Park} from "./Park";
import {Playground} from "./Playground";
import {Bookcases} from "./Bookcases";
import {Artwork} from "./Artwork";
import {GrbToFix} from "./GrbToFix";
export class KnownSet {
public name: string;
public title: string;
public layers: LayerDefinition[];
public welcomeMessage: string;
public gettingStartedPlzLogin: string;
public welcomeBackMessage: string;
public startzoom: number;
public startLon: number;
public startLat: number;
static allSets : any = {};
public welcomeTail: string;
constructor(
name: string,
title: string,
layers: LayerDefinition[],
startzoom: number,
startLat: number,
startLon: number,
welcomeMessage: string,
gettingStartedPlzLogin: string,
welcomeBackMessage: string,
welcomeTail: string = ""
) {
this.title = title;
this.startLon = startLon;
this.startLat = startLat;
this.startzoom = startzoom;
this.name = name;
this.layers = layers;
this.welcomeMessage = welcomeMessage;
this.gettingStartedPlzLogin = gettingStartedPlzLogin;
this.welcomeBackMessage = welcomeBackMessage;
this.welcomeTail = welcomeTail;
KnownSet.allSets[this.name] = this;
}
static groen = new KnownSet("groen",
"Buurtnatuur",
[new NatureReserves(), new Park(), new Bos()],
10,
50.8435,
4.3688,
"\n" +
"<img src='assets/groen.svg' alt='logo-groen' class='logo'> <br />" +
"<h3>Breng jouw buurtnatuur in kaart</h3>" +
"<b>Natuur maakt gelukkig.</b> Aan de hand van deze website willen we de natuur dicht bij ons beter inventariseren. Met als doel meer mensen te laten genieten van toegankelijke natuur én te strijden voor meer natuur in onze buurten. \n" +
"<ul>" +
"<li>In welke natuurgebieden kan jij terecht? Hoe toegankelijk zijn ze?</li>" +
"<li>In welke bossen kan een gezin in jouw gemeente opnieuw op adem komen?</li>" +
"<li>Op welke onbekende plekjes is het zalig spelen?</li>" +
"</ul>" +
"<p>Samen kleuren we heel Vlaanderen en Brussel groen.</p>" +
"<p>Blijf op de hoogte van de resultaten van buurtnatuur.be: <a href=\"https://www.groen.be/buurtnatuur\" target='_blank'>meld je aan voor e-mailupdates</a>.</p> \n"
,
"<b>Begin meteen door <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">een account te maken\n" +
" te maken</a> of\n" +
" <span onclick=\"authOsm()\" class=\"activate-osm-authentication\">in te loggen</span>.</b>",
"",
"<h4>Tips</h4>" +
"<ul>" +
"<li>Over groen ingekleurde gebieden weten we alles wat we willen weten.</li>" +
"<li>Bij rood ingekleurde gebieden ontbreekt nog heel wat info: klik een gebied aan en beantwoord de vragen.</li>" +
"<li>Je kan altijd een foto toevoegen</li>" +
"<li>Je kan ook zelf een gebied toevoegen door op de kaart te klikken</li>" +
"</ul>" +
"<small>" +
"<p>" +
"De oorspronkelijke data komt van <b>OpenStreetMap</b> en je antwoorden worden daar bewaard.<br/> Omdat iedereen vrij kan meewerken aan dit project, kunnen we niet garanderen dat er geen fouten opduiken." +
"</p>" +
"Je privacy is belangrijk. We tellen wel hoeveel gebruikers deze website bezoeken. We plaatsen een cookie waar geen persoonlijke informatie in bewaard wordt. " +
"Als je inlogt, komt er een tweede cookie bij met je inloggegevens." +
"</small>"
);
static openToiletMap = new KnownSet(
"toilets",
"Open Toilet Map",
[new Toilets()],
12,
51.2,
3.2,
" <h3>Open Toilet Map</h3>\n" +
"\n" +
"<p>Help us to create the most complete map about <i>all</i> the toilets in the world, based on openStreetMap." +
"One can answer questions here, which help users all over the world to find an accessible toilet, close to them.</p>"
,
" <p>Start by <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">creating an account\n" +
" </a> or by " +
" <span onclick=\"authOsm()\" class=\"activate-osm-authentication\">logging in</span>.</p>",
"Start by clicking a pin and answering the questions"
);
static bookcases = new KnownSet(
"bookcases",
"Open Bookcase Map",
[new Bookcases()],
14,
51.2,
3.2,
" <h3>Open BoekenkastjesKaart</h3>\n" +
"\n" +
"<p>" +
"Help mee met het creëeren van een volledige kaart met alle boekenruilkastjes!" +
"Een boekenruilkastje is een vaste plaats in publieke ruimte waar iedereen een boek in kan zetten of uit kan meenemen." +
"Meestal een klein kastje of doosje dat op straat staat, maar ook een oude telefooncellen of een schap in een station valt hieronder."+
"</p>"
,
" <p>Begin met <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">het aanmaken van een account\n" +
" </a> of door je " +
" <span onclick=\"authOsm()\" class=\"activate-osm-authentication\">aan te melden</span>.</p>",
"Klik op een boekenruilkastje om vragen te beantwoorden"
);
static statues = new KnownSet(
"statues",
"Open Artwork Map",
[new Artwork()],
10,
50.8435,
4.3688,
" <h3>Open Statue Map</h3>\n" +
"\n" +
"<p>" +
"Help with creating a map of all statues all over the world!"
,
" <p>Start by <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">creating an account\n" +
" </a> or by " +
" <span onclick=\"authOsm()\" class=\"activate-osm-authentication\">logging in</span>.</p>",
"Start by clicking a pin and answering the questions"
);
static grb = new KnownSet(
"grb",
"Grb import fix tool",
[new GrbToFix()],
10,
50.8435,
4.3688,
"<h3>GRB Fix tool</h3>\n" +
"\n" +
"Expert use only"
,
"",""
);
}

View file

@ -1,72 +0,0 @@
import {LayerDefinition} from "../LayerDefinition";
import {Quests} from "../Quests";
import {TagMappingOptions} from "../UI/TagMapping";
import L from "leaflet"
import {CommonTagMappings} from "./CommonTagMappings";
import {Tag} from "../Logic/TagsFilter";
export class Playground extends LayerDefinition {
constructor() {
super();
this.name = "speeltuin";
this.icon = "./assets/tree_white_background.svg";
this.overpassFilter = new Tag("leisure","playground");
this.newElementTags = [new Tag("leisure", "playground"), new Tag( "fixme", "Toegevoegd met MapComplete, geometry nog uit te tekenen")]
this.maxAllowedOverlapPercentage = 0;
this.minzoom = 13;
this.questions = [Quests.nameOf(this.name)];
this.style = this.generateStyleFunction();
this.elementsToShow = [
new TagMappingOptions({
key: "name",
template: "<h2>{name}</h2>",
missing: "<h2>Naamloos park</h2>"
}),
CommonTagMappings.access,
CommonTagMappings.operator,
CommonTagMappings.osmLink
];
}
private readonly treeIcon = new L.icon({
iconUrl: "assets/tree_white_background.svg",
iconSize: [40, 40]
})
private generateStyleFunction() {
const self = this;
return function (properties: any) {
let questionSeverity = 0;
for (const qd of self.questions) {
if (qd.isApplicable(properties)) {
questionSeverity = Math.max(questionSeverity, qd.severity);
}
}
let colormapping = {
0: "#00bb00",
1: "#00ff00",
10: "#dddd00",
20: "#ff0000"
};
let colour = colormapping[questionSeverity];
while (colour == undefined) {
questionSeverity--;
colormapping[questionSeverity];
}
return {
color: colour,
icon: self.treeIcon
};
};
}
}

View file

@ -51,11 +51,10 @@ export class Basemap {
private baseLayers = { private baseLayers = {
"OpenStreetMap Be": this.osmBeLayer, "Luchtfoto Vlaanderen (recentste door AIV)": this.aivLuchtLatestLayer,
"OpenStreetMap": this.osmLayer, "Luchtfoto Vlaanderen (2013-2015, door AIV)": this.aivLucht2013Layer,
"Luchtfoto AIV Vlaanderen (2013-2015)": this.aivLucht2013Layer, "Kaart van OpenStreetMap": this.osmLayer,
"Luchtfoto AIV Vlaanderen (laatste)": this.aivLuchtLatestLayer, "Kaart Grootschalig ReferentieBestand Vlaanderen (GRB) door AIV": this.grbLayer
"GRB Vlaanderen": this.grbLayer
}; };
constructor(leafletElementId: string, constructor(leafletElementId: string,

View file

@ -7,7 +7,7 @@ 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 {Question, QuestionDefinition} from "./Question";
import {Tag} from "./TagsFilter"; import {And, Tag, TagsFilter} from "./TagsFilter";
export class Changes { export class Changes {
@ -22,19 +22,34 @@ export class Changes {
public readonly pendingChangesES = new UIEventSource<number>(this._pendingChanges.length); public readonly pendingChangesES = new UIEventSource<number>(this._pendingChanges.length);
public readonly isSaving = new UIEventSource(false); public readonly isSaving = new UIEventSource(false);
private readonly _changesetComment: string; private readonly _changesetComment: string;
private readonly _centerMessage: UIEventSource<string>;
constructor( constructor(
changesetComment: string, changesetComment: string,
login: OsmConnection, login: OsmConnection,
allElements: ElementStorage, allElements: ElementStorage) {
centerMessage: UIEventSource<string>) {
this._changesetComment = changesetComment; this._changesetComment = changesetComment;
this.login = login; this.login = login;
this._allElements = allElements; this._allElements = allElements;
this._centerMessage = centerMessage;
} }
addTag(elementId: string, tagsFilter : TagsFilter){
if(tagsFilter instanceof Tag){
const tag = tagsFilter as Tag;
this.addChange(elementId, tag.key, tag.value);
return;
}
if(tagsFilter instanceof And){
const and = tagsFilter as And;
for (const tag of and.and) {
this.addTag(elementId, tag);
}
return;
}
console.log("Unsupported tagsfilter element to addTag", tagsFilter);
throw "Unsupported tagsFilter element";
}
/** /**
* Adds a change to the pending changes * Adds a change to the pending changes
* @param elementId * @param elementId
@ -42,23 +57,7 @@ export class Changes {
* @param value * @param value
*/ */
addChange(elementId: string, key: string, value: string) { addChange(elementId: string, key: string, value: string) {
console.log("Received change",key, value)
if (!this.login.userDetails.data.loggedIn) {
this._centerMessage.setData(
"<p>Bedankt voor je antwoord!</p>" +
"<p>Gelieve <span class='activate-osm-authentication'>in te loggen op OpenStreetMap</span> om dit op te slaan.</p>" +
"<p>Nog geen account? <a href=\'https://www.openstreetmap.org/user/new\' target=\'_blank\'>Registreer hier</a></p>"
);
const self = this;
this.login.userDetails.addCallback(() => {
if (self.login.userDetails.data.loggedIn) {
self._centerMessage.setData("");
}
});
return;
}
if (key === undefined || key === null) { if (key === undefined || key === null) {
console.log("Invalid key"); console.log("Invalid key");
return; return;

View file

@ -106,7 +106,6 @@ export class OsmConnection {
/** /**
* All elements with class 'activate-osm-authentication' are loaded and get an 'onclick' to authenticate * All elements with class 'activate-osm-authentication' are loaded and get an 'onclick' to authenticate
* @param osmConnection
*/ */
registerActivateOsmAUthenticationClass() { registerActivateOsmAUthenticationClass() {
@ -144,6 +143,10 @@ export class OsmConnection {
} }
public SetPreference(k:string, v:string) { public SetPreference(k:string, v:string) {
if(!this.userDetails.data.loggedIn){
console.log("Not saving preference: user not logged in");
return;
}
if (this.preferences.data[k] === v) { if (this.preferences.data[k] === v) {
return; return;
@ -239,7 +242,6 @@ export class OsmConnection {
private AddChange(changesetId: string, private AddChange(changesetId: string,
changesetXML: string, changesetXML: string,
continuation: ((changesetId: string, idMapping: any) => void)){ continuation: ((changesetId: string, idMapping: any) => void)){
const self = this;
this.auth.xhr({ this.auth.xhr({
method: 'POST', method: 'POST',
options: { header: { 'Content-Type': 'text/xml' } }, options: { header: { 'Content-Type': 'text/xml' } },

View file

@ -32,6 +32,19 @@ export abstract class OsmObject {
abstract SaveExtraData(element); abstract SaveExtraData(element);
/**
* Replaces all '"' (double quotes) by '&quot;'
* Bugfix where names containing '"' were not uploaded, such as '"Het Zwin" nature reserve'
* @param string
* @constructor
*/
private Escape(string: string) {
while (string.indexOf('"') >= 0) {
string = string.replace('"', '&quot;');
}
return string;
}
/** /**
* Generates the changeset-XML for tags * Generates the changeset-XML for tags
* @constructor * @constructor
@ -41,7 +54,7 @@ export abstract class OsmObject {
for (const key in this.tags) { for (const key in this.tags) {
const v = this.tags[key]; const v = this.tags[key];
if (v !== "") { if (v !== "") {
tags += ' <tag k="' + key + '" v="' + this.tags[key] + '"/>\n' tags += ' <tag k="' + this.Escape(key) + '" v="' + this.Escape(this.tags[key]) + '"/>\n'
} }
} }
return tags; return tags;

View file

@ -29,6 +29,10 @@ export class Regex implements TagsFilter {
return false; return false;
} }
substituteValues(tags: any) : TagsFilter{
throw "Substituting values is not supported on regex tags"
}
} }
export class Tag implements TagsFilter { export class Tag implements TagsFilter {
@ -42,11 +46,10 @@ export class Tag implements TagsFilter {
matches(tags: { k: string; v: string }[]): boolean { matches(tags: { k: string; v: string }[]): boolean {
for (const tag of tags) { for (const tag of tags) {
if (tag.k === this.key) { if (tag.k === this.key) {
if (tag.v === "") { if (tag.v === "") {
// This tag has been removed // This tag has been removed
return false; return this.value === "";
} }
if (this.value === "*") { if (this.value === "*") {
// Any is allowed // Any is allowed
@ -56,6 +59,10 @@ export class Tag implements TagsFilter {
return this.value === tag.v; return this.value === tag.v;
} }
} }
if(this.value === ""){
return true;
}
return false; return false;
} }
@ -70,6 +77,10 @@ export class Tag implements TagsFilter {
return ['["' + this.key + '"="' + this.value + '"]']; return ['["' + this.key + '"="' + this.value + '"]'];
} }
substituteValues(tags: any) {
return new Tag(this.key, TagUtils.ApplyTemplate(this.value, tags));
}
} }
export class Or implements TagsFilter { export class Or implements TagsFilter {
@ -104,6 +115,14 @@ export class Or implements TagsFilter {
return choices; return choices;
} }
substituteValues(tags: any): TagsFilter {
const newChoices = [];
for (const c of this.or) {
newChoices.push(c.substituteValues(tags));
}
return new Or(newChoices);
}
} }
export class And implements TagsFilter { export class And implements TagsFilter {
@ -154,12 +173,22 @@ export class And implements TagsFilter {
} }
return allChoices; return allChoices;
} }
substituteValues(tags: any): TagsFilter {
const newChoices = [];
for (const c of this.and) {
newChoices.push(c.substituteValues(tags));
}
return new And(newChoices);
}
} }
export interface TagsFilter { export interface TagsFilter {
matches(tags: { k: string, v: string }[]): boolean matches(tags: { k: string, v: string }[]): boolean
asOverpass(): string[] asOverpass(): string[]
substituteValues(tags: any) : TagsFilter;
} }
export class TagUtils { export class TagUtils {
@ -172,4 +201,13 @@ export class TagUtils {
return result; return result;
} }
static ApplyTemplate(template: string, tags: any): string {
for (const k in tags) {
while (template.indexOf("{" + k + "}") >= 0) {
template = template.replace("{" + k + "}", tags[k]);
}
}
return template;
}
} }

View file

@ -1,24 +1,32 @@
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import {UIInputElement} from "./UIInputElement";
export class TextField extends UIElement { export class TextField<T> extends UIInputElement<T> {
public value = new UIEventSource(""); public value: UIEventSource<string> = new UIEventSource<string>("");
/** /**
* Pings and has the value data * Pings and has the value data
*/ */
public enterPressed = new UIEventSource<string>(undefined); public enterPressed = new UIEventSource<string>(undefined);
private _placeholder: UIEventSource<string>; private _placeholder: UIEventSource<string>;
private _mapping: (string) => T;
constructor(placeholder : UIEventSource<string>) { constructor(placeholder: UIEventSource<string>,
mapping: ((string) => T)) {
super(placeholder); super(placeholder);
this._placeholder = placeholder; this._placeholder = placeholder;
this._mapping = mapping;
}
GetValue(): UIEventSource<T> {
return this.value.map(this._mapping);
} }
protected InnerRender(): string { protected InnerRender(): string {
return "<form onSubmit='return false' class='form-text-field'>" + return "<form onSubmit='return false' class='form-text-field'>" +
"<input type='text' placeholder='"+this._placeholder.data+"' id='text-" + this.id + "'>" + "<input type='text' placeholder='" + (this._placeholder.data ?? "") + "' id='text-" + this.id + "'>" +
"</form>"; "</form>";
} }
@ -27,19 +35,24 @@ export class TextField extends UIElement {
const field = document.getElementById('text-' + this.id); const field = document.getElementById('text-' + this.id);
const self = this; const self = this;
field.oninput = () => { field.oninput = () => {
// @ts-ignore
self.value.setData(field.value); self.value.setData(field.value);
}; };
field.addEventListener("keyup", function (event) { field.addEventListener("keyup", function (event) {
if (event.key === "Enter") { if (event.key === "Enter") {
// @ts-ignore
self.enterPressed.setData(field.value); self.enterPressed.setData(field.value);
} }
}); });
} }
Clear() { Clear() {
const field = document.getElementById('text-' + this.id); const field = document.getElementById('text-' + this.id);
if (field !== undefined) { if (field !== undefined) {
// @ts-ignore
field.value = ""; field.value = "";
} }
} }

View file

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

View file

@ -1,30 +1,33 @@
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import {FixedUiElement} from "./FixedUiElement"; import {UIInputElement} from "./UIInputElement";
import $ from "jquery"
export class UIRadioButton extends UIElement { export class UIRadioButton<T> extends UIInputElement<T> {
public readonly SelectedElementIndex: UIEventSource<{ index: number, value: string }> public readonly SelectedElementIndex: UIEventSource<number>
= new UIEventSource<{ index: number, value: string }>(null); = new UIEventSource<number>(null);
private readonly _elements: UIEventSource<{ element: UIElement, value: string }[]> private readonly _elements: UIEventSource<UIElement[]>
private _selectFirstAsDefault: boolean;
private _valueMapping: (i: number) => T;
constructor(elements: UIEventSource<{ element: UIElement, value: string }[]>) { constructor(elements: UIEventSource<UIElement[]>,
valueMapping: ((i: number) => T),
selectFirstAsDefault = true) {
super(elements); super(elements);
this._elements = 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);
} }
static FromStrings(choices: string[]): UIRadioButton {
const wrapped = [];
for (const choice of choices) {
wrapped.push({
element: new FixedUiElement(choice),
value: choice
});
}
return new UIRadioButton(new UIEventSource<{ element: UIElement, value: string }[]>(wrapped))
}
private IdFor(i) { private IdFor(i) {
return 'radio-' + this.id + '-' + i; return 'radio-' + this.id + '-' + i;
@ -35,12 +38,9 @@ export class UIRadioButton extends UIElement {
let body = ""; let body = "";
let i = 0; let i = 0;
for (const el of this._elements.data) { for (const el of this._elements.data) {
const uielement = el.element;
const value = el.value;
const htmlElement = const htmlElement =
'<input type="radio" id="' + this.IdFor(i) + '" name="radiogroup-' + this.id + '" value="' + value + '">' + '<input type="radio" id="' + this.IdFor(i) + '" name="radiogroup-' + this.id + '">' +
'<label for="' + this.IdFor(i) + '">' + uielement.Render() + '</label>' + '<label for="' + this.IdFor(i) + '">' + el.Render() + '</label>' +
'<br>'; '<br>';
body += htmlElement; body += htmlElement;
@ -51,7 +51,6 @@ export class UIRadioButton extends UIElement {
} }
InnerUpdate(htmlElement: HTMLElement) { InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
const self = this; const self = this;
function checkButtons() { function checkButtons() {
@ -59,8 +58,7 @@ export class UIRadioButton extends UIElement {
const el = document.getElementById(self.IdFor(i)); const el = document.getElementById(self.IdFor(i));
// @ts-ignore // @ts-ignore
if (el.checked) { if (el.checked) {
var v = {index: i, value: self._elements.data[i].value} self.SelectedElementIndex.setData(i);
self.SelectedElementIndex.setData(v);
} }
} }
} }
@ -74,29 +72,32 @@ export class UIRadioButton extends UIElement {
); );
if (this.SelectedElementIndex.data == null) { if (this.SelectedElementIndex.data == null) {
const el = document.getElementById(this.IdFor(0)); if (this._selectFirstAsDefault) {
el.checked = true; const el = document.getElementById(this.IdFor(0));
checkButtons(); // @ts-ignore
el.checked = true;
checkButtons();
}
} else { } else {
// We check that what is selected matches the previous rendering // We check that what is selected matches the previous rendering
var checked = -1; var checked = -1;
var expected = -1 var expected = this.SelectedElementIndex.data;
for (let i = 0; i < self._elements.data.length; i++) { if (expected) {
const el = document.getElementById(self.IdFor(i));
// @ts-ignore for (let i = 0; i < self._elements.data.length; i++) {
if (el.checked) { const el = document.getElementById(self.IdFor(i));
checked = i; // @ts-ignore
if (el.checked) {
checked = i;
}
} }
if (el.value === this.SelectedElementIndex.data.value) { if (expected != checked) {
expected = i; const el = document.getElementById(this.IdFor(expected));
// @ts-ignore
el.checked = true;
} }
} }
if (expected != checked) {
const el = document.getElementById(this.IdFor(expected));
// @ts-ignore
el.checked = true;
}
} }

View file

@ -0,0 +1,72 @@
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

@ -1,14 +1,15 @@
import {UIElement} from "./UIElement"; import {UIElement} from "./UIElement";
import {TagMapping, TagMappingOptions} from "./TagMapping";
import {Question, QuestionDefinition} from "../Logic/Question";
import {UIEventSource} from "./UIEventSource"; import {UIEventSource} from "./UIEventSource";
import {QuestionPicker} from "./QuestionPicker"; import {QuestionPicker} from "./QuestionPicker";
import {OsmImageUploadHandler} from "../Logic/OsmImageUploadHandler"; import {OsmImageUploadHandler} from "../Logic/OsmImageUploadHandler";
import {ImageCarousel} from "./Image/ImageCarousel"; import {ImageCarousel} from "./Image/ImageCarousel";
import {Changes} from "../Logic/Changes"; import {Changes} from "../Logic/Changes";
import {UserDetails} from "../Logic/OsmConnection"; import {UserDetails} from "../Logic/OsmConnection";
import {CommonTagMappings} from "../Layers/CommonTagMappings";
import {VerticalCombine} from "./Base/VerticalCombine"; import {VerticalCombine} from "./Base/VerticalCombine";
import {TagRendering, TagRenderingOptions} from "../Customizations/TagRendering";
import {OsmLink} from "../Customizations/Questions/OsmLink";
import {WikipediaLink} from "../Customizations/Questions/WikipediaLink";
import {And} from "../Logic/TagsFilter";
export class FeatureInfoBox extends UIElement { export class FeatureInfoBox extends UIElement {
@ -17,7 +18,6 @@ export class FeatureInfoBox extends UIElement {
private _title: UIElement; private _title: UIElement;
private _osmLink: UIElement; private _osmLink: UIElement;
private _infoElements: UIElement[]
private _questions: QuestionPicker; private _questions: QuestionPicker;
@ -27,15 +27,16 @@ export class FeatureInfoBox extends UIElement {
private _imageElement: ImageCarousel; private _imageElement: ImageCarousel;
private _pictureUploader: UIElement; private _pictureUploader: UIElement;
private _wikipedialink: UIElement; private _wikipedialink: UIElement;
private _infoboxes: TagRendering[];
constructor( constructor(
tagsES: UIEventSource<any>, tagsES: UIEventSource<any>,
elementsToShow: (TagMappingOptions | QuestionDefinition | UIElement)[], title: TagRenderingOptions,
questions: QuestionDefinition[], elementsToShow: TagRenderingOptions[],
changes: Changes, changes: Changes,
userDetails: UIEventSource<UserDetails>, userDetails: UIEventSource<UserDetails>,
preferedPictureLicense : UIEventSource<string> preferedPictureLicense: UIEventSource<string>
) { ) {
super(tagsES); super(tagsES);
this._tagsES = tagsES; this._tagsES = tagsES;
@ -45,48 +46,66 @@ export class FeatureInfoBox extends UIElement {
this._imageElement = new ImageCarousel(this._tagsES); this._imageElement = new ImageCarousel(this._tagsES);
this._questions = new QuestionPicker( this._infoboxes = [];
this._changes.asQuestions(questions), this._tagsES); for (const tagRenderingOption of elementsToShow) {
if (tagRenderingOption.options === undefined) {
var infoboxes: UIElement[] = []; throw "Tagrendering.options not defined"
for (const uiElement of elementsToShow) {
if (uiElement instanceof QuestionDefinition) {
const questionDef = uiElement as QuestionDefinition;
const question = new Question(this._changes, questionDef);
infoboxes.push(question.CreateHtml(this._tagsES));
} else if (uiElement instanceof TagMappingOptions) {
const tagMappingOpt = uiElement as TagMappingOptions;
infoboxes.push(new TagMapping(tagMappingOpt, this._tagsES))
} else {
const ui = uiElement as UIElement;
infoboxes.push(ui);
} }
this._infoboxes.push(new TagRendering(this._tagsES, this._changes, tagRenderingOption.options))
} }
this._title = infoboxes.shift(); title = title ?? new TagRenderingOptions(
this._infoElements = infoboxes; {
mappings: [{k: new And([]), txt: ""}]
}
)
this._osmLink = new TagMapping(CommonTagMappings.osmLink, this._tagsES); this._title = new TagRendering(this._tagsES, this._changes, title.options);
this._wikipedialink = new TagMapping(CommonTagMappings.wikipediaLink, this._tagsES);
this._osmLink = new TagRendering(this._tagsES, this._changes, new OsmLink().options);
this._wikipedialink = new TagRendering(this._tagsES, this._changes, new WikipediaLink().options);
this._pictureUploader = new OsmImageUploadHandler(tagsES, userDetails, preferedPictureLicense, this._pictureUploader = new OsmImageUploadHandler(tagsES, userDetails, preferedPictureLicense,
changes, this._imageElement.slideshow).getUI(); changes, this._imageElement.slideshow).getUI();
} }
InnerRender(): string { InnerRender(): string {
let questions = "";
if (this._userDetails.data.loggedIn) { const info = [];
// Questions is embedded in a span, because it'll hide the parent when the questions dissappear const questions = [];
questions = "<span>"+this._questions.HideOnEmpty(true).Render()+"</span>";
for (const infobox of this._infoboxes) {
if (infobox.IsKnown()) {
info.push(infobox);
} else if (infobox.IsQuestioning()) {
questions.push(infobox);
}
}
let questionsHtml = "";
if (this._userDetails.data.loggedIn && questions.length > 0) {
// We select the most important question and render that one
let mostImportantQuestion;
let score = -1000;
for (const question of questions) {
if (mostImportantQuestion === undefined || question.priority > score) {
mostImportantQuestion = question;
score = question.priority;
}
}
questionsHtml = mostImportantQuestion.Render();
} }
return "<div class='featureinfobox'>" + return "<div class='featureinfobox'>" +
"<div class='featureinfoboxtitle'>" + "<div class='featureinfoboxtitle'>" +
"<span>" + this._title.Render() + "</span>" + "<span>" +
this._title.Render() +
"</span>" +
this._wikipedialink.Render() + this._wikipedialink.Render() +
this._osmLink.Render() + this._osmLink.Render() +
"</div>" + "</div>" +
@ -96,9 +115,9 @@ export class FeatureInfoBox extends UIElement {
this._imageElement.Render() + this._imageElement.Render() +
this._pictureUploader.Render() + this._pictureUploader.Render() +
new VerticalCombine(this._infoElements, 'infobox-information').HideOnEmpty(true).Render() + new VerticalCombine(info, "infobox-information ").Render() +
questions + questionsHtml +
"</div>" + "</div>" +
@ -110,11 +129,18 @@ export class FeatureInfoBox extends UIElement {
super.Activate(); super.Activate();
this._imageElement.Activate(); this._imageElement.Activate();
this._pictureUploader.Activate(); this._pictureUploader.Activate();
for (const infobox of this._infoboxes) {
infobox.Activate();
}
} }
Update() { Update() {
super.Update(); super.Update();
this._imageElement.Update(); this._imageElement.Update();
this._pictureUploader.Update(); this._pictureUploader.Update();
this._title.Update();
for (const infobox of this._infoboxes) {
infobox.Update();
}
} }
} }

View file

@ -39,7 +39,7 @@ export class QuestionPicker extends UIElement {
return "Er zijn geen vragen meer!"; return "Er zijn geen vragen meer!";
} }
return "<div class='infobox-questions'>" + return "<div class='question'>" +
highestQ.CreateHtml(this.source).Render() + highestQ.CreateHtml(this.source).Render() +
"</div>"; "</div>";
} }

25
UI/SaveButton.ts Normal file
View file

@ -0,0 +1,25 @@
import {UIEventSource} from "./UIEventSource";
import {UIElement} from "./UIElement";
export class SaveButton extends UIElement {
private _value: UIEventSource<any>;
constructor(value: UIEventSource<any>) {
super(value);
if(value === undefined){
throw "No event source for savebutton, something is wrong"
}
this._value = value;
}
protected 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'>Opslaan</span>";
}
}

View file

@ -60,8 +60,8 @@ export class SearchAndGo extends UIElement {
protected InnerRender(): string { protected InnerRender(): string {
// "<img class='search' src='./assets/search.svg' alt='Search'> " + // "<img class='search' src='./assets/search.svg' alt='Search'> " +
return this._goButton.Render() + return this._searchField.Render() +
this._searchField.Render(); this._goButton.Render();
} }

View file

@ -1,80 +0,0 @@
import {UIElement} from "./UIElement";
import {UIEventSource} from "./UIEventSource";
export class TagMappingOptions {
key: string;// The key to show
mapping?: any;// dictionary for specific values, the values are substituted
template?: string; // The template, where {key} will be substituted
missing?: string// What to show when the key is not there
freeform?: ((string) => string) // Freeform template function, only applied on the value if nothing matches
constructor(options: {
key: string,
mapping?: any,
template?: string,
missing?: string
freeform?: ((string) => string)
}) {
this.key = options.key;
this.mapping = options.mapping;
this.template = options.template;
this.missing = options.missing;
this.freeform = options.freeform;
}
}
export class TagMapping extends UIElement {
private readonly tags;
private readonly options: TagMappingOptions;
constructor(
options: TagMappingOptions,
tags: UIEventSource<any>) {
super(tags);
this.tags = tags.data;
this.options = options;
}
IsEmpty(): boolean {
const o = this.options;
return this.tags[o.key] === undefined && o.missing === undefined;
}
protected InnerRender(): string {
const o = this.options;
const v = this.tags[o.key];
if (v === undefined) {
if (o.missing === undefined) {
return "";
}
return o.missing;
}
if (o.mapping !== undefined) {
const mapped = o.mapping[v];
if (mapped !== undefined) {
return mapped;
}
}
if (o.template !== undefined) {
return o.template.replace("{" + o.key + "}", v);
}
if(o.freeform !== undefined){
return o.freeform(v);
}
console.log("Warning: no match for " + o.key + "=" + v);
return v;
}
InnerUpdate(htmlElement: HTMLElement) {
}
}

View file

@ -160,6 +160,7 @@ body {
position: relative; position: relative;
float: left; float: left;
margin-top: 0.2em; margin-top: 0.2em;
margin-left: 1em;
} }
#searchbox input[type="text"] { #searchbox input[type="text"] {
@ -170,12 +171,13 @@ body {
.search-go { .search-go {
position: relative; position: relative;
float: right;
height: 1.2em; height: 1.2em;
border: 2px solid black; border: 2px solid black;
border-radius: 2em; border-radius: 2em;
padding: 0.4em; padding: 0.4em;
float: left; margin-left: 0.5em;
margin-right: 0.5em; margin-right: 0;
} }
@ -216,7 +218,7 @@ body {
#collapseButton { #collapseButton {
position: absolute; position: absolute;
right: 0; right: 1em;
background-color: white; background-color: white;
margin: 1.5em; margin: 1.5em;
border: 2px solid black; border: 2px solid black;
@ -570,6 +572,42 @@ body {
fill: #7ebc6f; fill: #7ebc6f;
} }
.featureinfoboxtitle .answer {
display: inline;
margin-right: 3em;
}
.featureinfoboxtitle .answer-text {
display: inline;
}
.featureinfoboxtitle .editbutton {
float: none;
width: 0.8em;
height: 0.8em;
padding: 0.3em;
border-radius: 0.35em;
border: solid black 1px;
margin-left: 1em;
top: 0.2em;
position: absolute;
}
.editbutton {
width: 1.3em;
height: 1.3em;
padding: 0.5em;
border-radius: 0.65em;
border: solid black 1px;
font-size: medium;
float: right;
}
.wikipedialink { .wikipedialink {
position: absolute; position: absolute;
right: 24px; right: 24px;
@ -606,11 +644,61 @@ body {
margin-top: 1em; margin-top: 1em;
} }
.infobox-questions { .question {
margin-top: 1em; margin-top: 1em;
background-color: #e5f5ff; background-color: #e5f5ff;
padding: 1em; padding: 1em;
border-radius: 1em; border-radius: 1em;
margin-right: 1em; margin-right: 1em;
font-size: larger;
}
.answer {
display: inline-block;
margin: 0.1em;
width: 100%;
font-size: large;
}
.answer-text {
width: 90%;
display: inline-block
}
/**** The save button *****/
.save {
display: inline-block;
border: solid white 2px;
background-color: #3a3aeb;
color: white;
padding: 0.2em;
padding-left: 0.3em;
padding-right: 0.3em;
font-size: x-large;
font-weight: bold;
border-radius: 1.5em;
}
.save-non-active {
display: inline-block;
border: solid lightgrey 2px;
color: grey;
padding: 0.2em;
padding-left: 0.3em;
padding-right: 0.3em;
font-size: x-large;
font-weight: bold;
border-radius: 1.5em;
}
.skip-button {
display: inline-block;
border: solid black 0.5px;
padding: 0.2em;
padding-left: 0.3em;
padding-right: 0.3em;
border-radius: 1.5em;
} }

View file

@ -7,7 +7,6 @@ import {Basemap} from "./Logic/Basemap";
import {PendingChanges} from "./UI/PendingChanges"; import {PendingChanges} from "./UI/PendingChanges";
import {CenterMessageBox} from "./UI/CenterMessageBox"; import {CenterMessageBox} from "./UI/CenterMessageBox";
import {Helpers} from "./Helpers"; import {Helpers} from "./Helpers";
import {KnownSet} from "./Layers/KnownSet";
import {Tag, TagUtils} from "./Logic/TagsFilter"; 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";
@ -21,6 +20,7 @@ import {SimpleAddUI} from "./UI/SimpleAddUI";
import {VariableUiElement} from "./UI/Base/VariableUIElement"; import {VariableUiElement} from "./UI/Base/VariableUIElement";
import {SearchAndGo} from "./UI/SearchAndGo"; import {SearchAndGo} from "./UI/SearchAndGo";
import {CollapseButton} from "./UI/Base/CollapseButton"; import {CollapseButton} from "./UI/Base/CollapseButton";
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
let dryRun = false; let dryRun = false;
@ -34,10 +34,11 @@ if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
} }
// ----------------- SELECT THE RIGHT QUESTSET ----------------- // ----------------- SELECT THE RIGHT QUESTSET -----------------
let questSetToRender = KnownSet.groen; let defaultQuest = "groen"
if (window.location.search) { if (window.location.search) {
const params = window.location.search.substr(1).split("&"); const params = window.location.search.substr(1).split("&");
const paramDict: any = {}; const paramDict: any = {};
@ -45,13 +46,17 @@ if (window.location.search) {
var kv = param.split("="); var kv = param.split("=");
paramDict[kv[0]] = kv[1]; paramDict[kv[0]] = kv[1];
} }
if (paramDict.quests) { if (paramDict.quests) {
questSetToRender = KnownSet.allSets[paramDict.quests]; defaultQuest = paramDict.quests
console.log("Using quests: ", questSetToRender.name); }
if(paramDict.test){
dryRun = true;
} }
} }
const questSetToRender = AllKnownLayouts.allSets[defaultQuest];
console.log("Using quests: ", questSetToRender.name);
document.title = questSetToRender.title; document.title = questSetToRender.title;
@ -79,12 +84,12 @@ const locationControl = new UIEventSource<{ lat: number, lon: number, zoom: numb
// ----------------- Prepare the important objects ----------------- // ----------------- Prepare the important objects -----------------
const saveTimeout = 5000; // 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 osmConnection = new OsmConnection(dryRun);
const changes = new Changes( const changes = new Changes(
"Beantwoorden van vragen met MapComplete voor vragenset #" + questSetToRender.name, "Beantwoorden van vragen met MapComplete voor vragenset #" + questSetToRender.name,
osmConnection, allElements, centerMessage); osmConnection, allElements);
const bm = new Basemap("leafletDiv", locationControl, new VariableUiElement( const bm = new Basemap("leafletDiv", locationControl, new VariableUiElement(
locationControl.map((location) => { locationControl.map((location) => {
const mapComplete = "<a href='https://github.com/pietervdvn/MapComplete' target='_blank'>Mapcomple</a> " + const mapComplete = "<a href='https://github.com/pietervdvn/MapComplete' target='_blank'>Mapcomple</a> " +
@ -138,8 +143,8 @@ for (const layer of questSetToRender.layers) {
return new FeatureInfoBox( return new FeatureInfoBox(
tagsES, tagsES,
layer.title,
layer.elementsToShow, layer.elementsToShow,
layer.questions,
changes, changes,
osmConnection.userDetails, osmConnection.userDetails,
preferedPictureLicense preferedPictureLicense
@ -191,8 +196,8 @@ selectedElement.addCallback((data) => {
leftMessage.setData(() => leftMessage.setData(() =>
new FeatureInfoBox( new FeatureInfoBox(
allElements.getElement(data.id), allElements.getElement(data.id),
layer.title,
layer.elementsToShow, layer.elementsToShow,
layer.questions,
changes, changes,
osmConnection.userDetails, osmConnection.userDetails,
preferedPictureLicense preferedPictureLicense
@ -214,6 +219,7 @@ new SearchAndGo(bm).AttachTo("searchbox");
new CollapseButton("messagesbox") new CollapseButton("messagesbox")
.AttachTo("collapseButton"); .AttachTo("collapseButton");
var welcomeMessage = () => { var welcomeMessage = () => {
return new VariableUiElement( return new VariableUiElement(
osmConnection.userDetails.map((userdetails) => { osmConnection.userDetails.map((userdetails) => {

5
package-lock.json generated
View file

@ -992,11 +992,6 @@
"physical-cpu-count": "^2.0.0" "physical-cpu-count": "^2.0.0"
} }
}, },
"@splidejs/splide": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@splidejs/splide/-/splide-2.4.0.tgz",
"integrity": "sha512-IiorzntnnTBdXFxgWgz2x2QgW8/xmr4AU6+biUD+knGMZrSoBU34DtrK9qAdLkYVMevNyUomrJtGZHFZl9nwrw=="
},
"@types/geojson": { "@types/geojson": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz",

View file

@ -15,7 +15,6 @@
"author": "pietervdvn", "author": "pietervdvn",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@splidejs/splide": "^2.4.0",
"fs": "0.0.1-security", "fs": "0.0.1-security",
"jquery": "latest", "jquery": "latest",
"leaflet": "^1.6.0", "leaflet": "^1.6.0",

39
test.ts
View file

@ -1,26 +1,21 @@
import {Geocoding} from "./Logic/Geocoding";
import {SearchAndGo} from "./UI/SearchAndGo";
import {TextField} from "./UI/Base/TextField";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
import {DropDownUI} from "./UI/Base/DropDownUI";
import {UIEventSource} from "./UI/UIEventSource"; import {UIEventSource} from "./UI/UIEventSource";
import {Changes} from "./Logic/Changes";
import {OsmConnection} from "./Logic/OsmConnection";
import {ElementStorage} from "./Logic/ElementStorage";
import {WikipediaLink} from "./Customizations/Questions/WikipediaLink";
import {OsmLink} from "./Customizations/Questions/OsmLink";
console.log("HI"); const tags = {name: "Test",
wikipedia: "nl:Pieter",
id: "node/-1"};
const tagsES = new UIEventSource(tags);
const login = new OsmConnection(true);
const allElements = new ElementStorage();
allElements.addElementById(tags.id, tagsES);
const changes = new Changes("Test", login, allElements)
var control = new UIEventSource<string>("b"); new OsmLink(tagsES, changes).AttachTo("maindiv");
control.addCallback((data) => {
console.log("> GOT", control.data)
})
new DropDownUI("Test",
[{value: "a", shown: "a"},
{value: "b", shown: "b"},
{value: "c", shown: "c"},
], control
).AttachTo("maindiv");
new VariableUiElement(control).AttachTo("extradiv");
window.setTimeout(() => {control.setData("a")}, 1000);