222 lines
7.9 KiB
TypeScript
222 lines
7.9 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"
|
|
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<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")[]
|
|
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")
|
|
}
|
|
}
|