adds photo upload for summit logs
This commit is contained in:
64
db/migrations/1732985146_updated_summit_logs.go
Normal file
64
db/migrations/1732985146_updated_summit_logs.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("dd2l9a4vxpy2ni8")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add
|
||||||
|
new_photos := &schema.SchemaField{}
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"system": false,
|
||||||
|
"id": "ixnksbkt",
|
||||||
|
"name": "photos",
|
||||||
|
"type": "file",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"mimeTypes": [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/vnd.mozilla.apng",
|
||||||
|
"image/webp",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/heic"
|
||||||
|
],
|
||||||
|
"thumbs": [],
|
||||||
|
"maxSelect": 99,
|
||||||
|
"maxSize": 5242880,
|
||||||
|
"protected": false
|
||||||
|
}
|
||||||
|
}`), new_photos); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Schema.AddField(new_photos)
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
}, func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("dd2l9a4vxpy2ni8")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove
|
||||||
|
collection.Schema.RemoveField("ixnksbkt")
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -17,8 +17,6 @@
|
|||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
"@types/leaflet": "^1.9.8",
|
|
||||||
"@types/leaflet-gpx": "^1.3.7",
|
|
||||||
"@types/node": "^20.11.25",
|
"@types/node": "^20.11.25",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.33",
|
||||||
@@ -39,7 +37,6 @@
|
|||||||
"@turf/destination": "^7.1.0",
|
"@turf/destination": "^7.1.0",
|
||||||
"@turf/distance": "^7.1.0",
|
"@turf/distance": "^7.1.0",
|
||||||
"@types/chart.js": "^2.9.41",
|
"@types/chart.js": "^2.9.41",
|
||||||
"@types/leaflet.awesome-markers": "^2.0.28",
|
|
||||||
"@types/three": "^0.161.2",
|
"@types/three": "^0.161.2",
|
||||||
"@types/xmldom": "^0.1.34",
|
"@types/xmldom": "^0.1.34",
|
||||||
"canvg": "^4.0.1",
|
"canvg": "^4.0.1",
|
||||||
@@ -52,9 +49,6 @@
|
|||||||
"isomorphic-xml2js": "^0.1.3",
|
"isomorphic-xml2js": "^0.1.3",
|
||||||
"jspdf": "^2.5.1",
|
"jspdf": "^2.5.1",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"leaflet": "^1.9.4",
|
|
||||||
"leaflet-gpx": "^1.7.0",
|
|
||||||
"leaflet.awesome-markers": "^2.0.5",
|
|
||||||
"maplibre-gl": "^4.7.1",
|
"maplibre-gl": "^4.7.1",
|
||||||
"meilisearch": "^0.37.0",
|
"meilisearch": "^0.37.0",
|
||||||
"nouislider": "^15.7.1",
|
"nouislider": "^15.7.1",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
></i>
|
></i>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
class="flex opacity-0 group-hover:opacity-100 absolute top-0 w-full h-full bg-white/75 rounded-xl items-center justify-center gap-6 transition-all"
|
class="flex opacity-0 group-hover:opacity-100 absolute top-0 w-full h-full bg-secondary-hover/75 rounded-xl items-center justify-center gap-6 transition-all"
|
||||||
>
|
>
|
||||||
{#if showThumbnailControls}
|
{#if showThumbnailControls}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { SummitLog } from "$lib/models/summit_log";
|
import type { SummitLog } from "$lib/models/summit_log";
|
||||||
import "leaflet/dist/leaflet.css";
|
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
||||||
|
|
||||||
import { browser } from "$app/environment";
|
|
||||||
import GPX from "$lib/models/gpx/gpx";
|
|
||||||
import {
|
import {
|
||||||
formatDistance,
|
formatDistance,
|
||||||
formatElevation,
|
formatElevation,
|
||||||
formatTimeHHMM,
|
formatTimeHHMM,
|
||||||
} from "$lib/util/format_util";
|
} from "$lib/util/format_util";
|
||||||
import { gpx } from "$lib/vendor/toGeoJSON/toGeoJSON";
|
import { getFileURL, readAsDataURLAsync } from "$lib/util/file_util";
|
||||||
import type { Map } from "leaflet";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
let map: Map;
|
let thumbnail: string = "/imgs/default_thumbnail.webp";
|
||||||
let L: any;
|
|
||||||
let layer: any;
|
$: Promise.all(
|
||||||
|
(log._photos ?? []).map(async (f) => {
|
||||||
|
return await readAsDataURLAsync(f);
|
||||||
|
}),
|
||||||
|
).then((v) => {
|
||||||
|
if (log.photos.length) {
|
||||||
|
thumbnail = getFileURL(log, log.photos[0]);
|
||||||
|
} else if (v.length) {
|
||||||
|
thumbnail = v[0];
|
||||||
|
} else {
|
||||||
|
thumbnail = "/imgs/default_thumbnail.webp";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export let index: number = 0;
|
|
||||||
export let log: SummitLog;
|
export let log: SummitLog;
|
||||||
export let mode: "show" | "edit" = "show";
|
export let mode: "show" | "edit" = "show";
|
||||||
|
|
||||||
@@ -27,64 +33,18 @@
|
|||||||
{ text: $_("edit"), value: "edit" },
|
{ text: $_("edit"), value: "edit" },
|
||||||
{ text: $_("delete"), value: "delete" },
|
{ text: $_("delete"), value: "delete" },
|
||||||
];
|
];
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (!map) {
|
|
||||||
await initMap();
|
|
||||||
}
|
|
||||||
if (log.expand.gpx_data) {
|
|
||||||
showTrailOnMap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function initMap() {
|
|
||||||
L = (await import("leaflet")).default;
|
|
||||||
|
|
||||||
map = L.map("mini-map-" + index, {
|
|
||||||
zoomControl: false,
|
|
||||||
scrollWheelZoom: false,
|
|
||||||
dragging: false,
|
|
||||||
});
|
|
||||||
map.attributionControl.setPrefix(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (log.expand.gpx_data) {
|
|
||||||
showTrailOnMap();
|
|
||||||
} else {
|
|
||||||
removeTrailFromMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function showTrailOnMap() {
|
|
||||||
if (!log.expand.gpx_data || !browser || !map) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const geoJson = gpx(
|
|
||||||
new DOMParser().parseFromString(log.expand.gpx_data, "text/xml"),
|
|
||||||
);
|
|
||||||
layer = L.geoJson(geoJson, {
|
|
||||||
filter: (feature: any, layer: any) => {
|
|
||||||
return feature.geometry.type !== "Point";
|
|
||||||
},
|
|
||||||
}).addTo(map);
|
|
||||||
map.fitBounds(layer.getBounds());
|
|
||||||
map.invalidateSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeTrailFromMap() {
|
|
||||||
if (layer) {
|
|
||||||
map?.removeLayer(layer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-4 my-2 border border-input-border rounded-xl">
|
<div class="p-4 my-2 border border-input-border rounded-xl">
|
||||||
<div class="flex items-center gap-x-4">
|
<div class="flex items-center gap-x-4">
|
||||||
<div
|
<div class="h-24 aspect-square shrink-0 rounded-xl overflow-hidden">
|
||||||
class="h-24 aspect-square shrink-0 rounded-xl !bg-background"
|
<img
|
||||||
class:hidden={!log.expand.gpx_data}
|
id="header-img"
|
||||||
id="mini-map-{index}"
|
class="object-cover h-full"
|
||||||
></div>
|
src={thumbnail}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="basis-full">
|
<div class="basis-full">
|
||||||
<div
|
<div
|
||||||
class="flex justify-between items-center"
|
class="flex justify-between items-center"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import Textarea from "../base/textarea.svelte";
|
import Textarea from "../base/textarea.svelte";
|
||||||
import TrailPicker from "../trail/trail_picker.svelte";
|
import TrailPicker from "../trail/trail_picker.svelte";
|
||||||
import GPX from "$lib/models/gpx/gpx";
|
import GPX from "$lib/models/gpx/gpx";
|
||||||
|
import PhotoPicker from "../trail/photo_picker.svelte";
|
||||||
export let openModal: (() => void) | undefined = undefined;
|
export let openModal: (() => void) | undefined = undefined;
|
||||||
export let closeModal: (() => void) | undefined = undefined;
|
export let closeModal: (() => void) | undefined = undefined;
|
||||||
|
|
||||||
@@ -30,6 +31,22 @@
|
|||||||
if (!$form.expand.gpx_data) {
|
if (!$form.expand.gpx_data) {
|
||||||
$form.gpx = "";
|
$form.gpx = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!$form._photos?.length &&
|
||||||
|
!$form.photos?.length &&
|
||||||
|
$form.expand?.gpx_data
|
||||||
|
) {
|
||||||
|
const canvas = document.querySelector(
|
||||||
|
"#trail-picker-map .maplibregl-canvas",
|
||||||
|
) as HTMLCanvasElement;
|
||||||
|
|
||||||
|
const dataURL = canvas.toDataURL();
|
||||||
|
const response = await fetch(dataURL);
|
||||||
|
const blob = await response.blob();
|
||||||
|
$form._photos = [new File([blob], "route")];
|
||||||
|
}
|
||||||
|
|
||||||
dispatch("save", submittedValues);
|
dispatch("save", submittedValues);
|
||||||
closeModal!();
|
closeModal!();
|
||||||
},
|
},
|
||||||
@@ -85,6 +102,18 @@
|
|||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
></Datepicker>
|
></Datepicker>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="summitlog-photo-input" class="text-sm font-medium pb-1">
|
||||||
|
{$_("photos")}
|
||||||
|
</label>
|
||||||
|
<PhotoPicker
|
||||||
|
id="summitlog-photo-input"
|
||||||
|
parent={$form}
|
||||||
|
bind:photos={$form.photos}
|
||||||
|
bind:photoFiles={$form._photos}
|
||||||
|
showThumbnailControls={false}
|
||||||
|
></PhotoPicker>
|
||||||
|
</div>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<TrailPicker
|
<TrailPicker
|
||||||
bind:trailFile={$form._gpx}
|
bind:trailFile={$form._gpx}
|
||||||
|
|||||||
@@ -79,7 +79,6 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{#each summitLogs as log, i}
|
{#each summitLogs as log, i}
|
||||||
<SummitLogTableRow
|
<SummitLogTableRow
|
||||||
index={i}
|
|
||||||
{log}
|
{log}
|
||||||
on:open={(e) => openMap(e.detail)}
|
on:open={(e) => openMap(e.detail)}
|
||||||
on:text={(e) => openText(e.detail)}
|
on:text={(e) => openText(e.detail)}
|
||||||
|
|||||||
@@ -1,66 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { SummitLog } from "$lib/models/summit_log";
|
import type { SummitLog } from "$lib/models/summit_log";
|
||||||
import "leaflet/dist/leaflet.css";
|
|
||||||
|
|
||||||
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
import {
|
import {
|
||||||
formatDistance,
|
formatDistance,
|
||||||
formatElevation,
|
formatElevation,
|
||||||
formatTimeHHMM,
|
formatTimeHHMM,
|
||||||
} from "$lib/util/format_util";
|
} from "$lib/util/format_util";
|
||||||
import { gpx } from "$lib/vendor/toGeoJSON/toGeoJSON";
|
import { createEventDispatcher } from "svelte";
|
||||||
import type { Map } from "leaflet";
|
|
||||||
import { createEventDispatcher, onMount } from "svelte";
|
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import { getFileURL } from "$lib/util/file_util";
|
import PhotoGallery from "../photo_gallery.svelte";
|
||||||
|
|
||||||
export let index: number;
|
|
||||||
export let log: SummitLog;
|
export let log: SummitLog;
|
||||||
export let showCategory: boolean = false;
|
export let showCategory: boolean = false;
|
||||||
export let showTrail: boolean = false;
|
export let showTrail: boolean = false;
|
||||||
export let showAuthor: boolean = false;
|
export let showAuthor: boolean = false;
|
||||||
|
|
||||||
let map: Map;
|
let openGallery: (idx?: number) => void;
|
||||||
|
|
||||||
|
let imgSrc: string[] = [];
|
||||||
|
$: if (log.photos?.length) {
|
||||||
|
imgSrc = log.photos
|
||||||
|
.filter((_, i) => i < 3)
|
||||||
|
.reverse()
|
||||||
|
.map((p) => getFileURL(log, p));
|
||||||
|
} else {
|
||||||
|
imgSrc = [];
|
||||||
|
}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (log.expand.gpx_data) {
|
|
||||||
await initMap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function initMap() {
|
|
||||||
const L = (await import("leaflet")).default;
|
|
||||||
|
|
||||||
map = L.map("mini-map-" + index, {
|
|
||||||
zoomControl: false,
|
|
||||||
scrollWheelZoom: false,
|
|
||||||
dragging: false,
|
|
||||||
});
|
|
||||||
map.attributionControl.setPrefix(false);
|
|
||||||
|
|
||||||
if (!log.expand.gpx_data || !map) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const geoJson = gpx(
|
|
||||||
new DOMParser().parseFromString(log.expand.gpx_data, "text/xml"),
|
|
||||||
);
|
|
||||||
const layer = (L as any)
|
|
||||||
.geoJson(geoJson, {
|
|
||||||
filter: (feature: any, layer: any) => {
|
|
||||||
return feature.geometry.type !== "Point";
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addTo(map);
|
|
||||||
map.fitBounds(layer.getBounds());
|
|
||||||
map.invalidateSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
function openMap() {
|
|
||||||
dispatch("open", log);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openText() {
|
function openText() {
|
||||||
dispatch("text", log);
|
dispatch("text", log);
|
||||||
}
|
}
|
||||||
@@ -75,13 +44,27 @@
|
|||||||
|
|
||||||
<tr class="text-center">
|
<tr class="text-center">
|
||||||
<td>
|
<td>
|
||||||
|
{#if imgSrc.length}
|
||||||
|
<PhotoGallery
|
||||||
|
photos={log.photos.map((p) => getFileURL(log, p))}
|
||||||
|
bind:open={openGallery}
|
||||||
|
></PhotoGallery>
|
||||||
<button
|
<button
|
||||||
|
class="relative w-16 aspect-square ml-2 mb-3 shrink-0"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={openMap}
|
on:click={() => openGallery()}
|
||||||
class="h-20 aspect-square shrink-0 rounded-xl !bg-background hover:!bg-secondary-hover transition-colors"
|
>
|
||||||
class:hidden={!log.expand?.gpx_data}
|
{#each imgSrc as img, i}
|
||||||
id="mini-map-{index}"
|
<img
|
||||||
></button>
|
class="absolute h-full rounded-xl object-cover"
|
||||||
|
style="top: {6 * i}px; right: {6 *
|
||||||
|
i}px; transform: rotate(-{i * 5}deg)"
|
||||||
|
src={img}
|
||||||
|
alt="waypoint"
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class:py-4={!log.expand?.gpx_data}
|
<td class:py-4={!log.expand?.gpx_data}
|
||||||
>{new Date(log.date).toLocaleDateString(undefined, {
|
>{new Date(log.date).toLocaleDateString(undefined, {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex gap-x-4 max-w-full overflow-x-auto shrink-0 rounded-xl {offerUpload
|
class="flex gap-x-4 max-w-full shrink-0 rounded-xl {offerUpload
|
||||||
? 'outline-dashed outline-input-border'
|
? 'outline-dashed outline-input-border'
|
||||||
: ''}"
|
: ''}"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
on:drop={handlePhotoDrop}
|
on:drop={handlePhotoDrop}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="btn-secondary h-32 w-32 m-2 shrink-0 grow-0 basis-auto"
|
class="btn-secondary h-32 w-32 shrink-0 grow-0 basis-auto"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={openPhotoBrowser}><i class="fa fa-plus"></i></button
|
on:click={openPhotoBrowser}><i class="fa fa-plus"></i></button
|
||||||
>
|
>
|
||||||
@@ -137,8 +137,9 @@
|
|||||||
style="display: none;"
|
style="display: none;"
|
||||||
on:change={() => handlePhotoSelection()}
|
on:change={() => handlePhotoSelection()}
|
||||||
/>
|
/>
|
||||||
|
<div class="flex overflow-x-auto gap-x-3">
|
||||||
{#each (photos ?? []).concat(photoPreviews) as photo, i}
|
{#each (photos ?? []).concat(photoPreviews) as photo, i}
|
||||||
<div class="shrink-0 grow-0 basis-auto m-2">
|
<div class="shrink-0 grow-0 basis-auto overflow-hidden">
|
||||||
<PhotoCard
|
<PhotoCard
|
||||||
src={i >= photos.length ? photo : getFileURL(parent, photo)}
|
src={i >= photos.length ? photo : getFileURL(parent, photo)}
|
||||||
on:delete={() => handlePhotoDelete(i)}
|
on:delete={() => handlePhotoDelete(i)}
|
||||||
@@ -151,3 +152,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
class="absolute bottom-0 w-full h-2/3 bg-gradient-to-b from-transparent to-black opacity-50"
|
||||||
></div>
|
></div>
|
||||||
{#if (trail.public || trailIsShared) && pb.authStore.model}
|
{#if (trail.public || trailIsShared) && pb.authStore.model}
|
||||||
<div
|
<div
|
||||||
@@ -366,7 +366,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if mode == "overview"}
|
{#if mode == "overview"}
|
||||||
<div class="relative h-72 rounded-xl">
|
<div class="relative h-72 rounded-xl overflow-hidden">
|
||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
trails={[trail]}
|
trails={[trail]}
|
||||||
showElevation={false}
|
showElevation={false}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { gpx } from "$lib/vendor/toGeoJSON/toGeoJSON";
|
import * as M from "maplibre-gl";
|
||||||
import "leaflet/dist/leaflet.css";
|
|
||||||
|
|
||||||
import type { Map } from "leaflet";
|
import { fromFile, toGeoJson } from "$lib/util/gpx_util";
|
||||||
import { createEventDispatcher, onMount, tick } from "svelte";
|
import { createEventDispatcher, onMount } from "svelte";
|
||||||
export let trailFile: File | null;
|
export let trailFile: File | null;
|
||||||
export let trailData: string | undefined;
|
export let trailData: string | undefined;
|
||||||
export let label: string = "";
|
export let label: string = "";
|
||||||
|
|
||||||
let map: Map;
|
let map: M.Map;
|
||||||
let L: any;
|
let layer: M.LineLayerSpecification | undefined;
|
||||||
let layer: any;
|
let source: M.GeoJSONSource | undefined;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
@@ -27,14 +26,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function initMap() {
|
async function initMap() {
|
||||||
L = (await import("leaflet")).default;
|
map = new M.Map({
|
||||||
|
container: "trail-picker-map",
|
||||||
map = L.map("trail-picker-map", {
|
attributionControl: false,
|
||||||
zoomControl: false,
|
dragPan: false,
|
||||||
scrollWheelZoom: false,
|
scrollZoom: false,
|
||||||
dragging: false,
|
preserveDrawingBuffer: true
|
||||||
});
|
});
|
||||||
map.attributionControl.setPrefix(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTrailBrowser() {
|
function openTrailBrowser() {
|
||||||
@@ -42,7 +40,7 @@
|
|||||||
trailFile = null;
|
trailFile = null;
|
||||||
trailData = undefined;
|
trailData = undefined;
|
||||||
|
|
||||||
dispatch("change", null)
|
dispatch("change", null);
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("trail-input")!.click();
|
document.getElementById("trail-input")!.click();
|
||||||
}
|
}
|
||||||
@@ -59,35 +57,61 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
trailFile = files.item(0);
|
trailFile = files.item(0);
|
||||||
trailData = await trailFile?.text();
|
if (!trailFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parseResult = await fromFile(trailFile);
|
||||||
|
|
||||||
|
trailData = parseResult.gpxData;
|
||||||
|
|
||||||
dispatch("change", trailData);
|
dispatch("change", trailData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showTrailOnMap() {
|
async function showTrailOnMap() {
|
||||||
if (!trailData) {
|
if (!trailData || !map) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const sourceId = "trail-picker-geojson-source";
|
||||||
|
const layerId = "trail-picker-geojson-layer";
|
||||||
|
|
||||||
|
const geojson = toGeoJson(trailData);
|
||||||
|
|
||||||
const geoJson = gpx(
|
|
||||||
new DOMParser().parseFromString(trailData, "text/xml"),
|
|
||||||
);
|
|
||||||
if (layer) {
|
if (layer) {
|
||||||
map.removeLayer(layer);
|
map.removeLayer(layerId);
|
||||||
|
map.removeSource(sourceId);
|
||||||
}
|
}
|
||||||
layer = L.geoJson(geoJson, {
|
|
||||||
filter: (feature: any, layer: any) => {
|
map.addSource(sourceId, {
|
||||||
return feature.geometry.type !== "Point";
|
type: "geojson",
|
||||||
|
data: geojson,
|
||||||
|
});
|
||||||
|
map.addLayer({
|
||||||
|
id: layerId,
|
||||||
|
type: "line",
|
||||||
|
source: sourceId,
|
||||||
|
paint: {
|
||||||
|
"line-color": "#3388ff",
|
||||||
|
"line-width": 3,
|
||||||
},
|
},
|
||||||
}).addTo(map);
|
});
|
||||||
map.fitBounds(layer.getBounds());
|
layer = map.getLayer(layerId) as M.LineLayerSpecification;
|
||||||
map.invalidateSize();
|
source = map.getSource(sourceId) as M.GeoJSONSource;
|
||||||
|
map.resize()
|
||||||
|
map.fitBounds(geojson.bbox as M.LngLatBoundsLike, {animate: false, padding: 8});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeTrailFromMap() {
|
function removeTrailFromMap() {
|
||||||
if (layer) {
|
if (layer) {
|
||||||
map?.removeLayer(layer);
|
map?.removeLayer(layer.id);
|
||||||
|
layer = undefined;
|
||||||
}
|
}
|
||||||
|
if (source) {
|
||||||
|
map?.removeSource(source.id);
|
||||||
|
source = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
trailData = undefined;
|
||||||
|
trailFile = null;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
$: if (waypoint.photos?.length) {
|
$: if (waypoint.photos?.length) {
|
||||||
imgSrc = waypoint.photos
|
imgSrc = waypoint.photos
|
||||||
.filter((_, i) => i < 3)
|
.filter((_, i) => i < 3)
|
||||||
|
.reverse()
|
||||||
.map((p) => getFileURL(waypoint, p));
|
.map((p) => getFileURL(waypoint, p));
|
||||||
} else if (waypoint._photos?.length && browser) {
|
} else if (waypoint._photos?.length && browser) {
|
||||||
Promise.all(
|
Promise.all(
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex gap-4 p-4 border border-input-border rounded-md my-2 hover:bg-menu-item-background-hover"
|
class="flex gap-4 p-4 outline outline-1 outline-input-border rounded-md my-2 hover:outline-2"
|
||||||
>
|
>
|
||||||
{#if imgSrc.length}
|
{#if imgSrc.length}
|
||||||
{#if mode == "show"}
|
{#if mode == "show"}
|
||||||
|
|||||||
@@ -120,11 +120,11 @@
|
|||||||
></TextField>
|
></TextField>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="trail-photo-input" class="text-sm font-medium pb-1">
|
<label for="waypoint-photo-input" class="text-sm font-medium pb-1">
|
||||||
{$_("photos")}
|
{$_("photos")}
|
||||||
</label>
|
</label>
|
||||||
<PhotoPicker
|
<PhotoPicker
|
||||||
id="waypoint"
|
id="waypoint-photo-input"
|
||||||
parent={$form}
|
parent={$form}
|
||||||
on:exif={(e) => getCoordinatesFromPhoto(e.detail)}
|
on:exif={(e) => getCoordinatesFromPhoto(e.detail)}
|
||||||
bind:photos={$form.photos}
|
bind:photos={$form.photos}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ class SummitLog {
|
|||||||
text?: string;
|
text?: string;
|
||||||
gpx?: string;
|
gpx?: string;
|
||||||
_gpx: File | null;
|
_gpx: File | null;
|
||||||
|
photos: string[];
|
||||||
|
_photos: File[];
|
||||||
distance?: number
|
distance?: number
|
||||||
elevation_gain?: number
|
elevation_gain?: number
|
||||||
elevation_loss?: number
|
elevation_loss?: number
|
||||||
@@ -19,7 +21,7 @@ class SummitLog {
|
|||||||
author?: UserAnonymous
|
author?: UserAnonymous
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(date: string, params?: { id?: string, text?: string, distance?: number, elevation_loss?: number, elevation_gain?: number, duration?: number }) {
|
constructor(date: string, params?: { id?: string, text?: string, distance?: number, elevation_loss?: number, elevation_gain?: number, duration?: number, photos?: string[] }) {
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.id = params?.id;
|
this.id = params?.id;
|
||||||
this.text = params?.text ?? "";
|
this.text = params?.text ?? "";
|
||||||
@@ -29,6 +31,8 @@ class SummitLog {
|
|||||||
this.elevation_loss = params?.elevation_loss
|
this.elevation_loss = params?.elevation_loss
|
||||||
this.duration = params?.duration
|
this.duration = params?.duration
|
||||||
this._gpx = null
|
this._gpx = null
|
||||||
|
this.photos = params?.photos ?? []
|
||||||
|
this._photos = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,9 +53,10 @@ export async function summit_logs_create(summitLog: SummitLog, f: (url: RequestI
|
|||||||
throw new ClientResponseError(await r.json())
|
throw new ClientResponseError(await r.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (summitLog._gpx && summitLog._gpx instanceof File) {
|
|
||||||
let model: SummitLog = await r.json();
|
let model: SummitLog = await r.json();
|
||||||
|
|
||||||
|
if (summitLog._gpx && summitLog._gpx instanceof File) {
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
formData.append("gpx", summitLog._gpx)
|
formData.append("gpx", summitLog._gpx)
|
||||||
@@ -66,6 +67,20 @@ export async function summit_logs_create(summitLog: SummitLog, f: (url: RequestI
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (summitLog._photos && summitLog._photos.length) {
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
for (const photo of summitLog._photos) {
|
||||||
|
formData.append("photos", photo)
|
||||||
|
}
|
||||||
|
|
||||||
|
r = await fetch(`/api/v1/summit-log/${model.id!}/file`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
return await r.json();
|
return await r.json();
|
||||||
} else {
|
} else {
|
||||||
@@ -73,28 +88,38 @@ export async function summit_logs_create(summitLog: SummitLog, f: (url: RequestI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function summit_logs_update(summitLog: SummitLog) {
|
export async function summit_logs_update(oldSummitLog: SummitLog, newSummitLog: SummitLog) {
|
||||||
summitLog.author = pb.authStore.model!.id
|
newSummitLog.author = pb.authStore.model!.id
|
||||||
|
|
||||||
let r = await fetch('/api/v1/summit-log/' + summitLog.id, {
|
let r = await fetch('/api/v1/summit-log/' + newSummitLog.id, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(summitLog),
|
body: JSON.stringify(newSummitLog),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
throw new ClientResponseError(await r.json())
|
throw new ClientResponseError(await r.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (summitLog._gpx) {
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
formData.append("gpx", summitLog._gpx);
|
for (const photo of newSummitLog._photos ?? []) {
|
||||||
r = await fetch(`/api/v1/summit-log/${summitLog.id!}/file`, {
|
formData.append("photos", photo)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedPhotos = oldSummitLog.photos.filter(oldPhoto => !newSummitLog.photos.find(newPhoto => newPhoto === oldPhoto));
|
||||||
|
|
||||||
|
for (const deletedPhoto of deletedPhotos) {
|
||||||
|
formData.append("photos-", deletedPhoto.replace(/^.*[\\/]/, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSummitLog._gpx) {
|
||||||
|
formData.append("gpx", newSummitLog._gpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
r = await fetch(`/api/v1/summit-log/${newSummitLog.id!}/file`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
return await r.json();
|
return await r.json();
|
||||||
|
|||||||
@@ -261,7 +261,9 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const updatedSummitLog of summitLogUpdates.updated) {
|
for (const updatedSummitLog of summitLogUpdates.updated) {
|
||||||
const model = await summit_logs_update(updatedSummitLog);
|
const oldSummitLog = oldTrail.expand.summit_logs.find(w => w.id == updatedSummitLog.id);
|
||||||
|
|
||||||
|
const model = await summit_logs_update(oldSummitLog!, updatedSummitLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const deletedSummitLog of summitLogUpdates.deleted) {
|
for (const deletedSummitLog of summitLogUpdates.deleted) {
|
||||||
|
|||||||
@@ -102,6 +102,37 @@ export async function trail2gpx(trail: Trail) {
|
|||||||
return gpx.toString();
|
return gpx.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fromFile(file: File | Blob) {
|
||||||
|
let gpxData = "";
|
||||||
|
let gpxFile: Blob;
|
||||||
|
const fileContent = await file.text();
|
||||||
|
const fileBuffer = await file.arrayBuffer();
|
||||||
|
|
||||||
|
if (!isFITFile(fileBuffer)) {
|
||||||
|
if (fileContent.includes("http://www.opengis.net/kml")) {
|
||||||
|
gpxData = fromKML(fileContent);
|
||||||
|
gpxFile = new Blob([gpxData], {
|
||||||
|
type: "application/gpx+xml",
|
||||||
|
});
|
||||||
|
} else if (fileContent.includes("TrainingCenterDatabase")) {
|
||||||
|
gpxData = fromTCX(fileContent);
|
||||||
|
gpxFile = new Blob([gpxData], {
|
||||||
|
type: "application/gpx+xml",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
gpxData = fileContent;
|
||||||
|
gpxFile = file;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gpxData = await fromFIT(fileBuffer);
|
||||||
|
gpxFile = new Blob([gpxData], {
|
||||||
|
type: "application/gpx+xml",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {gpxData, gpxFile};
|
||||||
|
}
|
||||||
|
|
||||||
export function fromKML(kmlData: string) {
|
export function fromKML(kmlData: string) {
|
||||||
const parser = browser ? new DOMParser() : new xmldom.DOMParser();
|
const parser = browser ? new DOMParser() : new xmldom.DOMParser();
|
||||||
const nodes = parser.parseFromString(kmlData, "text/xml")
|
const nodes = parser.parseFromString(kmlData, "text/xml")
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
import { haversineDistance } from "$lib/models/gpx/utils";
|
|
||||||
import type { Waypoint } from "$lib/models/waypoint";
|
|
||||||
import type { LeafletEvent, Map, Marker } from "leaflet";
|
|
||||||
import * as M from "maplibre-gl";
|
|
||||||
|
|
||||||
export const startIcon = () => L.divIcon({
|
|
||||||
html: '<i class="p-2 text-white bg-gray-500 rounded-full fa fa-bullseye -translate-x-1/2 -translate-y-1/3"></i>',
|
|
||||||
className: 'start-icon'
|
|
||||||
});
|
|
||||||
export const endIcon = () => L.divIcon({
|
|
||||||
html: '<i class="p-2 text-white bg-gray-500 rounded-full fa fa-flag-checkered -translate-x-1/2 -translate-y-1/3"></i>',
|
|
||||||
className: 'end-icon'
|
|
||||||
});
|
|
||||||
|
|
||||||
export function createMarkerFromWaypoint(L: any, waypoint: Waypoint, onDragEnd?: (event: LeafletEvent) => void): Marker {
|
|
||||||
const icon = L.divIcon({
|
|
||||||
html: `<i class="px-2 py-2 text-white bg-gray-500 rounded-full fa fa-${waypoint.icon}"></i>`,
|
|
||||||
className: 'waypoint-icon'
|
|
||||||
});
|
|
||||||
|
|
||||||
const marker = L.marker([waypoint.lat, waypoint.lon], {
|
|
||||||
title: waypoint.name,
|
|
||||||
icon: icon,
|
|
||||||
draggable: onDragEnd != null,
|
|
||||||
meta: {
|
|
||||||
waypointName: waypoint.name
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.bindPopup(
|
|
||||||
"<b>" +
|
|
||||||
waypoint.name +
|
|
||||||
"</b>" +
|
|
||||||
(waypoint.description && waypoint.description.length > 0
|
|
||||||
? "<br>" + waypoint.description
|
|
||||||
: ""),
|
|
||||||
);
|
|
||||||
if (onDragEnd) {
|
|
||||||
marker.on("dragend", onDragEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
return marker;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAnchorMarker(L: any, lat: number, lon: number, index: number, onDeleteClick: () => void, onDragEnd: (event: LeafletEvent) => void): Marker {
|
|
||||||
|
|
||||||
const anchorIconElement = document.createElement("span")
|
|
||||||
anchorIconElement.textContent = "" + index
|
|
||||||
const anchorIcon = L.divIcon({
|
|
||||||
html: anchorIconElement,
|
|
||||||
iconSize: [24, 24],
|
|
||||||
className: "leaflet-anchor"
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteButton = document.createElement("button");
|
|
||||||
deleteButton.className = "btn-icon fa fa-trash text-red-500";
|
|
||||||
deleteButton.addEventListener("click", onDeleteClick)
|
|
||||||
const marker = L.marker([lat, lon], {
|
|
||||||
icon: anchorIcon,
|
|
||||||
draggable: true,
|
|
||||||
})
|
|
||||||
.bindPopup(
|
|
||||||
deleteButton
|
|
||||||
);
|
|
||||||
marker.on("dragend", onDragEnd);
|
|
||||||
|
|
||||||
return marker
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculatePixelPerMeter(map: M.Map, meters: number) {
|
|
||||||
const y = map.getCanvas().getBoundingClientRect().y;
|
|
||||||
const x = map.getCanvas().getBoundingClientRect().x;
|
|
||||||
const maxMeters = map.unproject([0, y]).distanceTo(map.unproject([x, y]));
|
|
||||||
const pixelPerMeter = x / maxMeters;
|
|
||||||
|
|
||||||
return pixelPerMeter * meters
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateScaleFactor(map: M.Map) {
|
|
||||||
function _pxTOmm() {
|
|
||||||
let heightRef = document.createElement('div');
|
|
||||||
heightRef.style.height = '1mm';
|
|
||||||
heightRef.style.position = "absolute";
|
|
||||||
heightRef.id = 'heightRef';
|
|
||||||
document.body.appendChild(heightRef);
|
|
||||||
|
|
||||||
const pxPermm = heightRef.getBoundingClientRect().height;
|
|
||||||
|
|
||||||
document.body.removeChild(heightRef);
|
|
||||||
|
|
||||||
return function pxTOmm(px: number) {
|
|
||||||
return px / pxPermm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var centerOfMap = map.getCanvas().getBoundingClientRect().y / 2;
|
|
||||||
|
|
||||||
const p1 = map.unproject([0, centerOfMap]);
|
|
||||||
const p2 = map.unproject([100, centerOfMap]);
|
|
||||||
var realWorldMetersPer100Pixels = haversineDistance(
|
|
||||||
p1.lat, p1.lng, p2.lat, p2.lng
|
|
||||||
);
|
|
||||||
|
|
||||||
const screenMetersPer100Pixels = _pxTOmm()(100) / 1000;
|
|
||||||
|
|
||||||
const scaleFactor = realWorldMetersPer100Pixels / screenMetersPer100Pixels
|
|
||||||
|
|
||||||
return scaleFactor
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertDMSToDD(dms: Number[], direction: "N" | "O" | "S" | "W") {
|
|
||||||
var dd = dms[0].valueOf() + dms[1].valueOf() / 60 + dms[2].valueOf() / (60 * 60);
|
|
||||||
|
|
||||||
if (direction == "S" || direction == "W") {
|
|
||||||
dd = dd * -1;
|
|
||||||
}
|
|
||||||
return dd;
|
|
||||||
}
|
|
||||||
3
web/src/lib/vendor/svelte-form-lib/util.js
vendored
3
web/src/lib/vendor/svelte-form-lib/util.js
vendored
@@ -15,8 +15,7 @@ function update(object, path, value) {
|
|||||||
|
|
||||||
function cloneDeep(object) {
|
function cloneDeep(object) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(JSON.stringify(object));
|
return structuredClone(object);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,16 @@
|
|||||||
import { SummitLog } from "$lib/models/summit_log";
|
import { SummitLog } from "$lib/models/summit_log";
|
||||||
import type { Trail } from "$lib/models/trail";
|
import type { Trail } from "$lib/models/trail";
|
||||||
import { trails_create } from "$lib/stores/trail_store";
|
import { trails_create } from "$lib/stores/trail_store";
|
||||||
import { fromFIT, fromKML, fromTCX, gpx2trail, isFITFile } from "$lib/util/gpx_util";
|
import { fromFile, fromFIT, fromKML, fromTCX, gpx2trail, isFITFile } from "$lib/util/gpx_util";
|
||||||
import { error, json, type RequestEvent } from "@sveltejs/kit";
|
import { error, json, type RequestEvent } from "@sveltejs/kit";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
|
||||||
export async function PUT(event: RequestEvent) {
|
export async function PUT(event: RequestEvent) {
|
||||||
try {
|
try {
|
||||||
const data = await event.request.formData();
|
const data = await event.request.formData();
|
||||||
const fileBuffer = await (data.get("file") as Blob).arrayBuffer();
|
|
||||||
const fileContent = await (data.get("file") as Blob).text();
|
const { gpxData, gpxFile } = await fromFile(data.get("file") as Blob)
|
||||||
let gpxData = ""
|
|
||||||
let gpxFile: Blob;
|
|
||||||
if (isFITFile(fileBuffer)) {
|
|
||||||
gpxData = await fromFIT(fileBuffer);
|
|
||||||
gpxFile = new Blob([gpxData], {
|
|
||||||
type: "application/gpx+xml",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (fileContent.includes("http://www.opengis.net/kml")) {
|
|
||||||
gpxData = fromKML(fileContent);
|
|
||||||
gpxFile = new Blob([gpxData], {
|
|
||||||
type: "application/gpx+xml",
|
|
||||||
});
|
|
||||||
} else if (fileContent.includes("TrainingCenterDatabase")) {
|
|
||||||
gpxData = fromTCX(fileContent);
|
|
||||||
gpxFile = new Blob([gpxData], {
|
|
||||||
type: "application/gpx+xml",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
gpxData = fileContent;
|
|
||||||
gpxFile = data.get("file") as Blob
|
|
||||||
}
|
|
||||||
if (!gpxData.length) {
|
if (!gpxData.length) {
|
||||||
throw new ClientResponseError({ status: 400, response: { message: "Empty file" } })
|
throw new ClientResponseError({ status: 400, response: { message: "Empty file" } })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
formatTimeHHMM,
|
formatTimeHHMM,
|
||||||
} from "$lib/util/format_util";
|
} from "$lib/util/format_util";
|
||||||
import {
|
import {
|
||||||
|
fromFile,
|
||||||
fromFIT,
|
fromFIT,
|
||||||
fromKML,
|
fromKML,
|
||||||
fromTCX,
|
fromTCX,
|
||||||
@@ -231,39 +232,6 @@
|
|||||||
document.getElementById("fileInput")!.click();
|
document.getElementById("fileInput")!.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseFile(file: File) {
|
|
||||||
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
|
||||||
|
|
||||||
let gpxData = "";
|
|
||||||
const fileContent = await file.text();
|
|
||||||
const fileBuffer = await file.arrayBuffer();
|
|
||||||
|
|
||||||
if (!isFITFile(fileBuffer)) {
|
|
||||||
if (fileContent.includes("http://www.opengis.net/kml")) {
|
|
||||||
gpxData = fromKML(fileContent);
|
|
||||||
gpxFile = new Blob([gpxData], {
|
|
||||||
type: "application/gpx+xml",
|
|
||||||
});
|
|
||||||
return gpxData;
|
|
||||||
} else if (fileContent.includes("TrainingCenterDatabase")) {
|
|
||||||
gpxData = fromTCX(fileContent);
|
|
||||||
gpxFile = new Blob([gpxData], {
|
|
||||||
type: "application/gpx+xml",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
gpxData = fileContent;
|
|
||||||
gpxFile = file;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
gpxData = await fromFIT(fileBuffer);
|
|
||||||
gpxFile = new Blob([gpxData], {
|
|
||||||
type: "application/gpx+xml",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return gpxData;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleFileSelection() {
|
async function handleFileSelection() {
|
||||||
const selectedFile = (
|
const selectedFile = (
|
||||||
document.getElementById("fileInput") as HTMLInputElement
|
document.getElementById("fileInput") as HTMLInputElement
|
||||||
@@ -279,7 +247,8 @@
|
|||||||
drawingActive = false;
|
drawingActive = false;
|
||||||
overwriteGPX = false;
|
overwriteGPX = false;
|
||||||
|
|
||||||
let gpxData = await parseFile(selectedFile);
|
const { gpxData, gpxFile: file } = await fromFile(selectedFile);
|
||||||
|
gpxFile = file;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parseResult = await gpx2trail(gpxData, selectedFile.name);
|
const parseResult = await gpx2trail(gpxData, selectedFile.name);
|
||||||
@@ -848,7 +817,6 @@
|
|||||||
<li>
|
<li>
|
||||||
<SummitLogCard
|
<SummitLogCard
|
||||||
{log}
|
{log}
|
||||||
index={i}
|
|
||||||
mode="edit"
|
mode="edit"
|
||||||
on:change={(e) => handleSummitLogMenuClick(log, i, e)}
|
on:change={(e) => handleSummitLogMenuClick(log, i, e)}
|
||||||
></SummitLogCard>
|
></SummitLogCard>
|
||||||
|
|||||||
Reference in New Issue
Block a user