More work on the custom theme generator, add aed template, move bookcases to json template

This commit is contained in:
Pieter Vander Vennet 2020-08-22 02:12:46 +02:00
parent 146552e62c
commit 560c8e1567
34 changed files with 1048 additions and 590 deletions

View file

@ -11,10 +11,10 @@ import {ClimbingTrees} from "./Layouts/ClimbingTrees";
import {Smoothness} from "./Layouts/Smoothness";
import {MetaMap} from "./Layouts/MetaMap";
import {Natuurpunt} from "./Layouts/Natuurpunt";
import {Bookcases} from "./Layouts/Bookcases";
import {GhostBikes} from "./Layouts/GhostBikes";
import * as bookcases from "../assets/themes/bookcases/Bookcases.json";
import {CustomLayoutFromJSON} from "./JSON/CustomLayoutFromJSON";
import * as bookcases from "../assets/themes/bookcases/Bookcases.json";
import * as aed from "../assets/themes/aed/aed.json";
export class AllKnownLayouts {
@ -26,8 +26,9 @@ export class AllKnownLayouts {
new GRB(),
new Cyclofix(),
new GhostBikes(),
// new Bookcases(),
CustomLayoutFromJSON.LayoutFromJSON(bookcases),
CustomLayoutFromJSON.LayoutFromJSON(aed),
new MetaMap(),
new StreetWidth(),
new ClimbingTrees(),

View file

@ -2,15 +2,15 @@ import {TagRenderingOptions} from "../TagRenderingOptions";
import {LayerDefinition, Preset} from "../LayerDefinition";
import {Layout} from "../Layout";
import Translation from "../../UI/i18n/Translation";
import {type} from "os";
import Combine from "../../UI/Base/Combine";
import {UIElement} from "../../UI/UIElement";
import {And, Tag, TagsFilter} from "../../Logic/TagsFilter";
import {And, Tag} from "../../Logic/TagsFilter";
import FixedText from "../Questions/FixedText";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import {UIEventSource} from "../../Logic/UIEventSource";
import {TagDependantUIElementConstructor} from "../UIElementConstructor";
export interface TagRenderingConfigJson {
export interface TagRenderingConfigJson {
// If this key is present, then...
key?: string,
// Use this string to render
@ -33,11 +33,11 @@ export interface TagRenderingConfigJson {
export interface LayerConfigJson {
id: string;
icon: string;
icon: TagRenderingConfigJson;
title: TagRenderingConfigJson;
description: string;
minzoom: number,
color: string;
color: TagRenderingConfigJson;
overpassTags: string | string[] | { k: string, v: string }[];
presets: [
{
@ -58,7 +58,8 @@ export interface LayoutConfigJson {
name: string;
title: string;
description: string;
language: string;
maintainer: string;
language: string[];
layers: LayerConfigJson[],
startZoom: number;
startLat: number;
@ -71,86 +72,38 @@ export interface LayoutConfigJson {
export class CustomLayoutFromJSON {
public static exampleLayer: LayerConfigJson = {
id: "bookcase",
icon: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgaWQ9InN2ZzExMzgyIgogICBoZWlnaHQ9IjkwMCIKICAgd2lkdGg9IjkwMCIKICAgdmVyc2lvbj0iMS4wIj4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGExMCI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgICA8ZGM6dGl0bGU+PC9kYzp0aXRsZT4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGRlZnMKICAgICBpZD0iZGVmczExMzg0IiAvPgogIDxnCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLjkwMTAzMjU4LDAsMCwwLjkwMTAzMjU4LDExMi44NDA1OCwtMS45MDYwMTc3KSI+CiAgICA8ZwogICAgICAgaWQ9ImcxMTQ3NiI+CiAgICAgIDxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTE0NzIiCiAgICAgICAgIHN0eWxlPSJmb250LXN0eWxlOm5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zaXplOjEyMDEuOTI0OTI2NzZweDtmb250LWZhbWlseTonQml0c3RyZWFtIFZlcmEgU2Fucyc7dGV4dC1hbGlnbjpjZW50ZXI7dGV4dC1hbmNob3I6bWlkZGxlO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICAgIGQ9Ik0gNDc0LjUwODg4LDcxOC4yMjg0MSBIIDMwMy40OTU0NyB2IC0yMi4zMDEzNCBjIC0yLjRlLTQsLTM3Ljk1MTA4IDQuMzAzNTIsLTY4Ljc2MjExIDEyLjkxMTMsLTkyLjQzMzE5IDguNjA3MjgsLTIzLjY3MDMyIDIzLjYzMzUyLC00NS4yODY5NSA0MC42NTMyNCwtNjQuODQ5OTYgMTcuMDE5MTQsLTE5LjU2MjExIDQxLjk4NzM0LC0yNi4zMzI2NCAxMDEuNDU3OTMsLTc1LjYzMDg1IDMxLjY5MDk1LC0yNS44MjIwMyA1NS4yODEzLC03Ny4xNTIzIDU1LjI4MTc1LC05OC42NzE3NCAyLjIxMjMyLC01Ni45MjI0NSAtMTMuOTM5ODMsLTc5LjM0MjIgLTM0LjU2Mjg3LC05OS45NjUyNCAtMjIuNjczNTUsLTE5LjY3NzE3IC02MC42NzAyNywtMzAuMDY5OTggLTkwLjk5ODkyLC0zMC4wNjk5OCAtMjcuNzc5MjEsNi45ZS00IC02OC40NjczNSw4LjA4ODcxIC04Ny43NjY2LDI1LjM3MDQ3IC0yNS45MzgxNywxNy4yODMwOCAtNjUuMjM3NDcsNzMuNzA2MTEgLTU3LjA0Njg3LDEzMC41NDU3NyBsIC0xOTQuNTE2OTQzLDEuNzAyMjIgYyAwLC0xNTcuMjEzOTkgMjkuMzkzNjk5LC0xOTguNjk0NjUgOTkuMDA0MTEzLC0yNjMuMDMwMzIgNjcuMzk3MzksLTU0LjM3NjY0MyAxMjYuNTMxMjgsLTczLjI2ODM2NSAyNDMuODQ3NTcsLTczLjI2ODM2NSA4OS43MTc5MSwwIDE2MS44OTcyOCwxNy44MDI4MSAyMTQuMzI1NTIsNTMuNDA1ODU1IDcxLjIwNzE0LDQ4LjEyNDcyIDEyMi4zMDEwNSwxMTEuMTgzNTQgMTIyLjMwMTA1LDIzMC4xMTI4MSAtNi45ZS00LDQ0LjMyMDgxIC0xOS4xNTI1Myw5MC43ODYzOCAtNDMuMDcyNiwxMjguMzMyOTkgLTE4LjM4OTQ3LDMwLjkwOTM4IC02MC4zNzUxMSw2Ni40NTIzNiAtMTE4LjIxMjM3LDEwNC40MTYyOCAtNDIuODM2MDcsMjUuNzY4NiAtNjYuNjcxOTYsNTMuMTE5MjYgLTc3LjAzOTY0LDcyLjA5NDYgLTEwLjM2ODYzLDE4Ljk3NjAzIC0xNS41NTI3MSw0My43MjI2NyAtMTUuNTUyMjUsNzQuMjM5OTkgeiIgLz4KICAgICAgPHBhdGgKICAgICAgICAgaWQ9InBhdGgxMTQ3NCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpzcXVhcmU7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS4xMDYzODMsLTUuNTMxOTE0OSkiCiAgICAgICAgIGQ9Im0gNDgyLjM4Mjk4LDg2OS44MDkwMiBhIDk0LjA0MjU1Nyw3My4wMjEyNzggMCAxIDEgLTE4OC4wODUxMSwwIDk0LjA0MjU1Nyw3My4wMjEyNzggMCAxIDEgMTg4LjA4NTExLDAgeiIgLz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPgo=",
title: {render: "Bookcase"},
description: "A small, public cabinet with books. Anyone can leave or take a book",
minzoom: 12,
color: "#0000ff",
overpassTags: "amenity=public_bookcase",
presets: [
{
title: "bookcase"
// icon: optional. Uses the layer icon by default
// title: optional. Uses the layer title by default
// description: optional. Uses the layer description by default
// tags: optional list {k:string, v:string}[]
}
],
tagRenderings: [
{
// If this key is present, then...
key: "name",
// Use this string to render
render: "{name}",
// One of string, int, nat, float, pfloat, email, phone. Default: string
type: "string",
// If it is not known (and no mapping below matches), this question is asked; a textfield is inserted in the rendering above
question: "Wat is de naam van dit boekenruilkastje?",
// If a value is added with the textfield, this extra tag is addded. Optional field
addExtraTags: [{
"k": "fixme",
"v": "Added with mapcomplete, to be checked"
}],
// Alternatively, these tags are shown if they match - even if the key above is not there
// If unknown, these become a radio button
mappings: [
{
if: "noname=yes",
then: "Dit boekenruilkastje heeft geen naam"
}
]
}
]
}
public static exampleLayout: LayoutConfigJson = {
name: "bookcases",
title: "Custom Open bookcases map",
description: "Welcome to a custom layout",
language: "en",
layers: [CustomLayoutFromJSON.exampleLayer],
startZoom: 12,
startLat: 0,
startLon: 0,
icon: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgaWQ9InN2ZzExMzgyIgogICBoZWlnaHQ9IjkwMCIKICAgd2lkdGg9IjkwMCIKICAgdmVyc2lvbj0iMS4wIj4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGExMCI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgICA8ZGM6dGl0bGU+PC9kYzp0aXRsZT4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGRlZnMKICAgICBpZD0iZGVmczExMzg0IiAvPgogIDxnCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLjkwMTAzMjU4LDAsMCwwLjkwMTAzMjU4LDExMi44NDA1OCwtMS45MDYwMTc3KSI+CiAgICA8ZwogICAgICAgaWQ9ImcxMTQ3NiI+CiAgICAgIDxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTE0NzIiCiAgICAgICAgIHN0eWxlPSJmb250LXN0eWxlOm5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zaXplOjEyMDEuOTI0OTI2NzZweDtmb250LWZhbWlseTonQml0c3RyZWFtIFZlcmEgU2Fucyc7dGV4dC1hbGlnbjpjZW50ZXI7dGV4dC1hbmNob3I6bWlkZGxlO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICAgIGQ9Ik0gNDc0LjUwODg4LDcxOC4yMjg0MSBIIDMwMy40OTU0NyB2IC0yMi4zMDEzNCBjIC0yLjRlLTQsLTM3Ljk1MTA4IDQuMzAzNTIsLTY4Ljc2MjExIDEyLjkxMTMsLTkyLjQzMzE5IDguNjA3MjgsLTIzLjY3MDMyIDIzLjYzMzUyLC00NS4yODY5NSA0MC42NTMyNCwtNjQuODQ5OTYgMTcuMDE5MTQsLTE5LjU2MjExIDQxLjk4NzM0LC0yNi4zMzI2NCAxMDEuNDU3OTMsLTc1LjYzMDg1IDMxLjY5MDk1LC0yNS44MjIwMyA1NS4yODEzLC03Ny4xNTIzIDU1LjI4MTc1LC05OC42NzE3NCAyLjIxMjMyLC01Ni45MjI0NSAtMTMuOTM5ODMsLTc5LjM0MjIgLTM0LjU2Mjg3LC05OS45NjUyNCAtMjIuNjczNTUsLTE5LjY3NzE3IC02MC42NzAyNywtMzAuMDY5OTggLTkwLjk5ODkyLC0zMC4wNjk5OCAtMjcuNzc5MjEsNi45ZS00IC02OC40NjczNSw4LjA4ODcxIC04Ny43NjY2LDI1LjM3MDQ3IC0yNS45MzgxNywxNy4yODMwOCAtNjUuMjM3NDcsNzMuNzA2MTEgLTU3LjA0Njg3LDEzMC41NDU3NyBsIC0xOTQuNTE2OTQzLDEuNzAyMjIgYyAwLC0xNTcuMjEzOTkgMjkuMzkzNjk5LC0xOTguNjk0NjUgOTkuMDA0MTEzLC0yNjMuMDMwMzIgNjcuMzk3MzksLTU0LjM3NjY0MyAxMjYuNTMxMjgsLTczLjI2ODM2NSAyNDMuODQ3NTcsLTczLjI2ODM2NSA4OS43MTc5MSwwIDE2MS44OTcyOCwxNy44MDI4MSAyMTQuMzI1NTIsNTMuNDA1ODU1IDcxLjIwNzE0LDQ4LjEyNDcyIDEyMi4zMDEwNSwxMTEuMTgzNTQgMTIyLjMwMTA1LDIzMC4xMTI4MSAtNi45ZS00LDQ0LjMyMDgxIC0xOS4xNTI1Myw5MC43ODYzOCAtNDMuMDcyNiwxMjguMzMyOTkgLTE4LjM4OTQ3LDMwLjkwOTM4IC02MC4zNzUxMSw2Ni40NTIzNiAtMTE4LjIxMjM3LDEwNC40MTYyOCAtNDIuODM2MDcsMjUuNzY4NiAtNjYuNjcxOTYsNTMuMTE5MjYgLTc3LjAzOTY0LDcyLjA5NDYgLTEwLjM2ODYzLDE4Ljk3NjAzIC0xNS41NTI3MSw0My43MjI2NyAtMTUuNTUyMjUsNzQuMjM5OTkgeiIgLz4KICAgICAgPHBhdGgKICAgICAgICAgaWQ9InBhdGgxMTQ3NCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpzcXVhcmU7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS4xMDYzODMsLTUuNTMxOTE0OSkiCiAgICAgICAgIGQ9Im0gNDgyLjM4Mjk4LDg2OS44MDkwMiBhIDk0LjA0MjU1Nyw3My4wMjEyNzggMCAxIDEgLTE4OC4wODUxMSwwIDk0LjA0MjU1Nyw3My4wMjEyNzggMCAxIDEgMTg4LjA4NTExLDAgeiIgLz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPgo="
}
public static FromQueryParam(layoutFromBase64: string): Layout {
if(layoutFromBase64 === "test"){
console.log(btoa(JSON.stringify(CustomLayoutFromJSON.exampleLayout)));
return CustomLayoutFromJSON.LayoutFromJSON(CustomLayoutFromJSON.exampleLayout);
}
const spec = JSON.parse(atob(layoutFromBase64));
return CustomLayoutFromJSON.LayoutFromJSON(spec);
return CustomLayoutFromJSON.LayoutFromJSON(JSON.parse(atob(layoutFromBase64)));
}
private static TagRenderingFromJson(json: any): TagRenderingOptions {
public static TagRenderingFromJson(json: any): TagDependantUIElementConstructor {
if (typeof (json) === "string") {
return new FixedText(json);
}
let freeform = undefined;
if (json.key !== undefined && json.key !== "" && json.render !== undefined) {
if (json.render !== undefined) {
const type = json.type ?? "text";
let renderTemplate = CustomLayoutFromJSON.MaybeTranslation(json.render);;
const template = renderTemplate.replace("{" + json.key + "}", "$" + type + "$");
if(type === "url"){
renderTemplate = json.render.replace("{" + json.key + "}",
`<a href='{${json.key}}' target='_blank'>{${json.key}}</a>`
);
}
freeform = {
key: json.key,
template: json.render.replace("{" + json.key + "}", "$" + type + "$"),
renderTemplate: json.render,
template: template,
renderTemplate: renderTemplate,
extraTags: CustomLayoutFromJSON.TagsFromJson(json.addExtraTags),
}
if (freeform.key === "*") {
freeform.key = "id"; // Id is always there -> always take the rendering. Used for 'icon' and 'stroke'
}
}
let mappings = undefined;
@ -158,30 +111,37 @@ export class CustomLayoutFromJSON {
mappings = [];
for (const mapping of json.mappings) {
mappings.push({
k: new And(CustomLayoutFromJSON.TagsFromJson(mapping.if)), txt: mapping.then
k: new And(CustomLayoutFromJSON.TagsFromJson(mapping.if)),
txt: CustomLayoutFromJSON.MaybeTranslation(mapping.then)
})
}
}
return new TagRenderingOptions({
question: json.question,
const rendering = new TagRenderingOptions({
question: CustomLayoutFromJSON.MaybeTranslation(json.question),
freeform: freeform,
mappings: mappings
})
});
if (json.condition) {
const conditionTags: Tag[] = CustomLayoutFromJSON.TagsFromJson(json.condition);
return rendering.OnlyShowIf(new And(conditionTags));
}
return rendering;
}
private static PresetFromJson(layout: any, preset: any): Preset {
const t = CustomLayoutFromJSON.MaybeTranslation;
const tags = CustomLayoutFromJSON.TagsFromJson;
return {
icon: preset.icon ?? layout.icon,
icon: preset.icon ?? CustomLayoutFromJSON.TagRenderingFromJson(layout.icon),
tags: tags(preset.tags) ?? tags(layout.overpassTags),
title: t(preset.title) ?? t(layout.title),
description: t(preset.description) ?? t(layout.description)
}
}
private static StyleFromJson(layout: any, styleJson: any): ((tags) => {
private static StyleFromJson(layout: any, styleJson: any): ((tags: any) => {
color: string,
weight?: number,
icon: {
@ -189,12 +149,17 @@ export class CustomLayoutFromJSON {
iconSize: number[],
},
}) {
const iconRendering: TagDependantUIElementConstructor = CustomLayoutFromJSON.TagRenderingFromJson(layout.icon);
const colourRendering = CustomLayoutFromJSON.TagRenderingFromJson(layout.color);
return (tags) => {
const iconUrl = iconRendering.GetContent(tags);
const stroke = colourRendering.GetContent(tags);
return {
color: layout.color,
color: stroke,
weight: 10,
icon: {
iconUrl: layout.icon,
iconUrl: iconUrl,
iconSize: [40, 40],
},
}
@ -205,41 +170,76 @@ export class CustomLayoutFromJSON {
if (json === undefined) {
return undefined;
}
console.log(json)
if (typeof (json) === "string") {
const kv = json.split("=");
return new Tag(kv[0].trim(), kv[1].trim());
let kv: string[] = undefined;
let invert = false;
if (json.indexOf("!=") >= 0) {
kv = json.split("!=");
invert = true;
} else {
kv = json.split("=");
}
if (kv.length !== 2) {
return undefined;
}
if (kv[0].trim() === "") {
return undefined;
}
return new Tag(kv[0].trim(), kv[1].trim(), invert);
}
return new Tag(json.k.trim(), json.v.trim())
}
private static TagsFromJson(json: string | { k: string, v: string }[]): Tag[] {
if (json === undefined || json === "") {
public static TagsFromJson(json: string | { k: string, v: string }[]): Tag[] {
if (json === undefined) {
return undefined;
}
if (typeof (json) === "string") {
return json.split(",").map(CustomLayoutFromJSON.TagFromJson);
if (json === "") {
return [];
}
return json.map(CustomLayoutFromJSON.TagFromJson)
let tags = [];
if (typeof (json) === "string") {
tags = json.split("&").map(CustomLayoutFromJSON.TagFromJson);
} else {
tags = json.map(CustomLayoutFromJSON.TagFromJson);
}
for (const tag of tags) {
if (tag === undefined) {
return undefined;
}
}
return tags;
}
private static LayerFromJson(json: any): LayerDefinition {
const t = CustomLayoutFromJSON.MaybeTranslation;
const tr = CustomLayoutFromJSON.TagRenderingFromJson;
const tags = CustomLayoutFromJSON.TagsFromJson(json.overpassTags);
// We run the icon rendering with the bare minimum of tags (the overpass tags) to get the actual icon
const properties = {};
for (const tag of tags) {
tags[tag.key] = tag.value;
}
const icon = CustomLayoutFromJSON.TagRenderingFromJson(json.icon).construct({
tags: new UIEventSource<any>(properties)
}).InnerRender();
return new LayerDefinition(
json.id,
{
description: t(json.description),
name: t(json.title),
icon: json.icon,
icon: icon,
minzoom: json.minzoom,
title: tr(json.title) ,
title: tr(json.title),
presets: json.presets.map((preset) => {
return CustomLayoutFromJSON.PresetFromJson(json, preset)
}),
elementsToShow:
[new ImageCarouselWithUploadConstructor()].concat(json.tagRenderings.map(tr)),
overpassFilter: new And(CustomLayoutFromJSON.TagsFromJson(json.overpassTags)),
overpassFilter: new And(tags),
wayHandling: LayerDefinition.WAYHANDLING_CENTER_AND_WAY,
maxAllowedOverlapPercentage: 0,
style: CustomLayoutFromJSON.StyleFromJson(json, json.style)
@ -260,8 +260,12 @@ export class CustomLayoutFromJSON {
public static LayoutFromJSON(json: any) {
const t = CustomLayoutFromJSON.MaybeTranslation;
let languages = json.language;
if(typeof (json.language) === "string"){
languages = [json.language];
}
const layout = new Layout(json.name,
[json.language],
languages,
t(json.title),
json.layers.map(CustomLayoutFromJSON.LayerFromJson),
json.startZoom,
@ -270,6 +274,7 @@ export class CustomLayoutFromJSON {
new Combine(['<h3>', t(json.title), '</h3><br/>', t(json.description)])
);
layout.icon = json.icon;
layout.maintainer = json.maintainer;
return layout;
}

View file

@ -7,7 +7,7 @@ export interface Preset {
tags: Tag[],
title: string | UIElement,
description?: string | UIElement,
icon?: string
icon?: string | TagRenderingOptions
}
export class LayerDefinition {
@ -32,7 +32,7 @@ export class LayerDefinition {
* Not really used anymore
* This is meant to serve as icon in the buttons
*/
icon: string;
icon: string | TagRenderingOptions;
/**
* Only show this layer starting at this zoom level
*/
@ -58,7 +58,7 @@ export class LayerDefinition {
/**
* This UIElement is rendered as title element in the popup
*/
title: TagRenderingOptions | UIElement | string;
title: TagDependantUIElementConstructor | UIElement | string;
/**
* These are the questions/shown attributes in the popup
*/
@ -100,7 +100,7 @@ export class LayerDefinition {
icon: string,
minzoom: number,
overpassFilter: TagsFilter,
title?: TagRenderingOptions,
title?: TagDependantUIElementConstructor,
elementsToShow?: TagDependantUIElementConstructor[],
maxAllowedOverlapPercentage?: number,
wayHandling?: number,

View file

@ -1,183 +0,0 @@
import {LayerDefinition} from "../LayerDefinition";
import {And, Or, Tag} from "../../Logic/TagsFilter";
import {NameInline} from "../Questions/NameInline";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import Translations from "../../UI/i18n/Translations";
import T from "../../UI/i18n/Translation";
import {TagRenderingOptions} from "../TagRenderingOptions";
export class Bookcases extends LayerDefinition {
constructor() {
super("bookcases");
this.name = "boekenkast";
this.presets = [{
tags: [new Tag("amenity", "public_bookcase")],
description: "Add a new bookcase here",
title: Translations.t.bookcases.bookcase,
}];
this.icon = "./assets/bookcase.svg";
this.overpassFilter = new Tag("amenity", "public_bookcase");
this.minzoom = 11;
const Tr = Translations.t;
const Trq = Tr.bookcases.questions;
this.title = new NameInline(Translations.t.bookcases.bookcase);
this.elementsToShow = [
new ImageCarouselWithUploadConstructor(),
new TagRenderingOptions({
question: Trq.hasName,
freeform: {
key: "name",
template: "$$$",
renderTemplate: "", // We don't actually render it, only ask
placeholder: "",
extraTags: new Tag("noname", "")
},
mappings: [
{k: new Tag("noname", "yes"), txt: Trq.noname},
]
}),
new TagRenderingOptions(
{
question: Trq.capacity,
freeform: {
renderTemplate: Trq.capacityRender,
template: Trq.capacityInput,
key: "capacity",
placeholder: "aantal"
},
}
),
new TagRenderingOptions({
question: Trq.bookkinds,
mappings: [
{k: new Tag("books", "children"), txt: "Voornamelijk kinderboeken"},
{k: new Tag("books", "adults"), txt: "Voornamelijk boeken voor volwassenen"},
{k: new Tag("books", "children;adults"), txt: "Zowel kinderboeken als boeken voor volwassenen"}
],
}),
new TagRenderingOptions({
question: "Staat dit boekenruilkastje binnen of buiten?",
mappings: [
{k: new Tag("indoor", "yes"), txt: "Dit boekenruilkastje staat binnen"},
{k: new Tag("indoor", "no"), txt: "Dit boekenruilkastje staat buiten"},
{k: new Tag("indoor", ""), txt: "Dit boekenruilkastje staat buiten"}
]
}),
new TagRenderingOptions({
question: "Is dit boekenruilkastje vrij toegankelijk?",
mappings: [
{k: new Tag("access", "yes"), txt: "Ja, vrij toegankelijk"},
{k: new Tag("access", "customers"), txt: "Enkel voor klanten"},
]
}).OnlyShowIf(new Tag("indoor", "yes")),
new TagRenderingOptions({
question: "Wie (welke organisatie) beheert dit boekenruilkastje?",
freeform: {
key: "operator",
renderTemplate: "Dit boekenruilkastje wordt beheerd door {operator}",
template: "Dit boekenruilkastje wordt beheerd door $$$"
}
}),
new TagRenderingOptions({
question: "Zijn er openingsuren voor dit boekenruilkastje?",
mappings: [
{k: new Tag("opening_hours", "24/7"), txt: "Dag en nacht toegankelijk"},
{k: new Tag("opening_hours", ""), txt: "Dag en nacht toegankelijk"},
{k: new Tag("opening_hours", "sunrise-sunset"), txt: "Van zonsopgang tot zonsondergang"},
],
freeform: {
key: "opening_hours",
renderTemplate: "De openingsuren zijn {opening_hours}",
template: "De openingsuren zijn $$$"
}
}),
new TagRenderingOptions({
question: "Is dit boekenruilkastje deel van een netwerk?",
freeform: {
key: "brand",
renderTemplate: "Deel van het netwerk {brand}",
template: "Deel van het netwerk $$$"
},
mappings: [{
k: new And([new Tag("brand", "Little Free Library"), new Tag("nobrand", "")]),
txt: "Little Free Library"
},
{
k: new And([new Tag("brand", ""), new Tag("nobrand", "yes")]),
txt: "Maakt geen deel uit van een groter netwerk"
}]
}).OnlyShowIf(new Or([
new Tag("ref", ""),
new And([new Tag("ref","*"), new Tag("brand","")])
])),
new TagRenderingOptions({
question: "Wat is het referentienummer van dit boekenruilkastje?",
freeform: {
key: "ref",
template: "Het referentienummer is $$$",
renderTemplate: "Gekend als {brand} <b>{ref}</b>"
},
mappings: [
{k: new And([new Tag("brand",""), new Tag("nobrand","yes"), new Tag("ref", "")]),
txt: "Maakt geen deel uit van een netwerk"}
]
}).OnlyShowIf(new Tag("brand","*")),
new TagRenderingOptions({
question: "Wanneer werd dit boekenruilkastje geinstalleerd?",
priority: -1,
freeform: {
key: "start_date",
renderTemplate: "Geplaatst op {start_date}",
template: "Geplaatst op $$$"
}
}),
new TagRenderingOptions({
question: "Is er een website waar we er meer informatie is over dit boekenruilkastje?",
freeform: {
key: "website",
renderTemplate: "<a href='{website}' target='_blank'>Meer informatie over dit boekenruilkastje</a>",
template: "$$$",
placeholder: "website"
}
}),
new TagRenderingOptions({
freeform: {
key: "description",
renderTemplate: "<b>Beschrijving door de uitbater:</b><br>{description}",
template: "$$$",
}
})
];
this.style = function (tags) {
return {
icon: {
iconUrl: "assets/bookcase.svg",
iconSize: [40, 40],
iconAnchor: [20,20],
popupAnchor: [0, -15]
},
color: "#0000ff"
};
}
}
}

View file

@ -10,8 +10,9 @@ export class Layout {
public name: string;
public icon: string = "./assets/logo.svg";
public title: UIElement;
public maintainer: string;
public description: string | UIElement;
public socialImage: string = ""
public socialImage: string = "";
public layers: LayerDefinition[];
public welcomeMessage: UIElement;

View file

@ -1,20 +0,0 @@
import {Layout} from "../Layout";
import * as Layer from "../Layers/Bookcases";
import Translations from "../../UI/i18n/Translations";
import Combine from "../../UI/Base/Combine";
export class Bookcases extends Layout {
constructor() {
super("bookcases",
["nl", "en"],
Translations.t.bookcases.title,
[new Layer.Bookcases()],
14,
51.2,
3.2,
new Combine(["<h3>",Translations.t.bookcases.title,"</h3>", Translations.t.bookcases.description])
);
this.icon = "assets/bookcase.svg"
}
}

View file

@ -40,7 +40,14 @@ export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{
Priority(): number {
return this._embedded.Priority();
}
GetContent(tags: any): string {
if(!this.IsKnown(tags)){
return undefined;
}
return this._embedded.GetContent(tags);
}
private Matches(properties: any) : boolean{
return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties));
}

View file

@ -5,7 +5,7 @@ import {FixedUiElement} from "../UI/Base/FixedUiElement";
import {SaveButton} from "../UI/SaveButton";
import {VariableUiElement} from "../UI/Base/VariableUIElement";
import {TagDependantUIElement} from "./UIElementConstructor";
import {TextField} from "../UI/Input/TextField";
import {TextField, ValidatedTextField} from "../UI/Input/TextField";
import {InputElement} from "../UI/Input/InputElement";
import {InputElementWrapper} from "../UI/Input/InputElementWrapper";
import {FixedInputElement} from "../UI/Input/FixedInputElement";
@ -14,6 +14,7 @@ import Translations from "../UI/i18n/Translations";
import Locale from "../UI/i18n/Locale";
import {State} from "../State";
import {TagRenderingOptions} from "./TagRenderingOptions";
import Translation from "../UI/i18n/Translation";
export class TagRendering extends UIElement implements TagDependantUIElement {
@ -22,15 +23,15 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
private _priority: number;
private _question: UIElement;
private _mapping: { k: TagsFilter, txt: string | UIElement, priority?: number }[];
private _question: Translation;
private _mapping: { k: TagsFilter, txt: string | Translation, priority?: number }[];
private _tagsPreprocessor?: ((tags: any) => any);
private _freeform: {
key: string,
template: string | UIElement,
renderTemplate: string | UIElement,
placeholder?: string | UIElement,
key: string,
template: string | Translation,
renderTemplate: string | Translation,
placeholder?: string | Translation,
extraTags?: TagsFilter
};
@ -56,24 +57,25 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
constructor(tags: UIEventSource<any>, options: {
priority?: number
question?: string | UIElement,
question?: string | Translation,
freeform?: {
key: string,
template: string | UIElement,
renderTemplate: string | UIElement,
placeholder?: string | UIElement,
template: string | Translation,
renderTemplate: string | Translation,
placeholder?: string | Translation,
extraTags?: TagsFilter,
},
tagsPreprocessor?: ((tags: any) => any),
mappings?: { k: TagsFilter, txt: string | UIElement, priority?: number, substitute?: boolean }[]
mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean }[]
}) {
super(tags);
this.ListenTo(Locale.language);
this.ListenTo(this._questionSkipped);
this.ListenTo(this._editMode);
this.ListenTo(State.state.osmConnection.userDetails);
this.ListenTo(State.state?.osmConnection?.userDetails);
console.log("Creating tagRendering with", options)
const self = this;
@ -106,10 +108,10 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
};
if (choice.substitute) {
const newTags = this._tagsPreprocessor(this._source.data);
choiceSubbed = {
k: choice.k.substituteValues(
options.tagsPreprocessor(this._source.data)),
txt: choice.txt,
k: choice.k.substituteValues(newTags),
txt: this.ApplyTemplate(choice.txt),
priority: choice.priority
}
}
@ -168,12 +170,12 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
private InputElementFor(options: {
freeform?: {
key: string,
template: string | UIElement,
renderTemplate: string | UIElement,
placeholder?: string | UIElement,
template: string | Translation,
renderTemplate: string | Translation,
placeholder?: string | Translation,
extraTags?: TagsFilter,
},
mappings?: { k: TagsFilter, txt: string | UIElement, priority?: number, substitute?: boolean }[]
mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean }[]
}):
InputElement<TagsFilter> {
@ -189,7 +191,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
if(previousTexts.indexOf(mapping.txt) >= 0){
continue;
}
previousTexts.push(mapping.txt);
previousTexts.push(this.ApplyTemplate(mapping.txt));
elements.push(this.InputElementForMapping(mapping));
}
@ -201,7 +203,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
if (elements.length == 0) {
//console.warn("WARNING: no tagrendering with following options:", options);
console.warn("WARNING: no tagrendering with following options:", options);
return new FixedInputElement("This should not happen: no tag renderings defined", undefined);
}
if (elements.length == 1) {
@ -224,15 +226,15 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
}
const prepost = Translations.W(freeform.template).InnerRender()
.replace("$$$","$string$")
.replace("$$$", "$string$")
.split("$");
const type = prepost[1];
let isValid = TagRenderingOptions.inputValidation[type];
let isValid = ValidatedTextField.inputValidation[type];
if (isValid === undefined) {
isValid = (str) => true;
}
let formatter = TagRenderingOptions.formatting[type] ?? ((str) => str);
let formatter = ValidatedTextField.formatting[type] ?? ((str) => str);
const pickString =
(string: any) => {
@ -272,7 +274,10 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
toString: toString
});
return new InputElementWrapper(prepost[0], textField, prepost[2]);
const pre = prepost[0] !== undefined ? this.ApplyTemplate(prepost[0]) : "";
const post = prepost[2] !== undefined ? this.ApplyTemplate(prepost[2]) : "";
return new InputElementWrapper(pre, textField, post);
}
@ -323,7 +328,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
return true;
}
private RenderAnwser(): UIElement {
private RenderAnswer(): UIElement {
const tags = TagUtils.proprtiesToKV(this._source.data);
let freeform: UIElement = new FixedUiElement("");
@ -357,10 +362,9 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
// we render the found template
return this.ApplyTemplate(highestTemplate);
}
}
InnerRender(): string {
if (this.IsQuestioning() || this._editMode.data) {
// Not yet known or questioning, we have to ask a question
@ -378,13 +382,14 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
}
if (this.IsKnown()) {
const answer = this.RenderAnwser()
if (answer.IsEmpty()) {
const html = this.RenderAnswer().Render();
if (html === "") {
return "";
}
const html = answer.Render();
let editButton = "";
if (State.state.osmConnection.userDetails.data.loggedIn && this._question !== undefined) {
if (State.state?.osmConnection?.userDetails?.data?.loggedIn && this._question !== undefined) {
editButton = this._editButton.Render();
}
@ -403,24 +408,18 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
return this._priority;
}
private ApplyTemplate(template: string | UIElement): UIElement {
private ApplyTemplate(template: string | Translation): Translation {
if (template === undefined || template === null) {
throw "Trying to apply a template, but the template is null/undefined"
}
const contents = Translations.W(template).map(contents =>
{
let templateStr = "";
if (template instanceof UIElement) {
templateStr = template.Render();
} else {
templateStr = template;
}
const tags = this._tagsPreprocessor(this._source.data);
return TagUtils.ApplyTemplate(templateStr, tags);
}, [this._source]
);
return new VariableUiElement(contents);
if (typeof (template) === "string") {
const tags = this._tagsPreprocessor(this._source.data);
return new Translation ({en:TagUtils.ApplyTemplate(template, tags)});
}
const tags = this._tagsPreprocessor(this._source.data);
return template.Subs(tags);
}

View file

@ -5,29 +5,12 @@ import {UIElement} from "../UI/UIElement";
import {TagsFilter, TagUtils} from "../Logic/TagsFilter";
import {OnlyShowIfConstructor} from "./OnlyShowIf";
import {UIEventSource} from "../Logic/UIEventSource";
import Translation from "../UI/i18n/Translation";
export class TagRenderingOptions implements TagDependantUIElementConstructor {
public static inputValidation = {
"$": (str) => true,
"string": (str) => true,
"int": (str) => str.indexOf(".") < 0 && !isNaN(Number(str)),
"nat": (str) => str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0,
"float": (str) => !isNaN(Number(str)),
"pfloat": (str) => !isNaN(Number(str)) && Number(str) > 0,
"email": (str) => EmailValidator.validate(str),
"phone": (str, country) => {
return parsePhoneNumberFromString(str, country.toUpperCase())?.isValid() ?? false;
},
}
public static formatting = {
"phone": (str, country) => {
console.log("country formatting", country)
return parsePhoneNumberFromString(str, country.toUpperCase()).formatInternational()
}
}
/**
* Notes: by not giving a 'question', one disables the question form alltogether
@ -35,16 +18,16 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
public options: {
priority?: number;
question?: string | UIElement;
question?: string | Translation;
freeform?: {
key: string;
tagsPreprocessor?: (tags: any) => any;
template: string | UIElement;
renderTemplate: string | UIElement;
placeholder?: string | UIElement;
template: string | Translation;
renderTemplate: string | Translation;
placeholder?: string | Translation;
extraTags?: TagsFilter
};
mappings?: { k: TagsFilter; txt: string | UIElement; priority?: number, substitute?: boolean }[]
mappings?: { k: TagsFilter; txt: string | Translation; priority?: number, substitute?: boolean }[]
};
@ -57,7 +40,7 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
* If 'question' is undefined, then the question is never asked at all
* If the question is "" (empty string) then the question is
*/
question?: UIElement | string,
question?: Translation | string,
/**
* What is the priority of the question.
@ -78,7 +61,7 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
*
*
*/
mappings?: { k: TagsFilter, txt: UIElement | string, priority?: number, substitute?: boolean }[],
mappings?: { k: TagsFilter, txt: Translation | string, priority?: number, substitute?: boolean }[],
/**
@ -88,9 +71,9 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
*/
freeform?: {
key: string,
template: string | UIElement,
renderTemplate: string | UIElement
placeholder?: string | UIElement,
template: string | Translation,
renderTemplate: string | Translation
placeholder?: string | Translation,
extraTags?: TagsFilter,
},
@ -129,8 +112,33 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
return true;
}
GetContent(tags: any): string {
const tagsKV = TagUtils.proprtiesToKV(tags);
for (const oneOnOneElement of this.options.mappings ?? []) {
if (oneOnOneElement.k === null || oneOnOneElement.k.matches(tagsKV)) {
const mapping = oneOnOneElement.txt;
if (typeof (mapping) === "string") {
return mapping;
} else {
return mapping.InnerRender();
}
}
}
if (this.options.freeform !== undefined) {
let template = this.options.freeform.renderTemplate;
if (typeof (template) !== "string") {
template = template.InnerRender();
}
return TagUtils.ApplyTemplate(template, tags);
}
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 }; mappings?: { k: TagsFilter; txt: string | Translation; priority?: number; substitute?: boolean }[] }) => TagDependantUIElement;
public static tagRendering : (tags: UIEventSource<any>, options: { priority?: number; question?: string | UIElement; freeform?: { key: string; tagsPreprocessor?: (tags: any) => any; template: string | UIElement; renderTemplate: string | UIElement; placeholder?: string | UIElement; extraTags?: TagsFilter }; mappings?: { k: TagsFilter; txt: string | UIElement; priority?: number; substitute?: boolean }[] }) => TagDependantUIElement;
construct(dependencies: Dependencies): TagDependantUIElement {
return TagRenderingOptions.tagRendering(dependencies.tags, this.options);
}

View file

@ -12,6 +12,8 @@ export interface TagDependantUIElementConstructor {
IsKnown(properties: any): boolean;
IsQuestioning(properties: any): boolean;
Priority(): number;
GetContent(tags: any): string;
}
export abstract class TagDependantUIElement extends UIElement {