From 716fda39aadcf63784094b8cfd50b0d4be904718 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 22 Aug 2022 13:34:47 +0200 Subject: [PATCH] Add new statistics view --- Logic/State/MapState.ts | 18 +- Models/ThemeConfig/FilterConfig.ts | 4 +- UI/Base/ChartJs.ts | 11 + UI/BaseUIElement.ts | 4 +- UI/BigComponents/FilterView.ts | 3 +- UI/BigComponents/TagRenderingChart.ts | 90 +++++--- UI/StatisticsGUI.ts | 192 +++++++++--------- .../mapcomplete-changes.json | 99 ++++++--- .../mapcomplete-changes.proto.json | 81 +++++++- index.ts | 11 +- 10 files changed, 342 insertions(+), 171 deletions(-) diff --git a/Logic/State/MapState.ts b/Logic/State/MapState.ts index 9af4edf6b..4fddb0af7 100644 --- a/Logic/State/MapState.ts +++ b/Logic/State/MapState.ts @@ -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( 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 { - return this.osmConnection + private static getPref(osmConnection: OsmConnection, key: string, layer: LayerConfig): UIEventSource { + 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([]) + 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(flayers); + return flayers; } diff --git a/Models/ThemeConfig/FilterConfig.ts b/Models/ThemeConfig/FilterConfig.ts index d649dacea..6b1d826a7 100644 --- a/Models/ThemeConfig/FilterConfig.ts +++ b/Models/ThemeConfig/FilterConfig.ts @@ -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 diff --git a/UI/Base/ChartJs.ts b/UI/Base/ChartJs.ts index 0389ee754..670637334 100644 --- a/UI/Base/ChartJs.ts +++ b/UI/Base/ChartJs.ts @@ -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; } diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index 4c084d088..e4e1becd8 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -9,8 +9,8 @@ export default abstract class BaseUIElement { protected _constructedHtmlElement: HTMLElement; protected isDestroyed = false; - private readonly clss: Set = new Set(); - private style: string; + protected readonly clss: Set = new Set(); + protected style: string; private _onClick: () => void; public onClick(f: (() => void)) { diff --git a/UI/BigComponents/FilterView.ts b/UI/BigComponents/FilterView.ts index cf3b406a7..6e2b6b805 100644 --- a/UI/BigComponents/FilterView.ts +++ b/UI/BigComponents/FilterView.ts @@ -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({}) 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") diff --git a/UI/BigComponents/TagRenderingChart.ts b/UI/BigComponents/TagRenderingChart.ts index cce769caf..d1eeee155 100644 --- a/UI/BigComponents/TagRenderingChart.ts +++ b/UI/BigComponents/TagRenderingChart.ts @@ -7,32 +7,45 @@ import {Utils} from "../../Utils"; import {OsmFeature} from "../../Models/OsmFeature"; export interface TagRenderingChartOptions { - + groupToOtherCutoff?: 3 | number, sort?: boolean } 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,16 +111,18 @@ 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(); features.forEach((value, key) => { const d = new Date(value.properties.date); Utils.SetMidnight(d) - + if (earliest === undefined) { earliest = d } else if (d < earliest) { @@ -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 }[], tagRendering: TagRenderingConfig, options?: TagRenderingChartOptions & { chartclasses?: string, + constructor(features: { properties: Record }[], 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") @@ -167,10 +194,10 @@ export default class TagRenderingChart extends Combine { if (labels === undefined || data === undefined) { super([]) this.SetClass("hidden") - return + return } - - + + const borderColor = [TagRenderingChart.unkownBorderColor, TagRenderingChart.otherBorderColor, TagRenderingChart.notApplicableBorderColor] const backgroundColor = [TagRenderingChart.unkownColor, TagRenderingChart.otherColor, TagRenderingChart.notApplicableColor] @@ -188,9 +215,8 @@ export default class TagRenderingChart extends Combine { backgroundColor.splice(i, 1) } } - - - + + let barchartMode = tagRendering.multiAnswer; if (labels.length > 9) { barchartMode = true; @@ -231,15 +257,15 @@ export default class TagRenderingChart extends Combine { this.SetClass("block") } - - public static extractDataAndLabels}>(tagRendering: TagRenderingConfig, features: T[], options?:TagRenderingChartOptions): {labels: string[], data: T[][]} { + + public static extractDataAndLabels }>(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 = {} - 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,9 +333,9 @@ 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} } - + } \ No newline at end of file diff --git a/UI/StatisticsGUI.ts b/UI/StatisticsGUI.ts index 51ba553ad..9ccf5b864 100644 --- a/UI/StatisticsGUI.ts +++ b/UI/StatisticsGUI.ts @@ -6,84 +6,105 @@ 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>(new Map()) +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 => { + const downloaded = new UIEventSource<{ features: ChangeSetData[] }[]>([]) + + for (const filepath of paths) { + Utils.downloadJson(homeUrl + filepath).then(data => { + data?.features?.forEach(item => { + item.properties = {...item.properties, ...item.properties.metadata} + delete item.properties.metadata + }) + downloaded.data.push(data) + downloaded.ping() + }) + } + + 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 + if (tf === undefined) { + return + } + overview = overview.filter(cs => tf.matchesProperties(cs.properties)) + }) + } + + if (downloaded.length === 0) { + return "No data matched the filter" + } + + 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, 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") + ]) + this.SetClass("block w-full h-full") + } +} + +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...") } - const downloaded = new UIEventSource<{ features: ChangeSetData[] }[]>([]) + + return new StatisticsForOverviewFile(StatisticsGUI.homeUrl, paths) - for (const filepath of paths) { - Utils.downloadJson(StatisticsGUI.homeUrl + filepath).then(data => { - data.features.forEach(item => { - item.properties = {...item.properties, ...item.properties.metadata} - delete item.properties.metadata - }) - downloaded.data.push(data) - downloaded.ping() - }) - } - - 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) - - 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)) - }) - } - - if (downloaded.length === 0) { - return "No data matched the filter" - } - return new Combine(layer.tagRenderings.map(tr => { - - try { - - return new StackedRenderingChart(tr, overview._meta, "month") - } catch (e) { - return "Could not create stats for " + tr.id - } - }) - ) - }, [appliedFilters])).SetClass("block w-full h-full") - ]).SetClass("block w-full h-full") - - - })).SetClass("block w-full h-full").AttachTo("maindiv") - - const filteredLayer = { - appliedFilters, - layerDef: layer, - isDisplayed: new UIEventSource(true) - } - new LayerFilterPanel(undefined, filteredLayer).AttachTo("extradiv") + })) + 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( - this._meta, - this.themeBreakdown, - { - chartType: "doughnut", - sort: true, - groupToOtherCutoff: 25 - } - ) - } - - } interface ChangeSetData { diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.json index d2de2927f..d5e3765ad 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.json @@ -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 {_last_edit:contributor}{user}" - }, "question": { - "en": "What contributor made this change?" + "en": "What contributor did make this change?" }, "freeform": { "key": "user" + }, + "render": { + "en": "Change made by {user}" } }, { "id": "theme", - "render": { - "en": "Change with theme {theme}" - }, - "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 {theme}" + } + }, + { + "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 {host}" + }, + "question": { + "en": "What host (website) was this change made with?" + }, + "freeform": { + "key": "host" + }, "mappings": [ { - "hideInAnswer": true, - "if": "theme~http.*", - "then": { - "en": "Change with unofficial theme {theme}" - } + "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}" + } + } + ] } ] }, diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json index 3fbb16efb..bdb07d516 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json @@ -48,21 +48,60 @@ }, { "id": "contributor", + "question": { + "en": "What contributor did make this change?" + }, + "freeform": { + "key": "user" + }, "render": { - "en": "Change made by {_last_edit:contributor}" + "en": "Change made by {user}" } }, { "id": "theme", + "question": { + "en": "What theme was used to make this change?" + }, + "freeform": { + "key": "theme" + }, "render": { "en": "Change with theme {theme}" + } + }, + { + "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 {host}" + }, + "question": { + "en": "What host (website) was this change made with?" + }, + "freeform": { + "key": "host" }, "mappings": [ { - "if": "theme~http.*", - "then": { - "en": "Change with unofficial theme {theme}" - }, + "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}" + } + } + ] } ] }, diff --git a/index.ts b/index.ts index 3221d0c07..14aef1cd5 100644 --- a/index.ts +++ b/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() @@ -38,11 +39,13 @@ class Init { // This 'leaks' the global state via the window object, useful for debugging // @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() } }