mapcomplete/Logic/DetermineLayout.ts

245 lines
9.9 KiB
TypeScript
Raw Normal View History

2022-09-08 21:40:48 +02:00
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
2022-10-27 01:50:01 +02:00
import { QueryParameters } from "./Web/QueryParameters"
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
import { FixedUiElement } from "../UI/Base/FixedUiElement"
import { Utils } from "../Utils"
2022-09-08 21:40:48 +02:00
import Combine from "../UI/Base/Combine"
2022-10-27 01:50:01 +02:00
import { SubtleButton } from "../UI/Base/SubtleButton"
2022-09-08 21:40:48 +02:00
import BaseUIElement from "../UI/BaseUIElement"
2022-10-27 01:50:01 +02:00
import { UIEventSource } from "./UIEventSource"
import { LocalStorageSource } from "./Web/LocalStorageSource"
2022-09-08 21:40:48 +02:00
import LZString from "lz-string"
2022-10-27 01:50:01 +02:00
import { FixLegacyTheme } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
2022-09-08 21:40:48 +02:00
import SharedTagRenderings from "../Customizations/SharedTagRenderings"
import known_layers from "../assets/generated/known_layers.json"
2022-10-27 01:50:01 +02:00
import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme"
import licenses from "../assets/generated/license_info.json"
2022-09-08 21:40:48 +02:00
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
2022-10-27 01:50:01 +02:00
import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages"
2022-09-08 21:40:48 +02:00
import Svg from "../Svg"
2022-10-27 01:50:01 +02:00
import {
DoesImageExist,
PrevalidateTheme,
ValidateThemeAndLayers,
} from "../Models/ThemeConfig/Conversion/Validation"
2022-02-08 00:56:47 +01:00
export default class DetermineLayout {
2022-09-08 21:40:48 +02:00
private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path))
/**
* Gets the correct layout for this website
*/
2021-12-21 19:56:04 +01:00
public static async GetLayout(): Promise<LayoutConfig> {
2022-09-08 21:40:48 +02:00
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")) {
2021-12-21 19:56:04 +01:00
return await DetermineLayout.LoadRemoteTheme(layoutFromBase64)
}
if (layoutFromBase64 !== "false") {
// We have to load something from the hash (or from disk)
2021-12-21 19:56:04 +01:00
return DetermineLayout.LoadLayoutFromHash(loadCustomThemeParam)
}
let layoutId: string = undefined
2022-09-08 21:40:48 +02:00
const path = window.location.pathname.split("/").slice(-1)[0]
2021-12-21 18:35:31 +01:00
if (path !== "theme.html" && path !== "") {
2022-09-08 21:40:48 +02:00
layoutId = path
if (path.endsWith(".html")) {
2022-09-08 21:40:48 +02:00
layoutId = path.substr(0, path.length - 5)
}
2022-09-08 21:40:48 +02:00
console.log("Using layout", layoutId)
}
2022-09-08 21:40:48 +02:00
layoutId = QueryParameters.GetQueryParameter(
"layout",
layoutId,
"The layout to load into MapComplete"
).data
return AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase())
}
2022-09-08 21:40:48 +02:00
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(" ", "_")
2022-09-08 21:40:48 +02:00
)
if (dedicatedHashFromLocalStorage.data?.length < 10) {
2022-09-08 21:40:48 +02:00
dedicatedHashFromLocalStorage.setData(undefined)
}
2022-09-08 21:40:48 +02:00
const hashFromLocalStorage = LocalStorageSource.Get("last-loaded-user-layout")
if (hash.length < 10) {
2022-09-08 21:40:48 +02:00
hash = dedicatedHashFromLocalStorage.data ?? hashFromLocalStorage.data
} else {
2022-09-08 21:40:48 +02:00
console.log("Saving hash to local storage")
hashFromLocalStorage.setData(hash)
dedicatedHashFromLocalStorage.setData(hash)
}
try {
2022-09-08 21:40:48 +02:00
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)
2022-09-08 21:40:48 +02:00
DetermineLayout.ShowErrorOnCustomTheme(
"Could not decode the hash",
new FixedUiElement("Not a valid (LZ-compressed) JSON")
)
return null
}
}
const layoutToUse = DetermineLayout.prepCustomTheme(json)
2022-09-08 21:40:48 +02:00
userLayoutParam.setData(layoutToUse.id)
return layoutToUse
} catch (e) {
console.error(e)
if (hash === undefined || hash.length < 10) {
2022-09-08 21:40:48 +02:00
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)
2022-09-08 21:40:48 +02:00
return null
}
}
public static ShowErrorOnCustomTheme(
intro: string = "Error: could not parse the custom layout:",
error: BaseUIElement,
2022-09-08 21:40:48 +02:00
json?: any
) {
new Combine([
intro,
error.SetClass("alert"),
2022-09-08 21:40:48 +02:00
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(() => {
2022-10-27 01:50:01 +02:00
Utils.offerContentsAsDownloadableFile(
JSON.stringify(json, null, " "),
"theme_definition.json"
)
})
2022-09-08 21:40:48 +02:00
: undefined,
])
.SetClass("flex flex-col clickable")
2022-09-08 21:40:48 +02:00
.AttachTo("centermessage")
}
2022-06-21 16:47:54 +02:00
private static prepCustomTheme(json: any, sourceUrl?: string, forceId?: string): LayoutConfig {
2022-09-08 21:40:48 +02:00
if (json.layers === undefined && json.tagRenderings !== undefined) {
const iconTr = json.mapRendering.map((mr) => mr.icon).find((icon) => icon !== undefined)
2022-02-08 00:56:47 +01:00
const icon = new TagRenderingConfig(iconTr).render.txt
json = {
id: json.id,
description: json.description,
descriptionTail: {
2022-09-08 21:40:48 +02:00
en: "<div class='alert'>Layer only mode.</div> The loaded custom theme actually isn't a custom theme, but only contains a layer.",
2022-02-08 00:56:47 +01:00
},
icon,
title: json.name,
layers: [json],
}
}
2022-09-08 21:40:48 +02:00
2022-01-26 21:40:38 +01:00
const knownLayersDict = new Map<string, LayerConfigJson>()
for (const key in known_layers.layers) {
const layer = known_layers.layers[key]
2022-09-08 21:40:48 +02:00
knownLayersDict.set(layer.id, <LayerConfigJson>layer)
2022-01-26 21:40:38 +01:00
}
const converState = {
tagRenderings: SharedTagRenderings.SharedTagRenderingJson,
sharedLayers: knownLayersDict,
2022-09-08 21:40:48 +02:00
publicLayers: new Set<string>(),
2022-01-26 21:40:38 +01:00
}
2022-02-04 13:17:50 +01:00
json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme")
2022-09-08 21:40:48 +02:00
const raw = json
2022-09-08 21:40:48 +02:00
json = new FixImages(DetermineLayout._knownImages).convertStrict(
json,
"While fixing the images"
)
json.enableNoteImports = json.enableNoteImports ?? false
2022-02-04 13:17:50 +01:00
json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme")
2022-01-26 21:40:38 +01:00
console.log("The layoutconfig is ", json)
2022-09-08 21:40:48 +02:00
2022-06-21 16:47:54 +02:00
json.id = forceId ?? json.id
2022-09-08 21:40:48 +02:00
{
2022-10-27 01:50:01 +02:00
let { errors } = new PrevalidateTheme().convert(json, "validation")
if (errors.length > 0) {
throw "Detected errors: " + errors.join("\n")
}
}
{
2022-10-27 01:50:01 +02:00
let { errors } = new ValidateThemeAndLayers(
new DoesImageExist(new Set<string>(), (_) => 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, " "),
2022-09-08 21:40:48 +02:00
definedAtUrl: sourceUrl,
})
2022-01-26 21:40:38 +01:00
}
2021-11-07 16:34:51 +01:00
private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> {
2022-09-08 21:40:48 +02:00
console.log("Downloading map theme from ", link)
2021-11-07 16:34:51 +01:00
2022-09-08 21:40:48 +02:00
new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`).AttachTo(
"centermessage"
)
2021-11-07 16:34:51 +01:00
try {
2021-12-21 18:35:31 +01:00
let parsed = await Utils.downloadJson(link)
try {
2022-06-21 16:47:54 +02:00
let forcedId = parsed.id
const url = new URL(link)
2022-09-08 21:40:48 +02:00
if (!(url.hostname === "localhost" || url.hostname === "127.0.0.1")) {
forcedId = link
2022-06-21 16:47:54 +02:00
}
2022-01-27 20:37:22 +01:00
console.log("Loaded remote link:", link)
2022-09-08 21:40:48 +02:00
return DetermineLayout.prepCustomTheme(parsed, link, forcedId)
2021-11-07 16:34:51 +01:00
} catch (e) {
console.error(e)
DetermineLayout.ShowErrorOnCustomTheme(
`<a href="${link}">${link}</a> is invalid:`,
new FixedUiElement(e),
parsed
2021-11-07 16:34:51 +01:00
)
2022-09-08 21:40:48 +02:00
return null
2021-11-07 16:34:51 +01:00
}
} catch (e) {
console.error(e)
DetermineLayout.ShowErrorOnCustomTheme(
`<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
new FixedUiElement(e)
)
2022-09-08 21:40:48 +02:00
return null
2021-11-07 16:34:51 +01:00
}
}
2022-09-08 21:40:48 +02:00
}