forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			209 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			209 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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<Loc>,
 | 
						|
        options: {
 | 
						|
            currentBackground: UIEventSource<BaseLayer>
 | 
						|
            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<string | string[]>(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<Loc>
 | 
						|
            backgroundLayer: UIEventSource<BaseLayer>
 | 
						|
        },
 | 
						|
        currentBackground: UIEventSource<BaseLayer>,
 | 
						|
        options?: {
 | 
						|
            preferredCategory?: string
 | 
						|
            allowedCategories?: ("osmbasedmap" | "photo" | "map")[]
 | 
						|
        }
 | 
						|
    ) {
 | 
						|
        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()
 | 
						|
            }
 | 
						|
            buttons.push(button)
 | 
						|
        }
 | 
						|
 | 
						|
        // Selects the initial map
 | 
						|
 | 
						|
        super(buttons)
 | 
						|
        this.SetClass("flex")
 | 
						|
    }
 | 
						|
}
 |