forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
00c233a2eb
188 changed files with 4982 additions and 1745 deletions
|
@ -53,6 +53,8 @@
|
|||
fill: var(--button-background-hover);
|
||||
transition: fill 350ms linear;
|
||||
cursor: pointer;
|
||||
stroke-width: 0.8;
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
:global(.dots-menu:hover > path, .dots-menu-opened > path) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import { Translation } from "../i18n/Translation"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "./Tr.svelte"
|
||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Invalid from "../../assets/svg/Invalid.svelte"
|
||||
import ArrowPath from "@babeard/svelte-heroicons/mini/ArrowPath"
|
||||
|
||||
|
@ -21,6 +21,10 @@
|
|||
* Only show the 'successful' state, don't show loading or error messages
|
||||
*/
|
||||
export let silentFail: boolean = false
|
||||
/**
|
||||
* If set and the OSM-api fails, do _not_ show any error messages nor the successful state, just hide
|
||||
*/
|
||||
export let hiddenFail: boolean = false
|
||||
let loadingStatus = state?.osmConnection?.loadingStatus ?? new ImmutableStore("logged-in")
|
||||
let badge = state?.featureSwitches?.featureSwitchEnableLogin ?? new ImmutableStore(true)
|
||||
const t = Translations.t.general
|
||||
|
@ -30,7 +34,7 @@
|
|||
unknown: t.loginFailedUnreachableMode,
|
||||
readonly: t.loginFailedReadonlyMode,
|
||||
}
|
||||
const apiState =
|
||||
const apiState: Store<string> =
|
||||
state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online")
|
||||
</script>
|
||||
|
||||
|
@ -39,19 +43,21 @@
|
|||
<slot name="loading">
|
||||
<Loading />
|
||||
</slot>
|
||||
{:else if !silentFail && $loadingStatus === "error"}
|
||||
<slot name="error">
|
||||
<div class="alert flex flex-col items-center">
|
||||
<div class="max-w-64 flex items-center">
|
||||
<Invalid class="m-2 h-8 w-8 shrink-0" />
|
||||
<Tr t={offlineModes[$apiState] ?? t.loginFailedUnreachableMode} />
|
||||
{:else if !silentFail && ($loadingStatus === "error" || $apiState === "readonly" || $apiState === "offline")}
|
||||
{#if !hiddenFail}
|
||||
<slot name="error">
|
||||
<div class="alert flex flex-col items-center">
|
||||
<div class="max-w-64 flex items-center">
|
||||
<Invalid class="m-2 h-8 w-8 shrink-0" />
|
||||
<Tr t={offlineModes[$apiState] ?? t.loginFailedUnreachableMode} />
|
||||
</div>
|
||||
<button class="h-fit" on:click={() => state.osmConnection.AttemptLogin()}>
|
||||
<ArrowPath class="h-6 w-6" />
|
||||
<Tr t={t.retry} />
|
||||
</button>
|
||||
</div>
|
||||
<button class="h-fit" on:click={() => state.osmConnection.AttemptLogin()}>
|
||||
<ArrowPath class="h-6 w-6" />
|
||||
<Tr t={t.retry} />
|
||||
</button>
|
||||
</div>
|
||||
</slot>
|
||||
</slot>
|
||||
{/if}
|
||||
{:else if $loadingStatus === "logged-in"}
|
||||
<slot />
|
||||
{:else if !silentFail && $loadingStatus === "not-attempted"}
|
||||
|
|
|
@ -28,9 +28,9 @@
|
|||
(s) =>
|
||||
(s === "yes" &&
|
||||
state?.userRelatedState?.osmConnection?.userDetails?.data?.csCount >=
|
||||
Constants.userJourney.tagsVisibleAt) ||
|
||||
Constants.userJourney.tagsVisibleAt) ||
|
||||
s === "always" ||
|
||||
s === "full",
|
||||
s === "full"
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -80,9 +80,9 @@
|
|||
<Checkbox selected={getBooleanStateFor(filter)}>
|
||||
<Tr t={filter.options[0].question} />
|
||||
{#if $showTags && filter.options[0].osmTags !== undefined}
|
||||
<span class="subtle">
|
||||
{filter.options[0].osmTags.asHumanString()}
|
||||
</span>
|
||||
<span class="subtle">
|
||||
{filter.options[0].osmTags.asHumanString()}
|
||||
</span>
|
||||
{/if}
|
||||
</Checkbox>
|
||||
{/if}
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
</svelte:fragment>
|
||||
|
||||
<!-- All shown components are set by 'usersettings.json', which happily uses some special visualisations created specifically for it -->
|
||||
<LoginToggle {state}>
|
||||
<LoginToggle {state} silentFail>
|
||||
<div class="flex flex-col" slot="not-logged-in">
|
||||
<LanguagePicker availableLanguages={theme.language} />
|
||||
<Tr cls="alert" t={Translations.t.userinfo.notLoggedIn} />
|
||||
|
@ -146,7 +146,7 @@
|
|||
</LoginToggle>
|
||||
</Page>
|
||||
|
||||
<LoginToggle {state}>
|
||||
<LoginToggle {state} silentFail>
|
||||
<Page {onlyLink} shown={pg.favourites}>
|
||||
<svelte:fragment slot="header">
|
||||
<HeartIcon />
|
||||
|
|
|
@ -128,17 +128,20 @@
|
|||
|
||||
{#if $unknownImages.length > 0}
|
||||
{#if readonly}
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div class="flex h-32 w-max gap-x-2">
|
||||
{#each $unknownImages as image (image)}
|
||||
<div
|
||||
class="flex w-full space-x-2 overflow-x-auto border border-gray-600 p-1"
|
||||
style="scroll-snap-type: x proximity; border: 1px solid black"
|
||||
>
|
||||
{#each $unknownImages as image (image)}
|
||||
<div class="relative flex w-fit items-center bg-gray-200">
|
||||
<AttributedImage
|
||||
{state}
|
||||
imgClass="h-32 w-max shrink-0"
|
||||
image={{ url: image }}
|
||||
imgClass="h-32 shrink-0"
|
||||
image={{ url: image, id: image }}
|
||||
previewedImage={state.previewedImage}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
{#each $unknownImages as image (image)}
|
||||
|
|
|
@ -12,25 +12,31 @@
|
|||
|
||||
const downloader = new OsmObjectDownloader()
|
||||
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 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 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))))
|
||||
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}
|
||||
|
@ -38,7 +44,7 @@
|
|||
{:else}
|
||||
<div class="flex">
|
||||
{#each $addedImages as imgDiff}
|
||||
<div class="w-48 h-48">
|
||||
<div class="h-48 w-48">
|
||||
<AttributedPanoramaxImage hash={imgDiff.value} />
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
@ -18,26 +18,30 @@
|
|||
|
||||
const downloader = new OsmObjectDownloader()
|
||||
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 => HistoryUtils.fullHistoryDiff(histories, usernames))
|
||||
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))
|
||||
const trs = shared_questions.tagRenderings.map((tr) => new TagRenderingConfig(tr))
|
||||
|
||||
function detectQuestion(key: string): TagRenderingConfig {
|
||||
return trs.find(tr => tr.freeform?.key === key)
|
||||
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 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
|
||||
|
@ -50,11 +54,13 @@
|
|||
}
|
||||
|
||||
const perKey: {
|
||||
key: string, tr: TagRenderingConfig, count: number, values:
|
||||
{ value: string, count: number }[]
|
||||
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 })
|
||||
})
|
||||
|
@ -72,7 +78,6 @@
|
|||
})
|
||||
|
||||
const t = Translations.t.inspector
|
||||
|
||||
</script>
|
||||
|
||||
{#if allHistories === undefined}
|
||||
|
@ -88,7 +93,7 @@
|
|||
</h3>
|
||||
<AccordionSingle>
|
||||
<span slot="header">
|
||||
<Tr t={t.answeredCountTimes.Subs(diff)} />
|
||||
<Tr t={t.answeredCountTimes.Subs(diff)} />
|
||||
</span>
|
||||
<ul>
|
||||
{#each diff.values as value}
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
|
||||
|
||||
export let hash: string
|
||||
let image: UIEventSource<ProvidedImage> = UIEventSource.FromPromise(PanoramaxImageProvider.singleton.getInfo(hash))
|
||||
let image: UIEventSource<ProvidedImage> = UIEventSource.FromPromise(
|
||||
PanoramaxImageProvider.singleton.getInfo(hash)
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if $image !== undefined}
|
||||
<AttributedImage image={$image}></AttributedImage>
|
||||
<AttributedImage image={$image} />
|
||||
{/if}
|
||||
|
|
|
@ -17,41 +17,51 @@
|
|||
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 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))
|
||||
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">
|
||||
<div class="inline-block h-8 w-8 shrink-0">
|
||||
<ToSvelte construct={$lastStep.layer?.defaultIcon($lastStep.step.tags)} />
|
||||
</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>
|
||||
</a>
|
||||
{/if}
|
||||
|
@ -61,42 +71,48 @@
|
|||
{:else if $filteredHistory.length === 0}
|
||||
<Tr t={t.onlyGeometry} />
|
||||
{:else}
|
||||
<table class="w-full m-1">
|
||||
<table class="m-1 w-full">
|
||||
{#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"]})} />
|
||||
<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">
|
||||
<td class="flex w-full justify-center font-bold" 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>
|
||||
<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 }
|
||||
{:else if diff.value === undefined}
|
||||
<td>{diff.key}</td>
|
||||
<td class="line-through"> {diff.value}</td>
|
||||
<td class="line-through">{diff.value}</td>
|
||||
{:else}
|
||||
<td>{diff.key}</td>
|
||||
<td><span class="line-through"> {diff.oldValue}</span> → {diff.value}</td>
|
||||
<td>
|
||||
<span class="line-through">{diff.oldValue}</span>
|
||||
→ {diff.value}
|
||||
</td>
|
||||
{/if}
|
||||
|
||||
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
|
|
|
@ -3,49 +3,63 @@ 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)
|
||||
public static readonly personalTheme = new ThemeConfig(<any>all_layers, true)
|
||||
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)
|
||||
}
|
||||
|
||||
public static tagHistoryDiff(step: OsmObject, history: OsmObject[]): {
|
||||
key: string,
|
||||
value?: string,
|
||||
oldValue?: string,
|
||||
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
|
||||
}))
|
||||
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 => {
|
||||
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
|
||||
key,
|
||||
value,
|
||||
oldValue,
|
||||
step,
|
||||
}
|
||||
}).filter(ch => ch.oldValue !== ch.value)
|
||||
})
|
||||
.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"] ))
|
||||
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;
|
||||
key: string
|
||||
value?: string
|
||||
oldValue?: string
|
||||
}[][] = filtered.map(step => HistoryUtils.tagHistoryDiff(step, history))
|
||||
}[][] = filtered.map((step) => HistoryUtils.tagHistoryDiff(step, history))
|
||||
return [].concat(...diffs)
|
||||
}
|
||||
))
|
||||
})
|
||||
)
|
||||
return allDiffs
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
|
@ -9,18 +8,23 @@
|
|||
import Dropdown from "../Base/Dropdown.svelte"
|
||||
|
||||
export let osmConnection: OsmConnection
|
||||
export let inspectedContributors: UIEventSource<{
|
||||
name: string,
|
||||
visitedTime: string,
|
||||
label: string
|
||||
}[]>
|
||||
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 labels = UIEventSource.asObject<string[]>(
|
||||
osmConnection.getPreference("previously-spied-labels"),
|
||||
[]
|
||||
)
|
||||
let labelField = ""
|
||||
|
||||
function remove(user: string) {
|
||||
inspectedContributors.set(inspectedContributors.data.filter(entry => entry.name !== user))
|
||||
inspectedContributors.set(inspectedContributors.data.filter((entry) => entry.name !== user))
|
||||
}
|
||||
|
||||
function addLabel() {
|
||||
|
@ -39,16 +43,13 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<LoginToggle ignoreLoading state={{osmConnection}}>
|
||||
<LoginToggle ignoreLoading state={{ osmConnection }}>
|
||||
<table class="w-full">
|
||||
<tr>
|
||||
<td>
|
||||
<button class="as-link cursor-pointer" on:click={() => sort("name")}>
|
||||
Contributor
|
||||
</button>
|
||||
<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>
|
||||
|
@ -75,32 +76,46 @@
|
|||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<XCircleIcon class="w-6 h-6" on:click={() => remove(c.name)} />
|
||||
<XCircleIcon class="h-6 w-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
|
||||
<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="interactive m-2 flex 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
|
||||
on:click={() => addLabel()}
|
||||
class:disabled={!(labelField?.length > 0)}
|
||||
class="disabled shrink-0"
|
||||
>
|
||||
Add label
|
||||
</button>
|
||||
</div>
|
||||
</AccordionSingle>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
export let imgClass: string = undefined
|
||||
export let state: SpecialVisualizationState = undefined
|
||||
export let attributionFormat: "minimal" | "medium" | "large" = "medium"
|
||||
export let previewedImage: UIEventSource<ProvidedImage> = undefined
|
||||
export let previewedImage: UIEventSource<Partial<ProvidedImage>> = undefined
|
||||
export let canZoom = previewedImage !== undefined
|
||||
let loaded = false
|
||||
let showBigPreview = new UIEventSource(false)
|
||||
|
@ -37,14 +37,14 @@
|
|||
if (!shown) {
|
||||
previewedImage?.set(undefined)
|
||||
}
|
||||
})
|
||||
)
|
||||
if(previewedImage){
|
||||
onDestroy(
|
||||
previewedImage.addCallbackAndRun((previewedImage) => {
|
||||
showBigPreview.set(previewedImage?.id === image.id)
|
||||
})
|
||||
}),
|
||||
)
|
||||
if (previewedImage) {
|
||||
onDestroy(
|
||||
previewedImage.addCallbackAndRun((previewedImage) => {
|
||||
showBigPreview.set(previewedImage !== undefined && (previewedImage?.id ?? previewedImage?.url) === (image.id ?? image.url))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function highlight(entered: boolean = true) {
|
||||
|
@ -89,6 +89,8 @@
|
|||
/>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
|
||||
{#if image.status !== undefined && image.status !== "ready" && image.status !== "hidden"}
|
||||
<div class="flex h-full flex-col justify-center">
|
||||
<Loading>
|
||||
|
@ -113,6 +115,7 @@
|
|||
class={imgClass ?? ""}
|
||||
class:cursor-zoom-in={canZoom}
|
||||
on:click={() => {
|
||||
console.log("Setting",image.url)
|
||||
previewedImage?.set(image)
|
||||
}}
|
||||
on:error={() => {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { Store } from "../../Logic/UIEventSource.js"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource.js"
|
||||
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import DeletableImage from "./DeletableImage.svelte"
|
||||
|
||||
export let images: Store<ProvidedImage[]>
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: Store<Record<string, string>>
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
</script>
|
||||
|
||||
<div class="flex w-full space-x-2 overflow-x-auto" style="scroll-snap-type: x proximity">
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import DotMenu from "../Base/DotMenu.svelte"
|
||||
|
||||
export let image: ProvidedImage
|
||||
export let image: Partial<ProvidedImage> & ({ id: string, url: string })
|
||||
export let clss: string = undefined
|
||||
|
||||
let isLoaded = new UIEventSource(false)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import Zoomcontrol from "../Zoomcontrol"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
export let image: ProvidedImage
|
||||
export let image: Partial<ProvidedImage>
|
||||
let panzoomInstance = undefined
|
||||
let panzoomEl: HTMLElement
|
||||
export let isLoaded: UIEventSource<boolean> = undefined
|
||||
|
|
|
@ -143,6 +143,18 @@
|
|||
highlighted.set(feature.properties.id)
|
||||
},
|
||||
})
|
||||
onDestroy(
|
||||
tags.addCallbackAndRunD((tags) => {
|
||||
if (
|
||||
tags.id.startsWith("node/") ||
|
||||
tags.id.startsWith("way/") ||
|
||||
tags.id.startsWith("relation/")
|
||||
) {
|
||||
return
|
||||
}
|
||||
linkable = false
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col">
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
import FileSelector from "../Base/FileSelector.svelte"
|
||||
import LoginButton from "../Base/LoginButton.svelte"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Camera from "@babeard/svelte-heroicons/solid/Camera"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import NoteCommentElement from "../Popup/Notes/NoteCommentElement"
|
||||
import type { Feature } from "geojson"
|
||||
import Camera from "@babeard/svelte-heroicons/mini/Camera"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
|
@ -24,7 +24,7 @@
|
|||
export let targetKey: string = undefined
|
||||
export let layer: LayerConfig
|
||||
export let noBlur: boolean = false
|
||||
export let feature: Feature = undefined
|
||||
export let feature: Feature
|
||||
/**
|
||||
* Image to show in the button
|
||||
* NOT the image to upload!
|
||||
|
@ -65,13 +65,13 @@
|
|||
}
|
||||
const url = uploadResult.absoluteUrl
|
||||
await state.osmConnection.addCommentToNote(tags.data.id, url)
|
||||
NoteCommentElement.addCommentTo(url, <UIEventSource<any>>tags, {
|
||||
NoteCommentElement.addCommentTo(url, <UIEventSource<OsmTags>>tags, {
|
||||
osmConnection: state.osmConnection,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await state?.imageUploadManager?.uploadImageAndApply(file, tags, targetKey, noBlur)
|
||||
await state?.imageUploadManager?.uploadImageAndApply(file, tags, targetKey, noBlur, feature)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
state.reportError(e, "Could not upload image")
|
||||
|
@ -133,9 +133,9 @@
|
|||
cls="flex justify-center md:hidden button"
|
||||
multiple={true}
|
||||
on:submit={(e) => {
|
||||
return handleFiles(e.detail, true)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return handleFiles(e.detail, true)
|
||||
}}
|
||||
>
|
||||
<Tr t={t.selectFile} />
|
||||
|
|
|
@ -41,14 +41,14 @@
|
|||
zoom,
|
||||
location: new UIEventSource<{ lon: number; lat: number }>({ lat: lat.data, lon: lon.data }),
|
||||
})
|
||||
maplibremap.location.stabilized(500).addCallbackAndRunD(l => {
|
||||
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 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], {
|
||||
|
@ -62,19 +62,19 @@
|
|||
})
|
||||
|
||||
let osmConnection = new OsmConnection()
|
||||
let inspectedContributors: UIEventSource<{
|
||||
name: string,
|
||||
visitedTime: string,
|
||||
label: string
|
||||
}[]> = UIEventSource.asObject(
|
||||
osmConnection.getPreference("spied-upon-users"), [])
|
||||
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)
|
||||
const previousEntry = inspectedData.find((e) => e.name === user)
|
||||
if (previousEntry) {
|
||||
previousEntry.visitedTime = new Date().toISOString()
|
||||
} else {
|
||||
|
@ -89,7 +89,11 @@
|
|||
|
||||
step.setData("loading")
|
||||
featuresStore.set([])
|
||||
const overpass = new Overpass(undefined, user.split(";").map(user => "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
|
||||
}
|
||||
|
@ -117,11 +121,10 @@
|
|||
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">
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<div class="low-interaction flex items-center gap-x-2 p-2">
|
||||
<MagnifyingGlassCircle class="h-12 w-12" />
|
||||
<h1 class="m-0 mx-2 flex-shrink-0">
|
||||
<Tr t={t.title} />
|
||||
</h1>
|
||||
<ValidatedInput type="string" value={username} on:submit={() => load()} />
|
||||
|
@ -141,16 +144,16 @@
|
|||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<button class:primary={mode === "map"} on:click={() => mode = "map"}>
|
||||
<button class:primary={mode === "map"} on:click={() => (mode = "map")}>
|
||||
<Tr t={t.mapView} />
|
||||
</button>
|
||||
<button class:primary={mode === "table"} on:click={() => mode = "table"}>
|
||||
<button class:primary={mode === "table"} on:click={() => (mode = "table")}>
|
||||
<Tr t={t.tableView} />
|
||||
</button>
|
||||
<button class:primary={mode === "aggregate"} on:click={() => mode = "aggregate"}>
|
||||
<button class:primary={mode === "aggregate"} on:click={() => (mode = "aggregate")}>
|
||||
<Tr t={t.aggregateView} />
|
||||
</button>
|
||||
<button class:primary={mode === "images"} on:click={() => mode = "images"}>
|
||||
<button class:primary={mode === "images"} on:click={() => (mode = "images")}>
|
||||
<Tr t={t.images} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -167,32 +170,35 @@
|
|||
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,
|
||||
}}
|
||||
x: 640,
|
||||
duration: 0,
|
||||
easing: linear,
|
||||
}}
|
||||
divClass="overflow-y-auto z-50 bg-white"
|
||||
hidden={$selectedElement === undefined}
|
||||
on:close={() => {
|
||||
selectedElement.setData(undefined)
|
||||
}}
|
||||
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)} />
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href={"https://osm.org/" + $selectedElement.properties.id}
|
||||
>
|
||||
{$selectedElement.properties.id}
|
||||
</a>
|
||||
<XCircleIcon class="h-6 w-6" on:click={() => selectedElement.set(undefined)} />
|
||||
</div>
|
||||
|
||||
<History onlyShowChangesBy={$username} id={$selectedElement.properties.id}></History>
|
||||
<History onlyShowChangesBy={$username} id={$selectedElement.properties.id} />
|
||||
</TitledPanel>
|
||||
</Drawer>
|
||||
{/if}
|
||||
|
||||
<div class="flex-grow overflow-hidden m-1 rounded-xl">
|
||||
<MaplibreMap map={map} mapProperties={maplibremap} autorecovery={true} />
|
||||
<div class="m-1 flex-grow overflow-hidden rounded-xl">
|
||||
<MaplibreMap {map} mapProperties={maplibremap} autorecovery={true} />
|
||||
</div>
|
||||
{:else if mode === "table"}
|
||||
<div class="m-2 h-full overflow-y-auto">
|
||||
|
@ -213,7 +219,13 @@
|
|||
|
||||
<Page shown={showPreviouslyVisited}>
|
||||
<div slot="header">Earlier inspected constributors</div>
|
||||
<PreviouslySpiedUsers {osmConnection} {inspectedContributors} on:selectUser={(e) => {
|
||||
username.set(e.detail); load();showPreviouslyVisited.set(false)
|
||||
}} />
|
||||
<PreviouslySpiedUsers
|
||||
{osmConnection}
|
||||
{inspectedContributors}
|
||||
on:selectUser={(e) => {
|
||||
username.set(e.detail)
|
||||
load()
|
||||
showPreviouslyVisited.set(false)
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import Maproulette from "../../Logic/Maproulette"
|
||||
import Maproulette, { maprouletteStatus } from "../../Logic/Maproulette"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
|
||||
/**
|
||||
|
@ -38,11 +38,11 @@
|
|||
async function apply() {
|
||||
const maproulette_id = tags.data[maproulette_id_key] ?? tags.data.mr_taskId ?? tags.data.id
|
||||
try {
|
||||
await Maproulette.singleton.closeTask(Number(maproulette_id), Number(statusToSet), {
|
||||
tags: `MapComplete MapComplete:${state.theme.id}`,
|
||||
const statusIndex = Maproulette.codeToIndex(statusToSet) ?? Number(statusToSet)
|
||||
await Maproulette.singleton.closeTask(Number(maproulette_id), statusIndex, state, {
|
||||
comment: feedback,
|
||||
})
|
||||
tags.data["mr_taskStatus"] = Maproulette.STATUS_MEANING[Number(statusToSet)]
|
||||
tags.data["mr_taskStatus"] = maprouletteStatus[statusIndex]
|
||||
tags.data.status = statusToSet
|
||||
tags.ping()
|
||||
} catch (e) {
|
||||
|
|
|
@ -951,13 +951,13 @@ export class ToTextualDescription {
|
|||
* const oh = new opening_hours("mon 12:00-16:00")
|
||||
* const ranges = OH.createRangesForApplicableWeek(oh)
|
||||
* const tr = ToTextualDescription.createTextualDescriptionFor(oh, ranges.ranges)
|
||||
* tr.textFor("en") // => "On monday from 12:00 till 16:00"
|
||||
* tr.textFor("en") // => "On Monday from 12:00 till 16:00"
|
||||
* tr.textFor("nl") // => "Op maandag van 12:00 tot 16:00"
|
||||
*
|
||||
* const oh = new opening_hours("mon 12:00-16:00; tu 13:00-14:00")
|
||||
* const ranges = OH.createRangesForApplicableWeek(oh)
|
||||
* const tr = ToTextualDescription.createTextualDescriptionFor(oh, ranges.ranges)
|
||||
* tr.textFor("en") // => "On monday from 12:00 till 16:00. On tuesday from 13:00 till 14:00"
|
||||
* tr.textFor("en") // => "On Monday from 12:00 till 16:00. On Tuesday from 13:00 till 14:00"
|
||||
* tr.textFor("nl") // => "Op maandag van 12:00 tot 16:00. Op dinsdag van 13:00 tot 14:00"
|
||||
*/
|
||||
public static createTextualDescriptionFor(
|
||||
|
|
|
@ -3,15 +3,17 @@
|
|||
import { Stores } from "../../Logic/UIEventSource"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
|
||||
/**
|
||||
* Shows _all_ disabled questions
|
||||
*/
|
||||
export let state
|
||||
let layers = state.layout.layers.filter((l) => l.isNormal())
|
||||
export let state: ThemeViewState
|
||||
let layers = state.theme.layers.filter((l) => l.isNormal())
|
||||
|
||||
let allDisabled = Stores.concat<string>(
|
||||
layers.map((l) => state.userRelatedState.getThemeDisabled(state.layout.id, l.id))
|
||||
layers.map((l) => state.userRelatedState.getThemeDisabled(state.theme.id, l.id))
|
||||
).map((l) => [].concat(...l))
|
||||
const t = Translations.t.general.questions
|
||||
</script>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
/**
|
||||
* Gives an overview of questions which are disabled for the given theme
|
||||
*/
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
@ -13,7 +12,7 @@
|
|||
export let layer: LayerConfig
|
||||
export let state: ThemeViewState
|
||||
|
||||
let disabledQuestions = state.userRelatedState.getThemeDisabled(state.layout.id, layer.id)
|
||||
let disabledQuestions = state.userRelatedState.getThemeDisabled(state.theme.id, layer.id)
|
||||
|
||||
function getQuestion(id: string): Translation {
|
||||
return layer.tagRenderings.find((q) => q.id === id).question.Subs({})
|
||||
|
|
|
@ -9,6 +9,7 @@ import { PointImportFlowArguments, PointImportFlowState } from "./PointImportFlo
|
|||
import { Utils } from "../../../Utils"
|
||||
import { ImportFlowUtils } from "./ImportFlow"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { GeoOperations } from "../../../Logic/GeoOperations"
|
||||
|
||||
/**
|
||||
* The wrapper to make the special visualisation for the PointImportFlow
|
||||
|
@ -44,6 +45,10 @@ export class PointImportButtonViz implements SpecialVisualization {
|
|||
name: "maproulette_id",
|
||||
doc: "The property name of the maproulette_id - this is probably `mr_taskId`. If given, the maproulette challenge will be marked as fixed. Only use this if part of a maproulette-layer.",
|
||||
},
|
||||
{
|
||||
name: "to_point",
|
||||
doc: "If set, a feature will be converted to a centerpoint",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -53,8 +58,14 @@ export class PointImportButtonViz implements SpecialVisualization {
|
|||
argument: string[],
|
||||
feature: Feature
|
||||
): BaseUIElement {
|
||||
const to_point_index = this.args.findIndex((arg) => arg.name === "to_point")
|
||||
const summarizePointArg = argument[to_point_index].toLowerCase()
|
||||
if (feature.geometry.type !== "Point") {
|
||||
return Translations.t.general.add.import.wrongType.SetClass("alert")
|
||||
if (summarizePointArg !== "no" && summarizePointArg !== "false") {
|
||||
feature = GeoOperations.centerpoint(feature)
|
||||
} else {
|
||||
return Translations.t.general.add.import.wrongType.SetClass("alert")
|
||||
}
|
||||
}
|
||||
const baseArgs: PointImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, baseArgs)
|
||||
|
|
|
@ -19,7 +19,6 @@ export interface PointImportFlowArguments extends ImportFlowArguments {
|
|||
|
||||
export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
||||
public readonly startCoordinate: [number, number]
|
||||
private readonly _originalFeature: Feature<Point>
|
||||
|
||||
constructor(
|
||||
state: SpecialVisualizationState,
|
||||
|
@ -29,7 +28,6 @@ export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
|||
originalFeatureTags: UIEventSource<Record<string, string>>
|
||||
) {
|
||||
super(state, args, tagsToApply, originalFeatureTags)
|
||||
this._originalFeature = originalFeature
|
||||
this.startCoordinate = GeoOperations.centerpointCoordinates(originalFeature)
|
||||
}
|
||||
|
||||
|
@ -80,7 +78,7 @@ export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
|||
originalFeatureTags.ping()
|
||||
}
|
||||
|
||||
let maproulette_id = originalFeatureTags.data[this.args.maproulette_id]
|
||||
const maproulette_id = originalFeatureTags.data[this.args.maproulette_id]
|
||||
if (maproulette_id !== undefined) {
|
||||
if (this.state.featureSwitchIsTesting.data) {
|
||||
console.log(
|
||||
|
@ -90,7 +88,11 @@ export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
|||
)
|
||||
} else {
|
||||
console.log("Marking maproulette task as fixed")
|
||||
await Maproulette.singleton.closeTask(Number(maproulette_id))
|
||||
await Maproulette.singleton.closeTask(
|
||||
Number(maproulette_id),
|
||||
Maproulette.STATUS_FIXED,
|
||||
this.state
|
||||
)
|
||||
originalFeatureTags.data["mr_taskStatus"] = "Fixed"
|
||||
originalFeatureTags.ping()
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
<LoginToggle ignoreLoading={true} hiddenFail {state}>
|
||||
{#if $isFavourite}
|
||||
<button
|
||||
class="soft no-image-background m-0 h-8 w-8 p-0"
|
||||
|
|
|
@ -159,9 +159,14 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
const maproulette_id = tags.data[maproulette_id_key]
|
||||
const maproulette_feature = state.indexedFeatures.featuresById.data.get(maproulette_id)
|
||||
const maproulette_task_id = Number(maproulette_feature.properties.mr_taskId)
|
||||
await Maproulette.singleton.closeTask(maproulette_task_id, Maproulette.STATUS_FIXED, {
|
||||
comment: "Tags are copied onto " + targetId + " with MapComplete",
|
||||
})
|
||||
await Maproulette.singleton.closeTask(
|
||||
maproulette_task_id,
|
||||
Maproulette.STATUS_FIXED,
|
||||
state,
|
||||
{
|
||||
comment: "Tags are copied onto " + targetId + " with MapComplete",
|
||||
}
|
||||
)
|
||||
maproulette_feature.properties["mr_taskStatus"] = "Fixed"
|
||||
state.featureProperties.getStore(maproulette_id).ping()
|
||||
}
|
||||
|
|
|
@ -85,6 +85,8 @@
|
|||
}
|
||||
let answerId = "answer-" + Utils.randomString(5)
|
||||
let debug = state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false)
|
||||
|
||||
let apiState: Store<string> = state?.osmConnection?.apiIsOnline ?? new ImmutableStore("online")
|
||||
</script>
|
||||
|
||||
<div bind:this={htmlElem} class={twMerge(clss, "tr-" + config.id)}>
|
||||
|
@ -126,7 +128,7 @@
|
|||
{layer}
|
||||
extraClasses="my-2"
|
||||
/>
|
||||
{#if !editingEnabled || $editingEnabled}
|
||||
{#if (!editingEnabled || $editingEnabled) && $apiState !== "readonly" && $apiState !== "offline"}
|
||||
<EditButton
|
||||
arialabel={config.editButtonAriaLabel}
|
||||
ariaLabelledBy={answerId}
|
||||
|
|
|
@ -355,9 +355,11 @@
|
|||
disabledInTheme.set(newList)
|
||||
menuIsOpened.set(false)
|
||||
}
|
||||
|
||||
let apiState = state.osmConnection.apiIsOnline
|
||||
</script>
|
||||
|
||||
{#if question !== undefined}
|
||||
{#if question !== undefined && $apiState !== "readonly" && $apiState !== "offline"}
|
||||
<div class={clss}>
|
||||
{#if layer.isNormal()}
|
||||
<LoginToggle {state}>
|
||||
|
|
|
@ -12,7 +12,6 @@ import { ExportableMap, MapProperties } from "../Models/MapProperties"
|
|||
import LayerState from "../Logic/State/LayerState"
|
||||
import { Feature, Geometry, Point, Polygon } from "geojson"
|
||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
|
@ -22,14 +21,12 @@ import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
|||
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
||||
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||
import { SummaryTileSourceRewriter } from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource"
|
||||
import ThemeSource from "../Logic/FeatureSource/Sources/ThemeSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import ShowDataLayer from "./Map/ShowDataLayer"
|
||||
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
|
||||
import SearchState from "../Logic/State/SearchState"
|
||||
import UserRelatedState, { OptionallySyncedHistory } from "../Logic/State/UserRelatedState"
|
||||
import GeocodeResult from "./Search/GeocodeResult.svelte"
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
|
||||
|
||||
/**
|
||||
|
|
|
@ -112,7 +112,7 @@ class NearbyImageVis implements SpecialVisualization {
|
|||
{
|
||||
name: "readonly",
|
||||
required: false,
|
||||
doc: "If 'readonly', will not show the 'link'-button",
|
||||
doc: "If 'readonly' or 'yes', will not show the 'link'-button",
|
||||
},
|
||||
]
|
||||
docs =
|
||||
|
@ -128,7 +128,7 @@ class NearbyImageVis implements SpecialVisualization {
|
|||
layer: LayerConfig
|
||||
): SvelteUIElement {
|
||||
const isOpen = args[0] === "open"
|
||||
const readonly = args[1] === "readonly"
|
||||
const readonly = args[1] === "readonly" || args[1] === "yes"
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||
return new SvelteUIElement(isOpen ? NearbyImages : NearbyImagesCollapsed, {
|
||||
tags,
|
||||
|
@ -744,13 +744,14 @@ export default class SpecialVisualizations {
|
|||
required: false,
|
||||
},
|
||||
],
|
||||
constr: (state, tags, args) => {
|
||||
constr: (state, tags, args, feature) => {
|
||||
const targetKey = args[0] === "" ? undefined : args[0]
|
||||
const noBlur = args[3]?.toLowerCase()?.trim()
|
||||
return new SvelteUIElement(UploadImage, {
|
||||
state,
|
||||
tags,
|
||||
targetKey,
|
||||
feature,
|
||||
labelText: args[1],
|
||||
image: args[2],
|
||||
noBlur: noBlur === "true" || noBlur === "yes",
|
||||
|
@ -1093,7 +1094,7 @@ export default class SpecialVisualizations {
|
|||
tags
|
||||
.map((tags) => tags[args[0]])
|
||||
.map((commentsStr) => {
|
||||
const comments: any[] = JSON.parse(commentsStr)
|
||||
const comments: { text: string }[] = JSON.parse(commentsStr)
|
||||
const startLoc = Number(args[1] ?? 0)
|
||||
if (!isNaN(startLoc) && startLoc > 0) {
|
||||
comments.splice(0, startLoc)
|
||||
|
@ -1852,69 +1853,80 @@ export default class SpecialVisualizations {
|
|||
const key = argument[0] ?? "website"
|
||||
const useProxy = argument[1] !== "no"
|
||||
const readonly = argument[3] === "readonly"
|
||||
const isClosed = (arguments[4] ?? "yes") === "yes"
|
||||
const isClosed = (argument[4] ?? "yes") === "yes"
|
||||
|
||||
const url = tags
|
||||
.mapD((tags) => {
|
||||
if (!tags._country || !tags[key] || tags[key] === "undefined") {
|
||||
return null
|
||||
}
|
||||
return JSON.stringify({ url: tags[key], country: tags._country })
|
||||
})
|
||||
.mapD((data) => JSON.parse(data))
|
||||
const sourceUrl: Store<string | undefined> = url.mapD((url) => url.url)
|
||||
const countryStore: Store<string | undefined> = tags.mapD(
|
||||
(tags) => tags._country
|
||||
)
|
||||
const sourceUrl: Store<string | undefined> = tags.mapD((tags) => {
|
||||
if (!tags[key] || tags[key] === "undefined") {
|
||||
return null
|
||||
}
|
||||
return tags[key]
|
||||
})
|
||||
const externalData: Store<{ success: GeoJsonProperties } | { error: any }> =
|
||||
url.bindD(({ url, country }) => {
|
||||
if (url.startsWith("https://data.velopark.be/")) {
|
||||
sourceUrl.bindD(
|
||||
(url) => {
|
||||
const country = countryStore.data
|
||||
if (url.startsWith("https://data.velopark.be/")) {
|
||||
return Stores.FromPromiseWithErr(
|
||||
(async () => {
|
||||
try {
|
||||
const loadAll =
|
||||
layer.id.toLowerCase().indexOf("maproulette") >=
|
||||
0 // Dirty hack
|
||||
const features =
|
||||
await LinkedDataLoader.fetchVeloparkEntry(
|
||||
url,
|
||||
loadAll
|
||||
)
|
||||
const feature =
|
||||
features.find(
|
||||
(f) => f.properties["ref:velopark"] === url
|
||||
) ?? features[0]
|
||||
const properties = feature.properties
|
||||
properties["ref:velopark"] = url
|
||||
console.log(
|
||||
"Got properties from velopark:",
|
||||
properties
|
||||
)
|
||||
return properties
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
})()
|
||||
)
|
||||
}
|
||||
if (country === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return Stores.FromPromiseWithErr(
|
||||
(async () => {
|
||||
try {
|
||||
const loadAll =
|
||||
layer.id.toLowerCase().indexOf("maproulette") >= 0 // Dirty hack
|
||||
const features =
|
||||
await LinkedDataLoader.fetchVeloparkEntry(
|
||||
url,
|
||||
loadAll
|
||||
)
|
||||
const feature =
|
||||
features.find(
|
||||
(f) => f.properties["ref:velopark"] === url
|
||||
) ?? features[0]
|
||||
const properties = feature.properties
|
||||
properties["ref:velopark"] = url
|
||||
console.log("Got properties from velopark:", properties)
|
||||
return properties
|
||||
return await LinkedDataLoader.fetchJsonLd(
|
||||
url,
|
||||
{ country },
|
||||
useProxy ? "proxy" : "fetch-lod"
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
console.log(
|
||||
"Could not get with proxy/download LOD, attempting to download directly. Error for ",
|
||||
url,
|
||||
"is",
|
||||
e
|
||||
)
|
||||
return await LinkedDataLoader.fetchJsonLd(
|
||||
url,
|
||||
{ country },
|
||||
"fetch-raw"
|
||||
)
|
||||
}
|
||||
})()
|
||||
)
|
||||
}
|
||||
return Stores.FromPromiseWithErr(
|
||||
(async () => {
|
||||
try {
|
||||
return await LinkedDataLoader.fetchJsonLd(
|
||||
url,
|
||||
{ country },
|
||||
useProxy ? "proxy" : "fetch-lod"
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"Could not get with proxy/download LOD, attempting to download directly. Error for ",
|
||||
url,
|
||||
"is",
|
||||
e
|
||||
)
|
||||
return await LinkedDataLoader.fetchJsonLd(
|
||||
url,
|
||||
{ country },
|
||||
"fetch-raw"
|
||||
)
|
||||
}
|
||||
})()
|
||||
)
|
||||
})
|
||||
},
|
||||
[countryStore]
|
||||
)
|
||||
|
||||
externalData.addCallbackAndRunD((lod) =>
|
||||
console.log("linked_data_from_website received the following data:", lod)
|
||||
|
@ -1932,7 +1944,7 @@ export default class SpecialVisualizations {
|
|||
collapsed: isClosed,
|
||||
}),
|
||||
undefined,
|
||||
url.map((url) => !!url)
|
||||
sourceUrl.map((url) => !!url)
|
||||
)
|
||||
},
|
||||
},
|
||||
|
@ -1987,13 +1999,7 @@ export default class SpecialVisualizations {
|
|||
funcName: "pending_changes",
|
||||
docs: "A module showing the pending changes, with the option to clear the pending changes",
|
||||
args: [],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
constr(state: SpecialVisualizationState): BaseUIElement {
|
||||
return new SvelteUIElement(PendingChangesIndicator, { state, compact: false })
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import StatusIcon from "./StatusIcon.svelte"
|
||||
import type { MCService } from "./MCService"
|
||||
import ServiceIndicator from "./ServiceIndicator.svelte"
|
||||
|
@ -203,6 +203,30 @@
|
|||
})
|
||||
}
|
||||
|
||||
{
|
||||
const summaryTileServer = Constants.VectorTileServer
|
||||
// "mvt_layer_server": "https://cache.mapcomplete.org/public.{type}_{layer}/{z}/{x}/{y}.pbf",
|
||||
const status = testDownload(
|
||||
Utils.SubstituteKeys(summaryTileServer, {
|
||||
type: "pois",
|
||||
layer: "food",
|
||||
z: 14,
|
||||
x: 8848,
|
||||
y: 5828,
|
||||
})
|
||||
)
|
||||
services.push({
|
||||
name: summaryTileServer,
|
||||
status: status.mapD((s) => {
|
||||
if (s["error"]) {
|
||||
return "offline"
|
||||
}
|
||||
return "online"
|
||||
}),
|
||||
message: new ImmutableStore("See SettingUpPSQL.md to fix"),
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const s = Constants.countryCoderEndpoint
|
||||
const status = testDownload(s + "/0.0.0.json")
|
||||
|
|
|
@ -34,6 +34,9 @@
|
|||
>tags?.GPSLongitude?.value
|
||||
const exifLat = latD + latM / 60 + latS / (3600 * latSDenom)
|
||||
const exifLon = lonD + lonM / 60 + lonS / (3600 * lonSDenom)
|
||||
const directValueLat = tags?.GPSLatitude?.description
|
||||
const directValueLon = tags?.GPSLongitude?.description
|
||||
|
||||
if (
|
||||
typeof exifLat === "number" &&
|
||||
!isNaN(exifLat) &&
|
||||
|
@ -43,11 +46,29 @@
|
|||
) {
|
||||
lat = exifLat
|
||||
lon = exifLon
|
||||
if (tags?.GPSLatitudeRef?.value?.[0] === "S") {
|
||||
lat *= -1
|
||||
}
|
||||
if (tags?.GPSLongitudeRef?.value?.[0] === "W") {
|
||||
lon *= -1
|
||||
}
|
||||
l("Using EXIFLAT + EXIFLON")
|
||||
} else {
|
||||
l("NOT using exifLat and exifLon: invalid value detected")
|
||||
}
|
||||
l("Lat and lon are", lat, lon)
|
||||
l(
|
||||
"ref lat is",
|
||||
tags?.GPSLatitudeRef?.description,
|
||||
JSON.stringify(tags?.GPSLatitudeRef?.value)
|
||||
)
|
||||
l(
|
||||
"ref lon is",
|
||||
tags?.GPSLongitudeRef?.description,
|
||||
JSON.stringify(tags?.GPSLongitudeRef?.value)
|
||||
)
|
||||
|
||||
l("Direct values are", directValueLat, directValueLon, "corrected:", lat, lon)
|
||||
l("Datetime value is", JSON.stringify(tags.DateTime))
|
||||
const [date, time] = tags.DateTime.value[0].split(" ")
|
||||
datetime = new Date(date.replaceAll(":", "-") + "T" + time).toISOString()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
|
@ -42,11 +42,9 @@
|
|||
import DrawerLeft from "./Base/DrawerLeft.svelte"
|
||||
import DrawerRight from "./Base/DrawerRight.svelte"
|
||||
import SearchResults from "./Search/SearchResults.svelte"
|
||||
import { CloseButton } from "flowbite-svelte"
|
||||
import Hash from "../Logic/Web/Hash"
|
||||
import Searchbar from "./Base/Searchbar.svelte"
|
||||
import ChevronRight from "@babeard/svelte-heroicons/mini/ChevronRight"
|
||||
import ChevronLeft from "@babeard/svelte-heroicons/solid/ChevronLeft"
|
||||
import { Drawer } from "flowbite-svelte"
|
||||
import { linear } from "svelte/easing"
|
||||
|
||||
|
@ -167,6 +165,8 @@
|
|||
const animation = mlmap.keyboard?.keydown(e)
|
||||
animation?.cameraAnimation(mlmap)
|
||||
}
|
||||
|
||||
let apiState = state?.osmConnection?.apiIsOnline ?? new ImmutableStore("online")
|
||||
</script>
|
||||
|
||||
<main>
|
||||
|
@ -175,7 +175,7 @@
|
|||
<MaplibreMap map={maplibremap} mapProperties={mapproperties} autorecovery={true} />
|
||||
</div>
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
<LoginToggle ignoreLoading={true} silentFail {state}>
|
||||
{#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $visualFeedback}
|
||||
<!-- Don't use h-full: h-full does _not_ include the area under the URL-bar, which offsets the crosshair a bit -->
|
||||
<div
|
||||
|
@ -201,8 +201,8 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- bottom controls -->
|
||||
<div class="pointer-events-none absolute bottom-0 left-0 mb-4 w-screen">
|
||||
<!-- bottom controls -->
|
||||
<div class="flex w-full items-end justify-between px-4">
|
||||
<div class="flex flex-col">
|
||||
<If condition={featureSwitches.featureSwitchEnableLogin}>
|
||||
|
@ -218,9 +218,10 @@
|
|||
{#if $currentZoom < Constants.minZoomLevelToAddNewPoint}
|
||||
<Tr t={Translations.t.general.add.zoomInFurther} />
|
||||
{:else if state.theme.hasPresets()}
|
||||
✨ <Tr t={Translations.t.general.add.title} />
|
||||
✨
|
||||
<Tr t={Translations.t.general.add.title} />
|
||||
{:else}
|
||||
<Tr t={Translations.t.notes.addAComment} />
|
||||
<Tr t={Translations.t.notes.createNote} />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
@ -419,6 +420,9 @@
|
|||
<If condition={state.featureSwitches.featureSwitchFakeUser}>
|
||||
<div class="alert w-fit">Faking a user (Testmode)</div>
|
||||
</If>
|
||||
{#if $apiState !== "online"}
|
||||
<div class="alert w-fit">API is {$apiState}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col items-center justify-center">
|
||||
|
@ -429,11 +433,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<DrawerLeft shown={state.guistate.pageStates.menu}>
|
||||
<div class="h-screen overflow-y-auto">
|
||||
<MenuDrawer onlyLink={true} {state} />
|
||||
</div>
|
||||
</DrawerLeft>
|
||||
<div class="h-full overflow-hidden">
|
||||
<DrawerLeft shown={state.guistate.pageStates.menu}>
|
||||
<div class="h-screen overflow-y-auto">
|
||||
<MenuDrawer onlyLink={true} {state} />
|
||||
</div>
|
||||
</DrawerLeft>
|
||||
</div>
|
||||
|
||||
{#if $selectedElement !== undefined && $selectedLayer !== undefined && !$selectedLayer.popupInFloatover}
|
||||
<!-- right modal with the selected element view -->
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue