mapcomplete/Logic/State/MapState.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

502 lines
18 KiB
TypeScript
Raw Normal View History

import UserRelatedState from "./UserRelatedState"
import { Store, Stores, UIEventSource } from "../UIEventSource"
import BaseLayer from "../../Models/BaseLayer"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import AvailableBaseLayers from "../Actors/AvailableBaseLayers"
import Attribution from "../../UI/BigComponents/Attribution"
import Minimap, { MinimapObj } from "../../UI/Base/Minimap"
import { Tiles } from "../../Models/TileRange"
import BaseUIElement from "../../UI/BaseUIElement"
2022-01-08 04:22:50 +01:00
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"
import { QueryParameters } from "../Web/QueryParameters"
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"
import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource"
2021-11-08 02:36:01 +01:00
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"
2021-11-12 04:11:53 +01:00
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { GeoOperations } from "../GeoOperations"
2021-12-21 18:35:31 +01:00
import TitleHandler from "../Actors/TitleHandler"
import { BBox } from "../BBox"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { TiledStaticFeatureSource } from "../FeatureSource/Sources/StaticFeatureSource"
import { Translation, TypedTranslation } from "../../UI/i18n/Translation"
import { Tag } from "../Tags/Tag"
2022-08-22 13:34:47 +02:00
import { OsmConnection } from "../Osm/OsmConnection"
export interface GlobalFilter {
filter: FilterState
id: string
onNewPoint: {
safetyCheck: Translation
confirmAddNew: TypedTranslation<{ preset: Translation }>
tags: Tag[]
}
}
/**
* Contains all the leaflet-map related state
*/
export default class MapState extends UserRelatedState {
/**
The leaflet instance of the big basemap
*/
2021-10-15 14:52:11 +02:00
public leafletMap = new UIEventSource<any /*L.Map*/>(undefined, "leafletmap")
/**
* A list of currently available background layers
*/
public availableBackgroundLayers: Store<BaseLayer[]>
/**
* The current background layer
*/
public backgroundLayer: UIEventSource<BaseLayer>
/**
* Last location where a click was registered
*/
public readonly LastClickLocation: UIEventSource<{
lat: number
lon: number
}> = new UIEventSource<{ lat: number; lon: number }>(undefined)
2021-12-10 17:30:50 +01:00
/**
* The bounds of the current map view
*/
public currentView: FeatureSourceForLayer & Tiled
/**
* The location as delivered by the GPS
*/
public currentUserLocation: SimpleFeatureSource
2022-01-08 22:11:24 +01:00
2021-11-08 02:36:01 +01:00
/**
* All previously visited points
*/
public historicalUserLocations: SimpleFeatureSource
2021-11-12 04:11:53 +01:00
/**
* The number of seconds that the GPS-locations are stored in memory.
* Time in seconds
2021-11-12 04:11:53 +01:00
*/
public gpsLocationHistoryRetentionTime = new UIEventSource(
7 * 24 * 60 * 60,
"gps_location_retention"
)
2021-11-12 04:11:53 +01:00
public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled
2021-11-08 02:36:01 +01:00
/**
* A feature source containing the current home location of the user
*/
public homeLocation: FeatureSourceForLayer & Tiled
public readonly mainMapObject: BaseUIElement & MinimapObj
/**
* Which layers are enabled in the current theme and what filters are applied onto them
*/
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>(
[],
"filteredLayers"
2022-09-08 21:40:48 +02:00
)
2022-07-21 15:54:24 +02:00
/**
* Filters which apply onto all layers
*/
public globalFilters: UIEventSource<GlobalFilter[]> = new UIEventSource([], "globalFilters")
/**
* Which overlays are shown
*/
public overlayToggles: { config: TilesourceConfig; isDisplayed: UIEventSource<boolean> }[]
2022-01-08 22:11:24 +01:00
constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) {
super(layoutToUse, options)
this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl)
let defaultLayer = AvailableBaseLayers.osmCarto
const available = this.availableBackgroundLayers.data
for (const layer of available) {
if (this.backgroundLayerId.data === layer.id) {
defaultLayer = layer
}
}
const self = this
this.backgroundLayer = new UIEventSource<BaseLayer>(defaultLayer)
this.backgroundLayer.addCallbackAndRunD((layer) => self.backgroundLayerId.setData(layer.id))
2022-01-08 22:11:24 +01:00
const attr = new Attribution(
this.locationControl,
this.osmConnection.userDetails,
this.layoutToUse,
this.currentBounds
)
// Will write into this.leafletMap
this.mainMapObject = Minimap.createMiniMap({
background: this.backgroundLayer,
location: this.locationControl,
leafletMap: this.leafletMap,
bounds: this.currentBounds,
attribution: attr,
lastClickLocation: this.LastClickLocation,
})
2022-02-22 14:13:41 +01:00
this.overlayToggles =
this.layoutToUse?.tileLayerSources
?.filter((c) => c.name !== undefined)
?.map((c) => ({
config: c,
isDisplayed: QueryParameters.GetBooleanQueryParameter(
"overlay-" + c.id,
c.defaultState,
"Wether or not the overlay " + c.id + " is shown"
2022-09-08 21:40:48 +02:00
),
})) ?? []
2022-08-22 13:34:47 +02:00
this.filteredLayers = new UIEventSource<FilteredLayer[]>(
MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection)
2022-09-08 21:40:48 +02:00
)
2021-10-15 14:52:11 +02:00
this.lockBounds()
this.AddAllOverlaysToMap(this.leafletMap)
2021-11-08 02:36:01 +01:00
this.initHomeLocation()
this.initGpsLocation()
2021-11-08 14:18:45 +01:00
this.initUserLocationTrail()
2021-12-10 15:51:08 +01:00
this.initCurrentView()
2021-12-21 18:35:31 +01:00
new TitleHandler(this)
}
2021-11-07 16:34:51 +01:00
public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) {
const initialized = new Set()
for (const overlayToggle of this.overlayToggles) {
new ShowOverlayLayer(overlayToggle.config, leafletMap, overlayToggle.isDisplayed)
initialized.add(overlayToggle.config)
}
2022-02-22 14:13:41 +01:00
for (const tileLayerSource of this.layoutToUse?.tileLayerSources ?? []) {
2021-11-07 16:34:51 +01:00
if (initialized.has(tileLayerSource)) {
continue
}
new ShowOverlayLayer(tileLayerSource, leafletMap)
}
}
private lockBounds() {
const layout = this.layoutToUse
2022-02-22 14:13:41 +01:00
if (!layout?.lockLocation) {
return
}
2022-02-22 14:13:41 +01:00
console.warn("Locking the bounds to ", layout.lockLocation)
this.mainMapObject.installBounds(
new BBox(layout.lockLocation),
this.featureSwitchIsTesting.data
)
}
2022-01-08 22:11:24 +01:00
private initCurrentView() {
2021-12-10 17:30:50 +01:00
let currentViewLayer: FilteredLayer = this.filteredLayers.data.filter(
(l) => l.layerDef.id === "current_view"
)[0]
2022-01-08 22:11:24 +01:00
if (currentViewLayer === undefined) {
2021-12-10 17:30:50 +01:00
// This layer is not needed by the theme and thus unloaded
return
}
let i = 0
const self = this
const features: Store<{ feature: any; freshness: Date }[]> = this.currentBounds.map(
(bounds) => {
2022-01-08 22:11:24 +01:00
if (bounds === undefined) {
2021-12-10 17:30:50 +01:00
return []
}
i++
2021-12-10 15:51:08 +01:00
const feature = {
freshness: new Date(),
feature: {
2021-12-10 17:30:50 +01:00
type: "Feature",
2022-01-08 22:11:24 +01:00
properties: {
id: "current_view-" + i,
current_view: "yes",
zoom: "" + self.locationControl.data.zoom,
2022-09-08 21:40:48 +02:00
},
2022-01-08 22:11:24 +01:00
geometry: {
type: "Polygon",
coordinates: [
2022-09-08 21:40:48 +02:00
[
2021-12-10 15:51:08 +01:00
[bounds.maxLon, bounds.maxLat],
[bounds.minLon, bounds.maxLat],
[bounds.minLon, bounds.minLat],
[bounds.maxLon, bounds.minLat],
[bounds.maxLon, bounds.maxLat],
2022-09-08 21:40:48 +02:00
],
],
},
2021-12-10 15:51:08 +01:00
},
}
return [feature]
}
)
2022-01-08 22:11:24 +01:00
this.currentView = new TiledStaticFeatureSource(features, currentViewLayer)
2021-12-10 15:51:08 +01:00
}
private initGpsLocation() {
2021-11-08 02:36:01 +01:00
// Initialize the gps layer data. This is emtpy for now, the actual writing happens in the Geolocationhandler
let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(
(l) => l.layerDef.id === "gps_location"
)[0]
2022-01-26 21:40:38 +01:00
if (gpsLayerDef === undefined) {
2022-01-08 22:11:24 +01:00
return
}
2021-11-08 02:36:01 +01:00
this.currentUserLocation = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0))
}
private initUserLocationTrail() {
const features = LocalStorageSource.GetParsed<{ feature: any; freshness: Date }[]>(
"gps_location_history",
[]
)
2021-11-12 04:11:53 +01:00
const now = new Date().getTime()
features.data = features.data
.map((ff) => ({ feature: ff.feature, freshness: new Date(ff.freshness) }))
.filter(
(ff) =>
now - ff.freshness.getTime() < 1000 * this.gpsLocationHistoryRetentionTime.data
2022-09-08 21:40:48 +02:00
)
2021-11-12 04:11:53 +01:00
features.ping()
const self = this
2021-11-08 14:18:45 +01:00
let i = 0
2022-01-08 22:11:24 +01:00
this.currentUserLocation?.features?.addCallbackAndRunD(([location]) => {
if (location === undefined) {
2021-11-08 14:18:45 +01:00
return
}
2021-11-12 04:11:53 +01:00
const previousLocation = features.data[features.data.length - 1]
if (previousLocation !== undefined) {
2021-11-12 04:11:53 +01:00
const d = GeoOperations.distanceBetween(
previousLocation.feature.geometry.coordinates,
location.feature.geometry.coordinates
)
let timeDiff = Number.MAX_VALUE // in seconds
const olderLocation = features.data[features.data.length - 2]
if (olderLocation !== undefined) {
timeDiff =
(new Date(previousLocation.freshness).getTime() -
new Date(olderLocation.freshness).getTime()) /
1000
}
if (d < 20 && timeDiff < 60) {
2021-11-12 04:11:53 +01:00
// Do not append changes less then 20m - it's probably noise anyway
return
}
2021-11-08 14:18:45 +01:00
}
2021-11-12 04:11:53 +01:00
const feature = JSON.parse(JSON.stringify(location.feature))
feature.properties.id = "gps/" + features.data.length
2021-11-12 04:11:53 +01:00
i++
features.data.push({ feature, freshness: new Date() })
2021-11-08 14:18:45 +01:00
features.ping()
})
2021-11-12 04:11:53 +01:00
let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(
(l) => l.layerDef.id === "gps_location_history"
)[0]
2022-01-26 21:40:38 +01:00
if (gpsLayerDef !== undefined) {
2022-01-08 22:11:24 +01:00
this.historicalUserLocations = new SimpleFeatureSource(
gpsLayerDef,
Tiles.tile_index(0, 0, 0),
features
2022-09-08 21:40:48 +02:00
)
this.changes.setHistoricalUserLocations(this.historicalUserLocations)
2022-01-08 22:11:24 +01:00
}
2021-11-12 04:11:53 +01:00
const asLine = features.map((allPoints) => {
if (allPoints === undefined || allPoints.length < 2) {
2021-11-12 04:11:53 +01:00
return []
}
const feature = {
type: "Feature",
properties: {
id: "location_track",
2021-11-12 04:11:53 +01:00
"_date:now": new Date().toISOString(),
},
geometry: {
2021-11-12 04:11:53 +01:00
type: "LineString",
coordinates: allPoints.map((ff) => ff.feature.geometry.coordinates),
},
}
2021-11-12 04:11:53 +01:00
self.allElements.ContainingFeatures.set(feature.properties.id, feature)
2021-11-12 04:11:53 +01:00
return [
{
feature,
freshness: new Date(),
},
]
})
let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter(
(l) => l.layerDef.id === "gps_track"
)[0]
2022-01-26 21:40:38 +01:00
if (gpsLineLayerDef !== undefined) {
this.historicalUserLocationsTrack = new TiledStaticFeatureSource(
asLine,
gpsLineLayerDef
2022-09-08 21:40:48 +02:00
)
2022-01-08 22:11:24 +01:00
}
2021-11-08 14:18:45 +01:00
}
2021-11-08 02:36:01 +01:00
private initHomeLocation() {
const empty = []
const feature = Stores.ListStabilized(
this.osmConnection.userDetails.map((userDetails) => {
2021-11-08 02:36:01 +01:00
if (userDetails === undefined) {
return undefined
}
const home = userDetails.home
if (home === undefined) {
return undefined
}
return [home.lon, home.lat]
})
).map((homeLonLat) => {
if (homeLonLat === undefined) {
return empty
}
return [
{
feature: {
type: "Feature",
properties: {
id: "home",
2021-11-08 02:36:01 +01:00
"user:home": "yes",
_lon: homeLonLat[0],
_lat: homeLonLat[1],
2022-09-08 21:40:48 +02:00
},
geometry: {
2021-11-08 02:36:01 +01:00
type: "Point",
coordinates: homeLonLat,
2022-09-08 21:40:48 +02:00
},
2021-11-08 02:36:01 +01:00
},
freshness: new Date(),
},
]
})
const flayer = this.filteredLayers.data.filter((l) => l.layerDef.id === "home_location")[0]
2022-01-08 22:11:24 +01:00
if (flayer !== undefined) {
this.homeLocation = new TiledStaticFeatureSource(feature, flayer)
2022-01-08 22:11:24 +01:00
}
2021-11-08 02:36:01 +01:00
}
2021-10-15 14:52:11 +02:00
2022-08-22 13:34:47 +02:00
private static getPref(
osmConnection: OsmConnection,
key: string,
layer: LayerConfig
): UIEventSource<boolean> {
2022-07-26 16:51:00 +02:00
return osmConnection.GetPreference(key, layer.shownByDefault + "").sync(
(v) => {
if (v === undefined) {
return undefined
}
return v === "true"
},
[],
(b) => {
if (b === undefined) {
return undefined
}
return "" + b
}
)
}
2022-08-22 13:34:47 +02:00
public static InitializeFilteredLayers(
layoutToUse: { layers: LayerConfig[]; id: string },
osmConnection: OsmConnection
): FilteredLayer[] {
if (layoutToUse === undefined) {
2022-08-22 13:34:47 +02:00
return []
2022-02-22 14:13:41 +01:00
}
const flayers: FilteredLayer[] = []
for (const layer of layoutToUse.layers) {
let isDisplayed: UIEventSource<boolean>
if (layer.syncSelection === "local") {
isDisplayed = LocalStorageSource.GetParsed(
layoutToUse.id + "-layer-" + layer.id + "-enabled",
layer.shownByDefault
)
} else if (layer.syncSelection === "theme-only") {
2022-08-22 13:34:47 +02:00
isDisplayed = MapState.getPref(
osmConnection,
layoutToUse.id + "-layer-" + layer.id + "-enabled",
layer
)
} else if (layer.syncSelection === "global") {
2022-08-22 13:34:47 +02:00
isDisplayed = MapState.getPref(
osmConnection,
"layer-" + layer.id + "-enabled",
layer
)
} else {
isDisplayed = QueryParameters.GetBooleanQueryParameter(
"layer-" + layer.id,
layer.shownByDefault,
"Wether or not layer " + layer.id + " is shown"
)
}
2022-01-08 22:11:24 +01:00
const flayer: FilteredLayer = {
2022-07-26 16:51:00 +02:00
isDisplayed,
layerDef: layer,
2022-01-08 22:11:24 +01:00
appliedFilters: new UIEventSource<Map<string, FilterState>>(
new Map<string, FilterState>()
2022-09-08 21:40:48 +02:00
),
}
2022-01-08 04:22:50 +01:00
layer.filters.forEach((filterConfig) => {
const stateSrc = filterConfig.initState()
2022-01-08 22:11:24 +01:00
stateSrc.addCallbackAndRun((state) =>
flayer.appliedFilters.data.set(filterConfig.id, state)
2022-09-08 21:40:48 +02:00
)
2022-01-08 04:22:50 +01:00
flayer.appliedFilters
.map((dict) => dict.get(filterConfig.id))
.addCallback((state) => stateSrc.setData(state))
})
flayers.push(flayer)
}
for (const layer of layoutToUse.layers) {
if (layer.filterIsSameAs === undefined) {
continue
}
const toReuse = flayers.find((l) => l.layerDef.id === layer.filterIsSameAs)
if (toReuse === undefined) {
throw (
"Error in layer " +
layer.id +
": it defines that it should be use the filters of " +
layer.filterIsSameAs +
", but this layer was not loaded"
2022-09-08 21:40:48 +02:00
)
}
console.warn(
"Linking filter and isDisplayed-states of " +
layer.id +
" and " +
layer.filterIsSameAs
)
const selfLayer = flayers.findIndex((l) => l.layerDef.id === layer.id)
flayers[selfLayer] = {
isDisplayed: toReuse.isDisplayed,
layerDef: layer,
appliedFilters: toReuse.appliedFilters,
}
}
2022-08-22 13:34:47 +02:00
return flayers
}
}