Merge feature branch

This commit is contained in:
Pieter Vander Vennet 2024-12-04 18:49:18 +01:00
commit a6c752037b
28 changed files with 931 additions and 149 deletions

View file

@ -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(<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[]> {

View file

@ -14,7 +14,7 @@ export default class OsmObjectDownloader {
readonly isUploading: Store<boolean>
}
private readonly backend: string
private historyCache = new Map<string, UIEventSource<OsmObject[]>>()
private historyCache = new Map<string, Promise<OsmObject[]>>()
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<OsmNode[]>
public DownloadHistory(id: WayId): UIEventSource<OsmWay[]>
public DownloadHistory(id: RelationId): UIEventSource<OsmRelation[]>
public DownloadHistory(id: OsmId): UIEventSource<OsmObject[]>
public DownloadHistory(id: string): UIEventSource<OsmObject[]> {
if (this.historyCache.has(id)) {
return this.historyCache.get(id)
}
private async _downloadHistoryUncached(id: string): Promise<OsmObject[]> {
const splitted = id.split("/")
const type = splitted[0]
const idN = Number(splitted[1])
const src = new UIEventSource<OsmObject[]>([])
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<OsmNode[]>
public downloadHistory(id: WayId): Promise<OsmWay[]>
public downloadHistory(id: RelationId): Promise<OsmRelation[]>
public downloadHistory(id: OsmId): Promise<OsmObject[]>
public async downloadHistory(id: string): Promise<OsmObject[]> {
if (this.historyCache.has(id)) {
return this.historyCache.get(id)
}
const promise = this._downloadHistoryUncached(id)
this.historyCache.set(id, promise)
return promise
}
/**

View file

@ -26,7 +26,10 @@ export class Overpass {
) {
this._timeout = timeout ?? new ImmutableStore<number>(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'

View file

@ -727,6 +727,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
}
/**
* Parse the number and round to the nearest int
*
* @param source
* UIEventSource.asInt(new UIEventSource("123")).data // => 123

View file

@ -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

View file

@ -306,7 +306,7 @@ export default class ThemeConfig implements ThemeInformation {
return { untranslated, total }
}
public getMatchingLayer(tags: Record<string, string>): LayerConfig | undefined {
public getMatchingLayer(tags: Record<string, string>, blacklistLayers?: Set<string>): 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

View file

@ -1061,7 +1061,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<string, string>) {
public getMatchingLayer(properties: Record<string, string>): LayerConfig | undefined {
const id = properties.id
if (id.startsWith("summary_")) {

View file

@ -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<string, string>): 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)

View file

@ -279,6 +279,11 @@
</Page>
</div>
<a class="flex" href={window.location.protocol + "//" + window.location.host + "/inspector.html"}>
<MagnifyingGlassCircle class="mr-2 h-6 w-6" />
<Tr t={Translations.t.inspector.menu} />
</a>
<a class="flex" href="https://github.com/pietervdvn/MapComplete/" target="_blank">
<Github class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.gotoSourceCode} />

View file

@ -1,23 +1,21 @@
<script lang="ts">
import type { Feature } from "geojson"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { ariaLabel } from "../../Utils/ariaLabel"
import { CloseButton } from "flowbite-svelte"
export let state: SpecialVisualizationState
export let layer: LayerConfig
export let selectedElement: Feature
let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(
let tags: UIEventSource<Record<string, string>> = state?.featureProperties?.getStore(
selectedElement.properties.id
)
$: {
tags = state.featureProperties.getStore(selectedElement.properties.id)
tags = state?.featureProperties?.getStore(selectedElement.properties.id)
}
let isTesting = state.featureSwitchIsTesting

View file

@ -0,0 +1,46 @@
<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 AttributedPanoramaxImage from "./AttributedPanoramaxImage.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 usernamesSet = new Set(onlyShowUsername)
let allDiffs: Store<{
key: string;
value?: string;
oldValue?: string
}[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, usernamesSet))
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}
<div class="flex">
{#each $addedImages as imgDiff}
<div class="w-48 h-48">
<AttributedPanoramaxImage hash={imgDiff.value} />
</div>
{/each}
</div>
{/if}

View file

@ -0,0 +1,105 @@
<script lang="ts">
import type { Feature } from "geojson"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
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 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 features: Feature[]
let usernames = new Set(onlyShowUsername)
const downloader = new OsmObjectDownloader()
let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.FromPromise(
Promise.all(features.map(f => downloader.downloadHistory(f.properties.id)))
)
let allDiffs: Store<{
key: string;
value?: string;
oldValue?: string
}[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, usernames))
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>>()
for (const diff of allDiffs) {
const k = diff.key
if (!keyCounts.has(k)) {
keyCounts.set(k, new Map<string, number>())
}
const valueCounts = keyCounts.get(k)
const v = diff.value ?? ""
valueCounts.set(v, 1 + (valueCounts.get(v) ?? 0))
}
const perKey: {
key: string, tr: TagRenderingConfig, count: number, values:
{ value: string, count: number }[]
}[] = []
keyCounts.forEach((values, key) => {
const keyTotal: { value: string, count: number }[] = []
values.forEach((count, value) => {
keyTotal.push({ value, count })
})
let countForKey = 0
for (const { count } of keyTotal) {
countForKey += count
}
keyTotal.sort((a, b) => b.count - a.count)
const tr = detectQuestion(key)
perKey.push({ count: countForKey, tr, key, values: keyTotal })
})
perKey.sort((a, b) => b.count - a.count)
return perKey
})
const t = Translations.t.inspector
</script>
{#if allHistories === undefined}
<Loading />
{:else if $allDiffs !== undefined}
{#each $mergedCount as diff}
<h3>
{#if diff.tr}
<Tr t={diff.tr.question} />
{:else}
{diff.key}
{/if}
</h3>
<AccordionSingle>
<span slot="header">
<Tr t={t.answeredCountTimes.Subs(diff)} />
</span>
<ul>
{#each diff.values as value}
<li>
<b>{value.value}</b>
{#if value.count > 1}
- {value.count}
{/if}
</li>
{/each}
</ul>
</AccordionSingle>
{/each}
{/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

@ -0,0 +1,106 @@
<script lang="ts">
/**
* Shows a history of the object which focuses on changes made by a certain username
*/
import type { OsmId } from "../../Models/OsmFeature"
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
import { UIEventSource } from "../../Logic/UIEventSource"
import Loading from "../Base/Loading.svelte"
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 id: OsmId
let usernames = new Set(onlyShowChangesBy)
let fullHistory = UIEventSource.FromPromise(new OsmObjectDownloader().downloadHistory(id))
let partOfLayer = fullHistory.mapD(history => history.map(step => ({
step,
layer: HistoryUtils.determineLayer(step.tags)
})))
let filteredHistory = partOfLayer.mapD(history =>
history.filter(({ step }) => {
if (usernames.size == 0) {
return true
}
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)
return { step, layer, diff }
}))
let lastStep = filteredHistory.mapD(history => history.at(-1))
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<string> = new Set(["walls_and_buildings"])
const t = Translations.t.inspector.previousContributors
</script>
{#if !$allGeometry || !ignoreLayersIfNoChanges.has($lastStep?.layer?.id)}
{#if $lastStep?.layer}
<a href={"https://openstreetmap.org/" + $lastStep.step.tags.id} target="_blank">
<h3 class="flex items-center gap-x-2">
<div class="w-8 h-8 shrink-0 inline-block">
<ToSvelte construct={$lastStep.layer?.defaultIcon($lastStep.step.tags)} />
</div>
<Tr t={$lastStep.layer?.title?.GetRenderValue($lastStep.step.tags)?.Subs($lastStep.step.tags)} />
</h3>
</a>
{/if}
{#if !$filteredHistory}
<Loading>Loading history...</Loading>
{:else if $filteredHistory.length === 0}
<Tr t={t.onlyGeometry} />
{:else}
<table class="w-full m-1">
{#each $filteredHistory as { step, layer }}
{#if step.version === 1}
<tr>
<td colspan="3">
<h3>
<Tr t={t.createdBy.Subs({contributor: step.tags["_last_edit:contributor"]})} />
</h3>
</td>
</tr>
{/if}
{#if HistoryUtils.tagHistoryDiff(step, $fullHistory).length === 0}
<tr>
<td class="font-bold justify-center flex w-full" colspan="3">
<Tr t={t.onlyGeometry} />
</td>
</tr>
{:else}
{#each HistoryUtils.tagHistoryDiff(step, $fullHistory) as diff}
<tr>
<td><a href={"https://osm.org/changeset/"+step.tags["_last_edit:changeset"]}
target="_blank">{step.version}</a></td>
<td>{layer?.id ?? "Unknown layer"}</td>
{#if diff.oldValue === undefined}
<td>{diff.key}</td>
<td>{diff.value}</td>
{:else if diff.value === undefined }
<td>{diff.key}</td>
<td class="line-through"> {diff.value}</td>
{:else}
<td>{diff.key}</td>
<td><span class="line-through"> {diff.oldValue}</span>{diff.value}</td>
{/if}
</tr>
{/each}
{/if}
{/each}
</table>
{/if}
{/if}

View file

@ -0,0 +1,51 @@
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 {
public static readonly personalTheme = new ThemeConfig(<any> all_layers, true)
private static ignoredLayers = new Set<string>(["fixme"])
public static determineLayer(properties: Record<string, string>){
return this.personalTheme.getMatchingLayer(properties, this.ignoredLayers)
}
public static tagHistoryDiff(step: OsmObject, history: OsmObject[]): {
key: string,
value?: 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], step
}))
}
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, step
}
}).filter(ch => ch.oldValue !== ch.value)
}
public static fullHistoryDiff(histories: OsmObject[][], onlyShowUsername?: Set<string>){
const allDiffs: {key: string, oldValue?: string, value?: string}[] = [].concat(...histories.map(
history => {
const filtered = history.filter(step => !onlyShowUsername || onlyShowUsername?.has(step.tags["_last_edit:contributor"] ))
const diffs: {
key: string;
value?: string;
oldValue?: string
}[][] = filtered.map(step => HistoryUtils.tagHistoryDiff(step, history))
return [].concat(...diffs)
}
))
return allDiffs
}
}

View file

@ -0,0 +1,107 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import LoginToggle from "../Base/LoginToggle.svelte"
import { createEventDispatcher } from "svelte"
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import Dropdown from "../Base/Dropdown.svelte"
export let osmConnection: OsmConnection
export let inspectedContributors: UIEventSource<{
name: string,
visitedTime: string,
label: string
}[]>
let dispatch = createEventDispatcher<{ selectUser: string }>()
let labels = UIEventSource.asObject<string[]>(osmConnection.getPreference("previously-spied-labels"), [])
let labelField = ""
function remove(user: string) {
inspectedContributors.set(inspectedContributors.data.filter(entry => entry.name !== user))
}
function addLabel() {
if (labels.data.indexOf(labelField) >= 0) {
return
}
labels.data.push(labelField)
labels.ping()
labelField = ""
}
function sort(key: string) {
console.log("Sorting on", key)
inspectedContributors.data.sort((a, b) => (a[key] ?? "").localeCompare(b[key] ?? ""))
inspectedContributors.ping()
}
</script>
<LoginToggle ignoreLoading state={{osmConnection}}>
<table class="w-full">
<tr>
<td>
<button class="as-link cursor-pointer" on:click={() => sort("name")}>
Contributor
</button>
</td>
<td>
<button class="as-link cursor-pointer" on:click={() => sort("visitedTime")}>
Visited time
</button>
</td>
<td>
<button class="as-link cursor-pointer" on:click={() => sort("label")}>Label</button>
</td>
<td>Remove</td>
</tr>
{#each $inspectedContributors as c}
<tr>
<td>
<button class="as-link" on:click={() => dispatch("selectUser", c.name)}>{c.name}</button>
</td>
<td>
{c.visitedTime}
</td>
<td>
<select bind:value={c.label} on:change={() => inspectedContributors.ping()}>
<option value={undefined}><i>No label</i></option>
{#each $labels as l}
<option value={l}>{l}</option>
{/each}
</select>
</td>
<td>
<XCircleIcon class="w-6 h-6" on:click={() => remove(c.name)} />
</td>
</tr>
{/each}
</table>
<AccordionSingle>
<div slot="header">Labels</div>
{#if $labels.length === 0}
No labels
{:else}
{#each $labels as label}
<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}
{/if}
<div class="interactive flex m-2 items-center gap-x-2 rounded-lg p-2">
<div class="shrink-0">Create a new label</div>
<input bind:value={labelField} type="text" />
<button on:click={() => addLabel()} class:disabled={!(labelField?.length > 0) } class="disabled shrink-0">Add
label
</button>
</div>
</AccordionSingle>
</LoginToggle>

View file

@ -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<ProvidedImage>
export let previewedImage: UIEventSource<ProvidedImage> = 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)
}}
/>
</div>
@ -124,7 +126,7 @@
{#if canZoom && loaded}
<div
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" />
</div>

227
src/UI/InspectorGUI.svelte Normal file
View file

@ -0,0 +1,227 @@
<script lang="ts">
import { UIEventSource } from "../Logic/UIEventSource"
import { QueryParameters } from "../Logic/Web/QueryParameters"
import ValidatedInput from "./InputElement/ValidatedInput.svelte"
import { Overpass } from "../Logic/Osm/Overpass"
import Constants from "../Models/Constants"
import MaplibreMap from "./Map/MaplibreMap.svelte"
import { MapLibreAdaptor } from "./Map/MapLibreAdaptor"
import { Map as MlMap } from "maplibre-gl"
import ShowDataLayer from "./Map/ShowDataLayer"
import * as inspector_theme from "../assets/generated/themes/inspector.json"
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
import type { Feature } from "geojson"
import Loading from "./Base/Loading.svelte"
import { linear } from "svelte/easing"
import { Drawer } from "flowbite-svelte"
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
import History from "./History/History.svelte"
import TitledPanel from "./Base/TitledPanel.svelte"
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"
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"
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")
let map = new UIEventSource<MlMap>(undefined)
let zoom = UIEventSource.asFloat(QueryParameters.GetQueryParameter("z", "0"))
let lat = UIEventSource.asFloat(QueryParameters.GetQueryParameter("lat", "0"))
let lon = UIEventSource.asFloat(QueryParameters.GetQueryParameter("lon", "0"))
let theme = new ThemeConfig(<any>inspector_theme, true)
let layer = theme.layers.find(l => l.id === "usertouched")
// Is this a dirty hack? Yes it is!
theme.getMatchingLayer = () => {
return layer
}
let loadingData = false
let selectedElement = new UIEventSource<Feature>(undefined)
let maplibremap: MapLibreAdaptor = new MapLibreAdaptor(map, {
zoom,
location: new UIEventSource<{ lon: number; lat: number }>({ lat: lat.data, lon: lon.data })
})
maplibremap.location.stabilized(500).addCallbackAndRunD(l => {
lat.set(l.lat)
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 features = new StaticFeatureSource(featuresStore)
ShowDataLayer.showMultipleLayers(map, features, [...layersNoFixme, fixme] , {
zoomToFeatures: true,
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
if(user.indexOf(";")<0){
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")
featuresStore.set([])
const overpass = new Overpass(undefined, user.split(";").map(user => "nw(user_touched:\"" + user + "\");"), Constants.defaultOverpassUrls[0])
if (!maplibremap.bounds.data) {
return
}
loadingData = true
const [data, date] = await overpass.queryGeoJson(maplibremap.bounds.data)
console.log("Overpass result:", data)
loadingData = false
console.log(data, date)
featuresStore.set(data.features)
console.log("Loaded", data.features.length)
}
map.addCallbackAndRunD(() => {
// when the map is loaded: attempt to load the user given via Queryparams
if (username.data) {
console.log("Current username is", username.data)
load()
}
return true
})
let mode: "map" | "table" | "aggregate" | "images" = "map"
let showPreviouslyVisited = new UIEventSource(true)
const t = Translations.t.inspector
</script>
<div class="flex flex-col w-full h-full">
<div class="flex gap-x-2 items-center low-interaction p-2">
<MagnifyingGlassCircle class="w-12 h-12"/>
<h1 class="flex-shrink-0 m-0 mx-2">
<Tr t={t.title}/>
</h1>
<ValidatedInput type="string" value={username} on:submit={() => load()} />
{#if loadingData}
<Loading />
{:else}
<button class="primary" on:click={() => load()}>
<Tr t={t.load}/>
</button>
{/if}
<button on:click={() => showPreviouslyVisited.setData(true)}>
<Tr t={t.earlierInspected}/>
</button>
<a href="./index.html" class="button">
<Tr t={t.backToIndex}/>
</a>
</div>
<div class="flex">
<button class:primary={mode === "map"} on:click={() => mode = "map"}>
<Tr t={t.mapView}/>
</button>
<button class:primary={mode === "table"} on:click={() => mode = "table"}>
<Tr t={t.tableView}/>
</button>
<button class:primary={mode === "aggregate"} on:click={() => mode = "aggregate"}>
<Tr t={t.aggregateView}/>
</button>
<button class:primary={mode === "images"} on:click={() => mode = "images"}>
<Tr t={t.images}/>
</button>
</div>
{#if mode === "map"}
{#if $selectedElement !== undefined}
<!-- right modal with the selected element view -->
<Drawer
placement="right"
transitionType="fly"
activateClickOutside={false}
backdrop={false}
id="drawer-right"
width="w-full md:w-6/12 lg:w-5/12 xl:w-4/12"
rightOffset="inset-y-0 right-0"
transitionParams={{
x: 640,
duration: 0,
easing: linear,
}}
divClass="overflow-y-auto z-50 bg-white"
hidden={$selectedElement === undefined}
on:close={() => {
selectedElement.setData(undefined)
}}
>
<TitledPanel>
<div slot="title" class="flex justify-between">
<a target="_blank" rel="noopener"
href={"https://osm.org/"+$selectedElement.properties.id}>{$selectedElement.properties.id}</a>
<XCircleIcon class="w-6 h-6" on:click={() => selectedElement.set(undefined)} />
</div>
<History onlyShowChangesBy={$username} id={$selectedElement.properties.id}></History>
</TitledPanel>
</Drawer>
{/if}
<div class="flex-grow overflow-hidden m-1 rounded-xl">
<MaplibreMap map={map} mapProperties={maplibremap} autorecovery={true} />
</div>
{:else if mode === "table"}
<div class="m-2 h-full overflow-y-auto">
{#each $featuresStore as f}
<History onlyShowChangesBy={$username?.split(";")} id={f.properties.id} />
{/each}
</div>
{:else if mode === "aggregate"}
<div class="m-2 h-full overflow-y-auto">
<AggregateView features={$featuresStore} onlyShowUsername={$username?.split(";")} />
</div>
{:else if mode === "images"}
<div class="m-2 h-full overflow-y-auto">
<AggregateImages features={$featuresStore} onlyShowUsername={$username?.split(";")} />
</div>
{/if}
</div>
<Page shown={showPreviouslyVisited}>
<div slot="header">Earlier inspected constributors</div>
<PreviouslySpiedUsers {osmConnection} {inspectedContributors} on:selectUser={(e) => {
username.set(e.detail); load();showPreviouslyVisited.set(false)
}} />
</Page>

5
src/UI/InspectorGUI.ts Normal file
View file

@ -0,0 +1,5 @@
import InspectorGUI from "./InspectorGUI.svelte"
new InspectorGUI({
target: document.getElementById("main"),
})

View file

@ -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"])

View file

@ -87,7 +87,7 @@ export interface SpecialVisualizationState {
readonly geocodedImages: UIEventSource<Feature[]>
readonly searchState: SearchState
getMatchingLayer(properties: Record<string, string>)
getMatchingLayer(properties: Record<string, string>): LayerConfig | undefined
showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer
reportError(message: string | Error | XMLHttpRequest, extramessage?: string): Promise<void>