more file formats && list add in edit

This commit is contained in:
Christian Beutel
2024-03-27 18:43:46 +01:00
parent ded0ea4f80
commit 340a3c7c05
15 changed files with 1498 additions and 13 deletions

View File

@@ -19,7 +19,7 @@
}
.btn-disabled {
@apply bg-gray-500 hover:bg-gray-500
@apply bg-menu-item-background-hover border-none text-gray-500 hover:bg-menu-item-background-hover
}
.btn-icon {

View File

@@ -47,6 +47,7 @@
"entry": "Eintrag",
"error-creating-user": "Fehler beim Erstellen des Nutzers",
"error-during-login": "Fehler beim Login",
"error-reading-file": "",
"est-duration": "Gesch. Dauer",
"explore": "Erkunden",
"explore-some-trails": "Erkunden Sie einige Routen",

View File

@@ -47,6 +47,7 @@
"entry": "Entry",
"error-creating-user": "Error creating user",
"error-during-login": "Error during login",
"error-reading-file": "Error reading file",
"est-duration": "Est. duration",
"explore": "Explore",
"explore-some-trails": "Explore some trails",

View File

@@ -47,6 +47,7 @@
"entry": "Item",
"error-creating-user": "Fout bij het aanmaken van de gebruiker",
"error-during-login": "Fout bij het inloggen",
"error-reading-file": "",
"est-duration": "Geschatte duur",
"explore": "Verkennen",
"explore-some-trails": "Verken enkele routes",

View File

@@ -47,6 +47,7 @@
"entry": "Pozycja",
"error-creating-user": "Błąd tworzenia użytkownika",
"error-during-login": "Błąd podczas logowania",
"error-reading-file": "",
"est-duration": "Szacowany czas",
"explore": "Eksploruj",
"explore-some-trails": "Eksploruj różne ścieżki",

View File

@@ -47,6 +47,7 @@
"entry": "Entrada",
"error-creating-user": "Erro ao criar usuário",
"error-during-login": "Erro durante o login",
"error-reading-file": "",
"est-duration": "Duração prevista",
"explore": "Explorar",
"explore-some-trails": "Explore algumas trilhas",

View File

@@ -5,7 +5,7 @@ import { ClientResponseError } from "pocketbase";
export const categories: Writable<Category[]> = writable([])
export async function categories_index(f: Function = fetch) {
export async function categories_index(f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f('/api/v1/category', {
method: 'GET',
})

View File

@@ -167,7 +167,7 @@ export async function trails_search_bounding_box(northEast: LatLng, southWest: L
export async function trails_show(id: string, loadGPX?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f(`/api/v1/trail/${id}?` + new URLSearchParams({
expand: "category,waypoints,summit_logs",
expand: "category,waypoints,summit_logs,lists_via_trails",
}), {
method: 'GET',
})

View File

@@ -1,8 +1,10 @@
import { Trail } from "$lib/models/trail";
import { Waypoint } from "$lib/models/waypoint";
import { JSDOM } from "jsdom"
import { kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON"
import GeoJsonToGpx from "$lib/vendor/geoJSONToGPX"
export function gpx2trail(gpx: string) {
export async function gpx2trail(gpx: string) {
const JSDOM = (await import("jsdom")).JSDOM
const xml = new JSDOM(gpx).window.document
const trail = new Trail("");
@@ -15,7 +17,7 @@ export function gpx2trail(gpx: string) {
trail.description = desc[0].textContent ?? "";
}
const el = xml.getElementsByTagName('wpt');
const el = xml.getElementsByTagName('wpt');
for (let i = 0; i < el.length; i++) {
const wp = new Waypoint(parseFloat(el[i].getAttribute('lat')!), parseFloat(el[i].getAttribute('lon')!));
@@ -29,10 +31,45 @@ export function gpx2trail(gpx: string) {
}
const startPoint = xml.getElementsByTagName("trk")[0].getElementsByTagName("trkseg")[0].getElementsByTagName("trkpt")[0];
if(startPoint) {
if (startPoint) {
trail.lat = parseFloat(startPoint.getAttribute('lat')!)
trail.lon = parseFloat(startPoint.getAttribute('lon')!)
}
}
return trail
}
export function fromKML(kmlData: string) {
const geojson = kml(new DOMParser().parseFromString(kmlData, "text/xml"))
const options = {
metadata: {
name: '',
author: {
name: '',
link: {
href: ''
}
}
}
}
const gpx = GeoJsonToGpx(geojson as any, options)
return new XMLSerializer().serializeToString(gpx)
}
export function fromTCX(tcxData: string) {
const geojson = tcx(new DOMParser().parseFromString(tcxData, "text/xml"))
const options = {
metadata: {
name: '',
author: {
name: '',
link: {
href: ''
}
}
}
}
const gpx = GeoJsonToGpx(geojson as any, options)
return new XMLSerializer().serializeToString(gpx)
}

View File

@@ -0,0 +1,39 @@
import { Feature, FeatureCollection } from 'geojson';
declare interface Bounds {
minlat: string;
minlon: string;
maxlat: string;
maxlon: string;
}
declare interface Copyright {
author?: string;
year?: string;
license?: string;
}
declare interface Link {
href: string;
text?: string;
type?: string;
}
declare interface Person {
name?: string;
email?: string;
link?: Link;
}
declare interface MetaData {
name?: string;
desc?: string;
author?: Person;
copyright?: Copyright;
link?: Link;
time?: string;
keywords?: string;
bounds?: Bounds;
}
export declare interface Options {
creator?: string;
version?: string;
metadata?: MetaData;
}
export default function GeoJsonToGpx(geoJson: Feature | FeatureCollection, options?: Options, implementation?: DOMImplementation): XMLDocument;
export {};

155
web/src/lib/vendor/geoJSONToGPX/index.js vendored Normal file
View File

@@ -0,0 +1,155 @@
export default function GeoJsonToGpx(geoJson, options, implementation) {
if (implementation === void 0) { implementation = document.implementation; }
var doc = implementation.createDocument('http://www.topografix.com/GPX/1/1', '');
var instruct = doc.createProcessingInstruction('xml', 'version="1.0" encoding="UTF-8"');
doc.appendChild(instruct);
var defaultPackageName = '@dwayneparton/geojson-to-gpx';
var creator = (options === null || options === void 0 ? void 0 : options.creator) || defaultPackageName;
var createElementWithNS = function (tagName) {
return doc.createElementNS('http://www.topografix.com/GPX/1/1', tagName);
};
var gpx = createElementWithNS('gpx');
gpx.setAttribute('version', '1.1');
gpx.setAttribute('creator', creator);
var wpts = [];
var trks = [];
function createTagInParentElement(parent, tagName, content) {
if (content === undefined) {
return;
}
var element = createElementWithNS(tagName);
var contentEl = doc.createTextNode(String(content));
element.appendChild(contentEl);
parent.appendChild(element);
}
function addSupportedPropertiesFromObject(el, supports, properties) {
if (properties && typeof properties === 'object') {
supports.forEach(function (key) {
var value = properties[key];
if (value && typeof value === 'string' && supports.includes(key)) {
createTagInParentElement(el, key, value);
}
});
}
}
function createLinkInParentElement(parent, props) {
var href = props.href;
if (!href) {
return;
}
var el = createElementWithNS('link');
el.setAttribute('href', href);
addSupportedPropertiesFromObject(el, ['text', 'type'], props);
parent.appendChild(el);
}
function createTrk(properties) {
var el = createElementWithNS('trk');
var supports = ['name', 'desc', 'src', 'type'];
addSupportedPropertiesFromObject(el, supports, properties);
return el;
}
function createPt(type, position, properties) {
var lon = position[0], lat = position[1], ele = position[2], time = position[3];
var el = createElementWithNS(type);
el.setAttribute('lat', String(lat));
el.setAttribute('lon', String(lon));
createTagInParentElement(el, 'ele', ele);
createTagInParentElement(el, 'time', time);
var supports = ['name', 'desc', 'src', 'type'];
addSupportedPropertiesFromObject(el, supports, properties);
return el;
}
function createTrkSeg(coordinates) {
var el = createElementWithNS('trkseg');
coordinates.forEach(function (point) {
el.appendChild(createPt('trkpt', point));
});
return el;
}
function interpretFeature(feature) {
var geometry = feature.geometry, properties = feature.properties;
var type = geometry.type;
switch (type) {
case 'Polygon':
break;
case 'Point': {
wpts.push(createPt('wpt', geometry.coordinates, properties));
break;
}
case 'MultiPoint': {
geometry.coordinates.forEach(function (coord) {
wpts.push(createPt('wpt', coord, properties));
});
break;
}
case 'LineString': {
var lineTrk = createTrk(properties);
var trkseg = createTrkSeg(geometry.coordinates);
lineTrk.appendChild(trkseg);
trks.push(lineTrk);
break;
}
case 'MultiLineString': {
var trk_1 = createTrk(properties);
geometry.coordinates.forEach(function (pos) {
var trkseg = createTrkSeg(pos);
trk_1.appendChild(trkseg);
});
trks.push(trk_1);
break;
}
default:
break;
}
}
if (options && typeof options.metadata === 'object') {
var meta = options.metadata;
var metadata = createElementWithNS('metadata');
createTagInParentElement(metadata, 'name', meta.name);
createTagInParentElement(metadata, 'desc', meta.desc);
if (meta.author && typeof meta.author === 'object') {
var author = createElementWithNS('author');
createTagInParentElement(author, 'name', meta.author.name);
createTagInParentElement(author, 'email', meta.author.email);
if (meta.author.link && typeof meta.author.link === 'object') {
createLinkInParentElement(author, meta.author.link);
}
metadata.appendChild(author);
}
if (typeof meta.copyright === 'object') {
var copyright = createElementWithNS('copyright');
if (meta.copyright.author) {
copyright.setAttribute('author', meta.copyright.author);
}
createTagInParentElement(copyright, 'year', meta.copyright.year);
createTagInParentElement(copyright, 'license', meta.copyright.license);
metadata.appendChild(copyright);
}
if (typeof meta.link === 'object') {
createLinkInParentElement(metadata, meta.link);
}
createTagInParentElement(metadata, 'time', meta.time);
createTagInParentElement(metadata, 'keywords', meta.keywords);
gpx.appendChild(metadata);
}
var type = geoJson.type;
switch (type) {
case 'Feature': {
interpretFeature(geoJson);
break;
}
case 'FeatureCollection': {
var features = geoJson.features;
features.forEach(function (feature) {
interpretFeature(feature);
});
break;
}
default:
break;
}
wpts.forEach(function (wpt) { return gpx.appendChild(wpt); });
trks.forEach(function (trk) { return gpx.appendChild(trk); });
doc.appendChild(gpx);
return doc;
}

1157
web/src/lib/vendor/toGeoJSON/toGeoJSON.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -92,8 +92,6 @@
},
})
.on("addpoint", function (e: any) {
console.log(e);
if (e.point_type === "start") {
const marker: Marker = e.point as Marker;
marker.bindPopup(

View File

@@ -4,15 +4,23 @@
import TextField from "$lib/components/base/text_field.svelte";
import Textarea from "$lib/components/base/textarea.svelte";
import Toggle from "$lib/components/base/toggle.svelte";
import ListSelectModal from "$lib/components/list/list_select_modal.svelte";
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
import SummitLogModal from "$lib/components/summit_log/summit_log_modal.svelte";
import PhotoPicker from "$lib/components/trail/photo_picker.svelte";
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte";
import type { List } from "$lib/models/list";
import { SummitLog } from "$lib/models/summit_log";
import { Trail } from "$lib/models/trail";
import { Waypoint } from "$lib/models/waypoint";
import { categories } from "$lib/stores/category_store";
import {
lists,
lists_add_trail,
lists_index,
lists_remove_trail,
} from "$lib/stores/list_store";
import { summitLog } from "$lib/stores/summit_log_store";
import { show_toast } from "$lib/stores/toast_store";
import {
@@ -21,17 +29,19 @@
trails_update,
} from "$lib/stores/trail_store";
import { waypoint } from "$lib/stores/waypoint_store";
import { getFileURL } from "$lib/util/file_util";
import {
formatDistance,
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import { fromKML, fromTCX } from "$lib/util/gpx_util";
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
import "$lib/vendor/leaflet-elevation/src/index.css";
import { createForm } from "$lib/vendor/svelte-form-lib";
import cryptoRandomString from "crypto-random-string";
import { format } from "date-fns";
import type { GPX, Icon, LatLng, LeafletEvent, Map } from "leaflet";
import type { GPX, Icon, LeafletEvent, Map } from "leaflet";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
import "leaflet/dist/leaflet.css";
import { onMount } from "svelte";
@@ -47,6 +57,7 @@
let openWaypointModal: () => void;
let openSummitLogModal: () => void;
let openListSelectModal: () => void;
let loading = false;
@@ -281,7 +292,25 @@
reader.readAsText(selectedFile);
reader.onload = async function (e) {
await addGPXLayer(e.target?.result as string);
let gpxData = "";
const fileContent = e.target?.result as string;
if (fileContent.includes("http://www.opengis.net/kml")) {
gpxData = fromKML(e.target?.result as string);
} else if (fileContent.includes("TrainingCenterDatabase")) {
gpxData = fromTCX(e.target?.result as string);
} else {
gpxData = fileContent;
}
try {
await addGPXLayer(gpxData);
} catch (e) {
show_toast({
icon: "close",
type: "error",
text: $_("error-reading-file"),
});
return;
}
const r = await fetch("/api/v1/search/cities500", {
method: "POST",
body: JSON.stringify({
@@ -393,6 +422,28 @@
$form.expand.summit_logs = $form.expand.summit_logs;
}
}
async function handleListSelection(list: List) {
if (!$form.id) {
return;
}
try {
if (list.trails?.includes($form.id!)) {
await lists_remove_trail(list, $form);
} else {
await lists_add_trail(list, $form);
}
await lists_index();
} catch (e) {
console.error(e);
show_toast({
type: "error",
icon: "close",
text: "Error adding trail to list.",
});
}
}
</script>
<svelte:head>
@@ -415,7 +466,7 @@
type="file"
name="gpx"
id="fileInput"
accept=".gpx"
accept=".gpx,.kml,.tcx"
style="display: none;"
on:change={handleFileSelection}
/>
@@ -570,6 +621,43 @@
on:click={beforeSummitLogModalOpen}
><i class="fa fa-plus mr-2"></i>{$_("add-entry")}</button
>
{#if $lists.length}
<hr class="border-separator" />
<h3 class="text-xl font-semibold">
{$_("list", { values: { n: 2 } })}
</h3>
<div class="flex gap-4 flex-wrap">
{#each $lists as list}
{#if $form.id && list.trails?.includes($form.id)}
<div
class="flex gap-2 items-center border border-input-border rounded-xl p-2"
>
{#if list.avatar}
<img
class="w-8 aspect-square rounded-full object-cover"
src={getFileURL(list, list.avatar)}
alt="avatar"
/>
{:else}
<div
class="flex w-4 aspect-square shrink-0 items-center justify-center"
>
<i class="fa fa-table-list text-5xl"></i>
</div>
{/if}
<span class="text-sm">{list.name}</span>
</div>
{/if}
{/each}
</div>
<Button
secondary={true}
disabled={!$form.id}
type="button"
on:click={openListSelectModal}
><i class="fa fa-plus mr-2"></i>{$_("add-to-list")}</Button
>
{/if}
<hr class="border-separator" />
<Button
primary={true}
@@ -587,6 +675,10 @@
></WaypointModal>
<SummitLogModal bind:openModal={openSummitLogModal} on:save={saveSummitLog}
></SummitLogModal>
<ListSelectModal
bind:openModal={openListSelectModal}
on:change={(e) => handleListSelection(e.detail)}
></ListSelectModal>
<style>
#map {

View File

@@ -1,5 +1,6 @@
import { Trail } from "$lib/models/trail";
import { categories_index } from "$lib/stores/category_store";
import { lists_index } from "$lib/stores/list_store";
import { trails_show } from "$lib/stores/trail_store";
import { error, type Load } from "@sveltejs/kit";
@@ -8,6 +9,7 @@ export const load: Load = async ({ params, fetch }) => {
return error(400, "Bad Request")
}
const categories = await categories_index(fetch)
const lists = await lists_index(undefined, fetch)
let trail: Trail;
if (params.id === "new") {