/** * The statistics-gui shows statistics from previous MapComplete-edits */ import {UIEventSource} from "../Logic/UIEventSource"; import {VariableUiElement} from "./Base/VariableUIElement"; 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 {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; import MapState from "../Logic/State/MapState"; import BaseUIElement from "./BaseUIElement"; import Title from "./Base/Title"; import {FixedUiElement} from "./Base/FixedUiElement"; 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 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 (overview._meta.length === 0) { return "No data matched the filter" } const dateStrings = Utils.NoNull(overview._meta.map(cs => cs.properties.date)) const dates : number[] = dateStrings.map(d => new Date(d).getTime()) const mindate= Math.min(...dates) const maxdate = Math.max(...dates) const diffInDays = (maxdate - mindate) / (1000 * 60 * 60 * 24); console.log("Diff in days is ", diffInDays, "got", overview._meta.length) 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 } try{ 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: diffInDays <= 367 ? "day" : "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" )) }catch(e){ console.log("Could not generate a chart", e) elements.push(new FixedUiElement("No relevant information for "+tr.question.txt)) } } 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 = "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") } } class ChangesetsOverview { private static readonly theme_remappings = { "metamap": "maps", "groen": "buurtnatuur", "updaten van metadata met mapcomplete": "buurtnatuur", "Toevoegen of dit natuurreservaat toegangkelijk is": "buurtnatuur", "wiki:mapcomplete/fritures": "fritures", "wiki:MapComplete/Fritures": "fritures", "lits": "lit", "pomp": "cyclofix", "wiki:user:joost_schouppe/campersite": "campersite", "wiki-user-joost_schouppe-geveltuintjes": "geveltuintjes", "wiki-user-joost_schouppe-campersite": "campersite", "wiki-User-joost_schouppe-campersite": "campersite", "wiki-User-joost_schouppe-geveltuintjes": "geveltuintjes", "wiki:User:joost_schouppe/campersite": "campersite", "arbres": "arbres_llefia", "aed_brugge": "aed", "https://llefia.org/arbres/mapcomplete.json": "arbres_llefia", "https://llefia.org/arbres/mapcomplete1.json": "arbres_llefia", "toevoegen of dit natuurreservaat toegangkelijk is": "buurtnatuur", "testing mapcomplete 0.0.0": "buurtnatuur", "entrances": "indoor", "https://raw.githubusercontent.com/osmbe/play/master/mapcomplete/geveltuinen/geveltuinen.json": "geveltuintjes" } 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)) } 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) } cs.properties.theme = cs.properties.theme.toLowerCase() const remapped = ChangesetsOverview.theme_remappings[cs.properties.theme] cs.properties.theme = remapped ?? cs.properties.theme if (cs.properties.theme.startsWith("https://raw.githubusercontent.com/")) { cs.properties.theme = "gh://" + cs.properties.theme.substr("https://raw.githubusercontent.com/".length) } if (cs.properties.modify + cs.properties.delete + cs.properties.create == 0) { cs.properties.theme = "EMPTY CS" } try { cs.properties.host = new URL(cs.properties.host).host } catch (e) { } return cs } } interface ChangeSetData { "id": number, "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [number, number][][] }, "properties": { "check_user": null, "reasons": [], "tags": [], "features": [], "user": string, "uid": string, "editor": string, "comment": string, "comments_count": number, "source": string, "imagery_used": string, "date": string, "reviewed_features": [], "create": number, "modify": number, "delete": number, "area": number, "is_suspect": boolean, "harmful": any, "checked": boolean, "check_date": any, "host": string, "theme": string, "imagery": string, "language": string } }