forked from MapComplete/MapComplete
Refactoring: move uploadGPXToOSm into svelte
This commit is contained in:
parent
7b9a748199
commit
34d8527718
9 changed files with 222 additions and 198 deletions
|
@ -23,7 +23,22 @@
|
|||
"cs": "Ujetá cesta"
|
||||
}
|
||||
},
|
||||
"pointRendering": [],
|
||||
"pointRendering": [
|
||||
{
|
||||
"location": [
|
||||
"start"
|
||||
],
|
||||
"marker": [
|
||||
{
|
||||
"icon": "circle",
|
||||
"color": "#bb000077"
|
||||
}
|
||||
],
|
||||
"iconSize": "10,10",
|
||||
"pitchAlignment": "map",
|
||||
"rotationAlignment": "map"
|
||||
}
|
||||
],
|
||||
"lineRendering": [
|
||||
{
|
||||
"width": 3,
|
||||
|
|
|
@ -153,11 +153,7 @@ export default class GeoLocationHandler {
|
|||
const features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
|
||||
this.currentUserLocation = new StaticFeatureSource(features)
|
||||
let i = 0
|
||||
this.geolocationState.currentGPSLocation.addCallbackAndRun((location) => {
|
||||
if (location === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.geolocationState.currentGPSLocation.addCallbackAndRunD((location) => {
|
||||
const properties = {
|
||||
id: "gps-" + i,
|
||||
"user:location": "yes",
|
||||
|
@ -200,7 +196,6 @@ export default class GeoLocationHandler {
|
|||
)
|
||||
})
|
||||
features.ping()
|
||||
let i = 0
|
||||
this.currentUserLocation?.features?.addCallbackAndRunD(([location]: [Feature<Point>]) => {
|
||||
if (location === undefined) {
|
||||
return
|
||||
|
@ -231,7 +226,6 @@ export default class GeoLocationHandler {
|
|||
|
||||
const feature = JSON.parse(JSON.stringify(location))
|
||||
feature.properties.id = "gps/" + features.data.length
|
||||
i++
|
||||
features.data.push(feature)
|
||||
features.ping()
|
||||
})
|
||||
|
|
|
@ -399,11 +399,12 @@ export class OsmConnection {
|
|||
return id
|
||||
}
|
||||
|
||||
public static GpxTrackVisibility = ["private", "public", "trackable", "identifiable"] as const
|
||||
public async uploadGpxTrack(
|
||||
gpx: string,
|
||||
options: {
|
||||
description: string
|
||||
visibility: "private" | "public" | "trackable" | "identifiable"
|
||||
visibility: (typeof OsmConnection.GpxTrackVisibility)[number]
|
||||
filename?: string
|
||||
/**
|
||||
* Some words to give some properties;
|
||||
|
@ -425,11 +426,14 @@ export class OsmConnection {
|
|||
|
||||
const contents = {
|
||||
file: gpx,
|
||||
description: options.description ?? "",
|
||||
description: options.description,
|
||||
tags: options.labels?.join(",") ?? "",
|
||||
visibility: options.visibility,
|
||||
}
|
||||
|
||||
if (!contents.description) {
|
||||
throw "The description of a GPS-trace cannot be the empty string, undefined or null"
|
||||
}
|
||||
const extras = {
|
||||
file:
|
||||
'; filename="' +
|
||||
|
|
|
@ -66,32 +66,4 @@ export class SubtleButton extends UIElement {
|
|||
this.SetClass(classes)
|
||||
return button
|
||||
}
|
||||
|
||||
public OnClickWithLoading(
|
||||
loadingText: BaseUIElement | string,
|
||||
action: () => Promise<void>
|
||||
): BaseUIElement {
|
||||
const state = new UIEventSource<"idle" | "running">("idle")
|
||||
const button = this
|
||||
|
||||
button.onClick(async () => {
|
||||
state.setData("running")
|
||||
try {
|
||||
await action()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
state.setData("idle")
|
||||
}
|
||||
})
|
||||
const loading = new Lazy(() => new Loading(loadingText))
|
||||
return new VariableUiElement(
|
||||
state.map((st) => {
|
||||
if (st === "idle") {
|
||||
return button
|
||||
}
|
||||
return loading
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
181
src/UI/BigComponents/UploadTraceToOsmUI.svelte
Normal file
181
src/UI/BigComponents/UploadTraceToOsmUI.svelte
Normal file
|
@ -0,0 +1,181 @@
|
|||
<script lang="ts">
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Invalid from "../../assets/svg/Invalid.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Confirm from "../../assets/svg/Confirm.svelte"
|
||||
import Upload from "../../assets/svg/Upload.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import Close from "../../assets/svg/Close.svelte"
|
||||
import { placeholder } from "../../Utils/placeholder"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import { selectDefault } from "../../Utils/selectDefault"
|
||||
|
||||
export let trace: (title: string) => string
|
||||
export let state: {
|
||||
layout: LayoutConfig
|
||||
osmConnection: OsmConnection
|
||||
readonly featureSwitchUserbadge: Store<boolean>
|
||||
}
|
||||
export let options: {
|
||||
whenUploaded?: () => void | Promise<void>
|
||||
} = undefined
|
||||
|
||||
|
||||
let t = Translations.t.general.uploadGpx
|
||||
let currentStep = new UIEventSource<"init" | "please_confirm" | "uploading" | "done" | "error">("init")
|
||||
|
||||
let traceVisibilities: {
|
||||
key: "private" | "public"
|
||||
name: Translation
|
||||
docs: Translation
|
||||
}[] = [
|
||||
{
|
||||
key: "private",
|
||||
...t.modes.private,
|
||||
},
|
||||
{
|
||||
key: "public",
|
||||
...t.modes.public,
|
||||
},
|
||||
]
|
||||
|
||||
let gpxServerIsOnline: Store<boolean> = state.osmConnection.gpxServiceIsOnline.map((serviceState) => serviceState === "online")
|
||||
|
||||
|
||||
/**
|
||||
* More or less the same as the coalescing-operator '??', except that it checks for empty strings too
|
||||
*/
|
||||
function createDefault(s: string, defaultValue: string): string {
|
||||
if (defaultValue.length < 1) {
|
||||
throw "Default value should have some characters"
|
||||
}
|
||||
if (s === undefined || s === null || s === "") {
|
||||
return defaultValue
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
let title: string = undefined
|
||||
let description: string = undefined
|
||||
|
||||
let visibility = <UIEventSource<"public" | "private">>state?.osmConnection?.GetPreference("gps.trace.visibility") ?? new UIEventSource<"public" | "private">("private")
|
||||
async function uploadTrace() {
|
||||
try {
|
||||
|
||||
currentStep.setData("uploading")
|
||||
const titleStr = createDefault(
|
||||
title,
|
||||
"Track with mapcomplete",
|
||||
)
|
||||
const descriptionStr = createDefault(
|
||||
description,
|
||||
"Track created with MapComplete with theme " + state?.layout?.id,
|
||||
)
|
||||
await state?.osmConnection?.uploadGpxTrack(trace(titleStr), {
|
||||
visibility: visibility.data ?? "private",
|
||||
description: descriptionStr,
|
||||
filename: titleStr + ".gpx",
|
||||
labels: ["MapComplete", state?.layout?.id],
|
||||
})
|
||||
|
||||
if (options?.whenUploaded !== undefined) {
|
||||
await options.whenUploaded()
|
||||
}
|
||||
currentStep.setData("done")
|
||||
} catch (e) {
|
||||
currentStep.setData("error")
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoginToggle {state}>
|
||||
|
||||
{#if !$gpxServerIsOnline}
|
||||
<div class="flex border alert items-center">
|
||||
<Invalid class="w-8 h-8 m-2" />
|
||||
<Tr t={t.gpxServiceOffline} cls="p-2" />
|
||||
</div>
|
||||
{:else if $currentStep === "error"}
|
||||
<div class="alert flex w-full gap-x-2">
|
||||
<Invalid class="w-8 h-8"/>
|
||||
<Tr t={Translations.t.general.error} />
|
||||
</div>
|
||||
{:else if $currentStep === "init"}
|
||||
<button class="flex w-full m-0" on:click={() => {currentStep.setData("please_confirm")}}>
|
||||
<Upload class="w-12 h-12" />
|
||||
<Tr t={t.title} />
|
||||
</button>
|
||||
{:else if $currentStep === "please_confirm"}
|
||||
<form on:submit|preventDefault={() => uploadTrace()}
|
||||
class="flex flex-col border-interactive interactive px-2 gap-y-1">
|
||||
<h2>
|
||||
<Tr t={t.title} />
|
||||
</h2>
|
||||
<Tr t={t.intro0} />
|
||||
<Tr t={t.intro1} />
|
||||
|
||||
|
||||
<h3>
|
||||
<Tr t={t.meta.title} />
|
||||
</h3>
|
||||
<Tr t={t.meta.intro} />
|
||||
<input type="text" use:ariaLabel={t.meta.titlePlaceholder} use:placeholder={t.meta.titlePlaceholder}
|
||||
bind:value={title} />
|
||||
<Tr t={t.meta.descriptionIntro} />
|
||||
|
||||
<textarea use:ariaLabel={t.meta.descriptionPlaceHolder} use:placeholder={t.meta.descriptionPlaceHolder}
|
||||
bind:value={description} />
|
||||
|
||||
<Tr t={t.choosePermission} />
|
||||
|
||||
{#each traceVisibilities as option}
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="visibility"
|
||||
value={option.key}
|
||||
bind:group={$visibility}
|
||||
use:selectDefault={visibility}
|
||||
/>
|
||||
|
||||
<Tr t={option.name} cls="font-bold" />
|
||||
-
|
||||
<Tr t={option.docs} />
|
||||
</label>
|
||||
{/each}
|
||||
|
||||
|
||||
<div class="flex flex-wrap-reverse justify-between items-stretch">
|
||||
<button class="flex gap-x-2 w-1/2 flex-grow" on:click={() => currentStep.setData("init")}>
|
||||
<Close class="w-8 h-8" />
|
||||
<Tr t={Translations.t.general.cancel} />
|
||||
</button>
|
||||
|
||||
<button class="flex gap-x-2 primary flex-grow" on:click={() => uploadTrace()}>
|
||||
<Upload class="w-8 h-8" />
|
||||
<Tr t={t.confirm} />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
{:else if $currentStep === "uploading"}
|
||||
<Loading>
|
||||
<Tr t={t.uploading} />
|
||||
</Loading>
|
||||
|
||||
|
||||
{:else if $currentStep === "done"}
|
||||
<div class="flex p-2 rounded-xl border-2 subtle-border items-center">
|
||||
<Confirm class="w-12 h-12 mr-2" />
|
||||
<Tr t={t.uploadFinished} />
|
||||
</div>
|
||||
{/if}
|
||||
</LoginToggle>
|
|
@ -1,155 +0,0 @@
|
|||
import Toggle from "../Input/Toggle"
|
||||
import { RadioButton } from "../Input/RadioButton"
|
||||
import { FixedInputElement } from "../Input/FixedInputElement"
|
||||
import Combine from "../Base/Combine"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { TextField } from "../Input/TextField"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Title from "../Base/Title"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import Upload from "../../assets/svg/Upload.svelte"
|
||||
import Close from "../../assets/svg/Close.svelte"
|
||||
import Confirm from "../../assets/svg/Confirm.svelte"
|
||||
import Invalid from "../../assets/svg/Invalid.svelte"
|
||||
|
||||
export default class UploadTraceToOsmUI extends LoginToggle {
|
||||
constructor(
|
||||
trace: (title: string) => string,
|
||||
state: {
|
||||
layout: LayoutConfig
|
||||
osmConnection: OsmConnection
|
||||
readonly featureSwitchUserbadge: Store<boolean>
|
||||
},
|
||||
options?: {
|
||||
whenUploaded?: () => void | Promise<void>
|
||||
}
|
||||
) {
|
||||
const t = Translations.t.general.uploadGpx
|
||||
const uploadFinished = new UIEventSource(false)
|
||||
const traceVisibilities: {
|
||||
key: "private" | "public"
|
||||
name: Translation
|
||||
docs: Translation
|
||||
}[] = [
|
||||
{
|
||||
key: "private",
|
||||
...t.modes.private,
|
||||
},
|
||||
{
|
||||
key: "public",
|
||||
...t.modes.public,
|
||||
},
|
||||
]
|
||||
|
||||
const dropdown = new RadioButton<"private" | "public">(
|
||||
traceVisibilities.map(
|
||||
(tv) =>
|
||||
new FixedInputElement<"private" | "public">(
|
||||
new Combine([
|
||||
Translations.W(tv.name).SetClass("font-bold"),
|
||||
tv.docs,
|
||||
]).SetClass("flex flex-col"),
|
||||
tv.key
|
||||
)
|
||||
),
|
||||
{
|
||||
value: <any>state?.osmConnection?.GetPreference("gps.trace.visibility"),
|
||||
}
|
||||
)
|
||||
const description = new TextField({
|
||||
placeholder: t.meta.descriptionPlaceHolder,
|
||||
})
|
||||
const title = new TextField({
|
||||
placeholder: t.meta.titlePlaceholder,
|
||||
})
|
||||
const clicked = new UIEventSource<boolean>(false)
|
||||
|
||||
const confirmPanel = new Combine([
|
||||
new Title(t.title),
|
||||
t.intro0,
|
||||
t.intro1,
|
||||
|
||||
t.choosePermission,
|
||||
dropdown,
|
||||
new Title(t.meta.title, 4),
|
||||
t.meta.intro,
|
||||
title,
|
||||
t.meta.descriptionIntro,
|
||||
description,
|
||||
new Combine([
|
||||
new SubtleButton(new SvelteUIElement(Close), Translations.t.general.cancel)
|
||||
.onClick(() => {
|
||||
clicked.setData(false)
|
||||
})
|
||||
.SetClass(""),
|
||||
new SubtleButton(new SvelteUIElement(Upload, {}), t.confirm).OnClickWithLoading(
|
||||
t.uploading,
|
||||
async () => {
|
||||
const titleStr = UploadTraceToOsmUI.createDefault(
|
||||
title.GetValue().data,
|
||||
"Track with mapcomplete"
|
||||
)
|
||||
const descriptionStr = UploadTraceToOsmUI.createDefault(
|
||||
description.GetValue().data,
|
||||
"Track created with MapComplete with theme " + state?.layout?.id
|
||||
)
|
||||
await state?.osmConnection?.uploadGpxTrack(trace(title.GetValue().data), {
|
||||
visibility: dropdown.GetValue().data,
|
||||
description: descriptionStr,
|
||||
filename: titleStr + ".gpx",
|
||||
labels: ["MapComplete", state?.layout?.id],
|
||||
})
|
||||
|
||||
if (options?.whenUploaded !== undefined) {
|
||||
await options.whenUploaded()
|
||||
}
|
||||
uploadFinished.setData(true)
|
||||
}
|
||||
),
|
||||
]).SetClass("flex flex-wrap flex-wrap-reverse justify-between items-stretch"),
|
||||
]).SetClass("flex flex-col p-4 rounded border-2 m-2 border-subtle")
|
||||
|
||||
super(
|
||||
new Toggle(
|
||||
new Toggle(
|
||||
new Combine([
|
||||
new SvelteUIElement(Confirm).SetClass("w-12 h-12 mr-2"),
|
||||
t.uploadFinished,
|
||||
]).SetClass("flex p-2 rounded-xl border-2 subtle-border items-center"),
|
||||
new Toggle(
|
||||
confirmPanel,
|
||||
new SubtleButton(new SvelteUIElement(Upload), t.title)
|
||||
.onClick(() => clicked.setData(true))
|
||||
.SetClass("w-full"),
|
||||
clicked
|
||||
),
|
||||
uploadFinished
|
||||
),
|
||||
new Combine([
|
||||
new SvelteUIElement(Invalid).SetClass("w-8 h-8 m-2"),
|
||||
t.gpxServiceOffline.SetClass("p-2"),
|
||||
]).SetClass("flex border alert items-center"),
|
||||
state.osmConnection.gpxServiceIsOnline.map(
|
||||
(serviceState) => serviceState === "online"
|
||||
)
|
||||
),
|
||||
undefined,
|
||||
state
|
||||
)
|
||||
}
|
||||
|
||||
private static createDefault(s: string, defaultValue: string) {
|
||||
if (defaultValue.length < 1) {
|
||||
throw "Default value should have some characters"
|
||||
}
|
||||
if (s === undefined || s === null || s === "") {
|
||||
return defaultValue
|
||||
}
|
||||
return s
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import Constants from "../../Models/Constants"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI.svelte"
|
||||
|
||||
/**
|
||||
* Wrapper around 'UploadTraceToOsmUI'
|
||||
|
@ -20,10 +21,9 @@ export class UploadToOsmViz implements SpecialVisualization {
|
|||
__: string[]
|
||||
) {
|
||||
const locations = state.historicalUserLocations.features.data
|
||||
return new UploadTraceToOsmUI((title) => GeoOperations.toGpx(locations, title), state, {
|
||||
whenUploaded: async () => {
|
||||
state.historicalUserLocations.features.setData([])
|
||||
},
|
||||
return new SvelteUIElement(UploadTraceToOsmUI, {
|
||||
state,
|
||||
trace: (title: string) => GeoOperations.toGpx(locations, title)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,6 +96,9 @@
|
|||
if (element.properties.id.startsWith("current_view")) {
|
||||
return currentViewLayer
|
||||
}
|
||||
if(element.properties.id === "location_track"){
|
||||
return layout.layers.find(l => l.id === "gps_track")
|
||||
}
|
||||
return state.layout.getMatchingLayer(element.properties)
|
||||
},
|
||||
)
|
||||
|
|
10
src/Utils/selectDefault.ts
Normal file
10
src/Utils/selectDefault.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Store } from "../Logic/UIEventSource"
|
||||
|
||||
export function selectDefault(htmlElement: HTMLInputElement, value: Store<string>) {
|
||||
if (!document.body.contains(htmlElement) || value?.data === undefined) {
|
||||
return
|
||||
}
|
||||
if (value.data === htmlElement.value) {
|
||||
htmlElement.checked = true
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue