forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			242 lines
		
	
	
		
			No EOL
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			242 lines
		
	
	
		
			No EOL
		
	
	
		
			10 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)
 | 
						|
        });
 | 
						|
        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
 | 
						|
 | 
						|
    }
 | 
						|
 | 
						|
} |