From fcc49766d4eca1919759b8294c426e8c163c4374 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 24 Apr 2023 03:22:43 +0200 Subject: [PATCH] Refactoring: port statistics view --- Models/FilteredLayer.ts | 93 +++++++++++--------- UI/BigComponents/Filterview.svelte | 6 +- UI/BigComponents/FilterviewWithFields.svelte | 6 +- UI/BigComponents/TagRenderingChart.ts | 8 +- UI/StatisticsGUI.ts | 81 ++++++++--------- css/tabbedComponent.css | 74 ---------------- index.html | 2 - theme.html | 2 - 8 files changed, 103 insertions(+), 169 deletions(-) delete mode 100644 css/tabbedComponent.css diff --git a/Models/FilteredLayer.ts b/Models/FilteredLayer.ts index 9e4d83b31..de9348536 100644 --- a/Models/FilteredLayer.ts +++ b/Models/FilteredLayer.ts @@ -40,8 +40,17 @@ export default class FilteredLayer { ) { this.layerDef = layer this.isDisplayed = isDisplayed ?? new UIEventSource(true) - this.appliedFilters = - appliedFilters ?? new Map>() + if (!appliedFilters) { + const appliedFiltersWritable = new Map< + string, + UIEventSource + >() + for (const filter of this.layerDef.filters) { + appliedFiltersWritable.set(filter.id, new UIEventSource(undefined)) + } + appliedFilters = appliedFiltersWritable + } + this.appliedFilters = appliedFilters const self = this const currentTags = new UIEventSource(undefined) @@ -63,16 +72,6 @@ export default class FilteredLayer { return JSON.stringify(values) } - private static stringToFieldProperties(value: string): Record { - const values = JSON.parse(value) - for (const key in values) { - if (values[key] === "") { - delete values[key] - } - } - return values - } - /** * Creates a FilteredLayer which is tied into the QueryParameters and/or user preferences */ @@ -114,6 +113,16 @@ export default class FilteredLayer { return new FilteredLayer(layer, appliedFilters, isDisplayed) } + private static stringToFieldProperties(value: string): Record { + const values = JSON.parse(value) + for (const key in values) { + if (values[key] === "") { + delete values[key] + } + } + return values + } + private static fieldsToTags( option: FilterConfigOption, fieldstate: string | Record @@ -170,6 +179,36 @@ export default class FilteredLayer { this.appliedFilters.forEach((value) => value.setData(undefined)) } + /** + * Returns true if the given tags match the current filters (and the specified 'global filters') + */ + public isShown(properties: Record, globalFilters?: GlobalFilter[]): boolean { + if (properties._deleted === "yes") { + return false + } + { + const isShown: TagsFilter = this.layerDef.isShown + if (isShown !== undefined && !isShown.matchesProperties(properties)) { + return false + } + } + + { + let neededTags: TagsFilter = this.currentFilter.data + if (neededTags !== undefined && !neededTags.matchesProperties(properties)) { + return false + } + } + + for (const globalFilter of globalFilters ?? []) { + const neededTags = globalFilter.osmTags + if (neededTags !== undefined && !neededTags.matchesProperties(properties)) { + return false + } + } + return true + } + private calculateCurrentTags(): TagsFilter { let needed: TagsFilter[] = [] for (const filter of this.layerDef.filters) { @@ -209,34 +248,4 @@ export default class FilteredLayer { } return optimized } - - /** - * Returns true if the given tags match the current filters (and the specified 'global filters') - */ - public isShown(properties: Record, globalFilters?: GlobalFilter[]): boolean { - if (properties._deleted === "yes") { - return false - } - { - const isShown: TagsFilter = this.layerDef.isShown - if (isShown !== undefined && !isShown.matchesProperties(properties)) { - return false - } - } - - { - let neededTags: TagsFilter = this.currentFilter.data - if (neededTags !== undefined && !neededTags.matchesProperties(properties)) { - return false - } - } - - for (const globalFilter of globalFilters ?? []) { - const neededTags = globalFilter.osmTags - if (neededTags !== undefined && !neededTags.matchesProperties(properties)) { - return false - } - } - return true - } } diff --git a/UI/BigComponents/Filterview.svelte b/UI/BigComponents/Filterview.svelte index 172fea15d..a7f7a86c0 100644 --- a/UI/BigComponents/Filterview.svelte +++ b/UI/BigComponents/Filterview.svelte @@ -10,14 +10,14 @@ import type { Writable } from "svelte/store"; import If from "../Base/If.svelte"; import Dropdown from "../Base/Dropdown.svelte"; import { onDestroy } from "svelte"; -import { UIEventSource } from "../../Logic/UIEventSource"; +import { ImmutableStore, Store } from "../../Logic/UIEventSource"; import FilterviewWithFields from "./FilterviewWithFields.svelte"; import Tr from "../Base/Tr.svelte"; import Translations from "../i18n/Translations"; export let filteredLayer: FilteredLayer; -export let highlightedLayer: UIEventSource | undefined; -export let zoomlevel: UIEventSource; +export let highlightedLayer: Store = new ImmutableStore(undefined); +export let zoomlevel: Store = new ImmutableStore(22); let layer: LayerConfig = filteredLayer.layerDef; let isDisplayed: boolean = filteredLayer.isDisplayed.data; onDestroy(filteredLayer.isDisplayed.addCallbackAndRunD(d => { diff --git a/UI/BigComponents/FilterviewWithFields.svelte b/UI/BigComponents/FilterviewWithFields.svelte index 3d3706503..8e356eb2f 100644 --- a/UI/BigComponents/FilterviewWithFields.svelte +++ b/UI/BigComponents/FilterviewWithFields.svelte @@ -17,7 +17,7 @@ let fieldValues: Record> = {}; let fieldTypes: Record = {}; let appliedFilter = >filteredLayer.appliedFilters.get(id); - let initialState: Record = JSON.parse(appliedFilter.data ?? "{}"); + let initialState: Record = JSON.parse(appliedFilter?.data ?? "{}"); function setFields() { const properties: Record = {}; @@ -30,7 +30,7 @@ properties[k] = v; } } - appliedFilter.setData(FilteredLayer.fieldsToString(properties)); + appliedFilter?.setData(FilteredLayer.fieldsToString(properties)); } for (const field of option.fields) { @@ -38,7 +38,7 @@ fieldTypes[field.name + "}"] = field.type; const src = new UIEventSource(initialState[field.name] ?? ""); fieldValues[field.name + "}"] = src; - onDestroy(src.addCallback(() => { + onDestroy(src.stabilized(200).addCallback(() => { setFields(); })); } diff --git a/UI/BigComponents/TagRenderingChart.ts b/UI/BigComponents/TagRenderingChart.ts index 32a7371a1..c9b391c0b 100644 --- a/UI/BigComponents/TagRenderingChart.ts +++ b/UI/BigComponents/TagRenderingChart.ts @@ -25,7 +25,13 @@ export class StackedRenderingChart extends ChartJs { groupToOtherCutoff: options?.groupToOtherCutoff, }) if (labels === undefined || data === undefined) { - console.error("Could not extract data and labels for ", tr, " with features", features) + console.error( + "Could not extract data and labels for ", + tr, + " with features", + features, + ": no labels or no data" + ) throw "No labels or data given..." } // labels: ["cyclofix", "buurtnatuur", ...]; data : [ ["cyclofix-changeset", "cyclofix-changeset", ...], ["buurtnatuur-cs", "buurtnatuur-cs"], ... ] diff --git a/UI/StatisticsGUI.ts b/UI/StatisticsGUI.ts index 4bd947ebd..1e43a2a91 100644 --- a/UI/StatisticsGUI.ts +++ b/UI/StatisticsGUI.ts @@ -7,24 +7,25 @@ import Loading from "./Base/Loading" import { Utils } from "../Utils" import Combine from "./Base/Combine" import { StackedRenderingChart } from "./BigComponents/TagRenderingChart" -import { LayerFilterPanel } from "./BigComponents/FilterView" -import MapState from "../Logic/State/MapState" import BaseUIElement from "./BaseUIElement" import Title from "./Base/Title" import { FixedUiElement } from "./Base/FixedUiElement" import List from "./Base/List" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" import mcChanges from "../assets/generated/themes/mapcomplete-changes.json" +import SvelteUIElement from "./Base/SvelteUIElement" +import Filterview from "./BigComponents/Filterview.svelte" +import FilteredLayer from "../Models/FilteredLayer" + class StatisticsForOverviewFile extends Combine { constructor(homeUrl: string, paths: string[]) { paths = paths.filter((p) => !p.endsWith("file-overview.json")) const layer = new LayoutConfig(mcChanges, true).layers[0] - const filteredLayer = MapState.InitializeFilteredLayers( - { id: "statistics-view", layers: [layer] }, - undefined - )[0] - const filterPanel = new LayerFilterPanel(undefined, filteredLayer) - const appliedFilters = filteredLayer.appliedFilters + const filteredLayer = new FilteredLayer(layer) + const filterPanel = new Combine([ + new Title("Filters"), + new SvelteUIElement(Filterview, { filteredLayer }), + ]) const downloaded = new UIEventSource<{ features: ChangeSetData[] }[]>([]) @@ -63,20 +64,10 @@ class StatisticsForOverviewFile extends Combine { return loading } - let overview = ChangesetsOverview.fromDirtyData( + const overview = ChangesetsOverview.fromDirtyData( [].concat(...downloaded.map((d) => d.features)) - ) - if (appliedFilters.data.size > 0) { - appliedFilters.data.forEach((filterSpec) => { - const tf = filterSpec?.currentFilter - if (tf === undefined) { - return - } - overview = overview.filter((cs) => - tf.matchesProperties(cs.properties) - ) - }) - } + ).filter((cs) => filteredLayer.isShown(cs.properties)) + console.log("Overview is", overview) if (overview._meta.length === 0) { return "No data matched the filter" @@ -143,6 +134,10 @@ class StatisticsForOverviewFile extends Combine { new Title("Breakdown"), ] for (const tr of trs) { + if (tr.question === undefined) { + continue + } + console.log(tr) let total = undefined if (tr.freeform?.key !== undefined) { total = new Set( @@ -174,7 +169,7 @@ class StatisticsForOverviewFile extends Combine { return new Combine(elements) }, - [appliedFilters] + [filteredLayer.currentFilter] ) ).SetClass("block w-full h-full"), ]) @@ -232,30 +227,12 @@ class ChangesetsOverview { } public readonly _meta: ChangeSetData[] - public static fromDirtyData(meta: ChangeSetData[]) { - return new ChangesetsOverview(meta?.map((cs) => ChangesetsOverview.cleanChangesetData(cs))) - } - private constructor(meta: ChangeSetData[]) { this._meta = Utils.NoNull(meta) } - public filter(predicate: (cs: ChangeSetData) => boolean) { - return new ChangesetsOverview(this._meta.filter(predicate)) - } - - public sum(key: string, excludeThemes: Set): number { - let s = 0 - for (const feature of this._meta) { - if (excludeThemes.has(feature.properties.theme)) { - continue - } - const parsed = Number(feature.properties[key]) - if (!isNaN(parsed)) { - s += parsed - } - } - return s + public static fromDirtyData(meta: ChangeSetData[]) { + return new ChangesetsOverview(meta?.map((cs) => ChangesetsOverview.cleanChangesetData(cs))) } private static cleanChangesetData(cs: ChangeSetData): ChangeSetData { @@ -286,6 +263,24 @@ class ChangesetsOverview { } catch (e) {} return cs } + + public filter(predicate: (cs: ChangeSetData) => boolean) { + return new ChangesetsOverview(this._meta.filter(predicate)) + } + + public sum(key: string, excludeThemes: Set): number { + let s = 0 + for (const feature of this._meta) { + if (excludeThemes.has(feature.properties.theme)) { + continue + } + const parsed = Number(feature.properties[key]) + if (!isNaN(parsed)) { + s += parsed + } + } + return s + } } interface ChangeSetData { @@ -323,3 +318,5 @@ interface ChangeSetData { language: string } } + +new StatisticsGUI().AttachTo("main") diff --git a/css/tabbedComponent.css b/css/tabbedComponent.css deleted file mode 100644 index 39b949b29..000000000 --- a/css/tabbedComponent.css +++ /dev/null @@ -1,74 +0,0 @@ - -.tabs-header-bar { - padding-left: 1em; - padding-top: 10px; /* For the shadow */ - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: flex-start; - align-items: start; - background-color: var(--background-color); - max-width: 100%; - overflow-x: auto; -} - - -.tab-single-header img { - height: 3em; - max-width: 3em; - padding: 0.5em; - display: block; - margin: auto; -} - -.tab-single-header svg { - height: 3em; - max-width: 3em; - padding: 0.5em; - display: block; - margin: auto; -} - - -.tab-single-header { - border-top-left-radius: 1em; - border-top-right-radius: 1em; - z-index: 5000; - padding-bottom: 0; - margin-bottom: 0; -} - -.tab-active { - background-color: var(--background-color); - color: var(--foreground-color); - z-index: 5001; - box-shadow: 0 0 10px var(--shadow-color); - border: 1px solid var(--background-color); - min-width: 4em; -} - -.tab-active svg { - fill: var(--foreground-color); - stroke: var(--foreground-color); -} - -.tab-non-active { - background-color: var(--subtle-detail-color); - color: var(--foreground-color); - opacity: 0.5; - border-left: 1px solid gray; - border-right: 1px solid gray; - border-top: 1px solid gray; - border-bottom: 1px solid lightgray; - min-width: 4em; -} - -.tab-non-active svg { - fill: var(--non-active-tab-svg) !important; - stroke: var(--non-active-tab-svg) !important; -} - -.tab-non-active svg path { - fill: var(--non-active-tab-svg) !important; - stroke: var(--non-active-tab-svg) !important; -} diff --git a/index.html b/index.html index 6cc4bd75c..4175e853d 100644 --- a/index.html +++ b/index.html @@ -3,8 +3,6 @@ - - diff --git a/theme.html b/theme.html index 1623a00c9..7dd7b6fb8 100644 --- a/theme.html +++ b/theme.html @@ -4,8 +4,6 @@ - -