mapcomplete/Models/ThemeViewState.ts

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

459 lines
19 KiB
TypeScript
Raw Normal View History

2023-03-28 05:13:48 +02:00
import LayoutConfig from "./ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
import { Changes } from "../Logic/Osm/Changes"
import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource"
import {
FeatureSource,
2023-03-28 05:13:48 +02:00
IndexedFeatureSource,
WritableFeatureSource,
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { ExportableMap, MapProperties } from "./MapProperties"
2023-03-28 05:13:48 +02:00
import LayerState from "../Logic/State/LayerState"
2023-04-20 18:58:31 +02:00
import { Feature, Point } from "geojson"
2023-03-28 05:13:48 +02:00
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import { Map as MlMap } from "maplibre-gl"
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"
import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor"
import { GeoLocationState } from "../Logic/State/GeoLocationState"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import { QueryParameters } from "../Logic/Web/QueryParameters"
import UserRelatedState from "../Logic/State/UserRelatedState"
import LayerConfig from "./ThemeConfig/LayerConfig"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import { AvailableRasterLayers, RasterLayerPolygon } from "./RasterLayers"
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
import ShowDataLayer from "../UI/Map/ShowDataLayer"
import TitleHandler from "../Logic/Actors/TitleHandler"
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor"
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader"
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater"
import { BBox } from "../Logic/BBox"
import Constants from "./Constants"
import Hotkeys from "../UI/Base/Hotkeys"
import Translations from "../UI/i18n/Translations"
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
import { MenuState } from "./MenuState"
2023-04-14 17:53:08 +02:00
import MetaTagging from "../Logic/MetaTagging"
import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"
import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
2023-04-20 18:58:31 +02:00
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
2023-04-21 01:53:24 +02:00
import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer"
import { Utils } from "../Utils"
import { EliCategory } from "./RasterLayerProperties"
2023-03-28 05:13:48 +02:00
/**
*
* The themeviewState contains all the state needed for the themeViewGUI.
*
* This is pretty much the 'brain' or the HQ of MapComplete
*
* It ties up all the needed elements and starts some actors.
*/
export default class ThemeViewState implements SpecialVisualizationState {
readonly layout: LayoutConfig
readonly map: UIEventSource<MlMap>
readonly changes: Changes
readonly featureSwitches: FeatureSwitchState
readonly featureSwitchIsTesting: Store<boolean>
readonly featureSwitchUserbadge: Store<boolean>
readonly featureProperties: FeaturePropertiesStore
readonly osmConnection: OsmConnection
readonly selectedElement: UIEventSource<Feature>
readonly mapProperties: MapProperties & ExportableMap
readonly osmObjectDownloader: OsmObjectDownloader
2023-03-28 05:13:48 +02:00
readonly dataIsLoading: Store<boolean>
readonly guistate: MenuState
2023-03-28 05:13:48 +02:00
readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
2023-04-20 18:58:31 +02:00
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
readonly indexedFeatures: IndexedFeatureSource & LayoutSource
readonly newFeatures: WritableFeatureSource
2023-03-28 05:13:48 +02:00
readonly layerState: LayerState
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
readonly availableLayers: Store<RasterLayerPolygon[]>
readonly selectedLayer: UIEventSource<LayerConfig>
readonly userRelatedState: UserRelatedState
readonly geolocation: GeoLocationHandler
readonly lastClickObject: WritableFeatureSource
2023-04-21 01:53:24 +02:00
readonly overlayLayerStates: ReadonlyMap<
string,
{ readonly isDisplayed: UIEventSource<boolean> }
>
2023-03-28 05:13:48 +02:00
constructor(layout: LayoutConfig) {
this.layout = layout
this.guistate = new MenuState(layout.id)
2023-03-28 05:13:48 +02:00
this.map = new UIEventSource<MlMap>(undefined)
const initial = new InitialMapPositioning(layout)
this.mapProperties = new MapLibreAdaptor(this.map, initial)
const geolocationState = new GeoLocationState()
this.featureSwitches = new FeatureSwitchState(layout)
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchUserbadge
this.osmConnection = new OsmConnection({
dryRun: this.featureSwitches.featureSwitchIsTesting,
fakeUser: this.featureSwitches.featureSwitchFakeUser.data,
oauth_token: QueryParameters.GetQueryParameter(
"oauth_token",
undefined,
"Used to complete the login"
),
osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data,
})
2023-04-14 17:53:08 +02:00
this.userRelatedState = new UserRelatedState(
this.osmConnection,
layout?.language,
layout,
this.featureSwitches
)
2023-03-28 05:13:48 +02:00
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element")
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer")
this.geolocation = new GeoLocationHandler(
geolocationState,
this.selectedElement,
this.mapProperties,
this.userRelatedState.gpsLocationHistoryRetentionTime
)
2023-03-28 05:13:48 +02:00
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
const self = this
2023-03-28 05:13:48 +02:00
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
2023-04-21 01:53:24 +02:00
{
const overlayLayerStates = new Map<string, { isDisplayed: UIEventSource<boolean> }>()
for (const rasterInfo of this.layout.tileLayerSources) {
const isDisplayed = QueryParameters.GetBooleanQueryParameter(
"overlay-" + rasterInfo.id,
rasterInfo.defaultState ?? true,
"Wether or not overlayer layer " + rasterInfo.id + " is shown"
)
const state = { isDisplayed }
overlayLayerStates.set(rasterInfo.id, state)
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state)
}
this.overlayLayerStates = overlayLayerStates
}
{
/* Setup the layout source
* A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too
*/
const layoutSource = new LayoutSource(
layout.layers,
this.featureSwitches,
this.mapProperties,
this.osmConnection.Backend(),
(id) => self.layerState.filteredLayers.get(id).isDisplayed
)
this.indexedFeatures = layoutSource
this.dataIsLoading = layoutSource.isLoading
const indexedElements = this.indexedFeatures
this.featureProperties = new FeaturePropertiesStore(indexedElements)
this.changes = new Changes(
{
dryRun: this.featureSwitches.featureSwitchIsTesting,
allElements: indexedElements,
featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations,
},
layout?.isLeftRightSensitive() ?? false
)
2023-04-20 18:58:31 +02:00
this.historicalUserLocations = this.geolocation.historicalUserLocations
this.newFeatures = new NewGeometryFromChangesFeatureSource(
this.changes,
indexedElements,
this.osmConnection.Backend()
)
layoutSource.addSource(this.newFeatures)
2023-03-28 05:13:48 +02:00
const perLayer = new PerLayerFeatureSourceSplitter(
Array.from(this.layerState.filteredLayers.values()).filter(
(l) => l.layerDef?.source !== null
),
new ChangeGeometryApplicator(this.indexedFeatures, this.changes),
{
constructStore: (features, layer) =>
new GeoIndexedStoreForLayer(features, layer),
handleLeftovers: (features) => {
console.warn(
"Got ",
features.length,
"leftover features, such as",
features[0].properties
)
},
}
)
this.perLayer = perLayer.perLayer
}
2023-03-28 05:13:48 +02:00
this.perLayer.forEach((fs) => {
/* TODO enable new SaveFeatureSourceToLocalStorage(
this.osmConnection.Backend(),
2023-04-18 23:44:49 +02:00
fs.layer.layerDef.id,
15,
fs,
this.featureProperties
)//*/
2023-03-28 05:13:48 +02:00
const filtered = new FilteringFeatureSource(
fs.layer,
fs,
(id) => this.featureProperties.getStore(id),
this.layerState.globalFilters
)
const doShowLayer = this.mapProperties.zoom.map(
(z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
[fs.layer.isDisplayed]
)
2023-03-28 05:13:48 +02:00
new ShowDataLayer(this.map, {
layer: fs.layer.layerDef,
features: filtered,
doShowLayer,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
fetchStore: (id) => this.featureProperties.getStore(id),
})
})
const lastClick = (this.lastClickObject = new LastClickFeatureSource(
this.mapProperties.lastClickLocation,
this.layout
))
2023-03-28 05:13:48 +02:00
2023-04-20 18:58:31 +02:00
this.osmObjectDownloader = new OsmObjectDownloader(
this.osmConnection.Backend(),
this.changes
)
2023-03-28 05:13:48 +02:00
this.initActors()
this.drawSpecialLayers(lastClick)
2023-03-28 05:13:48 +02:00
this.initHotkeys()
this.miscSetup()
console.log("State setup completed", this)
2023-03-28 05:13:48 +02:00
}
/**
* Various small methods that need to be called
*/
private miscSetup() {
this.userRelatedState.markLayoutAsVisited(this.layout)
this.selectedElement.addCallbackAndRunD((feature) => {
// As soon as we have a selected element, we clear the selected element
// This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature
// The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear
if (feature.properties.id === "last_click") {
return
}
this.lastClickObject.features.setData([])
})
if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) {
Utils.LoadCustomCss(this.layout.customCss)
}
2023-03-28 05:13:48 +02:00
}
private initHotkeys() {
Hotkeys.RegisterHotkey(
{ nomod: "Escape", onUp: true },
Translations.t.hotkeyDocumentation.closeSidebar,
() => {
this.selectedElement.setData(undefined)
this.guistate.closeAll()
}
)
Hotkeys.RegisterHotkey(
{
nomod: "b",
},
Translations.t.hotkeyDocumentation.openLayersPanel,
() => {
if (this.featureSwitches.featureSwitchFilter.data) {
this.guistate.openFilterView()
}
}
)
Hotkeys.RegisterHotkey(
{ shift: "O" },
Translations.t.hotkeyDocumentation.selectMapnik,
() => {
this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto)
}
)
const self = this
function setLayerCategory(category: EliCategory) {
const available = self.availableLayers.data
const matchingCategoryLayers = available.filter(
(l) => l.properties.category === category
)
const best =
matchingCategoryLayers.find((l) => l.properties.best) ?? matchingCategoryLayers[0]
const rasterLayer = self.mapProperties.rasterLayer
if (rasterLayer.data !== best) {
rasterLayer.setData(best)
return
}
// The current layer is already selected...
// We switch the layers again
if (category === "osmbasedmap") {
rasterLayer.setData(undefined)
} else {
// search the _second_ best layer
const secondbest = matchingCategoryLayers.find(
(l) => l.properties.best && l !== best
)
const secondNotBest = matchingCategoryLayers.find((l) => l !== best)
rasterLayer.setData(secondbest ?? secondNotBest)
}
}
Hotkeys.RegisterHotkey(
{ nomod: "O" },
Translations.t.hotkeyDocumentation.selectOsmbasedmap,
() => setLayerCategory("osmbasedmap")
)
Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () =>
setLayerCategory("map")
)
Hotkeys.RegisterHotkey(
{ nomod: "P" },
Translations.t.hotkeyDocumentation.selectAerial,
() => setLayerCategory("photo")
)
2023-03-28 05:13:48 +02:00
}
/**
* Add the special layers to the map
*/
private drawSpecialLayers(last_click: LastClickFeatureSource) {
2023-03-28 05:13:48 +02:00
type AddedByDefaultTypes = typeof Constants.added_by_default[number]
const empty = []
{
// The last_click gets a _very_ special treatment
const last_click_layer = this.layerState.filteredLayers.get("last_click")
2023-04-20 18:58:31 +02:00
this.featureProperties.trackFeatureSource(last_click)
this.indexedFeatures.addSource(last_click)
new ShowDataLayer(this.map, {
features: new FilteringFeatureSource(last_click_layer, last_click),
doShowLayer: new ImmutableStore(true),
layer: last_click_layer.layerDef,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
onClick: (feature: Feature) => {
if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) {
this.map.data.flyTo({
zoom: Constants.minZoomLevelToAddNewPoint,
center: this.mapProperties.lastClickLocation.data,
})
return
}
// We first clear the selection to make sure no weird state is around
this.selectedLayer.setData(undefined)
this.selectedElement.setData(undefined)
this.selectedElement.setData(feature)
this.selectedLayer.setData(last_click_layer.layerDef)
},
})
}
2023-03-28 05:13:48 +02:00
/**
* A listing which maps the layerId onto the featureSource
*/
const specialLayers: Record<
Exclude<AddedByDefaultTypes, "last_click"> | "current_view",
FeatureSource
> = {
2023-03-28 05:13:48 +02:00
home_location: this.userRelatedState.homeLocation,
gps_location: this.geolocation.currentUserLocation,
gps_location_history: this.geolocation.historicalUserLocations,
gps_track: this.geolocation.historicalUserLocationsTrack,
selected_element: new StaticFeatureSource(
this.selectedElement.map((f) => (f === undefined ? empty : [f]))
),
range: new StaticFeatureSource(
this.mapProperties.maxbounds.map((bbox) =>
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })]
)
),
current_view: new StaticFeatureSource(
this.mapProperties.bounds.map((bbox) =>
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "current_view" })]
)
),
}
if (this.layout?.lockLocation) {
const bbox = new BBox(this.layout.lockLocation)
this.mapProperties.maxbounds.setData(bbox)
ShowDataLayer.showRange(
this.map,
new StaticFeatureSource([bbox.asGeoJson({})]),
this.featureSwitches.featureSwitchIsTesting
)
}
this.layerState.filteredLayers
.get("range")
?.isDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
// The following layers are _not_ indexed; they trigger to much and thus trigger the metatagging
const dontInclude = new Set(["gps_location", "gps_location_history", "gps_track"])
2023-03-28 05:13:48 +02:00
this.layerState.filteredLayers.forEach((flayer) => {
const id = flayer.layerDef.id
const features: FeatureSource = specialLayers[id]
2023-03-28 05:13:48 +02:00
if (features === undefined) {
return
}
2023-04-20 18:58:31 +02:00
if (!dontInclude.has(id)) {
this.featureProperties.trackFeatureSource(features)
this.indexedFeatures.addSource(features)
}
2023-03-28 05:13:48 +02:00
new ShowDataLayer(this.map, {
features,
doShowLayer: flayer.isDisplayed,
layer: flayer.layerDef,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
})
})
}
/**
* Setup various services for which no reference are needed
*/
private initActors() {
2023-04-14 17:53:08 +02:00
new MetaTagging(this)
new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this)
2023-03-28 05:13:48 +02:00
new ChangeToElementsActor(this.changes, this.featureProperties)
new PendingChangesUploader(this.changes, this.selectedElement)
2023-04-07 03:54:11 +02:00
new SelectedElementTagsUpdater(this)
2023-03-28 05:13:48 +02:00
}
}