From 3468837e0bddfc87432399c117401924e03c8dd0 Mon Sep 17 00:00:00 2001
From: Pieter Vander Vennet <pietervdvn@posteo.net>
Date: Tue, 4 Mar 2025 22:21:24 +0100
Subject: [PATCH] Refactoring: fix statistics in onwheels theme

---
 assets/themes/onwheels/onwheels.json          |  69 ------------
 public/css/index-tailwind-output.css          | 100 +-----------------
 src/Models/ThemeViewState/WithGuiState.ts     |   1 -
 src/UI/Base/ChartJs.ts                        |  56 +---------
 src/UI/BigComponents/StatisticsPanel.ts       |  10 +-
 src/UI/BigComponents/TagRenderingChart.ts     |  10 +-
 src/UI/SpecialVisualizations.ts               |  26 +----
 .../Statistics/AllFeaturesStatistics.svelte   |  19 ++++
 src/UI/Statistics/LayerStatistics.svelte      |  47 ++++++++
 tailwind.config.cjs                           |   2 +-
 10 files changed, 84 insertions(+), 256 deletions(-)
 create mode 100644 src/UI/Statistics/AllFeaturesStatistics.svelte
 create mode 100644 src/UI/Statistics/LayerStatistics.svelte

diff --git a/assets/themes/onwheels/onwheels.json b/assets/themes/onwheels/onwheels.json
index 3e601ffae..7ac1def38 100644
--- a/assets/themes/onwheels/onwheels.json
+++ b/assets/themes/onwheels/onwheels.json
@@ -511,75 +511,6 @@
           }
         ]
       }
