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

@ -0,0 +1,45 @@
import {Tag, TagsFilter} from "../Logic/TagsFilter";
import {UIElement} from "../UI/UIElement";
import {Basemap} from "../Logic/Basemap";
import {ElementStorage} from "../Logic/ElementStorage";
import {UIEventSource} from "../UI/UIEventSource";
import {FilteredLayer} from "../Logic/FilteredLayer";
import {Changes} from "../Logic/Changes";
import {UserDetails} from "../Logic/OsmConnection";
import {TagRenderingOptions} from "./TagRendering";
export class LayerDefinition {
name: string;
newElementTags: Tag[]
icon: string;
minzoom: number;
overpassFilter: TagsFilter;
title: TagRenderingOptions;
elementsToShow: TagRenderingOptions[];
style: (tags: any) => { color: string, icon: any };
/**
* If an object of the next layer is contained for this many percent in this feature, it is eaten and not shown
*/
maxAllowedOverlapPercentage: number = undefined;
asLayer(basemap: Basemap, allElements: ElementStorage, changes: Changes, userDetails: UIEventSource<UserDetails>, selectedElement: UIEventSource<any>,
showOnPopup: (tags: UIEventSource<(any)>) => UIElement):
FilteredLayer {
return new FilteredLayer(
this.name,
basemap, allElements, changes,
this.overpassFilter,
this.maxAllowedOverlapPercentage,
this.style,
selectedElement,
showOnPopup);
}
}

View file

@ -0,0 +1,87 @@
import {LayerDefinition} from "../LayerDefinition";
import {QuestionDefinition} from "../../Logic/Question";
import {Tag} from "../../Logic/TagsFilter";
import L from "leaflet";
export class Artwork extends LayerDefinition {
constructor() {
super();
this.name = "artwork";
this.newElementTags = [new Tag("tourism", "artwork")];
this.icon = "./assets/statue.svg";
this.overpassFilter = new Tag("tourism", "artwork");
this.minzoom = 13;
this.questions = [
QuestionDefinition.radioAndTextQuestion("What kind of artwork is this?", 10, "artwork_type",
[
{text: "A statue", value: "statue"},
{text: "A bust (thus a statue, but only of the head and shoulders)", value: "bust"},
{text: "A sculpture", value: "sculpture"},
{text: "A mural painting", value: "mural"},
{text: "A painting", value: "painting"},
{text: "A graffiti", value: "graffiti"},
{text: "A relief", value: "relief"},
{text: "An installation", value: "installation"}]),
QuestionDefinition.textQuestion("Whom or what is depicted in this statue?", "subject", 20).addUnrequiredTag("subject:wikidata","*"),
QuestionDefinition.textQuestion("Is there an inscription on this artwork?", "inscription", 16),
QuestionDefinition.textQuestion("What is the name of this artwork? If there is no explicit name, skip the question", "name", 15),
];
this.style = function (tags) {
return {
icon: new L.icon({
iconUrl: "assets/statue.svg",
iconSize: [40, 40],
text: "hi"
}),
color: "#0000ff"
};
}
this.elementsToShow = [
new TagMappingOptions(
{
key: "name",
template: "<h2>Artwork '{name}'</h2>",
missing: "Artwork"
}),
new TagMappingOptions({
key: "artwork_type",
template: "This artwork is a {artwork_type}"
}),
new TagMappingOptions({
key: "artist_name",
template: "This artwork was made by {artist_name}"
}),
new TagMappingOptions({
key: "subject",
template: "This artwork depicts {subject}"
}),
new TagMappingOptions({
key: "subject:wikidata",
template: "<a href='https://www.wikidata.org/wiki/{subject:wikidata}' target='_blank'>See more data about the subject</a>"
}),
new TagMappingOptions({
key: "website",
template: "<a href='{website}' target='_blank'>Website of the statue</a>"
}),
new TagMappingOptions({key: "image", template: "<img class='popupImg' alt='image' src='{image}' />"})
];
}
}

View file

