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 {TiledStaticFeatureSource} from "../FeatureSource/Sources/StaticFeatureSource";
 | 
				
			||||||
import {Translation, TypedTranslation} from "../../UI/i18n/Translation";
 | 
					import {Translation, TypedTranslation} from "../../UI/i18n/Translation";
 | 
				
			||||||
import {Tag} from "../Tags/Tag";
 | 
					import {Tag} from "../Tags/Tag";
 | 
				
			||||||
 | 
					import {OsmConnection} from "../Osm/OsmConnection";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface GlobalFilter {
 | 
					export interface GlobalFilter {
 | 
				
			||||||
| 
						 | 
					@ -143,7 +144,7 @@ export default class MapState extends UserRelatedState {
 | 
				
			||||||
                config: c,
 | 
					                config: c,
 | 
				
			||||||
                isDisplayed: QueryParameters.GetBooleanQueryParameter("overlay-" + c.id, c.defaultState, "Wether or not the overlay " + c.id + " is shown")
 | 
					                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()
 | 
					        this.lockBounds()
 | 
				
			||||||
| 
						 | 
					@ -353,8 +354,8 @@ export default class MapState extends UserRelatedState {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private getPref(key: string, layer: LayerConfig): UIEventSource<boolean> {
 | 
					    private static getPref(osmConnection: OsmConnection, key: string, layer: LayerConfig): UIEventSource<boolean> {
 | 
				
			||||||
        return this.osmConnection
 | 
					        return osmConnection
 | 
				
			||||||
            .GetPreference(key, layer.shownByDefault + "")
 | 
					            .GetPreference(key, layer.shownByDefault + "")
 | 
				
			||||||
            .sync(v => {
 | 
					            .sync(v => {
 | 
				
			||||||
                if (v === undefined) {
 | 
					                if (v === undefined) {
 | 
				
			||||||
| 
						 | 
					@ -369,10 +370,9 @@ export default class MapState extends UserRelatedState {
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private InitializeFilteredLayers() {
 | 
					    public static InitializeFilteredLayers(layoutToUse: {layers: LayerConfig[], id: string}, osmConnection: OsmConnection): FilteredLayer[] {
 | 
				
			||||||
        const layoutToUse = this.layoutToUse;
 | 
					 | 
				
			||||||
        if (layoutToUse === undefined) {
 | 
					        if (layoutToUse === undefined) {
 | 
				
			||||||
            return new UIEventSource<FilteredLayer[]>([])
 | 
					            return []
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        const flayers: FilteredLayer[] = [];
 | 
					        const flayers: FilteredLayer[] = [];
 | 
				
			||||||
        for (const layer of layoutToUse.layers) {
 | 
					        for (const layer of layoutToUse.layers) {
 | 
				
			||||||
| 
						 | 
					@ -380,9 +380,9 @@ export default class MapState extends UserRelatedState {
 | 
				
			||||||
            if (layer.syncSelection === "local") {
 | 
					            if (layer.syncSelection === "local") {
 | 
				
			||||||
                isDisplayed = LocalStorageSource.GetParsed(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer.shownByDefault)
 | 
					                isDisplayed = LocalStorageSource.GetParsed(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer.shownByDefault)
 | 
				
			||||||
            } else if (layer.syncSelection === "theme-only") {
 | 
					            } 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") {
 | 
					            } else if (layer.syncSelection === "global") {
 | 
				
			||||||
                isDisplayed = this.getPref("layer-" + layer.id + "-enabled", layer)
 | 
					                isDisplayed = MapState.getPref(osmConnection,"layer-" + layer.id + "-enabled", layer)
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                isDisplayed = QueryParameters.GetBooleanQueryParameter("layer-" + layer.id, layer.shownByDefault, "Wether or not layer " + layer.id + " is shown")
 | 
					                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 {TagUtils} from "../../Logic/Tags/TagUtils";
 | 
				
			||||||
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
 | 
					import ValidatedTextField from "../../UI/Input/ValidatedTextField";
 | 
				
			||||||
import {TagConfigJson} from "./Json/TagConfigJson";
 | 
					import {TagConfigJson} from "./Json/TagConfigJson";
 | 
				
			||||||
import {UIEventSource} from "../../Logic/UIEventSource";
 | 
					import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource";
 | 
				
			||||||
import {FilterState} from "../FilteredLayer";
 | 
					import {FilterState} from "../FilteredLayer";
 | 
				
			||||||
import {QueryParameters} from "../../Logic/Web/QueryParameters";
 | 
					import {QueryParameters} from "../../Logic/Web/QueryParameters";
 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
import {RegexTag} from "../../Logic/Tags/RegexTag";
 | 
					import {RegexTag} from "../../Logic/Tags/RegexTag";
 | 
				
			||||||
 | 
					import BaseUIElement from "../../UI/BaseUIElement";
 | 
				
			||||||
 | 
					import {InputElement} from "../../UI/Input/InputElement";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class FilterConfig {
 | 
					export default class FilterConfig {
 | 
				
			||||||
    public readonly id: string
 | 
					    public readonly id: string
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,17 @@ export default class ChartJs<
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    protected InnerConstructElement(): HTMLElement {
 | 
					    protected InnerConstructElement(): HTMLElement {
 | 
				
			||||||
        const canvas = document.createElement("canvas");
 | 
					        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);
 | 
					        new Chart(canvas, this._config);
 | 
				
			||||||
        return canvas;
 | 
					        return canvas;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,8 +9,8 @@ export default abstract class BaseUIElement {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected _constructedHtmlElement: HTMLElement;
 | 
					    protected _constructedHtmlElement: HTMLElement;
 | 
				
			||||||
    protected isDestroyed = false;
 | 
					    protected isDestroyed = false;
 | 
				
			||||||
    private readonly clss: Set<string> = new Set<string>();
 | 
					    protected readonly clss: Set<string> = new Set<string>();
 | 
				
			||||||
    private style: string;
 | 
					    protected style: string;
 | 
				
			||||||
    private _onClick: () => void;
 | 
					    private _onClick: () => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public onClick(f: (() => void)) {
 | 
					    public onClick(f: (() => void)) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -171,7 +171,7 @@ export class LayerFilterPanel extends Combine {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            ui.SetClass("mt-1")
 | 
					            ui.SetClass("mt-1")
 | 
				
			||||||
            toShow.push(ui)
 | 
					            toShow.push(ui)
 | 
				
			||||||
            actualTags.addCallback(tagsToFilterFor => {
 | 
					            actualTags.addCallbackAndRun(tagsToFilterFor => {
 | 
				
			||||||
                flayer.appliedFilters.data.set(filter.id, tagsToFilterFor)
 | 
					                flayer.appliedFilters.data.set(filter.id, tagsToFilterFor)
 | 
				
			||||||
                flayer.appliedFilters.ping()
 | 
					                flayer.appliedFilters.ping()
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
| 
						 | 
					@ -195,6 +195,7 @@ export class LayerFilterPanel extends Combine {
 | 
				
			||||||
        const properties = new UIEventSource<any>({})
 | 
					        const properties = new UIEventSource<any>({})
 | 
				
			||||||
        for (const {name, type} of filter.fields) {
 | 
					        for (const {name, type} of filter.fields) {
 | 
				
			||||||
            const value = QueryParameters.GetQueryParameter("filter-" + filterConfig.id + "-" + name, "", "Value for filter " + filterConfig.id)
 | 
					            const value = QueryParameters.GetQueryParameter("filter-" + filterConfig.id + "-" + name, "", "Value for filter " + filterConfig.id)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
            const field = ValidatedTextField.ForType(type).ConstructInputElement({
 | 
					            const field = ValidatedTextField.ForType(type).ConstructInputElement({
 | 
				
			||||||
                value
 | 
					                value
 | 
				
			||||||
            }).SetClass("inline-block")
 | 
					            }).SetClass("inline-block")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,26 +13,39 @@ export interface TagRenderingChartOptions {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class StackedRenderingChart extends ChartJs {
 | 
					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, {
 | 
					        const {labels, data} = TagRenderingChart.extractDataAndLabels(tr, features, {
 | 
				
			||||||
            sort: true
 | 
					            sort: true,
 | 
				
			||||||
 | 
					            groupToOtherCutoff: options?.groupToOtherCutoff
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        if (labels === undefined || data === undefined) {
 | 
					        if (labels === undefined || data === undefined) {
 | 
				
			||||||
            throw ("No labels or data given...")
 | 
					            throw ("No labels or data given...")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        // labels: ["cyclofix", "buurtnatuur", ...]; data : [ ["cyclofix-changeset", "cyclofix-changeset", ...], ["buurtnatuur-cs", "buurtnatuur-cs"], ... ]
 | 
					        // 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 datasets: { label: string /*themename*/, data: number[]/*counts per day*/, backgroundColor: string }[] = []
 | 
				
			||||||
        const allDays = StackedRenderingChart.getAllDays(features)
 | 
					        const allDays = StackedRenderingChart.getAllDays(features)
 | 
				
			||||||
        let trimmedDays = allDays.map(d => d.substr(0, d.indexOf("T")))
 | 
					        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 = trimmedDays.map(d => d.substr(0, 7))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        trimmedDays = Utils.Dedup(trimmedDays)
 | 
					        trimmedDays = Utils.Dedup(trimmedDays)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (let i = 0; i < labels.length; i++) {
 | 
					        for (let i = 0; i < labels.length; i++) {
 | 
				
			||||||
            const label = labels[i];
 | 
					            const label = labels[i];
 | 
				
			||||||
            const changesetsForTheme = data[i]
 | 
					            const changesetsForTheme = data[i]
 | 
				
			||||||
| 
						 | 
					@ -41,7 +54,7 @@ export class StackedRenderingChart extends ChartJs {
 | 
				
			||||||
                const csDate = new Date(changeset.properties.date)
 | 
					                const csDate = new Date(changeset.properties.date)
 | 
				
			||||||
                Utils.SetMidnight(csDate)
 | 
					                Utils.SetMidnight(csDate)
 | 
				
			||||||
                let str = csDate.toISOString();
 | 
					                let str = csDate.toISOString();
 | 
				
			||||||
                if (period === "month") {
 | 
					                if (options?.period === "month") {
 | 
				
			||||||
                    csDate.setUTCDate(1)
 | 
					                    csDate.setUTCDate(1)
 | 
				
			||||||
                    str = csDate.toISOString().substr(0, 7);
 | 
					                    str = csDate.toISOString().substr(0, 7);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
| 
						 | 
					@ -57,13 +70,23 @@ export class StackedRenderingChart extends ChartJs {
 | 
				
			||||||
                const day = trimmedDays[i];
 | 
					                const day = trimmedDays[i];
 | 
				
			||||||
                countsPerDay[i] = perDay[day]?.length ?? 0
 | 
					                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({
 | 
					            datasets.push({
 | 
				
			||||||
                data: countsPerDay,
 | 
					                data: countsPerDay,
 | 
				
			||||||
                backgroundColor: TagRenderingChart.borderColors[i % TagRenderingChart.borderColors.length],
 | 
					                backgroundColor,
 | 
				
			||||||
                label
 | 
					                label
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const perDayData = {
 | 
					        const perDayData = {
 | 
				
			||||||
            labels: trimmedDays,
 | 
					            labels: trimmedDays,
 | 
				
			||||||
            datasets
 | 
					            datasets
 | 
				
			||||||
| 
						 | 
					@ -88,9 +111,11 @@ export class StackedRenderingChart extends ChartJs {
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        super(config)
 | 
					        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 earliest: Date = undefined
 | 
				
			||||||
        let latest: Date = undefined;
 | 
					        let latest: Date = undefined;
 | 
				
			||||||
        let allDates = new Set<string>();
 | 
					        let allDates = new Set<string>();
 | 
				
			||||||
| 
						 | 
					@ -123,13 +148,13 @@ export class StackedRenderingChart extends ChartJs {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class TagRenderingChart extends Combine {
 | 
					export default class TagRenderingChart extends Combine {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static readonly unkownColor = 'rgba(128, 128, 128, 0.2)'
 | 
					    public static readonly unkownColor = 'rgba(128, 128, 128, 0.2)'
 | 
				
			||||||
    private static readonly unkownBorderColor = '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)'
 | 
					    public static readonly otherColor = 'rgba(128, 128, 128, 0.2)'
 | 
				
			||||||
    private static readonly otherBorderColor = 'rgba(128, 128, 255)'
 | 
					    public static readonly otherBorderColor = 'rgba(128, 128, 255)'
 | 
				
			||||||
    private static readonly notApplicableColor = 'rgba(128, 128, 128, 0.2)'
 | 
					    public static readonly notApplicableColor = 'rgba(128, 128, 128, 0.2)'
 | 
				
			||||||
    private static readonly notApplicableBorderColor = 'rgba(255, 0, 0)'
 | 
					    public static readonly notApplicableBorderColor = 'rgba(255, 0, 0)'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static readonly backgroundColors = [
 | 
					    public static readonly backgroundColors = [
 | 
				
			||||||
| 
						 | 
					@ -153,10 +178,12 @@ export default class TagRenderingChart extends Combine {
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Creates a chart about this tagRendering for the given data
 | 
					     * 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,
 | 
					        chartstyle?: string,
 | 
				
			||||||
        includeTitle?: boolean,
 | 
					        includeTitle?: boolean,
 | 
				
			||||||
        chartType?: "pie" | "bar" | "doughnut" }) {
 | 
					        chartType?: "pie" | "bar" | "doughnut"
 | 
				
			||||||
 | 
					    }) {
 | 
				
			||||||
        if (tagRendering.mappings?.length === 0 && tagRendering.freeform?.key === undefined) {
 | 
					        if (tagRendering.mappings?.length === 0 && tagRendering.freeform?.key === undefined) {
 | 
				
			||||||
            super([])
 | 
					            super([])
 | 
				
			||||||
            this.SetClass("hidden")
 | 
					            this.SetClass("hidden")
 | 
				
			||||||
| 
						 | 
					@ -190,7 +217,6 @@ export default class TagRenderingChart extends Combine {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let barchartMode = tagRendering.multiAnswer;
 | 
					        let barchartMode = tagRendering.multiAnswer;
 | 
				
			||||||
        if (labels.length > 9) {
 | 
					        if (labels.length > 9) {
 | 
				
			||||||
            barchartMode = true;
 | 
					            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 ?? []
 | 
					        const mappings = tagRendering.mappings ?? []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        options = options ?? {}
 | 
					        options = options ?? {}
 | 
				
			||||||
        let unknownCount : T[] = [];
 | 
					        let unknownCount: T[] = [];
 | 
				
			||||||
        const categoryCounts : T[][]= mappings.map(_ => [])
 | 
					        const categoryCounts: T[][] = mappings.map(_ => [])
 | 
				
			||||||
        const otherCounts: Record<string, T[]> = {}
 | 
					        const otherCounts: Record<string, T[]> = {}
 | 
				
			||||||
        let notApplicable : T[] = [];
 | 
					        let notApplicable: T[] = [];
 | 
				
			||||||
        for (const feature of features) {
 | 
					        for (const feature of features) {
 | 
				
			||||||
            const props = feature.properties
 | 
					            const props = feature.properties
 | 
				
			||||||
            if (tagRendering.condition !== undefined && !tagRendering.condition.matchesProperties(props)) {
 | 
					            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) {
 | 
					                if (tagRendering.freeform?.key !== undefined && props[tagRendering.freeform.key] !== undefined) {
 | 
				
			||||||
                    const otherValue = props[tagRendering.freeform.key]
 | 
					                    const otherValue = props[tagRendering.freeform.key]
 | 
				
			||||||
                    otherCounts[otherValue] = (otherCounts[otherValue] ?? [])
 | 
					                    otherCounts[otherValue] = (otherCounts[otherValue] ?? [])
 | 
				
			||||||
                     otherCounts[otherValue]      .push(feature)
 | 
					                    otherCounts[otherValue].push(feature)
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    unknownCount.push(feature)
 | 
					                    unknownCount.push(feature)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
| 
						 | 
					@ -286,7 +312,7 @@ export default class TagRenderingChart extends Combine {
 | 
				
			||||||
            return {labels: undefined, data: undefined}
 | 
					            return {labels: undefined, data: undefined}
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let otherGrouped : T[] = [];
 | 
					        let otherGrouped: T[] = [];
 | 
				
			||||||
        const otherLabels: string[] = []
 | 
					        const otherLabels: string[] = []
 | 
				
			||||||
        const otherData: T[][] = []
 | 
					        const otherData: T[][] = []
 | 
				
			||||||
        const sortedOtherCounts: [string, 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 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}
 | 
					        return {labels, data}
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,83 +6,104 @@ import {VariableUiElement} from "./Base/VariableUIElement";
 | 
				
			||||||
import Loading from "./Base/Loading";
 | 
					import Loading from "./Base/Loading";
 | 
				
			||||||
import {Utils} from "../Utils";
 | 
					import {Utils} from "../Utils";
 | 
				
			||||||
import Combine from "./Base/Combine";
 | 
					import Combine from "./Base/Combine";
 | 
				
			||||||
import BaseUIElement from "./BaseUIElement";
 | 
					import {StackedRenderingChart} from "./BigComponents/TagRenderingChart";
 | 
				
			||||||
import TagRenderingChart, {StackedRenderingChart} from "./BigComponents/TagRenderingChart";
 | 
					import {LayerFilterPanel} from "./BigComponents/FilterView";
 | 
				
			||||||
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
 | 
					 | 
				
			||||||
import FilterView, {LayerFilterPanel} from "./BigComponents/FilterView";
 | 
					 | 
				
			||||||
import FilteredLayer, {FilterState} from "../Models/FilteredLayer";
 | 
					 | 
				
			||||||
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
 | 
					import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
 | 
				
			||||||
 | 
					import MapState from "../Logic/State/MapState";
 | 
				
			||||||
 | 
					import BaseUIElement from "./BaseUIElement";
 | 
				
			||||||
 | 
					import Title from "./Base/Title";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class StatisticsGUI {
 | 
					class StatisticsForOverviewFile extends Combine{
 | 
				
			||||||
 | 
					    constructor(homeUrl: string, paths: string[]) {
 | 
				
			||||||
    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>())
 | 
					 | 
				
			||||||
        const layer = AllKnownLayouts.allKnownLayouts.get("mapcomplete-changes").layers[0]
 | 
					        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, <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 = "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) {
 | 
					            if (paths === undefined) {
 | 
				
			||||||
                return new Loading("Loading overview...")
 | 
					                return new Loading("Loading overview...")
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            const downloaded = new UIEventSource<{ features: ChangeSetData[] }[]>([])
 | 
					 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            for (const filepath of paths) {
 | 
					            return new StatisticsForOverviewFile(StatisticsGUI.homeUrl, 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")),
 | 
					            this.SetClass("block w-full h-full").AttachTo("maindiv")
 | 
				
			||||||
                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, <any>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 = <FilteredLayer>{
 | 
					 | 
				
			||||||
            appliedFilters,
 | 
					 | 
				
			||||||
            layerDef: layer,
 | 
					 | 
				
			||||||
            isDisplayed: new UIEventSource<boolean>(true)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        new LayerFilterPanel(undefined, filteredLayer).AttachTo("extradiv")
 | 
					 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -116,11 +137,11 @@ class ChangesetsOverview {
 | 
				
			||||||
    public readonly _meta: ChangeSetData[];
 | 
					    public readonly _meta: ChangeSetData[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static fromDirtyData(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[]) {
 | 
					    private constructor(meta: ChangeSetData[]) {
 | 
				
			||||||
        this._meta = meta;
 | 
					        this._meta = Utils.NoNull(meta);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public filter(predicate: (cs: ChangeSetData) => boolean) {
 | 
					    public filter(predicate: (cs: ChangeSetData) => boolean) {
 | 
				
			||||||
| 
						 | 
					@ -128,6 +149,13 @@ class ChangesetsOverview {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static cleanChangesetData(cs: ChangeSetData): ChangeSetData {
 | 
					    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) {
 | 
					        if (cs.properties.theme === undefined) {
 | 
				
			||||||
            cs.properties.theme = cs.properties.comment.substr(cs.properties.comment.lastIndexOf("#") + 1)
 | 
					            cs.properties.theme = cs.properties.comment.substr(cs.properties.comment.lastIndexOf("#") + 1)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -148,28 +176,6 @@ class ChangesetsOverview {
 | 
				
			||||||
        return cs
 | 
					        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 {
 | 
					interface ChangeSetData {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,9 +31,6 @@
 | 
				
			||||||
        "geoJsonZoomLevel": 8,
 | 
					        "geoJsonZoomLevel": 8,
 | 
				
			||||||
        "maxCacheAge": 0
 | 
					        "maxCacheAge": 0
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "calculatedTags": [
 | 
					 | 
				
			||||||
        "_last_edit:contributor:lc:=feat.properties['_last_edit:contributor'].toLowerCase()"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "title": {
 | 
					      "title": {
 | 
				
			||||||
        "render": {
 | 
					        "render": {
 | 
				
			||||||
          "en": "Changeset for {theme}"
 | 
					          "en": "Changeset for {theme}"
 | 
				
			||||||
| 
						 | 
					@ -51,34 +48,61 @@
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          "id": "contributor",
 | 
					          "id": "contributor",
 | 
				
			||||||
          "render": {
 | 
					 | 
				
			||||||
            "en": "Change made by <a href='https://openstreetmap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}{user}</a>"
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "question": {
 | 
					          "question": {
 | 
				
			||||||
            "en": "What contributor made this change?"
 | 
					            "en": "What contributor did make this change?"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          "freeform": {
 | 
					          "freeform": {
 | 
				
			||||||
            "key": "user"
 | 
					            "key": "user"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "render": {
 | 
				
			||||||
 | 
					            "en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          "id": "theme",
 | 
					          "id": "theme",
 | 
				
			||||||
          "render": {
 | 
					          "question": {
 | 
				
			||||||
            "en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>"
 | 
					            "en": "What theme was used to make this change?"
 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "question":{
 | 
					 | 
				
			||||||
            "en": "What theme was this change made with?"
 | 
					 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          "freeform": {
 | 
					          "freeform": {
 | 
				
			||||||
            "key": "theme"
 | 
					            "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": [
 | 
					          "mappings": [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              "hideInAnswer": true,
 | 
					              "if": "host~https://mapcomplete.osm.be/.*",
 | 
				
			||||||
              "if": "theme~http.*",
 | 
					              "then": "MapComplete",
 | 
				
			||||||
              "then": {
 | 
					              "hideInAnswer": true
 | 
				
			||||||
                "en": "Change with <b>unofficial</b> theme <a href='https://mapcomplete.osm.be/theme.html?userlayout={theme}'>{theme}</a>"
 | 
					            },
 | 
				
			||||||
              }
 | 
					            {
 | 
				
			||||||
 | 
					              "if": "host~https://pietervdvn.github.io/mc/develop/.*",
 | 
				
			||||||
 | 
					              "then": "Develop",
 | 
				
			||||||
 | 
					              "hideInAnswer": true
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          ]
 | 
					          ]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -394,12 +418,7 @@
 | 
				
			||||||
          "id": "created_by",
 | 
					          "id": "created_by",
 | 
				
			||||||
          "options": [
 | 
					          "options": [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              "osmTags": {
 | 
					              "osmTags": "user~i~.*{search}.*",
 | 
				
			||||||
                "or":[
 | 
					 | 
				
			||||||
                  "_last_edit:contributor:lc~i~.*{search}.*",
 | 
					 | 
				
			||||||
                "user~i~.*{search}.*"
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              "fields": [
 | 
					              "fields": [
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                  "name": "search"
 | 
					                  "name": "search"
 | 
				
			||||||
| 
						 | 
					@ -415,7 +434,7 @@
 | 
				
			||||||
          "id": "not_created_by",
 | 
					          "id": "not_created_by",
 | 
				
			||||||
          "options": [
 | 
					          "options": [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              "osmTags": "_last_edit:contributor:lc!~i~.*{search}.*",
 | 
					              "osmTags": "user!~i~.*{search}.*",
 | 
				
			||||||
              "fields": [
 | 
					              "fields": [
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                  "name": "search"
 | 
					                  "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",
 | 
					          "id": "contributor",
 | 
				
			||||||
 | 
					          "question": {
 | 
				
			||||||
 | 
					            "en": "What contributor did make this change?"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "freeform": {
 | 
				
			||||||
 | 
					            "key": "user"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "render": {
 | 
					          "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",
 | 
					          "id": "theme",
 | 
				
			||||||
 | 
					          "question": {
 | 
				
			||||||
 | 
					            "en": "What theme was used to make this change?"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "freeform": {
 | 
				
			||||||
 | 
					            "key": "theme"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "render": {
 | 
					          "render": {
 | 
				
			||||||
            "en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>"
 | 
					            "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": [
 | 
					          "mappings": [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              "if": "theme~http.*",
 | 
					              "if": "host=www.waldbrand-app.de",
 | 
				
			||||||
              "then": {
 | 
					              "then": "waldbrand-app.de",
 | 
				
			||||||
                "en": "Change with <b>unofficial</b> theme <a href='https://mapcomplete.osm.be/theme.html?userlayout={theme}'>{theme}</a>"
 | 
					              "hideInAnswer": true
 | 
				
			||||||
              },
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              "if": "host~https://pietervdvn.github.io/mc/develop/.*",
 | 
				
			||||||
 | 
					              "then": "Develop",
 | 
				
			||||||
              "hideInAnswer": true
 | 
					              "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 {DefaultGuiState} from "./UI/DefaultGuiState";
 | 
				
			||||||
import {QueryParameters} from "./Logic/Web/QueryParameters";
 | 
					import {QueryParameters} from "./Logic/Web/QueryParameters";
 | 
				
			||||||
import DashboardGui from "./UI/DashboardGui";
 | 
					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
 | 
					// 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()
 | 
					MinimapImplementation.initialize()
 | 
				
			||||||
| 
						 | 
					@ -39,10 +40,12 @@ class Init {
 | 
				
			||||||
        // @ts-ignore
 | 
					        // @ts-ignore
 | 
				
			||||||
        window.mapcomplete_state = State.state;
 | 
					        window.mapcomplete_state = State.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const mode = QueryParameters.GetQueryParameter("mode", "map", "The mode the application starts in, e.g. 'map' or 'dashboard'")
 | 
					        const mode = QueryParameters.GetQueryParameter("mode", "map", "The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'")
 | 
				
			||||||
        if(mode.data === "dashboard"){
 | 
					        if (mode.data === "statistics") {
 | 
				
			||||||
 | 
					            new StatisticsGUI().AttachTo("leafletDiv")
 | 
				
			||||||
 | 
					        } else if (mode.data === "dashboard") {
 | 
				
			||||||
            new DashboardGui(State.state, guiState).setup()
 | 
					            new DashboardGui(State.state, guiState).setup()
 | 
				
			||||||
        }else{
 | 
					        } else {
 | 
				
			||||||
            new DefaultGUI(State.state, guiState).setup()
 | 
					            new DefaultGUI(State.state, guiState).setup()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue