forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			305 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			305 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
 | 
						|
import { DefaultGuiState } from "./DefaultGuiState"
 | 
						|
import { FixedUiElement } from "./Base/FixedUiElement"
 | 
						|
import { Utils } from "../Utils"
 | 
						|
import Combine from "./Base/Combine"
 | 
						|
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"
 | 
						|
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
 | 
						|
import * as home_location_json from "../assets/layers/home_location/home_location.json"
 | 
						|
import State from "../State"
 | 
						|
import Title from "./Base/Title"
 | 
						|
import { MinimapObj } from "./Base/Minimap"
 | 
						|
import BaseUIElement from "./BaseUIElement"
 | 
						|
import { VariableUiElement } from "./Base/VariableUIElement"
 | 
						|
import { GeoOperations } from "../Logic/GeoOperations"
 | 
						|
import { OsmFeature } from "../Models/OsmFeature"
 | 
						|
import SearchAndGo from "./BigComponents/SearchAndGo"
 | 
						|
import FeatureInfoBox from "./Popup/FeatureInfoBox"
 | 
						|
import { UIEventSource } from "../Logic/UIEventSource"
 | 
						|
import LanguagePicker from "./LanguagePicker"
 | 
						|
import Lazy from "./Base/Lazy"
 | 
						|
import TagRenderingAnswer from "./Popup/TagRenderingAnswer"
 | 
						|
import Hash from "../Logic/Web/Hash"
 | 
						|
import FilterView from "./BigComponents/FilterView"
 | 
						|
import Translations from "./i18n/Translations"
 | 
						|
import Constants from "../Models/Constants"
 | 
						|
import SimpleAddUI from "./BigComponents/SimpleAddUI"
 | 
						|
import BackToIndex from "./BigComponents/BackToIndex"
 | 
						|
import StatisticsPanel from "./BigComponents/StatisticsPanel"
 | 
						|
 | 
						|
export default class DashboardGui {
 | 
						|
    private readonly state: FeaturePipelineState
 | 
						|
    private readonly currentView: UIEventSource<{
 | 
						|
        title: string | BaseUIElement
 | 
						|
        contents: string | BaseUIElement
 | 
						|
    }> = new UIEventSource(undefined)
 | 
						|
 | 
						|
    constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
 | 
						|
        this.state = state
 | 
						|
    }
 | 
						|
 | 
						|
    private viewSelector(
 | 
						|
        shown: BaseUIElement,
 | 
						|
        title: string | BaseUIElement,
 | 
						|
        contents: string | BaseUIElement,
 | 
						|
        hash?: string
 | 
						|
    ): BaseUIElement {
 | 
						|
        const currentView = this.currentView
 | 
						|
        const v = { title, contents }
 | 
						|
        shown.SetClass("pl-1 pr-1 rounded-md")
 | 
						|
        shown.onClick(() => {
 | 
						|
            currentView.setData(v)
 | 
						|
        })
 | 
						|
        Hash.hash.addCallbackAndRunD((h) => {
 | 
						|
            if (h === hash) {
 | 
						|
                currentView.setData(v)
 | 
						|
            }
 | 
						|
        })
 | 
						|
        currentView.addCallbackAndRunD((cv) => {
 | 
						|
            if (cv == v) {
 | 
						|
                shown.SetClass("bg-unsubtle")
 | 
						|
                Hash.hash.setData(hash)
 | 
						|
            } else {
 | 
						|
                shown.RemoveClass("bg-unsubtle")
 | 
						|
            }
 | 
						|
        })
 | 
						|
        return shown
 | 
						|
    }
 | 
						|
 | 
						|
    private singleElementCache: Record<string, BaseUIElement> = {}
 | 
						|
 | 
						|
    private singleElementView(
 | 
						|
        element: OsmFeature,
 | 
						|
        layer: LayerConfig,
 | 
						|
        distance: number
 | 
						|
    ): BaseUIElement {
 | 
						|
        if (this.singleElementCache[element.properties.id] !== undefined) {
 | 
						|
            return this.singleElementCache[element.properties.id]
 | 
						|
        }
 | 
						|
        const tags = this.state.allElements.getEventSourceById(element.properties.id)
 | 
						|
        const title = new Combine([
 | 
						|
            new Title(new TagRenderingAnswer(tags, layer.title, this.state), 4),
 | 
						|
            distance < 900
 | 
						|
                ? Math.floor(distance) + "m away"
 | 
						|
                : Utils.Round(distance / 1000) + "km away",
 | 
						|
        ]).SetClass("flex justify-between")
 | 
						|
 | 
						|
        return (this.singleElementCache[element.properties.id] = this.viewSelector(
 | 
						|
            title,
 | 
						|
            new Lazy(() => FeatureInfoBox.GenerateTitleBar(tags, layer, this.state)),
 | 
						|
            new Lazy(() => FeatureInfoBox.GenerateContent(tags, layer, this.state))
 | 
						|
            //  element.properties.id
 | 
						|
        ))
 | 
						|
    }
 | 
						|
 | 
						|
    private mainElementsView(
 | 
						|
        elements: { element: OsmFeature; layer: LayerConfig; distance: number }[]
 | 
						|
    ): BaseUIElement {
 | 
						|
        const self = this
 | 
						|
        if (elements === undefined) {
 | 
						|
            return new FixedUiElement("Initializing")
 | 
						|
        }
 | 
						|
        if (elements.length == 0) {
 | 
						|
            return new FixedUiElement("No elements in view")
 | 
						|
        }
 | 
						|
        return new Combine(
 | 
						|
            elements.map((e) => self.singleElementView(e.element, e.layer, e.distance))
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    private documentationButtonFor(layerConfig: LayerConfig): BaseUIElement {
 | 
						|
        return this.viewSelector(
 | 
						|
            Translations.W(layerConfig.name?.Clone() ?? layerConfig.id),
 | 
						|
            new Combine(["Documentation about ", layerConfig.name?.Clone() ?? layerConfig.id]),
 | 
						|
            layerConfig.GenerateDocumentation([]),
 | 
						|
            "documentation-" + layerConfig.id
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    private allDocumentationButtons(): BaseUIElement {
 | 
						|
        const layers = this.state.layoutToUse.layers
 | 
						|
            .filter((l) => Constants.priviliged_layers.indexOf(l.id) < 0)
 | 
						|
            .filter((l) => !l.id.startsWith("note_import_"))
 | 
						|
 | 
						|
        if (layers.length === 1) {
 | 
						|
            return this.documentationButtonFor(layers[0])
 | 
						|
        }
 | 
						|
        return this.viewSelector(
 | 
						|
            new FixedUiElement("Documentation"),
 | 
						|
            "Documentation",
 | 
						|
            new Combine(layers.map((l) => this.documentationButtonFor(l).SetClass("flex flex-col")))
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    public setup(): void {
 | 
						|
        const state = this.state
 | 
						|
 | 
						|
        if (this.state.layoutToUse.customCss !== undefined) {
 | 
						|
            if (window.location.pathname.indexOf("index") >= 0) {
 | 
						|
                Utils.LoadCustomCss(this.state.layoutToUse.customCss)
 | 
						|
            }
 | 
						|
        }
 | 
						|
        const map = this.SetupMap()
 | 
						|
 | 
						|
        Utils.downloadJson("./service-worker-version")
 | 
						|
            .then((data) => console.log("Service worker", data))
 | 
						|
            .catch((_) => console.log("Service worker not active"))
 | 
						|
 | 
						|
        document.getElementById("centermessage").classList.add("hidden")
 | 
						|
 | 
						|
        const layers: Record<string, LayerConfig> = {}
 | 
						|
        for (const layer of state.layoutToUse.layers) {
 | 
						|
            layers[layer.id] = layer
 | 
						|
        }
 | 
						|
 | 
						|
        const self = this
 | 
						|
        const elementsInview = new UIEventSource<
 | 
						|
            {
 | 
						|
                distance: number
 | 
						|
                center: [number, number]
 | 
						|
                element: OsmFeature
 | 
						|
                layer: LayerConfig
 | 
						|
            }[]
 | 
						|
        >([])
 | 
						|
 | 
						|
        function update() {
 | 
						|
            const mapCenter = <[number, number]>[
 | 
						|
                self.state.locationControl.data.lon,
 | 
						|
                self.state.locationControl.data.lon,
 | 
						|
            ]
 | 
						|
            const elements = self.state.featurePipeline
 | 
						|
                .getAllVisibleElementsWithmeta(self.state.currentBounds.data)
 | 
						|
                .map((el) => {
 | 
						|
                    const distance = GeoOperations.distanceBetween(el.center, mapCenter)
 | 
						|
                    return { ...el, distance }
 | 
						|
                })
 | 
						|
            elements.sort((e0, e1) => e0.distance - e1.distance)
 | 
						|
            elementsInview.setData(elements)
 | 
						|
        }
 | 
						|
 | 
						|
        map.bounds.addCallbackAndRun(update)
 | 
						|
        state.featurePipeline.newDataLoadedSignal.addCallback(update)
 | 
						|
        state.filteredLayers.addCallbackAndRun((fls) => {
 | 
						|
            for (const fl of fls) {
 | 
						|
                fl.isDisplayed.addCallback(update)
 | 
						|
                fl.appliedFilters.addCallback(update)
 | 
						|
            }
 | 
						|
        })
 | 
						|
 | 
						|
        const filterView = new Lazy(() => {
 | 
						|
            return new FilterView(state.filteredLayers, state.overlayToggles, state)
 | 
						|
        })
 | 
						|
        const welcome = new Combine([
 | 
						|
            state.layoutToUse.description,
 | 
						|
            state.layoutToUse.descriptionTail,
 | 
						|
        ])
 | 
						|
        self.currentView.setData({ title: state.layoutToUse.title, contents: welcome })
 | 
						|
        const filterViewIsOpened = new UIEventSource(false)
 | 
						|
        filterViewIsOpened.addCallback((_) =>
 | 
						|
            self.currentView.setData({ title: "filters", contents: filterView })
 | 
						|
        )
 | 
						|
 | 
						|
        const newPointIsShown = new UIEventSource(false)
 | 
						|
        const addNewPoint = new SimpleAddUI(
 | 
						|
            new UIEventSource(true),
 | 
						|
            new UIEventSource(undefined),
 | 
						|
            filterViewIsOpened,
 | 
						|
            state,
 | 
						|
            state.locationControl
 | 
						|
        )
 | 
						|
        const addNewPointTitle = "Add a missing point"
 | 
						|
        this.currentView.addCallbackAndRunD((cv) => {
 | 
						|
            newPointIsShown.setData(cv.contents === addNewPoint)
 | 
						|
        })
 | 
						|
        newPointIsShown.addCallbackAndRun((isShown) => {
 | 
						|
            if (isShown) {
 | 
						|
                if (self.currentView.data.contents !== addNewPoint) {
 | 
						|
                    self.currentView.setData({ title: addNewPointTitle, contents: addNewPoint })
 | 
						|
                }
 | 
						|
            } else {
 | 
						|
                if (self.currentView.data.contents === addNewPoint) {
 | 
						|
                    self.currentView.setData(undefined)
 | 
						|
                }
 | 
						|
            }
 | 
						|
        })
 | 
						|
 | 
						|
        new Combine([
 | 
						|
            new Combine([
 | 
						|
                this.viewSelector(
 | 
						|
                    new Title(state.layoutToUse.title.Clone(), 2),
 | 
						|
                    state.layoutToUse.title.Clone(),
 | 
						|
                    welcome,
 | 
						|
                    "welcome"
 | 
						|
                ),
 | 
						|
                map.SetClass("w-full h-64 shrink-0 rounded-lg"),
 | 
						|
                new SearchAndGo(state),
 | 
						|
                this.viewSelector(
 | 
						|
                    new Title(
 | 
						|
                        new VariableUiElement(
 | 
						|
                            elementsInview.map(
 | 
						|
                                (elements) => "There are " + elements?.length + " elements in view"
 | 
						|
                            )
 | 
						|
                        )
 | 
						|
                    ),
 | 
						|
                    "Statistics",
 | 
						|
                    new StatisticsPanel(elementsInview, this.state),
 | 
						|
                    "statistics"
 | 
						|
                ),
 | 
						|
 | 
						|
                this.viewSelector(new FixedUiElement("Filter"), "Filters", filterView, "filters"),
 | 
						|
                this.viewSelector(
 | 
						|
                    new Combine(["Add a missing point"]),
 | 
						|
                    addNewPointTitle,
 | 
						|
                    addNewPoint
 | 
						|
                ),
 | 
						|
 | 
						|
                new VariableUiElement(
 | 
						|
                    elementsInview.map((elements) =>
 | 
						|
                        this.mainElementsView(elements).SetClass("block m-2")
 | 
						|
                    )
 | 
						|
                ).SetClass(
 | 
						|
                    "block shrink-2 overflow-x-auto h-full border-2 border-subtle rounded-lg"
 | 
						|
                ),
 | 
						|
                this.allDocumentationButtons(),
 | 
						|
                new LanguagePicker(Object.keys(state.layoutToUse.title.translations)).SetClass(
 | 
						|
                    "mt-2"
 | 
						|
                ),
 | 
						|
                new BackToIndex(),
 | 
						|
            ]).SetClass("w-1/2 lg:w-1/4 m-4 flex flex-col shrink-0 grow-0"),
 | 
						|
            new VariableUiElement(
 | 
						|
                this.currentView.map(({ title, contents }) => {
 | 
						|
                    return new Combine([
 | 
						|
                        new Title(Translations.W(title), 2).SetClass(
 | 
						|
                            "shrink-0 border-b-4 border-subtle"
 | 
						|
                        ),
 | 
						|
                        Translations.W(contents).SetClass("shrink-2 overflow-y-auto block"),
 | 
						|
                    ]).SetClass("flex flex-col h-full")
 | 
						|
                })
 | 
						|
            ).SetClass(
 | 
						|
                "w-1/2 lg:w-3/4 m-4 p-2 border-2 border-subtle rounded-xl m-4 ml-0 mr-8 shrink-0 grow-0"
 | 
						|
            ),
 | 
						|
        ])
 | 
						|
            .SetClass("flex h-full")
 | 
						|
            .AttachTo("leafletDiv")
 | 
						|
    }
 | 
						|
 | 
						|
    private SetupMap(): MinimapObj & BaseUIElement {
 | 
						|
        const state = this.state
 | 
						|
 | 
						|
        new ShowDataLayer({
 | 
						|
            leafletMap: state.leafletMap,
 | 
						|
            layerToShow: new LayerConfig(home_location_json, "home_location", true),
 | 
						|
            features: state.homeLocation,
 | 
						|
            state,
 | 
						|
        })
 | 
						|
 | 
						|
        state.leafletMap.addCallbackAndRunD((_) => {
 | 
						|
            // Lets assume that all showDataLayers are initialized at this point
 | 
						|
            state.selectedElement.ping()
 | 
						|
            State.state.locationControl.ping()
 | 
						|
            return true
 | 
						|
        })
 | 
						|
 | 
						|
        return state.mainMapObject
 | 
						|
    }
 | 
						|
}
 |