import Combine from "../Base/Combine" import { UIEventSource } from "../../Logic/UIEventSource" import Loc from "../../Models/Loc" import Svg from "../../Svg" import Toggle from "../Input/Toggle" import BaseLayer from "../../Models/BaseLayer" import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" import BaseUIElement from "../BaseUIElement" import { GeoOperations } from "../../Logic/GeoOperations" import Hotkeys from "../Base/Hotkeys" import Translations from "../i18n/Translations" class SingleLayerSelectionButton extends Toggle { public readonly activate: () => void /** * * The SingeLayerSelectionButton also acts as an actor to keep the layers in check * * It works the following way: * * - It has a boolean state to indicate wether or not the button is active * - It keeps track of the available layers */ constructor( locationControl: UIEventSource, options: { currentBackground: UIEventSource preferredType: string preferredLayer?: BaseLayer notAvailable?: () => void } ) { const prefered = options.preferredType const previousLayer = new UIEventSource(options.preferredLayer) const unselected = SingleLayerSelectionButton.getIconFor(prefered).SetClass( "rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-invisible" ) const selected = SingleLayerSelectionButton.getIconFor(prefered).SetClass( "rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-attention-catch" ) const available = AvailableBaseLayers.SelectBestLayerAccordingTo( locationControl, new UIEventSource(options.preferredType) ) let toggle: BaseUIElement = new Toggle( selected, unselected, options.currentBackground.map((bg) => bg?.category === options.preferredType) ) super( toggle, undefined, available.map((av) => av?.category === options.preferredType) ) /** * Checks that the previous layer is still usable on the current location. * If not, clears the 'previousLayer' */ function checkPreviousLayer() { if (previousLayer.data === undefined) { return } if (previousLayer.data.feature === null || previousLayer.data.feature === undefined) { // Global layer return } const loc = locationControl.data if (!GeoOperations.inside([loc.lon, loc.lat], previousLayer.data.feature)) { // The previous layer is out of bounds previousLayer.setData(undefined) } } unselected.onClick(() => { // Note: a check if 'available' has the correct type is not needed: // Unselected will _not_ be visible if availableBaseLayer has a wrong type! checkPreviousLayer() previousLayer.setData(previousLayer.data ?? available.data) options.currentBackground.setData(previousLayer.data) }) options.currentBackground.addCallbackAndRunD((background) => { if (background.category === options.preferredType) { previousLayer.setData(background) } }) available.addCallbackD((availableLayer) => { // Called whenever a better layer is available if (previousLayer.data === undefined) { // PreviousLayer is unset -> we definitively weren't using this category -> no need to switch return } if (options.currentBackground.data?.id !== previousLayer.data?.id) { // The previously used layer doesn't match the current layer -> no need to switch return } // Is the previous layer still valid? If so, we don't bother to switch if ( previousLayer.data.feature === null || GeoOperations.inside(locationControl.data, previousLayer.data.feature) ) { return } if (availableLayer.category === options.preferredType) { // Allright, we can set this different layer options.currentBackground.setData(availableLayer) previousLayer.setData(availableLayer) } else { // Uh oh - no correct layer is available... We pass the torch! if (options.notAvailable !== undefined) { options.notAvailable() } else { // Fallback to OSM carto options.currentBackground.setData(AvailableBaseLayers.osmCarto) } } }) this.activate = () => { checkPreviousLayer() if (available.data.category !== options.preferredType) { // This object can't help either - pass the torch! if (options.notAvailable !== undefined) { options.notAvailable() } else { // Fallback to OSM carto options.currentBackground.setData(AvailableBaseLayers.osmCarto) } return } previousLayer.setData(previousLayer.data ?? available.data) options.currentBackground.setData(previousLayer.data) } } private static getIconFor(type: string) { switch (type) { case "map": return Svg.generic_map_svg() case "photo": return Svg.satellite_svg() case "osmbasedmap": return Svg.osm_logo_svg() default: return Svg.generic_map_svg() } } } export default class BackgroundMapSwitch extends Combine { /** * Three buttons to easily switch map layers between OSM, aerial and some map. * @param state * @param currentBackground * @param options */ constructor( state: { locationControl: UIEventSource backgroundLayer: UIEventSource }, currentBackground: UIEventSource, options?: { preferredCategory?: string allowedCategories?: ("osmbasedmap" | "photo" | "map")[] enableHotkeys?: boolean } ) { const allowedCategories = options?.allowedCategories ?? ["osmbasedmap", "photo", "map"] const previousLayer = state.backgroundLayer.data const buttons = [] let activatePrevious: () => void = undefined for (const category of allowedCategories) { let preferredLayer = undefined if (previousLayer?.category === category) { preferredLayer = previousLayer } const button = new SingleLayerSelectionButton(state.locationControl, { preferredType: category, preferredLayer: preferredLayer, currentBackground: currentBackground, notAvailable: activatePrevious, }) // Fall back to the first option: OSM activatePrevious = activatePrevious ?? button.activate if (category === options?.preferredCategory) { button.activate() } if (options?.enableHotkeys) { Hotkeys.RegisterHotkey( { nomod: category.charAt(0).toUpperCase() }, Translations.t.hotkeyDocumentation.selectBackground.Subs({ category }), () => { button.activate() } ) } buttons.push(button) } // Selects the initial map super(buttons) this.SetClass("flex") } }