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

View file

@ -2,15 +2,15 @@ import {TagRenderingOptions} from "../TagRenderingOptions";
import {LayerDefinition, Preset} from "../LayerDefinition"; import {LayerDefinition, Preset} from "../LayerDefinition";
import {Layout} from "../Layout"; import {Layout} from "../Layout";
import Translation from "../../UI/i18n/Translation"; import Translation from "../../UI/i18n/Translation";
import {type} from "os";
import Combine from "../../UI/Base/Combine"; import Combine from "../../UI/Base/Combine";
import {UIElement} from "../../UI/UIElement"; import {And, Tag} from "../../Logic/TagsFilter";
import {And, Tag, TagsFilter} from "../../Logic/TagsFilter";
import FixedText from "../Questions/FixedText"; import FixedText from "../Questions/FixedText";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; 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... // If this key is present, then...
key?: string, key?: string,
// Use this string to render // Use this string to render
@ -33,11 +33,11 @@ export interface TagRenderingConfigJson {
export interface LayerConfigJson { export interface LayerConfigJson {
id: string; id: string;
icon: string; icon: TagRenderingConfigJson;
title: TagRenderingConfigJson; title: TagRenderingConfigJson;
description: string; description: string;
minzoom: number, minzoom: number,
color: string; color: TagRenderingConfigJson;
overpassTags: string | string[] | { k: string, v: string }[]; overpassTags: string | string[] | { k: string, v: string }[];
presets: [ presets: [
{ {
@ -58,7 +58,8 @@ export interface LayoutConfigJson {
name: string; name: string;
title: string; title: string;
description: string; description: string;
language: string; maintainer: string;
language: string[];
layers: LayerConfigJson[], layers: LayerConfigJson[],
startZoom: number; startZoom: number;
startLat: number; startLat: number;
@ -71,86 +72,38 @@ export interface LayoutConfigJson {
export class CustomLayoutFromJSON { 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 { public static FromQueryParam(layoutFromBase64: string): Layout {
if(layoutFromBase64 === "test"){ return CustomLayoutFromJSON.LayoutFromJSON(JSON.parse(atob(layoutFromBase64)));
console.log(btoa(JSON.stringify(CustomLayoutFromJSON.exampleLayout)));
return CustomLayoutFromJSON.LayoutFromJSON(CustomLayoutFromJSON.exampleLayout);
}
const spec = JSON.parse(atob(layoutFromBase64));
return CustomLayoutFromJSON.LayoutFromJSON(spec);
} }
private static TagRenderingFromJson(json: any): TagRenderingOptions { public static TagRenderingFromJson(json: any): TagDependantUIElementConstructor {
if (typeof (json) === "string") { if (typeof (json) === "string") {
return new FixedText(json); return new FixedText(json);
} }
let freeform = undefined; let freeform = undefined;
if (json.key !== undefined && json.key !== "" && json.render !== undefined) { if (json.render !== undefined) {
const type = json.type ?? "text"; 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 = { freeform = {
key: json.key, key: json.key,
template: json.render.replace("{" + json.key + "}", "$" + type + "$"), template: template,
renderTemplate: json.render, renderTemplate: renderTemplate,
extraTags: CustomLayoutFromJSON.TagsFromJson(json.addExtraTags), 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; let mappings = undefined;
@ -158,30 +111,37 @@ export class CustomLayoutFromJSON {
mappings = []; mappings = [];
for (const mapping of json.mappings) { for (const mapping of json.mappings) {
mappings.push({ 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({ const rendering = new TagRenderingOptions({
question: json.question, question: CustomLayoutFromJSON.MaybeTranslation(json.question),
freeform: freeform, freeform: freeform,
mappings: mappings 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 { private static PresetFromJson(layout: any, preset: any): Preset {
const t = CustomLayoutFromJSON.MaybeTranslation; const t = CustomLayoutFromJSON.MaybeTranslation;
const tags = CustomLayoutFromJSON.TagsFromJson; const tags = CustomLayoutFromJSON.TagsFromJson;
return { return {
icon: preset.icon ?? layout.icon, icon: preset.icon ?? CustomLayoutFromJSON.TagRenderingFromJson(layout.icon),
tags: tags(preset.tags) ?? tags(layout.overpassTags), tags: tags(preset.tags) ?? tags(layout.overpassTags),
title: t(preset.title) ?? t(layout.title), title: t(preset.title) ?? t(layout.title),
description: t(preset.description) ?? t(layout.description) 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, color: string,
weight?: number, weight?: number,
icon: { icon: {
@ -189,12 +149,17 @@ export class CustomLayoutFromJSON {
iconSize: number[], iconSize: number[],
}, },
}) { }) {
const iconRendering: TagDependantUIElementConstructor = CustomLayoutFromJSON.TagRenderingFromJson(layout.icon);
const colourRendering = CustomLayoutFromJSON.TagRenderingFromJson(layout.color);
return (tags) => { return (tags) => {
const iconUrl = iconRendering.GetContent(tags);
const stroke = colourRendering.GetContent(tags);
return { return {
color: layout.color, color: stroke,
weight: 10, weight: 10,
icon: { icon: {
iconUrl: layout.icon, iconUrl: iconUrl,
iconSize: [40, 40], iconSize: [40, 40],
}, },
} }
@ -205,41 +170,76 @@ export class CustomLayoutFromJSON {
if (json === undefined) { if (json === undefined) {
return undefined; return undefined;
} }
console.log(json)
if (typeof (json) === "string") { if (typeof (json) === "string") {
const kv = json.split("="); let kv: string[] = undefined;
return new Tag(kv[0].trim(), kv[1].trim()); 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()) return new Tag(json.k.trim(), json.v.trim())
} }
private static TagsFromJson(json: string | { k: string, v: string }[]): Tag[] { public static TagsFromJson(json: string | { k: string, v: string }[]): Tag[] {
if (json === undefined || json === "") { if (json === undefined) {
return undefined; return undefined;
} }
if (typeof (json) === "string") { if (json === "") {
return json.split(",").map(CustomLayoutFromJSON.TagFromJson); 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 { private static LayerFromJson(json: any): LayerDefinition {
const t = CustomLayoutFromJSON.MaybeTranslation; const t = CustomLayoutFromJSON.MaybeTranslation;
const tr = CustomLayoutFromJSON.TagRenderingFromJson; 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( return new LayerDefinition(
json.id, json.id,
{ {
description: t(json.description), description: t(json.description),
name: t(json.title), name: t(json.title),
icon: json.icon, icon: icon,
minzoom: json.minzoom, minzoom: json.minzoom,
title: tr(json.title) , title: tr(json.title),
presets: json.presets.map((preset) => { presets: json.presets.map((preset) => {
return CustomLayoutFromJSON.PresetFromJson(json, preset) return CustomLayoutFromJSON.PresetFromJson(json, preset)
}), }),
elementsToShow: elementsToShow:
[new ImageCarouselWithUploadConstructor()].concat(json.tagRenderings.map(tr)), [new ImageCarouselWithUploadConstructor()].concat(json.tagRenderings.map(tr)),
overpassFilter: new And(CustomLayoutFromJSON.TagsFromJson(json.overpassTags)), overpassFilter: new And(tags),
wayHandling: LayerDefinition.WAYHANDLING_CENTER_AND_WAY, wayHandling: LayerDefinition.WAYHANDLING_CENTER_AND_WAY,
maxAllowedOverlapPercentage: 0, maxAllowedOverlapPercentage: 0,
style: CustomLayoutFromJSON.StyleFromJson(json, json.style) style: CustomLayoutFromJSON.StyleFromJson(json, json.style)
@ -260,8 +260,12 @@ export class CustomLayoutFromJSON {
public static LayoutFromJSON(json: any) { public static LayoutFromJSON(json: any) {
const t = CustomLayoutFromJSON.MaybeTranslation; const t = CustomLayoutFromJSON.MaybeTranslation;
let languages = json.language;
if(typeof (json.language) === "string"){
languages = [json.language];
}
const layout = new Layout(json.name, const layout = new Layout(json.name,
[json.language], languages,
t(json.title), t(json.title),
json.layers.map(CustomLayoutFromJSON.LayerFromJson), json.layers.map(CustomLayoutFromJSON.LayerFromJson),
json.startZoom, json.startZoom,
@ -270,6 +274,7 @@ export class CustomLayoutFromJSON {
new Combine(['<h3>', t(json.title), '</h3><br/>', t(json.description)]) new Combine(['<h3>', t(json.title), '</h3><br/>', t(json.description)])
); );
layout.icon = json.icon; layout.icon = json.icon;
layout.maintainer = json.maintainer;
return layout; return layout;
} }

View file

@ -7,7 +7,7 @@ export interface Preset {
tags: Tag[], tags: Tag[],
title: string | UIElement, title: string | UIElement,
description?: string | UIElement, description?: string | UIElement,
icon?: string icon?: string | TagRenderingOptions
} }
export class LayerDefinition { export class LayerDefinition {
@ -32,7 +32,7 @@ export class LayerDefinition {
* Not really used anymore * Not really used anymore
* This is meant to serve as icon in the buttons * This is meant to serve as icon in the buttons
*/ */
icon: string; icon: string | TagRenderingOptions;
/** /**
* Only show this layer starting at this zoom level * 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 * 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 * These are the questions/shown attributes in the popup
*/ */
@ -100,7 +100,7 @@ export class LayerDefinition {
icon: string, icon: string,
minzoom: number, minzoom: number,
overpassFilter: TagsFilter, overpassFilter: TagsFilter,
title?: TagRenderingOptions, title?: TagDependantUIElementConstructor,
elementsToShow?: TagDependantUIElementConstructor[], elementsToShow?: TagDependantUIElementConstructor[],
maxAllowedOverlapPercentage?: number, maxAllowedOverlapPercentage?: number,
wayHandling?: 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 name: string;
public icon: string = "./assets/logo.svg"; public icon: string = "./assets/logo.svg";
public title: UIElement; public title: UIElement;
public maintainer: string;
public description: string | UIElement; public description: string | UIElement;
public socialImage: string = "" public socialImage: string = "";
public layers: LayerDefinition[]; public layers: LayerDefinition[];
public welcomeMessage: UIElement; 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 { Priority(): number {
return this._embedded.Priority(); return this._embedded.Priority();
} }
GetContent(tags: any): string {
if(!this.IsKnown(tags)){
return undefined;
}
return this._embedded.GetContent(tags);
}
private Matches(properties: any) : boolean{ private Matches(properties: any) : boolean{
return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties)); 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 {SaveButton} from "../UI/SaveButton";
import {VariableUiElement} from "../UI/Base/VariableUIElement"; import {VariableUiElement} from "../UI/Base/VariableUIElement";
import {TagDependantUIElement} from "./UIElementConstructor"; import {TagDependantUIElement} from "./UIElementConstructor";
import {TextField} from "../UI/Input/TextField"; import {TextField, ValidatedTextField} from "../UI/Input/TextField";
import {InputElement} from "../UI/Input/InputElement"; import {InputElement} from "../UI/Input/InputElement";
import {InputElementWrapper} from "../UI/Input/InputElementWrapper"; import {InputElementWrapper} from "../UI/Input/InputElementWrapper";
import {FixedInputElement} from "../UI/Input/FixedInputElement"; import {FixedInputElement} from "../UI/Input/FixedInputElement";
@ -14,6 +14,7 @@ import Translations from "../UI/i18n/Translations";
import Locale from "../UI/i18n/Locale"; import Locale from "../UI/i18n/Locale";
import {State} from "../State"; import {State} from "../State";
import {TagRenderingOptions} from "./TagRenderingOptions"; import {TagRenderingOptions} from "./TagRenderingOptions";
import Translation from "../UI/i18n/Translation";
export class TagRendering extends UIElement implements TagDependantUIElement { export class TagRendering extends UIElement implements TagDependantUIElement {
@ -22,15 +23,15 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
private _priority: number; private _priority: number;
private _question: UIElement; private _question: Translation;
private _mapping: { k: TagsFilter, txt: string | UIElement, priority?: number }[]; private _mapping: { k: TagsFilter, txt: string | Translation, priority?: number }[];
private _tagsPreprocessor?: ((tags: any) => any); private _tagsPreprocessor?: ((tags: any) => any);
private _freeform: { private _freeform: {
key: string, key: string,
template: string | UIElement, template: string | Translation,
renderTemplate: string | UIElement, renderTemplate: string | Translation,
placeholder?: string | UIElement, placeholder?: string | Translation,
extraTags?: TagsFilter extraTags?: TagsFilter
}; };
@ -56,24 +57,25 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
constructor(tags: UIEventSource<any>, options: { constructor(tags: UIEventSource<any>, options: {
priority?: number priority?: number
question?: string | UIElement, question?: string | Translation,
freeform?: { freeform?: {
key: string, key: string,
template: string | UIElement, template: string | Translation,
renderTemplate: string | UIElement, renderTemplate: string | Translation,
placeholder?: string | UIElement, placeholder?: string | Translation,
extraTags?: TagsFilter, extraTags?: TagsFilter,
}, },
tagsPreprocessor?: ((tags: any) => any), 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); super(tags);
this.ListenTo(Locale.language); this.ListenTo(Locale.language);
this.ListenTo(this._questionSkipped); this.ListenTo(this._questionSkipped);
this.ListenTo(this._editMode); 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; const self = this;
@ -106,10 +108,10 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
}; };
if (choice.substitute) { if (choice.substitute) {
const newTags = this._tagsPreprocessor(this._source.data);
choiceSubbed = { choiceSubbed = {
k: choice.k.substituteValues( k: choice.k.substituteValues(newTags),
options.tagsPreprocessor(this._source.data)), txt: this.ApplyTemplate(choice.txt),
txt: choice.txt,
priority: choice.priority priority: choice.priority
} }
} }
@ -168,12 +170,12 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
private InputElementFor(options: { private InputElementFor(options: {
freeform?: { freeform?: {
key: string, key: string,
template: string | UIElement, template: string | Translation,
renderTemplate: string | UIElement, renderTemplate: string | Translation,
placeholder?: string | UIElement, placeholder?: string | Translation,
extraTags?: TagsFilter, extraTags?: TagsFilter,
}, },
mappings?: { k: TagsFilter, txt: string | UIElement, priority?: number, substitute?: boolean }[] mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean }[]
}): }):
InputElement<TagsFilter> { InputElement<TagsFilter> {
@ -189,7 +191,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
if(previousTexts.indexOf(mapping.txt) >= 0){ if(previousTexts.indexOf(mapping.txt) >= 0){
continue; continue;
} }
previousTexts.push(mapping.txt); previousTexts.push(this.ApplyTemplate(mapping.txt));
elements.push(this.InputElementForMapping(mapping)); elements.push(this.InputElementForMapping(mapping));
} }
@ -201,7 +203,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
if (elements.length == 0) { 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); return new FixedInputElement("This should not happen: no tag renderings defined", undefined);
} }
if (elements.length == 1) { if (elements.length == 1) {
@ -224,15 +226,15 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
} }
const prepost = Translations.W(freeform.template).InnerRender() const prepost = Translations.W(freeform.template).InnerRender()
.replace("$$$","$string$") .replace("$$$", "$string$")
.split("$"); .split("$");
const type = prepost[1]; const type = prepost[1];
let isValid = TagRenderingOptions.inputValidation[type]; let isValid = ValidatedTextField.inputValidation[type];
if (isValid === undefined) { if (isValid === undefined) {
isValid = (str) => true; isValid = (str) => true;
} }
let formatter = TagRenderingOptions.formatting[type] ?? ((str) => str); let formatter = ValidatedTextField.formatting[type] ?? ((str) => str);
const pickString = const pickString =
(string: any) => { (string: any) => {
@ -272,7 +274,10 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
toString: toString 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; return true;
} }
private RenderAnwser(): UIElement { private RenderAnswer(): UIElement {
const tags = TagUtils.proprtiesToKV(this._source.data); const tags = TagUtils.proprtiesToKV(this._source.data);
let freeform: UIElement = new FixedUiElement(""); let freeform: UIElement = new FixedUiElement("");
@ -357,10 +362,9 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
// we render the found template // we render the found template
return this.ApplyTemplate(highestTemplate); return this.ApplyTemplate(highestTemplate);
} }
} }
InnerRender(): string { InnerRender(): string {
if (this.IsQuestioning() || this._editMode.data) { if (this.IsQuestioning() || this._editMode.data) {
// Not yet known or questioning, we have to ask a question // 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()) { if (this.IsKnown()) {
const answer = this.RenderAnwser() const html = this.RenderAnswer().Render();
if (answer.IsEmpty()) { if (html === "") {
return ""; return "";
} }
const html = answer.Render();
let editButton = ""; 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(); editButton = this._editButton.Render();
} }
@ -403,24 +408,18 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
return this._priority; return this._priority;
} }
private ApplyTemplate(template: string | UIElement): UIElement { private ApplyTemplate(template: string | Translation): Translation {
if (template === undefined || template === null) { if (template === undefined || template === null) {
throw "Trying to apply a template, but the template is null/undefined" throw "Trying to apply a template, but the template is null/undefined"
} }
const contents = Translations.W(template).map(contents => if (typeof (template) === "string") {
{ const tags = this._tagsPreprocessor(this._source.data);
let templateStr = ""; return new Translation ({en:TagUtils.ApplyTemplate(template, tags)});
if (template instanceof UIElement) { }
templateStr = template.Render(); const tags = this._tagsPreprocessor(this._source.data);
} else {
templateStr = template; return template.Subs(tags);
}
const tags = this._tagsPreprocessor(this._source.data);
return TagUtils.ApplyTemplate(templateStr, tags);
}, [this._source]
);
return new VariableUiElement(contents);
} }

View file

@ -5,29 +5,12 @@ import {UIElement} from "../UI/UIElement";
import {TagsFilter, TagUtils} from "../Logic/TagsFilter"; import {TagsFilter, TagUtils} from "../Logic/TagsFilter";
import {OnlyShowIfConstructor} from "./OnlyShowIf"; import {OnlyShowIfConstructor} from "./OnlyShowIf";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import Translation from "../UI/i18n/Translation";
export class TagRenderingOptions implements TagDependantUIElementConstructor { 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 * Notes: by not giving a 'question', one disables the question form alltogether
@ -35,16 +18,16 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
public options: { public options: {
priority?: number; priority?: number;
question?: string | UIElement; question?: string | Translation;
freeform?: { freeform?: {
key: string; key: string;
tagsPreprocessor?: (tags: any) => any; tagsPreprocessor?: (tags: any) => any;
template: string | UIElement; template: string | Translation;
renderTemplate: string | UIElement; renderTemplate: string | Translation;
placeholder?: string | UIElement; placeholder?: string | Translation;
extraTags?: TagsFilter 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 'question' is undefined, then the question is never asked at all
* If the question is "" (empty string) then the question is * If the question is "" (empty string) then the question is
*/ */
question?: UIElement | string, question?: Translation | string,
/** /**
* What is the priority of the question. * 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?: { freeform?: {
key: string, key: string,
template: string | UIElement, template: string | Translation,
renderTemplate: string | UIElement renderTemplate: string | Translation
placeholder?: string | UIElement, placeholder?: string | Translation,
extraTags?: TagsFilter, extraTags?: TagsFilter,
}, },
@ -129,8 +112,33 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
return true; 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 { construct(dependencies: Dependencies): TagDependantUIElement {
return TagRenderingOptions.tagRendering(dependencies.tags, this.options); return TagRenderingOptions.tagRendering(dependencies.tags, this.options);
} }

View file

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

View file

@ -53,7 +53,7 @@ export class FilteredLayer {
this._style = layerDef.style; this._style = layerDef.style;
if (this._style === undefined) { if (this._style === undefined) {
this._style = function () { this._style = function () {
return {icon: {iconUrl: "./assets/bug.svg"}, color: "#000000"}; return {icon: {iconUrl: "./assets/bug.svg"}, color: "#000"};
} }
} }
this.name = name; this.name = name;
@ -94,9 +94,9 @@ export class FilteredLayer {
var tags = TagUtils.proprtiesToKV(feature.properties); var tags = TagUtils.proprtiesToKV(feature.properties);
if (this.filters.matches(tags)) { if (this.filters.matches(tags)) {
const centerPoint = GeoOperations.centerpoint(feature); const centerPoint = GeoOperations.centerpoint(feature);
feature.properties["_surface"] = GeoOperations.surfaceAreaInSqMeters(feature); feature.properties["_surface"] = ""+GeoOperations.surfaceAreaInSqMeters(feature);
const lat = centerPoint.geometry.coordinates[1]; const lat = ""+centerPoint.geometry.coordinates[1];
const lon = centerPoint.geometry.coordinates[0] const lon = ""+centerPoint.geometry.coordinates[0]
feature.properties["_lon"] = lat; feature.properties["_lon"] = lat;
feature.properties["_lat"] = lon; feature.properties["_lat"] = lon;
FilteredLayer.grid.getCode(lat, lon, (error, code) => { FilteredLayer.grid.getCode(lat, lon, (error, code) => {
@ -233,8 +233,6 @@ export class FilteredLayer {
const style = self._style(featureX.properties); const style = self._style(featureX.properties);
if (featureX === feature) { if (featureX === feature) {
console.log("Selected element is", featureX.properties.id) console.log("Selected element is", featureX.properties.id)
// style.weight = style.weight * 2;
// console.log(style)
} }
return style; return style;
}); });

View file

@ -19,12 +19,9 @@ export class Changes {
public readonly pendingChangesES = new UIEventSource<number>(this._pendingChanges.length); public readonly pendingChangesES = new UIEventSource<number>(this._pendingChanges.length);
public readonly isSaving = new UIEventSource(false); public readonly isSaving = new UIEventSource(false);
private readonly _changesetComment: string;
constructor( constructor(
changesetComment: string,
state: State) { state: State) {
this._changesetComment = changesetComment;
this.SetupAutoSave(state); this.SetupAutoSave(state);
this.LastEffortSave(); this.LastEffortSave();
@ -74,6 +71,9 @@ export class Changes {
const eventSource = State.state.allElements.getElement(elementId); const eventSource = State.state.allElements.getElement(elementId);
eventSource.data[key] = value; eventSource.data[key] = value;
if(value === undefined || value === ""){
delete eventSource.data[key];
}
eventSource.ping(); eventSource.ping();
// We get the id from the event source, as that ID might be rewritten // We get the id from the event source, as that ID might be rewritten
this._pendingChanges.push({elementId: eventSource.data.id, key: key, value: value}); this._pendingChanges.push({elementId: eventSource.data.id, key: key, value: value});
@ -223,7 +223,7 @@ export class Changes {
console.log("Beginning upload..."); console.log("Beginning upload...");
// At last, we build the changeset and upload // At last, we build the changeset and upload
State.state.osmConnection.UploadChangeset(self._changesetComment, State.state.osmConnection.UploadChangeset(
function (csId) { function (csId) {
let modifications = ""; let modifications = "";

View file

@ -2,6 +2,7 @@
import osmAuth from "osm-auth"; import osmAuth from "osm-auth";
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import {CustomLayersState} from "../CustomLayersState"; import {CustomLayersState} from "../CustomLayersState";
import {State} from "../../State";
export class UserDetails { export class UserDetails {
@ -262,16 +263,16 @@ export class OsmConnection {
const newId = parseInt(node.attributes.new_id.value); const newId = parseInt(node.attributes.new_id.value);
if (oldId !== undefined && newId !== undefined && if (oldId !== undefined && newId !== undefined &&
!isNaN(oldId) && !isNaN(newId)) { !isNaN(oldId) && !isNaN(newId)) {
mapping["node/"+oldId] = "node/"+newId; mapping["node/" + oldId] = "node/" + newId;
} }
} }
return mapping; return mapping;
} }
public UploadChangeset(comment: string, generateChangeXML: ((csid: string) => string), public UploadChangeset(generateChangeXML: (csid: string) => string,
handleMapping: ((idMapping: any) => void), handleMapping: (idMapping: any) => void,
continuation: (() => void)) { continuation: () => void) {
if (this._dryRun) { if (this._dryRun) {
console.log("NOT UPLOADING as dryrun is true"); console.log("NOT UPLOADING as dryrun is true");
@ -282,7 +283,7 @@ export class OsmConnection {
} }
const self = this; const self = this;
this.OpenChangeset(comment, this.OpenChangeset(
function (csId) { function (csId) {
var changesetXML = generateChangeXML(csId); var changesetXML = generateChangeXML(csId);
self.AddChange(csId, changesetXML, self.AddChange(csId, changesetXML,
@ -300,17 +301,20 @@ export class OsmConnection {
} }
private OpenChangeset(comment: string, continuation: ((changesetId: string) => void)) { private OpenChangeset(continuation: (changesetId: string) => void) {
const layout = State.state.layoutToUse.data;
this.auth.xhr({ this.auth.xhr({
method: 'PUT', method: 'PUT',
path: '/api/0.6/changeset/create', path: '/api/0.6/changeset/create',
options: { header: { 'Content-Type': 'text/xml' } }, options: {header: {'Content-Type': 'text/xml'}},
content: '<osm><changeset>' + content: [`<osm><changeset>`,
'<tag k="created_by" v="MapComplete 0.0.0" />' + `<tag k="created_by" v="MapComplete ${State.vNumber}" />`,
'<tag k="comment" v="' + comment + '"/>' + `<tag k="comment" v="Adding data with #MapComplete"/>`,
'</changeset></osm>' `<tag k="theme" v="${layout.name}">`,
layout.maintainer !== undefined ? `<tag k="theme-creator" v="${layout.maintainer}">` : "",
`</changeset></osm>`].join("")
}, function (err, response) { }, function (err, response) {
if (response === undefined) { if (response === undefined) {
console.log("err", err); console.log("err", err);

View file

@ -83,6 +83,9 @@ export abstract class OsmObject {
console.log("WARNING: overwriting ",oldV, " with ", v," for key ",k) console.log("WARNING: overwriting ",oldV, " with ", v," for key ",k)
} }
this.tags[k] = v; this.tags[k] = v;
if(v === undefined || v === ""){
delete this.tags[k];
}
this.changed = true; this.changed = true;
} }

View file

@ -60,9 +60,6 @@ export class Tag extends TagsFilter {
public invertValue: boolean public invertValue: boolean
constructor(key: string | RegExp, value: string | RegExp, invertValue = false) { constructor(key: string | RegExp, value: string | RegExp, invertValue = false) {
if (value === "*" && invertValue) {
throw new Error("Invalid combination: invertValue && value == *")
}
if (value instanceof RegExp && invertValue) { if (value instanceof RegExp && invertValue) {
throw new Error("Unsupported combination: RegExp value and inverted value (use regex to invert the match)") throw new Error("Unsupported combination: RegExp value and inverted value (use regex to invert the match)")
@ -88,12 +85,17 @@ export class Tag extends TagsFilter {
matches(tags: { k: string; v: string }[]): boolean { matches(tags: { k: string; v: string }[]): boolean {
for (const tag of tags) { for (const tag of tags) {
if (Tag.regexOrStrMatches(this.key, tag.k)) { if (Tag.regexOrStrMatches(this.key, tag.k)) {
if (tag.v === "") { if (tag.v === "") {
// This tag has been removed // This tag has been removed -> always matches false
return this.value === "" return false;
} }
if (this.value === "*") { if (this.value === "*") {
// Any is allowed // Any is allowed (as long as the tag is not empty)
return true;
}
if(this.value === tag.v){
return true; return true;
} }
@ -288,4 +290,12 @@ export class TagUtils {
} }
return template; return template;
} }
static KVtoProperties(tags: Tag[]): any {
const properties = {};
for (const tag of tags) {
properties[tag.key] = tag.value
}
return properties;
}
} }

View file

@ -84,6 +84,10 @@ Geolocation is available on mobile only throught hte device's GPS location (so n
TODO: erase cookies of third party websites and API's TODO: erase cookies of third party websites and API's
# Translating MapComplete
Help to translate mapcomplete. Fork this project, open [the file containing all translations](https://github.com/pietervdvn/MapComplete/blob/master/UI/i18n/Translations.ts), add your language and send a pull request.
# Attributions: # Attributions:
Data from OpenStreetMap Data from OpenStreetMap

View file

@ -24,7 +24,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.4"; public static vNumber = "0.0.5";
public static runningFromConsole: boolean = false; public static runningFromConsole: boolean = false;
@ -181,9 +181,7 @@ export class State {
this.allElements = new ElementStorage(); this.allElements = new ElementStorage();
this.changes = new Changes( this.changes = new Changes(this);
"Beantwoorden van vragen met #MapComplete voor vragenset #" + this.layoutToUse.data.name,
this);
if(State.runningFromConsole){ if(State.runningFromConsole){
console.warn("running from console - not initializing map. Assuming test.html"); console.warn("running from console - not initializing map. Assuming test.html");

View file

@ -2,32 +2,53 @@ import {LayoutConfigJson} from "../../Customizations/JSON/CustomLayoutFromJSON";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import {Button} from "../Base/Button";
import {VariableUiElement} from "../Base/VariableUIElement";
export class Preview extends UIElement { export class Preview extends UIElement {
private url: UIEventSource<string>; private url: UIEventSource<string>;
private config: UIEventSource<LayoutConfigJson>; private config: UIEventSource<LayoutConfigJson>;
private currentPreview = new UIEventSource<string>("")
private reloadButton: Button;
private otherPreviews: VariableUiElement;
constructor(url: UIEventSource<string>, config: UIEventSource<LayoutConfigJson>) { constructor(url: UIEventSource<string>, config: UIEventSource<LayoutConfigJson>) {
super(url); super(undefined);
this.config = config; this.config = config;
this.url = url; this.url = url;
this.reloadButton = new Button("Reload the preview", () => {
this.currentPreview.setData(`<iframe width="99%" height="70%" src="${this.url.data}"></iframe>` +
'<p class="alert">The above preview is in testmode. Changes will not be sent to OSM, so feel free to add points and answer questions</p> ',
);
});
this.ListenTo(this.currentPreview);
this.otherPreviews = new VariableUiElement(this.url.map(url => {
return [
`<h2>Your link</h2>`,
'<span class="alert">Bookmark the link below</span><br/>',
'MapComplete has no backend. The <i>entire</i> theme configuration is saved in the following URL. This means that this URL is needed to revive and change your MapComplete instance.<br/>',
`<a target='_blank' href='${this.url.data}'>${this.url.data}</a><br/>`,
'<h2>JSON-configuration</h2>',
'You can see the configuration in JSON format below.<br/>',
'<span class=\'literal-code iframe-code-block\' style="width:95%">',
JSON.stringify(this.config.data, null, 2).replace(/\n/g, "<br/>").replace(/ /g, "&nbsp;"),
'</span>'
].join("")
}));
} }
InnerRender(): string { InnerRender(): string {
const url = this.url.data; const url = this.url.data;
return new Combine([ return new Combine([
`<iframe width="99%" height="70%" src="${this.url.data}"></iframe>`, new VariableUiElement(this.currentPreview),
'<p class="alert">The above preview is in testmode. Changes will not be sent to OSM, so feel free to add points and answer questions</p> ', this.reloadButton,
`<h2>Your link</h2>`, this.otherPreviews
'<span class="alert">Bookmark the link below</span><br/>',
'MapComplete has no backend. The <i>entire</i> theme configuration is saved in the following URL. This means that this URL is needed to revive and change your MapComplete instance.<br/>',
`<a target='_blank' href='${this.url.data}'>${this.url.data}</a><br/>`,
'<h2>JSON-configuration</h2>',
'You can see the configuration in JSON format below.<br/>',
'<span class=\'literal-code iframe-code-block\' style="width:95%">',
JSON.stringify(this.config.data, null, 2).replace(/\n/g, "<br/>").replace(/ /g, "&nbsp;"),
'</span>'
]).Render(); ]).Render();
} }

View file

@ -3,6 +3,7 @@ import {VerticalCombine} from "../Base/VerticalCombine";
import {VariableUiElement} from "../Base/VariableUIElement"; import {VariableUiElement} from "../Base/VariableUIElement";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import { import {
CustomLayoutFromJSON,
LayerConfigJson, LayerConfigJson,
LayoutConfigJson, LayoutConfigJson,
TagRenderingConfigJson TagRenderingConfigJson
@ -12,9 +13,14 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import {OsmConnection, UserDetails} from "../../Logic/Osm/OsmConnection"; import {OsmConnection, UserDetails} from "../../Logic/Osm/OsmConnection";
import {Button} from "../Base/Button"; import {Button} from "../Base/Button";
import {FixedUiElement} from "../Base/FixedUiElement"; import {FixedUiElement} from "../Base/FixedUiElement";
import {TextField} from "../Input/TextField"; import {TextField, ValidatedTextField} from "../Input/TextField";
import {Tag} from "../../Logic/TagsFilter";
import {DropDown} from "../Input/DropDown";
import {TagRendering} from "../../Customizations/TagRendering";
TagRendering.injectFunction();
function TagsToString(tags: string | string [] | { k: string, v: string }[]) { function TagsToString(tags: string | string [] | { k: string, v: string }[]) {
if (tags === undefined) { if (tags === undefined) {
return undefined; return undefined;
@ -34,30 +40,31 @@ function TagsToString(tags: string | string [] | { k: string, v: string }[]) {
return newTags.join(","); return newTags.join(",");
} }
// Defined below, as it needs some context/closure
let createFieldUI: (label: string, key: string, root: any, options?: { deflt?: string }) => UIElement; let createFieldUI: (label: string, key: string, root: any, options: { deflt?: string, type?: string, description: string, emptyAllowed?: boolean }) => UIElement;
class MappingGenerator extends UIElement { class MappingGenerator extends UIElement {
private elements: UIElement[]; private elements: UIElement[];
constructor(fullConfig: UIEventSource<LayoutConfigJson>, constructor(tagRendering: TagRenderingConfigJson,
layerConfig: LayerConfigJson,
tagRendering: TagRenderingConfigJson,
mapping: { if: string | string[] | { k: string, v: string }[] }) { mapping: { if: string | string[] | { k: string, v: string }[] }) {
super(undefined); super(undefined);
this.CreateElements(fullConfig, layerConfig, tagRendering, mapping) this.CreateElements(tagRendering, mapping)
} }
private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson, private CreateElements(tagRendering: TagRenderingConfigJson,
tagRendering: TagRenderingConfigJson,
mapping) { mapping) {
{ {
const self = this; const self = this;
this.elements = [ this.elements = [
createFieldUI("If these tags apply", "if", mapping), new FixedUiElement("A mapping shows a specific piece of text if a specific tag is present. If no mapping is known and no key matches (and the question is defined), then the mappings show up as radio buttons to answer the question and to update OSM"),
createFieldUI("Then: show this text", "then", mapping), createFieldUI("If these tags apply", "if", mapping, {
type: "tags",
description: "The tags that have to be present. Use <span class='literal-code'>key=</span> to indicate an implicit assumption. 'key=' can be used to indicate: 'if this key is missing'"
}),
createFieldUI("Then: show this text", "then", mapping, {description: "The text that is shown"}),
new Button("Remove this mapping", () => { new Button("Remove this mapping", () => {
for (let i = 0; i < tagRendering.mappings.length; i++) { for (let i = 0; i < tagRendering.mappings.length; i++) {
if (tagRendering.mappings[i] === mapping) { if (tagRendering.mappings[i] === mapping) {
@ -89,39 +96,75 @@ class TagRenderingGenerator
constructor(fullConfig: UIEventSource<LayoutConfigJson>, constructor(fullConfig: UIEventSource<LayoutConfigJson>,
layerConfig: LayerConfigJson, layerConfig: LayerConfigJson,
tagRendering: TagRenderingConfigJson, tagRendering: TagRenderingConfigJson,
isTitle: boolean = false) { options: { header: string, description: string, removable: boolean, hideQuestion: boolean }) {
super(undefined); super(undefined);
this.CreateElements(fullConfig, layerConfig, tagRendering, isTitle) this.CreateElements(fullConfig, layerConfig, tagRendering, options)
} }
private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson, tagRendering: TagRenderingConfigJson, isTitle: boolean) { private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson, tagRendering: TagRenderingConfigJson,
options: { header: string, description: string, removable: boolean, hideQuestion: boolean }) {
const self = this; const self = this;
this.elements = [ this.elements = [
new FixedUiElement(isTitle ? "<h3>Popup title</h3>" : "<h3>TagRendering/TagQuestion</h3>"), new FixedUiElement(`<h3>${options.header}</h3>`),
createFieldUI("Key", "key", tagRendering), new FixedUiElement(options.description),
createFieldUI("Rendering", "render", tagRendering), createFieldUI("Key", "key", tagRendering, {
createFieldUI("Type", "type", tagRendering), deflt: "name",
createFieldUI("Question", "question", tagRendering), description: "Optional. If the object contains a tag with the specified key, the rendering below will be shown. Use '*' if you always want to show the rendering."
createFieldUI("Extra tags", "addExtraTags", tagRendering), }),
createFieldUI("Rendering", "render", tagRendering, {
deflt: "The name of this object is {name}",
description: "Optional. If the above key is present, this rendering will be used. Note that <span class='literal-code'>{key}</span> will be replaced by the value - if that key is present. This is _not_ limited to the given key above, it is allowed to use multiple subsitutions." +
"If the above key is _not_ present, the question will be shown and the rendering will be used as answer with {key} as textfield"
}),
options.hideQuestion ? new FixedUiElement("") : createFieldUI("Type", "type", tagRendering, {
deflt: "string",
description: "Input validation of this type",
type: "typeSelector",
}),
options.hideQuestion ? new FixedUiElement("") :
createFieldUI("Question", "question", tagRendering, {
deflt: "",
description: "Optional. If 'key' is not present (or not given) and none of the mappings below match, then this will be shown as question. Users are then able to answer this question and save the data to OSM. If no question is given, values can still be shown but not answered",
type: "string"
}),
options.hideQuestion ? new FixedUiElement("") :
createFieldUI("Extra tags", "addExtraTags", tagRendering,
{
deflt: "",
type: "tags",
emptyAllowed: true,
description: "Optional. If the freeform text field is used to fill out the tag, these tags are applied as well. The main use case is to flag the object for review. (A prime example is access. A few predefined values are given and the option to fill out something. Here, one can add e.g. <span class='literal-code'>fixme=access was filled out by user, value might not be correct</span>"
}),
createFieldUI(
"Only show if", "condition", tagRendering,
{
deflt: "",
type: "tags",
emptyAllowed: true,
description: "Only show this question/rendering if the object also has the specified tag. This can be useful to ask a follow up question only if the prerequisite is met"
}
),
...(tagRendering.mappings ?? []).map((mapping) => { ...(tagRendering.mappings ?? []).map((mapping) => {
return new MappingGenerator(fullConfig, layerConfig, tagRendering, mapping) return new MappingGenerator(tagRendering, mapping)
}), }),
new Button("Add mapping", () => { new Button("Add mapping", () => {
if (tagRendering.mappings === undefined) { if (tagRendering.mappings === undefined) {
tagRendering.mappings = [] tagRendering.mappings = []
} }
tagRendering.mappings.push({if: "", then: ""}); tagRendering.mappings.push({if: "", then: ""});
self.CreateElements(fullConfig, layerConfig, tagRendering, isTitle); self.CreateElements(fullConfig, layerConfig, tagRendering, options);
self.Update(); self.Update();
}) })
] ]
if (!isTitle) { if (!!options.removable) {
const b = new Button("Remove this preset", () => { const b = new Button("Remove this tag rendering", () => {
for (let i = 0; i < layerConfig.tagRenderings.length; i++) { for (let i = 0; i < layerConfig.tagRenderings.length; i++) {
if (layerConfig.tagRenderings[i] === tagRendering) { if (layerConfig.tagRenderings[i] === tagRendering) {
layerConfig.tagRenderings.splice(i, 1); layerConfig.tagRenderings.splice(i, 1);
@ -155,10 +198,21 @@ class PresetGenerator extends UIElement {
const self = this; const self = this;
this.elements = [ this.elements = [
new FixedUiElement("<h3>Preset</h3>"), new FixedUiElement("<h3>Preset</h3>"),
createFieldUI("Title", "title", preset0), new FixedUiElement("A preset allows the user to add a new point at a location that was clicked. Note that one layer can have zero, one or multiple presets"),
createFieldUI("Description", "description", preset0, {deflt: layerConfig.description}), createFieldUI("Title", "title", preset0, {
createFieldUI("icon", "icon", preset0, {deflt: layerConfig.icon}), description: "The title of this preset, shown in the 'add new {Title} here'-dialog"
createFieldUI("tags", "tags", preset0, {deflt: TagsToString(layerConfig.overpassTags)}), }),
createFieldUI("Description", "description", preset0,
{
deflt: layerConfig.description,
type: "string",
description: "A description shown alongside the 'add new'-button"
}),
createFieldUI("tags", "tags", preset0,
{
deflt: TagsToString(layerConfig.overpassTags), type: "tags",
description: "The tags that are added to the newly created point"
}),
new Button("Remove this preset", () => { new Button("Remove this preset", () => {
for (let i = 0; i < layerConfig.presets.length; i++) { for (let i = 0; i < layerConfig.presets.length; i++) {
if (layerConfig.presets[i] === preset0) { if (layerConfig.presets[i] === preset0) {
@ -201,12 +255,86 @@ class LayerGenerator extends UIElement {
private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson) { private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson) {
const self = this; const self = this;
this.uielements = [ this.uielements = [
createFieldUI("The name of this layer", "id", layerConfig),
createFieldUI("A description of objects for this layer", "description", layerConfig), new FixedUiElement("<p>A layer is a collection of related objects which have the same or very similar tags renderings. In general, all objects of one layer have the same icon (or at least very similar icons)</p>"),
createFieldUI("The icon of this layer, either a URL or a base64-encoded svg", "icon", layerConfig),
createFieldUI("The default stroke color", "color", layerConfig), createFieldUI("Name", "id", layerConfig, {description: "The name of this layer"}),
createFieldUI("The minimal needed zoom to start loading", "minzoom", layerConfig), createFieldUI("A description of objects for this layer", "description", layerConfig, {description: "The description of this layer"}),
createFieldUI("The tags to load from overpass", "overpassTags", layerConfig), createFieldUI("Minimum zoom level", "minzoom", layerConfig, {
type: "nat",
deflt: "12",
description: "The minimum zoom level to start loading data. This is mainly limited by the expected number of objects: if there are a lot of objects, then pick something higher. A generous bounding box is put around the map, so some scrolling should be possible"
}),
createFieldUI("The tags to load from overpass", "overpassTags", layerConfig, {
type: "tags",
description: "Tags to load from overpass. The format is <span class='literal-code'>key=value&key0=value0&key1=value1</span>, e.g. <span class='literal-code'>amenity=public_bookcase</span> or <span class='literal-code'>amenity=compressed_air&bicycle=yes</span>. Note that a wildcard is supported, e.g. <span class='literal-code'>key=*</span>"
}),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.title ?? {
key: "",
addExtraTags: "",
mappings: [],
question: "",
render: "Title",
type: "string"
}, {
header: "Title element",
description: "This element is shown in the title of the popup in a header-tag",
removable: false,
hideQuestion: true
}),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.icon ?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "Title",
type: "text"
}, {
header: "Icon",
description: "This decides which icon is used to represent an element on the map. Leave blank if you don't want icons to pop up",
removable: false,
hideQuestion: true
}),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.color ?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "Title",
type: "text"
}, {
header: "Colour",
description: "This decides which color is used to represent a way on the map. Note that if an icon is defined as well, the icon will be showed too",
removable: false,
hideQuestion: true
}),
...layerConfig.tagRenderings.map(tr => new TagRenderingGenerator(fullConfig, layerConfig, tr, {
header: "Tag rendering",
description: "A single tag rendering",
removable: true,
hideQuestion: false
})),
new Button("Add a tag rendering", () => {
layerConfig.tagRenderings.push({
key: undefined,
addExtraTags: undefined,
mappings: [],
question: undefined,
render: undefined,
type: "text"
});
self.CreateElements(fullConfig, layerConfig);
self.Update();
}),
...layerConfig.presets.map(preset => new PresetGenerator(fullConfig, layerConfig, preset)), ...layerConfig.presets.map(preset => new PresetGenerator(fullConfig, layerConfig, preset)),
new Button("Add a preset", () => { new Button("Add a preset", () => {
layerConfig.presets.push({ layerConfig.presets.push({
@ -217,28 +345,7 @@ class LayerGenerator extends UIElement {
}); });
self.CreateElements(fullConfig, layerConfig); self.CreateElements(fullConfig, layerConfig);
self.Update(); self.Update();
}), })
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.title ?? {
key: "",
addExtraTags: "",
mappings: [],
question: "",
render: "Title",
type: "text"
}, true),
...layerConfig.tagRenderings.map(tr => new TagRenderingGenerator(fullConfig, layerConfig, tr)),
new Button("Add a tag rendering", () => {
layerConfig.tagRenderings.push({
key: "",
addExtraTags: "",
mappings: [],
question: "",
render: "",
type: "text"
});
self.CreateElements(fullConfig, layerConfig);
self.Update();
}),
] ]
} }
@ -274,8 +381,12 @@ class AllLayerComponent extends UIElement {
const layerPanes: { header: UIElement | string, content: UIElement | string }[] = []; const layerPanes: { header: UIElement | string, content: UIElement | string }[] = [];
const config = this.config; const config = this.config;
for (const layer of this.config.data.layers) { for (const layer of this.config.data.layers) {
const iconUrl = CustomLayoutFromJSON.TagRenderingFromJson(layer?.icon)
.GetContent({id: "node/-1"});
const header = this.config.map(() => { const header = this.config.map(() => {
return `<img src="${layer?.icon ?? "./assets/help.svg"}">`
return `<img src="${iconUrl ?? "./assets/help.svg"}">`
}); });
layerPanes.push({ layerPanes.push({
header: new VariableUiElement(header), header: new VariableUiElement(header),
@ -290,10 +401,17 @@ class AllLayerComponent extends UIElement {
config.data.layers.push({ config.data.layers.push({
id: "", id: "",
title: { title: {
render: "Title" key: "",
render: "title"
},
icon: {
key: "",
render: "./assets/bug.svg"
},
color: {
key: "",
render: "#0000ff"
}, },
icon: "./assets/bug.svg",
color: "",
description: "", description: "",
minzoom: 12, minzoom: 12,
overpassTags: "", overpassTags: "",
@ -333,38 +451,118 @@ export class ThemeGenerator extends UIElement {
if (windowHash !== undefined && windowHash.length > 4) { if (windowHash !== undefined && windowHash.length > 4) {
loadedTheme = JSON.parse(atob(windowHash)); loadedTheme = JSON.parse(atob(windowHash));
} }
this.themeObject = new UIEventSource<LayoutConfigJson>(loadedTheme ?? defaultTheme); this.themeObject = new UIEventSource<LayoutConfigJson>(loadedTheme ?? defaultTheme);
const jsonObjectRoot = this.themeObject.data; const jsonObjectRoot = this.themeObject.data;
connection.userDetails.addCallback((userDetails) => {
jsonObjectRoot.maintainer = userDetails.name;
});
jsonObjectRoot.maintainer = connection.userDetails.data.name;
const base64 = this.themeObject.map(JSON.stringify).map(btoa); const base64 = this.themeObject.map(JSON.stringify).map(btoa);
this.url = base64.map((data) => `https://pietervdvn.github.io/MapComplete/index.html?test=true&userlayout=true#` + data); let baseUrl = "https://pietervdvn.github.io/MapComplete";
if (window.location.hostname === "127.0.0.1") {
baseUrl = "http://127.0.0.1:1234";
}
this.url = base64.map((data) => baseUrl + `/index.html?test=true&userlayout=true#` + data);
const self = this; const self = this;
createFieldUI = (label, key, root, options) => { createFieldUI = (label, key, root, options) => {
options = options ?? {description: "?"};
options.type = options.type ?? "string";
const value = new UIEventSource<string>(TagsToString(root[key]) ?? options?.deflt); const value = new UIEventSource<string>(TagsToString(root[key]) ?? options?.deflt);
value.addCallback((v) => { let textField: UIElement;
root[key] = v; if (options.type === "typeSelector") {
self.themeObject.ping(); // We assume the root is a part of the themeObject const options: { value: string, shown: string | UIElement }[] = [];
}) for (const possibleType in ValidatedTextField.inputValidation) {
return new Combine([ if (possibleType !== "$") {
label, options.push({value: possibleType, shown: possibleType});
new TextField<string>({ }
}
textField = new DropDown<string>("",
options,
value)
} else if (options.type === "tags") {
textField = ValidatedTextField.TagTextField(value.map(CustomLayoutFromJSON.TagsFromJson, [], tags => {
if (tags === undefined) {
return undefined;
}
return tags.map((tag: Tag) => tag.key + "=" + tag.value).join("&");
}), options?.emptyAllowed ?? false);
} else if (options.type === "img" || options.type === "colour") {
textField = new TextField<string>({
placeholder: options.type,
fromString: (str) => str, fromString: (str) => str,
toString: (str) => str, toString: (str) => str,
value: value value: value,
})]); startValidated: true
});
} else if (options.type) {
textField = ValidatedTextField.ValidatedTextField(options.type, {value: value});
} else {
textField = new TextField<string>({
placeholder: options.type,
fromString: (str) => str,
toString: (str) => str,
value: value,
startValidated: true
});
}
value.addCallback((v) => {
if (v === undefined || v === "") {
delete root[key];
} else {
root[key] = v;
}
self.themeObject.ping(); // We assume the root is a part of the themeObject
});
return new Combine([
label,
textField,
"<br>",
"<span class='subtle'>" + options.description + "</span>"
]);
} }
this.allQuestionFields = [ this.allQuestionFields = [
createFieldUI("Name of this theme", "name", jsonObjectRoot), createFieldUI("Name of this theme", "name", jsonObjectRoot, {description: "An identifier for this theme"}),
createFieldUI("Title (shown in the window and in the welcome message)", "title", jsonObjectRoot), createFieldUI("Title", "title", jsonObjectRoot, {
createFieldUI("Description (shown in the welcome message and various other places)", "description", jsonObjectRoot), deflt: "Title",
createFieldUI("The supported language", "language", jsonObjectRoot), description: "The title of this theme, as shown in the welcome message and in the title bar of the browser"
createFieldUI("startLat", "startLat", jsonObjectRoot), }),
createFieldUI("startLon", "startLon", jsonObjectRoot), createFieldUI("Description", "description", jsonObjectRoot, {
createFieldUI("startzoom", "startZoom", jsonObjectRoot), description: "Shown in the welcome message",
createFieldUI("icon: either a URL to an image file, a relative url to a MapComplete asset ('./asset/help.svg') or a base64-encoded value (including 'data:image/svg+xml;base64,'", "icon", jsonObjectRoot, {deflt: "./assets/bug.svg"}), deflt: "Description"
}),
createFieldUI("The supported language", "language", jsonObjectRoot, {
description: "The language of this mapcomplete instance. MapComplete can be translated, see <a href='https://github.com/pietervdvn/MapComplete#translating-mapcomplete' target='_blank'> here for more information</a>",
deflt: "en"
}),
createFieldUI("startLat", "startLat", jsonObjectRoot, {
type: "float",
deflt: "0",
description: "The latitude where this theme should start. Note that this is only for completely fresh users, as the last location is saved"
}),
createFieldUI("startLon", "startLon", jsonObjectRoot, {
type: "float",
deflt: "0",
description: "The longitude where this theme should start. Note that this is only for completely fresh users, as the last location is saved"
}),
createFieldUI("startzoom", "startZoom", jsonObjectRoot, {
type: "nat",
deflt: "12",
description: "The initial zoom level where the map is located"
}),
createFieldUI("icon", "icon", jsonObjectRoot, {
deflt: "./assets/bug.svg",
type: "img",
description: "The icon representing this MapComplete instance. It is shown in the welcome message and -if adopted as official theme- used as favicon and to browse themes"
}),
new AllLayerComponent(this.themeObject) new AllLayerComponent(this.themeObject)
] ]
@ -383,8 +581,6 @@ export class ThemeGenerator extends UIElement {
return new VerticalCombine([ return new VerticalCombine([
// new VariableUiElement(this.themeObject.map(JSON.stringify)),
// new VariableUiElement(this.url.map((url) => `Current URL: <a href="${url}" target="_blank">Click here to open</a>`)),
...this.allQuestionFields, ...this.allQuestionFields,
]).Render(); ]).Render();
} }

View file

@ -11,6 +11,8 @@ import {UserDetails} from "../Logic/Osm/OsmConnection";
import {FixedUiElement} from "./Base/FixedUiElement"; import {FixedUiElement} from "./Base/FixedUiElement";
import {State} from "../State"; import {State} from "../State";
import {TagRenderingOptions} from "../Customizations/TagRenderingOptions"; import {TagRenderingOptions} from "../Customizations/TagRenderingOptions";
import {UIEventSource} from "../Logic/UIEventSource";
import Combine from "./Base/Combine";
export class FeatureInfoBox extends UIElement { export class FeatureInfoBox extends UIElement {
@ -36,7 +38,7 @@ export class FeatureInfoBox extends UIElement {
constructor( constructor(
feature: any, feature: any,
tagsES: UIEventSource<any>, tagsES: UIEventSource<any>,
title: TagRenderingOptions | UIElement | string, title: TagDependantUIElementConstructor | UIElement | string,
elementsToShow: TagDependantUIElementConstructor[], elementsToShow: TagDependantUIElementConstructor[],
) { ) {
super(tagsES); super(tagsES);
@ -77,7 +79,7 @@ export class FeatureInfoBox extends UIElement {
} else if (title instanceof UIElement) { } else if (title instanceof UIElement) {
this._title = title; this._title = title;
} else { } else {
this._title = new TagRenderingOptions(title.options).construct(deps); this._title = title.construct(deps);
} }
this._osmLink = new OsmLink().construct(deps); this._osmLink = new OsmLink().construct(deps);
this._wikipedialink = new WikipediaLink().construct(deps); this._wikipedialink = new WikipediaLink().construct(deps);
@ -124,24 +126,18 @@ export class FeatureInfoBox extends UIElement {
questionsHtml = this._someSkipped.Render(); questionsHtml = this._someSkipped.Render();
} }
const title = new Combine([
this._title,
this._wikipedialink,
this._osmLink]);
const infoboxcontents = new Combine(
[ new VerticalCombine(info, "infobox-information "), questionsHtml]);
return "<div class='featureinfobox'>" + return "<div class='featureinfobox'>" +
"<div class='featureinfoboxtitle'>" + new Combine([
"<span>" + "<div class='featureinfoboxtitle'>" + title.Render() + "</div>",
this._title.Render() + "<div class='infoboxcontents'>" + infoboxcontents.Render() + "</div>"]).Render() + "</div>";
"</span>" +
this._wikipedialink.Render() +
this._osmLink.Render() +
"</div>" +
"<div class='infoboxcontents'>" +
new VerticalCombine(info, "infobox-information ").Render() +
questionsHtml +
"</div>" +
"" +
"</div>";
} }

View file

@ -24,6 +24,10 @@ export class ImageCarouselWithUploadConstructor implements TagDependantUIElement
construct(dependencies): TagDependantUIElement { construct(dependencies): TagDependantUIElement {
return new ImageCarouselWithUpload(dependencies); return new ImageCarouselWithUpload(dependencies);
} }
GetContent(tags: any): string {
return undefined;
}
} }
class ImageCarouselWithUpload extends TagDependantUIElement { class ImageCarouselWithUpload extends TagDependantUIElement {

View file

@ -2,9 +2,97 @@ import {UIElement} from "../UIElement";
import {InputElement} from "./InputElement"; import {InputElement} from "./InputElement";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import * as EmailValidator from "email-validator";
import {parsePhoneNumberFromString} from "libphonenumber-js";
import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions";
import {CustomLayoutFromJSON} from "../../Customizations/JSON/CustomLayoutFromJSON";
import {Tag} from "../../Logic/TagsFilter";
export class ValidatedTextField {
public static inputValidation = {
"$": (str) => true,
"string": (str) => true,
"date": (str) => true, // TODO validate and add a date picker
"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),
"url": (str) => 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()
}
}
public static TagTextField(value: UIEventSource<Tag[]> = undefined, allowEmpty: boolean) {
allowEmpty = allowEmpty ?? false;
return new TextField<Tag[]>({
placeholder: "Tags",
fromString: str => {
const tags = CustomLayoutFromJSON.TagsFromJson(str);
if (tags === []) {
if (allowEmpty) {
return []
} else {
return undefined;
}
}
return tags;
}
,
toString: (tags: Tag[]) => {
if (tags === undefined) {
return undefined;
}
if (tags === []) {
if (allowEmpty) {
return "";
} else {
return undefined;
}
}
return tags.map(tag =>
tag.invertValue ? tag.key + "!=" + tag.value :
tag.key + "=" + tag.value).join("&")
},
value: value,
startValidated: true
}
)
}
public static
ValidatedTextField(type: string, options: { value?: UIEventSource<string>, country?: string })
: TextField<string> {
let isValid = ValidatedTextField.inputValidation[type];
if (isValid === undefined
) {
throw "Invalid type for textfield: " + type
}
let formatter = ValidatedTextField.formatting[type] ?? ((str) => str);
return new TextField<string>({
placeholder: type,
toString: str => str,
fromString: str => isValid(str, options?.country) ? formatter(str, options.country) : undefined,
value: options.value,
startValidated: true
})
}
}
export class TextField<T> extends InputElement<T> { export class TextField<T> extends InputElement<T> {
private value: UIEventSource<string>; private value: UIEventSource<string>;
private mappedValue: UIEventSource<T>; private mappedValue: UIEventSource<T>;
/** /**
@ -14,6 +102,7 @@ export class TextField<T> extends InputElement<T> {
private _placeholder: UIElement; private _placeholder: UIElement;
private _fromString?: (string: string) => T; private _fromString?: (string: string) => T;
private _toString: (t: T) => string; private _toString: (t: T) => string;
private startValidated: boolean;
constructor(options: { constructor(options: {
@ -33,7 +122,8 @@ export class TextField<T> extends InputElement<T> {
* @param string * @param string
*/ */
fromString: (string: string) => T, fromString: (string: string) => T,
value?: UIEventSource<T> value?: UIEventSource<T>,
startValidated?: boolean,
}) { }) {
super(undefined); super(undefined);
const self = this; const self = this;
@ -63,7 +153,8 @@ export class TextField<T> extends InputElement<T> {
} }
// @ts-ignore // @ts-ignore
field.value = options.toString(t); field.value = options.toString(t);
}) });
this.startValidated = options.startValidated ?? false;
} }
GetValue(): UIEventSource<T> { GetValue(): UIEventSource<T> {
@ -92,6 +183,8 @@ export class TextField<T> extends InputElement<T> {
this.mappedValue.addCallback((data) => { this.mappedValue.addCallback((data) => {
field.className = this.mappedValue.data !== undefined ? "valid" : "invalid"; field.className = this.mappedValue.data !== undefined ? "valid" : "invalid";
}); });
field.className = this.mappedValue.data !== undefined ? "valid" : "invalid";
const self = this; const self = this;
field.oninput = () => { field.oninput = () => {

View file

@ -18,7 +18,8 @@ export class SearchAndGo extends UIElement {
this._placeholder.map(uiElement => uiElement.InnerRender(), [Locale.language]) this._placeholder.map(uiElement => uiElement.InnerRender(), [Locale.language])
), ),
fromString: str => str, fromString: str => str,
toString: str => str toString: str => str,
value: new UIEventSource<string>("")
} }
); );

View file

@ -1,5 +1,5 @@
import {UIElement} from "./UIElement"; import {UIElement} from "./UIElement";
import {Tag} from "../Logic/TagsFilter"; import {Tag, TagUtils} from "../Logic/TagsFilter";
import {FilteredLayer} from "../Logic/FilteredLayer"; import {FilteredLayer} from "../Logic/FilteredLayer";
import Translations from "./i18n/Translations"; import Translations from "./i18n/Translations";
import Combine from "./Base/Combine"; import Combine from "./Base/Combine";
@ -48,12 +48,18 @@ export class SimpleAddUI extends UIElement {
for (const layer of State.state.filteredLayers.data) { for (const layer of State.state.filteredLayers.data) {
for (const preset of layer.layerDef.presets) { for (const preset of layer.layerDef.presets) {
let icon: string = "./assets/bug.svg";
if (typeof (preset.icon) !== "string") {
console.log("Preset icon is:", preset.icon);
icon = preset.icon.GetContent(TagUtils.KVtoProperties(preset.tags));
} else {
icon = preset.icon;
}
console.log("Preset icon:", icon)
// <button type='button'> looks SO retarded
// the default type of button is 'submit', which performs a POST and page reload
const button = const button =
new SubtleButton( new SubtleButton(
preset.icon, icon,
new Combine([ new Combine([
"<b>", "<b>",
preset.title, preset.title,
@ -61,7 +67,7 @@ export class SimpleAddUI extends UIElement {
preset.description !== undefined ? preset.description : ""]) preset.description !== undefined ? preset.description : ""])
).onClick( ).onClick(
() => { () => {
self.confirmButton = new SubtleButton(preset.icon, self.confirmButton = new SubtleButton(icon,
new Combine([ new Combine([
"<b>", "<b>",
Translations.t.general.add.confirmButton.Subs({category: preset.title}), Translations.t.general.add.confirmButton.Subs({category: preset.title}),
@ -73,7 +79,7 @@ export class SimpleAddUI extends UIElement {
layerToAddTo: layer, layerToAddTo: layer,
name: preset.title, name: preset.title,
description: preset.description, description: preset.description,
icon: preset.icon icon: icon
}); });
} }
) )

View file

@ -26,9 +26,7 @@ export class WelcomeMessage extends UIElement {
this.ListenTo(Locale.language); this.ListenTo(Locale.language);
function fromLayout(f: (layout: Layout) => (string | UIElement)): UIElement { function fromLayout(f: (layout: Layout) => (string | UIElement)): UIElement {
return new VariableUiElement( return Translations.W(f(State.state.layoutToUse.data))
State.state.layoutToUse.map((layout) => Translations.W(f(layout)).Render())
)
} }
this.description = fromLayout((layout) => layout.welcomeMessage); this.description = fromLayout((layout) => layout.welcomeMessage);

View file

@ -23,7 +23,7 @@ export default class Translation extends UIElement {
} else { } else {
Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day
rtext = el.InnerRender(); rtext = el.InnerRender();
} }
for (let i = 0; i < parts.length - 1; i++) { for (let i = 0; i < parts.length - 1; i++) {
combined.push(parts[i]); combined.push(parts[i]);
combined.push(rtext) combined.push(rtext)
@ -65,6 +65,9 @@ export default class Translation extends UIElement {
this.translations = translations this.translations = translations
} }
public replace(a, b) {
return this.Subs({a: b});
}
public R(): string { public R(): string {
return new Translation(this.translations).Render(); return new Translation(this.translations).Render();
@ -74,5 +77,5 @@ export default class Translation extends UIElement {
return new Translation(this.translations) return new Translation(this.translations)
} }
} }

View file

@ -643,7 +643,7 @@ export default class Translations {
}), }),
bookkinds: new T({ bookkinds: new T({
nl: "Wat voor soort boeken heeft dit boekenruilkastje?", nl: "Wat voor soort boeken heeft dit boekenruilkastje?",
en: "What kind of books can be found in this public bookcase" en: "What kind of books can be found in this public bookcase?"
}) })
} }
@ -974,10 +974,17 @@ export default class Translations {
} }
public static W(s: string | UIElement): UIElement { public static W(s: string | UIElement): UIElement {
if (s instanceof UIElement) { if (typeof (s) === "string") {
return s; return new FixedUiElement(s);
} }
return new FixedUiElement(s); return s;
}
public static WT(s: string | Translation): Translation {
if (typeof (s) === "string") {
return new Translation({en: s});
}
return s;
} }
public static CountTranslations() { public static CountTranslations() {

View file

@ -0,0 +1,41 @@
{
"language": [
"en"
],
"startLat": "0",
"startLon": "0",
"startZoom": "12",
"maintainer": "Not logged in",
"layers": [
{
"id": "Defibrillator",
"title": {
"key": "*",
"render": "Defibrillator"
},
"icon": {
"key": "*",
"render": "./assets/themes/aed/aed.svg"
},
"color": {
"render": "#0000ff",
"key": "*"
},
"description": "A defibrillator",
"minzoom": "12",
"presets": [
{
"title": "Defibrillator",
"tags": "emergency=defibrillator",
"description": "A defibrillator"
}
],
"tagRenderings": [],
"overpassTags": "emergency=defibrillator"
}
],
"title": "Open AED Map",
"icon": "./assets/themes/aed/aed.svg",
"name": "aed",
"description": "On this map, one can find and mark nearby defibrillators"
}

View file

@ -1,67 +1,266 @@
{ {
"name": "bookcases",
"title": {
"en": "Open Bookcase Map",
"nl": "Open Boekenruilkastenkaart"
},
"maintainer": "Pieter Vander Vennet",
"icon": "./assets/themes/bookcases/bookcase.svg",
"description": {
"en": "A public bookcase is a small streetside cabinet, box, old phone boot or some other objects where books are stored. Everyone can place or take a book. This map aims to collect all these bookcases. You can discover new bookcases nearby and, with a free OpenStreetMap account, quickly add your favourite bookcases.",
"nl": "Een boekenruilkast is een kastje waar iedereen een boek kan nemen of achterlaten. Op deze kaart kan je deze boekenruilkasten terugvinden en met een gratis OpenStreetMap-account, ook boekenruilkasten toevoegen of informatie verbeteren"
},
"language": [
"en",
"nl"
],
"layers": [ "layers": [
{ {
"id": "bookcases", "id": "Bookcases",
"title": { "title": {
"render": "Bookcase" "key": "*",
"render": {
"en": "Bookcase",
"nl": "Boekenruilkast"
},
"mappings": [
{
"if": "name=*",
"then": "{name}"
}
]
},
"icon": {
"key": "*",
"render": "./assets/themes/bookcases/bookcase.svg",
"mappings": []
},
"color": {
"key": "*",
"render": "#0000ff"
},
"description": {
"en": "A streetside cabinet with books, accessible to anyone",
"nl": "Een straatkastje met boeken voor iedereen"
}, },
"icon": "./assets/themes/bookcases/bookcase.svg",
"color": "#000000",
"description": "A public bookcase",
"minzoom": "12", "minzoom": "12",
"overpassTags": "amenity=public_bookcase", "overpassTags": "amenity=public_bookcase",
"presets": [ "presets": [
{ {
"title": "Bookcase", "title": {
"description": "A bookcase is a small cabinet where everyone can take or leave a book", "en": "Bookcase",
"icon": "./assets/bookcase.svg", "nl": "Boekenruilkast"
"tags": "amenity=public_bookcase" }
} }
], ],
"tagRenderings": [ "tagRenderings": [
{ {
"key": "brand", "key": "name",
"addExtraTags": "",
"mappings": [ "mappings": [
{ {
"if": "brand=Little Free Library", "then": {
"then": "Part of the network Little Free Library" "en": "This bookcase doesn't have a name",
"nl": "Dit boekenruilkastje heeft geen naam"
},
"if": "noname=yes&name="
} }
], ],
"question": "Is this bookcase part of a network?", "question": {
"render": "Part of {brand}", "en": "What is the name of this public bookcase?",
"nl": "Wat is de naam van dit boekenuilkastje?"
},
"render": {
"en": "The name of this bookcase is {name}",
"nl": "De naam van dit boekenruilkastje is {name}"
},
"type": "text" "type": "text"
}, },
{ {
"key": "", "key": "capacity",
"addExtraTags": "", "mappings": [],
"question": {
"en": "How many books fit into this public bookcase?",
"nl": "Hoeveel boeken passen er in dit boekenruilkastje?"
},
"render": {
"en": "{capacity} books fit in this bookcase",
"nl": "Er passen {capacity} boeken"
},
"type": "nat"
},
{
"mappings": [ "mappings": [
{ {
"if": "books=children", "if": "books=children",
"then": "Mainly books for kids" "then": {
"en": "Mostly children books",
"nl": "Voornamelijk kinderboeken"
}
}, },
{ {
"if": "books=adult", "if": "books=adults",
"then": "Mainly books for adults" "then": {
"en": "Mostly books for adults",
"nl": "Voornamelijk boeken voor volwassenen"
}
}, },
{ {
"if": "books=adult;children", "if": "books=children;adults",
"then": "Books for both adults and children" "then": {
"en": "Both books for kids and adults",
"nl": "Boeken voor zowel kinderen als volwassenen"
}
} }
], ],
"question": "Which books can be found here?", "question": {
"render": "", "en": "What kind of books can be found in this public bookcase?",
"nl": "Voor welke doelgroep zijn de meeste boeken in dit boekenruilkastje?"
},
"type": "text" "type": "text"
},
{
"addExtraTags": "",
"mappings": [
{
"then": {
"en": "This bookcase is located indoors",
"nl": "Dit boekenruilkastje staat binnen"
},
"if": "indoor=yes"
},
{
"then": {
"en": "This bookcase is located outdoors",
"nl": "Dit boekenruilkastje staat buiten"
},
"if": "indoor=no"
},
{
"then": {
"en": "This bookcase is located outdoors",
"nl": "Dit boekenruilkastje staat buiten"
},
"if": "indoor="
}
],
"question": {
"en": "Is this bookcase located outdoors?",
"nl": "Staat dit boekenruilkastje binnen of buiten?"
},
"type": "text"
},
{
"mappings": [
{
"then": {
"en": "Publicly accessible",
"nl": "Publiek toegankelijk"
},
"if": "access=yes"
},
{
"then": {
"en": "Only accessible to customers",
"nl": "Enkel toegankelijk voor klanten"
},
"if": "access=customers"
}
],
"question": {
"en": "Is this public bookcase freely accessible?",
"nl": "Is dit boekenruilkastje publiek toegankelijk?"
},
"type": "text",
"condition": "indoor=yes"
},
{
"key": "operator",
"mappings": [],
"question": {
"en": "Who maintains this public bookcase?",
"nl": "Wie is verantwoordelijk voor dit boekenruilkastje?"
},
"type": "text",
"render": {
"en": "Operated by {operator}",
"nl": "Onderhouden door {operator}"
}
},
{
"key": "brand",
"mappings": [
{
"then": {
"en": "Part of the network 'Little Free Library'",
"nl": "Deel van het netwerk 'Little Free Library'"
},
"if": "brand=Little Free Library"
},
{
"then": {
"en": "This public bookcase is not part of a bigger network",
"nl": "Dit boekenruilkastje maakt geen deel uit van een netwerk"
},
"if": "nobrand=yes&brand="
}
],
"question": {
"en": "Is this public bookcase part of a bigger network?",
"nl": "Is dit boekenruilkastje deel van een netwerk?"
},
"render": {
"en": "This public bookcase is part of {brand}",
"nl": "Dit boekenruilkastje is deel van het netwerk {brand}"
},
"type": "text",
"condition": "ref="
},
{
"key": "ref",
"mappings": [
{
"then": {
"en": "This bookcase is not part of a bigger network",
"nl": "Dit boekenruilkastje maakt geen deel uit van een netwerk"
},
"if": "nobrand=yes&brand=&ref="
}
],
"question": {
"en": "What is the reference number of this public bookcase?",
"nl": "Wat is het referentienummer van dit boekenruilkastje?"
},
"type": "text",
"render": {
"en": "The reference number of this public bookcase within {brand} is {ref}",
"nl": "Het referentienummer binnen {brand} is {ref}"
},
"condition": "brand=*"
},
{
"key": "start_date",
"mappings": [],
"question": {
"en": "When was this public bookcase installed?",
"nl": "Op welke dag werd dit boekenruilkastje geinstalleerd?"
},
"type": "date",
"render": {
"en": "Installed on {start_date}",
"nl": "Geplaatst op {start_date}"
}
},
{
"key": "website",
"mappings": [],
"type": "url",
"question": {
"en": "Is there a website with more information about this public bookcase?",
"nl": "Is er een website over dit boekenruilkastje?"
},
"render": "{website}"
} }
] ]
} }
], ]
"icon": "./assets/bookcase.svg",
"name": "bookcases",
"title": "Bookcases",
"description": "Welcome to open Bookcase Map",
"language": "en",
"startLat": "0",
"startLon": "0",
"startZoom": "12"
} }

View file

@ -3,12 +3,42 @@ import {VariableUiElement} from "./UI/Base/VariableUIElement";
import {UIEventSource} from "./Logic/UIEventSource"; import {UIEventSource} from "./Logic/UIEventSource";
import {ThemeGenerator} from "./UI/CustomThemeGenerator/ThemeGenerator"; import {ThemeGenerator} from "./UI/CustomThemeGenerator/ThemeGenerator";
import {Preview} from "./UI/CustomThemeGenerator/Preview"; import {Preview} from "./UI/CustomThemeGenerator/Preview";
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
import {createHash} from "crypto";
import Combine from "./UI/Base/Combine";
import {Button} from "./UI/Base/Button";
const connection = new OsmConnection(true, new UIEventSource<string>(undefined), false); const connection = new OsmConnection(true, new UIEventSource<string>(undefined), false);
connection.AttemptLogin(); connection.AttemptLogin();
let hash = window.location.hash?.substr(1);
const localStorage = LocalStorageSource.Get("last-custom-save");
console.log("hash", hash)
console.log("Saved: ", localStorage.data)
const themeGenerator = new ThemeGenerator(connection, window.location.hash?.substr(1)); if (hash === undefined || hash === "") {
themeGenerator.AttachTo("layoutCreator") const previous = localStorage.data.split("#");
hash = previous[1];
console.log("Using previously saved data ", hash)
}
new Preview(themeGenerator.url, themeGenerator.themeObject).AttachTo("preview"); const themeGenerator = new ThemeGenerator(connection, hash);
themeGenerator.AttachTo("layoutCreator");
themeGenerator.url.syncWith(localStorage);
new Combine([
new Preview(themeGenerator.url, themeGenerator.themeObject),
"<h2>Danger zone</h2>",
new Button("Clear theme", () => {
themeGenerator.themeObject.data.title = undefined;
themeGenerator.themeObject.data.description = undefined;
themeGenerator.themeObject.data.icon = undefined;
themeGenerator.themeObject.data.language = ["en"];
themeGenerator.themeObject.data.name = undefined;
themeGenerator.themeObject.data.startLat = 0;
themeGenerator.themeObject.data.startLon = 0;
themeGenerator.themeObject.data.startZoom = 12;
themeGenerator.themeObject.data.maintainer = connection.userDetails.data.name;
themeGenerator.themeObject.data.layers = [];
themeGenerator.themeObject.ping();
})]).AttachTo("preview");

View file

@ -78,6 +78,10 @@ form {
padding-bottom: 0.15em; padding-bottom: 0.15em;
} }
.subtle {
color: #cccccc;
}
.thanks { .thanks {
background-color: #43d904; background-color: #43d904;
font-weight: bold; font-weight: bold;
@ -1196,6 +1200,7 @@ form {
background-color: lightgray; background-color: lightgray;
padding: 0.5em; padding: 0.5em;
word-break: break-all; word-break: break-all;
color: black;
} }
.iframe-code-block { .iframe-code-block {

View file

@ -1,3 +1,4 @@
import {TagRendering} from "./Customizations/TagRendering";
import {UserBadge} from "./UI/UserBadge"; import {UserBadge} from "./UI/UserBadge";
import {CenterMessageBox} from "./UI/CenterMessageBox"; import {CenterMessageBox} from "./UI/CenterMessageBox";
import {TagUtils} from "./Logic/TagsFilter"; import {TagUtils} from "./Logic/TagsFilter";
@ -13,13 +14,11 @@ import {StrayClickHandler} from "./Logic/Leaflet/StrayClickHandler";
import {GeoLocationHandler} from "./Logic/Leaflet/GeoLocationHandler"; import {GeoLocationHandler} from "./Logic/Leaflet/GeoLocationHandler";
import {State} from "./State"; import {State} from "./State";
import {CustomLayout} from "./Logic/CustomLayers"; import {CustomLayout} from "./Logic/CustomLayers";
import {TagRenderingOptions} from "./Customizations/TagRenderingOptions";
import {TagRendering} from "./Customizations/TagRendering";
import {Img} from "./UI/Img";
import Combine from "./UI/Base/Combine";
import {CustomLayoutFromJSON} from "./Customizations/JSON/CustomLayoutFromJSON"; import {CustomLayoutFromJSON} from "./Customizations/JSON/CustomLayoutFromJSON";
import {QueryParameters} from "./Logic/Web/QueryParameters"; import {QueryParameters} from "./Logic/Web/QueryParameters";
TagRendering.injectFunction();
// --------------------- Special actions based on the parameters ----------------- // --------------------- Special actions based on the parameters -----------------
@ -70,8 +69,14 @@ let layoutToUse: Layout = AllKnownLayouts.allSets[defaultLayout] ?? AllKnownLayo
const layoutFromBase64 = QueryParameters.GetQueryParameter("userlayout", "false").data; const layoutFromBase64 = QueryParameters.GetQueryParameter("userlayout", "false").data;
if(layoutFromBase64 !== "false"){ if (layoutFromBase64 !== "false") {
layoutToUse = CustomLayoutFromJSON.FromQueryParam(hash.substr(1)); try {
layoutToUse = CustomLayoutFromJSON.FromQueryParam(hash.substr(1));
} catch (e) {
new FixedUiElement("Error: could not parse the custom layout:<br/> "+e).AttachTo("centermessage");
throw e;
}
} }
@ -84,9 +89,8 @@ if (layoutToUse === undefined) {
console.log("Using layout: ", layoutToUse.name); console.log("Using layout: ", layoutToUse.name);
TagRendering.injectFunction();
State.state = new State(layoutToUse); State.state = new State(layoutToUse);
if(layoutFromBase64 !== "false"){ if (layoutFromBase64 !== "false") {
State.state.layoutDefinition = hash.substr(1); State.state.layoutDefinition = hash.substr(1);
console.log(State.state.layoutDefinition) console.log(State.state.layoutDefinition)
} }
@ -106,13 +110,26 @@ function setupAllLayerElements() {
InitUiElements.OnlyIf(State.state.featureSwitchAddNew, () => { InitUiElements.OnlyIf(State.state.featureSwitchAddNew, () => {
let presetCount = 0;
for (const layer of State.state.filteredLayers.data) {
for (const preset of layer.layerDef.presets) {
presetCount++;
}
}
if (presetCount == 0) {
console.log("No presets defined - not creating the StrayClickHandler");
return;
}
new StrayClickHandler(() => { new StrayClickHandler(() => {
return new SimpleAddUI(); return new SimpleAddUI();
} }
); );
}); });
new CenterMessageBox() .AttachTo("centermessage"); new CenterMessageBox().AttachTo("centermessage");
} }

View file

@ -12,9 +12,9 @@
] ]
}, },
"scripts": { "scripts": {
"start": "parcel *.html UI/** Logic/** assets/**/* assets/* vendor/* vendor/*/*", "start": "parcel *.html UI/** Logic/** assets/**/* assets/**/**/* assets/* vendor/* vendor/*/*",
"generate": "ts-node createLayouts.ts", "generate": "ts-node createLayouts.ts",
"build": "rm -rf dist/ && parcel build --public-url ./ *.html assets/* assets/**/* vendor/* vendor/*/*", "build": "rm -rf dist/ && parcel build --public-url ./ *.html assets/* assets/**/* assets/**/**/* vendor/* vendor/*/*",
"clean": "./clean.sh", "clean": "./clean.sh",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },

View file

@ -0,0 +1,4 @@
import {TextField, ValidatedTextField} from "./UI/Input/TextField";
ValidatedTextField.TagTextField().AttachTo("maindiv")
.GetValue().addCallback(console.log);