mapcomplete/Customizations/JSON/CustomLayoutFromJSON.ts
2020-08-23 02:26:17 +02:00

299 lines
No EOL
10 KiB
TypeScript

import {TagRenderingOptions} from "../TagRenderingOptions";
import {LayerDefinition, Preset} from "../LayerDefinition";
import {Layout} from "../Layout";
import Translation from "../../UI/i18n/Translation";
import Combine from "../../UI/Base/Combine";
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";
import {Map} from "../Layers/Map";
export interface TagRenderingConfigJson {
// If this key is present, then...
key?: string,
// Use this string to render
render: string,
// 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?: string,
// If a value is added with the textfield, this extra tag is addded. Optional field
addExtraTags?: string | { k: string, v: string }[];
// Extra tags: rendering is only shown/asked if these tags are present
condition?: string;
// 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: string,
then: string
}[]
}
export interface LayerConfigJson {
id: string;
title: string | any | TagRenderingConfigJson;
description: string | any;
minzoom: number | string,
icon?: TagRenderingConfigJson;
color?: TagRenderingConfigJson;
width?: TagRenderingConfigJson;
overpassTags: string | { k: string, v: string }[];
wayHandling: number,
presets: Preset[]
,
tagRenderings: TagRenderingConfigJson []
}
export interface LayoutConfigJson {
widenFactor: number;
name: string;
title: string;
description: string;
maintainer: string;
language: string[];
layers: LayerConfigJson[],
startZoom: number;
startLat: number;
startLon: number;
/**
* Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64,'
*/
icon: string;
}
export class CustomLayoutFromJSON {
public static FromQueryParam(layoutFromBase64: string): Layout {
return CustomLayoutFromJSON.LayoutFromJSON(JSON.parse(atob(layoutFromBase64)));
}
public static TagRenderingFromJson(json: TagRenderingConfigJson): TagDependantUIElementConstructor {
if(json === undefined){
return undefined;
}
if (typeof (json) === "string") {
return new FixedText(json);
}
let freeform = 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: 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;
if (json.mappings !== undefined) {
mappings = [];
for (const mapping of json.mappings) {
mappings.push({
k: new And(CustomLayoutFromJSON.TagsFromJson(mapping.if)),
txt: CustomLayoutFromJSON.MaybeTranslation(mapping.then)
})
}
}
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 ?? 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: LayerConfigJson): ((tags: any) => {
color: string,
weight?: number,
icon: {
iconUrl: string,
iconSize: number[],
},
}) {
const iconRendering: TagDependantUIElementConstructor = CustomLayoutFromJSON.TagRenderingFromJson(layout.icon);
const colourRendering = CustomLayoutFromJSON.TagRenderingFromJson(layout.color);
let thickness = CustomLayoutFromJSON.TagRenderingFromJson(layout.width);
return (tags) => {
const iconUrl = iconRendering.GetContent(tags);
const stroke = colourRendering.GetContent(tags) ?? "#00f";
let weight = parseInt(thickness?.GetContent(tags)) ?? 10;
if(isNaN(weight)){
weight = 10;
}
return {
color: stroke,
weight: weight,
icon: {
iconUrl: iconUrl,
iconSize: [40, 40],
},
}
};
}
private static TagFromJson(json: string | { k: string, v: string }): Tag {
if (json === undefined) {
return undefined;
}
if (typeof (json) !== "string") {
return new Tag(json.k.trim(), json.v.trim())
}
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("=");
}
if (kv.length !== 2) {
return undefined;
}
if (kv[0].trim() === "") {
return undefined;
}
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[] {
if (json === undefined) {
return undefined;
}
if (json === "") {
return [];
}
let tags = [];
if (typeof (json) === "string") {
tags = json.split("&").map(CustomLayoutFromJSON.TagFromJson);
} else {
tags = json.map(x => {CustomLayoutFromJSON.TagFromJson(x)});
}
for (const tag of tags) {
if (tag === undefined) {
return undefined;
}
}
return tags;
}
private static LayerFromJson(json: LayerConfigJson): 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 icon = CustomLayoutFromJSON.TagRenderingFromJson(json.icon).construct({
tags: new UIEventSource<any>({})
}).InnerRender();
return new LayerDefinition(
json.id,
{
description: t(json.description),
name: t(json.title.render),
icon: icon,
minzoom: parseInt(""+json.minzoom),
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(tags),
wayHandling: parseInt(""+json.wayHandling) ?? LayerDefinition.WAYHANDLING_CENTER_AND_WAY,
maxAllowedOverlapPercentage: 0,
style: CustomLayoutFromJSON.StyleFromJson(json)
}
)
}
private static MaybeTranslation(json: any): Translation | string {
if (json === undefined) {
return undefined;
}
if (typeof (json) === "string") {
return json;
}
return new Translation(json);
}
public static LayoutFromJSON(json: LayoutConfigJson) {
const t = CustomLayoutFromJSON.MaybeTranslation;
let languages = json.language;
if(typeof (json.language) === "string"){
languages = [json.language];
}
const layout = new Layout(json.name,
languages,
t(json.title),
json.layers.map(CustomLayoutFromJSON.LayerFromJson),
json.startZoom,
json.startLat,
json.startLon,
new Combine(['<h3>', t(json.title), '</h3><br/>', t(json.description)])
);
layout.icon = json.icon;
layout.maintainer = json.maintainer;
layout.widenFactor = parseFloat(""+json.widenFactor) ?? 0.03;
if(isNaN(layout.widenFactor)){
layout.widenFactor = 0.03;
}
if (layout.widenFactor > 0.1) {
layout.widenFactor = 0.1;
}
return layout;
}
}