forked from MapComplete/MapComplete
		
	
		
			
	
	
		
			168 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			168 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 
								 | 
							
								import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
							 | 
						||
| 
								 | 
							
								import {QueryParameters} from "./Web/QueryParameters";
							 | 
						||
| 
								 | 
							
								import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
							 | 
						||
| 
								 | 
							
								import {FixedUiElement} from "../UI/Base/FixedUiElement";
							 | 
						||
| 
								 | 
							
								import {Utils} from "../Utils";
							 | 
						||
| 
								 | 
							
								import Combine from "../UI/Base/Combine";
							 | 
						||
| 
								 | 
							
								import {SubtleButton} from "../UI/Base/SubtleButton";
							 | 
						||
| 
								 | 
							
								import BaseUIElement from "../UI/BaseUIElement";
							 | 
						||
| 
								 | 
							
								import {UIEventSource} from "./UIEventSource";
							 | 
						||
| 
								 | 
							
								import {LocalStorageSource} from "./Web/LocalStorageSource";
							 | 
						||
| 
								 | 
							
								import LZString from "lz-string";
							 | 
						||
| 
								 | 
							
								import * as personal from "../assets/themes/personal/personal.json";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export default class DetermineLayout {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Gets the correct layout for this website
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								    public static async GetLayout(): Promise<[LayoutConfig, string]> {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        const loadCustomThemeParam = QueryParameters.GetQueryParameter("userlayout", "false", "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme")
							 | 
						||
| 
								 | 
							
								        const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (layoutFromBase64.startsWith("http")) {
							 | 
						||
| 
								 | 
							
								            // The userLayout is actually an url
							 | 
						||
| 
								 | 
							
								            const layout = await DetermineLayout.LoadRemoteTheme(layoutFromBase64)
							 | 
						||
| 
								 | 
							
								            return [layout, undefined]
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (layoutFromBase64 !== "false") {
							 | 
						||
| 
								 | 
							
								            // We have to load something from the hash (or from disk)
							 | 
						||
| 
								 | 
							
								            let loaded = DetermineLayout.LoadLayoutFromHash(loadCustomThemeParam);
							 | 
						||
| 
								 | 
							
								            if (loaded === null) {
							 | 
						||
| 
								 | 
							
								                return [null, undefined]
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            return loaded
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        let layoutId: string = undefined
							 | 
						||
| 
								 | 
							
								        if (location.href.indexOf("buurtnatuur.be") >= 0) {
							 | 
						||
| 
								 | 
							
								            layoutId = "buurtnatuur"
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        const path = window.location.pathname.split("/").slice(-1)[0];
							 | 
						||
| 
								 | 
							
								        if (path !== "index.html" && path !== "") {
							 | 
						||
| 
								 | 
							
								            layoutId = path;
							 | 
						||
| 
								 | 
							
								            if (path.endsWith(".html")) {
							 | 
						||
| 
								 | 
							
								                layoutId = path.substr(0, path.length - 5);
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            console.log("Using layout", layoutId);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        layoutId = QueryParameters.GetQueryParameter("layout", layoutId, "The layout to load into MapComplete").data;
							 | 
						||
| 
								 | 
							
								        const layoutToUse: LayoutConfig = AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase());
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (layoutToUse?.id === personal.id) {
							 | 
						||
| 
								 | 
							
								            layoutToUse.layers = AllKnownLayouts.AllPublicLayers()
							 | 
						||
| 
								 | 
							
								            for (const layer of layoutToUse.layers) {
							 | 
						||
| 
								 | 
							
								                layer.minzoomVisible = Math.max(layer.minzoomVisible, layer.minzoom)
							 | 
						||
| 
								 | 
							
								                layer.minzoom = Math.max(16, layer.minzoom)
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        
							 | 
						||
| 
								 | 
							
								        return [layoutToUse, undefined]
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> {
							 | 
						||
| 
								 | 
							
								        console.log("Downloading map theme from ", link);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`)
							 | 
						||
| 
								 | 
							
								            .AttachTo("centermessage");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        try {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            const data = await Utils.downloadJson(link)
							 | 
						||
| 
								 | 
							
								            try {
							 | 
						||
| 
								 | 
							
								                let parsed = data;
							 | 
						||
| 
								 | 
							
								                if (typeof parsed == "string") {
							 | 
						||
| 
								 | 
							
								                    parsed = JSON.parse(parsed);
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								                // Overwrite the id to the url
							 | 
						||
| 
								 | 
							
								                parsed.id = link;
							 | 
						||
| 
								 | 
							
								                return new LayoutConfig(parsed, false).patchImages(link, data);
							 | 
						||
| 
								 | 
							
								            } catch (e) {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                DetermineLayout.ShowErrorOnCustomTheme(
							 | 
						||
| 
								 | 
							
								                    `<a href="${link}">${link}</a> is invalid:`,
							 | 
						||
| 
								 | 
							
								                    new FixedUiElement(e)
							 | 
						||
| 
								 | 
							
								                )
							 | 
						||
| 
								 | 
							
								                return null;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        } catch (e) {
							 | 
						||
| 
								 | 
							
								            DetermineLayout.ShowErrorOnCustomTheme(
							 | 
						||
| 
								 | 
							
								                `<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
							 | 
						||
| 
								 | 
							
								                new FixedUiElement(e)
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								            return null;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    public static LoadLayoutFromHash(
							 | 
						||
| 
								 | 
							
								        userLayoutParam: UIEventSource<string>
							 | 
						||
| 
								 | 
							
								    ): [LayoutConfig, string] | null {
							 | 
						||
| 
								 | 
							
								        let hash = location.hash.substr(1);
							 | 
						||
| 
								 | 
							
								        try {
							 | 
						||
| 
								 | 
							
								            // layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
							 | 
						||
| 
								 | 
							
								            const dedicatedHashFromLocalStorage = LocalStorageSource.Get(
							 | 
						||
| 
								 | 
							
								                "user-layout-" + userLayoutParam.data.replace(" ", "_")
							 | 
						||
| 
								 | 
							
								            );
							 | 
						||
| 
								 | 
							
								            if (dedicatedHashFromLocalStorage.data?.length < 10) {
							 | 
						||
| 
								 | 
							
								                dedicatedHashFromLocalStorage.setData(undefined);
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            const hashFromLocalStorage = LocalStorageSource.Get(
							 | 
						||
| 
								 | 
							
								                "last-loaded-user-layout"
							 | 
						||
| 
								 | 
							
								            );
							 | 
						||
| 
								 | 
							
								            if (hash.length < 10) {
							 | 
						||
| 
								 | 
							
								                hash =
							 | 
						||
| 
								 | 
							
								                    dedicatedHashFromLocalStorage.data ??
							 | 
						||
| 
								 | 
							
								                    hashFromLocalStorage.data;
							 | 
						||
| 
								 | 
							
								            } else {
							 | 
						||
| 
								 | 
							
								                console.log("Saving hash to local storage");
							 | 
						||
| 
								 | 
							
								                hashFromLocalStorage.setData(hash);
							 | 
						||
| 
								 | 
							
								                dedicatedHashFromLocalStorage.setData(hash);
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            let json: any;
							 | 
						||
| 
								 | 
							
								            try {
							 | 
						||
| 
								 | 
							
								                json = JSON.parse(atob(hash));
							 | 
						||
| 
								 | 
							
								            } catch (e) {
							 | 
						||
| 
								 | 
							
								                // We try to decode with lz-string
							 | 
						||
| 
								 | 
							
								                try {
							 | 
						||
| 
								 | 
							
								                    json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash)))
							 | 
						||
| 
								 | 
							
								                } catch (e) {
							 | 
						||
| 
								 | 
							
								                    DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON"))
							 | 
						||
| 
								 | 
							
								                    return null;
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            const layoutToUse = new LayoutConfig(json, false);
							 | 
						||
| 
								 | 
							
								            userLayoutParam.setData(layoutToUse.id);
							 | 
						||
| 
								 | 
							
								            return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))];
							 | 
						||
| 
								 | 
							
								        } catch (e) {
							 | 
						||
| 
								 | 
							
								            if (hash === undefined || hash.length < 10) {
							 | 
						||
| 
								 | 
							
								                DetermineLayout.ShowErrorOnCustomTheme("Could not load a theme from the hash", new FixedUiElement("Hash does not contain data"))
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            return null;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    public static ShowErrorOnCustomTheme(
							 | 
						||
| 
								 | 
							
								        intro: string = "Error: could not parse the custom layout:",
							 | 
						||
| 
								 | 
							
								        error: BaseUIElement) {
							 | 
						||
| 
								 | 
							
								        new Combine([
							 | 
						||
| 
								 | 
							
								            intro,
							 | 
						||
| 
								 | 
							
								            error.SetClass("alert"),
							 | 
						||
| 
								 | 
							
								            new SubtleButton("./assets/svg/mapcomplete_logo.svg",
							 | 
						||
| 
								 | 
							
								                "Go back to the theme overview",
							 | 
						||
| 
								 | 
							
								                {url: window.location.protocol + "//" + window.location.hostname + "/index.html", newTab: false})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        ])
							 | 
						||
| 
								 | 
							
								            .SetClass("flex flex-col clickable")
							 | 
						||
| 
								 | 
							
								            .AttachTo("centermessage");
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								}
							 |