-    },
-    {
-      "builtin": "maproulette_challenge",
-      "override": {
-        "source": {
-          "geoJson": "https://maproulette.org/api/v2/challenge/view/28012"
-        },
-        "calculatedTags": [
-          "_closest_osm_hotel=closest(feat)('tourism_accomodation')?.properties?.id",
-          "_closest_osm_hotel_distance=distanceTo(feat)(feat.properties._closest_osm_hotel)",
-          "_has_closeby_feature=Number(feat.properties._closest_osm_hotel_distance) < 50 ? 'yes' : 'no'"
-        ],
-        "+tagRenderings": [
-          {
-            "id": "import-button",
-            "condition": "_has_closeby_feature=no",
-            "render": {
-              "special": {
-                "type": "import_button",
-                "targetLayer": "tourism_accomodation",
-                "tags": "tags",
-                "text": {
-                  "en": "Import",
-                  "de": "Import",
-                  "fr": "Importation",
-                  "da": "Importere",
-                  "nb_NO": "Importer",
-                  "ca": "Importar",
-                  "pa_PK": "ایمپورٹ کرو",
-                  "nl": "Importeren",
-                  "cs": "Dovoz",
-                  "es": "Importar",
-                  "eu": "Inportatu",
-                  "pl": "Import",
-                  "zh_Hant": "匯入",
-                  "ko": "불러오기"
-                },
-                "icon": "./assets/svg/addSmall.svg",
-                "maproulette_id": "mr_taskId"
-              }
-            }
-          },
-          {
-            "id": "tag-apply-button",
-            "condition": "_has_closeby_feature=yes",
-            "render": {
-              "special": {
-                "type": "tag_apply",
-                "tags_to_apply": "$tags",
-                "message": {
-                  "en": "Add all the suggested tags",
-                  "de": "Alle vorgeschlagenen Tags hinzufügen",
-                  "fr": "Ajouter tous les attributs suggérés",
-                  "da": "Tilføj alle de foreslåede tags",
-                  "nb_NO": "Legg til alle foreslåtte",
-                  "nl": "Voeg alle gesuggereerde tags toe",
-                  "cs": "Přidat všechny navrhované značky",
-                  "es": "Agregar todas las etiquetas sugeridas",
-                  "ca": "Afegiu totes les etiquetes suggerides",
-                  "pl": "Dodaj wszystkie sugerowane znaczniki",
-                  "ko": "제안된 모든 태그 추가"
-                },
-                "image": "./assets/svg/addSmall.svg",
-                "id_of_object_to_apply_this_one": "_closest_osm_hotel"
-              }
-            }
-          }
-        ]
-      }
     }
   ],
   "overrideAll": {
diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css
index b167e1deb..72cb63208 100644
--- a/public/css/index-tailwind-output.css
+++ b/public/css/index-tailwind-output.css
@@ -1019,59 +1019,35 @@ input[type="range"].range-lg::-moz-range-thumb {
   width: 1.5rem;
 }
 
-.\!container {
-  width: 100% !important;
-}
-
 .container {
   width: 100%;
 }
 
 @media (min-width: 640px) {
-  .\!container {
-    max-width: 640px !important;
-  }
-
   .container {
     max-width: 640px;
   }
 }
 
 @media (min-width: 768px) {
-  .\!container {
-    max-width: 768px !important;
-  }
-
   .container {
     max-width: 768px;
   }
 }
 
 @media (min-width: 1024px) {
-  .\!container {
-    max-width: 1024px !important;
-  }
-
   .container {
     max-width: 1024px;
   }
 }
 
 @media (min-width: 1280px) {
-  .\!container {
-    max-width: 1280px !important;
-  }
-
   .container {
     max-width: 1280px;
   }
 }
 
 @media (min-width: 1536px) {
-  .\!container {
-    max-width: 1536px !important;
-  }
-
   .container {
     max-width: 1536px;
   }
@@ -1108,10 +1084,6 @@ input[type="range"].range-lg::-moz-range-thumb {
   pointer-events: auto;
 }
 
-.\!visible {
-  visibility: visible !important;
-}
-
 .visible {
   visibility: visible;
 }
@@ -1128,10 +1100,6 @@ input[type="range"].range-lg::-moz-range-thumb {
   position: static;
 }
 
-.\!fixed {
-  position: fixed !important;
-}
-
 .fixed {
   position: fixed;
 }
@@ -1391,74 +1359,22 @@ input[type="range"].range-lg::-moz-range-thumb {
   margin: 0.25rem;
 }
 
-.m-11 {
-  margin: 2.75rem;
-}
-
-.m-14 {
-  margin: 3.5rem;
-}
-
 .m-2 {
   margin: 0.5rem;
 }
 
-.m-20 {
-  margin: 5rem;
-}
-
-.m-28 {
-  margin: 7rem;
-}
-
 .m-3 {
   margin: 0.75rem;
 }
 
-.m-32 {
-  margin: 8rem;
-}
-
-.m-36 {
-  margin: 9rem;
-}
-
 .m-4 {
   margin: 1rem;
 }
 
-.m-44 {
-  margin: 11rem;
-}
-
-.m-5 {
-  margin: 1.25rem;
-}
-
-.m-52 {
-  margin: 13rem;
-}
-
-.m-6 {
-  margin: 1.5rem;
-}
-
-.m-7 {
-  margin: 1.75rem;
-}
-
-.m-72 {
-  margin: 18rem;
-}
-
 .m-8 {
   margin: 2rem;
 }
 
-.m-9 {
-  margin: 2.25rem;
-}
-
 .-mx-1\.5 {
   margin-left: -0.375rem;
   margin-right: -0.375rem;
@@ -2202,6 +2118,10 @@ input[type="range"].range-lg::-moz-range-thumb {
   width: 75%;
 }
 
+.w-96 {
+  width: 24rem;
+}
+
 .w-\[10px\] {
   width: 10px;
 }
@@ -2463,10 +2383,6 @@ input[type="range"].range-lg::-moz-range-thumb {
   transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
 }
 
-.\!transform {
-  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;
-}
-
 .transform {
   transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
 }
@@ -5204,18 +5120,10 @@ input[type="range"].range-lg::-moz-range-thumb {
   transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
 }
 
-.\[_\:string\] {
-  _: string;
-}
-
 .\[a-zA-Z0-9\:_\] {
   a-z-a--z0-9: ;
 }
 
-.\[key\:string\] {
-  key: string;
-}
-
 .\[out\:json\] {
   out: json;
 }
diff --git a/src/Models/ThemeViewState/WithGuiState.ts b/src/Models/ThemeViewState/WithGuiState.ts
index 2fd242315..f43ffcdff 100644
--- a/src/Models/ThemeViewState/WithGuiState.ts
+++ b/src/Models/ThemeViewState/WithGuiState.ts
@@ -66,7 +66,6 @@ export class WithGuiState extends WithSpecialLayers {
     }
 
     public selectCurrentView() {
-        this.guistate.closeAll()
         this.selectedElement.setData(this.currentView.features?.data?.[0])
     }
 }
diff --git a/src/UI/Base/ChartJs.ts b/src/UI/Base/ChartJs.ts
index bb48910bf..138eca609 100644
--- a/src/UI/Base/ChartJs.ts
+++ b/src/UI/Base/ChartJs.ts
@@ -9,8 +9,8 @@ export class ChartJsColours {
 
     public static readonly otherColor = "rgba(128, 128, 128, 0.2)"
     public static readonly otherBorderColor = "rgba(128, 128, 255)"
-    public static readonly notApplicableColor = "rgba(128, 128, 128, 0.2)"
-    public static readonly notApplicableBorderColor = "rgba(255, 0, 0)"
+    public static readonly notApplicableColor = "#fff" // "rgba(128, 128, 128, 0.2)"
+    public static readonly notApplicableBorderColor = "rgb(241,132,132)"
 
     public static readonly backgroundColors = [
         "rgba(255, 99, 132, 0.2)",
@@ -42,58 +42,6 @@ export default class ChartJs<
         this._config = config
     }
 
-    public static ConstructDoughnut(data: Record<string, number>) {
-        const borderColor = [
-            // ChartJsColours.unkownBorderColor,
-            //  ChartJsColours.otherBorderColor,
-            //  ChartJsColours.notApplicableBorderColor,
-        ]
-        const backgroundColor = [
-            //   ChartJsColours.unkownColor,
-            //   ChartJsColours.otherColor,
-            //   ChartJsColours.notApplicableColor,
-        ]
-
-        let i = 0
-        const borders = ChartJsColours.borderColors
-        const bg = ChartJsColours.backgroundColors
-
-        for (const key in data) {
-            if (key === "") {
-                borderColor.push(ChartJsColours.unknownBorderColor)
-                backgroundColor.push(ChartJsColours.unknownColor)
-            } else {
-                borderColor.push(borders[i % borders.length])
-                backgroundColor.push(bg[i % bg.length])
-                i++
-            }
-        }
-
-        const config = <ChartConfiguration>{
-            type: "doughnut",
-            data: {
-                labels: Object.keys(data),
-                datasets: [
-                    {
-                        data: Object.values(data),
-                        backgroundColor,
-                        borderColor,
-                        borderWidth: 1,
-                        label: undefined,
-                    },
-                ],
-            },
-            options: {
-                plugins: {
-                    legend: {
-                        display: false,
-                    },
-                },
-            },
-        }
-        return new ChartJs(config)
-    }
-
     protected InnerConstructElement(): HTMLElement {
         const canvas = document.createElement("canvas")
         // A bit exceptional: we apply the styles before giving them to 'chartJS'
diff --git a/src/UI/BigComponents/StatisticsPanel.ts b/src/UI/BigComponents/StatisticsPanel.ts
index f8eb293aa..906c5c1b3 100644
--- a/src/UI/BigComponents/StatisticsPanel.ts
+++ b/src/UI/BigComponents/StatisticsPanel.ts
@@ -1,5 +1,4 @@
 import { VariableUiElement } from "../Base/VariableUIElement"
-import Loading from "../Base/Loading"
 import Title from "../Base/Title"
 import TagRenderingChart from "./TagRenderingChart"
 import Combine from "../Base/Combine"
@@ -13,14 +12,7 @@ export default class StatisticsForLayerPanel extends VariableUiElement {
         super(
             elementsInview.features.stabilized(1000).map(
                 (features) => {
-                    if (features === undefined) {
-                        return new Loading("Loading data")
-                    }
-                    if (features.length === 0) {
-                        return new Combine(["No elements in view for layer ", layer.id]).SetClass(
-                            "block"
-                        )
-                    }
+
                     const els: BaseUIElement[] = []
                     const featuresForLayer = features
                     if (featuresForLayer.length === 0) {
diff --git a/src/UI/BigComponents/TagRenderingChart.ts b/src/UI/BigComponents/TagRenderingChart.ts
index c3994596a..3a9b9447c 100644
--- a/src/UI/BigComponents/TagRenderingChart.ts
+++ b/src/UI/BigComponents/TagRenderingChart.ts
@@ -5,6 +5,7 @@ import Combine from "../Base/Combine"
 import { TagUtils } from "../../Logic/Tags/TagUtils"
 import { Utils } from "../../Utils"
 import { OsmFeature } from "../../Models/OsmFeature"
+import Title from "../Base/Title"
 
 export interface TagRenderingChartOptions {
     groupToOtherCutoff?: 3 | number
@@ -275,8 +276,13 @@ export default class TagRenderingChart extends Combine {
         }
 
         super([
-            options?.includeTitle ? tagRendering.question.Clone() ?? tagRendering.id : undefined,
-            chart,
+            new Title(
+                options?.includeTitle ? tagRendering.question ?? tagRendering.id : undefined
+            ),
+            new Combine([
+
+                chart
+            ]).SetClass("flex flex-col justify-center h-full")
         ])
 
         this.SetClass("block")
diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts
index 68996e52c..30cb9fa7d 100644
--- a/src/UI/SpecialVisualizations.ts
+++ b/src/UI/SpecialVisualizations.ts
@@ -1,4 +1,3 @@
-import Combine from "./Base/Combine"
 import { FixedUiElement } from "./Base/FixedUiElement"
 import BaseUIElement from "./BaseUIElement"
 import { default as FeatureTitle } from "./Popup/Title.svelte"
@@ -12,11 +11,9 @@ import { VariableUiElement } from "./Base/VariableUIElement"
 import { Translation } from "./i18n/Translation"
 import Translations from "./i18n/Translations"
 import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
-import StatisticsPanel from "./BigComponents/StatisticsPanel"
 import AutoApplyButton from "./Popup/AutoApplyButton"
 import { LanguageElement } from "./Popup/LanguageElement/LanguageElement"
 import SvelteUIElement from "./Base/SvelteUIElement"
-import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
 import { Feature, LineString } from "geojson"
 import { GeoOperations } from "../Logic/GeoOperations"
 import LayerConfig from "../Models/ThemeConfig/LayerConfig"
@@ -45,6 +42,7 @@ import {
     WebAndCommunicationSpecialVisualisations
 } from "./SpecialVisualisations/WebAndCommunicationSpecialVisualisations"
 import ClearGPSHistory from "./BigComponents/ClearGPSHistory.svelte"
+import AllFeaturesStatistics from "./Statistics/AllFeaturesStatistics.svelte"
 
 export default class SpecialVisualizations {
     public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
@@ -407,27 +405,7 @@ export default class SpecialVisualizations {
                 docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
                 args: [],
 
-                constr: (state) => {
-                    return new Combine(
-                        state.theme.layers
-                            .filter(
-                                (l) =>
-                                    l.name !== null &&
-                                    l.title &&
-                                    state.perLayer.get(l.id) !== undefined
-                            )
-                            .map(
-                                (l) => {
-                                    const fs = state.perLayer.get(l.id)
-                                    console.log(">>>", l.id, fs)
-                                    const bbox = state.mapProperties.bounds
-                                    const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
-                                    return new StatisticsPanel(fsBboxed)
-                                },
-                                [state.mapProperties.bounds]
-                            )
-                    )
-                },
+                constr: (state) => new SvelteUIElement(AllFeaturesStatistics, { state })
             },
 
             {
diff --git a/src/UI/Statistics/AllFeaturesStatistics.svelte b/src/UI/Statistics/AllFeaturesStatistics.svelte
new file mode 100644
index 000000000..1357349e2
--- /dev/null
+++ b/src/UI/Statistics/AllFeaturesStatistics.svelte
@@ -0,0 +1,19 @@
+<script lang="ts">
+  import ThemeViewState from "../../Models/ThemeViewState"
+  import { Accordion } from "flowbite-svelte"
+  import LayerStatistics from "./LayerStatistics.svelte"
+
+  /**
+   * An element showing s
+   */
+  export let state: ThemeViewState
+  let layers = state.theme.layers.filter(l => l.isNormal())
+
+</script>
+
+<Accordion>
+
+  {#each layers as layer (layer.id)}
+    <LayerStatistics {state} {layer} />
+  {/each}
+</Accordion>
diff --git a/src/UI/Statistics/LayerStatistics.svelte b/src/UI/Statistics/LayerStatistics.svelte
new file mode 100644
index 000000000..0f5c29cd5
--- /dev/null
+++ b/src/UI/Statistics/LayerStatistics.svelte
@@ -0,0 +1,47 @@
+<script lang="ts">
+  /**
+   * Statistics, based on the tagRendering, for a single layer
+   */
+  import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
+  import Tr from "../Base/Tr.svelte"
+  import Loading from "../Base/Loading.svelte"
+  import ToSvelte from "../Base/ToSvelte.svelte"
+  import TagRenderingChart from "../BigComponents/TagRenderingChart"
+  import type { Feature } from "geojson"
+  import { AccordionItem } from "flowbite-svelte"
+  import ThemeViewState from "../../Models/ThemeViewState"
+  import DefaultIcon from "../Map/DefaultIcon.svelte"
+
+  export let layer: LayerConfig
+  export let state: ThemeViewState
+  let bbox = state.mapProperties.bounds
+  let elements: Feature[] = state.perLayer.get(layer.id).GetFeaturesWithin($bbox)
+  $: elements = state.perLayer.get(layer.id).GetFeaturesWithin($bbox)
+
+  let trs = layer.tagRenderings.filter(tr => tr.question)
+</script>
+
+<AccordionItem paddingDefault="p-2" inactiveClass="text-black" defaultClass="w-full flex-grow justify-start">
+  <div slot="header" class="flex items-center gap-x-2">
+    <div class="w-8 h-8 inline-block">
+      <DefaultIcon {layer} />
+    </div>
+    <Tr t={layer.name} />
+    ({elements.length} elements in view)
+  </div>
+
+  {#if elements === undefined}
+    <Loading />
+  {:else if elements.length === 0}
+    No features in view
+  {:else}
+    <div class="flex flex-wrap w-full gap-y-4 gap-x-4">
+
+      {#each trs as tr}
+        <ToSvelte construct={() => new TagRenderingChart(elements, tr, {
+                            chartclasses: "w-full self-center",includeTitle: true
+                        }).SetClass(tr.multiAnswer ? "w-128": "w-96") } />
+      {/each}
+    </div>
+  {/if}
+</AccordionItem>
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
index a2294bd3a..39838e759 100644
--- a/tailwind.config.cjs
+++ b/tailwind.config.cjs
@@ -3,7 +3,7 @@ const plugin = require("tailwindcss/plugin")
 const flowbitePlugin = require("flowbite/plugin")
 
 module.exports = {
-  content: ["./**/*.{html,ts,svelte}", './node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}'],
+  content: ["*.html", "src/**/*.{html,ts,svelte}", "app/**/*.{html,ts,svelte}", "./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}"],
 
   theme: {
     extend: {