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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -60,9 +60,6 @@ export class Tag extends TagsFilter {
public invertValue: boolean
constructor(key: string | RegExp, value: string | RegExp, invertValue = false) {
if (value === "*" && invertValue) {
throw new Error("Invalid combination: invertValue && value == *")
}
if (value instanceof RegExp && invertValue) {
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 {
for (const tag of tags) {
if (Tag.regexOrStrMatches(this.key, tag.k)) {
if (tag.v === "") {
// This tag has been removed
return this.value === ""
// This tag has been removed -> always matches false
return false;
}
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;
}
@ -288,4 +290,12 @@ export class TagUtils {
}
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
# 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:
Data from OpenStreetMap

View file

@ -24,7 +24,7 @@ export class State {
// The singleton of the global state
public static state: State;
public static vNumber = "0.0.4";
public static vNumber = "0.0.5";
public static runningFromConsole: boolean = false;
@ -181,9 +181,7 @@ export class State {
this.allElements = new ElementStorage();
this.changes = new Changes(
"Beantwoorden van vragen met #MapComplete voor vragenset #" + this.layoutToUse.data.name,
this);
this.changes = new Changes(this);
if(State.runningFromConsole){
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 {UIElement} from "../UIElement";
import Combine from "../Base/Combine";
import {Button} from "../Base/Button";
import {VariableUiElement} from "../Base/VariableUIElement";
export class Preview extends UIElement {
private url: UIEventSource<string>;
private config: UIEventSource<LayoutConfigJson>;
private currentPreview = new UIEventSource<string>("")
private reloadButton: Button;
private otherPreviews: VariableUiElement;
constructor(url: UIEventSource<string>, config: UIEventSource<LayoutConfigJson>) {
super(url);
super(undefined);
this.config = config;
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 {
const url = this.url.data;
return new Combine([
`<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> ',
`<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>'
new VariableUiElement(this.currentPreview),
this.reloadButton,
this.otherPreviews
]).Render();
}

View file

@ -3,6 +3,7 @@ import {VerticalCombine} from "../Base/VerticalCombine";
import {VariableUiElement} from "../Base/VariableUIElement";
import Combine from "../Base/Combine";
import {
CustomLayoutFromJSON,
LayerConfigJson,
LayoutConfigJson,
TagRenderingConfigJson
@ -12,9 +13,14 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import {OsmConnection, UserDetails} from "../../Logic/Osm/OsmConnection";
import {Button} from "../Base/Button";
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 }[]) {
if (tags === undefined) {
return undefined;
@ -34,30 +40,31 @@ function TagsToString(tags: string | string [] | { k: string, v: string }[]) {
return newTags.join(",");
}
let createFieldUI: (label: string, key: string, root: any, options?: { deflt?: string }) => UIElement;
// Defined below, as it needs some context/closure
let createFieldUI: (label: string, key: string, root: any, options: { deflt?: string, type?: string, description: string, emptyAllowed?: boolean }) => UIElement;
class MappingGenerator extends UIElement {
private elements: UIElement[];
constructor(fullConfig: UIEventSource<LayoutConfigJson>,
layerConfig: LayerConfigJson,
tagRendering: TagRenderingConfigJson,
constructor(tagRendering: TagRenderingConfigJson,
mapping: { if: string | string[] | { k: string, v: string }[] }) {
super(undefined);
this.CreateElements(fullConfig, layerConfig, tagRendering, mapping)
this.CreateElements(tagRendering, mapping)
}
private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson,
tagRendering: TagRenderingConfigJson,
private CreateElements(tagRendering: TagRenderingConfigJson,
mapping) {
{
const self = this;
this.elements = [
createFieldUI("If these tags apply", "if", mapping),
createFieldUI("Then: show this text", "then", 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("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", () => {
for (let i = 0; i < tagRendering.mappings.length; i++) {
if (tagRendering.mappings[i] === mapping) {
@ -89,39 +96,75 @@ class TagRenderingGenerator
constructor(fullConfig: UIEventSource<LayoutConfigJson>,
layerConfig: LayerConfigJson,
tagRendering: TagRenderingConfigJson,
isTitle: boolean = false) {
options: { header: string, description: string, removable: boolean, hideQuestion: boolean }) {
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;
this.elements = [
new FixedUiElement(isTitle ? "<h3>Popup title</h3>" : "<h3>TagRendering/TagQuestion</h3>"),
createFieldUI("Key", "key", tagRendering),
createFieldUI("Rendering", "render", tagRendering),
createFieldUI("Type", "type", tagRendering),
createFieldUI("Question", "question", tagRendering),
createFieldUI("Extra tags", "addExtraTags", tagRendering),
new FixedUiElement(`<h3>${options.header}</h3>`),
new FixedUiElement(options.description),
createFieldUI("Key", "key", tagRendering, {
deflt: "name",
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("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) => {
return new MappingGenerator(fullConfig, layerConfig, tagRendering, mapping)
return new MappingGenerator(tagRendering, mapping)
}),
new Button("Add mapping", () => {
if (tagRendering.mappings === undefined) {
tagRendering.mappings = []
}
tagRendering.mappings.push({if: "", then: ""});
self.CreateElements(fullConfig, layerConfig, tagRendering, isTitle);
self.CreateElements(fullConfig, layerConfig, tagRendering, options);
self.Update();
})
]
if (!isTitle) {
const b = new Button("Remove this preset", () => {
if (!!options.removable) {
const b = new Button("Remove this tag rendering", () => {
for (let i = 0; i < layerConfig.tagRenderings.length; i++) {
if (layerConfig.tagRenderings[i] === tagRendering) {
layerConfig.tagRenderings.splice(i, 1);
@ -155,10 +198,21 @@ class PresetGenerator extends UIElement {
const self = this;
this.elements = [
new FixedUiElement("<h3>Preset</h3>"),
createFieldUI("Title", "title", preset0),
createFieldUI("Description", "description", preset0, {deflt: layerConfig.description}),
createFieldUI("icon", "icon", preset0, {deflt: layerConfig.icon}),
createFieldUI("tags", "tags", preset0, {deflt: TagsToString(layerConfig.overpassTags)}),
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("Title", "title", preset0, {
description: "The title of this preset, shown in the 'add new {Title} here'-dialog"
}),
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", () => {
for (let i = 0; i < layerConfig.presets.length; i++) {
if (layerConfig.presets[i] === preset0) {
@ -201,12 +255,86 @@ class LayerGenerator extends UIElement {
private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson) {
const self = this;
this.uielements = [
createFieldUI("The name of this layer", "id", layerConfig),
createFieldUI("A description of objects for this layer", "description", layerConfig),
createFieldUI("The icon of this layer, either a URL or a base64-encoded svg", "icon", layerConfig),
createFieldUI("The default stroke color", "color", layerConfig),
createFieldUI("The minimal needed zoom to start loading", "minzoom", layerConfig),
createFieldUI("The tags to load from overpass", "overpassTags", 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("Name", "id", layerConfig, {description: "The name of this layer"}),
createFieldUI("A description of objects for this layer", "description", layerConfig, {description: "The description of this layer"}),
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)),
new Button("Add a preset", () => {
layerConfig.presets.push({
@ -217,28 +345,7 @@ class LayerGenerator extends UIElement {
});
self.CreateElements(fullConfig, layerConfig);
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 config = this.config;
for (const layer of this.config.data.layers) {
const iconUrl = CustomLayoutFromJSON.TagRenderingFromJson(layer?.icon)
.GetContent({id: "node/-1"});
const header = this.config.map(() => {
return `<img src="${layer?.icon ?? "./assets/help.svg"}">`
return `<img src="${iconUrl ?? "./assets/help.svg"}">`
});
layerPanes.push({
header: new VariableUiElement(header),
@ -290,10 +401,17 @@ class AllLayerComponent extends UIElement {
config.data.layers.push({
id: "",
title: {
render: "Title"
key: "",
render: "title"
},
icon: {
key: "",
render: "./assets/bug.svg"
},
color: {
key: "",
render: "#0000ff"
},
icon: "./assets/bug.svg",
color: "",
description: "",
minzoom: 12,
overpassTags: "",
@ -333,38 +451,118 @@ export class ThemeGenerator extends UIElement {
if (windowHash !== undefined && windowHash.length > 4) {
loadedTheme = JSON.parse(atob(windowHash));
}
this.themeObject = new UIEventSource<LayoutConfigJson>(loadedTheme ?? defaultTheme);
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);
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;
createFieldUI = (label, key, root, options) => {
options = options ?? {description: "?"};
options.type = options.type ?? "string";
const value = new UIEventSource<string>(TagsToString(root[key]) ?? options?.deflt);
value.addCallback((v) => {
root[key] = v;
self.themeObject.ping(); // We assume the root is a part of the themeObject
})
return new Combine([
label,
new TextField<string>({
let textField: UIElement;
if (options.type === "typeSelector") {
const options: { value: string, shown: string | UIElement }[] = [];
for (const possibleType in ValidatedTextField.inputValidation) {
if (possibleType !== "$") {
options.push({value: possibleType, shown: possibleType});
}
}
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,
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 = [
createFieldUI("Name of this theme", "name", jsonObjectRoot),
createFieldUI("Title (shown in the window and in the welcome message)", "title", jsonObjectRoot),
createFieldUI("Description (shown in the welcome message and various other places)", "description", jsonObjectRoot),
createFieldUI("The supported language", "language", jsonObjectRoot),
createFieldUI("startLat", "startLat", jsonObjectRoot),
createFieldUI("startLon", "startLon", jsonObjectRoot),
createFieldUI("startzoom", "startZoom", jsonObjectRoot),
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"}),
createFieldUI("Name of this theme", "name", jsonObjectRoot, {description: "An identifier for this theme"}),
createFieldUI("Title", "title", jsonObjectRoot, {
deflt: "Title",
description: "The title of this theme, as shown in the welcome message and in the title bar of the browser"
}),
createFieldUI("Description", "description", jsonObjectRoot, {
description: "Shown in the welcome message",
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)
]
@ -383,8 +581,6 @@ export class ThemeGenerator extends UIElement {
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,
]).Render();
}

View file

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

View file

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

View file

@ -2,9 +2,97 @@ import {UIElement} from "../UIElement";
import {InputElement} from "./InputElement";
import Translations from "../i18n/Translations";
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> {
private value: UIEventSource<string>;
private mappedValue: UIEventSource<T>;
/**
@ -14,6 +102,7 @@ export class TextField<T> extends InputElement<T> {
private _placeholder: UIElement;
private _fromString?: (string: string) => T;
private _toString: (t: T) => string;
private startValidated: boolean;
constructor(options: {
@ -33,7 +122,8 @@ export class TextField<T> extends InputElement<T> {
* @param string
*/
fromString: (string: string) => T,
value?: UIEventSource<T>
value?: UIEventSource<T>,
startValidated?: boolean,
}) {
super(undefined);
const self = this;
@ -63,7 +153,8 @@ export class TextField<T> extends InputElement<T> {
}
// @ts-ignore
field.value = options.toString(t);
})
});
this.startValidated = options.startValidated ?? false;
}
GetValue(): UIEventSource<T> {
@ -92,6 +183,8 @@ export class TextField<T> extends InputElement<T> {
this.mappedValue.addCallback((data) => {
field.className = this.mappedValue.data !== undefined ? "valid" : "invalid";
});
field.className = this.mappedValue.data !== undefined ? "valid" : "invalid";
const self = this;
field.oninput = () => {

View file

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

View file

@ -1,5 +1,5 @@
import {UIElement} from "./UIElement";
import {Tag} from "../Logic/TagsFilter";
import {Tag, TagUtils} from "../Logic/TagsFilter";
import {FilteredLayer} from "../Logic/FilteredLayer";
import Translations from "./i18n/Translations";
import Combine from "./Base/Combine";
@ -48,12 +48,18 @@ export class SimpleAddUI extends UIElement {
for (const layer of State.state.filteredLayers.data) {
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 =
new SubtleButton(
preset.icon,
icon,
new Combine([
"<b>",
preset.title,
@ -61,7 +67,7 @@ export class SimpleAddUI extends UIElement {
preset.description !== undefined ? preset.description : ""])
).onClick(
() => {
self.confirmButton = new SubtleButton(preset.icon,
self.confirmButton = new SubtleButton(icon,
new Combine([
"<b>",
Translations.t.general.add.confirmButton.Subs({category: preset.title}),
@ -73,7 +79,7 @@ export class SimpleAddUI extends UIElement {
layerToAddTo: layer,
name: preset.title,
description: preset.description,
icon: preset.icon
icon: icon
});
}
)

View file

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

View file

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

View file

@ -643,7 +643,7 @@ export default class Translations {
}),
bookkinds: new T({
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 {
if (s instanceof UIElement) {
return s;
if (typeof (s) === "string") {
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() {

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": [
{
"id": "bookcases",
"id": "Bookcases",
"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",
"overpassTags": "amenity=public_bookcase",
"presets": [
{
"title": "Bookcase",
"description": "A bookcase is a small cabinet where everyone can take or leave a book",
"icon": "./assets/bookcase.svg",
"tags": "amenity=public_bookcase"
"title": {
"en": "Bookcase",
"nl": "Boekenruilkast"
}
}
],
"tagRenderings": [
{
"key": "brand",
"addExtraTags": "",
"key": "name",
"mappings": [
{
"if": "brand=Little Free Library",
"then": "Part of the network Little Free Library"
"then": {
"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?",
"render": "Part of {brand}",
"question": {
"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"
},
{
"key": "",
"addExtraTags": "",
"key": "capacity",
"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": [
{
"if": "books=children",
"then": "Mainly books for kids"
"then": {
"en": "Mostly children books",
"nl": "Voornamelijk kinderboeken"
}
},
{
"if": "books=adult",
"then": "Mainly books for adults"
"if": "books=adults",
"then": {
"en": "Mostly books for adults",
"nl": "Voornamelijk boeken voor volwassenen"
}
},
{
"if": "books=adult;children",
"then": "Books for both adults and children"
"if": "books=children;adults",
"then": {
"en": "Both books for kids and adults",
"nl": "Boeken voor zowel kinderen als volwassenen"
}
}
],
"question": "Which books can be found here?",
"render": "",
"question": {
"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"
},
{
"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 {ThemeGenerator} from "./UI/CustomThemeGenerator/ThemeGenerator";
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);
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));
themeGenerator.AttachTo("layoutCreator")
if (hash === undefined || hash === "") {
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;
}
.subtle {
color: #cccccc;
}
.thanks {
background-color: #43d904;
font-weight: bold;
@ -1196,6 +1200,7 @@ form {
background-color: lightgray;
padding: 0.5em;
word-break: break-all;
color: black;
}
.iframe-code-block {

View file

@ -1,3 +1,4 @@
import {TagRendering} from "./Customizations/TagRendering";
import {UserBadge} from "./UI/UserBadge";
import {CenterMessageBox} from "./UI/CenterMessageBox";
import {TagUtils} from "./Logic/TagsFilter";
@ -13,13 +14,11 @@ import {StrayClickHandler} from "./Logic/Leaflet/StrayClickHandler";
import {GeoLocationHandler} from "./Logic/Leaflet/GeoLocationHandler";
import {State} from "./State";
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 {QueryParameters} from "./Logic/Web/QueryParameters";
TagRendering.injectFunction();
// --------------------- Special actions based on the parameters -----------------
@ -70,8 +69,14 @@ let layoutToUse: Layout = AllKnownLayouts.allSets[defaultLayout] ?? AllKnownLayo
const layoutFromBase64 = QueryParameters.GetQueryParameter("userlayout", "false").data;
if(layoutFromBase64 !== "false"){
layoutToUse = CustomLayoutFromJSON.FromQueryParam(hash.substr(1));
if (layoutFromBase64 !== "false") {
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);
TagRendering.injectFunction();
State.state = new State(layoutToUse);
if(layoutFromBase64 !== "false"){
if (layoutFromBase64 !== "false") {
State.state.layoutDefinition = hash.substr(1);
console.log(State.state.layoutDefinition)
}
@ -106,13 +110,26 @@ function setupAllLayerElements() {
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(() => {
return new SimpleAddUI();
}
);
});
new CenterMessageBox() .AttachTo("centermessage");
new CenterMessageBox().AttachTo("centermessage");
}

View file

@ -12,9 +12,9 @@
]
},
"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",
"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",
"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);