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"; import LegacyJsonConvert from "../Models/ThemeConfig/LegacyJsonConvert"; 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 { console.log("Downloading map theme from ", link); new FixedUiElement(`Downloading the theme from the link...`) .AttachTo("centermessage"); try { const parsed = await Utils.downloadJson(link) console.log("Got ", parsed) LegacyJsonConvert.fixThemeConfig(parsed) try { parsed.id = link; return new LayoutConfig(parsed, false).patchImages(link, JSON.stringify(parsed)); } catch (e) { console.error(e) DetermineLayout.ShowErrorOnCustomTheme( `${link} is invalid:`, new FixedUiElement(e) ) return null; } } catch (e) { console.error(e) DetermineLayout.ShowErrorOnCustomTheme( `${link} is invalid - probably not found or invalid JSON:`, new FixedUiElement(e) ) return null; } } public static LoadLayoutFromHash( userLayoutParam: UIEventSource ): [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) { console.error(e) DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON")) return null; } } LegacyJsonConvert.fixThemeConfig(json) const layoutToUse = new LayoutConfig(json, false); userLayoutParam.setData(layoutToUse.id); return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))]; } catch (e) { console.error(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"); } }