284 lines
9.5 KiB
TypeScript
284 lines
9.5 KiB
TypeScript
import GPX from "$lib/models/gpx/gpx";
|
|
import Track from "$lib/models/gpx/track";
|
|
import TrackSegment from "$lib/models/gpx/track-segment";
|
|
import { haversineDistance } from "$lib/models/gpx/utils";
|
|
import Waypoint from "$lib/models/gpx/waypoint";
|
|
import { type RoutingOptions, type ValhallaAnchor, type ValhallaHeightResponse, type ValhallaRouteResponse } from "$lib/models/valhalla";
|
|
import { APIError } from "$lib/util/api_util";
|
|
import { decodePolyline, encodePolyline } from "$lib/util/polyline_util";
|
|
import { applyChangeset, diff, revertChangeset, type Changeset } from 'json-diff-ts';
|
|
import type { LngLat } from "maplibre-gl";
|
|
import { _ } from "svelte-i18n";
|
|
import { get } from "svelte/store";
|
|
|
|
const emtpyTrack = new Track({ trkseg: [] })
|
|
|
|
class ValhallaStore {
|
|
route: GPX = $state(new GPX({ trk: [emtpyTrack] }));
|
|
anchors: ValhallaAnchor[] = $state([]);
|
|
undoStack: { delta: Changeset, reverseDelta: Changeset }[] = $state([]);
|
|
redoStack: { delta: Changeset, reverseDelta: Changeset }[] = $state([]);
|
|
}
|
|
|
|
export const valhallaStore = new ValhallaStore();
|
|
|
|
export function clearRoute() {
|
|
valhallaStore.route = new GPX({ trk: [emtpyTrack] });
|
|
}
|
|
|
|
export function clearAnchors() {
|
|
for (const anchor of valhallaStore.anchors) {
|
|
anchor.marker?.remove();
|
|
}
|
|
valhallaStore.anchors = [];
|
|
}
|
|
|
|
export function clearUndoRedoStack() {
|
|
valhallaStore.undoStack = []
|
|
valhallaStore.redoStack = []
|
|
}
|
|
|
|
function pushToUndoStack(delta: Changeset, reverseDelta: Changeset) {
|
|
valhallaStore.undoStack.push({ delta, reverseDelta })
|
|
valhallaStore.redoStack = []
|
|
}
|
|
|
|
|
|
export function setRoute(newRoute: GPX, undoable: boolean = false) {
|
|
const delta = diff(valhallaStore.route, newRoute);
|
|
const reverseDelta = diff(newRoute, valhallaStore.route);
|
|
valhallaStore.route = applyChangeset(valhallaStore.route, delta);
|
|
if (undoable) {
|
|
pushToUndoStack(delta, reverseDelta)
|
|
}
|
|
|
|
}
|
|
|
|
export async function calculateRouteBetween(startLat: number, startLon: number, endLat: number, endLon: number, options: RoutingOptions) {
|
|
|
|
let shape;
|
|
let duration: number;
|
|
if (options.autoRouting) {
|
|
let costingBody;
|
|
switch (options.modeOfTransport) {
|
|
case "bicycle":
|
|
costingBody = { "costing": options.modeOfTransport, "costing_options": { [options.modeOfTransport]: options.bicycleOptions } }
|
|
break;
|
|
case "auto":
|
|
costingBody = { "costing": options.modeOfTransport, "costing_options": { [options.modeOfTransport]: options.autoOptions } }
|
|
break;
|
|
case "pedestrian":
|
|
costingBody = { "costing": options.modeOfTransport, "costing_options": { [options.modeOfTransport]: options.pedestrianOptions } }
|
|
break;
|
|
}
|
|
const requestBody = {
|
|
"directions_type": "none",
|
|
"locations": [{ "lat": startLat, "lon": startLon }, { "lat": endLat, "lon": endLon }],
|
|
...costingBody
|
|
}
|
|
|
|
let r = await fetch("/api/v1/valhalla/route", { method: "POST", body: JSON.stringify(requestBody) })
|
|
|
|
if (!r.ok) {
|
|
const response = await r.json();
|
|
throw new APIError(r.status, response.message, response.detail)
|
|
}
|
|
|
|
const routeResponse: ValhallaRouteResponse = await r.json();
|
|
shape = routeResponse.trip.legs[0].shape
|
|
duration = routeResponse.trip.summary.time
|
|
} else {
|
|
shape = encodePolyline([[startLat, startLon], [endLat, endLon]])
|
|
duration = 0;
|
|
}
|
|
|
|
const r2 = await fetch("/api/v1/valhalla/height", { method: "POST", body: JSON.stringify({ encoded_polyline: shape }) })
|
|
|
|
if (!r2.ok) {
|
|
const response = await r2.json();
|
|
throw new APIError(r2.status, response.message, response.detail)
|
|
}
|
|
|
|
const heightResponse: ValhallaHeightResponse = await r2.json()
|
|
const points = decodePolyline(shape);
|
|
const startTime = new Date().getTime();
|
|
|
|
const waypoints = points.map((p, i) => new Waypoint({ $: { lat: p[1], lon: p[0] }, ele: heightResponse.height[i], time: new Date(startTime + (((duration * 1000) / points.length) * i)) }))
|
|
|
|
return waypoints
|
|
}
|
|
|
|
export async function insertIntoRoute(waypoints: Waypoint[], index?: number) {
|
|
const snapshot = new GPX({ ...valhallaStore.route })
|
|
const segment = new TrackSegment({ trkpt: waypoints })
|
|
|
|
if (index) {
|
|
snapshot.trk?.at(0)?.trkseg?.splice(index, 0, segment);
|
|
} else {
|
|
snapshot.trk?.at(0)?.trkseg?.push(segment);
|
|
}
|
|
|
|
const delta = diff(valhallaStore.route, snapshot);
|
|
const reverseDelta = diff(snapshot, valhallaStore.route);
|
|
valhallaStore.route = applyChangeset(valhallaStore.route, delta);
|
|
pushToUndoStack(delta, reverseDelta)
|
|
|
|
valhallaStore.route.features = valhallaStore.route.getTotals();
|
|
}
|
|
|
|
export async function editRoute(index: number, waypoints: Waypoint[]) {
|
|
const snapshot = new GPX({ ...valhallaStore.route })
|
|
|
|
const segment = snapshot.trk?.at(0)?.trkseg?.at(index)
|
|
if (segment) {
|
|
segment.trkpt = waypoints
|
|
}
|
|
|
|
const delta = diff(valhallaStore.route, snapshot);
|
|
const reverseDelta = diff(snapshot, valhallaStore.route)
|
|
valhallaStore.route = applyChangeset(valhallaStore.route, delta);
|
|
pushToUndoStack(delta, reverseDelta)
|
|
|
|
|
|
|
|
valhallaStore.route.features = valhallaStore.route.getTotals();
|
|
}
|
|
|
|
export function deleteFromRoute(index: number) {
|
|
const snapshot = new GPX({ ...valhallaStore.route })
|
|
|
|
snapshot.trk?.at(0)?.trkseg?.splice(index, 1);
|
|
snapshot.features = valhallaStore.route.getTotals();
|
|
|
|
const delta = diff(valhallaStore.route, snapshot);
|
|
const reverseDelta = diff(snapshot, valhallaStore.route)
|
|
valhallaStore.route = applyChangeset(valhallaStore.route, delta);
|
|
pushToUndoStack(delta, reverseDelta)
|
|
}
|
|
|
|
export function reverseRoute() {
|
|
const snapshot = new GPX({ ...valhallaStore.route })
|
|
for (const trk of snapshot.trk ?? []) {
|
|
for (const seg of trk.trkseg ?? []) {
|
|
seg.trkpt?.reverse()
|
|
}
|
|
trk.trkseg?.reverse()
|
|
}
|
|
snapshot.trk?.reverse()
|
|
|
|
const delta = diff(valhallaStore.route, snapshot);
|
|
const reverseDelta = diff(snapshot, valhallaStore.route);
|
|
valhallaStore.route = applyChangeset(valhallaStore.route, delta);
|
|
pushToUndoStack(delta, reverseDelta)
|
|
|
|
valhallaStore.route.features = valhallaStore.route.getTotals();
|
|
|
|
valhallaStore.anchors.reverse();
|
|
|
|
valhallaStore.anchors.forEach((a, i) => {
|
|
if (!a.marker) {
|
|
return;
|
|
}
|
|
a.marker.getElement().textContent = "" + (i + 1);
|
|
|
|
const anchorPopupHeading = a.marker
|
|
.getPopup()
|
|
._content.getElementsByTagName("h5")[0];
|
|
if (anchorPopupHeading) {
|
|
anchorPopupHeading.textContent =
|
|
get(_)("route-point") + " #" + (i + 1);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function resetRoute() {
|
|
const delta = diff(valhallaStore.route, new GPX({ trk: [new Track({ ...emtpyTrack })] }));
|
|
const reverseDelta = diff(new GPX({ trk: [new Track({ ...emtpyTrack })] }), valhallaStore.route);
|
|
valhallaStore.route = applyChangeset(valhallaStore.route, delta);
|
|
pushToUndoStack(delta, reverseDelta)
|
|
|
|
valhallaStore.anchors.forEach((a) => {
|
|
if (!a.marker) {
|
|
return;
|
|
}
|
|
|
|
a.marker.remove();
|
|
})
|
|
|
|
valhallaStore.anchors = []
|
|
}
|
|
|
|
export async function recalculateHeight() {
|
|
await valhallaStore.route.correctElevation();
|
|
}
|
|
|
|
export async function splitSegment(index: number, pos: LngLat) {
|
|
let seg = valhallaStore.route.trk?.at(0)?.trkseg?.at(index);
|
|
if (!seg || !seg.trkpt) {
|
|
return;
|
|
}
|
|
const points = seg.trkpt;
|
|
|
|
let bestSplitIndex: number = 0
|
|
let minDistance = Infinity
|
|
for (let i = 1; i < points.length; i++) {
|
|
const pt = points[i]
|
|
const dist = haversineDistance(pt.$.lat!, pt.$.lon!, pos.lat, pos.lng);
|
|
|
|
if (dist < minDistance) {
|
|
bestSplitIndex = i
|
|
minDistance = dist
|
|
}
|
|
}
|
|
|
|
const intersectionPoint = new Waypoint({ ...points[bestSplitIndex], $: { lat: pos.lat, lon: pos.lng } });
|
|
const firstSegmentPoints = [...points.slice(0, bestSplitIndex), intersectionPoint];
|
|
const secondSegmentPoints = [intersectionPoint, ...points.slice(bestSplitIndex)];
|
|
|
|
editRoute(index, firstSegmentPoints)
|
|
insertIntoRoute(secondSegmentPoints, index + 1)
|
|
|
|
}
|
|
|
|
export function normalizeRouteTime() {
|
|
let currentTime = new Date();
|
|
|
|
for (const seg of valhallaStore.route.trk?.at(0)?.trkseg ?? []) {
|
|
|
|
if (!seg.trkpt?.length) {
|
|
continue
|
|
}
|
|
const baseTime = seg.trkpt[0].time?.getTime() ?? 0;
|
|
for (let i = 0; i < seg.trkpt.length; i++) {
|
|
const wp = seg.trkpt![i];
|
|
const offset = (wp.time?.getTime() ?? 0) - baseTime;
|
|
const adjustedTime = new Date(currentTime.getTime() + offset);
|
|
|
|
wp.time = adjustedTime
|
|
}
|
|
currentTime = new Date(seg.trkpt[seg.trkpt.length - 1].time!.getTime());
|
|
}
|
|
}
|
|
|
|
export function undo() {
|
|
const historyItem = valhallaStore.undoStack.pop()
|
|
if (!historyItem) {
|
|
return
|
|
}
|
|
valhallaStore.redoStack.push(historyItem)
|
|
|
|
valhallaStore.route = applyChangeset(valhallaStore.route, historyItem.reverseDelta);
|
|
valhallaStore.route.features = valhallaStore.route.getTotals();
|
|
}
|
|
|
|
export function redo() {
|
|
const historyItem = valhallaStore.redoStack.pop()
|
|
if (!historyItem) {
|
|
return
|
|
}
|
|
valhallaStore.undoStack.push(historyItem)
|
|
|
|
valhallaStore.route = applyChangeset(valhallaStore.route, historyItem.delta);
|
|
valhallaStore.route.features = valhallaStore.route.getTotals();
|
|
}
|