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
 | |
| 
 | |
|     }
 | |
| 
 | |
| } |