forked from MapComplete/MapComplete
Feature(inspector): allow to load multiple contributors at once
This commit is contained in:
parent
7c6224fd3e
commit
063a912c82
8 changed files with 69 additions and 41 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
{}
|
{
|
||||||
|
}
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue