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"
2023-02-08 01:14:21 +01:00
import known_layers from "../assets/generated/known_layers.json"
2022-10-27 01:50:01 +02:00
import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme"
2023-02-08 01:14:21 +01:00
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
2021-10-15 05:20:02 +02: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 ) )
2021-10-15 05:20:02 +02:00
/ * *
* Gets the correct layout for this website
* /
2023-03-02 05:20:53 +01:00
public static async GetLayout ( ) : Promise < LayoutConfig | undefined > {
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 )
2021-10-15 05:20:02 +02:00
if ( layoutFromBase64 . startsWith ( "http" ) ) {
2021-12-21 19:56:04 +01:00
return await DetermineLayout . LoadRemoteTheme ( layoutFromBase64 )
2021-10-15 05:20:02 +02:00
}
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 )
2021-10-15 05:20:02 +02:00
}
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
2021-10-15 05:20:02 +02:00
if ( path . endsWith ( ".html" ) ) {
2022-09-08 21:40:48 +02:00
layoutId = path . substr ( 0 , path . length - 5 )
2021-10-15 05:20:02 +02:00
}
2022-09-08 21:40:48 +02:00
console . log ( "Using layout" , layoutId )
2021-10-15 05:20:02 +02:00
}
2022-09-08 21:40:48 +02:00
layoutId = QueryParameters . GetQueryParameter (
"layout" ,
layoutId ,
"The layout to load into MapComplete"
) . data
2022-06-13 03:13:42 +02:00
return AllKnownLayouts . allKnownLayouts . get ( layoutId ? . toLowerCase ( ) )
2021-10-15 05:20:02 +02:00
}
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
2022-02-14 02:26:03 +01:00
2021-10-15 05:20:02 +02:00
try {
// layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
const dedicatedHashFromLocalStorage = LocalStorageSource . Get (
2021-10-27 19:57:15 +02:00
"user-layout-" + userLayoutParam . data ? . replace ( " " , "_" )
2022-09-08 21:40:48 +02:00
)
2021-10-15 05:20:02 +02:00
if ( dedicatedHashFromLocalStorage . data ? . length < 10 ) {
2022-09-08 21:40:48 +02:00
dedicatedHashFromLocalStorage . setData ( undefined )
2021-10-15 05:20:02 +02:00
}
2022-09-08 21:40:48 +02:00
const hashFromLocalStorage = LocalStorageSource . Get ( "last-loaded-user-layout" )
2021-10-15 05:20:02 +02:00
if ( hash . length < 10 ) {
2022-09-08 21:40:48 +02:00
hash = dedicatedHashFromLocalStorage . data ? ? hashFromLocalStorage . data
2021-10-15 05:20:02 +02:00
} else {
2022-09-08 21:40:48 +02:00
console . log ( "Saving hash to local storage" )
hashFromLocalStorage . setData ( hash )
dedicatedHashFromLocalStorage . setData ( hash )
2021-10-15 05:20:02 +02:00
}
try {
2022-09-08 21:40:48 +02:00
json = JSON . parse ( atob ( hash ) )
2021-10-15 05:20:02 +02:00
} catch ( e ) {
// We try to decode with lz-string
try {
json = JSON . parse ( Utils . UnMinify ( LZString . decompressFromBase64 ( hash ) ) )
} catch ( e ) {
2021-10-27 19:57:15 +02:00
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
2021-10-15 05:20:02 +02:00
}
}
2021-12-21 20:57:25 +01:00
const layoutToUse = DetermineLayout . prepCustomTheme ( json )
2022-09-08 21:40:48 +02:00
userLayoutParam . setData ( layoutToUse . id )
2022-04-18 02:39:30 +02:00
return layoutToUse
2021-10-15 05:20:02 +02:00
} catch ( e ) {
2021-10-27 19:57:15 +02:00
console . error ( e )
2021-10-15 05:20:02 +02:00
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
)
2021-10-15 05:20:02 +02:00
}
2022-02-14 02:26:03 +01:00
this . ShowErrorOnCustomTheme ( "Could not parse the hash" , new FixedUiElement ( e ) , json )
2022-09-08 21:40:48 +02:00
return null
2021-10-15 05:20:02 +02:00
}
}
public static ShowErrorOnCustomTheme (
intro : string = "Error: could not parse the custom layout:" ,
2022-02-14 02:26:03 +01:00
error : BaseUIElement ,
2022-09-08 21:40:48 +02:00
json? : any
) {
2021-10-15 05:20:02 +02:00
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 ,
2021-10-15 05:20:02 +02:00
] )
. SetClass ( "flex flex-col clickable" )
2022-09-08 21:40:48 +02:00
. AttachTo ( "centermessage" )
2021-10-15 05:20:02 +02:00
}
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 > ( )
2022-01-29 02:45:59 +01:00
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 ,
2022-06-13 03:13:42 +02:00
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-04-18 02:39:30 +02:00
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-09-11 01:49:07 +02:00
{
2022-10-27 01:50:01 +02:00
let { errors } = new PrevalidateTheme ( ) . convert ( json , "validation" )
2022-09-11 01:49:07 +02:00
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 ) ,
2022-09-11 01:49:07 +02:00
"" ,
2023-02-03 03:57:30 +01:00
false
2022-09-11 01:49:07 +02:00
) . convert ( json , "validation" )
if ( errors . length > 0 ) {
throw "Detected errors: " + errors . join ( "\n" )
}
}
2022-04-18 02:39:30 +02:00
return new LayoutConfig ( json , false , {
definitionRaw : JSON.stringify ( raw , null , " " ) ,
2022-09-08 21:40:48 +02:00
definedAtUrl : sourceUrl ,
2022-04-18 02:39:30 +02:00
} )
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 )
2021-12-21 20:57:25 +01:00
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: ` ,
2022-02-14 02:26:03 +01:00
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
}