forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			203 lines
		
	
	
		
			No EOL
		
	
	
		
			7.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			203 lines
		
	
	
		
			No EOL
		
	
	
		
			7.1 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 {
 | |
| 
 | |
|     constructor(
 | |
|         state: {
 | |
|             locationControl: UIEventSource<Loc>,
 | |
|             backgroundLayer: UIEventSource<BaseLayer>
 | |
|         },
 | |
|         currentBackground: UIEventSource<BaseLayer>,
 | |
|         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")
 | |
|     }
 | |
| 
 | |
| } |