forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			305 lines
		
	
	
		
			No EOL
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			305 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";
 | 
						|
import {UIElement} from "../../UI/UIElement";
 | 
						|
import Translations from "../../UI/i18n/Translations";
 | 
						|
 | 
						|
 | 
						|
export interface TagRenderingConfigJson {   
 | 
						|
    // If this key is present, then...
 | 
						|
    key?: string,
 | 
						|
    // Use this string to render
 | 
						|
    render?: string | any,
 | 
						|
    // 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 | any,
 | 
						|
    // 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 | any
 | 
						|
        }[]
 | 
						|
}
 | 
						|
 | 
						|
export interface LayerConfigJson {
 | 
						|
    name: 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: {
 | 
						|
        tags: string,
 | 
						|
        title: string | any,
 | 
						|
        description?: string | any,
 | 
						|
        icon?: string
 | 
						|
    }[],
 | 
						|
    tagRenderings: TagRenderingConfigJson []
 | 
						|
}
 | 
						|
 | 
						|
export interface LayoutConfigJson {
 | 
						|
    widenFactor?: number;
 | 
						|
    name: string;
 | 
						|
    title: string | any;
 | 
						|
    description: string | any;
 | 
						|
    maintainer: string;
 | 
						|
    language: string | string[];
 | 
						|
    layers: LayerConfigJson[],
 | 
						|
    startZoom: string | number;
 | 
						|
    startLat: string | number;
 | 
						|
    startLon: string | 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).GetContent({id:"node/-1"});
 | 
						|
 | 
						|
        // @ts-ignore
 | 
						|
        const id = json.name?.replace(/[^a-zA-Z0-9_-]/g,'') ?? json.id;
 | 
						|
        return new LayerDefinition(
 | 
						|
            id,
 | 
						|
            {
 | 
						|
                description: t(json.description),
 | 
						|
                name: Translations.WT(t(json.name)),
 | 
						|
                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 : string[] ;
 | 
						|
        if(typeof (json.language) === "string"){
 | 
						|
            languages = [json.language];
 | 
						|
        }else{
 | 
						|
            languages = json.language
 | 
						|
        }
 | 
						|
        const layout = new Layout(json.name,
 | 
						|
            languages,
 | 
						|
            t(json.title),
 | 
						|
            json.layers.map(CustomLayoutFromJSON.LayerFromJson),
 | 
						|
            parseInt(""+json.startZoom),
 | 
						|
            parseFloat(""+json.startLat),
 | 
						|
            parseFloat(""+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;
 | 
						|
    }
 | 
						|
 | 
						|
} |