adds route editing

This commit is contained in:
Christian Beutel
2024-04-22 22:37:13 +02:00
parent 9af7ddeffd
commit 8d7ed16904
19 changed files with 744 additions and 132 deletions

View File

@@ -38,7 +38,7 @@
map = L.map("map", { preferCanvas: true }).setView(
[trail?.lat ?? 0, trail?.lon ?? 0],
3,
16,
);
map!.attributionControl.setPrefix(false);

View File

@@ -0,0 +1,12 @@
export default class Bounds {
minlat: number;
minlon: number;
maxlat: number;
maxlon: number;
constructor(object: {minlat: number, minlon: number, maxlat: number, maxlon: number}) {
this.minlat = object.minlat;
this.minlon = object.minlon;
this.maxlat = object.maxlat;
this.maxlon = object.maxlon;
}
}

View File

@@ -0,0 +1,10 @@
export default class Copyright {
author: string;
year: number;
license: string;
constructor(object: { author: string, year: number, license: string }) {
this.author = object.author;
this.year = object.year;
this.license = object.license;
}
}

View File

@@ -0,0 +1,101 @@
import * as xml2js from 'isomorphic-xml2js';
import Metadata from './metadata';
import Waypoint from './waypoint';
import Route from './route';
import Track from './track';
import { removeEmpty, allDatesToISOString } from './utils';
const defaultAttributes = {
version: '1.1',
creator: 'wanderer',
xmlns: 'http://www.topografix.com/GPX/1/1',
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xsi:schemaLocation':
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd'
}
export default class GPX {
$: {
version: string;
creator: string;
xmlns: string;
'xmlns:xsi': string;
'xsi:schemaLocation': string;
}
extensions?: string;
metadata?: Metadata;
wpt?: Waypoint[];
rte?: Route[];
trk?: Track[];
constructor(object: {
$?: {
version: string,
creator: string,
xmlns: string,
'xmlns:xsi': string,
'xsi:schemaLocation': string,
}
extensions?: string,
metadata?: Metadata,
wpt?: Waypoint[] | Waypoint,
rte?: Route[] | Route,
trk?: Track[],
}) {
this.$ = Object.assign({}, defaultAttributes, object.$ || {});
if (object.extensions) {
this.extensions = object.extensions;
}
if (object.metadata) {
this.metadata = object.metadata;
}
if (object.wpt) {
if (!Array.isArray(object.wpt)) {
object.wpt = [object.wpt];
}
this.wpt = object.wpt.map(wpt => new Waypoint(wpt))
}
if (object.rte) {
if (!Array.isArray(object.rte)) {
object.rte = [object.rte];
}
this.rte = object.rte.map(rte => new Route(rte))
}
if (object.trk) {
if (!Array.isArray(object.trk)) {
object.trk = [object.trk];
}
this.trk = object.trk.map(trk => new Track(trk))
}
removeEmpty(this);
}
static parse(gpxString: string): Promise<GPX | Error> {
return new Promise<GPX | Error>((resolve, reject) => xml2js.parseString(gpxString, {
explicitArray: false
}, (err, xml) => {
if (err) {
reject(err);
}
const gpx = new GPX({
$: xml.gpx.$,
metadata: xml.gpx.metadata,
wpt: xml.gpx.wpt,
rte: xml.gpx.rte,
trk: xml.gpx.trk
});
resolve(gpx)
}));
}
toString(options?: xml2js.BuilderOptions) {
options = options || {};
options.rootName = 'gpx';
const builder = new xml2js.Builder(options), gpx = new GPX(this);
allDatesToISOString(gpx);
return builder.buildObject(gpx);
}
}

View File

@@ -0,0 +1,13 @@
export default class Link {
$: {
href?: string;
}
text: string;
type: string;
constructor(object: any) {
this.$ = {};
this.$.href = object.$.href || object.href;
this.text = object.text;
this.type = object.type;
}
}

View File

@@ -0,0 +1,48 @@
import Copyright from './copyright';
import Link from './link';
import Person from './person';
import Bounds from './bounds';
export default class Metadata {
name: string;
desc: string;
time: Date;
keywords: string;
extensions: string;
author?: Person;
link?: Link[];
bounds?: Bounds;
copyright?: Copyright;
constructor(object: {
name: string,
desc: string,
time: string,
keywords: string,
extensions: string,
author?: Person,
link?: Link | Link[],
bounds?: Bounds,
copyright?: Copyright
}) {
this.name = object.name;
this.desc = object.desc;
this.time = object.time ? new Date(object.time) : new Date();
this.keywords = object.keywords;
this.extensions = object.extensions;
if (object.author) {
this.author = new Person(object.author);
}
if (object.link) {
if (!Array.isArray(object.link)) {
object.link = [object.link];
}
this.link = object.link.map(l => new Link(l));
}
if (object.bounds) {
this.bounds = new Bounds(object.bounds);
}
if (object.copyright) {
this.copyright = new Copyright(object.copyright);
}
}
}

View File

@@ -0,0 +1,14 @@
import Link from './link';
export default class Person {
name: string;
email: string;
link?: Link;
constructor(object: { name: string, email: string, link?: Link }) {
this.name = object.name;
this.email = object.email;
if (object.link) {
this.link = new Link(object.link);
}
}
}

View File

@@ -0,0 +1,45 @@
import Waypoint from './waypoint';
import Link from './link';
export default class Route {
name: string;
cmt: string;
desc: string;
src: string;
number: number;
type: string;
extensions: string;
link?: Link[];
rtept?: Waypoint[];
constructor(object: {
name: string,
cmt: string, desc: string,
src: string,
number: number,
type: string,
extensions: string,
link?: Link | Link[],
rtept?: Waypoint | Waypoint[]
}) {
this.name = object.name;
this.cmt = object.cmt;
this.desc = object.desc;
this.src = object.src;
this.number = object.number;
this.type = object.type;
this.extensions = object.extensions;
if (object.link) {
if (!Array.isArray(object.link)) {
object.link = [object.link];
}
this.link = object.link.map(l => new Link(l));
}
if (object.rtept) {
if (!Array.isArray(object.rtept)) {
this.rtept = [object.rtept];
}
this.rtept = (object.rtept as Waypoint[]).map(rtept => new Waypoint(rtept))
}
}
}

View File

@@ -0,0 +1,15 @@
import Waypoint from './waypoint';
export default class TrackSegment {
trkpt?: Waypoint[];
extensions?: string;
constructor(object: { trkpt?: Waypoint[], extensions?: string }) {
if (object.trkpt) {
if (!Array.isArray(object.trkpt)) {
object.trkpt = [object.trkpt];
}
this.trkpt = object.trkpt.map(trkpt => new Waypoint(trkpt))
}
this.extensions = object.extensions;
}
}

View File

@@ -0,0 +1,45 @@
import TrackSegment from './track-segment';
import Link from './link';
export default class Track {
name?: string;
cmt?: string;
desc?: string;
src?: string;
number?: number;
type?: string;
extensions?: string;
link?: Link[];
trkseg?: TrackSegment[];
constructor(object: {
name?: string,
cmt?: string,
desc?: string,
src?: string,
number?: number,
type?: string,
extensions?: string,
link?: Link | Link[],
trkseg?: TrackSegment | TrackSegment[]
}) {
this.name = object.name;
this.cmt = object.cmt;
this.desc = object.desc;
this.src = object.src;
this.number = object.number;
this.type = object.type;
this.extensions = object.extensions;
if (object.link) {
if (!Array.isArray(object.link)) {
object.link = [object.link];
}
this.link = object.link.map(l => new Link(l));
}
if (object.trkseg) {
if (!Array.isArray(object.trkseg)) {
object.trkseg = [object.trkseg];
}
this.trkseg = object.trkseg.map(trkseg => new TrackSegment(trkseg));;
}
}
}

View File

@@ -0,0 +1,23 @@
function removeEmpty(obj: Record<string, any>) {
Object.entries(obj).forEach(([key, val]) => {
if (val && val instanceof Object) {
removeEmpty(val);
} else if (val == null) {
delete obj[key];
}
});
}
function allDatesToISOString(obj: Record<string, any>) {
Object.entries(obj).forEach(([key, val]) => {
if (val) {
if (val instanceof Date) {
obj[key] = val.toISOString().split('.')[0] + 'Z';
} else if (val instanceof Object) {
allDatesToISOString(val);
}
}
});
}
export { removeEmpty, allDatesToISOString };

View File

