2025-01-23 05:01:55 +01:00
|
|
|
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
|
|
|
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
|
|
|
|
import { Map as MlMap } from "maplibre-gl"
|
|
|
|
|
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
|
|
|
|
|
import InitialMapPositioning from "../../Logic/Actors/InitialMapPositioning"
|
|
|
|
|
import { MapLibreAdaptor } from "../../UI/Map/MapLibreAdaptor"
|
|
|
|
|
import { ExportableMap, MapProperties } from "../MapProperties"
|
|
|
|
|
import { LastClickFeatureSource } from "../../Logic/FeatureSource/Sources/LastClickFeatureSource"
|
|
|
|
|
import { PreferredRasterLayerSelector } from "../../Logic/Actors/PreferredRasterLayerSelector"
|
|
|
|
|
import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "../RasterLayers"
|
|
|
|
|
import BackgroundLayerResetter from "../../Logic/Actors/BackgroundLayerResetter"
|
|
|
|
|
import Hotkeys from "../../UI/Base/Hotkeys"
|
|
|
|
|
import Translations from "../../UI/i18n/Translations"
|
|
|
|
|
import { EliCategory } from "../RasterLayerProperties"
|
|
|
|
|
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
|
|
|
|
import { Feature, Point, Polygon } from "geojson"
|
|
|
|
|
import { FeatureSource, WritableFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
|
|
|
|
import FullNodeDatabaseSource from "../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
|
|
|
|
import { WithUserRelatedState } from "./WithUserRelatedState"
|
|
|
|
|
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
|
|
|
|
|
import { GeolocationControlState } from "../../UI/BigComponents/GeolocationControl"
|
|
|
|
|
import ShowOverlayRasterLayer from "../../UI/Map/ShowOverlayRasterLayer"
|
|
|
|
|
import { BBox } from "../../Logic/BBox"
|
|
|
|
|
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The first core of the state management; everything related to:
|
|
|
|
|
* - the OSM connection
|
|
|
|
|
* - setting up the basemap
|
|
|
|
|
* - the feature switches
|
|
|
|
|
* - the GPS-location
|
|
|
|
|
*
|
|
|
|
|
* Anything that handles editable elements is _not_ done on this level.
|
|
|
|
|
* Anything that handles the UI is not done on this level
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export class UserMapFeatureswitchState extends WithUserRelatedState {
|
|
|
|
|
readonly map: UIEventSource<MlMap>
|
|
|
|
|
|
|
|
|
|
readonly mapProperties: MapLibreAdaptor & MapProperties & ExportableMap
|
|
|
|
|
readonly lastClickObject: LastClickFeatureSource
|
|
|
|
|
|
|
|
|
|
readonly geolocationState: GeoLocationState
|
|
|
|
|
readonly geolocation: GeoLocationHandler
|
|
|
|
|
readonly geolocationControl: GeolocationControlState
|
|
|
|
|
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
|
|
|
|
|
|
|
|
|
|
readonly availableLayers: { store: Store<RasterLayerPolygon[]> }
|
|
|
|
|
readonly currentView: FeatureSource<Feature<Polygon>>
|
|
|
|
|
readonly fullNodeDatabase?: FullNodeDatabaseSource
|
|
|
|
|
|
|
|
|
|
constructor(theme: ThemeConfig, selectedElement: Store<object>) {
|
2025-01-28 15:42:34 +01:00
|
|
|
const rasterLayer: UIEventSource<RasterLayerPolygon> =
|
|
|
|
|
new UIEventSource<RasterLayerPolygon>(undefined)
|
2025-01-23 05:01:55 +01:00
|
|
|
super(theme, rasterLayer)
|
|
|
|
|
this.geolocationState = new GeoLocationState()
|
|
|
|
|
const initial = new InitialMapPositioning(theme, this.geolocationState, this.osmConnection)
|
|
|
|
|
this.map = new UIEventSource<MlMap>(undefined)
|
2025-01-28 15:42:34 +01:00
|
|
|
this.mapProperties = new MapLibreAdaptor(
|
|
|
|
|
this.map,
|
|
|
|
|
{ rasterLayer, ...initial },
|
|
|
|
|
{ correctClick: 20 }
|
|
|
|
|
)
|
2025-01-23 05:01:55 +01:00
|
|
|
|
|
|
|
|
this.geolocation = new GeoLocationHandler(
|
|
|
|
|
this.geolocationState,
|
|
|
|
|
selectedElement,
|
|
|
|
|
this.mapProperties,
|
|
|
|
|
this.userRelatedState.gpsLocationHistoryRetentionTime
|
|
|
|
|
)
|
|
|
|
|
this.geolocationControl = new GeolocationControlState(this.geolocation, this.mapProperties)
|
|
|
|
|
this.historicalUserLocations = this.geolocation.historicalUserLocations
|
|
|
|
|
|
|
|
|
|
this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => {
|
|
|
|
|
this.mapProperties.allowRotating.setData(fixated !== "yes")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.availableLayers = AvailableRasterLayers.layersAvailableAt(
|
|
|
|
|
this.mapProperties.location,
|
|
|
|
|
this.osmConnection.isLoggedIn
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
this.userRelatedState.markLayoutAsVisited(this.theme)
|
|
|
|
|
|
|
|
|
|
this.lastClickObject = new LastClickFeatureSource(
|
|
|
|
|
this.theme,
|
|
|
|
|
this.mapProperties.lastClickLocation,
|
|
|
|
|
this.userRelatedState.addNewFeatureMode
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
let currentViewIndex = 0
|
|
|
|
|
const empty = []
|
|
|
|
|
this.currentView = new StaticFeatureSource(
|
|
|
|
|
this.mapProperties.bounds.map((bbox) => {
|
|
|
|
|
if (!bbox) {
|
|
|
|
|
return empty
|
|
|
|
|
}
|
|
|
|
|
currentViewIndex++
|
|
|
|
|
return <Feature[]>[
|
|
|
|
|
bbox.asGeoJson({
|
|
|
|
|
zoom: this.mapProperties.zoom.data,
|
|
|
|
|
...this.mapProperties.location.data,
|
2025-01-28 15:42:34 +01:00
|
|
|
id: "current_view_" + currentViewIndex,
|
|
|
|
|
}),
|
2025-01-23 05:01:55 +01:00
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.theme.layers.some((l) => l._needsFullNodeDatabase)) {
|
|
|
|
|
this.fullNodeDatabase = new FullNodeDatabaseSource()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
///////// Actors ///////////////
|
|
|
|
|
|
|
|
|
|
new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers)
|
|
|
|
|
|
|
|
|
|
this.userRelatedState.showScale.addCallbackAndRun((showScale) => {
|
|
|
|
|
this.mapProperties.showScale.set(showScale)
|
|
|
|
|
})
|
|
|
|
|
new PreferredRasterLayerSelector(
|
|
|
|
|
this.mapProperties.rasterLayer,
|
|
|
|
|
this.availableLayers,
|
|
|
|
|
this.featureSwitches.backgroundLayerId,
|
|
|
|
|
this.userRelatedState.preferredBackgroundLayer
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
this.initHotkeys()
|
|
|
|
|
this.drawOverlayLayers()
|
|
|
|
|
this.drawLock()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* If the map is locked to a certain area _and_ we are in test mode, draw this on the map
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
private drawLock() {
|
|
|
|
|
if (!this.theme?.lockLocation) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const bbox = new BBox(<[[number, number], [number, number]]>this.theme.lockLocation)
|
|
|
|
|
this.mapProperties.maxbounds.setData(bbox)
|
|
|
|
|
ShowDataLayer.showRange(
|
|
|
|
|
this.map,
|
|
|
|
|
new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]),
|
|
|
|
|
this.featureSwitches.featureSwitchIsTesting
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private drawOverlayLayers() {
|
|
|
|
|
for (const rasterInfo of this.theme.tileLayerSources) {
|
|
|
|
|
const state = this.overlayLayerStates.get(rasterInfo.id)
|
|
|
|
|
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* By focussing on the map, the keyboard panning and zoom with '+' and '+' works */
|
|
|
|
|
public focusOnMap() {
|
|
|
|
|
if (this.map.data) {
|
|
|
|
|
this.map.data.getCanvas().focus()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
this.map.addCallbackAndRunD((map) => {
|
|
|
|
|
map.on("load", () => {
|
|
|
|
|
map.getCanvas().focus()
|
|
|
|
|
})
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private initHotkeys() {
|
|
|
|
|
const docs = Translations.t.hotkeyDocumentation
|
|
|
|
|
|
|
|
|
|
this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => {
|
|
|
|
|
if (!enable) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const setLayerCategory = (category: EliCategory, skipLayers: number = 0) => {
|
|
|
|
|
const timeOfCall = new Date()
|
|
|
|
|
this.availableLayers.store.addCallbackAndRunD((available) => {
|
|
|
|
|
const now = new Date()
|
|
|
|
|
const timeDiff = (now.getTime() - timeOfCall.getTime()) / 1000
|
|
|
|
|
if (timeDiff > 3) {
|
|
|
|
|
return true // unregister
|
|
|
|
|
}
|
|
|
|
|
const current = this.mapProperties.rasterLayer
|
|
|
|
|
const best = RasterLayerUtils.SelectBestLayerAccordingTo(
|
|
|
|
|
available,
|
|
|
|
|
category,
|
|
|
|
|
current.data,
|
|
|
|
|
skipLayers
|
|
|
|
|
)
|
|
|
|
|
if (!best) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
console.log("Best layer for category", category, "is", best?.properties?.id)
|
|
|
|
|
current.setData(best)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-28 15:42:34 +01:00
|
|
|
Hotkeys.RegisterHotkey({ nomod: "O" }, docs.selectOsmbasedmap, () =>
|
|
|
|
|
setLayerCategory("osmbasedmap")
|
2025-01-23 05:01:55 +01:00
|
|
|
)
|
|
|
|
|
|
2025-01-28 15:42:34 +01:00
|
|
|
Hotkeys.RegisterHotkey({ nomod: "M" }, docs.selectMap, () => setLayerCategory("map"))
|
2025-01-23 05:01:55 +01:00
|
|
|
|
2025-01-28 15:42:34 +01:00
|
|
|
Hotkeys.RegisterHotkey({ nomod: "P" }, docs.selectAerial, () =>
|
|
|
|
|
setLayerCategory("photo")
|
2025-01-23 05:01:55 +01:00
|
|
|
)
|
2025-01-28 15:42:34 +01:00
|
|
|
Hotkeys.RegisterHotkey({ shift: "O" }, docs.selectOsmbasedmap, () =>
|
|
|
|
|
setLayerCategory("osmbasedmap", 2)
|
2025-01-23 05:01:55 +01:00
|
|
|
)
|
|
|
|
|
|
2025-01-28 15:42:34 +01:00
|
|
|
Hotkeys.RegisterHotkey({ shift: "M" }, docs.selectMap, () => setLayerCategory("map", 2))
|
2025-01-23 05:01:55 +01:00
|
|
|
|
2025-01-28 15:42:34 +01:00
|
|
|
Hotkeys.RegisterHotkey({ shift: "P" }, docs.selectAerial, () =>
|
|
|
|
|
setLayerCategory("photo", 2)
|
2025-01-23 05:01:55 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
|
2025-01-28 15:42:34 +01:00
|
|
|
Hotkeys.RegisterHotkey({ nomod: "L" }, Translations.t.hotkeyDocumentation.geolocate, () => {
|
|
|
|
|
this.geolocationControl.handleClick()
|
|
|
|
|
})
|
2025-01-23 05:01:55 +01:00
|
|
|
|
|
|
|
|
Hotkeys.RegisterHotkey(
|
|
|
|
|
{
|
2025-01-28 15:42:34 +01:00
|
|
|
shift: "T",
|
2025-01-23 05:01:55 +01:00
|
|
|
},
|
|
|
|
|
docs.translationMode,
|
|
|
|
|
() => {
|
|
|
|
|
const tm = this.userRelatedState.translationMode
|
|
|
|
|
if (tm.data === "false") {
|
|
|
|
|
tm.setData("true")
|
|
|
|
|
} else {
|
|
|
|
|
tm.setData("false")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Shows the current GPS-location marker on the given map.
|
|
|
|
|
* This is used to show the location on _other_ maps, e.g. on the map to add a new feature.
|
|
|
|
|
*
|
|
|
|
|
* This is _NOT_ to be used on the main map!
|
|
|
|
|
*/
|
|
|
|
|
public showCurrentLocationOn(map: Store<MlMap>) {
|
|
|
|
|
const id = "gps_location"
|
|
|
|
|
const layer = this.theme.getLayer(id)
|
|
|
|
|
if (layer === undefined) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (map === this.map) {
|
|
|
|
|
throw "Invalid use of showCurrentLocationOn"
|
|
|
|
|
}
|
|
|
|
|
const features = this.geolocation.currentUserLocation
|
|
|
|
|
return new ShowDataLayer(map, {
|
|
|
|
|
features,
|
|
|
|
|
layer,
|
2025-01-28 15:42:34 +01:00
|
|
|
metaTags: this.userRelatedState.preferencesAsTags,
|
2025-01-23 05:01:55 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|