This commit is contained in:
vcoppe committed 2026-10-07 17:58:17 +02:00
1 parent 29850c1c8f
commit 594dd8979a
35 files changed
+3135 -1171

No files matched your search

@@ -24,23 +24,15 @@
import { routingProfiles } from '$lib/components/toolbar/tools/routing/routing';
import { i18n } from '$lib/i18n.svelte';
import { slide } from 'svelte/transition';
import {
ListFileItem,
ListRootItem,
ListTrackItem,
ListTrackSegmentItem,
type ListItem,
} from '$lib/components/file-list/file-list';
import { get } from 'svelte/store';
import { getURLForLanguage } from '$lib/utils';
import { onDestroy, onMount } from 'svelte';
import { TrackPoint } from 'gpx';
import { settings } from '$lib/logic/settings';
import { map } from '$lib/components/map/map';
import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state';
import { selection } from '$lib/logic/selection';
import { fileActions, getFileIds, newGPXFile } from '$lib/logic/file-actions';
import { engine } from '$lib/engine';
import { fileActions, newFileName } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { RoutingControls, routingControls } from './routing-controls';
import { RoutingControls } from './routing-controls';
let {
minimized = $bindable(false),
@@ -58,52 +50,24 @@
const { privateRoads, routing, routingProfile } = settings;
let fileStateCollectionObserver: GPXFileStateCollectionObserver;
const selection = engine.selection;
let routingControls: RoutingControls | undefined = undefined;
// the tool works on files, tracks and segments, not on waypoints
let validSelection = $derived(
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
$selection.type === 'file' || $selection.type === 'track' || $selection.type === 'segment'
);
function createFileWithPoint(e: any) {
if ($selection.size === 0) {
let file = newGPXFile();
file.replaceTrackPoints(0, 0, 0, 0, [
new TrackPoint({
attributes: {
lat: e.lngLat.lat,
lon: e.lngLat.lng,
},
}),
]);
file._data.id = getFileIds(1)[0];
fileActions.add(file);
selection.selectFileWhenLoaded(file._data.id);
if ($selection.type === 'empty') {
// the engine selects the new file, which starts with the trackpoint: one undo step
engine.newFile(newFileName(), { lng: e.lngLat.lng, lat: e.lngLat.lat });
}
}
onMount(() => {
if ($map && popup && popupElement) {
fileStateCollectionObserver = new GPXFileStateCollectionObserver(
(newFiles) => {
newFiles.forEach((fileState, fileId) => {
routingControls.set(
fileId,
new RoutingControls(fileId, fileState, popup, popupElement)
);
});
},
(fileId) => {
const controls = routingControls.get(fileId);
if (controls) {
controls.destroy();
routingControls.delete(fileId);
}
},
() => {
routingControls.forEach((controls) => controls.destroy());
routingControls.clear();
}
);
routingControls = new RoutingControls(popup, popupElement);
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, true);
$map.on('click', createFileWithPoint);
@@ -112,9 +76,8 @@
onDestroy(() => {
if ($map) {
if (fileStateCollectionObserver) {
fileStateCollectionObserver.destroy();
}
routingControls?.destroy();
routingControls = undefined;
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
$map.off('click', createFileWithPoint);
@@ -203,31 +166,10 @@
class="gap-1 text-xs px-1.5 py-1.5 h-fit"
disabled={!validSelection}
onclick={() => {
const selected = selection.getOrderedSelection();
if (selected.length > 0) {
const firstFileId = selected[0].getFileId();
const firstFile = fileStateCollection.getFile(firstFileId);
if (firstFile) {
let start = (() => {
if (selected[0] instanceof ListFileItem) {
return firstFile.trk[0]?.trkseg[0]?.trkpt[0];
} else if (selected[0] instanceof ListTrackItem) {
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[0]
?.trkpt[0];
} else if (selected[0] instanceof ListTrackSegmentItem) {
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[
selected[0].getSegmentIndex()
]?.trkpt[0];
}
})();
if (start !== undefined) {
const lastFileId = selected[selected.length - 1].getFileId();
routingControls
.get(lastFileId)
?.appendAnchorWithCoordinates(start.getCoordinates());
}
}
// the first trackpoint of the selection
const { length, lng, lat } = get(engine.statistics);
if (length > 0) {
routingControls?.appendAnchorWithCoordinates({ lng: lng[0], lat: lat[0] });
}
}}
>
File diff suppressed because it is too large. Load diff
@@ -1,5 +1,4 @@
import type { Coordinates } from 'gpx';
import { TrackPoint, distance } from 'gpx';
import { distance, type Coordinates } from 'gpx';
import { settings } from '$lib/logic/settings';
import { getElevation } from '$lib/utils';
import { get } from 'svelte/store';
@@ -22,7 +21,36 @@ export const routingProfiles: { [key: string]: RoutingProfile } = {
railway: { engine: 'brouter', profile: 'rail' },
};
export function route(points: Coordinates[]): Promise<TrackPoint[]> {
/**
* The trackpoints of a route, as arrays with an entry per trackpoint. The OSM attributes are
* `undefined` where the router does not know them.
*/
export type RoutedPoints = {
lng: number[];
lat: number[];
ele: number[];
surface: (string | undefined)[];
highway: (string | undefined)[];
sacScale: (string | undefined)[];
mtbScale: (string | undefined)[];
};
function emptyRoute(): RoutedPoints {
return { lng: [], lat: [], ele: [], surface: [], highway: [], sacScale: [], mtbScale: [] };
}
/** Adds a trackpoint, which has the elevation of the previous one if it has none. */
function addPoint(route: RoutedPoints, lng: number, lat: number, ele: number | undefined) {
route.lng.push(lng);
route.lat.push(lat);
route.ele.push(ele ?? route.ele[route.ele.length - 1] ?? 0);
route.surface.push(undefined);
route.highway.push(undefined);
route.sacScale.push(undefined);
route.mtbScale.push(undefined);
}
export function route(points: Coordinates[]): Promise<RoutedPoints> {
if (get(routing)) {
const profile = routingProfiles[get(routingProfile)];
if (profile.engine === 'graphhopper') {
@@ -108,7 +136,7 @@ async function getGraphHopperRoute(
points: Coordinates[],
graphHopperProfile: string,
privateRoads: boolean
): Promise<TrackPoint[]> {
): Promise<RoutedPoints> {
let response = await fetch('https://graphhopper.gpx.studio/route', {
method: 'POST',
headers: {
@@ -147,42 +175,27 @@ async function getGraphHopperRoute(
let json = await response.json();
let route: TrackPoint[] = [];
let coordinates = json.paths[0].points.coordinates;
let details = json.paths[0].details;
const route = emptyRoute();
const coordinates = json.paths[0].points.coordinates;
const details = json.paths[0].details;
for (let i = 0; i < coordinates.length; i++) {
route.push(
new TrackPoint({
attributes: {
lat: coordinates[i][1],
lon: coordinates[i][0],
},
ele: coordinates[i][2] ?? (i > 0 ? route[i - 1].ele : 0),
extensions: {},
})
);
addPoint(route, coordinates[i][0], coordinates[i][1], coordinates[i][2]);
}
for (let key of graphhopperDetails) {
let detail = details[key];
for (const key of graphhopperDetails) {
const detail = details[key];
for (let i = 0; i < detail.length; i++) {
for (let j = detail[i][0]; j < detail[i][1] + (i == detail.length - 1); j++) {
for (let j = detail[i][0]; j < detail[i][1] + (i == detail.length - 1 ? 1 : 0); j++) {
if (detail[i][2] !== undefined && detail[i][2] !== 'missing') {
if (key === 'road_class') {
route[j].setExtension('highway', detail[i][2]);
route.highway[j] = detail[i][2];
} else if (key === 'hike_rating') {
const sacScale = hikeRatingToSACScale[detail[i][2]];
if (sacScale) {
route[j].setExtension('sac_scale', sacScale);
}
route.sacScale[j] = hikeRatingToSACScale[detail[i][2]];
} else if (key === 'mtb_rating') {
const mtbScale = mtbRatingToScale[detail[i][2]];
if (mtbScale) {
route[j].setExtension('mtb_scale', mtbScale);
}
route.mtbScale[j] = mtbRatingToScale[detail[i][2]];
} else if (key === 'surface' && detail[i][2] !== 'other') {
route[j].setExtension('surface', detail[i][2]);
route.surface[j] = detail[i][2];
}
}
}
@@ -195,7 +208,7 @@ async function getGraphHopperRoute(
async function getBRouterRoute(
points: Coordinates[],
brouterProfile: string
): Promise<TrackPoint[]> {
): Promise<RoutedPoints> {
let url = `https://brouter.de/brouter?lonlats=${points.map((point) => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile}&format=geojson&alternativeidx=0`;
let response = await fetch(url);
@@ -217,9 +230,9 @@ async function getBRouterRoute(
let geojson = await response.json();
let route: TrackPoint[] = [];
let coordinates = geojson.features[0].geometry.coordinates;
let messages = geojson.features[0].properties.messages;
const route = emptyRoute();
const coordinates = geojson.features[0].geometry.coordinates;
const messages = geojson.features[0].properties.messages;
const lngIdx = messages[0].indexOf('Longitude');
const latIdx = messages[0].indexOf('Latitude');
@@ -228,15 +241,7 @@ async function getBRouterRoute(
let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {};
for (let i = 0; i < coordinates.length; i++) {
route.push(
new TrackPoint({
attributes: {
lat: coordinates[i][1],
lon: coordinates[i][0],
},
ele: coordinates[i][2] ?? (i > 0 ? route[i - 1].ele : 0),
})
);
addPoint(route, coordinates[i][0], coordinates[i][1], coordinates[i][2]);
if (
messageIdx < messages.length &&
@@ -249,7 +254,10 @@ async function getBRouterRoute(
else tags = getTags(messages[messageIdx][tagIdx]);
}
route[route.length - 1].setExtensions(tags);
route.surface[i] = tags.surface;
route.highway[i] = tags.highway;
route.sacScale[i] = tags.sac_scale;
route.mtbScale[i] = tags.mtb_scale;
}
return route;
@@ -266,40 +274,27 @@ function getTags(message: string): { [key: string]: string } {
return tags;
}
function getIntermediatePoints(points: Coordinates[]): Promise<TrackPoint[]> {
let route: TrackPoint[] = [];
let step = 0.05;
function getIntermediatePoints(points: Coordinates[]): Promise<RoutedPoints> {
const route = emptyRoute();
const step = 0.05;
for (let i = 0; i < points.length - 1; i++) {
// Add intermediate points between each pair of points
let dist = distance(points[i], points[i + 1]) / 1000;
const dist = distance(points[i], points[i + 1]) / 1000;
for (let d = 0; d < dist; d += step) {
let lat = points[i].lat + (d / dist) * (points[i + 1].lat - points[i].lat);
let lon = points[i].lon + (d / dist) * (points[i + 1].lon - points[i].lon);
route.push(
new TrackPoint({
attributes: {
lat: lat,
lon: lon,
},
})
);
const lat = points[i].lat + (d / dist) * (points[i + 1].lat - points[i].lat);
const lon = points[i].lon + (d / dist) * (points[i + 1].lon - points[i].lon);
addPoint(route, lon, lat, 0);
}
}
route.push(
new TrackPoint({
attributes: {
lat: points[points.length - 1].lat,
lon: points[points.length - 1].lon,
},
})
);
const last = points[points.length - 1];
addPoint(route, last.lon, last.lat, 0);
return getElevation(route).then((elevations) => {
route.forEach((point, i) => {
point.ele = elevations[i];
});
return route;
});
return getElevation(route.lng.map((lng, i) => ({ lon: lng, lat: route.lat[i] }))).then(
(elevations) => {
route.ele = elevations;
return route;
}
);
}