Merge master

This commit is contained in:
Pieter Vander Vennet 2024-12-15 23:00:20 +01:00
commit 80aa551c4a
9 changed files with 285 additions and 232 deletions

View file

@ -41,19 +41,49 @@ export default class UserDetails {
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
interface CapabilityResult {
"version": "0.6" | string,
"generator": "OpenStreetMap server" | string,
"copyright": "OpenStreetMap and contributors" | string,
"attribution": "http://www.openstreetmap.org/copyright" | string,
"license": "http://opendatacommons.org/licenses/odbl/1-0/" | string,
"api": {
"version": { "minimum": "0.6", "maximum": "0.6" },
"area": { "maximum": 0.25 | number },
"note_area": { "maximum": 25 | number },
"tracepoints": { "per_page": 5000 | number },
"waynodes": { "maximum": 2000 | number },
"relationmembers": { "maximum": 32000 | number },
"changesets": { "maximum_elements": 10000 | number,
"default_query_limit": 100 | number,
"maximum_query_limit": 100 |number},
"notes": { "default_query_limit": 100 | number, "maximum_query_limit": 10000 |number},
"timeout": { "seconds": 300 |number},
"status": {
"database": OsmServiceState,
"api": OsmServiceState,
"gpx": OsmServiceState }
},
"policy": {
"imagery": {
"blacklist":{regex: string}[]
}
}
}
export class OsmConnection {
public auth: osmAuth
public userDetails: UIEventSource<UserDetails>
public isLoggedIn: Store<boolean>
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
"unknown"
"unknown",
)
public apiIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
"unknown"
"unknown",
)
public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">(
"not-attempted"
"not-attempted",
)
public preferencesHandler: OsmPreferences
public readonly _oauth_config: AuthConfig
@ -97,7 +127,7 @@ export class OsmConnection {
this.userDetails = new UIEventSource<UserDetails>(
new UserDetails(this._oauth_config.url),
"userDetails"
"userDetails",
)
if (options.fakeUser) {
const ud = this.userDetails.data
@ -118,7 +148,7 @@ export class OsmConnection {
(user) =>
user.loggedIn &&
(this.apiIsOnline.data === "unknown" || this.apiIsOnline.data === "online"),
[this.apiIsOnline]
[this.apiIsOnline],
)
this.isLoggedIn.addCallback((isLoggedIn) => {
if (this.userDetails.data.loggedIn == false && isLoggedIn == true) {
@ -161,7 +191,7 @@ export class OsmConnection {
defaultValue: string = undefined,
options?: {
prefix?: string
}
},
): UIEventSource<T | undefined> {
const prefix = options?.prefix ?? "mapcomplete-"
return <UIEventSource<T>>this.preferencesHandler.getPreference(key, defaultValue, prefix)
@ -170,7 +200,7 @@ export class OsmConnection {
public getPreference<T extends string = string>(
key: string,
defaultValue: string = undefined,
prefix: string = "mapcomplete-"
prefix: string = "mapcomplete-",
): UIEventSource<T | undefined> {
return <UIEventSource<T>>this.preferencesHandler.getPreference(key, defaultValue, prefix)
}
@ -214,7 +244,7 @@ export class OsmConnection {
this.updateAuthObject()
LocalStorageSource.get("location_before_login").setData(
Utils.runningFromConsole ? undefined : window.location.href
Utils.runningFromConsole ? undefined : window.location.href,
)
this.auth.xhr(
{
@ -252,13 +282,13 @@ export class OsmConnection {
data.account_created = userInfo.getAttribute("account_created")
data.uid = Number(userInfo.getAttribute("id"))
data.languages = Array.from(
userInfo.getElementsByTagName("languages")[0].getElementsByTagName("lang")
userInfo.getElementsByTagName("languages")[0].getElementsByTagName("lang"),
).map((l) => l.textContent)
data.csCount = Number.parseInt(
userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? "0"
userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? "0",
)
data.tracesCount = Number.parseInt(
userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? "0"
userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? "0",
)
data.img = undefined
@ -290,7 +320,7 @@ export class OsmConnection {
action(this.userDetails.data)
}
this._onLoggedIn = []
}
},
)
}
@ -308,7 +338,7 @@ export class OsmConnection {
method: "GET" | "POST" | "PUT" | "DELETE",
header?: Record<string, string>,
content?: string,
allowAnonymous: boolean = false
allowAnonymous: boolean = false,
): Promise<string> {
const connection: osmAuth = this.auth
if (allowAnonymous && !this.auth.authenticated()) {
@ -316,7 +346,7 @@ export class OsmConnection {
`${this.Backend()}/api/0.6/${path}`,
header,
method,
content
content,
)
if (possibleResult["content"]) {
return possibleResult["content"]
@ -339,7 +369,7 @@ export class OsmConnection {
} else {
ok(response)
}
}
},
)
})
}
@ -348,7 +378,7 @@ export class OsmConnection {
path: string,
content?: string,
header?: Record<string, string>,
allowAnonymous: boolean = false
allowAnonymous: boolean = false,
): Promise<T> {
return <T>await this.interact(path, "POST", header, content, allowAnonymous)
}
@ -356,7 +386,7 @@ export class OsmConnection {
public async put<T extends string>(
path: string,
content?: string,
header?: Record<string, string>
header?: Record<string, string>,
): Promise<T> {
return <T>await this.interact(path, "PUT", header, content)
}
@ -364,7 +394,7 @@ export class OsmConnection {
public async get(
path: string,
header?: Record<string, string>,
allowAnonymous: boolean = false
allowAnonymous: boolean = false,
): Promise<string> {
return await this.interact(path, "GET", header, undefined, allowAnonymous)
}
@ -403,7 +433,7 @@ export class OsmConnection {
return new Promise<{ id: number }>((ok) => {
window.setTimeout(
() => ok({ id: Math.floor(Math.random() * 1000) }),
Math.random() * 5000
Math.random() * 5000,
)
})
}
@ -415,7 +445,7 @@ export class OsmConnection {
{
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
true
true,
)
const parsed = JSON.parse(response)
console.log("Got result:", parsed)
@ -444,14 +474,14 @@ export class OsmConnection {
* Note: these are called 'tags' on the wiki, but I opted to name them 'labels' instead as they aren't "key=value" tags, but just words.
*/
labels: string[]
}
},
): Promise<{ id: number }> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually uploading GPX ", gpx)
return new Promise<{ id: number }>((ok) => {
window.setTimeout(
() => ok({ id: Math.floor(Math.random() * 1000) }),
Math.random() * 5000
Math.random() * 5000,
)
})
}
@ -470,9 +500,9 @@ export class OsmConnection {
file:
"; filename=\"" +
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
"\"\r\nContent-Type: application/gpx+xml"
'"\r\nContent-Type: application/gpx+xml',
}
user
const boundary = "987654"
let body = ""
@ -518,7 +548,7 @@ export class OsmConnection {
} else {
ok()
}
}
},
)
})
}
@ -607,20 +637,22 @@ export class OsmConnection {
return parsed
}
private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState }> {
private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState; database: OsmServiceState }> {
if (Utils.runningFromConsole) {
return { api: "online", gpx: "online" }
return { api: "online", gpx: "online" , database: "online"}
}
const result = await Utils.downloadAdvanced(this.Backend() + "/api/0.6/capabilities")
if (result["content"] === undefined) {
try{
const result = await Utils.downloadJson<CapabilityResult>(this.Backend() + "/api/0.6/capabilities.json")
if (result?.api?.status === undefined) {
console.log("Something went wrong:", result)
return { api: "unreachable", gpx: "unreachable" }
return { api: "unreachable", gpx: "unreachable" , database: "unreachable"}
}
return result.api.status
}catch (e) {
console.error("Could not fetch capabilities")
return { api: "offline", gpx: "offline" , database: "online"}
}
const xmlRaw = result["content"]
const parsed = new DOMParser().parseFromString(xmlRaw, "text/xml")
const statusEl = parsed.getElementsByTagName("status")[0]
const api = <OsmServiceState>statusEl.getAttribute("api")
const gpx = <OsmServiceState>statusEl.getAttribute("gpx")
return { api, gpx }
}
}

