From 552ea22275fa038a61ac074b0bb8b9d67a5d7813 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 25 Nov 2024 23:44:26 +0100 Subject: [PATCH] WIP: more features in inspector --- assets/layers/usertouched/usertouched.json | 67 +++++++ assets/themes/inspector/inspector.json | 13 ++ .../mapcomplete-changes.json | 4 + scripts/generateLayerOverview.ts | 14 +- src/Logic/Osm/OsmObjectDownloader.ts | 78 +++++---- src/Logic/Osm/Overpass.ts | 10 +- src/Logic/UIEventSource.ts | 1 + src/Models/Constants.ts | 1 + src/Models/ThemeConfig/ThemeConfig.ts | 5 +- src/Models/ThemeViewState.ts | 2 +- src/UI/Base/SelectedElementPanel.svelte | 11 +- .../BigComponents/SelectedElementTitle.svelte | 8 +- src/UI/History/AggregateView.svelte | 73 ++++++++ src/UI/History/History.svelte | 95 ++++++++++ src/UI/History/HistoryUtils.ts | 35 ++++ src/UI/InspectorGUI.svelte | 165 ++++++++++++++++++ src/UI/InspectorGUI.ts | 5 + src/UI/Popup/DeleteFlow/DeleteFlowState.ts | 2 +- src/UI/SpecialVisualization.ts | 2 +- 19 files changed, 526 insertions(+), 65 deletions(-) create mode 100644 assets/layers/usertouched/usertouched.json create mode 100644 assets/themes/inspector/inspector.json create mode 100644 src/UI/History/AggregateView.svelte create mode 100644 src/UI/History/History.svelte create mode 100644 src/UI/History/HistoryUtils.ts create mode 100644 src/UI/InspectorGUI.svelte create mode 100644 src/UI/InspectorGUI.ts diff --git a/assets/layers/usertouched/usertouched.json b/assets/layers/usertouched/usertouched.json new file mode 100644 index 000000000..6e75ebada --- /dev/null +++ b/assets/layers/usertouched/usertouched.json @@ -0,0 +1,67 @@ +{ + "id": "usertouched", + "description": { + "en": "Special layer showing all items which were changed by a certain user" + }, + "name": { + "en": "Changed by user" + }, + "title": { + "render": { + "en": "Changed by user" + } + }, + "source": "special", + "tagRenderings": [ + { + "id": "test", + "render": { + "en": "Changed by user" + } + }, + "all_tags" + ], + "pointRendering": [ + { + "location": [ + "point", + "centroid" + ], + "iconSize": "15,15", + "marker": [ + { + "icon": "circle", + "color": "#aaa" + }, + { + "icon": "ring", + "color": "#000" + } + ] + } + ], + "lineRendering": [ + { + "color": "black", + "width": 3, + "fillColor": "#00000000" + }, + { + "color": "#cccccccc", + "width": { + "render": 0, + "mappings": [ + { + "if": {"or": + ["_geometry:type=Polygon","_geometry:type=MultiPolygon"] + }, + "then": 20 + } + ] + }, + "offset": 15, + "fillColor": "#00000000" + } + ], + "allowMove": false +} diff --git a/assets/themes/inspector/inspector.json b/assets/themes/inspector/inspector.json new file mode 100644 index 000000000..9a531f3d8 --- /dev/null +++ b/assets/themes/inspector/inspector.json @@ -0,0 +1,13 @@ +{ + "id": "inspector", + "title": { + "en": "Inspect changes from a single user" + }, + "description": { + "en": "A theme to inspect what a single user did in the past" + }, + "icon": "./assets/svg/add.svg", + "layers": [ + "usertouched" + ] +} diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.json index 504e47bf2..9cc62392c 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.json @@ -351,6 +351,10 @@ "if": "theme=indoors", "then": "./assets/layers/entrance/entrance.svg" }, + { + "if": "theme=inspector", + "then": "./assets/svg/add.svg" + }, { "if": "theme=items_with_image", "then": "./assets/layers/item_with_image/camera.svg" diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 74cfa9a43..a61aab92b 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -643,11 +643,15 @@ class LayerOverviewUtils extends Script { LayerOverviewUtils.layerPath + sharedLayerPath.substring(sharedLayerPath.lastIndexOf("/")) if (!forceReload && !this.shouldBeUpdated(sharedLayerPath, targetPath)) { - const sharedLayer = JSON.parse(readFileSync(targetPath, "utf8")) - sharedLayers.set(sharedLayer.id, sharedLayer) - skippedLayers.push(sharedLayer.id) - ScriptUtils.erasableLog("Loaded " + sharedLayer.id) - continue + try{ + const sharedLayer = JSON.parse(readFileSync(targetPath, "utf8")) + sharedLayers.set(sharedLayer.id, sharedLayer) + skippedLayers.push(sharedLayer.id) + ScriptUtils.erasableLog("Loaded " + sharedLayer.id) + continue + }catch (e) { + throw "Could not parse "+targetPath+" : "+e + } } } diff --git a/src/Logic/Osm/OsmObjectDownloader.ts b/src/Logic/Osm/OsmObjectDownloader.ts index d9afcace4..b0e9f9cec 100644 --- a/src/Logic/Osm/OsmObjectDownloader.ts +++ b/src/Logic/Osm/OsmObjectDownloader.ts @@ -14,7 +14,7 @@ export default class OsmObjectDownloader { readonly isUploading: Store } private readonly backend: string - private historyCache = new Map>() + private historyCache = new Map>() constructor( backend: string = "https://api.openstreetmap.org", @@ -75,49 +75,51 @@ export default class OsmObjectDownloader { return await this.applyPendingChanges(obj) } - public DownloadHistory(id: NodeId): UIEventSource - - public DownloadHistory(id: WayId): UIEventSource - - public DownloadHistory(id: RelationId): UIEventSource - - public DownloadHistory(id: OsmId): UIEventSource - - public DownloadHistory(id: string): UIEventSource { - if (this.historyCache.has(id)) { - return this.historyCache.get(id) - } + private async _downloadHistoryUncached(id: string): Promise { const splitted = id.split("/") const type = splitted[0] const idN = Number(splitted[1]) - const src = new UIEventSource([]) - this.historyCache.set(id, src) - Utils.downloadJsonCached( + const data = await Utils.downloadJsonCached( `${this.backend}api/0.6/${type}/${idN}/history`, 10 * 60 * 1000 - ).then((data) => { - const elements: any[] = data.elements - const osmObjects: OsmObject[] = [] - for (const element of elements) { - let osmObject: OsmObject = null - element.nodes = [] - switch (type) { - case "node": - osmObject = new OsmNode(idN, element) - break - case "way": - osmObject = new OsmWay(idN, element) - break - case "relation": - osmObject = new OsmRelation(idN, element) - break - } - osmObject?.SaveExtraData(element, []) - osmObjects.push(osmObject) + ) + const elements: [] = data["elements"] + const osmObjects: OsmObject[] = [] + for (const element of elements) { + let osmObject: OsmObject = null + element["nodes"] = [] + switch (type) { + case "node": + osmObject = new OsmNode(idN, element) + break + case "way": + osmObject = new OsmWay(idN, element) + break + case "relation": + osmObject = new OsmRelation(idN, element) + break } - src.setData(osmObjects) - }) - return src + osmObject?.SaveExtraData(element, []) + osmObjects.push(osmObject) + } + return osmObjects + } + + public downloadHistory(id: NodeId): Promise + + public downloadHistory(id: WayId): Promise + + public downloadHistory(id: RelationId): Promise + + public downloadHistory(id: OsmId): Promise + + public async downloadHistory(id: string): Promise { + if (this.historyCache.has(id)) { + return this.historyCache.get(id) + } + const promise = this._downloadHistoryUncached(id) + this.historyCache.set(id, promise) + return promise } /** diff --git a/src/Logic/Osm/Overpass.ts b/src/Logic/Osm/Overpass.ts index 31f9b79cd..8c890db0b 100644 --- a/src/Logic/Osm/Overpass.ts +++ b/src/Logic/Osm/Overpass.ts @@ -26,7 +26,10 @@ export class Overpass { ) { this._timeout = timeout ?? new ImmutableStore(90) this._interpreterUrl = interpreterUrl - const optimized = filter.optimize() + if (filter === undefined && !extraScripts) { + throw "Filter is undefined. This is probably a bug. Alternatively, pass an 'extraScript'" + } + const optimized = filter?.optimize() if (optimized === true || optimized === false) { throw "Invalid filter: optimizes to true of false" } @@ -85,7 +88,7 @@ export class Overpass { * new Overpass(new Tag("key","value"), [], "").buildScript("{{bbox}}") // => `[out:json][timeout:90]{{bbox}};(nwr["key"="value"];);out body;out meta;>;out skel qt;` */ public buildScript(bbox: string, postCall: string = "", pretty = false): string { - const filters = this._filter.asOverpass() + const filters = this._filter?.asOverpass() ?? [] let filter = "" for (const filterOr of filters) { if (pretty) { @@ -97,12 +100,13 @@ export class Overpass { } } for (const extraScript of this._extraScripts) { - filter += "(" + extraScript + ");" + filter += extraScript } return `[out:json][timeout:${this._timeout.data}]${bbox};(${filter});out body;${ this._includeMeta ? "out meta;" : "" }>;out skel qt;` } + /** * Constructs the actual script to execute on Overpass with geocoding * 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink' diff --git a/src/Logic/UIEventSource.ts b/src/Logic/UIEventSource.ts index 77cfe209f..43f40558f 100644 --- a/src/Logic/UIEventSource.ts +++ b/src/Logic/UIEventSource.ts @@ -727,6 +727,7 @@ export class UIEventSource extends Store implements Writable { } /** + * Parse the number and round to the nearest int * * @param source * UIEventSource.asInt(new UIEventSource("123")).data // => 123 diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts index 04c69f44f..8b5292baf 100644 --- a/src/Models/Constants.ts +++ b/src/Models/Constants.ts @@ -41,6 +41,7 @@ export default class Constants { "usersettings", "icons", "filters", + "usertouched" ] as const /** * Layer IDs of layers which have special properties through built-in hooks diff --git a/src/Models/ThemeConfig/ThemeConfig.ts b/src/Models/ThemeConfig/ThemeConfig.ts index 9917007ea..d91b9525c 100644 --- a/src/Models/ThemeConfig/ThemeConfig.ts +++ b/src/Models/ThemeConfig/ThemeConfig.ts @@ -306,7 +306,7 @@ export default class ThemeConfig implements ThemeInformation { return { untranslated, total } } - public getMatchingLayer(tags: Record): LayerConfig | undefined { + public getMatchingLayer(tags: Record, blacklistLayers?: Set): LayerConfig | undefined { if (tags === undefined) { return undefined } @@ -314,6 +314,9 @@ export default class ThemeConfig implements ThemeInformation { return this.getLayer("current_view") } for (const layer of this.layers) { + if(blacklistLayers?.has(layer.id)){ + continue + } if (!layer.source) { if (layer.isShown?.matchesProperties(tags)) { return layer diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index ce21d1308..210d701aa 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -1040,7 +1040,7 @@ export default class ThemeViewState implements SpecialVisualizationState { /** * Searches the appropriate layer - will first try if a special layer matches; if not, a normal layer will be used by delegating to the theme */ - public getMatchingLayer(properties: Record) { + public getMatchingLayer(properties: Record): LayerConfig | undefined { const id = properties.id if (id.startsWith("summary_")) { diff --git a/src/UI/Base/SelectedElementPanel.svelte b/src/UI/Base/SelectedElementPanel.svelte index 617801eb1..d570b741f 100644 --- a/src/UI/Base/SelectedElementPanel.svelte +++ b/src/UI/Base/SelectedElementPanel.svelte @@ -3,24 +3,15 @@ import type { Feature } from "geojson" import SelectedElementView from "../BigComponents/SelectedElementView.svelte" import SelectedElementTitle from "../BigComponents/SelectedElementTitle.svelte" - import UserRelatedState from "../../Logic/State/UserRelatedState" - import { LastClickFeatureSource } from "../../Logic/FeatureSource/Sources/LastClickFeatureSource" import Loading from "./Loading.svelte" import { onDestroy } from "svelte" - import LayerConfig from "../../Models/ThemeConfig/LayerConfig" - import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider" - import ThemeViewState from "../../Models/ThemeViewState" export let state: SpecialVisualizationState export let selected: Feature let tags = state.featureProperties.getStore(selected.properties.id) export let absolute = true - function getLayer(properties: Record): LayerConfig { - return state.getMatchingLayer(properties) - } - - let layer = getLayer(selected.properties) + let layer = state.getMatchingLayer(selected.properties) let stillMatches = tags.map( (tags) => !layer?.source?.osmTags || layer?.source?.osmTags?.matchesProperties(tags) diff --git a/src/UI/BigComponents/SelectedElementTitle.svelte b/src/UI/BigComponents/SelectedElementTitle.svelte index 898f0437b..4a759362f 100644 --- a/src/UI/BigComponents/SelectedElementTitle.svelte +++ b/src/UI/BigComponents/SelectedElementTitle.svelte @@ -1,23 +1,21 @@ + +{#if allHistories === undefined} + +{:else if $allDiffs !== undefined} + {#each $mergedCount as diff} +
+ {JSON.stringify(diff)} +
+ {/each} +{/if} diff --git a/src/UI/History/History.svelte b/src/UI/History/History.svelte new file mode 100644 index 000000000..16ee90ed3 --- /dev/null +++ b/src/UI/History/History.svelte @@ -0,0 +1,95 @@ + + +{#if $lastStep?.layer} + +

+
+ +
+ +

+
+{/if} + +{#if !$filteredHistory} + Loading history... +{:else if $filteredHistory.length === 0} + Only geometry changes found +{:else} + + {#each $filteredHistory as { step, layer }} + + {#if step.version === 1} + + + + {/if} + {#if HistoryUtils.tagHistoryDiff(step, $fullHistory).length === 0} + + + + {:else} + {#each HistoryUtils.tagHistoryDiff(step, $fullHistory) as diff} + + + + {#if diff.oldValue === undefined} + + + {:else if diff.value === undefined } + + + {:else} + + + {/if} + + + + {/each} + {/if} + {/each} +
+

+ Created by {step.tags["_last_edit:contributor"]} +

+
+ Only changes in geometry +
{step.version}{layer?.id ?? "Unknown layer"}{diff.key}{diff.value}{diff.key} {diff.value}{diff.key} {diff.oldValue} → {diff.value}
+{/if} diff --git a/src/UI/History/HistoryUtils.ts b/src/UI/History/HistoryUtils.ts new file mode 100644 index 000000000..768d0754d --- /dev/null +++ b/src/UI/History/HistoryUtils.ts @@ -0,0 +1,35 @@ +import * as all_layers from "../../assets/generated/themes/personal.json" +import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig" +import { OsmObject } from "../../Logic/Osm/OsmObject" + +export class HistoryUtils { + + private static personalTheme = new ThemeConfig( all_layers, true) + private static ignoredLayers = new Set(["fixme"]) + public static determineLayer(properties: Record){ + return this.personalTheme.getMatchingLayer(properties, this.ignoredLayers) + } + + public static tagHistoryDiff(step: OsmObject, history: OsmObject[]): { + key: string, + value?: string, + oldValue?: string + }[] { + const previous = history[step.version - 2] + if (!previous) { + return Object.keys(step.tags).filter(key => !key.startsWith("_") && key !== "id").map(key => ({ + key, value: step.tags[key] + })) + } + const previousTags = previous.tags + return Object.keys(step.tags).filter(key => !key.startsWith("_") ) + .map(key => { + const value = step.tags[key] + const oldValue = previousTags[key] + return { + key, value, oldValue + } + }).filter(ch => ch.oldValue !== ch.value) + } + +} diff --git a/src/UI/InspectorGUI.svelte b/src/UI/InspectorGUI.svelte new file mode 100644 index 000000000..d02f513a3 --- /dev/null +++ b/src/UI/InspectorGUI.svelte @@ -0,0 +1,165 @@ + + +
+ +
+

Inspect contributor

+ load()} /> + {#if loadingData} + + {:else} + + {/if} + Back to index +
+ +
+ + + +
+ + {#if mode === "map"} + {#if $selectedElement !== undefined} + + + {/if} + +
+ +
+ {:else if mode === "table"} + {#each $featuresStore as f} +

{f.properties.id}

+ + {/each} + {:else} + + {/if} +
diff --git a/src/UI/InspectorGUI.ts b/src/UI/InspectorGUI.ts new file mode 100644 index 000000000..d26f12987 --- /dev/null +++ b/src/UI/InspectorGUI.ts @@ -0,0 +1,5 @@ +import InspectorGUI from "./InspectorGUI.svelte" + +new InspectorGUI({ + target: document.getElementById("main"), +}) diff --git a/src/UI/Popup/DeleteFlow/DeleteFlowState.ts b/src/UI/Popup/DeleteFlow/DeleteFlowState.ts index 9bab13c12..97bd82876 100644 --- a/src/UI/Popup/DeleteFlow/DeleteFlowState.ts +++ b/src/UI/Popup/DeleteFlow/DeleteFlowState.ts @@ -97,7 +97,7 @@ export class DeleteFlowState { if (allByMyself.data === null && useTheInternet) { // We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above const hist = this.objectDownloader - .DownloadHistory(id) + .downloadHistory(id) .map((versions) => versions.map((version) => Number(version.tags["_last_edit:contributor:uid"]) diff --git a/src/UI/SpecialVisualization.ts b/src/UI/SpecialVisualization.ts index 424049720..2accea518 100644 --- a/src/UI/SpecialVisualization.ts +++ b/src/UI/SpecialVisualization.ts @@ -87,7 +87,7 @@ export interface SpecialVisualizationState { readonly geocodedImages: UIEventSource readonly searchState: SearchState - getMatchingLayer(properties: Record) + getMatchingLayer(properties: Record): LayerConfig | undefined showCurrentLocationOn(map: Store): ShowDataLayer reportError(message: string | Error | XMLHttpRequest, extramessage?: string): Promise