forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			346 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			346 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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,
 | 
						|
    IndexedFeatureSource,
 | 
						|
    WritableFeatureSource,
 | 
						|
} from "../Logic/FeatureSource/FeatureSource"
 | 
						|
import { OsmConnection } from "../Logic/Osm/OsmConnection"
 | 
						|
import { MapProperties } from "./MapProperties"
 | 
						|
import LayerState from "../Logic/State/LayerState"
 | 
						|
import { Feature } from "geojson"
 | 
						|
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 SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
 | 
						|
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 SimpleFeatureSource from "../Logic/FeatureSource/Sources/SimpleFeatureSource"
 | 
						|
import { MenuState } from "./MenuState"
 | 
						|
import MetaTagging from "../Logic/MetaTagging"
 | 
						|
 | 
						|
/**
 | 
						|
 *
 | 
						|
 * 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
 | 
						|
 | 
						|
    readonly dataIsLoading: Store<boolean> // TODO
 | 
						|
    readonly guistate: MenuState
 | 
						|
    readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
 | 
						|
 | 
						|
    readonly historicalUserLocations: WritableFeatureSource
 | 
						|
    readonly indexedFeatures: IndexedFeatureSource
 | 
						|
    readonly newFeatures: WritableFeatureSource
 | 
						|
    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
 | 
						|
    constructor(layout: LayoutConfig) {
 | 
						|
        this.layout = layout
 | 
						|
        this.guistate = new MenuState()
 | 
						|
        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,
 | 
						|
        })
 | 
						|
        this.userRelatedState = new UserRelatedState(
 | 
						|
            this.osmConnection,
 | 
						|
            layout?.language,
 | 
						|
            layout,
 | 
						|
            this.featureSwitches
 | 
						|
        )
 | 
						|
        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
 | 
						|
        )
 | 
						|
 | 
						|
        this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
 | 
						|
 | 
						|
        const self = this
 | 
						|
        this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
 | 
						|
        this.newFeatures = new SimpleFeatureSource(undefined)
 | 
						|
        const layoutSource = new LayoutSource(
 | 
						|
            layout.layers,
 | 
						|
            this.featureSwitches,
 | 
						|
            this.newFeatures,
 | 
						|
            this.mapProperties,
 | 
						|
            this.osmConnection.Backend(),
 | 
						|
            (id) => self.layerState.filteredLayers.get(id).isDisplayed
 | 
						|
        )
 | 
						|
        this.indexedFeatures = layoutSource
 | 
						|
        this.dataIsLoading = layoutSource.isLoading
 | 
						|
        const lastClick = (this.lastClickObject = new LastClickFeatureSource(
 | 
						|
            this.mapProperties.lastClickLocation,
 | 
						|
            this.layout
 | 
						|
        ))
 | 
						|
        const indexedElements = this.indexedFeatures
 | 
						|
        this.featureProperties = new FeaturePropertiesStore(indexedElements)
 | 
						|
        const perLayer = new PerLayerFeatureSourceSplitter(
 | 
						|
            Array.from(this.layerState.filteredLayers.values()).filter(
 | 
						|
                (l) => l.layerDef?.source !== null
 | 
						|
            ),
 | 
						|
            indexedElements,
 | 
						|
            {
 | 
						|
                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
 | 
						|
 | 
						|
        this.perLayer.forEach((fs) => {
 | 
						|
            new SaveFeatureSourceToLocalStorage(fs.layer.layerDef.id, 15, fs)
 | 
						|
 | 
						|
            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]
 | 
						|
            )
 | 
						|
 | 
						|
            new ShowDataLayer(this.map, {
 | 
						|
                layer: fs.layer.layerDef,
 | 
						|
                features: filtered,
 | 
						|
                doShowLayer,
 | 
						|
                selectedElement: this.selectedElement,
 | 
						|
                selectedLayer: this.selectedLayer,
 | 
						|
                fetchStore: (id) => this.featureProperties.getStore(id),
 | 
						|
            })
 | 
						|
        })
 | 
						|
 | 
						|
        this.changes = new Changes(
 | 
						|
            {
 | 
						|
                dryRun: this.featureSwitches.featureSwitchIsTesting,
 | 
						|
                allElements: indexedElements,
 | 
						|
                featurePropertiesStore: this.featureProperties,
 | 
						|
                osmConnection: this.osmConnection,
 | 
						|
                historicalUserLocations: this.geolocation.historicalUserLocations,
 | 
						|
            },
 | 
						|
            layout?.isLeftRightSensitive() ?? false
 | 
						|
        )
 | 
						|
 | 
						|
        this.initActors()
 | 
						|
        this.drawSpecialLayers(lastClick)
 | 
						|
        this.initHotkeys()
 | 
						|
        this.miscSetup()
 | 
						|
        console.log("State setup completed", this)
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Various small methods that need to be called
 | 
						|
     */
 | 
						|
    private miscSetup() {
 | 
						|
        this.userRelatedState.markLayoutAsVisited(this.layout)
 | 
						|
 | 
						|
        this.selectedElement.addCallbackAndRunD(() => {
 | 
						|
            // As soon as we have a selected element, we clear it
 | 
						|
            // This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature
 | 
						|
            this.lastClickObject.features.setData([])
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
    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.state.backgroundLayer.setData(AvailableBaseLayers.osmCarto)
 | 
						|
            }
 | 
						|
        )//*/
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Add the special layers to the map
 | 
						|
     * @private
 | 
						|
     */
 | 
						|
    private drawSpecialLayers(last_click: LastClickFeatureSource) {
 | 
						|
        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")
 | 
						|
            this.featureProperties.addSpecial(
 | 
						|
                "last_click",
 | 
						|
                new UIEventSource<Record<string, string>>(last_click.properties)
 | 
						|
            )
 | 
						|
            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
 | 
						|
                    }
 | 
						|
                    this.selectedElement.setData(feature)
 | 
						|
                    this.selectedLayer.setData(last_click_layer.layerDef)
 | 
						|
                },
 | 
						|
            })
 | 
						|
        }
 | 
						|
 | 
						|
        /**
 | 
						|
         * A listing which maps the layerId onto the featureSource
 | 
						|
         */
 | 
						|
        const specialLayers: Record<
 | 
						|
            Exclude<AddedByDefaultTypes, "last_click"> | "current_view",
 | 
						|
            FeatureSource
 | 
						|
        > = {
 | 
						|
            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)
 | 
						|
 | 
						|
        this.layerState.filteredLayers.forEach((flayer) => {
 | 
						|
            const features = specialLayers[flayer.layerDef.id]
 | 
						|
            if (features === undefined) {
 | 
						|
                return
 | 
						|
            }
 | 
						|
            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() {
 | 
						|
        new MetaTagging(this)
 | 
						|
        new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this)
 | 
						|
        new ChangeToElementsActor(this.changes, this.featureProperties)
 | 
						|
        new PendingChangesUploader(this.changes, this.selectedElement)
 | 
						|
        new SelectedElementTagsUpdater(this)
 | 
						|
    }
 | 
						|
}
 |