forked from MapComplete/MapComplete
		
	Add new statistics view
This commit is contained in:
		
							parent
							
								
									6f6a5d7092
								
							
						
					
					
						commit
						716fda39aa
					
				
					 10 changed files with 342 additions and 171 deletions
				
			
		|  | @ -21,6 +21,7 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | |||
| import {TiledStaticFeatureSource} from "../FeatureSource/Sources/StaticFeatureSource"; | ||||
| import {Translation, TypedTranslation} from "../../UI/i18n/Translation"; | ||||
| import {Tag} from "../Tags/Tag"; | ||||
| import {OsmConnection} from "../Osm/OsmConnection"; | ||||
| 
 | ||||
| 
 | ||||
| export interface GlobalFilter { | ||||
|  | @ -143,7 +144,7 @@ export default class MapState extends UserRelatedState { | |||
|                 config: c, | ||||
|                 isDisplayed: QueryParameters.GetBooleanQueryParameter("overlay-" + c.id, c.defaultState, "Wether or not the overlay " + c.id + " is shown") | ||||
|             })) ?? [] | ||||
|         this.filteredLayers = this.InitializeFilteredLayers() | ||||
|         this.filteredLayers = new UIEventSource<FilteredLayer[]>( MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection)) | ||||
| 
 | ||||
| 
 | ||||
