adds trail export

This commit is contained in:
Christian Beutel
2024-05-05 15:46:17 +02:00
parent 930feda4d6
commit 756a9fa79f
18 changed files with 310 additions and 31 deletions

View File

@@ -9,17 +9,22 @@
} from "$lib/stores/list_store";
import { show_toast } from "$lib/stores/toast_store";
import { trails_delete } from "$lib/stores/trail_store";
import { getFileURL } from "$lib/util/file_util";
import { trail2gpx } from "$lib/util/gpx_util";
import { gpx } from "$lib/vendor/toGeoJSON/toGeoJSON";
import { _ } from "svelte-i18n";
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
import ConfirmModal from "../confirm_modal.svelte";
import ListSelectModal from "../list/list_select_modal.svelte";
import { _ } from "svelte-i18n";
import TrailExportModal from "./trail_export_modal.svelte";
import JSZip from "jszip";
import { getFileURL, saveAs } from "$lib/util/file_util";
export let trail: Trail;
export let mode: "overview" | "map";
let openConfirmModal: () => void;
let openListSelectModal: () => void;
let openExportModal: () => void;
const dropdownItems: DropdownItem[] = [
mode == "overview"
@@ -34,7 +39,7 @@
...(trail.gpx
? [
{
text: $_("download-gpx"),
text: $_("export"),
value: "download",
icon: "download",
},
@@ -65,7 +70,7 @@
} else if (item.value == "print") {
goto(`/map/trail/${trail.id}/print`);
} else if (item.value == "download") {
downloadURI(getFileURL(trail, trail.gpx), trail.gpx!);
openExportModal();
} else if (item.value == "edit") {
goto(`/trail/edit/${trail.id}`);
} else if (item.value == "delete") {
@@ -73,13 +78,46 @@
}
}
function downloadURI(uri: string, name: string) {
var link = document.createElement("a");
link.setAttribute("download", name);
link.href = uri;
document.body.appendChild(link);
link.click();
link.remove();
async function exportTrail(exportSettings: {
fileFormat: "gpx" | "json";
photos: boolean;
summitLog: boolean;
}) {
try {
let fileData: string = await trail2gpx(trail);
if (exportSettings.fileFormat == "json") {
fileData = JSON.stringify(gpx(new DOMParser().parseFromString(fileData, "text/xml")));
}
const zip = new JSZip();
zip.file(`${trail.name}.${exportSettings.fileFormat}`, fileData);
if (exportSettings.photos) {
const photoFolder = zip.folder($_("photos"));
for (const photo of trail.photos) {
const photoURL = getFileURL(trail, photo);
const photoBlob = await fetch(photoURL).then((response) =>
response.blob(),
);
const photoData = new File([photoBlob], photo);
photoFolder?.file(photo, photoData, { base64: true });
}
}
if(exportSettings.summitLog) {
let summitLogString = ""
for (const summitLog of trail.expand.summit_logs) {
summitLogString += `${summitLog.date},${summitLog.text}\n`
}
zip.file(`${trail.name} - ${$_("summit-book")}.csv`, summitLogString)
}
const blob = await zip.generateAsync({ type: "blob" });
saveAs(blob, `${trail.name}.zip`);
} catch (e) {
console.error(e);
show_toast({
type: "error",
icon: "close",
text: $_("error-exporting-trail"),
});
}
}
async function deleteTrail() {
@@ -137,3 +175,7 @@
bind:openModal={openListSelectModal}
on:change={(e) => handleListSelection(e.detail)}
></ListSelectModal>
<TrailExportModal
bind:openModal={openExportModal}
on:export={(e) => exportTrail(e.detail)}
></TrailExportModal>

View File

@@ -0,0 +1,80 @@
<script lang="ts">
import Modal from "$lib/components/base/modal.svelte";
import { createEventDispatcher } from "svelte";
import { _ } from "svelte-i18n";
import Select from "../base/select.svelte";
export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
const dispatch = createEventDispatcher();
const fileFormats = [
{ text: "GPX", value: "gpx" },
{ text: "GeoJSON", value: "json" },
];
const exportSettings = {
fileFormat: "gpx",
photos: false,
summitLog: false,
};
function exportTrail() {
dispatch("export", exportSettings);
closeModal!();
}
</script>
<Modal
id="export-modal"
title="Export trail"
size="max-w-sm"
bind:openModal
bind:closeModal
>
<div slot="content">
<div class="mb-3">
<Select
bind:value={exportSettings.fileFormat}
items={fileFormats}
label={$_("file-format")}
></Select>
</div>
<div class="mb-2">
<input
id="include-photos-checkbox"
type="checkbox"
bind:checked={exportSettings.photos}
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
/>
<label for="include-photos-checkbox" class="ms-2 text-sm"
>{$_("photos")}</label
>
</div>
<div class="mb-2">
<input
id="include-summit-log-checkbox"
type="checkbox"
bind:checked={exportSettings.summitLog}
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
/>
<label for="include-summit-log-checkbox" class="ms-2 text-sm"
>{$_("summit-book")}</label
>
</div>
</div>
<div slot="footer" class="flex items-center gap-4">
<button class="btn-secondary" on:click={closeModal}
>{$_("cancel")}</button
>
<button
class="btn-primary"
type="button"
on:click={exportTrail}
name="save">{$_("export")}</button
>
</div></Modal
>

View File

@@ -55,12 +55,15 @@
"entry": "Eintrag",
"error-creating-user": "Fehler beim Erstellen des Nutzers",
"error-during-login": "Fehler beim Login",
"error-exporting-trail": "Fehler beim Exportieren des Trails",
"error-printing-map": "Fehler beim Drucken der Karte",
"error-reading-file": "Fehler beim Lesen der Datei",
"est-duration": "Gesch. Dauer",
"explore": "Erkunden",
"explore-some-trails": "Erkunden Sie einige Routen",
"export": "Exportieren",
"features": "Features",
"file-format": "Dateiformat",
"french": "Französisch",
"german": "Deutsch",
"grid": "Gitter",

View File

@@ -55,12 +55,15 @@
"entry": "Entry",
"error-creating-user": "Error creating user",
"error-during-login": "Error during login",
"error-exporting-trail": "Error exporting trail",
"error-printing-map": "Error printing map",
"error-reading-file": "Error reading file",
"est-duration": "Est. duration",
"explore": "Explore",
"explore-some-trails": "Explore some trails",
"export": "Export",
"features": "Features",
"file-format": "File format",
"french": "French",
"german": "German",
"grid": "Grid",

View File

@@ -55,12 +55,15 @@
"entry": "Entrée",
"error-creating-user": "Erreur durant la création de l'utilisateur",
"error-during-login": "Erreur durant la connexion",
"error-exporting-trail": "",
"error-printing-map": "Erreur d'impression de la carte",
"error-reading-file": "Erreur de lecture du fichier",
"est-duration": "Temps estimé",
"explore": "Explorer",
"explore-some-trails": "Explorer certains itinéraires",
"export": "",
"features": "Fonctionnalités",
"file-format": "",
"french": "Français",
"german": "Allemand",
"grid": "Grille",

View File

@@ -55,12 +55,15 @@
"entry": "Bejegyzés",
"error-creating-user": "Hiba felhasználó hozzáadása közben",
"error-during-login": "Hiba bejelentkezés közben",
"error-exporting-trail": "",
"error-printing-map": "",
"error-reading-file": "Hiba a fájl olvasása közben",
"est-duration": "Becsült időtartam",
"explore": "Felfedezés",
"explore-some-trails": "Fedezzen fel néhány ösvényt",
"export": "",
"features": "Jellemzők",
"file-format": "",
"french": "Francia",
"german": "Német",
"grid": "",

View File

@@ -55,12 +55,15 @@
"entry": "Item",
"error-creating-user": "Het account kan niet worden aangemaakt",
"error-during-login": "Het inloggen is mislukt",
"error-exporting-trail": "",
"error-printing-map": "Fout bij afdrukken van kaart",
"error-reading-file": "Het bestand kan niet worden ingelezen",
"est-duration": "Geschatte duur",
"explore": "Verkennen",
"explore-some-trails": "Verken enkele wandelroutes",
"export": "",
"features": "Kenmerken",
"file-format": "",
"french": "Frans",
"german": "Duits",
"grid": "Rooster",

View File

@@ -55,12 +55,15 @@
"entry": "Pozycja",
"error-creating-user": "Błąd tworzenia użytkownika",
"error-during-login": "Błąd podczas logowania",
"error-exporting-trail": "",
"error-printing-map": "",
"error-reading-file": "Błąd wczytywania pliku",
"est-duration": "Szacowany czas",
"explore": "Eksploruj",
"explore-some-trails": "Eksploruj różne ścieżki",
"export": "",
"features": "Funkcje",
"file-format": "",
"french": "Francuski",
"german": "Niemiecki",
"grid": "",

View File

@@ -55,12 +55,15 @@
"entry": "Entrada",
"error-creating-user": "Erro ao criar usuário",
"error-during-login": "Erro durante o login",
"error-exporting-trail": "",
"error-printing-map": "",
"error-reading-file": "Erro ao ler o arquivo",
"est-duration": "Duração prevista",
"explore": "Explorar",
"explore-some-trails": "Explore algumas trilhas",
"export": "",
"features": "Características",
"file-format": "",
"french": "Francês",
"german": "Alemão",
"grid": "",

View File

@@ -55,12 +55,15 @@
"entry": "日程",
"error-creating-user": "创建用户错误",
"error-during-login": "登录错误",
"error-exporting-trail": "",
"error-printing-map": "",
"error-reading-file": "读取文件错误",
"est-duration": "预计时长",
"explore": "探索",
"explore-some-trails": "探索行程",
"export": "",
"features": "特性",
"file-format": "",
"french": "法语",
"german": "德语",
"grid": "",

View File

@@ -8,7 +8,7 @@ export default class Metadata {
desc: string;
time: Date;
keywords: string;
extensions: string;
extensions?: string;
author?: Person;
link?: Link[];
bounds?: Bounds;
@@ -18,7 +18,7 @@ export default class Metadata {
desc: string,
time: string,
keywords: string,
extensions: string,
extensions?: string,
author?: Person,
link?: Link | Link[],
bounds?: Bounds,

View File

@@ -61,7 +61,7 @@ class Trail {
this.id = params?.id;
this.name = name;
this.location = params?.location;
this.date = params?.date;
this.date = params?.date ?? new Date().toISOString().split('T')[0];
this.public = params?.public ?? false
this.distance = params?.distance;
this.elevation_gain = params?.elevation_gain;

View File

@@ -17,3 +17,14 @@ export function readAsDataURLAsync(file: File) {
fr.readAsDataURL(file);
});
}
export function saveAs(data: Blob, fileName: string) {
var a = document.createElement("a") as HTMLAnchorElement;
a.setAttribute("style", "display: none");
document.body.appendChild(a);
const url = window.URL.createObjectURL(data);
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
};

View File

@@ -1,9 +1,11 @@
import GPX from "$lib/models/gpx/gpx";
import { Trail } from "$lib/models/trail";
import { Waypoint } from "$lib/models/waypoint";
import { currentUser } from "$lib/stores/user_store";
import GeoJsonToGpx from "$lib/vendor/geoJSONToGPX";
import { kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON";
import cryptoRandomString from "crypto-random-string";
import { get } from "svelte/store";
export async function gpx2trail(gpxString: string) {
const gpx = await GPX.parse(gpxString);
@@ -18,7 +20,7 @@ export async function gpx2trail(gpxString: string) {
trail.description = gpx.metadata?.desc;
for (const wpt of gpx.wpt ?? []) {
for (const wpt of gpx.wpt ?? []) {
const wp = new Waypoint(wpt.$.lat ?? 0, wpt.$.lon ?? 0);
wp.id = cryptoRandomString({ length: 15 });
wp.name = wpt.name
@@ -29,13 +31,13 @@ export async function gpx2trail(gpxString: string) {
const totals = gpx.getTotals()
const trackPoints = gpx.trk?.at(0)?.trkseg?.at(0)?.trkpt
const routePoints = gpx.rte?.at(0)?.rtept;
const routePoints = gpx.rte?.at(0)?.rtept;
const startPoint = trackPoints?.at(0) ?? routePoints?.at(0);
const startPoint = trackPoints?.at(0) ?? routePoints?.at(0);
if (startPoint) {
trail.lat = startPoint.$.lat
trail.lon = startPoint.$.lon
}
}
const startTime = trackPoints?.at(0)?.time;
const endTime = trackPoints?.at((trackPoints?.length ?? 1) - 1)?.time
@@ -49,7 +51,44 @@ export async function gpx2trail(gpxString: string) {
trail.elevation_gain = totals.elevationGain;
trail.distance = totals.distance
return {gpx: gpx, trail: trail}
return { gpx: gpx, trail: trail }
}
export async function trail2gpx(trail: Trail) {
if (!trail.expand.gpx_data) {
throw Error("Trail has no GPX data")
}
const gpx = await GPX.parse(trail.expand.gpx_data) as GPX;
if (gpx instanceof Error) {
throw gpx;
}
gpx.metadata = {
name: trail.name,
desc: trail.description ?? "",
time: trail.date ? new Date(trail.date) : new Date(),
keywords: `${trail.category ?? ""}, ${trail.location ?? ""}`,
author: { name: trail.author ?? "", email: get(currentUser)?.email ?? "" }
}
if(!gpx.wpt) {
gpx.wpt = [];
}
for (const wp of trail.expand.waypoints) {
const gpxWpt = gpx.wpt.find((w) => w.$.lat == wp.lat && w.$.lon == wp.lon)
if(!gpxWpt) {
gpx.wpt.push({
$: {
lat: wp.lat,
lon: wp.lon
}
})
}
}
return gpx.toString();
}
export function fromKML(kmlData: string) {