draft support for graphhopper

This commit is contained in:
vcoppe
2025-12-23 16:49:47 +01:00
parent e92e48ffde
commit 7c2e24bbc4
3 changed files with 131 additions and 25 deletions

View File

@@ -1375,10 +1375,7 @@ export class TrackPoint {
: undefined; : undefined;
} }
setExtensions(extensions: Record<string, string>) { setExtension(key: string, value: string) {
if (Object.keys(extensions).length === 0) {
return;
}
if (!this.extensions) { if (!this.extensions) {
this.extensions = {}; this.extensions = {};
} }
@@ -1388,8 +1385,12 @@ export class TrackPoint {
if (!this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']) { if (!this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']) {
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'] = {}; this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'] = {};
} }
Object.entries(extensions).forEach(([key, value]) => {
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'][key] = value; this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'][key] = value;
}
setExtensions(extensions: Record<string, string>) {
Object.entries(extensions).forEach(([key, value]) => {
this.setExtension(key, value);
}); });
} }

View File

@@ -21,7 +21,7 @@
SquareArrowUpLeft, SquareArrowUpLeft,
SquareArrowOutDownRight, SquareArrowOutDownRight,
} from '@lucide/svelte'; } from '@lucide/svelte';
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing'; import { routingProfiles } from '$lib/components/toolbar/tools/routing/routing';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { import {
@@ -167,7 +167,7 @@
{i18n._(`toolbar.routing.activities.${$routingProfile}`)} {i18n._(`toolbar.routing.activities.${$routingProfile}`)}
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
{#each Object.keys(brouterProfiles) as profile} {#each Object.keys(routingProfiles) as profile}
<Select.Item value={profile} <Select.Item value={profile}
>{i18n._( >{i18n._(
`toolbar.routing.activities.${profile}` `toolbar.routing.activities.${profile}`

View File

@@ -6,35 +6,141 @@ import { get } from 'svelte/store';
const { routing, routingProfile, privateRoads } = settings; const { routing, routingProfile, privateRoads } = settings;
export const brouterProfiles: { [key: string]: string } = { export type RoutingProfile = {
bike: 'Trekking-dry', engine: 'graphhopper' | 'brouter';
racing_bike: 'fastbike', profile: string;
gravel_bike: 'gravel', };
mountain_bike: 'MTB',
foot: 'Hiking-Alpine-SAC6', export const routingProfiles: { [key: string]: RoutingProfile } = {
motorcycle: 'Car-FastEco', bike: { engine: 'graphhopper', profile: 'bike' },
water: 'river', racing_bike: { engine: 'graphhopper', profile: 'racingbike' },
railway: 'rail', 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' },
}; };
export function route(points: Coordinates[]): Promise<TrackPoint[]> { export function route(points: Coordinates[]): Promise<TrackPoint[]> {
if (get(routing)) { if (get(routing)) {
return getRoute(points, brouterProfiles[get(routingProfile)], get(privateRoads)); const profile = routingProfiles[get(routingProfile)];
if (profile.engine === 'graphhopper') {
return getGraphHopperRoute(points, profile.profile, get(privateRoads));
} else {
return getBRouterRoute(points, profile.profile);
}
} else { } else {
return getIntermediatePoints(points); return getIntermediatePoints(points);
} }
} }
async function getRoute( 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(
points: Coordinates[], points: Coordinates[],
brouterProfile: string, graphHopperProfile: string,
privateRoads: boolean privateRoads: boolean
): Promise<TrackPoint[]> { ): Promise<TrackPoint[]> {
let url = `https://brouter.gpx.studio?lonlats=${points.map((point) => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`; 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`;
let response = await fetch(url); let response = await fetch(url);
// Check if the response is ok
if (!response.ok) { if (!response.ok) {
throw new Error(`${await response.text()}`); throw new Error(`${await response.text()}`);
} }
@@ -52,14 +158,13 @@ async function getRoute(
let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {}; let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {};
for (let i = 0; i < coordinates.length; i++) { for (let i = 0; i < coordinates.length; i++) {
let coord = coordinates[i];
route.push( route.push(
new TrackPoint({ new TrackPoint({
attributes: { attributes: {
lat: coord[1], lat: coordinates[i][1],
lon: coord[0], lon: coordinates[i][0],
}, },
ele: coord[2] ?? (i > 0 ? route[i - 1].ele : 0), ele: coordinates[i][2] ?? (i > 0 ? route[i - 1].ele : 0),
}) })
); );