Feature(inspector): allow to load multiple contributors at once

This commit is contained in:
Pieter Vander Vennet 2024-12-04 18:48:05 +01:00
parent 7c6224fd3e
commit 063a912c82
8 changed files with 69 additions and 41 deletions

View file

@ -52,8 +52,11 @@
"render": 0, "render": 0,
"mappings": [ "mappings": [
{ {
"if": {"or": "if": {
["_geometry:type=Polygon","_geometry:type=MultiPolygon"] "or": [
"_geometry:type=Polygon",
"_geometry:type=MultiPolygon"
]
}, },
"then": 20 "then": 20
} }

View file

@ -1 +1,2 @@
{} {
}

View file

@ -5,11 +5,9 @@
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader" import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
import { OsmObject } from "../../Logic/Osm/OsmObject" import { OsmObject } from "../../Logic/Osm/OsmObject"
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import AttributedImage from "../Image/AttributedImage.svelte"
import AttributedPanoramaxImage from "./AttributedPanoramaxImage.svelte" import AttributedPanoramaxImage from "./AttributedPanoramaxImage.svelte"
import History from "./History.svelte"
export let onlyShowUsername: string export let onlyShowUsername: string[]
export let features: Feature[] export let features: Feature[]
const downloader = new OsmObjectDownloader() const downloader = new OsmObjectDownloader()
@ -23,11 +21,12 @@
} }
return result return result
})) }))
let usernamesSet = new Set(onlyShowUsername)
let allDiffs: Store<{ let allDiffs: Store<{
key: string; key: string;
value?: string; value?: string;
oldValue?: 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)))) let addedImages = allDiffs.mapD(diffs => [].concat(...diffs.filter(({ key }) => imageKeys.has(key))))
@ -37,7 +36,11 @@
{:else if $addedImages.length === 0} {:else if $addedImages.length === 0}
No images added by this contributor No images added by this contributor
{:else} {:else}
{#each $addedImages as imgDiff} <div class="flex">
<AttributedPanoramaxImage hash={imgDiff.value} /> {#each $addedImages as imgDiff}
{/each} <div class="w-48 h-48">
<AttributedPanoramaxImage hash={imgDiff.value} />
</div>
{/each}
</div>
{/if} {/if}

View file

@ -6,14 +6,16 @@
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import { HistoryUtils } from "./HistoryUtils" import { HistoryUtils } from "./HistoryUtils"
import * as shared_questions from "../../assets/generated/layers/questions.json" import * as shared_questions from "../../assets/generated/layers/questions.json"
import TagRenderingQuestion from "../Popup/TagRendering/TagRenderingQuestion.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte" import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import Translations from "../i18n/Translations"
export let onlyShowUsername: string export let onlyShowUsername: string[]
export let features: Feature[] export let features: Feature[]
let usernames = new Set(onlyShowUsername)
const downloader = new OsmObjectDownloader() const downloader = new OsmObjectDownloader()
let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.FromPromise( let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.FromPromise(
Promise.all(features.map(f => downloader.downloadHistory(f.properties.id))) Promise.all(features.map(f => downloader.downloadHistory(f.properties.id)))
@ -22,7 +24,7 @@
key: string; key: string;
value?: string; value?: string;
oldValue?: 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)) const trs = shared_questions.tagRenderings.map(tr => new TagRenderingConfig(tr))
@ -69,6 +71,7 @@
return perKey return perKey
}) })
const t = Translations.t.inspector
</script> </script>
@ -85,8 +88,7 @@
</h3> </h3>
<AccordionSingle> <AccordionSingle>
<span slot="header"> <span slot="header">
<Tr t={t.answeredCountTimes.Subs(diff)} />
Answered {diff.count} times
</span> </span>
<ul> <ul>
{#each diff.values as value} {#each diff.values as value}

View file

@ -9,10 +9,12 @@
import { HistoryUtils } from "./HistoryUtils" import { HistoryUtils } from "./HistoryUtils"
import ToSvelte from "../Base/ToSvelte.svelte" import ToSvelte from "../Base/ToSvelte.svelte"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
export let onlyShowChangesBy: string export let onlyShowChangesBy: string[]
export let id: OsmId export let id: OsmId
let usernames = new Set(onlyShowChangesBy)
let fullHistory = UIEventSource.FromPromise(new OsmObjectDownloader().downloadHistory(id)) let fullHistory = UIEventSource.FromPromise(new OsmObjectDownloader().downloadHistory(id))
let partOfLayer = fullHistory.mapD(history => history.map(step => ({ let partOfLayer = fullHistory.mapD(history => history.map(step => ({
@ -21,11 +23,11 @@
}))) })))
let filteredHistory = partOfLayer.mapD(history => let filteredHistory = partOfLayer.mapD(history =>
history.filter(({ step }) => { history.filter(({ step }) => {
if (!onlyShowChangesBy) { if (usernames.size == 0) {
return true return true
} }
console.log("Comparing ", step.tags["_last_edit:contributor"], onlyShowChangesBy, step.tags["_last_edit:contributor"] === onlyShowChangesBy) console.log("Checking if ", step.tags["_last_edit:contributor"],"is contained in", onlyShowChangesBy)
return step.tags["_last_edit:contributor"] === onlyShowChangesBy return usernames.has(step.tags["_last_edit:contributor"])
}).map(({ step, layer }) => { }).map(({ step, layer }) => {
const diff = HistoryUtils.tagHistoryDiff(step, fullHistory.data) const diff = HistoryUtils.tagHistoryDiff(step, fullHistory.data)
@ -38,6 +40,8 @@
* These layers are only shown if there are tag changes as well * These layers are only shown if there are tag changes as well
*/ */
const ignoreLayersIfNoChanges: ReadonlySet<string> = new Set(["walls_and_buildings"]) const ignoreLayersIfNoChanges: ReadonlySet<string> = new Set(["walls_and_buildings"])
const t = Translations.t.inspector.previousContributors
</script> </script>
{#if !$allGeometry || !ignoreLayersIfNoChanges.has($lastStep?.layer?.id)} {#if !$allGeometry || !ignoreLayersIfNoChanges.has($lastStep?.layer?.id)}
@ -55,7 +59,7 @@
{#if !$filteredHistory} {#if !$filteredHistory}
<Loading>Loading history...</Loading> <Loading>Loading history...</Loading>
{:else if $filteredHistory.length === 0} {:else if $filteredHistory.length === 0}
Only geometry changes found <Tr t={t.onlyGeometry} />
{:else} {:else}
<table class="w-full m-1"> <table class="w-full m-1">
{#each $filteredHistory as { step, layer }} {#each $filteredHistory as { step, layer }}
@ -64,7 +68,7 @@
<tr> <tr>
<td colspan="3"> <td colspan="3">
<h3> <h3>
Created by {step.tags["_last_edit:contributor"]} <Tr t={t.createdBy.Subs({contributor: step.tags["_last_edit:contributor"]})} />
</h3> </h3>
</td> </td>
</tr> </tr>
@ -72,7 +76,7 @@
{#if HistoryUtils.tagHistoryDiff(step, $fullHistory).length === 0} {#if HistoryUtils.tagHistoryDiff(step, $fullHistory).length === 0}
<tr> <tr>
<td class="font-bold justify-center flex w-full" colspan="3"> <td class="font-bold justify-center flex w-full" colspan="3">
Only changes in geometry <Tr t={t.onlyGeometry} />
</td> </td>
</tr> </tr>
{:else} {:else}

View file

@ -33,10 +33,10 @@ export class HistoryUtils {
}).filter(ch => ch.oldValue !== ch.value) }).filter(ch => ch.oldValue !== ch.value)
} }
public static fullHistoryDiff(histories: OsmObject[][], onlyShowUsername?: string){ public static fullHistoryDiff(histories: OsmObject[][], onlyShowUsername?: Set<string>){
const allDiffs: {key: string, oldValue?: string, value?: string}[] = [].concat(...histories.map( const allDiffs: {key: string, oldValue?: string, value?: string}[] = [].concat(...histories.map(
history => { 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: { const diffs: {
key: string; key: string;
value?: string; value?: string;

View file

@ -88,7 +88,12 @@
No labels No labels
{:else} {:else}
{#each $labels as label} {#each $labels as label}
<div class="mx-2">{label} </div> <div class="mx-2">{label}
<button class:disabled={!$inspectedContributors.some(c => c.label === label)} on:click={() => {dispatch("selectUser",
inspectedContributors.data.filter(c =>c.label === label).map(c => c .name).join(";")
)}}>See all changes for these users
</button>
</div>
{/each} {/each}
{/if} {/if}
<div class="interactive flex m-2 items-center gap-x-2 rounded-lg p-2"> <div class="interactive flex m-2 items-center gap-x-2 rounded-lg p-2">

View file

@ -27,6 +27,8 @@
import PreviouslySpiedUsers from "./History/PreviouslySpiedUsers.svelte" import PreviouslySpiedUsers from "./History/PreviouslySpiedUsers.svelte"
import { OsmConnection } from "../Logic/Osm/OsmConnection" import { OsmConnection } from "../Logic/Osm/OsmConnection"
import MagnifyingGlassCircle from "@babeard/svelte-heroicons/outline/MagnifyingGlassCircle" 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 username = QueryParameters.GetQueryParameter("user", undefined, "Inspect this user")
let step = new UIEventSource<"waiting" | "loading" | "done">("waiting") let step = new UIEventSource<"waiting" | "loading" | "done">("waiting")
@ -52,10 +54,12 @@
lon.set(l.lon) 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<Feature[]>([]) let featuresStore = new UIEventSource<Feature[]>([])
let features = new StaticFeatureSource(featuresStore) let features = new StaticFeatureSource(featuresStore)
ShowDataLayer.showMultipleLayers(map, features, HistoryUtils.personalTheme.layers, { ShowDataLayer.showMultipleLayers(map, features, [...layersNoFixme, fixme] , {
zoomToFeatures: true, zoomToFeatures: true,
onClick: (f: Feature) => { onClick: (f: Feature) => {
selectedElement.set(undefined) selectedElement.set(undefined)
@ -75,7 +79,7 @@
async function load() { async function load() {
const user = username.data const user = username.data
{ if(user.indexOf(";")<0){
const inspectedData = inspectedContributors.data const inspectedData = inspectedContributors.data
const previousEntry = inspectedData.find(e => e.name === user) const previousEntry = inspectedData.find(e => e.name === user)
@ -93,7 +97,7 @@
step.setData("loading") step.setData("loading")
featuresStore.set([]) 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) { if (!maplibremap.bounds.data) {
return return
} }
@ -118,38 +122,44 @@
let mode: "map" | "table" | "aggregate" | "images" = "map" let mode: "map" | "table" | "aggregate" | "images" = "map"
let showPreviouslyVisited = new UIEventSource(true) let showPreviouslyVisited = new UIEventSource(true)
const t = Translations.t.inspector
</script> </script>
<div class="flex flex-col w-full h-full"> <div class="flex flex-col w-full h-full">
<div class="flex gap-x-2 items-center low-interaction p-2"> <div class="flex gap-x-2 items-center low-interaction p-2">
<MagnifyingGlassCircle class="w-12 h-12"/> <MagnifyingGlassCircle class="w-12 h-12"/>
<h1 class="flex-shrink-0 m-0 mx-2">Inspect contributor</h1> <h1 class="flex-shrink-0 m-0 mx-2">
<Tr t={t.title}/>
</h1>
<ValidatedInput type="string" value={username} on:submit={() => load()} /> <ValidatedInput type="string" value={username} on:submit={() => load()} />
{#if loadingData} {#if loadingData}
<Loading /> <Loading />
{:else} {:else}
<button class="primary" on:click={() => load()}>Load</button> <button class="primary" on:click={() => load()}>
<Tr t={t.load}/>
</button>
{/if} {/if}
<button on:click={() => showPreviouslyVisited.setData(true)}> <button on:click={() => showPreviouslyVisited.setData(true)}>
Show earlier inspected contributors <Tr t={t.earlierInspected}/>
</button> </button>
<a href="./index.html" class="button">Back to index</a> <a href="./index.html" class="button">
<Tr t={t.backToIndex}/>
</a>
</div> </div>
<div class="flex"> <div class="flex">
<button class:primary={mode === "map"} on:click={() => mode = "map"}> <button class:primary={mode === "map"} on:click={() => mode = "map"}>
Map view <Tr t={t.mapView}/>
</button> </button>
<button class:primary={mode === "table"} on:click={() => mode = "table"}> <button class:primary={mode === "table"} on:click={() => mode = "table"}>
Table view <Tr t={t.tableView}/>
</button> </button>
<button class:primary={mode === "aggregate"} on:click={() => mode = "aggregate"}> <button class:primary={mode === "aggregate"} on:click={() => mode = "aggregate"}>
Aggregate <Tr t={t.aggregateView}/>
</button> </button>
<button class:primary={mode === "images"} on:click={() => mode = "images"}> <button class:primary={mode === "images"} on:click={() => mode = "images"}>
Images <Tr t={t.images}/>
</button> </button>
</div> </div>
@ -195,16 +205,16 @@
{:else if mode === "table"} {:else if mode === "table"}
<div class="m-2 h-full overflow-y-auto"> <div class="m-2 h-full overflow-y-auto">
{#each $featuresStore as f} {#each $featuresStore as f}
<History onlyShowChangesBy={$username} id={f.properties.id} /> <History onlyShowChangesBy={$username?.split(";")} id={f.properties.id} />
{/each} {/each}
</div> </div>
{:else if mode === "aggregate"} {:else if mode === "aggregate"}
<div class="m-2 h-full overflow-y-auto"> <div class="m-2 h-full overflow-y-auto">
<AggregateView features={$featuresStore} onlyShowUsername={$username} /> <AggregateView features={$featuresStore} onlyShowUsername={$username?.split(";")} />
</div> </div>
{:else if mode === "images"} {:else if mode === "images"}
<div class="m-2 h-full overflow-y-auto"> <div class="m-2 h-full overflow-y-auto">
<AggregateImages features={$featuresStore} onlyShowUsername={$username} /> <AggregateImages features={$featuresStore} onlyShowUsername={$username?.split(";")} />
</div> </div>
{/if} {/if}
</div> </div>