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…
	
	Add table
		Add a link
		
	
		Reference in a new issue