@@ -0,0 +1,81 @@
import Link from './link';
export default class Waypoint {
$: {
lat?: number;
lon?: number;
}
ele?: number;
time?: Date;
magvar?: string;
geoidheight?: string;
name?: string;
cmt?: string
desc?: string;
src?: string;
sym?: string;
type?: string;
sat?: string;
hdop?: string;
vdop?: string;
pdop?: string;
ageofdgpsdata?: string;
dgpsid?: string;
extensions?: string;
link?: Link[];
constructor(object: {
lat?: number,
lon?: number,
$: {
lat?: number,
lon?: number
},
ele?: number,
time?: Date,
magvar?: string,
geoidheight?: string,
name?: string,
cmt?: string,
desc?: string,
src?: string,
sym?: string,
type?: string,
sat?: string,
hdop?: string,
vdop?: string,
pdop?: string,
ageofdgpsdata?: string,
dgpsid?: string,
extensions?: string,
link?: Link[]
}) {
this.$ = {};
this.$.lat = object.$.lat === 0 || object.lat === 0 ? 0 : object.$.lat || object.lat || -1;
this.$.lon = object.$.lon === 0 || object.lon === 0 ? 0 : object.$.lon || object.lon || -1;
this.ele = object.ele;
if (object.time) {
this.time = new Date(object.time);
}
this.magvar = object.magvar;
this.geoidheight = object.geoidheight;
this.name = object.name;
this.cmt = object.cmt;
this.desc = object.desc;
this.src = object.src;
this.sym = object.sym;
this.type = object.type;
this.sat = object.sat;
this.hdop = object.hdop;
this.vdop = object.vdop;
this.pdop = object.pdop;
this.ageofdgpsdata = object.ageofdgpsdata;
this.dgpsid = object.dgpsid;
this.extensions = object.extensions;
if (object.link) {
if (!Array.isArray(object.link)) {
object.link = [object.link];
}
this.link = object.link.map(l => new Link(l));
}
}
}

View File

@@ -1,31 +1,9 @@
interface ValhallaResponse {
trip: {
legs: {
shape: string[];
shape: string;
}[];
};
}
class ValhallaRoute {
type: "Feature";
properties: {};
geometry: {
type: "LineString";
coordinates: number[][];
};
constructor() {
this.type = "Feature"
this.properties = {}
this.geometry = {
type: "LineString",
coordinates: []
}
}
toString() {
return JSON.stringify(this);
}
}
export {type ValhallaResponse, ValhallaRoute}
export { type ValhallaResponse }

View File

