From 2412828a69c41c3f9887de2d3177486f8636ec68 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 25 Jul 2022 18:55:15 +0200 Subject: [PATCH] Extract statistics panel, add statistics panel as special rendering --- Logic/FeatureSource/FeaturePipeline.ts | 61 +++++++++++- UI/BigComponents/StatisticsPanel.ts | 51 ++++++++++ UI/DashboardGui.ts | 130 +++---------------------- UI/LanguagePicker.ts | 1 + UI/SpecialVisualizations.ts | 34 ++++++- Utils.ts | 8 ++ assets/themes/onwheels/onwheels.json | 29 +++++- css/index-tailwind-output.css | 22 +++-- 8 files changed, 204 insertions(+), 132 deletions(-) create mode 100644 UI/BigComponents/StatisticsPanel.ts diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index 92c3c9999..09f88853e 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -23,8 +23,11 @@ import TileFreshnessCalculator from "./TileFreshnessCalculator"; import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource"; import MapState from "../State/MapState"; import {ElementStorage} from "../ElementStorage"; -import {Feature, Geometry} from "@turf/turf"; import {OsmFeature} from "../../Models/OsmFeature"; +import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; +import {FilterState} from "../../Models/FilteredLayer"; +import {GeoOperations} from "../GeoOperations"; +import {Utils} from "../../Utils"; /** @@ -514,6 +517,62 @@ export default class FeaturePipeline { return updater; } + /** + * Builds upon 'GetAllFeaturesAndMetaWithin', but does stricter BBOX-checking and applies the filters + */ + public getAllVisibleElementsWithmeta(bbox: BBox): { center: [number, number], element: OsmFeature, layer: LayerConfig }[] { + if (bbox === undefined) { + console.warn("No bbox") + return [] + } + + const layers = Utils.toIdRecord(this.state.layoutToUse.layers) + const elementsWithMeta: { features: OsmFeature[], layer: string }[] = this.GetAllFeaturesAndMetaWithin(bbox) + + let elements: {center: [number, number], element: OsmFeature, layer: LayerConfig }[] = [] + let seenElements = new Set() + for (const elementsWithMetaElement of elementsWithMeta) { + const layer = layers[elementsWithMetaElement.layer] + if(layer.title === undefined){ + continue + } + const filtered = this.state.filteredLayers.data.find(fl => fl.layerDef == layer); + for (let i = 0; i < elementsWithMetaElement.features.length; i++) { + const element = elementsWithMetaElement.features[i]; + if (!filtered.isDisplayed.data) { + continue + } + if (seenElements.has(element.properties.id)) { + continue + } + seenElements.add(element.properties.id) + if (!bbox.overlapsWith(BBox.get(element))) { + continue + } + if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) { + continue + } + const activeFilters: FilterState[] = Array.from(filtered.appliedFilters.data.values()); + if (!activeFilters.every(filter => filter?.currentFilter === undefined || filter?.currentFilter?.matchesProperties(element.properties))) { + continue + } + const center = GeoOperations.centerpointCoordinates(element); + elements.push({ + element, + center, + layer: layers[elementsWithMetaElement.layer], + }) + + } + } + + + + + return elements; + } + + /** * Inject a new point */ diff --git a/UI/BigComponents/StatisticsPanel.ts b/UI/BigComponents/StatisticsPanel.ts new file mode 100644 index 000000000..f1e6e98b7 --- /dev/null +++ b/UI/BigComponents/StatisticsPanel.ts @@ -0,0 +1,51 @@ +import {VariableUiElement} from "../Base/VariableUIElement"; +import Loading from "../Base/Loading"; +import Title from "../Base/Title"; +import TagRenderingChart from "./TagRenderingChart"; +import Combine from "../Base/Combine"; +import Locale from "../i18n/Locale"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {OsmFeature} from "../../Models/OsmFeature"; +import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; + +export default class StatisticsPanel extends VariableUiElement { + constructor(elementsInview: UIEventSource<{ element: OsmFeature, layer: LayerConfig }[]>, state: { + layoutToUse: LayoutConfig + }) { + super(elementsInview.stabilized(1000).map(features => { + if (features === undefined) { + return new Loading("Loading data") + } + if (features.length === 0) { + return "No elements in view" + } + const els = [] + for (const layer of state.layoutToUse.layers) { + if(layer.name === undefined){ + continue + } + const featuresForLayer = features.filter(f => f.layer === layer).map(f => f.element) + if(featuresForLayer.length === 0){ + continue + } + els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8")) + + const layerStats = [] + for (const tagRendering of (layer?.tagRenderings ?? [])) { + const chart = new TagRenderingChart(featuresForLayer, tagRendering, { + chartclasses: "w-full", + chartstyle: "height: 60rem", + includeTitle: false + }) + const title = new Title(tagRendering.question?.Clone() ?? tagRendering.id, 4).SetClass("mt-8") + if(!chart.HasClass("hidden")){ + layerStats.push(new Combine([title, chart]).SetClass("flex flex-col w-full lg:w-1/3")) + } + } + els.push(new Combine(layerStats).SetClass("flex flex-wrap")) + } + return new Combine(els) + }, [Locale.language])); + } +} \ No newline at end of file diff --git a/UI/DashboardGui.ts b/UI/DashboardGui.ts index 02dfddd20..d657f9667 100644 --- a/UI/DashboardGui.ts +++ b/UI/DashboardGui.ts @@ -12,7 +12,6 @@ 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"; @@ -22,14 +21,11 @@ import Lazy from "./Base/Lazy"; import TagRenderingAnswer from "./Popup/TagRenderingAnswer"; import Hash from "../Logic/Web/Hash"; import FilterView from "./BigComponents/FilterView"; -import {FilterState} from "../Models/FilteredLayer"; import Translations from "./i18n/Translations"; import Constants from "../Models/Constants"; import SimpleAddUI from "./BigComponents/SimpleAddUI"; -import TagRenderingChart from "./BigComponents/TagRenderingChart"; -import Loading from "./Base/Loading"; import BackToIndex from "./BigComponents/BackToIndex"; -import Locale from "./i18n/Locale"; +import StatisticsPanel from "./BigComponents/StatisticsPanel"; export default class DashboardGui { @@ -94,63 +90,7 @@ export default class DashboardGui { return new Combine(elements.map(e => self.singleElementView(e.element, e.layer, e.distance))) } - private visibleElements(map: MinimapObj & BaseUIElement, layers: Record): { distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[] { - const bbox = map.bounds.data - if (bbox === undefined) { - console.warn("No bbox") - return undefined - } - const location = map.location.data; - const loc: [number, number] = [location.lon, location.lat] - - const elementsWithMeta: { features: OsmFeature[], layer: string }[] = this.state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox) - - let elements: { distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[] = [] - let seenElements = new Set() - for (const elementsWithMetaElement of elementsWithMeta) { - const layer = layers[elementsWithMetaElement.layer] - if(layer.title === undefined){ - continue - } - const filtered = this.state.filteredLayers.data.find(fl => fl.layerDef == layer); - for (let i = 0; i < elementsWithMetaElement.features.length; i++) { - const element = elementsWithMetaElement.features[i]; - if (!filtered.isDisplayed.data) { - continue - } - if (seenElements.has(element.properties.id)) { - continue - } - seenElements.add(element.properties.id) - if (!bbox.overlapsWith(BBox.get(element))) { - continue - } - if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) { - continue - } - const activeFilters: FilterState[] = Array.from(filtered.appliedFilters.data.values()); - if (!activeFilters.every(filter => filter?.currentFilter === undefined || filter?.currentFilter?.matchesProperties(element.properties))) { - continue - } - 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; - } - - private documentationButtonFor(layerConfig: LayerConfig): BaseUIElement { + 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) @@ -166,6 +106,7 @@ export default class DashboardGui { return this.viewSelector(new FixedUiElement("Documentation"), "Documentation", new Combine(layers.map(l => this.documentationButtonFor(l).SetClass("flex flex-col")))) } + public setup(): void { @@ -191,7 +132,14 @@ export default class DashboardGui { const elementsInview = new UIEventSource<{ distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[]>([]); function update() { - elementsInview.setData(self.visibleElements(map, layers)) + 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) @@ -235,60 +183,6 @@ export default class DashboardGui { } }) - const statistics = - new VariableUiElement(elementsInview.stabilized(1000).map(features => { - if (features === undefined) { - return new Loading("Loading data") - } - if (features.length === 0) { - return "No elements in view" - } - const els = [] - for (const layer of state.layoutToUse.layers) { - if(layer.name === undefined){ - continue - } - const featuresForLayer = features.filter(f => f.layer === layer).map(f => f.element) - if(featuresForLayer.length === 0){ - continue - } - els.push(new Title(layer.name)) - - const layerStats = [] - for (const tagRendering of (layer?.tagRenderings ?? [])) { - const chart = new TagRenderingChart(featuresForLayer, tagRendering, { - chartclasses: "w-full", - chartstyle: "height: 60rem", - includeTitle: false - }) - const full = new Lazy(() => - new TagRenderingChart(featuresForLayer, tagRendering, { - chartstyle: "max-height: calc(100vh - 10rem)", - groupToOtherCutoff: 0 - }) - ) - const title = new Title(tagRendering.question?.Clone() ?? tagRendering.id) - title.onClick(() => { - const current = self.currentView.data - full.onClick(() => { - self.currentView.setData(current) - }) - self.currentView.setData( - { - title: new Title(tagRendering.question.Clone() ?? tagRendering.id), - contents: full - }) - } - ) - if(!chart.HasClass("hidden")){ - layerStats.push(new Combine([title, chart]).SetClass("flex flex-col w-full lg:w-1/3")) - } - } - els.push(new Combine(layerStats).SetClass("flex flex-wrap")) - } - return new Combine(els) - }, [Locale.language])) - new Combine([ new Combine([ @@ -298,7 +192,7 @@ export default class DashboardGui { this.viewSelector(new Title( new VariableUiElement(elementsInview.map(elements => "There are " + elements?.length + " elements in view"))), "Statistics", - statistics, "statistics"), + new StatisticsPanel(elementsInview, this.state), "statistics"), this.viewSelector(new FixedUiElement("Filter"), "Filters", filterView, "filters"), diff --git a/UI/LanguagePicker.ts b/UI/LanguagePicker.ts index 608468816..60c8eae58 100644 --- a/UI/LanguagePicker.ts +++ b/UI/LanguagePicker.ts @@ -16,6 +16,7 @@ export default class LanguagePicker extends Toggle { if (languages === undefined || languages.length <= 1) { + super(undefined,undefined,undefined) return undefined; } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index ea538f4ab..4d70c850c 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -57,7 +57,8 @@ import {SaveButton} from "./Popup/SaveButton"; import {MapillaryLink} from "./BigComponents/MapillaryLink"; import {CheckBox} from "./Input/Checkboxes"; import Slider from "./Input/Slider"; -import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; +import {OsmFeature} from "../Models/OsmFeature"; +import StatisticsPanel from "./BigComponents/StatisticsPanel"; export interface SpecialVisualization { funcName: string, @@ -1098,7 +1099,36 @@ export default class SpecialVisualizations { })) }, new NearbyImageVis(), - new MapillaryLinkVis() + new MapillaryLinkVis(), + { + funcName: "statistics", + docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer", + args: [], + constr : (state, tagsSource, args, guiState) => { + const elementsInview = new UIEventSource<{ distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[]>([]); + function update() { + const mapCenter = <[number,number]> [state.locationControl.data.lon, state.locationControl.data.lon] + const bbox = state.currentBounds.data + const elements = state.featurePipeline.getAllVisibleElementsWithmeta(bbox).map(el => { + const distance = GeoOperations.distanceBetween(el.center, mapCenter) + return {...el, distance } + }) + elements.sort((e0, e1) => e0.distance - e1.distance) + elementsInview.setData(elements) + + } + + state.currentBounds.addCallbackAndRun(update) + state.featurePipeline.newDataLoadedSignal.addCallback(update); + state.filteredLayers.addCallbackAndRun(fls => { + for (const fl of fls) { + fl.isDisplayed.addCallback(update) + fl.appliedFilters.addCallback(update) + } + }) + return new StatisticsPanel(elementsInview, state) + } + } ] specialVisualizations.push(new AutoApplyButton(specialVisualizations)) diff --git a/Utils.ts b/Utils.ts index 0da5a205e..370e76f5f 100644 --- a/Utils.ts +++ b/Utils.ts @@ -1031,5 +1031,13 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be private static colorDiff(c0: { r: number, g: number, b: number }, c1: { r: number, g: number, b: number }) { return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b); } + + static toIdRecord(ts: T[]): Record { + const result : Record = {} + for (const t of ts) { + result[t.id] = t + } + return result + } } diff --git a/assets/themes/onwheels/onwheels.json b/assets/themes/onwheels/onwheels.json index 2a822fe20..15a0637da 100644 --- a/assets/themes/onwheels/onwheels.json +++ b/assets/themes/onwheels/onwheels.json @@ -176,6 +176,30 @@ } ] } + }, + { + "builtin": "current_view", + "override": { + "+mapRendering": [ + { + "location": [ + "point", "centroid" + ], + "icon": "statistics" + } + ], + "=title": { + "render": { + "en": "Statistics" + } + }, + "tagRenderings": [ + { + "id": "stats", + "render": "{statistics()}" + } + ] + } } ], "overrideAll": { @@ -190,13 +214,14 @@ "condition": { "and": [ "entrance=", - "kerb=" + "kerb=", + "current_view!=yes" ] }, "render": { "en": "This door has a width of {canonical(_poi_entrance:width)} meters", "nl": "Deze deur heeft een breedte van {canonical(_poi_entrance:width)} meter", - "de": "Diese Tür hat eine Durchgangsbreite von {canonical(_poi_entrance:width)} Meter", + "de": "Diese Tür hat eine Drchgangsbreite von {canonical(_poi_entrance:width)} Meter", "es": "Esta puerta tiene una ancho de {canonical(_poi_entrance:width)} metros" }, "freeform": { diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index d242eee6c..5b3eb4abf 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -866,6 +866,14 @@ video { margin-bottom: 1rem; } +.mt-8 { + margin-top: 2rem; +} + +.mt-4 { + margin-top: 1rem; +} + .mt-2 { margin-top: 0.5rem; } @@ -878,10 +886,6 @@ video { margin-right: 2rem; } -.mt-4 { - margin-top: 1rem; -} - .mt-6 { margin-top: 1.5rem; } @@ -1515,11 +1519,6 @@ video { background-color: rgba(224, 231, 255, var(--tw-bg-opacity)); } -.bg-red-500 { - --tw-bg-opacity: 1; - background-color: rgba(239, 68, 68, var(--tw-bg-opacity)); -} - .bg-black { --tw-bg-opacity: 1; background-color: rgba(0, 0, 0, var(--tw-bg-opacity)); @@ -1540,6 +1539,11 @@ video { background-color: rgba(209, 213, 219, var(--tw-bg-opacity)); } +.bg-red-500 { + --tw-bg-opacity: 1; + background-color: rgba(239, 68, 68, var(--tw-bg-opacity)); +} + .bg-red-200 { --tw-bg-opacity: 1; background-color: rgba(254, 202, 202, var(--tw-bg-opacity));