From 552ea22275fa038a61ac074b0bb8b9d67a5d7813 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 25 Nov 2024 23:44:26 +0100 Subject: [PATCH 1/8] 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 0000000000..6e75ebada4 --- /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 0000000000..9a531f3d80 --- /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 504e47bf2b..9cc62392c5 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 74cfa9a43f..a61aab92b9 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 d9afcace44..b0e9f9cec6 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 31f9b79cdd..8c890db0b7 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 77cfe209f7..43f40558ff 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 04c69f44f5..8b5292baf2 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 9917007eaf..d91b9525c1 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 ce21d1308f..210d701aa2 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 617801eb1e..d570b741f5 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 898f0437b3..4a759362ff 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 0000000000..16ee90ed33 --- /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 0000000000..768d0754df --- /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 0000000000..d02f513a33 --- /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 0000000000..d26f129876 --- /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 9bab13c120..97bd828769 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 4240497202..2accea5187 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 From 951bd3c0aee598d23fad9ded2aede2b8b6496ade Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 1 Dec 2024 01:39:13 +0100 Subject: [PATCH 2/8] More work on inspector --- assets/layers/questions/questions.json | 14 ++- src/Logic/ImageProviders/Panoramax.ts | 8 +- src/UI/History/AggregateImages.svelte | 43 +++++++ src/UI/History/AggregateView.svelte | 80 ++++++++---- .../History/AttributedPanoramaxImage.svelte | 13 ++ src/UI/History/History.svelte | 119 +++++++++--------- src/UI/History/HistoryUtils.ts | 24 +++- src/UI/Image/AttributedImage.svelte | 10 +- src/UI/InspectorGUI.svelte | 54 +++++--- 9 files changed, 257 insertions(+), 108 deletions(-) create mode 100644 src/UI/History/AggregateImages.svelte create mode 100644 src/UI/History/AttributedPanoramaxImage.svelte diff --git a/assets/layers/questions/questions.json b/assets/layers/questions/questions.json index a5b70f1828..8ca79bbffe 100644 --- a/assets/layers/questions/questions.json +++ b/assets/layers/questions/questions.json @@ -3077,7 +3077,19 @@ } } ] + }, + { + "id": "name", + "question":{ + "en": "What is the name of this place?" + }, + "render": { + "*": "{name}" + }, + "freeform": { + "key": "name" + } } ], "allowMove": false -} \ No newline at end of file +} diff --git a/src/Logic/ImageProviders/Panoramax.ts b/src/Logic/ImageProviders/Panoramax.ts index bd8970efee..4f818ba187 100644 --- a/src/Logic/ImageProviders/Panoramax.ts +++ b/src/Logic/ImageProviders/Panoramax.ts @@ -12,7 +12,7 @@ import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte" import Link from "../../UI/Base/Link" export default class PanoramaxImageProvider extends ImageProvider { - public static readonly singleton = new PanoramaxImageProvider() + public static readonly singleton: PanoramaxImageProvider = new PanoramaxImageProvider() private static readonly xyz = new PanoramaxXYZ() private static defaultPanoramax = new AuthorizedPanoramax( Constants.panoramax.url, @@ -126,7 +126,11 @@ export default class PanoramaxImageProvider extends ImageProvider { if (!Panoramax.isId(value)) { return undefined } - return [await this.getInfoFor(value).then((r) => this.featureToImage(r))] + return [await this.getInfo(value)] + } + + public async getInfo(hash: string): Promise { + return await this.getInfoFor(hash).then((r) => this.featureToImage(r)) } getRelevantUrls(tags: Record, prefixes: string[]): Store { diff --git a/src/UI/History/AggregateImages.svelte b/src/UI/History/AggregateImages.svelte new file mode 100644 index 0000000000..9955360eb3 --- /dev/null +++ b/src/UI/History/AggregateImages.svelte @@ -0,0 +1,43 @@ + +{#if $allDiffs === undefined} + +{:else if $addedImages.length === 0} + No images added by this contributor +{:else} + {#each $addedImages as imgDiff} + + {/each} +{/if} diff --git a/src/UI/History/AggregateView.svelte b/src/UI/History/AggregateView.svelte index ae1afcb249..79baeda81a 100644 --- a/src/UI/History/AggregateView.svelte +++ b/src/UI/History/AggregateView.svelte @@ -5,6 +5,11 @@ import { OsmObject } from "../../Logic/Osm/OsmObject" import Loading from "../Base/Loading.svelte" import { HistoryUtils } from "./HistoryUtils" + import * as shared_questions from "../../assets/generated/layers/questions.json" + import TagRenderingQuestion from "../Popup/TagRendering/TagRenderingQuestion.svelte" + import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" + import Tr from "../Base/Tr.svelte" + import AccordionSingle from "../Flowbite/AccordionSingle.svelte" export let onlyShowUsername: string export let features: Feature[] @@ -13,26 +18,28 @@ let allHistories: UIEventSource = UIEventSource.FromPromise( Promise.all(features.map(f => downloader.downloadHistory(f.properties.id))) ) - let allDiffs: Store<{ key: string; value?: string; oldValue?: string }[]> = allHistories.mapD(histories => { - const allDiffs = [].concat(...histories.map( - history => { - const filtered = history.filter(step => !onlyShowUsername || step.tags["_last_edit:contributor"] === onlyShowUsername) - const diffs: { - key: string; - value?: string; - oldValue?: string - }[][] = filtered.map(step => HistoryUtils.tagHistoryDiff(step, history)) - return [].concat(...diffs) - } - )) - return allDiffs - }) + let allDiffs: Store<{ + key: string; + value?: string; + oldValue?: string + }[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, onlyShowUsername)) - const mergedCount = allDiffs.mapD(allDiffs => { + const trs = shared_questions.tagRenderings.map(tr => new TagRenderingConfig(tr)) + + function detectQuestion(key: string): TagRenderingConfig { + return trs.find(tr => tr.freeform?.key === key) + } + + const mergedCount: Store<{ + key: string; + tr: TagRenderingConfig; + count: number; + values: { value: string; count: number }[] + }[]> = allDiffs.mapD(allDiffs => { const keyCounts = new Map>() for (const diff of allDiffs) { const k = diff.key - if(!keyCounts.has(k)){ + if (!keyCounts.has(k)) { keyCounts.set(k, new Map()) } const valueCounts = keyCounts.get(k) @@ -40,34 +47,57 @@ valueCounts.set(v, 1 + (valueCounts.get(v) ?? 0)) } - const perKey: {key: string, count: number, values: - {value: string, count: number}[] + const perKey: { + key: string, tr: TagRenderingConfig, count: number, values: + { value: string, count: number }[] }[] = [] keyCounts.forEach((values, key) => { - const keyTotal : {value: string, count: number}[] = [] + const keyTotal: { value: string, count: number }[] = [] values.forEach((count, value) => { - keyTotal.push({value, count}) + keyTotal.push({ value, count }) }) let countForKey = 0 - for (const {count} of keyTotal) { + for (const { count } of keyTotal) { countForKey += count } keyTotal.sort((a, b) => b.count - a.count) - perKey.push({count: countForKey, key, values: keyTotal}) + const tr = detectQuestion(key) + perKey.push({ count: countForKey, tr, key, values: keyTotal }) }) perKey.sort((a, b) => b.count - a.count) return perKey }) + {#if allHistories === undefined} {:else if $allDiffs !== undefined} {#each $mergedCount as diff} -
- {JSON.stringify(diff)} -
+

+ {#if diff.tr} + + {:else} + {diff.key} + {/if} +

+ + + + Answered {diff.count} times + +
    + {#each diff.values as value} +
  • + {value.value} + {#if value.count > 1} + - {value.count} + {/if} +
  • + {/each} +
+
{/each} {/if} diff --git a/src/UI/History/AttributedPanoramaxImage.svelte b/src/UI/History/AttributedPanoramaxImage.svelte new file mode 100644 index 0000000000..83463ada67 --- /dev/null +++ b/src/UI/History/AttributedPanoramaxImage.svelte @@ -0,0 +1,13 @@ + + +{#if $image !== undefined} + +{/if} diff --git a/src/UI/History/History.svelte b/src/UI/History/History.svelte index 16ee90ed33..e4354b0489 100644 --- a/src/UI/History/History.svelte +++ b/src/UI/History/History.svelte @@ -8,7 +8,6 @@ import Loading from "../Base/Loading.svelte" import { HistoryUtils } from "./HistoryUtils" import ToSvelte from "../Base/ToSvelte.svelte" - import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import Tr from "../Base/Tr.svelte" export let onlyShowChangesBy: string @@ -28,68 +27,76 @@ console.log("Comparing ", step.tags["_last_edit:contributor"], onlyShowChangesBy, step.tags["_last_edit:contributor"] === onlyShowChangesBy) return step.tags["_last_edit:contributor"] === onlyShowChangesBy + }).map(({ step, layer }) => { + const diff = HistoryUtils.tagHistoryDiff(step, fullHistory.data) + return { step, layer, diff } })) let lastStep = filteredHistory.mapD(history => history.at(-1)) -let l : LayerConfig - // l.title.GetRenderValue({}).Subs({}) + let allGeometry = filteredHistory.mapD(all => !all.some(x => x.diff.length > 0)) + /** + * These layers are only shown if there are tag changes as well + */ + const ignoreLayersIfNoChanges: ReadonlySet = new Set(["walls_and_buildings"]) -{#if $lastStep?.layer} - -

-
- -
- -

-
-{/if} +{#if !$allGeometry || !ignoreLayersIfNoChanges.has($lastStep?.layer?.id)} + {#if $lastStep?.layer} + +

+
+ +
+ +

+
+ {/if} -{#if !$filteredHistory} - Loading history... -{:else if $filteredHistory.length === 0} - Only geometry changes found -{:else} - - {#each $filteredHistory as { step, layer }} + {#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 step.version === 1} - - - {#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} +

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

+
+ {/if} + {#if HistoryUtils.tagHistoryDiff(step, $fullHistory).length === 0} + + + Only changes in geometry + + + {:else} + {#each HistoryUtils.tagHistoryDiff(step, $fullHistory) as diff} + + {step.version} + {layer?.id ?? "Unknown layer"} + {#if diff.oldValue === undefined} + {diff.key} + {diff.value} + {:else if diff.value === undefined } + {diff.key} + {diff.value} + {:else} + {diff.key} + {diff.oldValue} → {diff.value} + {/if} + + + + {/each} + {/if} + {/each} + + {/if} {/if} diff --git a/src/UI/History/HistoryUtils.ts b/src/UI/History/HistoryUtils.ts index 768d0754df..9ba4bf0fed 100644 --- a/src/UI/History/HistoryUtils.ts +++ b/src/UI/History/HistoryUtils.ts @@ -4,7 +4,7 @@ import { OsmObject } from "../../Logic/Osm/OsmObject" export class HistoryUtils { - private static personalTheme = new ThemeConfig( all_layers, true) + public static readonly personalTheme = new ThemeConfig( all_layers, true) private static ignoredLayers = new Set(["fixme"]) public static determineLayer(properties: Record){ return this.personalTheme.getMatchingLayer(properties, this.ignoredLayers) @@ -13,12 +13,13 @@ export class HistoryUtils { public static tagHistoryDiff(step: OsmObject, history: OsmObject[]): { key: string, value?: string, - oldValue?: string + oldValue?: string, + step: OsmObject }[] { 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] + key, value: step.tags[key], step })) } const previousTags = previous.tags @@ -27,9 +28,24 @@ export class HistoryUtils { const value = step.tags[key] const oldValue = previousTags[key] return { - key, value, oldValue + key, value, oldValue, step } }).filter(ch => ch.oldValue !== ch.value) } + public static fullHistoryDiff(histories: OsmObject[][], onlyShowUsername?: string){ + const allDiffs: {key: string, oldValue?: string, value?: string}[] = [].concat(...histories.map( + history => { + const filtered = history.filter(step => !onlyShowUsername || step.tags["_last_edit:contributor"] === onlyShowUsername) + const diffs: { + key: string; + value?: string; + oldValue?: string + }[][] = filtered.map(step => HistoryUtils.tagHistoryDiff(step, history)) + return [].concat(...diffs) + } + )) + return allDiffs + } + } diff --git a/src/UI/Image/AttributedImage.svelte b/src/UI/Image/AttributedImage.svelte index 02a871249f..546de016d4 100644 --- a/src/UI/Image/AttributedImage.svelte +++ b/src/UI/Image/AttributedImage.svelte @@ -28,22 +28,24 @@ export let imgClass: string = undefined export let state: SpecialVisualizationState = undefined export let attributionFormat: "minimal" | "medium" | "large" = "medium" - export let previewedImage: UIEventSource + export let previewedImage: UIEventSource = undefined export let canZoom = previewedImage !== undefined let loaded = false let showBigPreview = new UIEventSource(false) onDestroy( showBigPreview.addCallbackAndRun((shown) => { if (!shown) { - previewedImage.set(undefined) + previewedImage?.set(undefined) } }) ) + if(previewedImage){ onDestroy( previewedImage.addCallbackAndRun((previewedImage) => { showBigPreview.set(previewedImage?.id === image.id) }) ) + } function highlight(entered: boolean = true) { if (!entered) { @@ -82,7 +84,7 @@ class="normal-background" on:click={() => { console.log("Closing") - previewedImage.set(undefined) + previewedImage?.set(undefined) }} /> @@ -124,7 +126,7 @@ {#if canZoom && loaded}
previewedImage.set(image)} + on:click={() => previewedImage?.set(image)} >
diff --git a/src/UI/InspectorGUI.svelte b/src/UI/InspectorGUI.svelte index d02f513a33..c7d1870137 100644 --- a/src/UI/InspectorGUI.svelte +++ b/src/UI/InspectorGUI.svelte @@ -21,6 +21,8 @@ import { XCircleIcon } from "@babeard/svelte-heroicons/solid" import { Utils } from "../Utils" import AggregateView from "./History/AggregateView.svelte" + import { HistoryUtils } from "./History/HistoryUtils" + import AggregateImages from "./History/AggregateImages.svelte" let username = QueryParameters.GetQueryParameter("user", undefined, "Inspect this user") let step = new UIEventSource<"waiting" | "loading" | "done">("waiting") @@ -49,18 +51,28 @@ let featuresStore = new UIEventSource([]) let features = new StaticFeatureSource(featuresStore) - new ShowDataLayer(map, - { - layer, - zoomToFeatures: true, - features, - onClick: (f: Feature) => { - selectedElement.set(undefined) - Utils.waitFor(200).then(() => { - selectedElement.set(f) - }) - } - }) + ShowDataLayer.showMultipleLayers(map, features, HistoryUtils.personalTheme.layers, { + zoomToFeatures: true, + onClick: (f: Feature) => { + selectedElement.set(undefined) + Utils.waitFor(200).then(() => { + selectedElement.set(f) + }) + } + }) + + /* new ShowDataLayer(map, + { + layer, + zoomToFeatures: true, + features, + onClick: (f: Feature) => { + selectedElement.set(undefined) + Utils.waitFor(200).then(() => { + selectedElement.set(f) + }) + } + })*/ async function load() { @@ -87,7 +99,7 @@ return true }) - let mode: "map" | "table" | "aggregate" = "map" + let mode: "map" | "table" | "aggregate" | "images" = "map"
@@ -113,6 +125,9 @@ +
{#if mode === "map"} @@ -155,11 +170,18 @@ {:else if mode === "table"} +
{#each $featuresStore as f} -

{f.properties.id}

{/each} - {:else} - +
+ {:else if mode === "aggregate"} +
+ +
+ {:else if mode === "images"} +
+ +
{/if} From e7a0e5765b2fedea299bf6d711956f25ea90a27f Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 3 Dec 2024 16:12:57 +0100 Subject: [PATCH 3/8] Actions: try to build on forgejo --- .github/workflows/deploy_hosted.yml | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/deploy_hosted.yml diff --git a/.github/workflows/deploy_hosted.yml b/.github/workflows/deploy_hosted.yml new file mode 100644 index 0000000000..ea20b10948 --- /dev/null +++ b/.github/workflows/deploy_hosted.yml @@ -0,0 +1,50 @@ +name: Deploy develop on dev.mapcomplete.org +on: + push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: package-lock.json + + - name: install deps + run: npm ci + shell: bash + + - name: create generated dir + run: mkdir ./assets/generated + shell: bash + + - name: create dependencies + run: npm run generate:licenses; npm run generate:images; npm run generate:charging-stations; npm run generate:service-worker; npm run download:editor-layer-index + shell: bash + + - name: sync translations + run: npm run generate:translations + shell: bash + + - name: Prepare deploy + run: npm run prepare-deploy + shell: bash + + - name: run tests + run: npm run test + shell: bash + + - name: Upload artefact + env: + SSH_KEY: ${{ secrets.HETZNER_KEY }} + run: | + mkdir .ssh + echo $SSH_KEY > .ssh/id_ed25519 + scp dist/* pietervdvn@hosted.mapcomplete.org:/root/public/${{ github.ref_name }} + + From f4160ebcf0a139270ab97913c7150df3b87eefa4 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 3 Dec 2024 16:50:10 +0100 Subject: [PATCH 4/8] Actions: try to get tests running --- .github/workflows/deploy_hosted.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy_hosted.yml b/.github/workflows/deploy_hosted.yml index ea20b10948..0c88d12ee8 100644 --- a/.github/workflows/deploy_hosted.yml +++ b/.github/workflows/deploy_hosted.yml @@ -36,7 +36,10 @@ jobs: shell: bash - name: run tests - run: npm run test + run: | + pwd + ls + npm run test shell: bash - name: Upload artefact From b86b8dd1ee6a7630dba3afd4d0b56aaf35d2097b Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 3 Dec 2024 17:04:37 +0100 Subject: [PATCH 5/8] Actions: run tests before the heavy deploy --- .github/workflows/deploy_hosted.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy_hosted.yml b/.github/workflows/deploy_hosted.yml index 0c88d12ee8..ca25e6aa80 100644 --- a/.github/workflows/deploy_hosted.yml +++ b/.github/workflows/deploy_hosted.yml @@ -31,10 +31,6 @@ jobs: run: npm run generate:translations shell: bash - - name: Prepare deploy - run: npm run prepare-deploy - shell: bash - - name: run tests run: | pwd @@ -42,6 +38,12 @@ jobs: npm run test shell: bash + - name: Prepare deploy + run: npm run prepare-deploy + shell: bash + + + - name: Upload artefact env: SSH_KEY: ${{ secrets.HETZNER_KEY }} From 4d0d92d250aaae890de91c94bf531089e96d39b4 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 3 Dec 2024 19:12:42 +0100 Subject: [PATCH 6/8] Feature(inspector): keep track of previously visited contributors --- src/UI/History/PreviouslySpiedUsers.svelte | 102 +++++++++++++++++++++ src/UI/InspectorGUI.svelte | 64 +++++++++---- 2 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 src/UI/History/PreviouslySpiedUsers.svelte diff --git a/src/UI/History/PreviouslySpiedUsers.svelte b/src/UI/History/PreviouslySpiedUsers.svelte new file mode 100644 index 0000000000..f8c45347ee --- /dev/null +++ b/src/UI/History/PreviouslySpiedUsers.svelte @@ -0,0 +1,102 @@ + + + + + + + + + + + {#each $inspectedContributors as c} + + + + + + + {/each} +
+ + + + + + + Remove
+ + + {c.visitedTime} + + + + remove(c.name)} /> +
+ + + +
Labels
+ {#if $labels.length === 0} + No labels + {:else} + {#each $labels as label} +
{label}
+ {/each} + {/if} +
+
Create a new label
+ + +
+
+
diff --git a/src/UI/InspectorGUI.svelte b/src/UI/InspectorGUI.svelte index c7d1870137..1afa7ba5bb 100644 --- a/src/UI/InspectorGUI.svelte +++ b/src/UI/InspectorGUI.svelte @@ -23,6 +23,9 @@ import AggregateView from "./History/AggregateView.svelte" import { HistoryUtils } from "./History/HistoryUtils" import AggregateImages from "./History/AggregateImages.svelte" + import Page from "./Base/Page.svelte" + import PreviouslySpiedUsers from "./History/PreviouslySpiedUsers.svelte" + import { OsmConnection } from "../Logic/Osm/OsmConnection" let username = QueryParameters.GetQueryParameter("user", undefined, "Inspect this user") let step = new UIEventSource<"waiting" | "loading" | "done">("waiting") @@ -32,8 +35,8 @@ let lon = UIEventSource.asFloat(QueryParameters.GetQueryParameter("lon", "0")) let theme = new ThemeConfig(inspector_theme, true) let layer = theme.layers.find(l => l.id === "usertouched") + // Is this a dirty hack? Yes it is! theme.getMatchingLayer = () => { - // Is this a dirty hack? Yes it is! return layer } let loadingData = false @@ -61,23 +64,35 @@ } }) - /* new ShowDataLayer(map, - { - layer, - zoomToFeatures: true, - features, - onClick: (f: Feature) => { - selectedElement.set(undefined) - Utils.waitFor(200).then(() => { - selectedElement.set(f) - }) - } - })*/ + let osmConnection = new OsmConnection() + let inspectedContributors: UIEventSource<{ + name: string, + visitedTime: string, + label: string + }[]> = UIEventSource.asObject( + osmConnection.getPreference("spied-upon-users"), []) async function load() { + const user = username.data + { + + const inspectedData = inspectedContributors.data + const previousEntry = inspectedData.find(e => e.name === user) + if (previousEntry) { + previousEntry.visitedTime = new Date().toISOString() + } else { + inspectedData.push({ + label: undefined, + visitedTime: new Date().toISOString(), + name: user + }) + } + inspectedContributors.ping() + } step.setData("loading") - const overpass = new Overpass(undefined, ["nw(user_touched:\"" + username.data + "\");"], Constants.defaultOverpassUrls[0]) + featuresStore.set([]) + const overpass = new Overpass(undefined, ["nw(user_touched:\"" + user + "\");"], Constants.defaultOverpassUrls[0]) if (!maplibremap.bounds.data) { return } @@ -100,6 +115,9 @@ }) let mode: "map" | "table" | "aggregate" | "images" = "map" + + let showPreviouslyVisited = new UIEventSource(true) +
@@ -112,6 +130,9 @@ {:else} {/if} + Back to index
@@ -171,13 +192,13 @@ {:else if mode === "table"}
- {#each $featuresStore as f} - - {/each} + {#each $featuresStore as f} + + {/each}
{:else if mode === "aggregate"}
- +
{:else if mode === "images"}
@@ -185,3 +206,10 @@
{/if} + + +
Earlier inspected constributors
+ { + username.set(e.detail); load();showPreviouslyVisited.set(false) + }} /> +
From 7c6224fd3edf4cd833b62ac7ee70b5477ac456e7 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 3 Dec 2024 19:26:29 +0100 Subject: [PATCH 7/8] Feature(inspector): add link in menu, add icon --- langs/en.json | 3 +++ src/UI/BigComponents/MenuDrawer.svelte | 6 ++++++ src/UI/InspectorGUI.svelte | 4 +++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/langs/en.json b/langs/en.json index bad4e5e3d0..256c49b5a9 100644 --- a/langs/en.json +++ b/langs/en.json @@ -669,6 +669,9 @@ "pickTheme": "Pick a theme below to get started.", "title": "MapComplete" }, + "inspector": { + "menu": "Inspect a contributor" + }, "move": { "cancel": "Select a different reason", "cannotBeMoved": "This feature cannot be moved.", diff --git a/src/UI/BigComponents/MenuDrawer.svelte b/src/UI/BigComponents/MenuDrawer.svelte index b11cd12895..cb2031d231 100644 --- a/src/UI/BigComponents/MenuDrawer.svelte +++ b/src/UI/BigComponents/MenuDrawer.svelte @@ -50,6 +50,7 @@ import Squares2x2 from "@babeard/svelte-heroicons/mini/Squares2x2" import EnvelopeOpen from "@babeard/svelte-heroicons/mini/EnvelopeOpen" import PanoramaxLink from "./PanoramaxLink.svelte" + import MagnifyingGlassCircle from "@babeard/svelte-heroicons/outline/MagnifyingGlassCircle" export let state: ThemeViewState let userdetails = state.osmConnection.userDetails @@ -262,6 +263,11 @@ + + + + + diff --git a/src/UI/InspectorGUI.svelte b/src/UI/InspectorGUI.svelte index 1afa7ba5bb..1a27816657 100644 --- a/src/UI/InspectorGUI.svelte +++ b/src/UI/InspectorGUI.svelte @@ -26,6 +26,7 @@ import Page from "./Base/Page.svelte" import PreviouslySpiedUsers from "./History/PreviouslySpiedUsers.svelte" import { OsmConnection } from "../Logic/Osm/OsmConnection" + import MagnifyingGlassCircle from "@babeard/svelte-heroicons/outline/MagnifyingGlassCircle" let username = QueryParameters.GetQueryParameter("user", undefined, "Inspect this user") let step = new UIEventSource<"waiting" | "loading" | "done">("waiting") @@ -123,7 +124,8 @@
-

Inspect contributor

+ +

Inspect contributor

load()} /> {#if loadingData} From 063a912c825906f66eb83d9f52222845b6e3b218 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 4 Dec 2024 18:48:05 +0100 Subject: [PATCH 8/8] Feature(inspector): allow to load multiple contributors at once --- assets/layers/usertouched/usertouched.json | 7 ++-- langs/el.json | 3 +- src/UI/History/AggregateImages.svelte | 17 +++++---- src/UI/History/AggregateView.svelte | 12 ++++--- src/UI/History/History.svelte | 18 ++++++---- src/UI/History/HistoryUtils.ts | 4 +-- src/UI/History/PreviouslySpiedUsers.svelte | 7 +++- src/UI/InspectorGUI.svelte | 42 +++++++++++++--------- 8 files changed, 69 insertions(+), 41 deletions(-) diff --git a/assets/layers/usertouched/usertouched.json b/assets/layers/usertouched/usertouched.json index 6e75ebada4..f22e5e104c 100644 --- a/assets/layers/usertouched/usertouched.json +++ b/assets/layers/usertouched/usertouched.json @@ -52,8 +52,11 @@ "render": 0, "mappings": [ { - "if": {"or": - ["_geometry:type=Polygon","_geometry:type=MultiPolygon"] + "if": { + "or": [ + "_geometry:type=Polygon", + "_geometry:type=MultiPolygon" + ] }, "then": 20 } diff --git a/langs/el.json b/langs/el.json index 0967ef424b..7a73a41bfd 100644 --- a/langs/el.json +++ b/langs/el.json @@ -1 +1,2 @@ -{} +{ +} \ No newline at end of file diff --git a/src/UI/History/AggregateImages.svelte b/src/UI/History/AggregateImages.svelte index 9955360eb3..78dda93858 100644 --- a/src/UI/History/AggregateImages.svelte +++ b/src/UI/History/AggregateImages.svelte @@ -5,11 +5,9 @@ import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader" import { OsmObject } from "../../Logic/Osm/OsmObject" import Loading from "../Base/Loading.svelte" - import AttributedImage from "../Image/AttributedImage.svelte" import AttributedPanoramaxImage from "./AttributedPanoramaxImage.svelte" - import History from "./History.svelte" - export let onlyShowUsername: string + export let onlyShowUsername: string[] export let features: Feature[] const downloader = new OsmObjectDownloader() @@ -23,11 +21,12 @@ } return result })) + let usernamesSet = new Set(onlyShowUsername) let allDiffs: Store<{ key: string; value?: string; oldValue?: string - }[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, onlyShowUsername)) + }[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, usernamesSet)) let addedImages = allDiffs.mapD(diffs => [].concat(...diffs.filter(({ key }) => imageKeys.has(key)))) @@ -37,7 +36,11 @@ {:else if $addedImages.length === 0} No images added by this contributor {:else} - {#each $addedImages as imgDiff} - - {/each} +
+ {#each $addedImages as imgDiff} +
+ +
+ {/each} +
{/if} diff --git a/src/UI/History/AggregateView.svelte b/src/UI/History/AggregateView.svelte index 79baeda81a..22bd1d8227 100644 --- a/src/UI/History/AggregateView.svelte +++ b/src/UI/History/AggregateView.svelte @@ -6,14 +6,16 @@ import Loading from "../Base/Loading.svelte" import { HistoryUtils } from "./HistoryUtils" import * as shared_questions from "../../assets/generated/layers/questions.json" - import TagRenderingQuestion from "../Popup/TagRendering/TagRenderingQuestion.svelte" import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" import Tr from "../Base/Tr.svelte" import AccordionSingle from "../Flowbite/AccordionSingle.svelte" + import Translations from "../i18n/Translations" - export let onlyShowUsername: string + export let onlyShowUsername: string[] export let features: Feature[] + let usernames = new Set(onlyShowUsername) + const downloader = new OsmObjectDownloader() let allHistories: UIEventSource = UIEventSource.FromPromise( Promise.all(features.map(f => downloader.downloadHistory(f.properties.id))) @@ -22,7 +24,7 @@ key: string; value?: string; oldValue?: string - }[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, onlyShowUsername)) + }[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, usernames)) const trs = shared_questions.tagRenderings.map(tr => new TagRenderingConfig(tr)) @@ -69,6 +71,7 @@ return perKey }) + const t = Translations.t.inspector @@ -85,8 +88,7 @@ - - Answered {diff.count} times +
    {#each diff.values as value} diff --git a/src/UI/History/History.svelte b/src/UI/History/History.svelte index e4354b0489..5cfa6b908f 100644 --- a/src/UI/History/History.svelte +++ b/src/UI/History/History.svelte @@ -9,10 +9,12 @@ import { HistoryUtils } from "./HistoryUtils" import ToSvelte from "../Base/ToSvelte.svelte" import Tr from "../Base/Tr.svelte" + import Translations from "../i18n/Translations" - export let onlyShowChangesBy: string + export let onlyShowChangesBy: string[] export let id: OsmId + let usernames = new Set(onlyShowChangesBy) let fullHistory = UIEventSource.FromPromise(new OsmObjectDownloader().downloadHistory(id)) let partOfLayer = fullHistory.mapD(history => history.map(step => ({ @@ -21,11 +23,11 @@ }))) let filteredHistory = partOfLayer.mapD(history => history.filter(({ step }) => { - if (!onlyShowChangesBy) { + if (usernames.size == 0) { return true } - console.log("Comparing ", step.tags["_last_edit:contributor"], onlyShowChangesBy, step.tags["_last_edit:contributor"] === onlyShowChangesBy) - return step.tags["_last_edit:contributor"] === onlyShowChangesBy + console.log("Checking if ", step.tags["_last_edit:contributor"],"is contained in", onlyShowChangesBy) + return usernames.has(step.tags["_last_edit:contributor"]) }).map(({ step, layer }) => { const diff = HistoryUtils.tagHistoryDiff(step, fullHistory.data) @@ -38,6 +40,8 @@ * These layers are only shown if there are tag changes as well */ const ignoreLayersIfNoChanges: ReadonlySet = new Set(["walls_and_buildings"]) + const t = Translations.t.inspector.previousContributors + {#if !$allGeometry || !ignoreLayersIfNoChanges.has($lastStep?.layer?.id)} @@ -55,7 +59,7 @@ {#if !$filteredHistory} Loading history... {:else if $filteredHistory.length === 0} - Only geometry changes found + {:else} {#each $filteredHistory as { step, layer }} @@ -64,7 +68,7 @@ @@ -72,7 +76,7 @@ {#if HistoryUtils.tagHistoryDiff(step, $fullHistory).length === 0} {:else} diff --git a/src/UI/History/HistoryUtils.ts b/src/UI/History/HistoryUtils.ts index 9ba4bf0fed..c85aaeff22 100644 --- a/src/UI/History/HistoryUtils.ts +++ b/src/UI/History/HistoryUtils.ts @@ -33,10 +33,10 @@ export class HistoryUtils { }).filter(ch => ch.oldValue !== ch.value) } - public static fullHistoryDiff(histories: OsmObject[][], onlyShowUsername?: string){ + public static fullHistoryDiff(histories: OsmObject[][], onlyShowUsername?: Set){ const allDiffs: {key: string, oldValue?: string, value?: string}[] = [].concat(...histories.map( history => { - const filtered = history.filter(step => !onlyShowUsername || step.tags["_last_edit:contributor"] === onlyShowUsername) + const filtered = history.filter(step => !onlyShowUsername || onlyShowUsername?.has(step.tags["_last_edit:contributor"] )) const diffs: { key: string; value?: string; diff --git a/src/UI/History/PreviouslySpiedUsers.svelte b/src/UI/History/PreviouslySpiedUsers.svelte index f8c45347ee..4bb757b436 100644 --- a/src/UI/History/PreviouslySpiedUsers.svelte +++ b/src/UI/History/PreviouslySpiedUsers.svelte @@ -88,7 +88,12 @@ No labels {:else} {#each $labels as label} -
    {label}
    +
    {label} + +
    {/each} {/if}
    diff --git a/src/UI/InspectorGUI.svelte b/src/UI/InspectorGUI.svelte index 1a27816657..ea44576888 100644 --- a/src/UI/InspectorGUI.svelte +++ b/src/UI/InspectorGUI.svelte @@ -27,6 +27,8 @@ import PreviouslySpiedUsers from "./History/PreviouslySpiedUsers.svelte" import { OsmConnection } from "../Logic/Osm/OsmConnection" import MagnifyingGlassCircle from "@babeard/svelte-heroicons/outline/MagnifyingGlassCircle" + import Translations from "./i18n/Translations" + import Tr from "./Base/Tr.svelte" let username = QueryParameters.GetQueryParameter("user", undefined, "Inspect this user") let step = new UIEventSource<"waiting" | "loading" | "done">("waiting") @@ -52,10 +54,12 @@ lon.set(l.lon) }) - + let allLayers = HistoryUtils.personalTheme.layers + let layersNoFixme = allLayers.filter(l => l.id !== "fixme") +let fixme = allLayers.find(l => l.id === "fixme") let featuresStore = new UIEventSource([]) let features = new StaticFeatureSource(featuresStore) - ShowDataLayer.showMultipleLayers(map, features, HistoryUtils.personalTheme.layers, { + ShowDataLayer.showMultipleLayers(map, features, [...layersNoFixme, fixme] , { zoomToFeatures: true, onClick: (f: Feature) => { selectedElement.set(undefined) @@ -75,7 +79,7 @@ async function load() { const user = username.data - { + if(user.indexOf(";")<0){ const inspectedData = inspectedContributors.data const previousEntry = inspectedData.find(e => e.name === user) @@ -93,7 +97,7 @@ step.setData("loading") featuresStore.set([]) - const overpass = new Overpass(undefined, ["nw(user_touched:\"" + user + "\");"], Constants.defaultOverpassUrls[0]) + const overpass = new Overpass(undefined, user.split(";").map(user => "nw(user_touched:\"" + user + "\");"), Constants.defaultOverpassUrls[0]) if (!maplibremap.bounds.data) { return } @@ -118,38 +122,44 @@ let mode: "map" | "table" | "aggregate" | "images" = "map" let showPreviouslyVisited = new UIEventSource(true) - +const t = Translations.t.inspector
    -

    Inspect contributor

    +

    +

    + load()} /> {#if loadingData} {:else} - + + {/if} - Back to index + + +
    @@ -195,16 +205,16 @@ {:else if mode === "table"}
    {#each $featuresStore as f} - + {/each}
    {:else if mode === "aggregate"}
    - +
    {:else if mode === "images"}
    - +
    {/if}

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

    - Only changes in geometry +