From 812563ddc5567ea04c05d0f90c1527a51fe642a7 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 13 Jul 2022 17:58:01 +0200 Subject: [PATCH] Add first dashboard layout --- UI/Base/Toggleable.ts | 5 +- UI/DashboardGui.ts | 186 +++++++++++++++++++++++++++++++++++++ UI/DefaultGUI.ts | 7 +- UI/Popup/FeatureInfoBox.ts | 4 +- index.ts | 10 +- 5 files changed, 201 insertions(+), 11 deletions(-) create mode 100644 UI/DashboardGui.ts diff --git a/UI/Base/Toggleable.ts b/UI/Base/Toggleable.ts index 848fc80b6..5b884fa6b 100644 --- a/UI/Base/Toggleable.ts +++ b/UI/Base/Toggleable.ts @@ -26,7 +26,8 @@ export default class Toggleable extends Combine { public readonly isVisible = new UIEventSource(false) constructor(title: Title | Combine | BaseUIElement, content: BaseUIElement, options?: { - closeOnClick: true | boolean + closeOnClick?: true | boolean, + height?: "100vh" | string }) { super([title, content]) content.SetClass("animate-height border-l-4 pl-2 block") @@ -72,7 +73,7 @@ export default class Toggleable extends Combine { this.isVisible.addCallbackAndRun(isVisible => { if (isVisible) { - contentElement.style.maxHeight = "100vh" + contentElement.style.maxHeight = options?.height ?? "100vh" contentElement.style.overflowY = "auto" contentElement.style["-webkit-mask-image"] = "unset" } else { diff --git a/UI/DashboardGui.ts b/UI/DashboardGui.ts new file mode 100644 index 000000000..7fc5d7e3f --- /dev/null +++ b/UI/DashboardGui.ts @@ -0,0 +1,186 @@ +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 {BBox} from "../Logic/BBox"; +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"; + +export default class DashboardGui { + private readonly guiState: DefaultGuiState; + private readonly state: FeaturePipelineState; + private readonly currentView: UIEventSource = new UIEventSource("No selection") + + + constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { + this.state = state; + this.guiState = guiState; + + } + + private singleElementCache: Record = {} + + 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), + Math.floor(distance) + "m away" + ]).SetClass("flex"); + // FeatureInfoBox.GenerateTitleBar(tags, layer, this.state) + + const currentView = this.currentView + const info = new Lazy(() => new Combine([ + FeatureInfoBox.GenerateTitleBar(tags, layer, this.state), + FeatureInfoBox.GenerateContent(tags, layer, this.state)]).SetStyle("overflox-x: hidden")); + title.onClick(() => { + currentView.setData(info) + }) + + currentView.addCallbackAndRunD(cv => { + if (cv == info) { + title.SetClass("bg-blue-300") + } else { + title.RemoveClass("bg-blue-300") + } + }) + + return title; + } + + 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))) + } + + 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(e => console.log("Service worker not active")) + + document.getElementById("centermessage").classList.add("hidden") + + const layers: Record = {} + for (const layer of state.layoutToUse.layers) { + layers[layer.id] = layer; + } + + + const elementsInview = map.bounds.map(bbox => { + if (bbox === undefined) { + return undefined + } + const location = map.location.data; + const loc: [number, number] = [location.lon, location.lat] + + const elementsWithMeta: { features: OsmFeature[], layer: string }[] = state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox) + + let elements: { distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[] = [] + let seenElements = new Set() + for (const elementsWithMetaElement of elementsWithMeta) { + for (const element of elementsWithMetaElement.features) { + if (!bbox.overlapsWith(BBox.get(element))) { + continue + } + if (seenElements.has(element.properties.id)) { + continue + } + seenElements.add(element.properties.id) + const center = GeoOperations.centerpointCoordinates(element); + elements.push({ + element, + center, + layer: layers[elementsWithMetaElement.layer], + distance: GeoOperations.distanceBetween(loc, center) + }) + + } + } + + + elements.sort((e0, e1) => e0.distance - e1.distance) + + + return elements; + }, [this.state.featurePipeline.newDataLoadedSignal]); + + const welcome = new Combine([state.layoutToUse.description, state.layoutToUse.descriptionTail]) + const self = this; + self.currentView.setData(welcome) + new Combine([ + + new Combine([map.SetClass("w-full h-64"), + new Title(state.layoutToUse.title, 2).onClick(() => { + self.currentView.setData(welcome) + }), + new LanguagePicker(Object.keys(state.layoutToUse.title)), + new SearchAndGo(state), + new Title( + new VariableUiElement(elementsInview.map(elements => "There are " + elements?.length + " elements in view"))) + .onClick(() => self.currentView.setData("Statistics")), + new VariableUiElement(elementsInview.map(elements => this.mainElementsView(elements)))]) + .SetClass("w-1/2 m-4"), + new VariableUiElement(this.currentView).SetClass("w-1/2 h-full overflow-y-auto m-4") + ]).SetClass("flex h-full") + .AttachTo("leafletDiv") + + } + + private SetupElement() { + const t = new Title("Elements in view", 3) + + } + + private SetupMap(): MinimapObj & BaseUIElement { + const state = this.state; + const guiState = this.guiState; + + 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 + + } + +} \ No newline at end of file diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts index a14d38f25..9252faed3 100644 --- a/UI/DefaultGUI.ts +++ b/UI/DefaultGUI.ts @@ -44,14 +44,9 @@ export default class DefaultGUI { } public setup(){ - if (this.state.layoutToUse.customCss !== undefined) { - Utils.LoadCustomCss(this.state.layoutToUse.customCss); - } - this.SetupUIElements(); this.SetupMap() - if (this.state.layoutToUse.customCss !== undefined && window.location.pathname.indexOf("index") >= 0) { Utils.LoadCustomCss(this.state.layoutToUse.customCss) } @@ -144,7 +139,7 @@ export default class DefaultGUI { new ShowDataLayer({ leafletMap: state.leafletMap, - layerToShow: new LayerConfig(home_location_json, "all_known_layers", true), + layerToShow: new LayerConfig(home_location_json, "home_location", true), features: state.homeLocation, state }) diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index a277a7d7f..f3be3a279 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -46,7 +46,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { } - private static GenerateTitleBar(tags: UIEventSource, + public static GenerateTitleBar(tags: UIEventSource, layerConfig: LayerConfig, state: {}): BaseUIElement { const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI"), state) @@ -64,7 +64,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { ]) } - private static GenerateContent(tags: UIEventSource, + public static GenerateContent(tags: UIEventSource, layerConfig: LayerConfig, state: FeaturePipelineState): BaseUIElement { let questionBoxes: Map = new Map(); diff --git a/index.ts b/index.ts index 355eb5e93..3221d0c07 100644 --- a/index.ts +++ b/index.ts @@ -9,6 +9,8 @@ import DefaultGUI from "./UI/DefaultGUI"; import State from "./State"; import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation"; import {DefaultGuiState} from "./UI/DefaultGuiState"; +import {QueryParameters} from "./Logic/Web/QueryParameters"; +import DashboardGui from "./UI/DashboardGui"; // Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts running from console MinimapImplementation.initialize() @@ -36,7 +38,13 @@ class Init { // This 'leaks' the global state via the window object, useful for debugging // @ts-ignore window.mapcomplete_state = State.state; - new DefaultGUI(State.state, guiState).setup() + + const mode = QueryParameters.GetQueryParameter("mode", "map", "The mode the application starts in, e.g. 'map' or 'dashboard'") + if(mode.data === "dashboard"){ + new DashboardGui(State.state, guiState).setup() + }else{ + new DefaultGUI(State.state, guiState).setup() + } } }