Huge refactoring (WIP)

This commit is contained in:
Pieter Vander Vennet 2020-10-27 01:01:34 +01:00
parent 62c4f0a928
commit 895aa132ec
55 changed files with 1177 additions and 2190 deletions

View file

@ -1,4 +1,3 @@
import {LayerDefinition} from "./LayerDefinition";
import {Layout} from "./Layout";
import {FromJSON} from "./JSON/FromJSON";
import * as bookcases from "../assets/themes/bookcases/Bookcases.json";
@ -18,14 +17,15 @@ import * as benches from "../assets/themes/benches/benches.json";
import * as charging_stations from "../assets/themes/charging_stations/charging_stations.json"
import {PersonalLayout} from "../Logic/PersonalLayout";
import {StreetWidth} from "./StreetWidth/StreetWidth";
import LayerConfig from "./JSON/LayerConfig";
import SharedLayers from "./SharedLayers";
export class AllKnownLayouts {
public static allLayers: Map<string, LayerDefinition> = undefined;
public static allLayers: Map<string, LayerConfig> = undefined;
private static GenerateCycloFix(): Layout {
const layout = FromJSON.LayoutFromJSON(cyclofix)
const layout = Layout.LayoutFromJSON(cyclofix, SharedLayers.sharedLayers)
const now = new Date();
const m = now.getMonth() + 1;
const day = new Date().getDay() + 1;
@ -33,7 +33,7 @@ export class AllKnownLayouts {
if (date === "31/10" || date === "1/11" || date === "2/11") {
// Around Halloween/Fiesta de muerte/Allerzielen, we remember the dead
layout.layers.push(
FromJSON.sharedLayers.get("ghost_bike")
SharedLayers.sharedLayers.get("ghost_bike")
);
}
@ -42,7 +42,7 @@ export class AllKnownLayouts {
}
private static GenerateBuurtNatuur(): Layout {
const layout = FromJSON.LayoutFromJSON(buurtnatuur);
const layout = Layout.LayoutFromJSON(buurtnatuur, SharedLayers.sharedLayers);
layout.enableMoreQuests = false;
layout.enableShareScreen = false;
layout.hideFromOverview = true;
@ -50,7 +50,7 @@ export class AllKnownLayouts {
}
private static GenerateBikeMonitoringStations(): Layout {
const layout = FromJSON.LayoutFromJSON(bike_monitoring_stations);
const layout = Layout.LayoutFromJSON(bike_monitoring_stations, SharedLayers.sharedLayers);
layout.hideFromOverview = true;
return layout;
}
@ -60,37 +60,36 @@ export class AllKnownLayouts {
public static layoutsList: Layout[] = [
new PersonalLayout(),
FromJSON.LayoutFromJSON(shops),
FromJSON.LayoutFromJSON(bookcases),
FromJSON.LayoutFromJSON(aed),
FromJSON.LayoutFromJSON(toilets),
FromJSON.LayoutFromJSON(artworks),
Layout.LayoutFromJSON(shops, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(bookcases, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(aed, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(toilets, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(artworks, SharedLayers.sharedLayers),
AllKnownLayouts.GenerateCycloFix(),
FromJSON.LayoutFromJSON(ghostbikes),
FromJSON.LayoutFromJSON(nature),
FromJSON.LayoutFromJSON(cyclestreets),
FromJSON.LayoutFromJSON(maps),
FromJSON.LayoutFromJSON(fritures),
FromJSON.LayoutFromJSON(benches),
FromJSON.LayoutFromJSON(charging_stations),
Layout.LayoutFromJSON(ghostbikes, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(nature, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(cyclestreets, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(maps, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(fritures, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(benches, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(charging_stations, SharedLayers.sharedLayers),
AllKnownLayouts.GenerateBuurtNatuur(),
AllKnownLayouts.GenerateBikeMonitoringStations(),
new StreetWidth(), // The ugly duckling
];
public static allSets: Map<string, Layout> = AllKnownLayouts.AllLayouts();
private static AllLayouts(): Map<string, Layout> {
this.allLayers = new Map<string, LayerDefinition>();
this.allLayers = new Map<string, LayerConfig>();
for (const layout of this.layoutsList) {
for (let i = 0; i < layout.layers.length; i++) {
let layer = layout.layers[i];
if (typeof (layer) === "string") {
layer = layout.layers[i] = FromJSON.sharedLayers.get(layer);
layer = layout.layers[i] = SharedLayers.sharedLayers.get(layer);
if(layer === undefined){
console.log("Defined layers are ", FromJSON.sharedLayers.keys())
console.log("Defined layers are ", SharedLayers.sharedLayers.keys())
throw `Layer ${layer} was not found or defined - probably a type was made`
}
}

View file

@ -1,102 +1,11 @@
import {Layout} from "../Layout";
import {LayoutConfigJson} from "./LayoutConfigJson";
import {AndOrTagConfigJson} from "./TagConfigJson";
import {And, Or, RegexTag, Tag, TagsFilter} from "../../Logic/Tags";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import {TagRenderingOptions} from "../TagRenderingOptions";
import Translation from "../../UI/i18n/Translation";
import {LayerConfigJson} from "./LayerConfigJson";
import {LayerDefinition, Preset} from "../LayerDefinition";
import {TagDependantUIElementConstructor} from "../UIElementConstructor";
import Combine from "../../UI/Base/Combine";
import * as drinkingWater from "../../assets/layers/drinking_water/drinking_water.json";
import * as ghostbikes from "../../assets/layers/ghost_bike/ghost_bike.json"
import * as viewpoint from "../../assets/layers/viewpoint/viewpoint.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 birdhides from "../../assets/layers/bird_hide/birdhides.json"
import * as nature_reserve from "../../assets/layers/nature_reserve/nature_reserve.json"
import * as bike_cafes from "../../assets/layers/bike_cafe/bike_cafes.json"
import * as bike_monitoring_station from "../../assets/layers/bike_monitoring_station/bike_monitoring_station.json"
import * as cycling_themed_objects from "../../assets/layers/cycling_themed_object/cycling_themed_objects.json"
import * as bike_shops from "../../assets/layers/bike_shop/bike_shop.json"
import * as maps from "../../assets/layers/maps/maps.json"
import * as information_boards from "../../assets/layers/information_board/information_board.json"
import {Utils} from "../../Utils";
import State from "../../State";
export class FromJSON {
public static sharedLayers: Map<string, LayerDefinition> = FromJSON.getSharedLayers();
private static getSharedLayers() {
// We inject a function into state while we are busy
State.FromBase64 = FromJSON.FromBase64;
const sharedLayers = new Map<string, LayerDefinition>();
const sharedLayersList = [
FromJSON.Layer(drinkingWater),
FromJSON.Layer(ghostbikes),
FromJSON.Layer(viewpoint),
FromJSON.Layer(bike_parking),
FromJSON.Layer(bike_repair_station),
FromJSON.Layer(bike_monitoring_station),
FromJSON.Layer(birdhides),
FromJSON.Layer(nature_reserve),
FromJSON.Layer(bike_cafes),
FromJSON.Layer(cycling_themed_objects),
FromJSON.Layer(bike_shops),
FromJSON.Layer(maps),
FromJSON.Layer(information_boards)
];
for (const layer of sharedLayersList) {
sharedLayers.set(layer.id, layer);
}
return sharedLayers;
}
public static FromBase64(layoutFromBase64: string): Layout {
return FromJSON.LayoutFromJSON(JSON.parse(atob(layoutFromBase64)));
}
public static LayoutFromJSON(json: LayoutConfigJson): Layout {
const tr = FromJSON.Translation;
const layers = json.layers.map(FromJSON.Layer);
const roaming: TagDependantUIElementConstructor[] = json.roamingRenderings?.map((tr, i) => FromJSON.TagRendering(tr, "Roaming rendering "+i)) ?? [];
for (const layer of layers) {
layer.elementsToShow.push(...roaming);
}
const layout = new Layout(
json.id,
typeof (json.language) === "string" ? [json.language] : json.language,
tr(json.title ?? "Title not defined"),
layers,
json.startZoom,
json.startLat,
json.startLon,
new Combine(["<h3>", tr(json.title), "</h3>", tr(json.description)]),
undefined,
undefined,
tr(json.descriptionTail)
);
layout.defaultBackground = json.defaultBackgroundId ?? "osm";
layout.widenFactor = json.widenFactor ?? 0.07;
layout.icon = json.icon;
layout.maintainer = json.maintainer;
layout.version = json.version;
layout.socialImage = json.socialImage;
layout.description = tr(json.shortDescription) ?? tr(json.description)?.FirstSentence();
layout.changesetMessage = json.changesetmessage;
return layout;
}
public static Translation(json: string | any): Translation {
if (json === undefined) {
@ -122,104 +31,6 @@ export class FromJSON {
return transl;
}
public static TagRendering(json: TagRenderingConfigJson | string, propertyeName: string): TagDependantUIElementConstructor {
return FromJSON.TagRenderingWithDefault(json, propertyeName, undefined);
}
public static TagRenderingWithDefault(json: TagRenderingConfigJson | string, propertyName, defaultValue: string): TagDependantUIElementConstructor {
if (json === undefined) {
if(defaultValue !== undefined){
return FromJSON.TagRendering(defaultValue, propertyName);
}
throw `Tagrendering ${propertyName} is undefined...`
}
if (typeof json === "string") {
switch (json) {
case "pictures": {
json = "{image_carousel()}{image_upload()}";
break;
}
case "images": {
json = "{image_carousel()}{image_upload()}";
}
}
return new TagRenderingOptions({
freeform: {
key: "id",
renderTemplate: json,
template: "$$$"
}
});
}
// It's the question that drives us, neo
const question = FromJSON.Translation(json.question);
let template = FromJSON.Translation(json.render);
let freeform = undefined;
if (json.freeform?.key && json.freeform.key !== "") {
// Setup the freeform
if (template === undefined) {
console.error("Freeform.key is defined, but render is not. This is not allowed.", json)
throw `Freeform is defined in tagrendering ${propertyName}, but render is not. This is not allowed.`
}
freeform = {
template: `$${json.freeform.type ?? "string"}$`,
renderTemplate: template,
key: json.freeform.key
};
if (json.freeform.addExtraTags) {
freeform.extraTags = new And(json.freeform.addExtraTags.map(FromJSON.SimpleTag))
}
} else if (json.render) {
// Template (aka rendering) is defined, but freeform.key is not. We allow an input as string
freeform = {
template: undefined, // Template to ask is undefined -> we block asking for this key
renderTemplate: template,
key: "id" // every object always has an id
}
}
const mappings = json.mappings?.map((mapping, i) => {
const k = FromJSON.Tag(mapping.if, `IN mapping #${i} of tagrendering ${propertyName}`)
if (question !== undefined && !mapping.hideInAnswer && !k.isUsableAsAnswer()) {
throw `Invalid mapping in ${propertyName}.${i}: this mapping uses a regex tag or an OR, but is also answerable. Either mark 'Not an answer option' or only use '=' to map key/values.`
}
return {
k: k,
txt: FromJSON.Translation(mapping.then),
hideInAnswer: mapping.hideInAnswer
};
}
);
if (template === undefined && (mappings === undefined || mappings.length === 0)) {
console.error(`Empty tagrendering detected in ${propertyName}: no mappings nor template given`, json)
throw `Empty tagrendering ${propertyName} detected: no mappings nor template given`
}
let rendering = new TagRenderingOptions({
question: question,
freeform: freeform,
mappings: mappings,
multiAnswer: json.multiAnswer
});
if (json.condition) {
const condition = FromJSON.Tag(json.condition, `In tagrendering ${propertyName}.condition`);
return rendering.OnlyShowIf(condition);
}
return rendering;
}
public static SimpleTag(json: string): Tag {
const tag = Utils.SplitFirst(json, "=");
return new Tag(tag[0], tag[1]);
@ -227,7 +38,7 @@ export class FromJSON {
public static Tag(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter {
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: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`
}
if (typeof (json) == "string") {
const tag = json as string;
@ -286,120 +97,4 @@ export class FromJSON {
return new Or(json.or.map(t => FromJSON.Tag(t, context)));
}
}
public static Layer(json: LayerConfigJson | string): LayerDefinition {
if (typeof (json) === "string") {
const cached = FromJSON.sharedLayers.get(json);
if (cached) {
return cached;
}
throw `Layer ${json} not yet loaded...`
}
try {
return FromJSON.LayerUncaught(json);
} catch (e) {
throw `While parsing layer ${json.id}: ${e}`
}
}
private static LayerUncaught(json: LayerConfigJson): LayerDefinition {
const tr = FromJSON.Translation;
const overpassTags = FromJSON.Tag(json.overpassTags, "overpasstags for layer "+json.id);
const icon = FromJSON.TagRenderingWithDefault(json.icon, "icon", "./assets/bug.svg");
const iconSize = FromJSON.TagRenderingWithDefault(json.iconSize, "iconSize", "40,40,center");
const color = FromJSON.TagRenderingWithDefault(json.color, "color", "#0000ff");
const width = FromJSON.TagRenderingWithDefault(json.width, "width", "10");
if (json.title === "Layer") {
json.title = {};
}
let title = FromJSON.TagRendering(json.title, "Popup title");
let tagRenderingDefs = json.tagRenderings ?? [];
let hasImageElement = false;
for (const tagRenderingDef of tagRenderingDefs) {
if (typeof tagRenderingDef !== "string") {
continue;
}
let str = tagRenderingDef as string;
if (tagRenderingDef.indexOf("images") >= 0 || str.indexOf("pictures") >= 0) {
hasImageElement = true;
break;
}
}
if (!hasImageElement) {
tagRenderingDefs = ["images", ...tagRenderingDefs];
}
let tagRenderings = tagRenderingDefs.map((tr, i) => FromJSON.TagRendering(tr, "Tagrendering #"+i));
const renderTags = {"id": "node/-1"}
const presets: Preset[] = json?.presets?.map(preset => {
return ({
title: tr(preset.title),
description: tr(preset.description),
tags: preset.tags.map(FromJSON.SimpleTag)
});
}) ?? [];
function style(tags) {
const iconSizeStr =
iconSize.GetContent(tags).txt.split(",");
const iconwidth = Number(iconSizeStr[0]);
const iconheight = Number(iconSizeStr[1]);
const iconmode = iconSizeStr[2];
const iconAnchor = [iconwidth / 2, iconheight / 2] // x, y
// If iconAnchor is set to [0,0], then the top-left of the icon will be placed at the geographical location
if (iconmode.indexOf("left") >= 0) {
iconAnchor[0] = 0;
}
if (iconmode.indexOf("right") >= 0) {
iconAnchor[0] = iconwidth;
}
if (iconmode.indexOf("top") >= 0) {
iconAnchor[1] = 0;
}
if (iconmode.indexOf("bottom") >= 0) {
iconAnchor[1] = iconheight;
}
// the anchor is always set from the center of the point
// x, y with x going right and y going down if the values are bigger
const popupAnchor = [0, 3 - iconAnchor[1]];
return {
color: color.GetContent(tags).txt,
weight: width.GetContent(tags).txt,
icon: {
iconUrl: icon.GetContent(tags).txt,
iconSize: [iconwidth, iconheight],
popupAnchor: popupAnchor,
iconAnchor: iconAnchor
},
}
}
const layer = new LayerDefinition(
json.id,
{
name: tr(json.name),
description: tr(json.description),
icon: icon.GetContent(renderTags).txt,
overpassFilter: overpassTags,
title: title,
minzoom: json.minzoom,
presets: presets,
elementsToShow: tagRenderings,
style: style,
wayHandling: json.wayHandling
}
);
layer.maxAllowedOverlapPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0;
return layer;
}
}

View file

@ -0,0 +1,114 @@
import Translation from "../../UI/i18n/Translation";
import TagRenderingConfig from "./TagRenderingConfig";
import {Tag, TagsFilter} from "../../Logic/Tags";
import {LayerConfigJson} from "./LayerConfigJson";
import Translations from "../../UI/i18n/Translations";
import {FromJSON} from "./FromJSON";
import SharedTagRenderings from "../SharedTagRenderings";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
export default class LayerConfig {
id: string;
name: Translation
description: Translation;
overpassTags: TagsFilter;
minzoom: number;
title: TagRenderingConfig;
titleIcons: TagRenderingConfig[];
icon?: TagRenderingConfig;
iconSize?: TagRenderingConfig;
color?: TagRenderingConfig;
width?: TagRenderingConfig;
wayHandling: number;
static WAYHANDLING_DEFAULT = 0;
static WAYHANDLING_CENTER_ONLY = 1;
static WAYHANDLING_CENTER_AND_WAY = 2;
hideUnderlayingFeaturesMinPercentage?: number;
presets: {
title: Translation,
tags: Tag[],
description?: Translation,
}[];
tagRenderings: TagRenderingConfig [];
constructor(json: LayerConfigJson, context?: string) {
context = context + "." + json.id;
this.id = json.id;
this.name = Translations.T(json.name);
this.description = Translations.T(json.name);
this.overpassTags = FromJSON.Tag(json.overpassTags, context + ".overpasstags");
this.minzoom = json.minzoom;
this.wayHandling = json.wayHandling ?? 0;
this.hideUnderlayingFeaturesMinPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0;
this.title = new TagRenderingConfig(json.title);
this.presets = (json.presets ?? []).map(pr => ({
title: Translations.T(pr.title),
tags: pr.tags.map(t => FromJSON.SimpleTag(t)),
description: Translations.T(pr.description)
}))
/**
* Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig
* A string is interpreted as a name to call
* @param tagRenderings
*/
function trs(tagRenderings?: (string | TagRenderingConfigJson)[]) {
if (tagRenderings === undefined) {
return [];
}
return tagRenderings.map(
(renderingJson, i) => {
if (typeof renderingJson === "string") {
const shared = SharedTagRenderings.SharedTagRendering[renderingJson];
if (shared !== undefined) {
return shared;
}
throw `Predefined tagRendering ${renderingJson} not found in ${context}`;
}
return new TagRenderingConfig(renderingJson, `${context}.tagrendering[${i}]`);
});
}
this.tagRenderings = trs(json.tagRenderings);
this.titleIcons = trs(json.titleIcons ?? ["wikipedialink","osmlink"]);
function tr(key, deflt) {
const v = json[key];
if (v === undefined) {
return new TagRenderingConfig(deflt);
}
if (typeof v === "string") {
const shared = SharedTagRenderings.SharedTagRendering[v];
if (shared) {
console.log("Got shared TR:", v, "-->", shared)
return shared;
}
}
return new TagRenderingConfig(v, context + "." + key);
}
this.title = tr("title", "");
this.icon = tr("icon", "./assets/bug.svg");
this.iconSize = tr("iconSize", "40,40,center");
this.color = tr("color", "#0000ff");
this.width = tr("width", "7");
}
}

View file

@ -37,9 +37,11 @@ export interface LayerConfigJson {
/**
* The title shown in a popup for elements of this layer
* The title shown in a popup for elements of this layer.
*/
title: string | TagRenderingConfigJson;
titleIcons?: (string | TagRenderingConfigJson)[];
/**
* The icon for an element.

View file

@ -0,0 +1,112 @@
import Translation from "../../UI/i18n/Translation";
import {TagsFilter} from "../../Logic/Tags";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import Translations from "../../UI/i18n/Translations";
import {FromJSON} from "./FromJSON";
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
/***
* The parsed version of TagRenderingConfigJSON
* Identical data, but with some methods and validation
*/
export default class TagRenderingConfig {
render?: Translation;
question?: Translation;
condition?: TagsFilter;
freeform?: {
key: string,
type: string,
addExtraTags: TagsFilter[];
};
multiAnswer: boolean;
mappings?: {
if: TagsFilter,
then: Translation
hideInAnswer: boolean
}[]
constructor(json: string | TagRenderingConfigJson, context?: string) {
if(json === undefined){
throw "Initing a TagRenderingConfig with undefined in "+context;
}
if (typeof json === "string") {
this.render = Translations.T(json);
this.multiAnswer = false;
return;
}
this.render = Translations.T(json.render);
this.question = Translations.T(json.question);
this.condition = FromJSON.Tag(json.condition ?? {"and": []}, `${context}.condition`);
if (json.freeform) {
this.freeform = {
key: json.freeform.key,
type: json.freeform.type ?? "string",
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? []
}
if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) {
throw `Freeform.key ${this.freeform.key} is an invalid type`
}
}
this.multiAnswer = json.multiAnswer ?? false
if (json.mappings) {
this.mappings = json.mappings.map((mapping, i) => {
if (mapping.then === undefined) {
throw "Invalid mapping: if without body"
}
return {
if: FromJSON.Tag(mapping.if, `${context}.mapping[${i}]`),
then: Translations.T(mapping.then),
hideInAnswer: mapping.hideInAnswer ?? false
};
});
}
if (this.question && this.freeform?.key === undefined && this.mappings === undefined) {
throw `A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}`
}
if (json.multiAnswer) {
if ((this.mappings?.length ?? 0) === 0) {
throw "MultiAnswer is set, but no mappings are defined"
}
}
}
/**
* Gets the correct rendering value (or undefined if not known)
* @constructor
*/
public GetRenderValue(tags: any): Translation {
if (this.mappings !== undefined && !this.multiAnswer) {
for (const mapping of this.mappings) {
if (mapping.if === undefined) {
return mapping.then;
}
if (mapping.if.matchesProperties(tags)) {
return mapping.then;
}
}
}
if (this.freeform?.key === undefined){
return this.render;
}
if (tags[this.freeform.key] !== undefined) {
return this.render;
}
return undefined;
}
}

View file

@ -1,133 +0,0 @@
import {Tag, TagsFilter} from "../Logic/Tags";
import {UIElement} from "../UI/UIElement";
import {TagDependantUIElementConstructor} from "./UIElementConstructor";
import {TagRenderingOptions} from "./TagRenderingOptions";
import Translation from "../UI/i18n/Translation";
export interface Preset {
tags: Tag[],
title: string | UIElement,
description?: string | UIElement,
icon?: string | TagRenderingOptions
}
export class LayerDefinition {
/**
* This name is used in the 'hide or show this layer'-buttons
*/
name: string | Translation;
/***
* This is shown under the 'add new' button to indicate what kind of feature one is adding.
*/
description: string | Translation
/**
* These tags are added whenever a new point is added by the user on the map.
* This is the ideal place to add extra info, such as "fixme=added by MapComplete, geometry should be checked"
*/
presets: Preset[]
/**
* Not really used anymore
* This is meant to serve as icon in the buttons
*/
icon: string | TagRenderingOptions;
/**
* Only show this layer starting at this zoom level
*/
minzoom: number;
/**
* This tagfilter is used to query overpass.
* Examples are:
*
* new Tag("amenity","drinking_water")
*
* or a query for bicycle pumps which have two tagging schemes:
* new Or([
* new Tag("service:bicycle:pump","yes") ,
* new And([
* new Tag("amenity","compressed_air"),
* new Tag("bicycle","yes")])
* ])
*/
overpassFilter: TagsFilter;
public readonly id: string;
/**
* This UIElement is rendered as title element in the popup
*/
title: TagDependantUIElementConstructor | UIElement | string;
/**
* These are the questions/shown attributes in the popup
*/
elementsToShow: TagDependantUIElementConstructor[];
/**
* A simple styling for the geojson element
* color is the color for areas and ways
* icon is the Leaflet icon
* Note that this is passed entirely to leaflet, so other leaflet attributes work too
*/
style: (tags: any) => {
color: string,
weight?: number,
icon: {
iconUrl: string,
iconSize?: [number, number],
popupAnchor?: [number,number],
iconAnchor?: [number,number]
},
};
/**
* 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;
/**
* If true, then ways (and polygons) will be converted to a 'point' at the center instead before further processing
*/
wayHandling: number = 0;
static WAYHANDLING_DEFAULT = 0;
static WAYHANDLING_CENTER_ONLY = 1;
static WAYHANDLING_CENTER_AND_WAY = 2;
constructor(id: string, options: {
name: string | Translation,
description: string | Translation,
presets: Preset[],
icon: string,
minzoom: number,
overpassFilter: TagsFilter,
title?: TagDependantUIElementConstructor,
elementsToShow?: TagDependantUIElementConstructor[],
maxAllowedOverlapPercentage?: number,
wayHandling?: number,
widenFactor?: number,
style?: (tags: any) => {
color: string,
icon: any
}
} = undefined) {
this.id = id;
if (options === undefined) {
return;
}
this.name = options.name;
this.description = options.description;
this.maxAllowedOverlapPercentage = options.maxAllowedOverlapPercentage ?? 0;
this.presets = options.presets;
this.icon = options.icon;
this.minzoom = options.minzoom;
this.overpassFilter = options.overpassFilter;
this.title = options.title;
this.elementsToShow = options.elementsToShow;
this.style = options.style;
this.wayHandling = options.wayHandling ?? LayerDefinition.WAYHANDLING_DEFAULT;
}
}

View file

@ -1,9 +1,12 @@
import {LayerDefinition} from "./LayerDefinition";
import {UIElement} from "../UI/UIElement";
import Translations from "../UI/i18n/Translations";
import Combine from "../UI/Base/Combine";
import State from "../State";
import Translation from "../UI/i18n/Translation";
import LayerConfig from "./JSON/LayerConfig";
import {LayoutConfigJson} from "./JSON/LayoutConfigJson";
import TagRenderingConfig from "./JSON/TagRenderingConfig";
import {FromJSON} from "./JSON/FromJSON";
/**
* A layout is a collection of settings of the global view (thus: welcome text, title, selection of layers).
@ -23,7 +26,7 @@ export class Layout {
*/
public customCss: string = undefined;
public layers: (LayerDefinition | string)[];
public layers: LayerConfig[];
public welcomeMessage: UIElement;
public gettingStartedPlzLogin: UIElement;
public welcomeBackMessage: UIElement;
@ -52,11 +55,51 @@ export class Layout {
public widenFactor: number = 0.07;
public defaultBackground: string = "osm";
public static LayoutFromJSON(json: LayoutConfigJson, sharedLayers): Layout {
const tr = FromJSON.Translation;
const layers = json.layers.map(jsonLayer => {
if(typeof jsonLayer === "string"){
return sharedLayers[jsonLayer];
}
return new LayerConfig(jsonLayer, "theme."+json.id);
});
const roaming: TagRenderingConfig[] = json.roamingRenderings?.map((tr, i) =>
new TagRenderingConfig(tr, `theme.${json.id}.roamingRendering[${i}]`)) ?? [];
for (const layer of layers) {
layer.tagRenderings.push(...roaming);
}
const layout = new Layout(
json.id,
typeof (json.language) === "string" ? [json.language] : json.language,
tr(json.title ?? "Title not defined"),
layers,
json.startZoom,
json.startLat,
json.startLon,
new Combine(["<h3>", tr(json.title), "</h3>", tr(json.description)]),
undefined,
undefined,
tr(json.descriptionTail)
);
layout.defaultBackground = json.defaultBackgroundId ?? "osm";
layout.widenFactor = json.widenFactor ?? 0.07;
layout.icon = json.icon;
layout.maintainer = json.maintainer;
layout.version = json.version;
layout.socialImage = json.socialImage;
layout.description = tr(json.shortDescription) ?? tr(json.description)?.FirstSentence();
layout.changesetMessage = json.changesetmessage;
return layout;
}
constructor(
id: string,
supportedLanguages: string[],
title: Translation | string,
layers: (LayerDefinition | string)[],
layers: LayerConfig[],
startzoom: number,
startLat: number,
startLon: number,

View file

@ -1,98 +0,0 @@
import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor";
import {TagsFilter, TagUtils} from "../Logic/Tags";
import {UIElement} from "../UI/UIElement";
import {UIEventSource} from "../Logic/UIEventSource";
import Translation from "../UI/i18n/Translation";
/**
* Wrapper around another TagDependandElement, which only shows if the filters match
*/
export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{
private readonly _tagsFilter: TagsFilter;
private readonly _embedded: TagDependantUIElementConstructor;
constructor(tagsFilter: TagsFilter, embedded: TagDependantUIElementConstructor) {
this._tagsFilter = tagsFilter;
this._embedded = embedded;
}
construct(tags: UIEventSource<any>): TagDependantUIElement {
return new OnlyShowIf(tags,
this._embedded.construct(tags),
this._tagsFilter);
}
IsKnown(properties: any): boolean {
if(!this.Matches(properties)){
return true;
}
return this._embedded.IsKnown(properties);
}
IsQuestioning(properties: any): boolean {
if(!this.Matches(properties)){
return false;
}
return this._embedded.IsQuestioning(properties);
}
GetContent(tags: any): Translation {
if(!this.IsKnown(tags)){
return undefined;
}
return this._embedded.GetContent(tags);
}
private Matches(properties: any) : boolean{
return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties));
}
}
class OnlyShowIf extends UIElement implements TagDependantUIElement {
private readonly _embedded: TagDependantUIElement;
private readonly _filter: TagsFilter;
constructor(
tags: UIEventSource<any>,
embedded: TagDependantUIElement,
filter: TagsFilter) {
super(tags);
this._filter = filter;
this._embedded = embedded;
}
private Matches() : boolean{
return this._filter.matches(TagUtils.proprtiesToKV(this._source.data));
}
InnerRender(): string {
if (this.Matches()) {
return this._embedded.Render();
} else {
return "";
}
}
IsKnown(): boolean {
if(!this.Matches()){
return false;
}
return this._embedded.IsKnown();
}
IsSkipped(): boolean {
if(!this.Matches()){
return false;
}
return this._embedded.IsSkipped();
}
IsQuestioning(): boolean {
if(!this.Matches()){
return false;
}
return this._embedded.IsQuestioning();
}
}

View file

@ -1,29 +0,0 @@
import {Img} from "../../UI/Img";
import {RegexTag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";
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 RegexTag("id", /node\/-.+/), txt: ""}
]
}
constructor() {
super(OsmLink.options);
}
}

View file

@ -1,50 +0,0 @@
import {TagRenderingOptions} from "../TagRenderingOptions";
export class WikipediaLink extends TagRenderingOptions {
private static FixLink(value: string): string {
if (value === undefined) {
return undefined;
}
// @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?",
tagsPreprocessor: (tags) => {
if (tags.wikipedia !== undefined) {
tags.wikipedia = WikipediaLink.FixLink(tags.wikipedia);
}
},
freeform: {
key: "wikipedia",
template: "$$$",
renderTemplate:
"<a href='{wikipedia}' target='_blank'>" +
"<img style='width: 24px;height: 24px;' src='./assets/wikipedia.svg' alt='wikipedia'>" +
"</a>",
placeholder: ""
},
}
constructor() {
super(WikipediaLink.options);
}
}

View file

@ -0,0 +1,50 @@
import * as drinkingWater from "../assets/layers/drinking_water/drinking_water.json";
import * as ghostbikes from "../assets/layers/ghost_bike/ghost_bike.json"
import * as viewpoint from "../assets/layers/viewpoint/viewpoint.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 birdhides from "../assets/layers/bird_hide/birdhides.json"
import * as nature_reserve from "../assets/layers/nature_reserve/nature_reserve.json"
import * as bike_cafes from "../assets/layers/bike_cafe/bike_cafes.json"
import * as bike_monitoring_station from "../assets/layers/bike_monitoring_station/bike_monitoring_station.json"
import * as cycling_themed_objects from "../assets/layers/cycling_themed_object/cycling_themed_objects.json"
import * as bike_shops from "../assets/layers/bike_shop/bike_shop.json"
import * as maps from "../assets/layers/maps/maps.json"
import * as information_boards from "../assets/layers/information_board/information_board.json"
import LayerConfig from "./JSON/LayerConfig";
export default class SharedLayers {
public static sharedLayers: Map<string, LayerConfig> = SharedLayers.getSharedLayers();
private static getSharedLayers(){
const sharedLayersList = [
new LayerConfig(drinkingWater, "shared_layers"),
new LayerConfig(ghostbikes, "shared_layers"),
new LayerConfig(viewpoint, "shared_layers"),
new LayerConfig(bike_parking, "shared_layers"),
new LayerConfig(bike_repair_station, "shared_layers"),
new LayerConfig(bike_monitoring_station, "shared_layers"),
new LayerConfig(birdhides, "shared_layers"),
new LayerConfig(nature_reserve, "shared_layers"),
new LayerConfig(bike_cafes, "shared_layers"),
new LayerConfig(cycling_themed_objects, "shared_layers"),
new LayerConfig(bike_shops, "shared_layers"),
new LayerConfig(maps, "shared_layers"),
new LayerConfig(information_boards, "shared_layers")
];
const sharedLayers = new Map<string, LayerConfig>();
for (const layer of sharedLayersList) {
sharedLayers.set(layer.id, layer);
sharedLayers[layer.id] = layer;
}
return sharedLayers;
}
}

View file

@ -0,0 +1,20 @@
import * as questions from "../assets/questions/questions.json";
import TagRenderingConfig from "./JSON/TagRenderingConfig";
export default class SharedTagRenderings {
public static SharedTagRendering = SharedTagRenderings.generatedSharedFields();
private static generatedSharedFields() {
const dict = {}
for (const key in questions) {
try {
dict[key] = new TagRenderingConfig(questions[key])
} catch (e) {
console.error("COULD NOT PARSE", key, " FROM QUESTIONS:", e)
}
}
return dict;
}
}

View file

@ -1,107 +0,0 @@
import {Layout} from "../Layout";
import {Widths} from "./Widths";
export class StreetWidth extends Layout{
private static meetMethode = `
We meten de ruimte die gedeeld wordt door auto's, fietsers en -in sommige gevallen- voetgangers.
We meten dus van _verhoogde_ stoeprand tot stoeprand omdat dit de ruimte is die wordt gedeeld door auto's en fietsers.
Daarnaast zoeken we ook een smaller stuk van de weg waar dat smallere stuk toch minstens 2m zo smal blijft.
Een obstakel (zoals een trap, elektriciteitkast) negeren we omdat dit de meting te fel beinvloed.
In een aantal straten is er geen verhoogde stoep. In dit geval meten we van muur tot muur, omdat dit de gedeelde ruimte is.
We geven ook altijd een aanduiding of er al dan niet een voetpad aanwezig (en aan welke kant indien er maar één is), want indien er geen is heeft de voetganger ook ruimte nodig.
(In sommige straten zijn er wel 'voetpadsuggesties' door een meter in andere kasseien te leggen, bv. met een kleurtje. Dit rekenen we niet als voetpad.
Ook het parkeren van auto's wordt opgemeten.
Als er een parallele parkeerstrook is, dan duiden we dit aan en nemen we de parkeerstrook mee in de straatbreedte.
Als er een witte lijn is, dan negeren we dit. Deze witte lijnen duiden immers vaak een smalle parkeerplaats aan - bv. 1.6m.
Een auto is tegenwoordig al snel 1.8m tot zelfs 2.0m, dus dan springt die auto gemakkelijk 20 tot 30cm uit op de baan.
Staan de auto's schuin geparkeerd of dwarsgeparkeerd?
Ook hier kan men het argument maken dat auto's er soms overspringen, maar dat is hier te variabel om in kaart te brengen.
Daarnaast gebeurt het minder dat auto's overspringen én zijn deze gevallen relatief zeldzaam in de binnenstad.
Concreet:
- Sla de 'parkeren'-vraag over
- Maak een foto en stuur die door naar Pieter (+ vermelding straatnaam of dergelijke)
- Meet de breedte vanaf de afbakening van de parkeerstrook.
Ook bij andere lastige gevallen: maak een foto en vraag Pieter
Instellen van de lasermeter
===========================
1) Zet de lasermeter aan met de rode knop
2) Het icoontje linksboven indiceert vanaf waar de laser meet - de voorkant of de achterkant van het apparaatje.
Dit kan aangepast worden met het knopje links-onderaan.
Kies wat je het liefste hebt
3) Het icoontje bovenaan-midden indiceert de stand van de laser: directe afstand, of afstand over de grond.
Dit MOET een driehoekje tonen.
Indien niet: duw op het knopje links-bovenaan totdat dit een rechte driehoek toont
4) Duw op de rode knop. Het lasertje gaat branden
5) Hou het meetbakje boven de stoeprand (met de juiste rand), richt de laser op de andere stoep
6) Duw opnieuw op de rode knop om te meten (de laser flikkert en gaat uit)
7) Lees de afstand af op het scherm. Let op: in 'hoekstand' is dit niet de onderste waarde, maar die er net boven.
`
constructor() {
super( "width",
["nl"],
"Straatbreedtes in Brugge",
[new Widths(
2,
1.5,
0.75
)],
15,
51.20875,
3.22435,
"<h3>De straat is opgebruikt</h3>" +
"<p>Er is steeds meer druk op de openbare ruimte. Voetgangers, fietsers, steps, auto's, bussen, bestelwagens, buggies, cargobikes, ... willen allemaal hun deel van de openbare ruimte.</p>" +
"" +
"<p>In deze studie nemen we Brugge onder de loep en kijken we hoe breed elke straat is én hoe breed elke straat zou moeten zijn voor een veilig én vlot verkeer.</p>" +
"<h3>Legende</h3>" +
"<span style='background: red'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Straat te smal voor veilig verkeer<br/>"+
"<span style='background: #0f0'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Straat is breed genoeg veilig verkeer<br/>"+
"<span style='background: orange'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Straat zonder voetpad, te smal als ook voetgangers plaats krijgen<br/>"+
"<span style='background: lightgrey'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Woonerf, autoluw, autoloos of enkel plaatselijk verkeer<br/>" +
"<br/>" +
"<br/>" +
"Een gestippelde lijn is een straat waar ook voor fietsers éénrichtingsverkeer geldt.<br/>" +
"Klik op een straat om meer informatie te zien."+
"<h3>Hoe gaan we verder?</h3>" +
"Verschillende ingrepen kunnen de stad teruggeven aan de inwoners en de stad leefbaarder en levendiger maken.<br/>" +
"Denk aan:" +
"<ul>" +
"<li>De autovrije zone's uitbreiden</li>" +
"<li>De binnenstad fietszone maken</li>" +
"<li>Het aantal woonerven uitbreiden</li>" +
"<li>Grotere auto's meer belasten - ze nemen immers meer parkeerruimte in.</li>" +
"<li>Laat toeristen verplicht parkeren onder het zand; een (fiets)taxi kan hen naar hun hotel brengen</li>" +
"<li>Voorzie in elke straat enkele parkeerplaatsen voor kortparkeren. Zo kunnen leveringen, iemand afzetten,... gebeuren zonder op het voetpad en fietspad te parkeren</li>" +
"</ul>");
this.icon = "./assets/bug.svg";
this.enableSearch = false;
this.enableUserBadge = false;
this.enableAdd = false;
this.hideFromOverview = true;
this.enableMoreQuests = false;
this.enableShareScreen = false;
this.defaultBackground = "Stadia.AlidadeSmoothDark"
this.enableBackgroundLayers = false;
}
}

View file

@ -1,312 +0,0 @@
import {LayerDefinition} from "../LayerDefinition";
import {And, Or, RegexTag, Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";
import {FromJSON} from "../JSON/FromJSON";
export class Widths extends LayerDefinition {
private readonly cyclistWidth: number;
private readonly carWidth: number;
private readonly pedestrianWidth: number;
private readonly _bothSideParking = new Tag("parking:lane:both", "parallel");
private readonly _noSideParking = new Tag("parking:lane:both", "no_parking");
private readonly _otherParkingMode =
new Or([
new Tag("parking:lane:both", "perpendicular"),
new Tag("parking:lane:left", "perpendicular"),
new Tag("parking:lane:right", "perpendicular"),
new Tag("parking:lane:both", "diagonal"),
new Tag("parking:lane:left", "diagonal"),
new Tag("parking:lane:right", "diagonal"),
])
private readonly _leftSideParking =
new And([new Tag("parking:lane:left", "parallel"), new Tag("parking:lane:right", "no_parking")]);
private readonly _rightSideParking =
new And([new Tag("parking:lane:right", "parallel"), new Tag("parking:lane:left", "no_parking")]);
private _sidewalkBoth = new Tag("sidewalk", "both");
private _sidewalkLeft = new Tag("sidewalk", "left");
private _sidewalkRight = new Tag("sidewalk", "right");
private _sidewalkNone = new Tag("sidewalk", "none");
private readonly _oneSideParking = new Or([this._leftSideParking, this._rightSideParking]);
private readonly _notCarfree =
FromJSON.Tag({"and":[
"highway!~pedestrian|living_street",
"access!~destination",
"motor_vehicle!~destination|no"
]});
private calcProps(properties) {
let parkingStateKnown = true;
let parallelParkingCount = 0;
if (this._oneSideParking.matchesProperties(properties)) {
parallelParkingCount = 1;
} else if (this._bothSideParking.matchesProperties(properties)) {
parallelParkingCount = 2;
} else if (this._noSideParking.matchesProperties(properties)) {
parallelParkingCount = 0;
} else if (this._otherParkingMode.matchesProperties(properties)) {
parallelParkingCount = 0;
} else {
parkingStateKnown = false;
console.log("No parking data for ", properties.name, properties.id, properties)
}
let pedestrianFlowNeeded;
if (this._sidewalkBoth.matchesProperties(properties)) {
pedestrianFlowNeeded = 0;
} else if (this._sidewalkNone.matchesProperties(properties)) {
pedestrianFlowNeeded = 2;
} else if (this._sidewalkLeft.matchesProperties(properties) || this._sidewalkRight.matches(properties)) {
pedestrianFlowNeeded = 1;
} else {
pedestrianFlowNeeded = -1;
}
let onewayCar = properties.oneway === "yes";
let onewayBike = properties["oneway:bicycle"] === "yes" ||
(onewayCar && properties["oneway:bicycle"] === undefined)
let cyclingAllowed =
!(properties.bicycle === "use_sidepath"
|| properties.bicycle === "no");
let carWidth = (onewayCar ? 1 : 2) * this.carWidth;
let cyclistWidth = 0;
if (cyclingAllowed) {
cyclistWidth = (onewayBike ? 1 : 2) * this.cyclistWidth;
}
const width = parseFloat(properties["width:carriageway"]);
const targetWidthIgnoringPedestrians =
carWidth +
cyclistWidth +
parallelParkingCount * this.carWidth;
const targetWidth = targetWidthIgnoringPedestrians + Math.max(0, pedestrianFlowNeeded) * this.pedestrianWidth;
return {
parkingLanes: parallelParkingCount,
parkingStateKnown: parkingStateKnown,
width: width,
targetWidth: targetWidth,
targetWidthIgnoringPedestrians: targetWidthIgnoringPedestrians,
onewayBike: onewayBike,
pedestrianFlowNeeded: pedestrianFlowNeeded,
cyclingAllowed: cyclingAllowed
}
}
constructor(carWidth: number,
cyclistWidth: number,
pedestrianWidth: number) {
super("width");
this.carWidth = carWidth;
this.cyclistWidth = cyclistWidth;
this.pedestrianWidth = pedestrianWidth;
this.minzoom = 12;
function r(n: number) {
const pre = Math.floor(n);
const post = Math.floor((n * 10) % 10);
return "" + pre + "." + post;
}
this.name = "widths";
this.overpassFilter = new RegexTag("width:carriageway", /.*/);
this.title = new TagRenderingOptions({
freeform: {
renderTemplate: "{name}",
template: "$$$",
key: "name"
}
})
const self = this;
this.style = (properties) => {
let c = "#f00";
const props = self.calcProps(properties);
if (props.width >= props.targetWidthIgnoringPedestrians) {
c = "#fa0"
}
if (props.width >= props.targetWidth || !props.cyclingAllowed) {
c = "#0c0";
}
if (!props.parkingStateKnown && properties["note:width:carriageway"] === undefined) {
c = "#f0f"
}
if (!this._notCarfree.matchesProperties(properties)) {
c = "#aaa";
}
// Mark probably wrong data
if (props.width > 15) {
c = "#f0f"
}
let dashArray = undefined;
if (props.onewayBike) {
dashArray = [5, 6]
}
return {
icon: null,
color: c,
weight: 5,
dashArray: dashArray
}
}
this.elementsToShow = [
new TagRenderingOptions({
mappings: [
{
k: this._bothSideParking,
txt: "Auto's kunnen langs beide zijden parkeren.<br+>Dit gebruikt <b>" + r(this.carWidth * 2) + "m</b><br/>"
},
{
k: this._oneSideParking,
txt: "Auto's kunnen langs één kant parkeren.<br/>Dit gebruikt <b>" + r(this.carWidth) + "m</b><br/>"
},
{
k: this._otherParkingMode,
txt: "Deze straat heeft dwarsparkeren of diagonaalparkeren aan minstens één zijde. Deze parkeerruimte is niet opgenomen in de straatbreedte."
},
{k: this._noSideParking, txt: "Auto's mogen hier niet parkeren"},
],
freeform: {
key: "note:width:carriageway",
renderTemplate: "{note:width:carriageway}",
template: "$$$",
}
}).OnlyShowIf(this._notCarfree),
new TagRenderingOptions({
mappings: [
{
k: this._sidewalkNone,
txt: "Deze straat heeft geen voetpaden. Voetgangers hebben hier <b>" + r(this.pedestrianWidth * 2) + "m</b> nodig"
},
{
k: new Or([this._sidewalkLeft, this._sidewalkRight]),
txt: "Deze straat heeft een voetpad aan één kant. Voetgangers hebben hier <b>" + r(this.pedestrianWidth) + "m</b> nodig"
},
{k: this._sidewalkBoth, txt: "Deze straat heeft voetpad aan beide zijden."},
],
freeform: {
key: "note:width:carriageway",
renderTemplate: "{note:width:carriageway}",
template: "$$$",
}
}).OnlyShowIf(this._notCarfree),
new TagRenderingOptions({
mappings: [
{
k: new Tag("bicycle", "use_sidepath"),
txt: "Er is een afgescheiden, verplicht te gebruiken fietspad. Fietsen op dit wegsegment hoeft dus niet"
},
{
k: new Tag("bicycle", "no"),
txt: "Fietsen is hier niet toegestaan"
},
{
k: new Tag("oneway:bicycle", "yes"),
txt: "Eenrichtingsverkeer, óók voor fietsers. Dit gebruikt <b>" + r(this.carWidth + this.cyclistWidth) + "m</b>"
},
{
k: new And([new Tag("oneway", "yes"), new Tag("oneway:bicycle", "no")]),
txt: "Tweerichtingverkeer voor fietsers, eenrichting voor auto's Dit gebruikt <b>" + r(this.carWidth + 2 * this.cyclistWidth) + "m</b>"
},
{
k: new Tag("oneway", "yes"),
txt: "Eenrichtingsverkeer voor iedereen. Dit gebruikt <b>" + (this.carWidth + this.cyclistWidth) + "m</b>"
},
{
k: null,
txt: "Tweerichtingsverkeer voor iedereen. Dit gebruikt <b>" + r(2 * this.carWidth + 2 * this.cyclistWidth) + "m</b>"
}
]
}).OnlyShowIf(this._notCarfree),
new TagRenderingOptions(
{
tagsPreprocessor: (tags) => {
const props = self.calcProps(tags);
tags.targetWidth = r(props.targetWidth);
tags.short = "";
if (props.width < props.targetWidth) {
tags.short = `<span class='alert'>Dit is ${r(props.targetWidth - props.width)}m te weinig</span>`
}
console.log("SHORT", tags.short)
},
mappings: [
{
k: null,
txt: "De totale nodige ruimte voor vlot en veilig verkeer is dus <span class='thanks'>{targetWidth}m</span><br/>{short}"
}
]
}
).OnlyShowIf(this._notCarfree),
new TagRenderingOptions({
mappings: [
{k:new Tag("highway","living_street"),txt: "Dit is een woonerf"},
{k:new Tag("highway","pedestrian"),txt: "Deze weg is autovrij"}
]
}),
new TagRenderingOptions({
mappings: [
{
k: new Tag("sidewalk", "none"),
txt: "De afstand van huis tot huis is <b>{width:carriageway}m</b>"
},
{
k: new Tag("sidewalk", "left"),
txt: "De afstand van huis tot voetpad is <b>{width:carriageway}m</b>"
},
{
k: new Tag("sidewalk", "right"),
txt: "De afstand van huis tot voetpad is <b>{width:carriageway}m</b>"
},
{
k: new Tag("sidewalk", "both"),
txt: "De afstand van voetpad tot voetpad is <b>{width:carriageway}m</b>"
},
{
k: new Tag("sidewalk", ""),
txt: "De straatbreedte is <b>{width:carriageway}m</b>"
}
]
})
]
}
}

View file

@ -1,148 +0,0 @@
import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor";
import {TagsFilter, TagUtils} from "../Logic/Tags";
import {OnlyShowIfConstructor} from "./OnlyShowIf";
import {UIEventSource} from "../Logic/UIEventSource";
import Translation from "../UI/i18n/Translation";
import Translations from "../UI/i18n/Translations";
export class TagRenderingOptions implements TagDependantUIElementConstructor {
/**
* Notes: by not giving a 'question', one disables the question form alltogether
*/
public options: {
question?: string | Translation;
freeform?: {
key: string;
tagsPreprocessor?: (tags: any) => any;
template: string | Translation;
renderTemplate: string | Translation;
placeholder?: string | Translation;
extraTags?: TagsFilter
};
multiAnswer?: boolean,
mappings?: { k: TagsFilter; txt: string | Translation; substitute?: boolean, hideInAnwser?: boolean }[]
};
constructor(options: {
/**
* This is the string that is shown in the popup if this tag is missing.
*
* If 'question' is undefined, then the question is never asked at all
* If the question is "" (empty string) then the question is
*/
question?: Translation | string,
/**
* What is the priority of the question.
* By default, in the popup of a feature, only one question is shown at the same time. If multiple questions are unanswered, the question with the highest priority is asked first
*/
priority?: number,
/**
* Mappings convert a well-known tag combination into a user friendly text.
* It converts e.g. 'access=yes' into 'this area can be accessed'
*
* If there are multiple tags that should be matched, And can be used. All tags in AND will be added when the question is picked (and the corresponding text will only be shown if all tags are present).
* If AND is used, it is best practice to make sure every used tag is in every option (with empty string) to erase extra tags.
*
* If a 'k' is null, then this one is shown by default. It can be used to force a default value, e.g. to show that the name of a POI is not (yet) known .
* A mapping where 'k' is null will not be shown as option in the radio buttons.
*
*
*/
mappings?: { k: TagsFilter, txt: Translation | string, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[],
/**
* If true, use checkboxes to answer instead of radiobuttons
*/
multiAnswer?: boolean,
/**
* If one wants to render a freeform tag (thus no predefined key/values) or if there are a few well-known tags with a freeform object,
* use this.
* In the question, it'll offer a textfield
*/
freeform?: {
key: string,
template: string | Translation,
renderTemplate: string | Translation
placeholder?: string | Translation,
extraTags?: TagsFilter,
},
/**
* In some very rare cases, tags have to be rewritten before displaying
* This function can be used for that.
* This function is ran on a _copy_ of the original properties
*/
tagsPreprocessor?: ((tags: any) => void)
}) {
this.options = options;
}
OnlyShowIf(tagsFilter: TagsFilter): TagDependantUIElementConstructor {
return new OnlyShowIfConstructor(tagsFilter, this);
}
IsQuestioning(tags: any): boolean {
const tagsKV = TagUtils.proprtiesToKV(tags);
for (const oneOnOneElement of this.options.mappings ?? []) {
if (oneOnOneElement.k === null || oneOnOneElement.k.matches(tagsKV)) {
return false;
}
}
if (this.options.freeform !== undefined && tags[this.options.freeform.key] !== undefined) {
return false;
}
return this.options.question !== undefined;
}
GetContent(tags: any): Translation {
const tagsKV = TagUtils.proprtiesToKV(tags);
for (const oneOnOneElement of this.options.mappings ?? []) {
if (oneOnOneElement.k === null || oneOnOneElement.k.matches(tagsKV)) {
return Translations.WT(oneOnOneElement.txt);
}
}
if (this.options.freeform !== undefined) {
let template = Translations.WT(this.options.freeform.renderTemplate);
return template.Subs(tags);
}
console.warn("No content defined for", tags, " with mapping", this);
return undefined;
}
public static tagRendering: (tags: UIEventSource<any>,
options: {
priority?: number;
question?: string | Translation;
freeform?: {
key: string;
tagsPreprocessor?: (tags: any) => any;
template: string | Translation;
renderTemplate: string | Translation;
placeholder?: string | Translation; extraTags?: TagsFilter
},
multiAnswer?: boolean,
mappings?: { k: TagsFilter; txt: string | Translation; priority?: number; substitute?: boolean, hideInAnswer?: boolean }[]
}) => TagDependantUIElement;
construct(tags: UIEventSource<any>): TagDependantUIElement {
return TagRenderingOptions.tagRendering(tags, this.options);
}
IsKnown(properties: any): boolean {
return !this.IsQuestioning(properties);
}
}

View file

@ -1,21 +0,0 @@
import {UIElement} from "../UI/UIElement";
import {UIEventSource} from "../Logic/UIEventSource";
import Translation from "../UI/i18n/Translation";
export interface TagDependantUIElementConstructor {
construct(tags: UIEventSource<any>): TagDependantUIElement;
IsKnown(properties: any): boolean;
IsQuestioning(properties: any): boolean;
GetContent(tags: any): Translation;
}
export abstract class TagDependantUIElement extends UIElement {
abstract IsKnown(): boolean;
abstract IsQuestioning(): boolean;
abstract IsSkipped() : boolean;
}