@@ -1,13 +1,17 @@
import { ValhallaRoute, type ValhallaResponse } from "$lib/models/valhalla";
import GPX from "$lib/models/gpx/gpx";
import type Track from "$lib/models/gpx/track";
import TrackSegment from "$lib/models/gpx/track-segment";
import Waypoint from "$lib/models/gpx/waypoint";
import { type ValhallaResponse } from "$lib/models/valhalla";
import { ClientResponseError } from "pocketbase";
export const route: ValhallaRoute = new ValhallaRoute();
const emtpyTrack: Track = { trkseg: [] }
export let route: GPX = new GPX({ trk: [emtpyTrack] });
export function clearRoute() {
route.geometry.coordinates = [];
route = new GPX({ trk: [emtpyTrack] });
}
export async function calculateRouteBetween(startLat: number, startLon: number, endLat: number, endLon: number) {
@@ -21,16 +25,30 @@ export async function calculateRouteBetween(startLat: number, startLon: number,
throw new ClientResponseError(await r.json())
}
const response: ValhallaResponse = await r.json();
const points = decodeShape(response.trip.legs[0].shape);
const waypoints = points.map((p) => new Waypoint({ $: { lat: p[0], lon: p[1] } }))
appendToRoute(response);
return waypoints
}
function appendToRoute(valhallaResponse: any) {
const routeGeometry = decodeShape(valhallaResponse.trip.legs[0].shape);
export async function appendToRoute(waypoints: Waypoint[]) {
const segment = new TrackSegment({ trkpt: [] })
for (const point of routeGeometry) {
route.geometry.coordinates.push([point[1], point[0]])
for (const wpt of waypoints) {
segment.trkpt!.push(wpt)
}
route.trk?.at(0)?.trkseg?.push(segment);
}
export async function editRoute(index: number, waypoints: Waypoint[]) {
const segment = route.trk?.at(0)?.trkseg?.at(index)
if (segment) {
segment.trkpt = waypoints
}
}
export function deleteFromRoute(index: number) {
route.trk?.at(0)?.trkseg?.splice(index, 1);
}

View File

@@ -1,8 +1,8 @@
import GPX from "$lib/models/gpx/gpx";
import { Trail } from "$lib/models/trail";
import { Waypoint } from "$lib/models/waypoint";
import { kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON"
import GeoJsonToGpx from "$lib/vendor/geoJSONToGPX"
import { browser } from "$app/environment";
import GeoJsonToGpx from "$lib/vendor/geoJSONToGPX";
import { kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON";
function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371; // Radius of the Earth in km
@@ -17,38 +17,23 @@ function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: numbe
return distance;
}
export async function gpx2trail(gpx: string) {
let xml;
if (browser) {
const parser = new DOMParser();
xml = parser.parseFromString(gpx, "application/xml")
} else {
const JSDOM = (await import("jsdom")).JSDOM
xml = new JSDOM(gpx).window.document
export async function gpx2trail(gpxString: string) {
const gpx = await GPX.parse(gpxString);
if (gpx instanceof Error) {
throw gpx;
}
const trail = new Trail("");
let name = xml.getElementsByTagName('name');
if (name.length > 0) {
trail.name = name[0].textContent ?? "";
}
let desc = xml.getElementsByTagName('desc');
if (desc.length > 0) {
trail.description = desc[0].textContent ?? "";
}
trail.name = gpx.metadata?.name ?? "";
const el = xml.getElementsByTagName('wpt');
const elLength = el.length
for (let i = 0; i < elLength; i++) {
const wp = new Waypoint(parseFloat(el[i].getAttribute('lat')!), parseFloat(el[i].getAttribute('lon')!));
let nameEl = el[i].getElementsByTagName('name');
wp.name = nameEl.length > 0 ? nameEl[0].textContent ?? "" : '';
let descEl = el[i].getElementsByTagName('desc');
wp.description = descEl.length > 0 ? descEl[0].textContent ?? "" : '';
trail.description = gpx.metadata?.desc;
for (const wpt of gpx.wpt ?? []) {
const wp = new Waypoint(wpt.$.lat ?? 0, wpt.$.lon ?? 0);
wp.name = wp.name
wp.description = wp.description;
trail.expand.waypoints.push(wp);
}
@@ -56,17 +41,13 @@ export async function gpx2trail(gpx: string) {
let totalDuration = 0;
let totalDistance = 0;
const tracks = xml.getElementsByTagName("trk")
for (const track of tracks) {
const segments = track.getElementsByTagName("trkseg")
for (const segment of segments) {
const points = segment.getElementsByTagName("trkpt")
for (const track of gpx.trk ?? []) {
for (const segment of track.trkseg ?? []) {
const points = segment.trkpt ?? [];
if (points.length >= 2) {
const startTimeString = points[0].querySelector('time')?.textContent;
const startTime = startTimeString ? new Date(startTimeString) : undefined;
const endTimeString = points[points.length - 1].querySelector('time')?.textContent;
const endTime = endTimeString ? new Date(endTimeString) : undefined;
const startTime = points[0].time;
const endTime = points[points.length - 1].time
if (startTime && endTime) {
totalDuration += endTime.getTime() - startTime.getTime();
@@ -79,30 +60,30 @@ export async function gpx2trail(gpx: string) {
const pointLength = points.length
for (let i = 1; i < pointLength; i++) {
const elevation = parseFloat(points[i].querySelector('ele')?.textContent || '0')
const previousElevation = parseFloat(points[i - 1].querySelector('ele')?.textContent || '0')
const prevPoint = points[i - 1];
const point = points[i];
const elevation = point.ele ?? 0
const previousElevation = prevPoint.ele ?? 0
const elevationDiff = elevation - previousElevation;
if (elevationDiff > 0) {
totalElevationGain += elevationDiff;
}
const prevPoint = points[i - 1];
const point = points[i];
const distance = calculateDistance(
parseFloat(prevPoint.getAttribute("lat") || '0'),
parseFloat(prevPoint.getAttribute("lon") || '0'),
parseFloat(point.getAttribute("lat") || '0'),
parseFloat(point.getAttribute("lon") || '0')
prevPoint.$.lat ?? 0,
prevPoint.$.lon ?? 0,
point.$.lat ?? 0,
point.$.lon ?? 0,
);
totalDistance += distance;
}
}
}
const startPoint = xml.getElementsByTagName("trk")[0].getElementsByTagName("trkseg")[0].getElementsByTagName("trkpt")[0];
const startPoint = gpx.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0);
if (startPoint) {
trail.lat = parseFloat(startPoint.getAttribute('lat')!)
trail.lon = parseFloat(startPoint.getAttribute('lon')!)
trail.lat = startPoint.$.lat
trail.lon = startPoint.$.lon
}
trail.duration = totalDuration / 1000 / 60