Various bug fixes and updates

This commit is contained in:
Pieter Vander Vennet 2020-09-09 18:42:13 +02:00
parent 97ec893479
commit e069b31e4e
29 changed files with 482 additions and 148 deletions

View file

@ -17,6 +17,7 @@ import * as viewpoint from "../../assets/layers/viewpoint/viewpoint.json"
import * as bike_parking from "../../assets/layers/bike_parking/bike_parking.json" import * as bike_parking from "../../assets/layers/bike_parking/bike_parking.json"
import * as bike_repair_station from "../../assets/layers/bike_repair_station/bike_repair_station.json" import * as bike_repair_station from "../../assets/layers/bike_repair_station/bike_repair_station.json"
import * as birdhides from "../../assets/layers/bird_hide/birdhides.json" import * as birdhides from "../../assets/layers/bird_hide/birdhides.json"
import * as nature_reserve from "../../assets/layers/nature_reserve/nature_reserve.json"
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
@ -34,6 +35,7 @@ export class FromJSON {
FromJSON.Layer(bike_parking), FromJSON.Layer(bike_parking),
FromJSON.Layer(bike_repair_station), FromJSON.Layer(bike_repair_station),
FromJSON.Layer(birdhides), FromJSON.Layer(birdhides),
FromJSON.Layer(nature_reserve),
]; ];
for (const layer of sharedLayersList) { for (const layer of sharedLayersList) {
@ -102,7 +104,6 @@ export class FromJSON {
public static TagRenderingWithDefault(json: TagRenderingConfigJson | string, propertyName, defaultValue: string): TagDependantUIElementConstructor { public static TagRenderingWithDefault(json: TagRenderingConfigJson | string, propertyName, defaultValue: string): TagDependantUIElementConstructor {
if (json === undefined) { if (json === undefined) {
if(defaultValue !== undefined){ if(defaultValue !== undefined){
console.log(`Using default value ${defaultValue} for ${propertyName}`)
return FromJSON.TagRendering(defaultValue, propertyName); return FromJSON.TagRendering(defaultValue, propertyName);
} }
throw `Tagrendering ${propertyName} is undefined...` throw `Tagrendering ${propertyName} is undefined...`
@ -207,7 +208,7 @@ export class FromJSON {
return new Tag(tag[0], tag[1]); return new Tag(tag[0], tag[1]);
} }
public static Tag(json: AndOrTagConfigJson | string, context: string): TagsFilter { public static Tag(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter {
if(json === undefined){ if(json === undefined){
throw "Error while parsing a tag: nothing defined. Make sure all the tags are defined and at least one tag is present in a complex expression" throw "Error while parsing a tag: nothing defined. Make sure all the tags are defined and at least one tag is present in a complex expression"
} }
@ -286,7 +287,6 @@ export class FromJSON {
private static LayerUncaught(json: LayerConfigJson): LayerDefinition { private static LayerUncaught(json: LayerConfigJson): LayerDefinition {
console.log("Parsing layer", json)
const tr = FromJSON.Translation; const tr = FromJSON.Translation;
const overpassTags = FromJSON.Tag(json.overpassTags, "overpasstags for layer "+json.id); const overpassTags = FromJSON.Tag(json.overpassTags, "overpasstags for layer "+json.id);
const icon = FromJSON.TagRenderingWithDefault(json.icon, "icon", "./assets/bug.svg"); const icon = FromJSON.TagRenderingWithDefault(json.icon, "icon", "./assets/bug.svg");
@ -381,6 +381,7 @@ export class FromJSON {
} }
); );
layer.maxAllowedOverlapPercentage = json.hideUnderlayingFeaturesMinPercentage;
return layer; return layer;
} }

View file

@ -70,6 +70,14 @@ export interface LayerConfigJson {
*/ */
wayHandling?: number; wayHandling?: number;
/**
* Consider that we want to show 'Nature Reserves' and 'Forests'. Now, ofter, there are pieces of forest mapped _in_ the nature reserve.
* Now, showing those pieces of forest overlapping with the nature reserve truly clutters the map and is very user-unfriendly.
*
* The features are placed layer by layer. If a feature below a feature on this layer overlaps for more then 'x'-percent, the underlying feature is hidden.
*/
hideUnderlayingFeaturesMinPercentage?:number;
/** /**
* Presets for this layer * Presets for this layer
*/ */

View file

@ -130,46 +130,4 @@ export class LayerDefinition {
this.wayHandling = options.wayHandling ?? LayerDefinition.WAYHANDLING_DEFAULT; this.wayHandling = options.wayHandling ?? LayerDefinition.WAYHANDLING_DEFAULT;
} }
/*
ToJson() {
function t(translation: string | Translation | UIElement) {
if (translation === undefined) {
return undefined;
}
if (typeof (translation) === "string") {
return translation;
}
if (translation instanceof Translation && translation.translations !== undefined) {
return translation.translations;
}
return translation.InnerRender();
}
function tr(tagRendering : TagRenderingOptions) : TagRenderingConfigJson{
const o = tagRendering.options;
return {
key: o.freeform.key,
render: o.freeform.renderTemplate,
type: o.freeform.template.
}
}
const layerConfig : LayerConfigJson = {
name: t(this.name),
description: t(this.description),
maxAllowedOverlapPercentage: this.maxAllowedOverlapPercentage,
presets: this.presets,
icon: this.icon,
minzoom: this.minzoom,
overpassFilter: this.overpassFilter,
title: this.title,
elementsToShow: this.elementsToShow,
style: this.style,
wayHandling: this.wayHandling,
};
return JSON.stringify(layerConfig)
}*/
} }

View file

@ -35,7 +35,7 @@ export class Bos extends LayerDefinition {
this.minzoom = 13; this.minzoom = 13;
this.style = this.generateStyleFunction(); this.style = this.generateStyleFunction();
this.title = new NameInline("bos"); this.title = new NameInline("Bos");
this.elementsToShow = [ this.elementsToShow = [
new ImageCarouselWithUploadConstructor(), new ImageCarouselWithUploadConstructor(),
new NameQuestion(), new NameQuestion(),

View file

@ -26,7 +26,7 @@ export class NatureReserves extends LayerDefinition {
} }
]; ];
this.minzoom = 13; this.minzoom = 13;
this.title = new NameInline("natuurreservaat"); this.title = new NameInline("Natuurreservaat");
this.style = this.generateStyleFunction(); this.style = this.generateStyleFunction();
this.elementsToShow = [ this.elementsToShow = [
new ImageCarouselWithUploadConstructor(), new ImageCarouselWithUploadConstructor(),

View file

@ -13,7 +13,7 @@ export class Park extends LayerDefinition {
question: "Is dit park publiek toegankelijk?", question: "Is dit park publiek toegankelijk?",
mappings: [ mappings: [
{k: new Tag("access", "yes"), txt: "Publiek toegankelijk"}, {k: new Tag("access", "yes"), txt: "Publiek toegankelijk"},
{k: new Tag("access", ""), txt: "Publiek toegankelijk"}, {k: new Tag("access", ""), txt: "Publiek toegankelijk", hideInAnswer: true},
{k: new Tag("access", "no"), txt: "Niet publiek toegankelijk"}, {k: new Tag("access", "no"), txt: "Niet publiek toegankelijk"},
{k: new Tag("access", "private"), txt: "Niet publiek toegankelijk, want privaat"}, {k: new Tag("access", "private"), txt: "Niet publiek toegankelijk, want privaat"},
{k: new Tag("access", "guided"), txt: "Enkel toegankelijk met een gids of op een activiteit"}, {k: new Tag("access", "guided"), txt: "Enkel toegankelijk met een gids of op een activiteit"},
@ -59,7 +59,7 @@ export class Park extends LayerDefinition {
this.minzoom = 13; this.minzoom = 13;
this.style = this.generateStyleFunction(); this.style = this.generateStyleFunction();
this.title = new NameInline("park"); this.title = new NameInline("Park");
this.elementsToShow = [ this.elementsToShow = [
new ImageCarouselWithUploadConstructor(), new ImageCarouselWithUploadConstructor(),
new NameQuestion(), new NameQuestion(),

View file

@ -168,12 +168,12 @@ export class Widths extends LayerDefinition {
let dashArray = undefined; let dashArray = undefined;
if (props.onewayBike) { if (props.onewayBike) {
dashArray = [20, 8] dashArray = [5, 6]
} }
return { return {
icon: null, icon: null,
color: c, color: c,
weight: 9, weight: 5,
dashArray: dashArray dashArray: dashArray
} }
} }

View file

@ -1,7 +1,7 @@
import {NatureReserves} from "../Layers/NatureReserves";
import {Park} from "../Layers/Park"; import {Park} from "../Layers/Park";
import {Bos} from "../Layers/Bos"; import {Bos} from "../Layers/Bos";
import {Layout} from "../Layout"; import {Layout} from "../Layout";
import {NatureReserves} from "../Layers/NatureReserves";
export class Groen extends Layout { export class Groen extends Layout {

View file

@ -41,7 +41,7 @@ export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{
} }
GetContent(tags: any): Translation { GetContent(tags: any): Translation {
if(this.IsKnown(tags)){ if(!this.IsKnown(tags)){
return undefined; return undefined;
} }
return this._embedded.GetContent(tags); return this._embedded.GetContent(tags);

View file

@ -17,7 +17,7 @@ export class AccessTag extends TagRenderingOptions {
{k: new And([new Tag("access", "no"), new Tag("fee", "")]), txt: "Niet 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", "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", "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", "guided"), new Tag("fee", "")]), txt: "Enkel met een gids of tijdens een activiteit toegankelijk"},
{ {
k: new And([new Tag("access", "yes"), k: new And([new Tag("access", "yes"),
new Tag("fee", "yes")]), new Tag("fee", "yes")]),

View file

@ -1,4 +1,4 @@
import {Tag} from "../../Logic/Tags"; import {RegexTag, Tag} from "../../Logic/Tags";
import Translations from "../../UI/i18n/Translations"; import Translations from "../../UI/i18n/Translations";
import {TagRenderingOptions} from "../TagRenderingOptions"; import {TagRenderingOptions} from "../TagRenderingOptions";
import Translation from "../../UI/i18n/Translation"; import Translation from "../../UI/i18n/Translation";
@ -8,18 +8,10 @@ export class NameInline extends TagRenderingOptions{
constructor(category: string | Translation ) { constructor(category: string | Translation ) {
super({ super({
question: "",
freeform: {
renderTemplate: "{name}",
template: Translations.t.general.nameInlineQuestion.Subs({category: category}),
key: "name",
extraTags: new Tag("noname", "") // Remove 'noname=yes'
},
mappings: [ mappings: [
{k: new Tag("noname","yes"), txt: Translations.t.general.noNameCategory.Subs({category: category})}, {k: new Tag("noname", "yes"), txt: Translations.t.general.noNameCategory.Subs({category: category})},
{k: null, txt: category} {k: new RegexTag("name", /.+/), txt: "{name}"},
{k:new Tag("name",""), txt: category}
] ]
}); });
} }

View file

@ -3,12 +3,13 @@
* One is a big 'name-question', the other is the 'edit name' in the title. * One is a big 'name-question', the other is the 'edit name' in the title.
* THis one is the big question * THis one is the big question
*/ */
import {Tag} from "../../Logic/Tags"; import {And, Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions"; import {TagRenderingOptions} from "../TagRenderingOptions";
export class NameQuestion extends TagRenderingOptions{ export class NameQuestion extends TagRenderingOptions {
static options = { constructor() {
super({
priority: 10, // Move this last on the priority list, in order to prevent ppl to enter access restrictions and descriptions priority: 10, // Move this last on the priority list, in order to prevent ppl to enter access restrictions and descriptions
question: "Wat is de <i>officiële</i> naam van dit gebied?<br><span class='question-subtext'>" + question: "Wat is de <i>officiële</i> naam van dit gebied?<br><span class='question-subtext'>" +
"Zelf een naam bedenken wordt afgeraden.<br/>" + "Zelf een naam bedenken wordt afgeraden.<br/>" +
@ -17,17 +18,14 @@ export class NameQuestion extends TagRenderingOptions{
freeform: { freeform: {
key: "name", key: "name",
template: "De naam is $$$", template: "De naam is $$$",
renderTemplate: "", // We don't actually render it, only ask renderTemplate: "Dit gebied heet <i>{name}</i>",
placeholder: "", placeholder: "",
extraTags: new Tag("noname","") extraTags: new Tag("noname", "")
}, },
mappings: [ mappings: [
{k: new Tag("noname", "yes"), txt: "Dit gebied heeft geen naam"}, {k: new And([new Tag("name", ""), new Tag("noname", "yes")]), txt: "Dit gebied heeft geen naam"},
] ]
} });
constructor() {
super(NameQuestion.options);
} }
} }

View file

@ -1,5 +1,5 @@
import {Img} from "../../UI/Img"; import {Img} from "../../UI/Img";
import {Tag} from "../../Logic/Tags"; import {RegexTag, Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions"; import {TagRenderingOptions} from "../TagRenderingOptions";
@ -18,7 +18,7 @@ export class OsmLink extends TagRenderingOptions {
placeholder: "", placeholder: "",
}, },
mappings: [ mappings: [
{k: new Tag("id", "node/-1"), txt: "<span class='alert'>Uploading</span>"} {k: new RegexTag("id", /node\/-.+/), txt: "<span class='alert'>Uploading</span>"}
] ]
} }

View file

@ -18,15 +18,14 @@ import Translation from "../UI/i18n/Translation";
import Combine from "../UI/Base/Combine"; import Combine from "../UI/Base/Combine";
export class export class TagRendering extends UIElement implements TagDependantUIElement {
TagRendering extends UIElement implements TagDependantUIElement {
private readonly _priority: number; private readonly _priority: number;
private readonly _question: string | Translation; private readonly _question: string | Translation;
private readonly _mapping: { k: TagsFilter, txt: string | UIElement, priority?: number }[]; private readonly _mapping: { k: TagsFilter, txt: string | UIElement, priority?: number }[];
private currentTags : UIEventSource<any> ; private currentTags: UIEventSource<any>;
private readonly _freeform: { private readonly _freeform: {
@ -110,7 +109,6 @@ TagRendering extends UIElement implements TagDependantUIElement {
for (const choice of options.mappings ?? []) { for (const choice of options.mappings ?? []) {
let choiceSubbed = { let choiceSubbed = {
k: choice.k?.substituteValues(this.currentTags.data), k: choice.k?.substituteValues(this.currentTags.data),
txt: choice.txt, txt: choice.txt,
@ -225,7 +223,7 @@ TagRendering extends UIElement implements TagDependantUIElement {
} }
previousTexts.push(this.ApplyTemplate(mapping.txt)); previousTexts.push(this.ApplyTemplate(mapping.txt));
elements.push(this.InputElementForMapping(mapping)); elements.push(this.InputElementForMapping(mapping, mapping.substitute));
} }
} }
@ -247,14 +245,26 @@ TagRendering extends UIElement implements TagDependantUIElement {
} }
private InputElementForMapping(mapping: { k: TagsFilter, txt: (string | Translation) }) { private InputElementForMapping(mapping: { k: TagsFilter, txt: (string | Translation) }, substituteValues: boolean) {
if (substituteValues) {
return new FixedInputElement(this.ApplyTemplate(mapping.txt), return new FixedInputElement(this.ApplyTemplate(mapping.txt),
mapping.k.substituteValues(this.currentTags.data) mapping.k.substituteValues(this.currentTags.data),
(t0, t1) => t0.isEquivalent(t1)
); );
} }
return new FixedInputElement(this.ApplyTemplate(mapping.txt),mapping.k,
(t0, t1) => t0.isEquivalent(t1));
}
private InputForFreeForm(freeform): InputElement<TagsFilter> { private InputForFreeForm(freeform : {
key: string,
template: string | Translation,
renderTemplate: string | Translation,
placeholder?: string | Translation,
extraTags?: TagsFilter,
}): InputElement<TagsFilter> {
if (freeform?.template === undefined) { if (freeform?.template === undefined) {
return undefined; return undefined;
} }
@ -283,7 +293,7 @@ TagRendering extends UIElement implements TagDependantUIElement {
const tag = new Tag(freeform.key, formatter(string, this._source.data._country)); const tag = new Tag(freeform.key, formatter(string, this._source.data._country));
if (tag.value.length > 255) { if (tag.value.length > 255) {
return undefined; // Toolong return undefined; // Too long
} }
if (freeform.extraTags === undefined) { if (freeform.extraTags === undefined) {
@ -299,7 +309,13 @@ TagRendering extends UIElement implements TagDependantUIElement {
const toString = const toString =
(tag) => { (tag) => {
if (tag instanceof And) { if (tag instanceof And) {
return toString(tag.and[0]) for (const subtag of tag.and) {
if(subtag instanceof Tag && subtag.key === freeform.key){
return subtag.value;
}
}
return undefined;
} else if (tag instanceof Tag) { } else if (tag instanceof Tag) {
return tag.value return tag.value
} }

View file

@ -222,9 +222,14 @@ export class FilteredLayer {
color: style.color color: style.color
}); });
} else if (style.icon.iconUrl.startsWith("$circle ")) {
marker = L.circle(latLng, {
radius: 25,
color: style.color
});
} else { } else {
if(style.icon.iconSize === undefined){ if (style.icon.iconSize === undefined) {
style.icon.iconSize = [50,50] style.icon.iconSize = [50, 50]
} }
marker = L.marker(latLng, { marker = L.marker(latLng, {
@ -253,11 +258,7 @@ export class FilteredLayer {
} }
} else { } else {
self._geolayer.setStyle(function (featureX) { self._geolayer.setStyle(function (featureX) {
const style = self._style(featureX.properties); return self._style(featureX.properties);
if (featureX === feature) {
console.log("Selected element is", featureX.properties.id)
}
return style;
}); });
} }
} }

View file

@ -4,7 +4,9 @@ export abstract class TagsFilter {
abstract matches(tags: { k: string, v: string }[]): boolean abstract matches(tags: { k: string, v: string }[]): boolean
abstract asOverpass(): string[] abstract asOverpass(): string[]
abstract substituteValues(tags: any) : TagsFilter; abstract substituteValues(tags: any) : TagsFilter;
abstract isUsableAsAnswer() : boolean; abstract isUsableAsAnswer(): boolean;
abstract isEquivalent(other: TagsFilter): boolean;
matchesProperties(properties: Map<string, string>): boolean { matchesProperties(properties: Map<string, string>): boolean {
return this.matches(TagUtils.proprtiesToKV(properties)); return this.matches(TagUtils.proprtiesToKV(properties));
@ -58,7 +60,7 @@ export class RegexTag extends TagsFilter {
return this.invert; return this.invert;
} }
substituteValues(tags: any) : TagsFilter{ substituteValues(tags: any): TagsFilter {
return this; return this;
} }
@ -68,6 +70,16 @@ export class RegexTag extends TagsFilter {
} }
return `~${this.key.source}${this.invert ? "!" : ""}~${RegexTag.source(this.value)}` return `~${this.key.source}${this.invert ? "!" : ""}~${RegexTag.source(this.value)}`
} }
isEquivalent(other: TagsFilter): boolean {
if (other instanceof RegexTag) {
return other.asHumanString() == this.asHumanString();
}
if(other instanceof Tag){
return RegexTag.doesMatch(other.key, this.key) && RegexTag.doesMatch(other.value, this.value);
}
return false;
}
} }
@ -140,6 +152,16 @@ export class Tag extends TagsFilter {
isUsableAsAnswer(): boolean { isUsableAsAnswer(): boolean {
return true; return true;
} }
isEquivalent(other: TagsFilter): boolean {
if(other instanceof Tag){
return this.key === other.key && this.value === other.value;
}
if(other instanceof RegexTag){
other.isEquivalent(this);
}
return false;
}
} }
@ -187,6 +209,24 @@ export class Or extends TagsFilter {
isUsableAsAnswer(): boolean { isUsableAsAnswer(): boolean {
return false; return false;
} }
isEquivalent(other: TagsFilter): boolean {
if(other instanceof Or){
for (const selfTag of this.or) {
let matchFound = false;
for (let i = 0; i < other.or.length && !matchFound; i++){
let otherTag = other.or[i];
matchFound = selfTag.isEquivalent(otherTag);
}
if(!matchFound){
return false;
}
}
return true;
}
return false;
}
} }
@ -256,6 +296,24 @@ export class And extends TagsFilter {
} }
return true; return true;
} }
isEquivalent(other: TagsFilter): boolean {
if(other instanceof And){
for (const selfTag of this.and) {
let matchFound = false;
for (let i = 0; i < other.and.length && !matchFound; i++){
let otherTag = other.and[i];
matchFound = selfTag.isEquivalent(otherTag);
}
if(!matchFound){
return false;
}
}
return true;
}
return false;
}
} }

View file

@ -23,7 +23,7 @@ export class State {
// The singleton of the global state // The singleton of the global state
public static state: State; public static state: State;
public static vNumber = "0.0.7j"; public static vNumber = "0.0.7k";
// The user journey states thresholds when a new feature gets unlocked // The user journey states thresholds when a new feature gets unlocked
public static userJourney = { public static userJourney = {

View file

@ -10,6 +10,8 @@ import {UserDetails} from "../../Logic/Osm/OsmConnection";
import {MultiInput} from "../Input/MultiInput"; import {MultiInput} from "../Input/MultiInput";
import TagRenderingPanel from "./TagRenderingPanel"; import TagRenderingPanel from "./TagRenderingPanel";
import SingleSetting from "./SingleSetting"; import SingleSetting from "./SingleSetting";
import {VariableUiElement} from "../Base/VariableUIElement";
import {FromJSON} from "../../Customizations/JSON/FromJSON";
export default class AllLayersPanel extends UIElement { export default class AllLayersPanel extends UIElement {
@ -49,9 +51,22 @@ export default class AllLayersPanel extends UIElement {
const layers = this._config.data.layers; const layers = this._config.data.layers;
for (let i = 0; i < layers.length; i++) { for (let i = 0; i < layers.length; i++) {
tabs.push({ tabs.push({
header: "<img src='./assets/bug.svg'>", header: new VariableUiElement(this._config.map((config: LayoutConfigJson) => {
const layer = config.layers[i];
if (typeof layer !== "string") {
try {
const iconTagRendering = FromJSON.TagRendering(layer.icon, "icon");
const icon = iconTagRendering.GetContent({"id": "node/-1"}).txt;
return `<img src='${icon}'>`
} catch (e) {
return "<img src='./assets/bug.svg'>"
// Nothing to do here
}
}
return "<img src='./assets/help.svg'>"
})),
content: new LayerPanelWithPreview(this._config, this.languages, i, userDetails) content: new LayerPanelWithPreview(this._config, this.languages, i, userDetails)
}); });
} }

View file

@ -12,6 +12,7 @@ export class GenerateEmpty {
title: {}, title: {},
description: {}, description: {},
tagRenderings: [], tagRenderings: [],
hideUnderlayingFeaturesMinPercentage: 0,
icon: { icon: {
render: "./assets/bug.svg" render: "./assets/bug.svg"
}, },

View file

@ -96,7 +96,10 @@ export default class LayerPanel extends UIElement {
{value: 2, shown: "Show both the ways/areas and the centerpoints"}, {value: 2, shown: "Show both the ways/areas and the centerpoints"},
{value: 1, shown: "Show everything as centerpoint"}]), "wayHandling", "Way handling", {value: 1, shown: "Show everything as centerpoint"}]), "wayHandling", "Way handling",
"Describes how ways and areas are represented on the map: areas can be represented as the area itself, or it can be converted into the centerpoint"), "Describes how ways and areas are represented on the map: areas can be represented as the area itself, or it can be converted into the centerpoint"),
setting(TextField.NumberInput("nat", n => n <= 100), "hideUnderlayingFeaturesMinPercentage", "Max allowed overlap percentage",
"Consider that we want to show 'Nature Reserves' and 'Forests'. Now, ofter, there are pieces of forest mapped _in_ the nature reserve.<br/>" +
"Now, showing those pieces of forest overlapping with the nature reserve truly clutters the map and is very user-unfriendly.<br/>" +
"The features are placed layer by layer. If a feature below a feature on this layer overlaps for more then 'x'-percent, the underlying feature is hidden."),
setting(new AndOrTagInput(), "overpassTags", "Overpass query", setting(new AndOrTagInput(), "overpassTags", "Overpass query",
"The tags of the objects to load from overpass"), "The tags of the objects to load from overpass"),

View file

@ -13,7 +13,6 @@ import {UserDetails} from "../../Logic/Osm/OsmConnection";
export default class LayerPanelWithPreview extends UIElement{ export default class LayerPanelWithPreview extends UIElement{
private panel: UIElement; private panel: UIElement;
constructor(config: UIEventSource<any>, languages: UIEventSource<string[]>, index: number, userDetails: UserDetails) { constructor(config: UIEventSource<any>, languages: UIEventSource<string[]>, index: number, userDetails: UserDetails) {
super(); super();

View file

@ -91,7 +91,7 @@ export default class TagRenderingPanel extends InputElement<TagRenderingConfigJs
"<h3>Mappings</h3>", "<h3>Mappings</h3>",
setting(new MultiInput<{ if: AndOrTagConfigJson, then: (string | any), hideInAnswer?: boolean }>("Add a mapping", setting(new MultiInput<{ if: AndOrTagConfigJson, then: (string | any), hideInAnswer?: boolean }>("Add a mapping",
() => ({if: undefined, then: undefined}), () => ({if: {and: []}, then: {}}),
() => new MappingInput(languages, options?.disableQuestions ?? false), () => new MappingInput(languages, options?.disableQuestions ?? false),
undefined, {allowMovement: true}), "mappings", undefined, {allowMovement: true}), "mappings",
"If a tag matches, then show the first respective text", "") "If a tag matches, then show the first respective text", "")

View file

@ -7,9 +7,13 @@ export class FixedInputElement<T> extends InputElement<T> {
private readonly rendering: UIElement; private readonly rendering: UIElement;
private readonly value: UIEventSource<T>; private readonly value: UIEventSource<T>;
public readonly IsSelected : UIEventSource<boolean> = new UIEventSource<boolean>(false); public readonly IsSelected : UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _comparator: (t0: T, t1: T) => boolean;
constructor(rendering: UIElement | string, value: T) { constructor(rendering: UIElement | string,
value: T,
comparator: ((t0: T, t1: T) => boolean ) = undefined) {
super(undefined); super(undefined);
this._comparator = comparator ?? ((t0, t1) => t0 == t1);
this.value = new UIEventSource<T>(value); this.value = new UIEventSource<T>(value);
this.rendering = typeof (rendering) === 'string' ? new FixedUiElement(rendering) : rendering; this.rendering = typeof (rendering) === 'string' ? new FixedUiElement(rendering) : rendering;
} }
@ -22,7 +26,9 @@ export class FixedInputElement<T> extends InputElement<T> {
} }
IsValid(t: T): boolean { IsValid(t: T): boolean {
return t == this.value.data;
console.log("Comparing ",t, "with", this.value.data);
return this._comparator(t, this.value.data);
} }
protected InnerUpdate(htmlElement: HTMLElement) { protected InnerUpdate(htmlElement: HTMLElement) {

View file

@ -8,14 +8,15 @@ export class RadioButton<T> extends InputElement<T> {
private readonly _selectedElementIndex: UIEventSource<number> private readonly _selectedElementIndex: UIEventSource<number>
= new UIEventSource<number>(null); = new UIEventSource<number>(null);
private value: UIEventSource<T>; private readonly value: UIEventSource<T>;
private readonly _elements: InputElement<T>[] private readonly _elements: InputElement<T>[]
private _selectFirstAsDefault: boolean; private readonly _selectFirstAsDefault: boolean;
constructor(elements: InputElement<T>[], constructor(elements: InputElement<T>[],
selectFirstAsDefault = true) { selectFirstAsDefault = true) {
super(undefined); super(undefined);
console.log("Created new radiobutton with values ", elements)
this._elements = Utils.NoNull(elements); this._elements = Utils.NoNull(elements);
this._selectFirstAsDefault = selectFirstAsDefault; this._selectFirstAsDefault = selectFirstAsDefault;
const self = this; const self = this;

View file

@ -54,6 +54,9 @@ export class SimpleAddUI extends UIElement {
if (typeof (preset.icon) !== "string") { if (typeof (preset.icon) !== "string") {
const tags = Utils.MergeTags(TagUtils.KVtoProperties(preset.tags), {id:"node/-1"}); const tags = Utils.MergeTags(TagUtils.KVtoProperties(preset.tags), {id:"node/-1"});
icon = preset.icon.GetContent(tags).txt; icon = preset.icon.GetContent(tags).txt;
if(icon.startsWith("$")){
icon = undefined;
}
} else { } else {
icon = preset.icon; icon = preset.icon;
} }

View file

@ -14,6 +14,8 @@ export abstract class UIElement extends UIEventSource<string> {
public dumbMode = false; public dumbMode = false;
private lastInnerRender: string;
/** /**
* In the 'deploy'-step, some code needs to be run by ts-node. * In the 'deploy'-step, some code needs to be run by ts-node.
* However, ts-node crashes when it sees 'document'. When running from console, we flag this and disable all code where document is needed. * However, ts-node crashes when it sees 'document'. When running from console, we flag this and disable all code where document is needed.
@ -37,6 +39,7 @@ export abstract class UIElement extends UIEventSource<string> {
this.dumbMode = false; this.dumbMode = false;
const self = this; const self = this;
source.addCallback(() => { source.addCallback(() => {
self.lastInnerRender = undefined;
self.Update(); self.Update();
}) })
return this; return this;
@ -92,7 +95,7 @@ export abstract class UIElement extends UIEventSource<string> {
return; return;
} }
this.setData(this.InnerRender()); this.setData(this.lastInnerRender ?? this.InnerRender());
element.innerHTML = this.data; element.innerHTML = this.data;
if (this._hideIfEmpty) { if (this._hideIfEmpty) {
@ -151,14 +154,15 @@ export abstract class UIElement extends UIEventSource<string> {
} }
Render(): string { Render(): string {
this.lastInnerRender = this.lastInnerRender ?? this.InnerRender();
if (this.dumbMode) { if (this.dumbMode) {
return this.InnerRender(); return this.lastInnerRender;
} }
let style = ""; let style = "";
if (this.style !== undefined && this.style !== "") { if (this.style !== undefined && this.style !== "") {
style = `style="${this.style}"`; style = `style="${this.style}"`;
} }
return `<span class='uielement ${this.clss.join(" ")}' ${style} id='${this.id}'>${this.InnerRender()}</span>` return `<span class='uielement ${this.clss.join(" ")}' ${style} id='${this.id}'>${this.lastInnerRender}</span>`
} }
AttachTo(divId: string) { AttachTo(divId: string) {

View file

@ -52,7 +52,12 @@
"if": { "if": {
"and": [ "and": [
"service:bicycle:pump:operational_status=broken", "service:bicycle:pump:operational_status=broken",
"service:bicycle:tools=no" {
"or": [
"service:bicycle:tools=no",
"service:bicycle:tools="
]
}
] ]
}, },
"then": { "then": {
@ -381,7 +386,12 @@
"if": { "if": {
"and": [ "and": [
"service:bicycle:pump=yes", "service:bicycle:pump=yes",
"service:bicycle:tools=no" {
"or": [
"service:bicycle:tools=no",
"service:bicycle:tools="
]
}
] ]
}, },
"then": "./assets/layers/bike_repair_station/pump.svg" "then": "./assets/layers/bike_repair_station/pump.svg"

View file

@ -0,0 +1,252 @@
{
"id": "nature_reserve_simple",
"name": "Layer",
"minzoom": 10,
"overpassTags": {
"or": [
"leisure=nature_reserve",
"boundary=protected_area"
]
},
"title": {
"mappings": [
{
"if": {
"and": [
"name~*"
]
},
"then": {
"nl": "Natuurgebied <i>{name}</i>"
}
}
],
"render": {
"nl": "Natuurgebied"
}
},
"description": {
"nl": "Een natuurreservaat is een gebied dat wordt beheerd door Natuurpunt, ANB of een privépersoon zodat deze biodiversiteit bevordert. "
},
"tagRenderings": [
{
"question": {
"nl": "Is dit gebied vrij toegankelijk?"
},
"freeform": {
"key": "access:description",
"addExtraTags": []
},
"render": {
"nl": "De toegankelijkheid van dit gebied is {access:description}"
},
"mappings": [
{
"if": {
"and": [
"access=yes",
"fee="
]
},
"then": {
"nl": "Publiek toegankelijk"
}
},
{
"if": {
"and": [
"access=no",
"fee="
]
},
"then": {
"nl": "Niet publiek toegankelijk"
}
},
{
"if": {
"and": [
"access=guided",
"fee="
]
},
"then": {
"nl": "Enkel met gids of op activiteit"
}
},
{
"if": {
"and": [
"access=private",
"fee="
]
},
"then": {
"nl": "Niet toegankelijk privégebied"
}
},
{
"if": {
"and": [
"access=permissive",
"fee="
]
},
"then": {
"nl": "Toegankelijk, maar het is privégebied"
}
},
{
"if": {
"and": [
"access=yes",
"fee=yes"
]
},
"then": {
"nl": "Toegankelijk mits betaling"
}
}
]
},
{
"render": {
"nl": "Beheer door {operator}"
},
"question": {
"nl": "Wie beheert dit natuurgebied?"
},
"freeform": {
"key": "operator"
},
"mappings": [
{
"if": {
"and": [
"operator=Natuurpunt"
]
},
"then": {
"nl": "Beheer door Natuurpunt"
}
},
{
"if": {
"and": [
"operator=Agenstchap Natuur en Bos"
]
},
"then": {
"nl": "Beheer door het Agentschap Natuur en Bos (ANB)"
}
}
]
},
{
"question": {
"nl": "<b>Wat is de <i>officiële</i> naam van dit natuurgebied?</b><br/><span class='subtle'>Sommige gebieden hebben geen naam</span>"
},
"freeform": {
"key": "name",
"addExtraTags": [
"noname="
]
},
"render": {
"nl": "Dit gebied heet <i>{name}</i>"
},
"mappings": [
{
"if": {
"and": [
"noname=yes",
"name="
]
},
"then": {
"nl": "Dit gebied heeft geen naam"
}
}
],
"condition": {
"and": [
"name:nl="
]
}
},
{
"render": {
"nl": "De naam van dit gebied is {name:nl}"
},
"freeform": {
"key": "name:nl"
},
"question": {
"nl": "Wat is de Nederlandstalige naam van dit gebied?"
},
"condition": {
"and": [
"name:nl~*"
]
}
},
{
"render": {
"nl": "Meer uitleg:<br/><i>{description:0}</i>"
},
"question": {
"nl": "Zijn er nog opmerkingen of vermeldenswaardigheden?"
},
"freeform": {
"key": "description:0"
}
}
],
"icon": {
"render": "$circle"
},
"width": {
"render": "3"
},
"iconSize": {
"render": "40,40,center"
},
"color": {
"render": "#c90014",
"mappings": [
{
"if": {
"and": [
"name~*",
"operator~*",
"access~*"
]
},
"then": "#37c65b"
},
{
"if": {
"and": [
"name~*",
"access~*"
]
},
"then": "#c98d00"
}
]
},
"presets": [
{
"tags": [
"leisure=nature_reserve"
],
"title": {
"nl": "Natuurreservaat"
},
"description": {
"nl": "Voeg een ontbrekend, erkend natuurreservaat toe, bv. een gebied dat beheerd wordt door het ANB of natuurpunt"
}
}
],
"hideUnderlayingFeaturesMinPercentage": 10
}

View file

@ -54,14 +54,22 @@ new T([
} }
], ],
condition: "x=" condition: "x="
}); }, "");
equal(true, tr.IsKnown({"noname": "yes"})); equal(true, tr.IsKnown({"noname": "yes"}));
equal(true, tr.IsKnown({"name": "ABC"})); equal(true, tr.IsKnown({"name": "ABC"}));
equal(false, tr.IsKnown({"foo": "bar"})); equal(false, tr.IsKnown({"foo": "bar"}));
equal("Has no name", tr.GetContent({"noname": "yes"})); equal("Has no name", tr.GetContent({"noname": "yes"})?.txt);
equal("Ook een xyz", tr.GetContent({"name": "xyz"})); equal("Ook een xyz", tr.GetContent({"name": "xyz"})?.txt);
equal("Ook een {name}", tr.GetContent({"foo": "bar"})); equal(undefined, tr.GetContent({"foo": "bar"}));
})],
[
"Select right value test",
() => {
}
]
})]
]); ]);