2025-02-02 11:17:22 +01:00
|
|
|
import type { Coordinates } from 'gpx';
|
|
|
|
|
import { TrackPoint, distance } from 'gpx';
|
2025-10-17 23:54:45 +02:00
|
|
|
import { settings } from '$lib/logic/settings';
|
2025-02-02 11:17:22 +01:00
|
|
|
import { getElevation } from '$lib/utils';
|
2025-10-18 16:10:08 +02:00
|
|
|
import { get } from 'svelte/store';
|
2024-04-25 16:41:06 +02:00
|
|
|
|
2024-05-04 15:10:30 +02:00
|
|
|
const { routing, routingProfile, privateRoads } = settings;
|
|
|
|
|
|
2025-12-23 16:49:47 +01:00
|
|
|
export type RoutingProfile = {
|
|
|
|
|
engine: 'graphhopper' | 'brouter';
|
|
|
|
|
profile: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const routingProfiles: { [key: string]: RoutingProfile } = {
|
|
|
|
|
bike: { engine: 'graphhopper', profile: 'bike' },
|
|
|
|
|
racing_bike: { engine: 'graphhopper', profile: 'racingbike' },
|
|
|
|
|
gravel_bike: { engine: 'brouter', profile: 'gravel' },
|
|
|
|
|
mountain_bike: { engine: 'graphhopper', profile: 'mtb' },
|
|
|
|
|
foot: { engine: 'graphhopper', profile: 'foot' },
|
|
|
|
|
motorcycle: { engine: 'graphhopper', profile: 'motorcycle' },
|
|
|
|
|
water: { engine: 'brouter', profile: 'river' },
|
|
|
|
|
railway: { engine: 'brouter', profile: 'rail' },
|
2024-04-25 16:41:06 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function route(points: Coordinates[]): Promise<TrackPoint[]> {
|
2025-10-18 16:10:08 +02:00
|
|
|
if (get(routing)) {
|
2025-12-23 16:49:47 +01:00
|
|
|
const profile = routingProfiles[get(routingProfile)];
|
|
|
|
|
if (profile.engine === 'graphhopper') {
|
|
|
|
|
return getGraphHopperRoute(points, profile.profile, get(privateRoads));
|
|
|
|
|
} else {
|
|
|
|
|
return getBRouterRoute(points, profile.profile);
|
|
|
|
|
}
|
2024-04-23 18:36:16 +02:00
|
|
|
} else {
|
2024-05-09 00:02:27 +02:00
|
|
|
return getIntermediatePoints(points);
|
2024-04-23 18:36:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-23 16:49:47 +01:00
|
|
|
const graphhopperDetails = ['road_class', 'surface', 'hike_rating', 'mtb_rating'];
|
|
|
|
|
const hikeRatingToSACScale: { [key: string]: string } = {
|
|
|
|
|
'1': 'hiking',
|
|
|
|
|
'2': 'mountain_hiking',
|
|
|
|
|
'3': 'demanding_mountain_hiking',
|
|
|
|
|
'4': 'alpine_hiking',
|
|
|
|
|
'5': 'demanding_alpine_hiking',
|
|
|
|
|
'6': 'difficult_alpine_hiking',
|
|
|
|
|
};
|
|
|
|
|
const mtbRatingToScale: { [key: string]: string } = {
|
|
|
|
|
'1': '0',
|
|
|
|
|
'2': '1',
|
|
|
|
|
'3': '2',
|
|
|
|
|
'4': '3',
|
|
|
|
|
'5': '4',
|
|
|
|
|
'6': '5',
|
|
|
|
|
'7': '6',
|
|
|
|
|
};
|
|
|
|
|
async function getGraphHopperRoute(
|
2025-02-02 11:17:22 +01:00
|
|
|
points: Coordinates[],
|
2025-12-23 16:49:47 +01:00
|
|
|
graphHopperProfile: string,
|
2025-02-02 11:17:22 +01:00
|
|
|
privateRoads: boolean
|
|
|
|
|
): Promise<TrackPoint[]> {
|
2025-12-23 16:49:47 +01:00
|
|
|
let response = await fetch('https://graphhopper-a.gpx.studio/route', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
points: points.map((point) => [point.lon, point.lat]),
|
|
|
|
|
profile: graphHopperProfile,
|
|
|
|
|
elevation: true,
|
|
|
|
|
points_encoded: false,
|
|
|
|
|
details: graphhopperDetails,
|
|
|
|
|
custom_model: privateRoads
|
|
|
|
|
? {}
|
|
|
|
|
: {
|
|
|
|
|
priority: [
|
|
|
|
|
{
|
|
|
|
|
if: 'road_access == PRIVATE',
|
|
|
|
|
multiply_by: '0.0',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`${await response.text()}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let json = await response.json();
|
|
|
|
|
|
|
|
|
|
let route: TrackPoint[] = [];
|
|
|
|
|
let coordinates = json.paths[0].points.coordinates;
|
|
|
|
|
let 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: {},
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let key of graphhopperDetails) {
|
|
|
|
|
let detail = details[key];
|
|
|
|
|
for (let i = 0; i < detail.length; i++) {
|
|
|
|
|
for (let j = detail[i][0]; j < detail[i][1]; j++) {
|
|
|
|
|
if (detail[i][2] !== undefined && detail[i][2] !== 'missing') {
|
|
|
|
|
if (key === 'road_class') {
|
|
|
|
|
route[j].setExtension('highway', detail[i][2]);
|
|
|
|
|
} else if (key === 'hike_rating') {
|
|
|
|
|
const sacScale = hikeRatingToSACScale[detail[i][2]];
|
|
|
|
|
if (sacScale) {
|
|
|
|
|
route[j].setExtension('sac_scale', sacScale);
|
|
|
|
|
}
|
|
|
|
|
} else if (key === 'mtb_rating') {
|
|
|
|
|
const mtbScale = mtbRatingToScale[detail[i][2]];
|
|
|
|
|
if (mtbScale) {
|
|
|
|
|
route[j].setExtension('mtb_scale', mtbScale);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
route[j].setExtension(key, detail[i][2]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return route;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getBRouterRoute(
|
|
|
|
|
points: Coordinates[],
|
|
|
|
|
brouterProfile: string
|
|
|
|
|
): Promise<TrackPoint[]> {
|
|
|
|
|
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`;
|
2024-04-24 19:32:55 +02:00
|
|
|
|
|
|
|
|
let response = await fetch(url);
|
2024-04-27 11:16:59 +02:00
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`${await response.text()}`);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-24 19:32:55 +02:00
|
|
|
let geojson = await response.json();
|
|
|
|
|
|
|
|
|
|
let route: TrackPoint[] = [];
|
|
|
|
|
let coordinates = geojson.features[0].geometry.coordinates;
|
2024-04-26 22:35:42 +02:00
|
|
|
let messages = geojson.features[0].properties.messages;
|
|
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
const lngIdx = messages[0].indexOf('Longitude');
|
|
|
|
|
const latIdx = messages[0].indexOf('Latitude');
|
|
|
|
|
const tagIdx = messages[0].indexOf('WayTags');
|
2024-04-26 22:35:42 +02:00
|
|
|
let messageIdx = 1;
|
2024-10-03 18:14:01 +02:00
|
|
|
let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {};
|
2024-04-26 22:35:42 +02:00
|
|
|
|
2024-04-24 19:32:55 +02:00
|
|
|
for (let i = 0; i < coordinates.length; i++) {
|
2025-02-02 11:17:22 +01:00
|
|
|
route.push(
|
|
|
|
|
new TrackPoint({
|
|
|
|
|
attributes: {
|
2025-12-23 16:49:47 +01:00
|
|
|
lat: coordinates[i][1],
|
|
|
|
|
lon: coordinates[i][0],
|
2025-02-02 11:17:22 +01:00
|
|
|
},
|
2025-12-23 16:49:47 +01:00
|
|
|
ele: coordinates[i][2] ?? (i > 0 ? route[i - 1].ele : 0),
|
2025-02-02 11:17:22 +01:00
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
messageIdx < messages.length &&
|
2024-04-26 22:35:42 +02:00
|
|
|
coordinates[i][0] == Number(messages[messageIdx][lngIdx]) / 1000000 &&
|
2025-02-02 11:17:22 +01:00
|
|
|
coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000
|
|
|
|
|
) {
|
2024-04-26 22:35:42 +02:00
|
|
|
messageIdx++;
|
|
|
|
|
|
2024-10-03 18:14:01 +02:00
|
|
|
if (messageIdx == messages.length) tags = {};
|
|
|
|
|
else tags = getTags(messages[messageIdx][tagIdx]);
|
2024-04-26 22:35:42 +02:00
|
|
|
}
|
2024-09-22 13:05:28 +02:00
|
|
|
|
2024-10-03 18:14:01 +02:00
|
|
|
route[route.length - 1].setExtensions(tags);
|
2024-04-24 19:32:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return route;
|
2024-04-26 22:35:42 +02:00
|
|
|
}
|
|
|
|
|
|
2024-10-03 18:14:01 +02:00
|
|
|
function getTags(message: string): { [key: string]: string } {
|
2025-02-02 11:17:22 +01:00
|
|
|
const fields = message.split(' ');
|
2024-10-03 18:14:01 +02:00
|
|
|
let tags: { [key: string]: string } = {};
|
|
|
|
|
for (let i = 0; i < fields.length; i++) {
|
2025-02-02 11:17:22 +01:00
|
|
|
let [key, value] = fields[i].split('=');
|
2024-10-08 12:04:07 +02:00
|
|
|
key = key.replace(/:/g, '_');
|
|
|
|
|
tags[key] = value;
|
2024-04-26 22:35:42 +02:00
|
|
|
}
|
2024-10-03 18:14:01 +02:00
|
|
|
return tags;
|
|
|
|
|
}
|
2024-05-09 00:02:27 +02:00
|
|
|
|
|
|
|
|
function getIntermediatePoints(points: Coordinates[]): Promise<TrackPoint[]> {
|
|
|
|
|
let route: TrackPoint[] = [];
|
|
|
|
|
let step = 0.05;
|
|
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
for (let i = 0; i < points.length - 1; i++) {
|
|
|
|
|
// Add intermediate points between each pair of points
|
2024-05-09 00:02:27 +02:00
|
|
|
let dist = distance(points[i], points[i + 1]) / 1000;
|
|
|
|
|
for (let d = 0; d < dist; d += step) {
|
2025-02-02 11:17:22 +01:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
);
|
2024-05-09 00:02:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
route.push(
|
|
|
|
|
new TrackPoint({
|
|
|
|
|
attributes: {
|
|
|
|
|
lat: points[points.length - 1].lat,
|
|
|
|
|
lon: points[points.length - 1].lon,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
);
|
2024-05-09 00:02:27 +02:00
|
|
|
|
2024-09-04 19:11:56 +02:00
|
|
|
return getElevation(route).then((elevations) => {
|
|
|
|
|
route.forEach((point, i) => {
|
|
|
|
|
point.ele = elevations[i];
|
|
|
|
|
});
|
|
|
|
|
return route;
|
2024-05-09 00:02:27 +02:00
|
|
|
});
|
2025-02-02 11:17:22 +01:00
|
|
|
}
|