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"; 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 { constructor( state: { locationControl: UIEventSource, backgroundLayer: UIEventSource }, currentBackground: UIEventSource, preferredCategory?: string ) { const 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 === preferredCategory) { button.activate() } buttons.push(button) } // Selects the initial map super(buttons) this.SetClass("flex") } }