mapcomplete/Logic/DetermineLayout.ts

209 lines
No EOL
8.9 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 {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert";
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
import SharedTagRenderings from "../Customizations/SharedTagRenderings";
import * as known_layers from "../assets/generated/known_layers.json"
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
import * as licenses from "../assets/generated/license_info.json"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
import {FixImages} from "../Models/ThemeConfig/Conversion/FixImages";
import Svg from "../Svg";
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<LayoutConfig> {
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<string>
): 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: "<div class='alert'>Layer only mode.</div> 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<string, LayerConfigJson>()
for (const key in known_layers.layers) {
const layer = known_layers.layers[key]
knownLayersDict.set(layer.id,<LayerConfigJson> layer)
}
const converState = {
tagRenderings: SharedTagRenderings.SharedTagRenderingJson,
sharedLayers: knownLayersDict,
publicLayers: new Set<string>()
}
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
return new LayoutConfig(json, false, {
definitionRaw: JSON.stringify(raw, null, " "),
definedAtUrl: sourceUrl
})
}
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 {
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(
`<a href="${link}">${link}</a> is invalid:`,
new FixedUiElement(e),
parsed
)
return null;
}
} catch (e) {
console.error(e)
DetermineLayout.ShowErrorOnCustomTheme(
`<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
new FixedUiElement(e)
)
return null;
}
}
}