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 {
widenFactor: number;
name: string;
title: string;
description: string;
@ -161,7 +162,7 @@ export class CustomLayoutFromJSON {
return (tags) => {
const iconUrl = iconRendering.GetContent(tags);
const stroke = colourRendering.GetContent(tags);
const stroke = colourRendering.GetContent(tags) ?? "#00f";
let weight = parseInt(thickness?.GetContent(tags)) ?? 10;
if(isNaN(weight)){
weight = 10;
@ -187,9 +188,13 @@ export class CustomLayoutFromJSON {
let kv: string[] = undefined;
let invert = false;
let regex = false;
if (json.indexOf("!=") >= 0) {
kv = json.split("!=");
invert = true;
} else if (json.indexOf("~=") >= 0) {
kv = json.split("~=");
regex = true;
} else {
kv = json.split("=");
}
@ -200,7 +205,12 @@ export class CustomLayoutFromJSON {
if (kv[0].trim() === "") {
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[] {
@ -229,12 +239,8 @@ export class CustomLayoutFromJSON {
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)
tags: new UIEventSource<any>({})
}).InnerRender();
@ -287,6 +293,10 @@ export class CustomLayoutFromJSON {
);
layout.icon = json.icon;
layout.maintainer = json.maintainer;
layout.widenFactor = parseFloat(json.widenFactor) ?? 0.03;
if (layout.widenFactor > 0.1) {
layout.widenFactor = 0.1;
}
return layout;
}

View file

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

View file

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

View file

@ -62,7 +62,7 @@ export class Regex extends TagsFilter {
export class Tag extends TagsFilter {
public key: string
public value: string
public value: string | RegExp
public invertValue: boolean
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>`
}
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
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
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
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 {
@ -75,6 +75,7 @@ class MappingGenerator extends UIElement {
new FixedUiElement("Tag mapping removed")
]
self.Update();
pingThemeObject();
break;
}
}
@ -111,9 +112,10 @@ class TagRenderingGenerator
this.elements = [
new FixedUiElement(`<h3>${options.header}</h3>`),
new FixedUiElement(options.description),
createFieldUI("Key", "key", tagRendering, {
options.hideQuestion ? new FixedUiElement("") : 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."
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, {
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>"
}),
createFieldUI(
options.hideQuestion ? new FixedUiElement("") : createFieldUI(
"Only show if", "condition", tagRendering,
{
deflt: "",
@ -223,6 +225,7 @@ class PresetGenerator extends UIElement {
new FixedUiElement("Preset removed")
]
self.Update();
pingThemeObject();
break;
}
}
@ -255,6 +258,51 @@ class LayerGenerator extends UIElement {
}
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;
this.uielements = [
@ -269,7 +317,16 @@ class LayerGenerator extends UIElement {
}),
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> 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, {
@ -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"
}),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.title ?? {
key: "",
addExtraTags: "",
mappings: [],
question: "",
render: "Title",
type: "string"
}, {
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.title, {
header: "Title element",
description: "This element is shown in the title of the popup in a header-tag",
removable: false,
@ -292,14 +342,7 @@ class LayerGenerator extends UIElement {
}),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.icon ?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "./assets/bug.svg",
type: "text"
}, {
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.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",
removable: false,
@ -307,28 +350,14 @@ class LayerGenerator extends UIElement {
}),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.color ?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "#0000ff",
type: "text"
}, {
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.color, {
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
}),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.width ?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "10",
type: "nat"
}, {
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.width , {
header: "Line thickness",
description: "This decides the line thickness of ways (in pixels)",
removable: false,
@ -402,9 +431,8 @@ 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"});
?.GetContent({id: "node/-1"});
const header = this.config.map(() => {
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);
const self = this;
pingThemeObject = () => {self.themeObject.ping()};
createFieldUI = (label, key, root, options) => {
options = options ?? {description: "?"};
options.type = options.type ?? "string";
const value = new UIEventSource<string>(TagsToString(root[key]) ?? options?.deflt);
let textField: UIElement;
if (options.type === "typeSelector") {
const options: { value: string, shown: string | UIElement }[] = [];
@ -532,6 +562,24 @@ export class ThemeGenerator extends UIElement {
options,
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") {
textField = ValidatedTextField.TagTextField(value.map(CustomLayoutFromJSON.TagsFromJson, [], tags => {
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.addCallback(() => {
value.setData(root[key]);
})
return new Combine([
label,
textField,
@ -581,6 +634,11 @@ export class ThemeGenerator extends UIElement {
deflt: "Title",
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, {
description: "Shown in the welcome message",
deflt: "Description"
@ -604,12 +662,13 @@ export class ThemeGenerator extends UIElement {
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"
createFieldUI("Query widening factor", "widenFactor", jsonObjectRoot, {
type: "pfloat",
deflt: "0.05",
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)
]

View file

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

View file

@ -6,15 +6,15 @@ 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";
import {And, 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,
"int": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))},
"nat": (str) => {str = ""+str; return str !== undefined && 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),
@ -37,6 +37,7 @@ export class ValidatedTextField {
placeholder: "Tags",
fromString: str => {
const tags = CustomLayoutFromJSON.TagsFromJson(str);
console.log("Parsed",str," --> ",tags)
if (tags === []) {
if (allowEmpty) {
return []
@ -48,19 +49,14 @@ export class ValidatedTextField {
}
,
toString: (tags: Tag[]) => {
if (tags === undefined) {
return undefined;
}
if (tags === []) {
if (tags === undefined || tags === []) {
if (allowEmpty) {
return "";
} else {
return undefined;
}
}
return tags.map(tag =>
tag.invertValue ? tag.key + "!=" + tag.value :
tag.key + "=" + tag.value).join("&")
return new And(tags).asHumanString(false);
},
value: value,
startValidated: true

View file

@ -11,6 +11,7 @@ import {Basemap} from "../Logic/Leaflet/Basemap";
import {FilteredLayer} from "../Logic/FilteredLayer";
import {Utils} from "../Utils";
import {UIEventSource} from "../Logic/UIEventSource";
import {UserDetails} from "../Logic/Osm/OsmConnection";
export class ShareScreen extends UIElement {
@ -167,10 +168,19 @@ export class ShareScreen extends UIElement {
);
this._editLayout = new FixedUiElement("");
if(State.state.layoutDefinition !== undefined){
if ((State.state.layoutDefinition !== undefined)) {
this._editLayout =
new FixedUiElement(`<h3>Edit this theme</h3>`+
`<a target='_blank' href='https://pietervdvn.github.io/MapComplete/customGenerator.html#${State.state.layoutDefinition}'>Click here to edit</a>`)
new VariableUiElement(
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>`
}
));
}
@ -220,11 +230,11 @@ export class ShareScreen extends UIElement {
tr.intro,
this._link,
this._linkStatus,
this._editLayout,
tr.addToHomeScreen,
tr.embedIntro,
this._options,
this._iframeCode,
this._editLayout
]).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 {FixedUiElement} from "./UI/Base/FixedUiElement";
import {State} from "./State";
import {TextField} from "./UI/Input/TextField";
const connection = new OsmConnection(true, new UIEventSource<string>(undefined), false);
connection.AttemptLogin();
@ -28,22 +29,52 @@ const themeGenerator = new ThemeGenerator(connection, hash);
themeGenerator.AttachTo("layoutCreator");
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 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();
}),
loadFrom,
loadFromTextField,
"<span class='alert'>Loading from the text field will erase the current theme</span>",
"<br/>",
new Button("Clear theme", setDefault),
"<br/>",
"Version: ",
State.vNumber

View file

@ -16,6 +16,7 @@ import {State} from "./State";
import {CustomLayout} from "./Logic/CustomLayers";
import {CustomLayoutFromJSON} from "./Customizations/JSON/CustomLayoutFromJSON";
import {QueryParameters} from "./Logic/Web/QueryParameters";
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
TagRendering.injectFunction();
@ -71,7 +72,13 @@ let layoutToUse: Layout = AllKnownLayouts.allSets[defaultLayout] ?? AllKnownLayo
const layoutFromBase64 = QueryParameters.GetQueryParameter("userlayout", "false").data;
if (layoutFromBase64 !== "false") {
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));
} catch (e) {
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([
["Parse and match advanced tagging", () => {
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"}]);
equal(m0, true);
const m1 = new And(tags).matches([{k: "indoor", v: "yes"}, {k: "access", v: "private"}]);
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", () => {
const tr = new Translation({
"en": "Test {key} abc"