forked from MapComplete/MapComplete
		
	
		
			
	
	
		
			168 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			168 lines
		
	
	
	
		
			6.8 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 * as personal from "../assets/themes/personal/personal.json"; | ||
|  | 
 | ||
|  | export default class DetermineLayout { | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Gets the correct layout for this website | ||
|  |      */ | ||
|  |     public static async GetLayout(): Promise<[LayoutConfig, string]> { | ||
|  | 
 | ||
|  | 
 | ||
|  |         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")) { | ||
|  |             // The userLayout is actually an url
 | ||
|  |             const layout = await DetermineLayout.LoadRemoteTheme(layoutFromBase64) | ||
|  |             return [layout, undefined] | ||
|  |         } | ||
|  | 
 | ||
|  |         if (layoutFromBase64 !== "false") { | ||
|  |             // We have to load something from the hash (or from disk)
 | ||
|  |             let loaded = DetermineLayout.LoadLayoutFromHash(loadCustomThemeParam); | ||
|  |             if (loaded === null) { | ||
|  |                 return [null, undefined] | ||
|  |             } | ||
|  |             return loaded | ||
|  |         } | ||
|  | 
 | ||
|  |         let layoutId: string = undefined | ||
|  |         if (location.href.indexOf("buurtnatuur.be") >= 0) { | ||
|  |             layoutId = "buurtnatuur" | ||
|  |         } | ||
|  | 
 | ||
|  | 
 | ||
|  |         const path = window.location.pathname.split("/").slice(-1)[0]; | ||
|  |         if (path !== "index.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 layoutToUse: LayoutConfig = AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase()); | ||
|  | 
 | ||
|  |         if (layoutToUse?.id === personal.id) { | ||
|  |             layoutToUse.layers = AllKnownLayouts.AllPublicLayers() | ||
|  |             for (const layer of layoutToUse.layers) { | ||
|  |                 layer.minzoomVisible = Math.max(layer.minzoomVisible, layer.minzoom) | ||
|  |                 layer.minzoom = Math.max(16, layer.minzoom) | ||
|  |             } | ||
|  |         } | ||
|  |          | ||
|  |         return [layoutToUse, undefined] | ||
|  |     } | ||
|  | 
 | ||
|  |     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 { | ||
|  | 
 | ||
|  |             const data = await Utils.downloadJson(link) | ||
|  |             try { | ||
|  |                 let parsed = data; | ||
|  |                 if (typeof parsed == "string") { | ||
|  |                     parsed = JSON.parse(parsed); | ||
|  |                 } | ||
|  |                 // Overwrite the id to the url
 | ||
|  |                 parsed.id = link; | ||
|  |                 return new LayoutConfig(parsed, false).patchImages(link, data); | ||
|  |             } catch (e) { | ||
|  | 
 | ||
|  |                 DetermineLayout.ShowErrorOnCustomTheme( | ||
|  |                     `<a href="${link}">${link}</a> is invalid:`, | ||
|  |                     new FixedUiElement(e) | ||
|  |                 ) | ||
|  |                 return null; | ||
|  |             } | ||
|  | 
 | ||
|  |         } catch (e) { | ||
|  |             DetermineLayout.ShowErrorOnCustomTheme( | ||
|  |                 `<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`, | ||
|  |                 new FixedUiElement(e) | ||
|  |             ) | ||
|  |             return null; | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     public static LoadLayoutFromHash( | ||
|  |         userLayoutParam: UIEventSource<string> | ||
|  |     ): [LayoutConfig, string] | null { | ||
|  |         let hash = location.hash.substr(1); | ||
|  |         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); | ||
|  |             } | ||
|  | 
 | ||
|  |             let json: any; | ||
|  |             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) { | ||
|  |                     DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON")) | ||
|  |                     return null; | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             const layoutToUse = new LayoutConfig(json, false); | ||
|  |             userLayoutParam.setData(layoutToUse.id); | ||
|  |             return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))]; | ||
|  |         } catch (e) { | ||
|  |             if (hash === undefined || hash.length < 10) { | ||
|  |                 DetermineLayout.ShowErrorOnCustomTheme("Could not load a theme from the hash", new FixedUiElement("Hash does not contain data")) | ||
|  |             } | ||
|  |             return null; | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     public static ShowErrorOnCustomTheme( | ||
|  |         intro: string = "Error: could not parse the custom layout:", | ||
|  |         error: BaseUIElement) { | ||
|  |         new Combine([ | ||
|  |             intro, | ||
|  |             error.SetClass("alert"), | ||
|  |             new SubtleButton("./assets/svg/mapcomplete_logo.svg", | ||
|  |                 "Go back to the theme overview", | ||
|  |                 {url: window.location.protocol + "//" + window.location.hostname + "/index.html", newTab: false}) | ||
|  | 
 | ||
|  |         ]) | ||
|  |             .SetClass("flex flex-col clickable") | ||
|  |             .AttachTo("centermessage"); | ||
|  |     } | ||
|  | 
 | ||
|  | } |