@ -0,0 +1,65 @@
import {LayerDefinition} from "../LayerDefinition";
import L from "leaflet";
import {Tag} from "../../Logic/TagsFilter";
import {QuestionDefinition} from "../../Logic/Question";
import {TagRenderingOptions} from "../TagRendering";
export class Bookcases extends LayerDefinition {
constructor() {
super();
this.name = "boekenkast";
this.newElementTags = [new Tag( "amenity", "public_bookcase")];
this.icon = "./assets/bookcase.svg";
this.overpassFilter = new Tag("amenity","public_bookcase");
this.minzoom = 13;
this.questions = [
QuestionDefinition.noNameOrNameQuestion("Wat is de naam van dit boekenruilkastje?", "Dit boekenruilkastje heeft niet echt een naam", 20),
QuestionDefinition.textQuestion("Hoeveel boeken kunnen er in?", "capacity", 15),
QuestionDefinition.textQuestion("Heeft dit boekenkastje een peter, meter of voogd?", "operator", 10),
// QuestionDefinition.textQuestion("Wie kunnen we (per email) contacteren voor dit boekenruilkastje?", "email", 5),
]
;
this.style = function (tags) {
return {
icon: new L.icon({
iconUrl: "assets/bookcase.svg",
iconSize: [40, 40]
}),
color: "#0000ff"
};
}
this.elementsToShow = [
new TagMappingOptions({
key: "name",
template: "{name}",
missing: "Boekenruilkastje"
}
),
new TagMappingOptions({key: "capacity", template: "Plaats voor {capacity} boeken"}),
new TagMappingOptions({key: "operator", template: "Onder de hoede van {operator}"}),
new TagMappingOptions({
key: "website",
mapping: "Meer informatie beschikbaar op <a href='{website}'>{website}</a>"
}),
new TagMappingOptions({key: "start_date", template: "Geplaatst op {start_date}"}),
new TagMappingOptions({key: "brand", template: "Deel van het netwerk {brand}"}),
new TagMappingOptions({key: "ref", template: "Referentienummer {ref}"}),
new TagMappingOptions({key: "description", template: "Extra beschrijving: <br /> <p>{description}</p>"}),
]
;
}
}

View file

@ -0,0 +1,73 @@
import {LayerDefinition} from "../LayerDefinition";
import {Quests} from "../../Quests";
import {And, Or, Tag} from "../../Logic/TagsFilter";
import {AccessTag} from "../Questions/AccessTag";
import {OperatorTag} from "../Questions/OperatorTag";
import {TagRenderingOptions} from "../TagRendering";
import {NameQuestion} from "../Questions/NameQuestion";
import {NameInline} from "../Questions/NameInline";
export class Bos extends LayerDefinition {
constructor() {
super();
this.name = "bos";
this.icon = "./assets/tree_white_background.svg";
this.overpassFilter = new Or([
new Tag("natural", "wood"),
new Tag("landuse", "forest"),
new Tag("natural", "scrub")
]
);
this.newElementTags = [
new Tag("landuse", "forest"),
new Tag("fixme", "Toegevoegd met MapComplete, geometry nog uit te tekenen")
];
this.maxAllowedOverlapPercentage = 10;
this.minzoom = 13;
this.style = this.generateStyleFunction();
this.title = new NameInline("bos");
this.elementsToShow = [
new NameQuestion(),
new AccessTag(),
new OperatorTag()
];
}
private generateStyleFunction() {
const self = this;
return function (properties: any) {
let questionSeverity = 0;
for (const qd of self.elementsToShow) {
if (qd.IsQuestioning(properties)) {
questionSeverity = Math.max(questionSeverity, qd.options.priority ?? 0);
}
}
let colormapping = {
0: "#00bb00",
1: "#00ff00",
10: "#dddd00",
20: "#ff0000"
};
let colour = colormapping[questionSeverity];
while (colour == undefined) {
questionSeverity--;
colour = colormapping[questionSeverity];
}
return {
color: colour,
icon: undefined
};
};
}
}

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

@ -0,0 +1,62 @@
import {LayerDefinition} from "../LayerDefinition";
import {Or, Tag} from "../../Logic/TagsFilter";
import {TagRenderingOptions} from "../TagRendering";
import {AccessTag} from "../Questions/AccessTag";
import {OperatorTag} from "../Questions/OperatorTag";
import {NameQuestion} from "../Questions/NameQuestion";
import {NameInline} from "../Questions/NameInline";
export class NatureReserves extends LayerDefinition {
constructor() {
super();
this.name = "natuurgebied";
this.icon = "./assets/tree_white_background.svg";
this.overpassFilter =
new Or([new Tag("leisure", "nature_reserve"), new Tag("boundary","protected_area")]);
this.maxAllowedOverlapPercentage = 10;
this.newElementTags = [new Tag("leisure", "nature_reserve"),
new Tag("fixme", "Toegevoegd met MapComplete, geometry nog uit te tekenen")]
this.minzoom = 13;
this.title = new NameInline("natuurreservaat");
this.style = this.generateStyleFunction();
this.elementsToShow = [
new NameQuestion(),
new AccessTag(),
new OperatorTag(),
];
}
private generateStyleFunction() {
const self = this;
return function (properties: any) {
let questionSeverity = 0;
for (const qd of self.elementsToShow) {
if (qd.IsQuestioning(properties)) {
questionSeverity = Math.max(questionSeverity, qd.options.priority ?? 0);
}
}
let colormapping = {
0: "#00bb00",
1: "#00ff00",
10: "#dddd00",
20: "#ff0000"
};
let colour = colormapping[questionSeverity];
while (colour == undefined) {
questionSeverity--;
colour = colormapping[questionSeverity];
}
return {
color: colour,
icon: undefined
};
};
}
}

