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 { FixLegacyTheme } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert" import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" import SharedTagRenderings from "../Customizations/SharedTagRenderings" import known_layers from "../assets/generated/known_layers.json" import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme" import licenses from "../assets/generated/license_info.json" import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages" import Svg from "../Svg" import { DoesImageExist, PrevalidateTheme, ValidateThemeAndLayers, } from "../Models/ThemeConfig/Conversion/Validation" export default class DetermineLayout { private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path)) /** * Gets the correct layout for this website */ public static async GetLayout(): Promise { 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")) { return await DetermineLayout.LoadRemoteTheme(layoutFromBase64) } if (layoutFromBase64 !== "false") { // We have to load something from the hash (or from disk) return DetermineLayout.LoadLayoutFromHash(loadCustomThemeParam) } let layoutId: string = undefined const path = window.location.pathname.split("/").slice(-1)[0] if (path !== "theme.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 return AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase()) } public static LoadLayoutFromHash(userLayoutParam: UIEventSource): LayoutConfig | null { let hash = location.hash.substr(1) let json: any 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) } 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 } } const layoutToUse = DetermineLayout.prepCustomTheme(json) userLayoutParam.setData(layoutToUse.id) return layoutToUse } 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"), json ) } this.ShowErrorOnCustomTheme("Could not parse the hash", new FixedUiElement(e), json) return null } } public static ShowErrorOnCustomTheme( intro: string = "Error: could not parse the custom layout:", error: BaseUIElement, json?: any ) { new Combine([ intro, error.SetClass("alert"), new SubtleButton(Svg.back_svg(), "Go back to the theme overview", { url: window.location.protocol + "//" + window.location.host + "/index.html", newTab: false, }), json !== undefined ? new SubtleButton(Svg.download_svg(), "Download the JSON file").onClick(() => { Utils.offerContentsAsDownloadableFile( JSON.stringify(json, null, " "), "theme_definition.json" ) }) : undefined, ]) .SetClass("flex flex-col clickable") .AttachTo("centermessage") } private static prepCustomTheme(json: any, sourceUrl?: string, forceId?: string): LayoutConfig { if (json.layers === undefined && json.tagRenderings !== undefined) { const iconTr = json.mapRendering.map((mr) => mr.icon).find((icon) => icon !== undefined) const icon = new TagRenderingConfig(iconTr).render.txt json = { id: json.id, description: json.description, descriptionTail: { en: "
Layer only mode.
The loaded custom theme actually isn't a custom theme, but only contains a layer.", }, icon, title: json.name, layers: [json], } } const knownLayersDict = new Map() for (const key in known_layers.layers) { const layer = known_layers.layers[key] knownLayersDict.set(layer.id, layer) } const converState = { tagRenderings: SharedTagRenderings.SharedTagRenderingJson, sharedLayers: knownLayersDict, publicLayers: new Set(), } json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme") const raw = json json = new FixImages(DetermineLayout._knownImages).convertStrict( json, "While fixing the images" ) json.enableNoteImports = json.enableNoteImports ?? false json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme") console.log("The layoutconfig is ", json) json.id = forceId ?? json.id { let { errors } = new PrevalidateTheme().convert(json, "validation") if (errors.length > 0) { throw "Detected errors: " + errors.join("\n") } } { let { errors } = new ValidateThemeAndLayers( new DoesImageExist(new Set(), (_) => true), "", false ).convert(json, "validation") if (errors.length > 0) { throw "Detected errors: " + errors.join("\n") } } return new LayoutConfig(json, false, { definitionRaw: JSON.stringify(raw, null, " "), definedAtUrl: sourceUrl, }) } 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 { let parsed = await Utils.downloadJson(link) try { let forcedId = parsed.id const url = new URL(link) if (!(url.hostname === "localhost" || url.hostname === "127.0.0.1")) { forcedId = link } console.log("Loaded remote link:", link) return DetermineLayout.prepCustomTheme(parsed, link, forcedId) } catch (e) { console.error(e) DetermineLayout.ShowErrorOnCustomTheme( `${link} is invalid:`, new FixedUiElement(e), parsed ) return null } } catch (e) { console.error(e) DetermineLayout.ShowErrorOnCustomTheme( `${link} is invalid - probably not found or invalid JSON:`, new FixedUiElement(e) ) return null } } }