More work on the theme generator

This commit is contained in:
Pieter Vander Vennet 2020-08-23 01:49:19 +02:00
parent cd37d8db98
commit 0f433d026a
13 changed files with 340 additions and 87 deletions

View file

@ -57,6 +57,7 @@ export interface LayerConfigJson {
} }
export interface LayoutConfigJson { export interface LayoutConfigJson {
widenFactor: number;
name: string; name: string;
title: string; title: string;
description: string; description: string;
@ -161,7 +162,7 @@ export class CustomLayoutFromJSON {
return (tags) => { return (tags) => {
const iconUrl = iconRendering.GetContent(tags); const iconUrl = iconRendering.GetContent(tags);
const stroke = colourRendering.GetContent(tags); const stroke = colourRendering.GetContent(tags) ?? "#00f";
let weight = parseInt(thickness?.GetContent(tags)) ?? 10; let weight = parseInt(thickness?.GetContent(tags)) ?? 10;
if(isNaN(weight)){ if(isNaN(weight)){
weight = 10; weight = 10;
@ -187,9 +188,13 @@ export class CustomLayoutFromJSON {
let kv: string[] = undefined; let kv: string[] = undefined;
let invert = false; let invert = false;
let regex = false;
if (json.indexOf("!=") >= 0) { if (json.indexOf("!=") >= 0) {
kv = json.split("!="); kv = json.split("!=");
invert = true; invert = true;
} else if (json.indexOf("~=") >= 0) {
kv = json.split("~=");
regex = true;
} else { } else {
kv = json.split("="); kv = json.split("=");
} }
@ -200,7 +205,12 @@ export class CustomLayoutFromJSON {
if (kv[0].trim() === "") { if (kv[0].trim() === "") {
return undefined; return undefined;
} }
return new Tag(kv[0].trim(), kv[1].trim(), invert); let v = kv[1].trim();
if(v.startsWith("/") && v.endsWith("/")){
v = v.substr(1, v.length - 2);
regex = true;
}
return new Tag(kv[0].trim(), regex ? new RegExp(v): v, invert);
} }
public static TagsFromJson(json: string | { k: string, v: string }[]): Tag[] { public static TagsFromJson(json: string | { k: string, v: string }[]): Tag[] {
@ -229,12 +239,8 @@ export class CustomLayoutFromJSON {
const tr = CustomLayoutFromJSON.TagRenderingFromJson; const tr = CustomLayoutFromJSON.TagRenderingFromJson;
const tags = CustomLayoutFromJSON.TagsFromJson(json.overpassTags); 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 // 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({ const icon = CustomLayoutFromJSON.TagRenderingFromJson(json.icon).construct({
tags: new UIEventSource<any>(properties) tags: new UIEventSource<any>({})
}).InnerRender(); }).InnerRender();
@ -287,6 +293,10 @@ export class CustomLayoutFromJSON {
); );
layout.icon = json.icon; layout.icon = json.icon;
layout.maintainer = json.maintainer; layout.maintainer = json.maintainer;
layout.widenFactor = parseFloat(json.widenFactor) ?? 0.03;
if (layout.widenFactor > 0.1) {
layout.widenFactor = 0.1;
}
return layout; return layout;
} }

View file

@ -391,7 +391,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
InnerRender(): string { InnerRender(): string {
if (this.IsQuestioning() && !State.state.osmConnection.userDetails.data.loggedIn) { if (this.IsQuestioning() && !State.state?.osmConnection?.userDetails?.data?.loggedIn) {
const question = const question =
this.ApplyTemplate(this._question).Render(); this.ApplyTemplate(this._question).Render();
return "<div class='question'>" + return "<div class='question'>" +

View file

@ -30,6 +30,9 @@ export class Changes {
addTag(elementId: string, tagsFilter : TagsFilter){ addTag(elementId: string, tagsFilter : TagsFilter){
if(tagsFilter instanceof Tag){ if(tagsFilter instanceof Tag){
const tag = tagsFilter as Tag; const tag = tagsFilter as Tag;
if(typeof tag.value !== "string"){
throw "Invalid value"
}
this.addChange(elementId, tag.key, tag.value); this.addChange(elementId, tag.key, tag.value);
return; return;
} }
@ -114,6 +117,9 @@ export class Changes {
// The tags are not yet written into the OsmObject, but this is applied onto a // The tags are not yet written into the OsmObject, but this is applied onto a
for (const kv of basicTags) { for (const kv of basicTags) {
properties[kv.key] = kv.value; properties[kv.key] = kv.value;
if(typeof kv.value !== "string"){
throw "Invalid value"
}
this._pendingChanges.push({elementId: id, key: kv.key, value: kv.value}); this._pendingChanges.push({elementId: id, key: kv.key, value: kv.value});
} }
this.pendingChangesES.setData(this._pendingChanges.length); this.pendingChangesES.setData(this._pendingChanges.length);

View file

@ -62,7 +62,7 @@ export class Regex extends TagsFilter {
export class Tag extends TagsFilter { export class Tag extends TagsFilter {
public key: string public key: string
public value: string public value: string | RegExp
public invertValue: boolean public invertValue: boolean
constructor(key: string | RegExp, value: string | RegExp, invertValue = false) { constructor(key: string | RegExp, value: string | RegExp, invertValue = false) {
@ -156,7 +156,15 @@ export class Tag extends TagsFilter {
`=` + `=` +
`<a href='https://wiki.openstreetmap.org/wiki/Tag:${this.key}%3D${this.value}' target='_blank'>${this.value}</a>` `<a href='https://wiki.openstreetmap.org/wiki/Tag:${this.key}%3D${this.value}' target='_blank'>${this.value}</a>`
} }
return this.key + "=" + this.value;
console.log("Humanizing", this)
if (typeof (this.value) === "string") {
return this.key + (this.invertValue ? "!=": "=") + this.value;
}else{
// value is a regex
return this.key + "~=" + this.value.source;
}
} }
} }

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.5e"; public static vNumber = "0.0.5h";
// The user journey states thresholds when a new feature gets unlocked // The user journey states thresholds when a new feature gets unlocked
public static userJourney = { public static userJourney = {

View file

@ -44,7 +44,7 @@ function TagsToString(tags: string | string [] | { k: string, v: string }[]) {
// Defined below, as it needs some context/closure // 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; let createFieldUI: (label: string, key: string, root: any, options: { deflt?: string, type?: string, description: string, emptyAllowed?: boolean }) => UIElement;
let pingThemeObject: () => void;
class MappingGenerator extends UIElement { class MappingGenerator extends UIElement {
@ -75,6 +75,7 @@ class MappingGenerator extends UIElement {
new FixedUiElement("Tag mapping removed") new FixedUiElement("Tag mapping removed")
] ]
self.Update(); self.Update();
pingThemeObject();
break; break;
} }
} }
@ -111,9 +112,10 @@ class TagRenderingGenerator
this.elements = [ this.elements = [
new FixedUiElement(`<h3>${options.header}</h3>`), new FixedUiElement(`<h3>${options.header}</h3>`),
new FixedUiElement(options.description), new FixedUiElement(options.description),
createFieldUI("Key", "key", tagRendering, { options.hideQuestion ? new FixedUiElement("") : createFieldUI("Key", "key", tagRendering, {
deflt: "name", 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." type: "key",
description: "Optional. A single key, such as <span class='literal-code'>name</span> &npbs, &npbs <span class='literal-code'>surface</span>. If the object contains a tag with the specified key, the rendering below will be shown. Use <span class='literal-code'>*</span> if you want to show the rendering by default. Note that a mapping overrides this"
}), }),
createFieldUI("Rendering", "render", tagRendering, { createFieldUI("Rendering", "render", tagRendering, {
deflt: "The name of this object is {name}", deflt: "The name of this object is {name}",
@ -141,7 +143,7 @@ class TagRenderingGenerator
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>" 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( options.hideQuestion ? new FixedUiElement("") : createFieldUI(
"Only show if", "condition", tagRendering, "Only show if", "condition", tagRendering,
{ {
deflt: "", deflt: "",
@ -223,6 +225,7 @@ class PresetGenerator extends UIElement {
new FixedUiElement("Preset removed") new FixedUiElement("Preset removed")
] ]
self.Update(); self.Update();
pingThemeObject();
break; break;
} }
} }
@ -255,6 +258,51 @@ class LayerGenerator extends UIElement {
} }
private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson) { private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson) {
// Init some defaults
layerConfig.title = layerConfig.title ?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "Title",
type: "text"
};
layerConfig.title.key = "*";
layerConfig.icon = layerConfig.icon ?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "./assets/bug.svg",
type: "text"
};
layerConfig.icon.key = "*";
layerConfig.color = layerConfig.color ?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "#00f",
type: "text"
};
layerConfig.color.key = "*";
layerConfig.width = layerConfig.width?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "10",
type: "nat"
};
layerConfig.width.key = "*"
const self = this; const self = this;
this.uielements = [ this.uielements = [
@ -269,7 +317,16 @@ class LayerGenerator extends UIElement {
}), }),
createFieldUI("The tags to load from overpass", "overpassTags", layerConfig, { createFieldUI("The tags to load from overpass", "overpassTags", layerConfig, {
type: "tags", 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> to have everything. An missing tag can be expressed as <span class='literal-code'>key=</span>, not as <span class='literal-code'>key!=value</span>. E.g. something that is indoor and not private and has no name tag can be queried as <span class='literal-code'>indoor=yes&name=&access!=private</span>" 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>." +
"Special values are:" +
"<ul>" +
"<li> <span class='literal-code'>key=*</span> to indicate that this key can be anything</li>. " +
"<li><span class='literal-code'>key=</span> means 'key is NOT present'</li>" +
"<li><span class='literal-code'>key!=value</span> means 'key does NOT have this value'</li>" +
"<li><span class='literal-code'>key~=regex</span> indicates a regex, e.g. <b>highway~=residential|tertiary</b></li>"+
"</ul>"+
". E.g. something that is indoor, not private and has no name tag can be queried as <span class='literal-code'>indoor=yes&name=&access!=private</span>"
}), }),
createFieldUI("Wayhandling","wayHandling", layerConfig, { createFieldUI("Wayhandling","wayHandling", layerConfig, {
@ -277,14 +334,7 @@ class LayerGenerator extends UIElement {
description: "Specifies how ways (lines and areas) are handled: either the way is shown, a center point is shown or both" description: "Specifies how ways (lines and areas) are handled: either the way is shown, a center point is shown or both"
}), }),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.title ?? { new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.title, {
key: "",
addExtraTags: "",
mappings: [],
question: "",
render: "Title",
type: "string"
}, {
header: "Title element", header: "Title element",
description: "This element is shown in the title of the popup in a header-tag", description: "This element is shown in the title of the popup in a header-tag",
removable: false, removable: false,
@ -292,14 +342,7 @@ class LayerGenerator extends UIElement {
}), }),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.icon ?? { new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.icon , {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "./assets/bug.svg",
type: "text"
}, {
header: "Icon", 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", 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, removable: false,
@ -307,28 +350,14 @@ class LayerGenerator extends UIElement {
}), }),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.color ?? { new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.color, {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "#0000ff",
type: "text"
}, {
header: "Colour", 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", 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, removable: false,
hideQuestion: true hideQuestion: true
}), }),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.width ?? { new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.width , {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "10",
type: "nat"
}, {
header: "Line thickness", header: "Line thickness",
description: "This decides the line thickness of ways (in pixels)", description: "This decides the line thickness of ways (in pixels)",
removable: false, removable: false,
@ -402,9 +431,8 @@ 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) const iconUrl = CustomLayoutFromJSON.TagRenderingFromJson(layer?.icon)
.GetContent({id: "node/-1"}); ?.GetContent({id: "node/-1"});
const header = this.config.map(() => { const header = this.config.map(() => {
return `<img src="${iconUrl ?? "./assets/help.svg"}">` return `<img src="${iconUrl ?? "./assets/help.svg"}">`
@ -498,12 +526,14 @@ export class ThemeGenerator extends UIElement {
this.url = base64.map((data) => baseUrl + `/index.html?test=true&userlayout=true#` + data); this.url = base64.map((data) => baseUrl + `/index.html?test=true&userlayout=true#` + data);
const self = this; const self = this;
pingThemeObject = () => {self.themeObject.ping()};
createFieldUI = (label, key, root, options) => { createFieldUI = (label, key, root, options) => {
options = options ?? {description: "?"}; options = options ?? {description: "?"};
options.type = options.type ?? "string"; 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);
let textField: UIElement; let textField: UIElement;
if (options.type === "typeSelector") { if (options.type === "typeSelector") {
const options: { value: string, shown: string | UIElement }[] = []; const options: { value: string, shown: string | UIElement }[] = [];
@ -532,6 +562,24 @@ export class ThemeGenerator extends UIElement {
options, options,
value) value)
} else if (options.type === "key") {
textField = new TextField<string>({
placeholder: "single key",
startValidated: false,
value: new UIEventSource<string>(""),
toString: str => str,
fromString: str => {
if (str === "*") {
return str;
}
str = str.trim();
if (str.match("^_*[a-zA-Z]*[a-zA-Z0-9:]*$") == null) {
return undefined;
}
return str.trim();
}
})
} else if (options.type === "tags") { } else if (options.type === "tags") {
textField = ValidatedTextField.TagTextField(value.map(CustomLayoutFromJSON.TagsFromJson, [], tags => { textField = ValidatedTextField.TagTextField(value.map(CustomLayoutFromJSON.TagsFromJson, [], tags => {
if (tags === undefined) { if (tags === undefined) {
@ -567,6 +615,11 @@ export class ThemeGenerator extends UIElement {
} }
self.themeObject.ping(); // We assume the root is a part of the themeObject self.themeObject.ping(); // We assume the root is a part of the themeObject
}); });
self.themeObject.addCallback(() => {
value.setData(root[key]);
})
return new Combine([ return new Combine([
label, label,
textField, textField,
@ -581,6 +634,11 @@ export class ThemeGenerator extends UIElement {
deflt: "Title", deflt: "Title",
description: "The title of this theme, as shown in the welcome message and in the title bar of the browser" description: "The title of this theme, as shown in the welcome message and in the title bar of the browser"
}), }),
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"
}),
createFieldUI("Description", "description", jsonObjectRoot, { createFieldUI("Description", "description", jsonObjectRoot, {
description: "Shown in the welcome message", description: "Shown in the welcome message",
deflt: "Description" deflt: "Description"
@ -604,11 +662,12 @@ export class ThemeGenerator extends UIElement {
deflt: "12", deflt: "12",
description: "The initial zoom level where the map is located" description: "The initial zoom level where the map is located"
}), }),
createFieldUI("icon", "icon", jsonObjectRoot, { createFieldUI("Query widening factor", "widenFactor", jsonObjectRoot, {
deflt: "./assets/bug.svg", type: "pfloat",
type: "img", deflt: "0.05",
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" description: "When a query is run, the current map view is taken and a margin with a certain factor is added to allow panning and zooming. If you are running heavy queries (e.g. highway=residential), to much data is returned. In that case, lower the widenfactor, e.g. to 0.01-0.02"
}), }),
new AllLayerComponent(this.themeObject) new AllLayerComponent(this.themeObject)
] ]

View file

@ -118,7 +118,7 @@ export class FeatureInfoBox extends UIElement {
} }
} }
questionsHtml = mostImportantQuestion.Render(); questionsHtml = mostImportantQuestion?.Render() ?? "";
} else if (questions.length > 0) { } else if (questions.length > 0) {
// We select the most important question and render that one // We select the most important question and render that one
let mostImportantQuestion; let mostImportantQuestion;
@ -131,7 +131,7 @@ export class FeatureInfoBox extends UIElement {
} }
} }
questionsHtml = mostImportantQuestion.Render(); questionsHtml = mostImportantQuestion?.Render() ?? "";
} else if (skippedQuestions == 1) { } else if (skippedQuestions == 1) {
questionsHtml = this._oneSkipped.Render(); questionsHtml = this._oneSkipped.Render();
} else if (skippedQuestions > 0) { } else if (skippedQuestions > 0) {

View file

@ -6,15 +6,15 @@ import * as EmailValidator from "email-validator";
import {parsePhoneNumberFromString} from "libphonenumber-js"; import {parsePhoneNumberFromString} from "libphonenumber-js";
import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions"; import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions";
import {CustomLayoutFromJSON} from "../../Customizations/JSON/CustomLayoutFromJSON"; import {CustomLayoutFromJSON} from "../../Customizations/JSON/CustomLayoutFromJSON";
import {Tag} from "../../Logic/TagsFilter"; import {And, Tag} from "../../Logic/TagsFilter";
export class ValidatedTextField { export class ValidatedTextField {
public static inputValidation = { public static inputValidation = {
"$": (str) => true, "$": (str) => true,
"string": (str) => true, "string": (str) => true,
"date": (str) => true, // TODO validate and add a date picker "date": (str) => true, // TODO validate and add a date picker
"int": (str) => str.indexOf(".") < 0 && !isNaN(Number(str)), "int": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))},
"nat": (str) => str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0, "nat": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0},
"float": (str) => !isNaN(Number(str)), "float": (str) => !isNaN(Number(str)),
"pfloat": (str) => !isNaN(Number(str)) && Number(str) > 0, "pfloat": (str) => !isNaN(Number(str)) && Number(str) > 0,
"email": (str) => EmailValidator.validate(str), "email": (str) => EmailValidator.validate(str),
@ -37,6 +37,7 @@ export class ValidatedTextField {
placeholder: "Tags", placeholder: "Tags",
fromString: str => { fromString: str => {
const tags = CustomLayoutFromJSON.TagsFromJson(str); const tags = CustomLayoutFromJSON.TagsFromJson(str);
console.log("Parsed",str," --> ",tags)
if (tags === []) { if (tags === []) {
if (allowEmpty) { if (allowEmpty) {
return [] return []
@ -48,19 +49,14 @@ export class ValidatedTextField {
} }
, ,
toString: (tags: Tag[]) => { toString: (tags: Tag[]) => {
if (tags === undefined) { if (tags === undefined || tags === []) {
return undefined;
}
if (tags === []) {
if (allowEmpty) { if (allowEmpty) {
return ""; return "";
} else { } else {
return undefined; return undefined;
} }
} }
return tags.map(tag => return new And(tags).asHumanString(false);
tag.invertValue ? tag.key + "!=" + tag.value :
tag.key + "=" + tag.value).join("&")
}, },
value: value, value: value,
startValidated: true startValidated: true

View file

@ -11,6 +11,7 @@ import {Basemap} from "../Logic/Leaflet/Basemap";
import {FilteredLayer} from "../Logic/FilteredLayer"; import {FilteredLayer} from "../Logic/FilteredLayer";
import {Utils} from "../Utils"; import {Utils} from "../Utils";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import {UserDetails} from "../Logic/Osm/OsmConnection";
export class ShareScreen extends UIElement { export class ShareScreen extends UIElement {
@ -167,11 +168,20 @@ export class ShareScreen extends UIElement {
); );
this._editLayout = new FixedUiElement(""); this._editLayout = new FixedUiElement("");
if(State.state.layoutDefinition !== undefined){ if ((State.state.layoutDefinition !== undefined)) {
this._editLayout = this._editLayout =
new FixedUiElement(`<h3>Edit this theme</h3>`+ new VariableUiElement(
`<a target='_blank' href='https://pietervdvn.github.io/MapComplete/customGenerator.html#${State.state.layoutDefinition}'>Click here to edit</a>`) State.state.osmConnection.userDetails.map(
userDetails => {
if (userDetails.csCount <= State.userJourney.themeGeneratorUnlock) {
return "";
}
return `<h3>Edit this theme</h3>` +
`<a target='_blank' href='https://pietervdvn.github.io/MapComplete/customGenerator.html#${State.state.layoutDefinition}'>Click here to edit</a>`
}
));
} }
const status = new UIEventSource(" "); const status = new UIEventSource(" ");
@ -220,11 +230,11 @@ export class ShareScreen extends UIElement {
tr.intro, tr.intro,
this._link, this._link,
this._linkStatus, this._linkStatus,
this._editLayout,
tr.addToHomeScreen, tr.addToHomeScreen,
tr.embedIntro, tr.embedIntro,
this._options, this._options,
this._iframeCode, this._iframeCode,
this._editLayout
]).Render() ]).Render()
} }

View file

@ -0,0 +1,120 @@
{
"layers": [
{
"id": "Superficie",
"title": {
"render": "Calle sin nombre",
"key": "*",
"type": "text",
"question": "¿Cómo se llama esta calle?",
"mappings": [
{
"if": "name=*",
"then": "Nombre de la calle: {name}"
}
]
},
"description": "Completar datos de superficie",
"minzoom": "16",
"overpassTags": "highway=/residential|tertiary|pedestrian|unclassified|secondary|primary/",
"presets": [],
"tagRenderings": [
{
"key": "surface",
"addExtraTags": "",
"mappings": [
{
"if": "surface=asphalt",
"then": "asfalto"
},
{
"then": "cemento",
"if": "surface=concrete"
},
{
"then": "pavimentado",
"if": "surface=paved"
},
{
"then": "sin pavimentar",
"if": "surface=unpaved"
}
],
"question": "Qué superficie tiene?",
"render": "Surface: {surface}",
"type": "text"
}
],
"wayHandling": "0",
"icon": {
"key": "*",
"addExtraTags": "",
"mappings": [
{
"then": "https://raw.githubusercontent.com/yopaseopor/beta_preset_josm/master/ES/traffic_signs/ES/ES_P26.png",
"if": "surface=asphalt"
}
],
"question": "",
"render": "https://github.com/yopaseopor/beta_preset_josm/raw/master/ES/traffic_signs/ES/ES_P25.png",
"type": "text"
},
"color": {
"key": "*",
"addExtraTags": "",
"mappings": [
{
"then": "#000",
"if": "surface=asphalt"
},
{
"then": "#ccc",
"if": "surface=concrete"
},
{
"then": "#f3f",
"if": "surface=paving_stones"
},
{
"then": "#b5721b",
"if": "surface=sett"
}
],
"question": "",
"render": "#00f",
"type": "text"
},
"width": {
"key": "*",
"addExtraTags": "",
"mappings": [
{
"then": "12",
"if": "highway=tertiary"
},
{
"then": "15",
"if": "highway=secondary"
},
{
"then": "18",
"if": "highway=primary"
}
],
"question": "",
"render": "6",
"type": "nat"
}
}
],
"startLat": "41.39767",
"startLon": "2.17614",
"startZoom": "16",
"maintainer": "Pieter Vander Vennet",
"language": "es",
"icon": "https://github.com/yopaseopor/beta_preset_josm/raw/master/ES/traffic_signs/ES/ES_P28.png",
"name": "Superficie",
"title": "Test completar superficie",
"description": "Completar datos de superficie",
"widenFactor": "0.01"
}

View file

@ -9,6 +9,7 @@ import Combine from "./UI/Base/Combine";
import {Button} from "./UI/Base/Button"; import {Button} from "./UI/Base/Button";
import {FixedUiElement} from "./UI/Base/FixedUiElement"; import {FixedUiElement} from "./UI/Base/FixedUiElement";
import {State} from "./State"; import {State} from "./State";
import {TextField} from "./UI/Input/TextField";
const connection = new OsmConnection(true, new UIEventSource<string>(undefined), false); const connection = new OsmConnection(true, new UIEventSource<string>(undefined), false);
connection.AttemptLogin(); connection.AttemptLogin();
@ -28,22 +29,52 @@ const themeGenerator = new ThemeGenerator(connection, hash);
themeGenerator.AttachTo("layoutCreator"); themeGenerator.AttachTo("layoutCreator");
themeGenerator.url.syncWith(localStorage); themeGenerator.url.syncWith(localStorage);
function setDefault() {
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.data.widenFactor = 0.07;
themeGenerator.themeObject.ping();
}
var loadFrom = new TextField<string>(
{
placeholder: "Load from a previous JSON (paste the value here)",
toString: str => str,
fromString: str => str,
}
);
loadFrom.GetValue().setData("");
const loadFromTextField = new Button("Load", () => {
const parsed = JSON.parse(loadFrom.GetValue().data);
setDefault();
for (const parsedKey in parsed) {
themeGenerator.themeObject.data[parsedKey] = parsed[parsedKey];
}
themeGenerator.themeObject.ping();
loadFrom.GetValue().setData("");
});
new Combine([ new Combine([
new Preview(themeGenerator.url, themeGenerator.themeObject), new Preview(themeGenerator.url, themeGenerator.themeObject),
"<h2>Danger zone</h2>", "<h2>Danger zone</h2>",
new Button("Clear theme", () => { loadFrom,
themeGenerator.themeObject.data.title = undefined; loadFromTextField,
themeGenerator.themeObject.data.description = undefined; "<span class='alert'>Loading from the text field will erase the current theme</span>",
themeGenerator.themeObject.data.icon = undefined;
themeGenerator.themeObject.data.language = ["en"]; "<br/>",
themeGenerator.themeObject.data.name = undefined; new Button("Clear theme", setDefault),
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();
}),
"<br/>", "<br/>",
"Version: ", "Version: ",
State.vNumber State.vNumber

View file

@ -16,6 +16,7 @@ import {State} from "./State";
import {CustomLayout} from "./Logic/CustomLayers"; import {CustomLayout} from "./Logic/CustomLayers";
import {CustomLayoutFromJSON} from "./Customizations/JSON/CustomLayoutFromJSON"; import {CustomLayoutFromJSON} from "./Customizations/JSON/CustomLayoutFromJSON";
import {QueryParameters} from "./Logic/Web/QueryParameters"; import {QueryParameters} from "./Logic/Web/QueryParameters";
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
TagRendering.injectFunction(); TagRendering.injectFunction();
@ -71,7 +72,13 @@ 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") {
try { try {
const hashFromLocalStorage = LocalStorageSource.Get("last-loaded-user-layout");
if(hash.length < 10){
hash = hashFromLocalStorage.data;
}else{
console.log("Saving hash to local storage")
hashFromLocalStorage.setData(hash);
}
layoutToUse = CustomLayoutFromJSON.FromQueryParam(hash.substr(1)); layoutToUse = CustomLayoutFromJSON.FromQueryParam(hash.substr(1));
} catch (e) { } catch (e) {
new FixedUiElement("Error: could not parse the custom layout:<br/> "+e).AttachTo("centermessage"); new FixedUiElement("Error: could not parse the custom layout:<br/> "+e).AttachTo("centermessage");

View file

@ -11,13 +11,19 @@ import T from "./TestHelper";
new T([ new T([
["Parse and match advanced tagging", () => { ["Parse and match advanced tagging", () => {
const tags = CustomLayoutFromJSON.TagsFromJson("indoor=yes&access!=private"); const tags = CustomLayoutFromJSON.TagsFromJson("indoor=yes&access!=private");
console.log(tags);
const m0 = new And(tags).matches([{k: "indoor", v: "yes"}, {k: "access", v: "yes"}]); const m0 = new And(tags).matches([{k: "indoor", v: "yes"}, {k: "access", v: "yes"}]);
equal(m0, true); equal(m0, true);
const m1 = new And(tags).matches([{k: "indoor", v: "yes"}, {k: "access", v: "private"}]); const m1 = new And(tags).matches([{k: "indoor", v: "yes"}, {k: "access", v: "private"}]);
equal(m1, false); equal(m1, false);
} }
], ],
["Parse tagging with regex", () => {
const tags = CustomLayoutFromJSON.TagsFromJson("highway~=residential|tertiary");
equal(""+tags[0].value, ""+/residential|tertiary/);
console.log(tags[0].asOverpass());
}
],
["Tag replacement works in translation", () => { ["Tag replacement works in translation", () => {
const tr = new Translation({ const tr = new Translation({
"en": "Test {key} abc" "en": "Test {key} abc"