|         this.lockBounds() | ||||
|  | @ -353,8 +354,8 @@ export default class MapState extends UserRelatedState { | |||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private getPref(key: string, layer: LayerConfig): UIEventSource<boolean> { | ||||
|         return this.osmConnection | ||||
|     private static getPref(osmConnection: OsmConnection, key: string, layer: LayerConfig): UIEventSource<boolean> { | ||||
|         return osmConnection | ||||
|             .GetPreference(key, layer.shownByDefault + "") | ||||
|             .sync(v => { | ||||
|                 if (v === undefined) { | ||||
|  | @ -369,10 +370,9 @@ export default class MapState extends UserRelatedState { | |||
|             }) | ||||
|     } | ||||
| 
 | ||||
|     private InitializeFilteredLayers() { | ||||
|         const layoutToUse = this.layoutToUse; | ||||
|     public static InitializeFilteredLayers(layoutToUse: {layers: LayerConfig[], id: string}, osmConnection: OsmConnection): FilteredLayer[] { | ||||
|         if (layoutToUse === undefined) { | ||||
|             return new UIEventSource<FilteredLayer[]>([]) | ||||
|             return [] | ||||
|         } | ||||
|         const flayers: FilteredLayer[] = []; | ||||
|         for (const layer of layoutToUse.layers) { | ||||
|  | @ -380,9 +380,9 @@ export default class MapState extends UserRelatedState { | |||
|             if (layer.syncSelection === "local") { | ||||
|                 isDisplayed = LocalStorageSource.GetParsed(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer.shownByDefault) | ||||
|             } else if (layer.syncSelection === "theme-only") { | ||||
|                 isDisplayed = this.getPref(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer) | ||||
|                 isDisplayed = MapState.getPref(osmConnection, layoutToUse.id + "-layer-" + layer.id + "-enabled", layer) | ||||
|             } else if (layer.syncSelection === "global") { | ||||
|                 isDisplayed = this.getPref("layer-" + layer.id + "-enabled", layer) | ||||
|                 isDisplayed = MapState.getPref(osmConnection,"layer-" + layer.id + "-enabled", layer) | ||||
|             } else { | ||||
|                 isDisplayed = QueryParameters.GetBooleanQueryParameter("layer-" + layer.id, layer.shownByDefault, "Wether or not layer " + layer.id + " is shown") | ||||
|             } | ||||
|  | @ -420,7 +420,7 @@ export default class MapState extends UserRelatedState { | |||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         return new UIEventSource<FilteredLayer[]>(flayers); | ||||
|         return flayers; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,11 +5,13 @@ import Translations from "../../UI/i18n/Translations"; | |||
| import {TagUtils} from "../../Logic/Tags/TagUtils"; | ||||
| import ValidatedTextField from "../../UI/Input/ValidatedTextField"; | ||||
| import {TagConfigJson} from "./Json/TagConfigJson"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {FilterState} from "../FilteredLayer"; | ||||
| import {QueryParameters} from "../../Logic/Web/QueryParameters"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {RegexTag} from "../../Logic/Tags/RegexTag"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import {InputElement} from "../../UI/Input/InputElement"; | ||||
| 
 | ||||
| export default class FilterConfig { | ||||
|     public readonly id: string | ||||
|  |  | |||
|  | @ -17,6 +17,17 @@ export default class ChartJs< | |||
|      | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         const canvas = document.createElement("canvas"); | ||||
|         // A bit exceptional: we apply the styles before giving them to 'chartJS'
 | ||||
|         if(this.style !== undefined){ | ||||
|             canvas.style.cssText = this.style | ||||
|         } | ||||
|         if (this.clss?.size > 0) { | ||||
|             try { | ||||
|                 canvas.classList.add(...Array.from(this.clss)) | ||||
|             } catch (e) { | ||||
|                 console.error("Invalid class name detected in:", Array.from(this.clss).join(" "), "\nErr msg is ", e) | ||||
|             } | ||||
|         } | ||||
|         new Chart(canvas, this._config); | ||||
|         return canvas; | ||||
|     } | ||||
|  |  | |||
|  | @ -9,8 +9,8 @@ export default abstract class BaseUIElement { | |||
| 
 | ||||
|     protected _constructedHtmlElement: HTMLElement; | ||||
|     protected isDestroyed = false; | ||||
|     private readonly clss: Set<string> = new Set<string>(); | ||||
|     private style: string; | ||||
|     protected readonly clss: Set<string> = new Set<string>(); | ||||
|     protected style: string; | ||||
|     private _onClick: () => void; | ||||
| 
 | ||||
|     public onClick(f: (() => void)) { | ||||
|  |  | |||
|  | @ -171,7 +171,7 @@ export class LayerFilterPanel extends Combine { | |||
| 
 | ||||
|             ui.SetClass("mt-1") | ||||
|             toShow.push(ui) | ||||
|             actualTags.addCallback(tagsToFilterFor => { | ||||
|             actualTags.addCallbackAndRun(tagsToFilterFor => { | ||||
|                 flayer.appliedFilters.data.set(filter.id, tagsToFilterFor) | ||||
|                 flayer.appliedFilters.ping() | ||||
|             }) | ||||
|  | @ -195,6 +195,7 @@ export class LayerFilterPanel extends Combine { | |||
|         const properties = new UIEventSource<any>({}) | ||||
|         for (const {name, type} of filter.fields) { | ||||
|             const value = QueryParameters.GetQueryParameter("filter-" + filterConfig.id + "-" + name, "", "Value for filter " + filterConfig.id) | ||||
|              | ||||
|             const field = ValidatedTextField.ForType(type).ConstructInputElement({ | ||||
|                 value | ||||
|             }).SetClass("inline-block") | ||||
|  |  | |||
|  | @ -13,26 +13,39 @@ export interface TagRenderingChartOptions { | |||
| } | ||||
| 
 | ||||
| export class StackedRenderingChart extends ChartJs { | ||||
|     constructor(tr: TagRenderingConfig, features: (OsmFeature & {properties : {date: string}})[], period: "day" | "month" = "day") { | ||||
|     constructor(tr: TagRenderingConfig, features: (OsmFeature & { properties: { date: string } })[], options?: { | ||||
|         period: "day" | "month", | ||||
|         groupToOtherCutoff?: 3 | number | ||||
|     }) { | ||||
|         const {labels, data} = TagRenderingChart.extractDataAndLabels(tr, features, { | ||||
|             sort: true | ||||
|             sort: true, | ||||
|             groupToOtherCutoff: options?.groupToOtherCutoff | ||||
|         }) | ||||
|         if (labels === undefined || data === undefined) { | ||||
|             throw ("No labels or data given...") | ||||
|         } | ||||
|         // labels: ["cyclofix", "buurtnatuur", ...]; data : [ ["cyclofix-changeset", "cyclofix-changeset", ...], ["buurtnatuur-cs", "buurtnatuur-cs"], ... ]
 | ||||
| 
 | ||||
|         console.log("LABELS:", labels, "DATA:", data) | ||||
|          | ||||
|         for (let i = labels.length; i >= 0; i--) { | ||||
|             if (data[i]?.length != 0) { | ||||
|                 continue | ||||
|             } | ||||
|             data.splice(i, 1) | ||||
|             labels.splice(i, 1) | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         const datasets: { label: string /*themename*/, data: number[]/*counts per day*/, backgroundColor: string }[] = [] | ||||
|         const allDays = StackedRenderingChart.getAllDays(features) | ||||
|         let trimmedDays = allDays.map(d => d.substr(0, d.indexOf("T"))) | ||||
| 
 | ||||
|         if (period === "month") { | ||||
|         if (options?.period === "month") { | ||||
|             trimmedDays = trimmedDays.map(d => d.substr(0, 7)) | ||||
|         } | ||||
|         trimmedDays = Utils.Dedup(trimmedDays) | ||||
| 
 | ||||
| 
 | ||||
|         for (let i = 0; i < labels.length; i++) { | ||||
|             const label = labels[i]; | ||||
|             const changesetsForTheme = data[i] | ||||
|  | @ -41,7 +54,7 @@ export class StackedRenderingChart extends ChartJs { | |||
|                 const csDate = new Date(changeset.properties.date) | ||||
|                 Utils.SetMidnight(csDate) | ||||
|                 let str = csDate.toISOString(); | ||||
|                 if (period === "month") { | ||||
|                 if (options?.period === "month") { | ||||
|                     csDate.setUTCDate(1) | ||||
|                     str = csDate.toISOString().substr(0, 7); | ||||
|                 } | ||||
|  | @ -57,13 +70,23 @@ export class StackedRenderingChart extends ChartJs { | |||
|                 const day = trimmedDays[i]; | ||||
|                 countsPerDay[i] = perDay[day]?.length ?? 0 | ||||
|             } | ||||
|             let backgroundColor = TagRenderingChart.borderColors[i % TagRenderingChart.borderColors.length] | ||||
|             if (label === "Unknown") { | ||||
|                 backgroundColor = TagRenderingChart.unkownBorderColor | ||||
|             } | ||||
|             if (label === "Other") { | ||||
|                 backgroundColor = TagRenderingChart.otherBorderColor | ||||
|             } | ||||
|             datasets.push({ | ||||
|                 data: countsPerDay, | ||||
|                 backgroundColor: TagRenderingChart.borderColors[i % TagRenderingChart.borderColors.length], | ||||
|                 backgroundColor, | ||||
|                 label | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|         const perDayData = { | ||||
|             labels: trimmedDays, | ||||
|             datasets | ||||
|  | @ -88,9 +111,11 @@ export class StackedRenderingChart extends ChartJs { | |||
|             } | ||||
|         } | ||||
|         super(config) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public static getAllDays(features: (OsmFeature & {properties : {date: string}})[]): string[] { | ||||
|     public static getAllDays(features: (OsmFeature & { properties: { date: string } })[]): string[] { | ||||
|         let earliest: Date = undefined | ||||
|         let latest: Date = undefined; | ||||
|         let allDates = new Set<string>(); | ||||
|  | @ -123,13 +148,13 @@ export class StackedRenderingChart extends ChartJs { | |||
| 
 | ||||
| export default class TagRenderingChart extends Combine { | ||||
| 
 | ||||
|     private static readonly unkownColor = 'rgba(128, 128, 128, 0.2)' | ||||
|     private static readonly unkownBorderColor = 'rgba(128, 128, 128, 0.2)' | ||||
|     public static readonly unkownColor = 'rgba(128, 128, 128, 0.2)' | ||||
|     public static readonly unkownBorderColor = 'rgba(128, 128, 128, 0.2)' | ||||
| 
 | ||||
|     private static readonly otherColor = 'rgba(128, 128, 128, 0.2)' | ||||
|     private static readonly otherBorderColor = 'rgba(128, 128, 255)' | ||||
|     private static readonly notApplicableColor = 'rgba(128, 128, 128, 0.2)' | ||||
|     private static readonly notApplicableBorderColor = 'rgba(255, 0, 0)' | ||||
|     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 backgroundColors = [ | ||||
|  | @ -153,10 +178,12 @@ export default class TagRenderingChart extends Combine { | |||
|     /** | ||||
|      * Creates a chart about this tagRendering for the given data | ||||
|      */ | ||||
|     constructor(features: { properties: Record<string, string> }[], tagRendering: TagRenderingConfig, options?: TagRenderingChartOptions & {  chartclasses?: string, | ||||
|     constructor(features: { properties: Record<string, string> }[], tagRendering: TagRenderingConfig, options?: TagRenderingChartOptions & { | ||||
|         chartclasses?: string, | ||||
|         chartstyle?: string, | ||||
|         includeTitle?: boolean, | ||||
|         chartType?: "pie" | "bar" | "doughnut" }) { | ||||
|         chartType?: "pie" | "bar" | "doughnut" | ||||
|     }) { | ||||
|         if (tagRendering.mappings?.length === 0 && tagRendering.freeform?.key === undefined) { | ||||
|             super([]) | ||||
|             this.SetClass("hidden") | ||||
|  | @ -190,7 +217,6 @@ export default class TagRenderingChart extends Combine { | |||
|         } | ||||
| 
 | ||||
| 
 | ||||
|          | ||||
|         let barchartMode = tagRendering.multiAnswer; | ||||
|         if (labels.length > 9) { | ||||
|             barchartMode = true; | ||||
|  | @ -232,14 +258,14 @@ export default class TagRenderingChart extends Combine { | |||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static extractDataAndLabels<T extends {properties: Record<string, string>}>(tagRendering: TagRenderingConfig, features: T[], options?:TagRenderingChartOptions): {labels: string[], data: T[][]} { | ||||
|     public static extractDataAndLabels<T extends { properties: Record<string, string> }>(tagRendering: TagRenderingConfig, features: T[], options?: TagRenderingChartOptions): { labels: string[], data: T[][] } { | ||||
|         const mappings = tagRendering.mappings ?? [] | ||||
| 
 | ||||
|         options = options ?? {} | ||||
|         let unknownCount : T[] = []; | ||||
|         const categoryCounts : T[][]= mappings.map(_ => []) | ||||
|         let unknownCount: T[] = []; | ||||
|         const categoryCounts: T[][] = mappings.map(_ => []) | ||||
|         const otherCounts: Record<string, T[]> = {} | ||||
|         let notApplicable : T[] = []; | ||||
|         let notApplicable: T[] = []; | ||||
|         for (const feature of features) { | ||||
|             const props = feature.properties | ||||
|             if (tagRendering.condition !== undefined && !tagRendering.condition.matchesProperties(props)) { | ||||
|  | @ -274,7 +300,7 @@ export default class TagRenderingChart extends Combine { | |||
|                 if (tagRendering.freeform?.key !== undefined && props[tagRendering.freeform.key] !== undefined) { | ||||
|                     const otherValue = props[tagRendering.freeform.key] | ||||
|                     otherCounts[otherValue] = (otherCounts[otherValue] ?? []) | ||||
|                      otherCounts[otherValue]      .push(feature) | ||||
|                     otherCounts[otherValue].push(feature) | ||||
|                 } else { | ||||
|                     unknownCount.push(feature) | ||||
|                 } | ||||
|  | @ -286,7 +312,7 @@ export default class TagRenderingChart extends Combine { | |||
|             return {labels: undefined, data: undefined} | ||||
|         } | ||||
| 
 | ||||
|         let otherGrouped : T[] = []; | ||||
|         let otherGrouped: T[] = []; | ||||
|         const otherLabels: string[] = [] | ||||
|         const otherData: T[][] = [] | ||||
|         const sortedOtherCounts: [string, T[]][] = [] | ||||
|  | @ -307,7 +333,7 @@ export default class TagRenderingChart extends Combine { | |||
| 
 | ||||
| 
 | ||||
|         const labels = ["Unknown", "Other", "Not applicable", ...mappings?.map(m => m.then.txt) ?? [], ...otherLabels] | ||||
|         const data : T[][] = [unknownCount, otherGrouped, notApplicable, ...categoryCounts, ...otherData] | ||||
|         const data: T[][] = [unknownCount, otherGrouped, notApplicable, ...categoryCounts, ...otherData] | ||||
| 
 | ||||
|         return {labels, data} | ||||
|     } | ||||
|  |  | |||
|  | @ -6,34 +6,25 @@ import {VariableUiElement} from "./Base/VariableUIElement"; | |||
| import Loading from "./Base/Loading"; | ||||
| import {Utils} from "../Utils"; | ||||
| import Combine from "./Base/Combine"; | ||||
| import BaseUIElement from "./BaseUIElement"; | ||||
| import TagRenderingChart, {StackedRenderingChart} from "./BigComponents/TagRenderingChart"; | ||||
| import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; | ||||
| import FilterView, {LayerFilterPanel} from "./BigComponents/FilterView"; | ||||
| import FilteredLayer, {FilterState} from "../Models/FilteredLayer"; | ||||
| import {StackedRenderingChart} from "./BigComponents/TagRenderingChart"; | ||||
| import {LayerFilterPanel} from "./BigComponents/FilterView"; | ||||
| import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; | ||||
| import MapState from "../Logic/State/MapState"; | ||||
| import BaseUIElement from "./BaseUIElement"; | ||||
| import Title from "./Base/Title"; | ||||
| 
 | ||||
| export default class StatisticsGUI { | ||||
| 
 | ||||
|     private static readonly homeUrl = "https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/Docs/Tools/stats/" | ||||
|     private static readonly stats_files = "file-overview.json" | ||||
|     private readonly index = UIEventSource.FromPromise(Utils.downloadJson(StatisticsGUI.homeUrl + StatisticsGUI.stats_files)) | ||||
| 
 | ||||
| 
 | ||||
|     public setup(): void { | ||||
| 
 | ||||
|         const appliedFilters = new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>()) | ||||
| class StatisticsForOverviewFile extends Combine{ | ||||
|     constructor(homeUrl: string, paths: string[]) { | ||||
|         const layer = AllKnownLayouts.allKnownLayouts.get("mapcomplete-changes").layers[0] | ||||
|         const filteredLayer = MapState.InitializeFilteredLayers({id: "statistics-view", layers: [layer]}, undefined)[0] | ||||
|        const filterPanel = new LayerFilterPanel(undefined, filteredLayer) | ||||
|         const appliedFilters = filteredLayer.appliedFilters | ||||
| 
 | ||||
|         new VariableUiElement(this.index.map(paths => { | ||||
|             if (paths === undefined) { | ||||
|                 return new Loading("Loading overview...") | ||||
|             } | ||||
|         const downloaded = new UIEventSource<{ features: ChangeSetData[] }[]>([]) | ||||
| 
 | ||||
|         for (const filepath of paths) { | ||||
|                 Utils.downloadJson(StatisticsGUI.homeUrl + filepath).then(data => { | ||||
|                     data.features.forEach(item => { | ||||
|             Utils.downloadJson(homeUrl + filepath).then(data => { | ||||
|                 data?.features?.forEach(item => { | ||||
|                     item.properties = {...item.properties, ...item.properties.metadata} | ||||
|                     delete item.properties.metadata | ||||
|                 }) | ||||
|  | @ -42,12 +33,18 @@ export default class StatisticsGUI { | |||
|             }) | ||||
|         } | ||||
| 
 | ||||
|             return new Combine([ | ||||
|                 new VariableUiElement(downloaded.map(dl => "Downloaded " + dl.length + " items")), | ||||
|                 new VariableUiElement(downloaded.map(l => [...l]).stabilized(250).map(downloaded => { | ||||
|                     let overview = ChangesetsOverview.fromDirtyData([].concat(...downloaded.map(d => d.features))) | ||||
|                     //  return overview.breakdownPerDay(overview.themeBreakdown)
 | ||||
|         const loading = new Loading( new VariableUiElement( | ||||
|             downloaded.map(dl => "Downloaded " + dl.length + " items out of "+paths.length)) | ||||
|         ); | ||||
|          | ||||
|         super([ | ||||
|             filterPanel, | ||||
|             new VariableUiElement(downloaded.map(downloaded => { | ||||
|                 if(downloaded.length !== paths.length){ | ||||
|                     return loading | ||||
|                 } | ||||
|                  | ||||
|                 let overview = ChangesetsOverview.fromDirtyData([].concat(...downloaded.map(d => d.features))) | ||||
|                 if (appliedFilters.data.size > 0) { | ||||
|                     appliedFilters.data.forEach((filterSpec) => { | ||||
|                         const tf = filterSpec?.currentFilter | ||||
|  | @ -61,28 +58,52 @@ export default class StatisticsGUI { | |||
|                 if (downloaded.length === 0) { | ||||
|                     return "No data matched the filter" | ||||
|                 } | ||||
|                     return new Combine(layer.tagRenderings.map(tr => { | ||||
|                  | ||||
|                             try { | ||||
| 
 | ||||
|                                 return new StackedRenderingChart(tr, <any>overview._meta, "month") | ||||
|                             } catch (e) { | ||||
|                                 return "Could not create stats for " + tr.id | ||||
|                 const trs =layer.tagRenderings | ||||
|                     .filter(tr => tr.mappings?.length > 0 || tr.freeform?.key !== undefined); | ||||
|                 const elements : BaseUIElement[] = [] | ||||
|                 for (const tr of trs) { | ||||
|                     let total = undefined | ||||
|                     if(tr.freeform?.key !== undefined) { | ||||
|                      total =  new Set(  overview._meta.map(f => f.properties[tr.freeform.key])).size | ||||
|                     } | ||||
|                         }) | ||||
|                     ) | ||||
|                      | ||||
|                      | ||||
|                     elements.push(new Combine([ | ||||
|                         new Title(tr.question ?? tr.id).SetClass("p-2") , | ||||
|                         total > 1 ? total + " unique value"  : undefined, | ||||
|                         new StackedRenderingChart(tr, <any>overview._meta,  { | ||||
|                             period: "month", | ||||
|                             groupToOtherCutoff: total > 50 ? 25 : (total > 10 ? 3 : 0) | ||||
|                          | ||||
|                         }).SetStyle("width: 100%; height: 600px") | ||||
|                     ]).SetClass("block border-2 border-subtle p-2 m-2 rounded-xl" )) | ||||
|                 } | ||||
|                  | ||||
|                 return new Combine(elements) | ||||
|             }, [appliedFilters])).SetClass("block w-full h-full") | ||||
|             ]).SetClass("block w-full h-full") | ||||
| 
 | ||||
| 
 | ||||
|         })).SetClass("block w-full h-full").AttachTo("maindiv") | ||||
| 
 | ||||
|         const filteredLayer = <FilteredLayer>{ | ||||
|             appliedFilters, | ||||
|             layerDef: layer, | ||||
|             isDisplayed: new UIEventSource<boolean>(true) | ||||
|         ]) | ||||
|             this.SetClass("block w-full h-full") | ||||
|     } | ||||
|         new LayerFilterPanel(undefined, filteredLayer).AttachTo("extradiv") | ||||
| } | ||||
| 
 | ||||
| export default class StatisticsGUI extends VariableUiElement{ | ||||
| 
 | ||||
|     private static readonly homeUrl = "http://127.0.0.1:8080/" /*/ "https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/Docs/Tools/stats/" //*/ | ||||
|     private static readonly stats_files = "file-overview.json" | ||||
| 
 | ||||
| constructor() { | ||||
|    const index = UIEventSource.FromPromise(Utils.downloadJson(StatisticsGUI.homeUrl + StatisticsGUI.stats_files)) | ||||
|         super(index.map(paths => { | ||||
|             if (paths === undefined) { | ||||
|                 return new Loading("Loading overview...") | ||||
|             } | ||||
|              | ||||
|             return new StatisticsForOverviewFile(StatisticsGUI.homeUrl, paths) | ||||
| 
 | ||||
|         })) | ||||
|             this.SetClass("block w-full h-full").AttachTo("maindiv") | ||||
| 
 | ||||
|        | ||||
|     } | ||||
| } | ||||
|  | @ -116,11 +137,11 @@ class ChangesetsOverview { | |||
|     public readonly _meta: ChangeSetData[]; | ||||
| 
 | ||||
|     public static fromDirtyData(meta: ChangeSetData[]) { | ||||
|         return new ChangesetsOverview(meta.map(cs => ChangesetsOverview.cleanChangesetData(cs))) | ||||
|         return new ChangesetsOverview(meta?.map(cs => ChangesetsOverview.cleanChangesetData(cs))) | ||||
|     } | ||||
| 
 | ||||
|     private constructor(meta: ChangeSetData[]) { | ||||
|         this._meta = meta; | ||||
|         this._meta = Utils.NoNull(meta); | ||||
|     } | ||||
| 
 | ||||
|     public filter(predicate: (cs: ChangeSetData) => boolean) { | ||||
|  | @ -128,6 +149,13 @@ class ChangesetsOverview { | |||
|     } | ||||
| 
 | ||||
|     private static cleanChangesetData(cs: ChangeSetData): ChangeSetData { | ||||
|         if(cs === undefined){ | ||||
|             return undefined | ||||
|         } | ||||
|         if(cs.properties.editor?.startsWith("iD")){ | ||||
|             // We also fetch based on hashtag, so some edits with iD show up as well    
 | ||||
|             return undefined | ||||
|         } | ||||
|         if (cs.properties.theme === undefined) { | ||||
|             cs.properties.theme = cs.properties.comment.substr(cs.properties.comment.lastIndexOf("#") + 1) | ||||
|         } | ||||
|  | @ -148,28 +176,6 @@ class ChangesetsOverview { | |||
|         return cs | ||||
|     } | ||||
| 
 | ||||
|     public themeBreakdown = new TagRenderingConfig({ | ||||
|         id: "theme-breakdown", | ||||
|         question: "What theme was used?", | ||||
|         freeform: { | ||||
|             key: "theme" | ||||
|         }, | ||||
|         render: "{theme}" | ||||
|     }, "statistics.themes") | ||||
| 
 | ||||
|     public ThemeBreakdown(): BaseUIElement { | ||||
|         return new TagRenderingChart( | ||||
|             <any>this._meta, | ||||
|             this.themeBreakdown, | ||||
|             { | ||||
|                 chartType: "doughnut", | ||||
|                 sort: true, | ||||
|                 groupToOtherCutoff: 25 | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| interface ChangeSetData { | ||||
|  |  | |||
|  | @ -31,9 +31,6 @@ | |||
|         "geoJsonZoomLevel": 8, | ||||
|         "maxCacheAge": 0 | ||||
|       }, | ||||
|       "calculatedTags": [ | ||||
|         "_last_edit:contributor:lc:=feat.properties['_last_edit:contributor'].toLowerCase()" | ||||
|       ], | ||||
|       "title": { | ||||
|         "render": { | ||||
|           "en": "Changeset for {theme}" | ||||
|  | @ -51,34 +48,61 @@ | |||
|         }, | ||||
|         { | ||||
|           "id": "contributor", | ||||
|           "render": { | ||||
|             "en": "Change made by <a href='https://openstreetmap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}{user}</a>" | ||||
|           }, | ||||
|           "question": { | ||||
|             "en": "What contributor made this change?" | ||||
|             "en": "What contributor did make this change?" | ||||
|           }, | ||||
|           "freeform": { | ||||
|             "key": "user" | ||||
|           }, | ||||
|           "render": { | ||||
|             "en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "id": "theme", | ||||
|           "render": { | ||||
|             "en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>" | ||||
|           }, | ||||
|           "question":{ | ||||
|             "en": "What theme was this change made with?" | ||||
|           "question": { | ||||
|             "en": "What theme was used to make this change?" | ||||
|           }, | ||||
|           "freeform": { | ||||
|             "key": "theme" | ||||
|           }, | ||||
|           "render": { | ||||
|             "en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "id": "locale", | ||||
|           "freeform": { | ||||
|             "key": "locale" | ||||
|           }, | ||||
|           "question": { | ||||
|             "en": "What locale (language) was this change made in?" | ||||
|           }, | ||||
|           "render": { | ||||
|             "en": "User locale is {locale}" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "id": "host", | ||||
|           "render": { | ||||
|             "en": "Change with with <a href='{host}'>{host}</a>" | ||||
|           }, | ||||
|           "question": { | ||||
|             "en": "What host (website) was this change made with?" | ||||
|           }, | ||||
|           "freeform": { | ||||
|             "key": "host" | ||||
|           }, | ||||
|           "mappings": [ | ||||
|             { | ||||
|               "hideInAnswer": true, | ||||
|               "if": "theme~http.*", | ||||
|               "then": { | ||||
|                 "en": "Change with <b>unofficial</b> theme <a href='https://mapcomplete.osm.be/theme.html?userlayout={theme}'>{theme}</a>" | ||||
|               } | ||||
|               "if": "host~https://mapcomplete.osm.be/.*", | ||||
|               "then": "MapComplete", | ||||
|               "hideInAnswer": true | ||||
|             }, | ||||
|             { | ||||
|               "if": "host~https://pietervdvn.github.io/mc/develop/.*", | ||||
|               "then": "Develop", | ||||
|               "hideInAnswer": true | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|  | @ -394,12 +418,7 @@ | |||
|           "id": "created_by", | ||||
|           "options": [ | ||||
|             { | ||||
|               "osmTags": { | ||||
|                 "or":[ | ||||
|                   "_last_edit:contributor:lc~i~.*{search}.*", | ||||
|                 "user~i~.*{search}.*" | ||||
|                 ] | ||||
|               }, | ||||
|               "osmTags": "user~i~.*{search}.*", | ||||
|               "fields": [ | ||||
|                 { | ||||
|                   "name": "search" | ||||
|  | @ -415,7 +434,7 @@ | |||
|           "id": "not_created_by", | ||||
|           "options": [ | ||||
|             { | ||||
|               "osmTags": "_last_edit:contributor:lc!~i~.*{search}.*", | ||||
|               "osmTags": "user!~i~.*{search}.*", | ||||
|               "fields": [ | ||||
|                 { | ||||
|                   "name": "search" | ||||
|  | @ -426,6 +445,38 @@ | |||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "id": "locale-filter", | ||||
|           "options": [ | ||||
|             { | ||||
|               "osmTags": "locale~i~.*{search}.*", | ||||
|               "fields": [ | ||||
|                 { | ||||
|                   "name": "search" | ||||
|                 } | ||||
|               ], | ||||
|               "question": { | ||||
|                 "en": "User language (iso-code) {search}" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "id": "host_name", | ||||
|           "options": [ | ||||
|             { | ||||
|               "osmTags": "host~i~.*{search}.*", | ||||
|               "fields": [ | ||||
|                 { | ||||
|                   "name": "search" | ||||
|                 } | ||||
|               ], | ||||
|               "question": { | ||||
|                 "en": "Made with host {search}" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       ] | ||||
|     }, | ||||
|  |  | |||
|  | @ -48,21 +48,60 @@ | |||
|         }, | ||||
|         { | ||||
|           "id": "contributor", | ||||
|           "question": { | ||||
|             "en": "What contributor did make this change?" | ||||
|           }, | ||||
|           "freeform": { | ||||
|             "key": "user" | ||||
|           }, | ||||
|           "render": { | ||||
|             "en": "Change made by <a href='https://openstreetmap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a>" | ||||
|             "en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "id": "theme", | ||||
|           "question": { | ||||
|             "en": "What theme was used to make this change?" | ||||
|           }, | ||||
|           "freeform": { | ||||
|             "key": "theme" | ||||
|           }, | ||||
|           "render": { | ||||
|             "en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "id": "locale", | ||||
|           "freeform": { | ||||
|             "key": "locale" | ||||
|           }, | ||||
|           "question": { | ||||
|             "en": "What locale (language) was this change made in?" | ||||
|           }, | ||||
|           "render": { | ||||
|           "en": "User locale is {locale}" | ||||
|         } | ||||
|         }, | ||||
|         { | ||||
|           "id": "host", | ||||
|           "render": { | ||||
|             "en": "Change with with <a href='{host}'>{host}</a>" | ||||
|           }, | ||||
|           "question": { | ||||
|             "en": "What host (website) was this change made with?" | ||||
|           }, | ||||
|           "freeform": { | ||||
|             "key": "host" | ||||
|           }, | ||||
|           "mappings": [ | ||||
|             { | ||||
|               "if": "theme~http.*", | ||||
|               "then": { | ||||
|                 "en": "Change with <b>unofficial</b> theme <a href='https://mapcomplete.osm.be/theme.html?userlayout={theme}'>{theme}</a>" | ||||
|               "if": "host=www.waldbrand-app.de", | ||||
|               "then": "waldbrand-app.de", | ||||
|               "hideInAnswer": true | ||||
|             }, | ||||
|             { | ||||
|               "if": "host~https://pietervdvn.github.io/mc/develop/.*", | ||||
|               "then": "Develop", | ||||
|               "hideInAnswer": true | ||||
|             } | ||||
|           ] | ||||
|  | @ -128,6 +167,38 @@ | |||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "id": "locale-filter", | ||||
|           "options": [ | ||||
|             { | ||||
|               "osmTags": "locale~i~.*{search}.*", | ||||
|               "fields": [ | ||||
|                 { | ||||
|                   "name": "search" | ||||
|                 } | ||||
|               ], | ||||
|               "question": { | ||||
|                 "en": "User language (iso-code) {search}" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "id": "host_name", | ||||
|           "options": [ | ||||
|             { | ||||
|               "osmTags": "host~i~.*{search}.*", | ||||
|               "fields": [ | ||||
|                 { | ||||
|                   "name": "search" | ||||
|                 } | ||||
|               ], | ||||
|               "question": { | ||||
|                 "en": "Made with host {search}" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       ] | ||||
|     }, | ||||
|  |  | |||
							
								
								
									
										9
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										9
									
								
								index.ts
									
										
									
									
									
								
							|  | @ -11,6 +11,7 @@ import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerI | |||
| import {DefaultGuiState} from "./UI/DefaultGuiState"; | ||||
| import {QueryParameters} from "./Logic/Web/QueryParameters"; | ||||
| import DashboardGui from "./UI/DashboardGui"; | ||||
| import StatisticsGUI from "./UI/StatisticsGUI"; | ||||
| 
 | ||||
| // 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() | ||||
|  | @ -39,10 +40,12 @@ class Init { | |||
|         // @ts-ignore
 | ||||
|         window.mapcomplete_state = State.state; | ||||
| 
 | ||||
|         const mode = QueryParameters.GetQueryParameter("mode", "map", "The mode the application starts in, e.g. 'map' or 'dashboard'") | ||||
|         if(mode.data === "dashboard"){ | ||||
|         const mode = QueryParameters.GetQueryParameter("mode", "map", "The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'") | ||||
|         if (mode.data === "statistics") { | ||||
|             new StatisticsGUI().AttachTo("leafletDiv") | ||||
|         } else if (mode.data === "dashboard") { | ||||
|             new DashboardGui(State.state, guiState).setup() | ||||
|         }else{ | ||||
|         } else { | ||||
|             new DefaultGUI(State.state, guiState).setup() | ||||
|         } | ||||
|     } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue