forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			215 lines
		
	
	
	
		
			8.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			215 lines
		
	
	
	
		
			8.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * 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";
 | |
| 
 | |
| 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 (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, <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")
 | |
|         ])
 | |
|             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").AttachTo("maindiv")
 | |
| 
 | |
|       
 | |
|     }
 | |
| }
 | |
| 
 | |
| 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
 | |
|     }
 | |
| }
 |