forked from MapComplete/MapComplete
218 lines
8.5 KiB
TypeScript
218 lines
8.5 KiB
TypeScript
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
|
|
import { QueryParameters } from "./Web/QueryParameters"
|
|
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
|
|
import { FixedUiElement } from "../UI/Base/FixedUiElement"
|
|
import { Utils } from "../Utils"
|
|
import LZString from "lz-string"
|
|
import { FixLegacyTheme } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"
|
|
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
|
|
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 questions from "../assets/generated/layers/questions.json"
|
|
import { DoesImageExist, PrevalidateTheme } from "../Models/ThemeConfig/Conversion/Validation"
|
|
import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion"
|
|
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
|
|
import Hash from "./Web/Hash"
|
|
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
|
import { ThemeConfigJson } from "../Models/ThemeConfig/Json/ThemeConfigJson"
|
|
import { ValidateThemeAndLayers } from "../Models/ThemeConfig/Conversion/ValidateThemeAndLayers"
|
|
|
|
export default class DetermineTheme {
|
|
private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path))
|
|
private static readonly loadCustomThemeParam = QueryParameters.GetQueryParameter(
|
|
"userlayout",
|
|
"false",
|
|
"If the parameter is an URL, it should point to a .json of a theme which will be loaded and used"
|
|
)
|
|
|
|
public static getCustomDefinition(): string {
|
|
const layoutFromBase64 = decodeURIComponent(DetermineTheme.loadCustomThemeParam.data)
|
|
|
|
if (layoutFromBase64.startsWith("http")) {
|
|
return layoutFromBase64
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
private static async expandRemoteLayers(
|
|
layoutConfig: ThemeConfigJson
|
|
): Promise<ThemeConfigJson> {
|
|
if (!layoutConfig.layers) {
|
|
// This is probably a layer in 'layer-only-mode'
|
|
return layoutConfig
|
|
}
|
|
for (let i = 0; i < layoutConfig.layers.length; i++) {
|
|
const l = layoutConfig.layers[i]
|
|
if (typeof l !== "string") {
|
|
continue
|
|
}
|
|
try {
|
|
new URL(l)
|
|
console.log("Downloading remote layer " + l)
|
|
layoutConfig.layers[i] = <LayerConfigJson>await Utils.downloadJson(l)
|
|
} catch (_) {
|
|
continue
|
|
}
|
|
}
|
|
return layoutConfig
|
|
}
|
|
|
|
/**
|
|
* Gets the correct layout for this website
|
|
*/
|
|
public static async getTheme(): Promise<ThemeConfig | undefined> {
|
|
const layoutFromBase64 = decodeURIComponent(DetermineTheme.loadCustomThemeParam.data)
|
|
|
|
if (layoutFromBase64.startsWith("http")) {
|
|
return await DetermineTheme.LoadRemoteTheme(layoutFromBase64)
|
|
}
|
|
|
|
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
|
|
const id = layoutId?.toLowerCase()
|
|
const layouts = AllKnownLayouts.allKnownLayouts
|
|
if (layouts.size() == 0) {
|
|
throw "Build failed or running, no layouts are known at all"
|
|
}
|
|
if (layouts.getConfig(id) === undefined) {
|
|
const alternatives = Utils.sortedByLevenshteinDistance(
|
|
id,
|
|
Array.from(layouts.keys()),
|
|
(i) => i
|
|
).slice(0, 3)
|
|
const msg = `No builtin map theme with name ${layoutId} exists. Perhaps you meant one of ${alternatives.join(
|
|
", "
|
|
)}`
|
|
throw msg
|
|
}
|
|
return layouts.get(id)
|
|
}
|
|
|
|
private static getSharedTagRenderings(): Map<string, QuestionableTagRenderingConfigJson> {
|
|
const dict = new Map<string, QuestionableTagRenderingConfigJson>()
|
|
|
|
for (const tagRendering of questions.tagRenderings) {
|
|
dict.set(tagRendering.id, <QuestionableTagRenderingConfigJson>tagRendering)
|
|
}
|
|
|
|
return dict
|
|
}
|
|
|
|
private static getSharedTagRenderingOrder(): string[] {
|
|
return questions.tagRenderings.map((tr) => tr.id)
|
|
}
|
|
|
|
private static prepCustomTheme(json: any, sourceUrl?: string, forceId?: string): ThemeConfig {
|
|
if (json.layers === undefined && json.tagRenderings !== undefined) {
|
|
// We got fed a layer instead of a theme
|
|
const layerConfig = <LayerConfigJson>json
|
|
let icon = Utils.NoNull(
|
|
layerConfig.pointRendering
|
|
.flatMap((pr) => pr.marker)
|
|
.map((iconSpec) => {
|
|
if (!iconSpec) {
|
|
return undefined
|
|
}
|
|
const icon = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.icon)
|
|
.render.txt
|
|
if (
|
|
iconSpec.color === undefined ||
|
|
icon.startsWith("http:") ||
|
|
icon.startsWith("https:")
|
|
) {
|
|
return icon
|
|
}
|
|
const color = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.color)
|
|
.render.txt
|
|
return icon + ":" + color
|
|
})
|
|
).join(";")
|
|
|
|
if (!icon) {
|
|
icon = "./assets/svg/bug.svg"
|
|
}
|
|
|
|
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 convertState: DesugaringContext = {
|
|
tagRenderings: DetermineTheme.getSharedTagRenderings(),
|
|
tagRenderingOrder: DetermineTheme.getSharedTagRenderingOrder(),
|
|
sharedLayers: knownLayersDict,
|
|
publicLayers: new Set<string>(),
|
|
}
|
|
json = new FixLegacyTheme().convertStrict(json)
|
|
const raw = json
|
|
|
|
json = new FixImages(DetermineTheme._knownImages).convertStrict(json)
|
|
json.enableNoteImports = json.enableNoteImports ?? false
|
|
json = new PrepareTheme(convertState).convertStrict(json)
|
|
console.log("The layoutconfig is ", json)
|
|
|
|
json.id = forceId ?? json.id
|
|
|
|
{
|
|
new PrevalidateTheme().convertStrict(json)
|
|
}
|
|
{
|
|
new ValidateThemeAndLayers(
|
|
new DoesImageExist(new Set<string>(), () => true),
|
|
"",
|
|
false
|
|
).convertStrict(json)
|
|
}
|
|
return new ThemeConfig(json, false, {
|
|
definitionRaw: JSON.stringify(raw, null, " "),
|
|
definedAtUrl: sourceUrl,
|
|
})
|
|
}
|
|
|
|
private static async LoadRemoteTheme(link: string): Promise<ThemeConfig | null> {
|
|
console.log("Downloading map theme from ", link)
|
|
|
|
new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`).AttachTo(
|
|
"maindiv"
|
|
)
|
|
|
|
let parsed = <ThemeConfigJson>await Utils.downloadJson(link)
|
|
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)
|
|
parsed = await this.expandRemoteLayers(parsed)
|
|
return DetermineTheme.prepCustomTheme(parsed, link, forcedId)
|
|
}
|
|
}
|