View file

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

View file

@ -124,7 +124,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} />
@ -144,7 +144,7 @@
</LoginToggle>
</Page>
<LoginToggle {state}>
<LoginToggle {state} silentFail>
<Page {onlyLink} shown={pg.favourites}>
<svelte:fragment slot="header">
<HeartIcon />

View file

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

View file

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

View file

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

View file

@ -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"
@ -105,11 +105,11 @@
let canZoomIn = mapproperties.maxzoom.map(
(mz) => mapproperties.zoom.data < mz,
[mapproperties.zoom]
[mapproperties.zoom],
)
let canZoomOut = mapproperties.minzoom.map(
(mz) => mapproperties.zoom.data > mz,
[mapproperties.zoom]
[mapproperties.zoom],
)
let rasterLayerName =
@ -118,7 +118,7 @@
onDestroy(
rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name
})
}),
)
debug.addCallbackAndRun((dbg) => {
@ -165,6 +165,8 @@
const animation = mlmap.keyboard?.keydown(e)
animation?.cameraAnimation(mlmap)
}
let apiState = state?.osmConnection?.apiIsOnline ?? new ImmutableStore("online")
</script>
<main>
@ -173,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
@ -216,7 +218,8 @@
{#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.createNote} />
{/if}
@ -417,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">
@ -427,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 -->