mirror of
https://gitlab.com/mobicoop/v3/service/matcher.git
synced 2026-01-01 14:12:41 +00:00
almost full coverage for graphhopper georouter
This commit is contained in:
27
src/modules/geography/infrastructure/geodesic.ts
Normal file
27
src/modules/geography/infrastructure/geodesic.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Geodesic as Geolib, GeodesicClass } from 'geographiclib-geodesic';
|
||||
import { GeodesicPort } from '../core/application/ports/geodesic.port';
|
||||
|
||||
@Injectable()
|
||||
export class Geodesic implements GeodesicPort {
|
||||
private geod: GeodesicClass;
|
||||
|
||||
constructor() {
|
||||
this.geod = Geolib.WGS84;
|
||||
}
|
||||
|
||||
inverse = (
|
||||
lon1: number,
|
||||
lat1: number,
|
||||
lon2: number,
|
||||
lat2: number,
|
||||
): { azimuth: number; distance: number } => {
|
||||
const { azi2: azimuth, s12: distance } = this.geod.Inverse(
|
||||
lat1,
|
||||
lon1,
|
||||
lat2,
|
||||
lon2,
|
||||
);
|
||||
return { azimuth, distance };
|
||||
};
|
||||
}
|
||||
@@ -2,15 +2,21 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { GeorouterPort } from '../core/application/ports/georouter.port';
|
||||
import { GeorouterSettings } from '../core/application/types/georouter-settings.type';
|
||||
import { Path, PathType, Route } from '../core/domain/route.types';
|
||||
import {
|
||||
Path,
|
||||
PathType,
|
||||
Route,
|
||||
SpacetimePoint,
|
||||
} from '../core/domain/route.types';
|
||||
import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port';
|
||||
import { PARAMS_PROVIDER } from '../geography.di-tokens';
|
||||
import { GEODESIC, PARAMS_PROVIDER } from '../geography.di-tokens';
|
||||
import { catchError, lastValueFrom, map } from 'rxjs';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import {
|
||||
GeorouterUnavailableException,
|
||||
RouteNotFoundException,
|
||||
} from '../core/domain/route.errors';
|
||||
import { GeodesicPort } from '../core/application/ports/geodesic.port';
|
||||
|
||||
@Injectable()
|
||||
export class GraphhopperGeorouter implements GeorouterPort {
|
||||
@@ -21,8 +27,12 @@ export class GraphhopperGeorouter implements GeorouterPort {
|
||||
private readonly httpService: HttpService,
|
||||
@Inject(PARAMS_PROVIDER)
|
||||
private readonly defaultParamsProvider: DefaultParamsProviderPort,
|
||||
@Inject(GEODESIC) private readonly geodesic: GeodesicPort,
|
||||
) {
|
||||
this.url = defaultParamsProvider.getParams().GEOROUTER_URL;
|
||||
this.url = [
|
||||
defaultParamsProvider.getParams().GEOROUTER_URL,
|
||||
'/route?',
|
||||
].join('');
|
||||
}
|
||||
|
||||
routes = async (
|
||||
@@ -31,17 +41,7 @@ export class GraphhopperGeorouter implements GeorouterPort {
|
||||
): Promise<Route[]> => {
|
||||
this.setDefaultUrlArgs();
|
||||
this.setSettings(settings);
|
||||
return [
|
||||
{
|
||||
type: PathType.DRIVER,
|
||||
distance: 1000,
|
||||
duration: 1000,
|
||||
fwdAzimuth: 280,
|
||||
backAzimuth: 100,
|
||||
distanceAzimuth: 900,
|
||||
points: [],
|
||||
},
|
||||
];
|
||||
return this.getRoutes(paths);
|
||||
};
|
||||
|
||||
private setDefaultUrlArgs = (): void => {
|
||||
@@ -99,7 +99,197 @@ export class GraphhopperGeorouter implements GeorouterPort {
|
||||
private createRoute = (
|
||||
response: AxiosResponse<GraphhopperResponse>,
|
||||
type: PathType,
|
||||
): Route => undefined;
|
||||
): Route => {
|
||||
const route = {} as Route;
|
||||
route.type = type;
|
||||
if (response.data.paths && response.data.paths[0]) {
|
||||
const shortestPath = response.data.paths[0];
|
||||
route.distance = shortestPath.distance ?? 0;
|
||||
route.duration = shortestPath.time ? shortestPath.time / 1000 : 0;
|
||||
if (shortestPath.points && shortestPath.points.coordinates) {
|
||||
route.points = shortestPath.points.coordinates.map((coordinate) => ({
|
||||
lon: coordinate[0],
|
||||
lat: coordinate[1],
|
||||
}));
|
||||
const inverse = this.geodesic.inverse(
|
||||
route.points[0].lon,
|
||||
route.points[0].lat,
|
||||
route.points[route.points.length - 1].lon,
|
||||
route.points[route.points.length - 1].lat,
|
||||
);
|
||||
route.fwdAzimuth =
|
||||
inverse.azimuth >= 0
|
||||
? inverse.azimuth
|
||||
: 360 - Math.abs(inverse.azimuth);
|
||||
route.backAzimuth =
|
||||
route.fwdAzimuth > 180
|
||||
? route.fwdAzimuth - 180
|
||||
: route.fwdAzimuth + 180;
|
||||
route.distanceAzimuth = inverse.distance;
|
||||
if (
|
||||
shortestPath.details &&
|
||||
shortestPath.details.time &&
|
||||
shortestPath.snapped_waypoints &&
|
||||
shortestPath.snapped_waypoints.coordinates
|
||||
) {
|
||||
let instructions: GraphhopperInstruction[] = [];
|
||||
if (shortestPath.instructions)
|
||||
instructions = shortestPath.instructions;
|
||||
route.spacetimeWaypoints = this.generateSpacetimePoints(
|
||||
shortestPath.points.coordinates,
|
||||
shortestPath.snapped_waypoints.coordinates,
|
||||
shortestPath.details.time,
|
||||
instructions,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return route;
|
||||
};
|
||||
|
||||
private generateSpacetimePoints = (
|
||||
points: [[number, number]],
|
||||
snappedWaypoints: [[number, number]],
|
||||
durations: [[number, number, number]],
|
||||
instructions: GraphhopperInstruction[],
|
||||
): SpacetimePoint[] => {
|
||||
const indices = this.getIndices(points, snappedWaypoints);
|
||||
const times = this.getTimes(durations, indices);
|
||||
const distances = this.getDistances(instructions, indices);
|
||||
return indices.map((index) => ({
|
||||
lon: points[index][1],
|
||||
lat: points[index][0],
|
||||
distance: distances.find((distance) => distance.index == index)?.distance,
|
||||
duration: times.find((time) => time.index == index)?.duration,
|
||||
}));
|
||||
};
|
||||
|
||||
private getIndices = (
|
||||
points: [[number, number]],
|
||||
snappedWaypoints: [[number, number]],
|
||||
): number[] => {
|
||||
const indices = snappedWaypoints.map((waypoint) =>
|
||||
points.findIndex(
|
||||
(point) => point[0] == waypoint[0] && point[1] == waypoint[1],
|
||||
),
|
||||
);
|
||||
if (indices.find((index) => index == -1) === undefined) return indices;
|
||||
const missedWaypoints = indices
|
||||
.map(
|
||||
(value, index) =>
|
||||
<
|
||||
{
|
||||
index: number;
|
||||
originIndex: number;
|
||||
waypoint: number[];
|
||||
nearest: number;
|
||||
distance: number;
|
||||
}
|
||||
>{
|
||||
index: value,
|
||||
originIndex: index,
|
||||
waypoint: snappedWaypoints[index],
|
||||
nearest: undefined,
|
||||
distance: 999999999,
|
||||
},
|
||||
)
|
||||
.filter((element) => element.index == -1);
|
||||
for (const index in points) {
|
||||
for (const missedWaypoint of missedWaypoints) {
|
||||
const inverse = this.geodesic.inverse(
|
||||
missedWaypoint.waypoint[0],
|
||||
missedWaypoint.waypoint[1],
|
||||
points[index][0],
|
||||
points[index][1],
|
||||
);
|
||||
if (inverse.distance < missedWaypoint.distance) {
|
||||
missedWaypoint.distance = inverse.distance;
|
||||
missedWaypoint.nearest = parseInt(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const missedWaypoint of missedWaypoints) {
|
||||
indices[missedWaypoint.originIndex] = missedWaypoint.nearest;
|
||||
}
|
||||
return indices;
|
||||
};
|
||||
|
||||
private getTimes = (
|
||||
durations: [[number, number, number]],
|
||||
indices: number[],
|
||||
): Array<{ index: number; duration: number }> => {
|
||||
const times: Array<{ index: number; duration: number }> = [];
|
||||
let duration = 0;
|
||||
for (const [origin, destination, stepDuration] of durations) {
|
||||
let indexFound = false;
|
||||
const indexAsOrigin = indices.find((index) => index == origin);
|
||||
if (
|
||||
indexAsOrigin !== undefined &&
|
||||
times.find((time) => origin == time.index) == undefined
|
||||
) {
|
||||
times.push({
|
||||
index: indexAsOrigin,
|
||||
duration: Math.round(stepDuration / 1000),
|
||||
});
|
||||
indexFound = true;
|
||||
}
|
||||
if (!indexFound) {
|
||||
const indexAsDestination = indices.find(
|
||||
(index) => index == destination,
|
||||
);
|
||||
if (
|
||||
indexAsDestination !== undefined &&
|
||||
times.find((time) => destination == time.index) == undefined
|
||||
) {
|
||||
times.push({
|
||||
index: indexAsDestination,
|
||||
duration: Math.round((duration + stepDuration) / 1000),
|
||||
});
|
||||
indexFound = true;
|
||||
}
|
||||
}
|
||||
if (!indexFound) {
|
||||
const indexInBetween = indices.find(
|
||||
(index) => origin < index && index < destination,
|
||||
);
|
||||
if (indexInBetween !== undefined) {
|
||||
times.push({
|
||||
index: indexInBetween,
|
||||
duration: Math.round((duration + stepDuration / 2) / 1000),
|
||||
});
|
||||
}
|
||||
}
|
||||
duration += stepDuration;
|
||||
}
|
||||
return times;
|
||||
};
|
||||
|
||||
private getDistances = (
|
||||
instructions: GraphhopperInstruction[],
|
||||
indices: number[],
|
||||
): Array<{ index: number; distance: number }> => {
|
||||
let distance = 0;
|
||||
const distances: Array<{ index: number; distance: number }> = [
|
||||
{
|
||||
index: 0,
|
||||
distance,
|
||||
},
|
||||
];
|
||||
for (const instruction of instructions) {
|
||||
distance += instruction.distance;
|
||||
if (
|
||||
(instruction.sign == GraphhopperSign.SIGN_WAYPOINT ||
|
||||
instruction.sign == GraphhopperSign.SIGN_FINISH) &&
|
||||
indices.find((index) => index == instruction.interval[0]) !== undefined
|
||||
) {
|
||||
distances.push({
|
||||
index: instruction.interval[0],
|
||||
distance: Math.round(distance),
|
||||
});
|
||||
}
|
||||
}
|
||||
return distances;
|
||||
};
|
||||
}
|
||||
|
||||
type GraphhopperResponse = {
|
||||
@@ -113,7 +303,7 @@ type GraphhopperResponse = {
|
||||
points: GraphhopperCoordinates;
|
||||
snapped_waypoints: GraphhopperCoordinates;
|
||||
details: {
|
||||
time: number[];
|
||||
time: [[number, number, number]];
|
||||
};
|
||||
instructions: GraphhopperInstruction[];
|
||||
},
|
||||
@@ -121,14 +311,14 @@ type GraphhopperResponse = {
|
||||
};
|
||||
|
||||
type GraphhopperCoordinates = {
|
||||
coordinates: number[];
|
||||
coordinates: [[number, number]];
|
||||
};
|
||||
|
||||
type GraphhopperInstruction = {
|
||||
distance: number;
|
||||
heading: number;
|
||||
sign: GraphhopperSign;
|
||||
interval: number[];
|
||||
interval: [number, number];
|
||||
text: string;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user