View file

@ -0,0 +1,62 @@
import {LayerDefinition} from "../LayerDefinition";
import {Quests} from "../../Quests";
import {And, Or, Tag} from "../../Logic/TagsFilter";
import {AccessTag} from "../Questions/AccessTag";
import {OperatorTag} from "../Questions/OperatorTag";
import {TagRenderingOptions} from "../TagRendering";
import {NameQuestion} from "../Questions/NameQuestion";
import {NameInline} from "../Questions/NameInline";
export class Park extends LayerDefinition {
constructor() {
super();
this.name = "park";
this.icon = "./assets/tree_white_background.svg";
this.overpassFilter =
new Or([new Tag("leisure", "park"), new Tag("landuse", "village_green")]);
this.newElementTags = [new Tag("leisure", "park"),
new Tag("fixme", "Toegevoegd met MapComplete, geometry nog uit te tekenen")];
this.maxAllowedOverlapPercentage = 25;
this.minzoom = 13;
this.style = this.generateStyleFunction();
this.title = new NameInline("park");
this.elementsToShow = [new NameQuestion()];
}
private generateStyleFunction() {
const self = this;
return function (properties: any) {
let questionSeverity = 0;
for (const qd of self.elementsToShow) {
if (qd.IsQuestioning(properties)) {
questionSeverity = Math.max(questionSeverity, qd.options.priority ?? 0);
}
}
let colormapping = {
0: "#00bb00",
1: "#00ff00",
10: "#dddd00",
20: "#ff0000"
};
let colour = colormapping[questionSeverity];
while (colour == undefined) {
questionSeverity--;
colour = colormapping[questionSeverity];
}
return {
color: colour,
icon: undefined
};
};
}
}

View file

@ -0,0 +1,85 @@
import {LayerDefinition} from "../LayerDefinition";
import {Quests} from "../../Quests";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import L from "leaflet";
import {Tag} from "../../Logic/TagsFilter";
export class Toilets extends LayerDefinition{
constructor() {
super();
this.name="toilet";
this.newElementTags = [new Tag( "amenity", "toilets")];
this.icon = "./assets/toilets.svg";
this.overpassFilter = new Tag("amenity","toilets");
this.minzoom = 13;
this.questions = [Quests.hasFee,
Quests.toiletsWheelChairs,
Quests.toiletsChangingTable,
Quests.toiletsChangingTableLocation,
Quests.toiletsPosition];
this.style = function(tags){
if(tags.wheelchair == "yes"){
return {icon : new L.icon({
iconUrl: "assets/wheelchair.svg",
iconSize: [40, 40]
})};
}
return {icon : new L.icon({
iconUrl: "assets/toilets.svg",
iconSize: [40, 40]
})};
}
this.elementsToShow = [
new FixedUiElement("Toiletten"),
new TagMappingOptions({
key: "access",
mapping: {
yes: "Toegankelijk",
no: "Niet toegankelijk",
private: "Niet toegankelijk",
customers: "Enkel voor klanten",
}
}),
new TagMappingOptions({
key: "fee",
mapping: {
yes: "Betalend",
no: "Gratis",
["0"]: "Gratis"
},
template: "Betalend, men vraagt {fee}"
}),
new TagMappingOptions({
key: "toilets:position",
mapping: {
seated: 'Gewone zittoiletten',
urinal: 'Een enkele urinoir',
urinals: 'Urinoirs',
['urinals;seated']: "Urinoirs en gewone toiletten",
['seated;urinals']: "Urinoirs en gewone toiletten",
}
}),
new TagMappingOptions({
key: "wheelchair",
mapping: {
yes: "Rolstoeltoegankelijk",
no: "Niet Rolstoeltoegankelijk",
limited: "Beperkt rolstoeltoegankelijk",
}
}),
];
}
}

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