325 lines
9.5 KiB
TypeScript
325 lines
9.5 KiB
TypeScript
|
import { HttpService } from '@nestjs/axios';
|
||
|
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
|
||
|
import { Injectable } from '@nestjs/common';
|
||
|
import { catchError, lastValueFrom, map } from 'rxjs';
|
||
|
import { AxiosError, AxiosResponse } from 'axios';
|
||
|
import { IGeodesic } from '../../../geography/domain/interfaces/geodesic.interface';
|
||
|
import { GeorouterSettings } from '../../domain/types/georouter-settings.type';
|
||
|
import { Path } from '../../domain/types/path.type';
|
||
|
import { NamedRoute } from '../../domain/types/named-route';
|
||
|
import { GeographyException } from '../../exceptions/geography.exception';
|
||
|
import { ExceptionCode } from '../../..//utils/exception-code.enum';
|
||
|
import { Route } from '../../domain/entities/route';
|
||
|
import { SpacetimePoint } from '../../domain/entities/spacetime-point';
|
||
|
|
||
|
@Injectable()
|
||
|
export class GraphhopperGeorouter implements IGeorouter {
|
||
|
private url: string;
|
||
|
private urlArgs: string[];
|
||
|
private withTime: boolean;
|
||
|
private withPoints: boolean;
|
||
|
private withDistance: boolean;
|
||
|
private paths: Path[];
|
||
|
private httpService: HttpService;
|
||
|
private geodesic: IGeodesic;
|
||
|
|
||
|
constructor(url: string, httpService: HttpService, geodesic: IGeodesic) {
|
||
|
this.url = url + '/route?';
|
||
|
this.httpService = httpService;
|
||
|
this.geodesic = geodesic;
|
||
|
}
|
||
|
|
||
|
route = async (
|
||
|
paths: Path[],
|
||
|
settings: GeorouterSettings,
|
||
|
): Promise<NamedRoute[]> => {
|
||
|
this.setDefaultUrlArgs();
|
||
|
this.setWithTime(settings.withTime);
|
||
|
this.setWithPoints(settings.withPoints);
|
||
|
this.setWithDistance(settings.withDistance);
|
||
|
this.paths = paths;
|
||
|
return await this.getRoutes();
|
||
|
};
|
||
|
|
||
|
private setDefaultUrlArgs = (): void => {
|
||
|
this.urlArgs = ['vehicle=car', 'weighting=fastest', 'points_encoded=false'];
|
||
|
};
|
||
|
|
||
|
private setWithTime = (withTime: boolean): void => {
|
||
|
this.withTime = withTime;
|
||
|
if (withTime) {
|
||
|
this.urlArgs.push('details=time');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
private setWithPoints = (withPoints: boolean): void => {
|
||
|
this.withPoints = withPoints;
|
||
|
if (!withPoints) {
|
||
|
this.urlArgs.push('calc_points=false');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
private setWithDistance = (withDistance: boolean): void => {
|
||
|
this.withDistance = withDistance;
|
||
|
if (withDistance) {
|
||
|
this.urlArgs.push('instructions=true');
|
||
|
} else {
|
||
|
this.urlArgs.push('instructions=false');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
private getRoutes = async (): Promise<NamedRoute[]> => {
|
||
|
const routes = Promise.all(
|
||
|
this.paths.map(async (path) => {
|
||
|
const url: string = [
|
||
|
this.getUrl(),
|
||
|
'&point=',
|
||
|
path.points
|
||
|
.map((point) => [point.lat, point.lon].join())
|
||
|
.join('&point='),
|
||
|
].join('');
|
||
|
const route = await lastValueFrom(
|
||
|
this.httpService.get(url).pipe(
|
||
|
map((res) => (res.data ? this.createRoute(res) : undefined)),
|
||
|
catchError((error: AxiosError) => {
|
||
|
throw new GeographyException(
|
||
|
ExceptionCode.INTERNAL,
|
||
|
'Georouter unavailable : ' + error.message,
|
||
|
);
|
||
|
}),
|
||
|
),
|
||
|
);
|
||
|
return <NamedRoute>{
|
||
|
key: path.key,
|
||
|
route,
|
||
|
};
|
||
|
}),
|
||
|
);
|
||
|
return routes;
|
||
|
};
|
||
|
|
||
|
private getUrl = (): string => {
|
||
|
return [this.url, this.urlArgs.join('&')].join('');
|
||
|
};
|
||
|
|
||
|
private createRoute = (
|
||
|
response: AxiosResponse<GraphhopperResponse>,
|
||
|
): Route => {
|
||
|
const route = new Route(this.geodesic);
|
||
|
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.setPoints(
|
||
|
shortestPath.points.coordinates.map((coordinate) => ({
|
||
|
lon: coordinate[0],
|
||
|
lat: coordinate[1],
|
||
|
})),
|
||
|
);
|
||
|
if (
|
||
|
shortestPath.details &&
|
||
|
shortestPath.details.time &&
|
||
|
shortestPath.snapped_waypoints &&
|
||
|
shortestPath.snapped_waypoints.coordinates
|
||
|
) {
|
||
|
let instructions: GraphhopperInstruction[] = [];
|
||
|
if (shortestPath.instructions)
|
||
|
instructions = shortestPath.instructions;
|
||
|
route.setSpacetimePoints(
|
||
|
this.generateSpacetimePoints(
|
||
|
shortestPath.points.coordinates,
|
||
|
shortestPath.snapped_waypoints.coordinates,
|
||
|
shortestPath.details.time,
|
||
|
instructions,
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return route;
|
||
|
};
|
||
|
|
||
|
private generateSpacetimePoints = (
|
||
|
points: Array<number[]>,
|
||
|
snappedWaypoints: Array<number[]>,
|
||
|
durations: Array<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) =>
|
||
|
new SpacetimePoint(
|
||
|
{ lon: points[index][1], lat: points[index][0] },
|
||
|
times.find((time) => time.index == index)?.duration,
|
||
|
distances.find((distance) => distance.index == index)?.distance,
|
||
|
),
|
||
|
);
|
||
|
};
|
||
|
|
||
|
private getIndices = (
|
||
|
points: Array<number[]>,
|
||
|
snappedWaypoints: Array<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: Array<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 = {
|
||
|
paths: [
|
||
|
{
|
||
|
distance: number;
|
||
|
weight: number;
|
||
|
time: number;
|
||
|
points_encoded: boolean;
|
||
|
bbox: number[];
|
||
|
points: GraphhopperCoordinates;
|
||
|
snapped_waypoints: GraphhopperCoordinates;
|
||
|
details: {
|
||
|
time: Array<number[]>;
|
||
|
};
|
||
|
instructions: GraphhopperInstruction[];
|
||
|
},
|
||
|
];
|
||
|
};
|
||
|
|
||
|
type GraphhopperCoordinates = {
|
||
|
coordinates: Array<number[]>;
|
||
|
};
|
||
|
|
||
|
type GraphhopperInstruction = {
|
||
|
distance: number;
|
||
|
heading: number;
|
||
|
sign: GraphhopperSign;
|
||
|
interval: number[];
|
||
|
text: string;
|
||
|
};
|
||
|
|
||
|
enum GraphhopperSign {
|
||
|
SIGN_START = 0,
|
||
|
SIGN_FINISH = 4,
|
||
|
SIGN_WAYPOINT = 5,
|
||
|
}
|