More work on inspector

This commit is contained in:
Pieter Vander Vennet 2024-12-01 01:39:13 +01:00
parent 552ea22275
commit 951bd3c0ae
9 changed files with 257 additions and 108 deletions

View file

@ -3077,6 +3077,18 @@
} }
} }
] ]
},
{
"id": "name",
"question":{
"en": "What is the name of this place?"
},
"render": {
"*": "<b>{name}</b>"
},
"freeform": {
"key": "name"
}
} }
], ],
"allowMove": false "allowMove": false

View file

@ -12,7 +12,7 @@ import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte"
import Link from "../../UI/Base/Link" import Link from "../../UI/Base/Link"
export default class PanoramaxImageProvider extends ImageProvider { 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 readonly xyz = new PanoramaxXYZ()
private static defaultPanoramax = new AuthorizedPanoramax( private static defaultPanoramax = new AuthorizedPanoramax(
Constants.panoramax.url, Constants.panoramax.url,
@ -126,7 +126,11 @@ export default class PanoramaxImageProvider extends ImageProvider {
if (!Panoramax.isId(value)) { if (!Panoramax.isId(value)) {
return undefined return undefined
} }
return [await this.getInfoFor(value).then((r) => this.featureToImage(<any>r))] return [await this.getInfo(value)]
}
public async getInfo(hash: string): Promise<ProvidedImage> {
return await this.getInfoFor(hash).then((r) => this.featureToImage(<any>r))
} }
getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> { getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {

View file

@ -0,0 +1,43 @@
<script lang="ts">
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { HistoryUtils } from "./HistoryUtils"
import type { Feature } from "geojson"
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 features: Feature[]
const downloader = new OsmObjectDownloader()
let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.FromPromise(
Promise.all(features.map(f => downloader.downloadHistory(f.properties.id)))
)
let imageKeys = new Set(...["panoramax", "image:streetsign", "image:menu"].map(k => {
const result: string[] = [k]
for (let i = 0; i < 10; i++) {
result.push(k + ":" + i)
}
return result
}))
let allDiffs: Store<{
key: string;
value?: string;
oldValue?: string
}[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, onlyShowUsername))
let addedImages = allDiffs.mapD(diffs => [].concat(...diffs.filter(({ key }) => imageKeys.has(key))))
</script>
{#if $allDiffs === undefined}
<Loading />
{:else if $addedImages.length === 0}
No images added by this contributor
{:else}
{#each $addedImages as imgDiff}
<AttributedPanoramaxImage hash={imgDiff.value} />
{/each}
{/if}

View file

@ -5,6 +5,11 @@
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 { HistoryUtils } from "./HistoryUtils" 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 onlyShowUsername: string
export let features: Feature[] export let features: Feature[]
@ -13,26 +18,28 @@
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)))
) )
let allDiffs: Store<{ key: string; value?: string; oldValue?: string }[]> = allHistories.mapD(histories => { let allDiffs: Store<{
const allDiffs = [].concat(...histories.map(
history => {
const filtered = history.filter(step => !onlyShowUsername || step.tags["_last_edit:contributor"] === onlyShowUsername)
const diffs: {
key: string; key: string;
value?: string; value?: string;
oldValue?: string oldValue?: string
}[][] = filtered.map(step => HistoryUtils.tagHistoryDiff(step, history)) }[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, onlyShowUsername))
return [].concat(...diffs)
}
))
return allDiffs
})
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<string, Map<string, number>>() const keyCounts = new Map<string, Map<string, number>>()
for (const diff of allDiffs) { for (const diff of allDiffs) {
const k = diff.key const k = diff.key
if(!keyCounts.has(k)){ if (!keyCounts.has(k)) {
keyCounts.set(k, new Map<string, number>()) keyCounts.set(k, new Map<string, number>())
} }
const valueCounts = keyCounts.get(k) const valueCounts = keyCounts.get(k)
@ -40,34 +47,57 @@
valueCounts.set(v, 1 + (valueCounts.get(v) ?? 0)) valueCounts.set(v, 1 + (valueCounts.get(v) ?? 0))
} }
const perKey: {key: string, count: number, values: const perKey: {
{value: string, count: number}[] key: string, tr: TagRenderingConfig, count: number, values:
{ value: string, count: number }[]
}[] = [] }[] = []
keyCounts.forEach((values, key) => { keyCounts.forEach((values, key) => {
const keyTotal : {value: string, count: number}[] = [] const keyTotal: { value: string, count: number }[] = []
values.forEach((count, value) => { values.forEach((count, value) => {
keyTotal.push({value, count}) keyTotal.push({ value, count })
}) })
let countForKey = 0 let countForKey = 0
for (const {count} of keyTotal) { for (const { count } of keyTotal) {
countForKey += count countForKey += count
} }
keyTotal.sort((a, b) => b.count - a.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) perKey.sort((a, b) => b.count - a.count)
return perKey return perKey
}) })
</script> </script>
{#if allHistories === undefined} {#if allHistories === undefined}
<Loading /> <Loading />
{:else if $allDiffs !== undefined} {:else if $allDiffs !== undefined}
{#each $mergedCount as diff} {#each $mergedCount as diff}
<div class="m-1 border-black border p-1"> <h3>
{JSON.stringify(diff)} {#if diff.tr}
</div> <Tr t={diff.tr.question} />
{:else}
{diff.key}
{/if}
</h3>
<AccordionSingle>
<span slot="header">
Answered {diff.count} times
</span>
<ul>
{#each diff.values as value}
<li>
<b>{value.value}</b>
{#if value.count > 1}
- {value.count}
{/if}
</li>
{/each}
</ul>
</AccordionSingle>
{/each} {/each}
{/if} {/if}

View file

@ -0,0 +1,13 @@
<script lang="ts">
import AttributedImage from "../Image/AttributedImage.svelte"
import PanoramaxImageProvider from "../../Logic/ImageProviders/Panoramax"
import { UIEventSource } from "../../Logic/UIEventSource"
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
export let hash: string
let image: UIEventSource<ProvidedImage> = UIEventSource.FromPromise(PanoramaxImageProvider.singleton.getInfo(hash))
</script>
{#if $image !== undefined}
<AttributedImage image={$image}></AttributedImage>
{/if}

View file

@ -8,7 +8,6 @@
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import { HistoryUtils } from "./HistoryUtils" import { HistoryUtils } from "./HistoryUtils"
import ToSvelte from "../Base/ToSvelte.svelte" import ToSvelte from "../Base/ToSvelte.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
export let onlyShowChangesBy: string export let onlyShowChangesBy: string
@ -28,29 +27,36 @@
console.log("Comparing ", step.tags["_last_edit:contributor"], onlyShowChangesBy, step.tags["_last_edit:contributor"] === onlyShowChangesBy) console.log("Comparing ", step.tags["_last_edit:contributor"], onlyShowChangesBy, step.tags["_last_edit:contributor"] === onlyShowChangesBy)
return 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 lastStep = filteredHistory.mapD(history => history.at(-1))
let l : LayerConfig let allGeometry = filteredHistory.mapD(all => !all.some(x => x.diff.length > 0))
// l.title.GetRenderValue({}).Subs({}) /**
* These layers are only shown if there are tag changes as well
*/
const ignoreLayersIfNoChanges: ReadonlySet<string> = new Set(["walls_and_buildings"])
</script> </script>
{#if $lastStep?.layer} {#if !$allGeometry || !ignoreLayersIfNoChanges.has($lastStep?.layer?.id)}
{#if $lastStep?.layer}
<a href={"https://openstreetmap.org/" + $lastStep.step.tags.id} target="_blank"> <a href={"https://openstreetmap.org/" + $lastStep.step.tags.id} target="_blank">
<h3 class="flex items-center gap-x-2"> <h3 class="flex items-center gap-x-2">
<div class="w-8 h-8 shrink-0 inline-block"> <div class="w-8 h-8 shrink-0 inline-block">
<ToSvelte construct={$lastStep.layer?.defaultIcon($lastStep.step.tags)} /> <ToSvelte construct={$lastStep.layer?.defaultIcon($lastStep.step.tags)} />
</div> </div>
<Tr t={$lastStep.layer?.title?.GetRenderValue($lastStep.step.tags)?.Subs($lastStep.step.tags)}/> <Tr t={$lastStep.layer?.title?.GetRenderValue($lastStep.step.tags)?.Subs($lastStep.step.tags)} />
</h3> </h3>
</a> </a>
{/if} {/if}
{#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 Only geometry changes found
{:else} {:else}
<table class="w-full m-1"> <table class="w-full m-1">
{#each $filteredHistory as { step, layer }} {#each $filteredHistory as { step, layer }}
@ -92,4 +98,5 @@ let l : LayerConfig
{/if} {/if}
{/each} {/each}
</table> </table>
{/if}
{/if} {/if}

View file

@ -4,7 +4,7 @@ import { OsmObject } from "../../Logic/Osm/OsmObject"
export class HistoryUtils { export class HistoryUtils {
private static personalTheme = new ThemeConfig(<any> all_layers, true) public static readonly personalTheme = new ThemeConfig(<any> all_layers, true)
private static ignoredLayers = new Set<string>(["fixme"]) private static ignoredLayers = new Set<string>(["fixme"])
public static determineLayer(properties: Record<string, string>){ public static determineLayer(properties: Record<string, string>){
return this.personalTheme.getMatchingLayer(properties, this.ignoredLayers) return this.personalTheme.getMatchingLayer(properties, this.ignoredLayers)
@ -13,12 +13,13 @@ export class HistoryUtils {
public static tagHistoryDiff(step: OsmObject, history: OsmObject[]): { public static tagHistoryDiff(step: OsmObject, history: OsmObject[]): {
key: string, key: string,
value?: string, value?: string,
oldValue?: string oldValue?: string,
step: OsmObject
}[] { }[] {
const previous = history[step.version - 2] const previous = history[step.version - 2]
if (!previous) { if (!previous) {
return Object.keys(step.tags).filter(key => !key.startsWith("_") && key !== "id").map(key => ({ 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 const previousTags = previous.tags
@ -27,9 +28,24 @@ export class HistoryUtils {
const value = step.tags[key] const value = step.tags[key]
const oldValue = previousTags[key] const oldValue = previousTags[key]
return { return {
key, value, oldValue key, value, oldValue, step
} }
}).filter(ch => ch.oldValue !== ch.value) }).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
}
} }

View file

@ -28,22 +28,24 @@
export let imgClass: string = undefined export let imgClass: string = undefined
export let state: SpecialVisualizationState = undefined export let state: SpecialVisualizationState = undefined
export let attributionFormat: "minimal" | "medium" | "large" = "medium" export let attributionFormat: "minimal" | "medium" | "large" = "medium"
export let previewedImage: UIEventSource<ProvidedImage> export let previewedImage: UIEventSource<ProvidedImage> = undefined
export let canZoom = previewedImage !== undefined export let canZoom = previewedImage !== undefined
let loaded = false let loaded = false
let showBigPreview = new UIEventSource(false) let showBigPreview = new UIEventSource(false)
onDestroy( onDestroy(
showBigPreview.addCallbackAndRun((shown) => { showBigPreview.addCallbackAndRun((shown) => {
if (!shown) { if (!shown) {
previewedImage.set(undefined) previewedImage?.set(undefined)
} }
}) })
) )
if(previewedImage){
onDestroy( onDestroy(
previewedImage.addCallbackAndRun((previewedImage) => { previewedImage.addCallbackAndRun((previewedImage) => {
showBigPreview.set(previewedImage?.id === image.id) showBigPreview.set(previewedImage?.id === image.id)
}) })
) )
}
function highlight(entered: boolean = true) { function highlight(entered: boolean = true) {
if (!entered) { if (!entered) {
@ -82,7 +84,7 @@
class="normal-background" class="normal-background"
on:click={() => { on:click={() => {
console.log("Closing") console.log("Closing")
previewedImage.set(undefined) previewedImage?.set(undefined)
}} }}
/> />
</div> </div>
@ -124,7 +126,7 @@
{#if canZoom && loaded} {#if canZoom && loaded}
<div <div
class="bg-black-transparent absolute right-0 top-0 rounded-bl-full" class="bg-black-transparent absolute right-0 top-0 rounded-bl-full"
on:click={() => previewedImage.set(image)} on:click={() => previewedImage?.set(image)}
> >
<MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" /> <MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" />
</div> </div>

View file

@ -21,6 +21,8 @@
import { XCircleIcon } from "@babeard/svelte-heroicons/solid" import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
import { Utils } from "../Utils" import { Utils } from "../Utils"
import AggregateView from "./History/AggregateView.svelte" 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 username = QueryParameters.GetQueryParameter("user", undefined, "Inspect this user")
let step = new UIEventSource<"waiting" | "loading" | "done">("waiting") let step = new UIEventSource<"waiting" | "loading" | "done">("waiting")
@ -49,7 +51,17 @@
let featuresStore = new UIEventSource<Feature[]>([]) let featuresStore = new UIEventSource<Feature[]>([])
let features = new StaticFeatureSource(featuresStore) let features = new StaticFeatureSource(featuresStore)
new ShowDataLayer(map, 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, layer,
zoomToFeatures: true, zoomToFeatures: true,
@ -60,7 +72,7 @@
selectedElement.set(f) selectedElement.set(f)
}) })
} }
}) })*/
async function load() { async function load() {
@ -87,7 +99,7 @@
return true return true
}) })
let mode: "map" | "table" | "aggregate" = "map" let mode: "map" | "table" | "aggregate" | "images" = "map"
</script> </script>
<div class="flex flex-col w-full h-full"> <div class="flex flex-col w-full h-full">
@ -113,6 +125,9 @@
<button class:primary={mode === "aggregate"} on:click={() => mode = "aggregate"}> <button class:primary={mode === "aggregate"} on:click={() => mode = "aggregate"}>
Aggregate Aggregate
</button> </button>
<button class:primary={mode === "images"} on:click={() => mode = "images"}>
Images
</button>
</div> </div>
{#if mode === "map"} {#if mode === "map"}
@ -155,11 +170,18 @@
<MaplibreMap map={map} mapProperties={maplibremap} autorecovery={true} /> <MaplibreMap map={map} mapProperties={maplibremap} autorecovery={true} />
</div> </div>
{:else if mode === "table"} {:else if mode === "table"}
<div class="m-2 h-full overflow-y-auto">
{#each $featuresStore as f} {#each $featuresStore as f}
<h3><a href={"https://osm.org/"+f.properties.id} target="_blank">{f.properties.id}</a></h3>
<History onlyShowChangesBy={$username} id={f.properties.id} /> <History onlyShowChangesBy={$username} id={f.properties.id} />
{/each} {/each}
{:else} </div>
<AggregateView features={$featuresStore} onlyShowUsername={$username}></AggregateView> {:else if mode === "aggregate"}
<div class="m-2 h-full overflow-y-auto">
<AggregateView features={$featuresStore} onlyShowUsername={$username} />
</div>
{:else if mode === "images"}
<div class="m-2 h-full overflow-y-auto">
<AggregateImages features={$featuresStore} onlyShowUsername={$username} />
</div>
{/if} {/if}
</div> </div>