almost full coverage for graphhopper georouter
This commit is contained in:
parent
158b12b150
commit
66d4d58dd1
|
@ -32,7 +32,8 @@ export type Route = {
|
||||||
fwdAzimuth: number;
|
fwdAzimuth: number;
|
||||||
backAzimuth: number;
|
backAzimuth: number;
|
||||||
distanceAzimuth: number;
|
distanceAzimuth: number;
|
||||||
points: SpacetimePoint[] | Coordinates[];
|
points: Coordinates[];
|
||||||
|
spacetimeWaypoints: SpacetimePoint[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Path = {
|
export type Path = {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER');
|
export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER');
|
||||||
export const DIRECTION_ENCODER = Symbol('DIRECTION_ENCODER');
|
export const DIRECTION_ENCODER = Symbol('DIRECTION_ENCODER');
|
||||||
export const GEOROUTER = Symbol('GEOROUTER');
|
export const GEOROUTER = Symbol('GEOROUTER');
|
||||||
|
export const GEODESIC = Symbol('GEODESIC');
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import { Module, Provider } from '@nestjs/common';
|
import { Module, Provider } from '@nestjs/common';
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
import { CqrsModule } from '@nestjs/cqrs';
|
||||||
import { DIRECTION_ENCODER, PARAMS_PROVIDER } from './geography.di-tokens';
|
import {
|
||||||
|
DIRECTION_ENCODER,
|
||||||
|
GEODESIC,
|
||||||
|
PARAMS_PROVIDER,
|
||||||
|
} from './geography.di-tokens';
|
||||||
import { DefaultParamsProvider } from './infrastructure/default-params-provider';
|
import { DefaultParamsProvider } from './infrastructure/default-params-provider';
|
||||||
import { PostgresDirectionEncoder } from './infrastructure/postgres-direction-encoder';
|
import { PostgresDirectionEncoder } from './infrastructure/postgres-direction-encoder';
|
||||||
import { GetBasicRouteController } from './interface/controllers/get-basic-route.controller';
|
import { GetBasicRouteController } from './interface/controllers/get-basic-route.controller';
|
||||||
import { RouteMapper } from './route.mapper';
|
import { RouteMapper } from './route.mapper';
|
||||||
|
import { Geodesic } from './infrastructure/geodesic';
|
||||||
|
|
||||||
const mappers: Provider[] = [RouteMapper];
|
const mappers: Provider[] = [RouteMapper];
|
||||||
|
|
||||||
|
@ -17,6 +22,10 @@ const adapters: Provider[] = [
|
||||||
provide: DIRECTION_ENCODER,
|
provide: DIRECTION_ENCODER,
|
||||||
useClass: PostgresDirectionEncoder,
|
useClass: PostgresDirectionEncoder,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: GEODESIC,
|
||||||
|
useClass: Geodesic,
|
||||||
|
},
|
||||||
GetBasicRouteController,
|
GetBasicRouteController,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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 { HttpService } from '@nestjs/axios';
|
||||||
import { GeorouterPort } from '../core/application/ports/georouter.port';
|
import { GeorouterPort } from '../core/application/ports/georouter.port';
|
||||||
import { GeorouterSettings } from '../core/application/types/georouter-settings.type';
|
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 { 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 { catchError, lastValueFrom, map } from 'rxjs';
|
||||||
import { AxiosError, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
import {
|
import {
|
||||||
GeorouterUnavailableException,
|
GeorouterUnavailableException,
|
||||||
RouteNotFoundException,
|
RouteNotFoundException,
|
||||||
} from '../core/domain/route.errors';
|
} from '../core/domain/route.errors';
|
||||||
|
import { GeodesicPort } from '../core/application/ports/geodesic.port';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GraphhopperGeorouter implements GeorouterPort {
|
export class GraphhopperGeorouter implements GeorouterPort {
|
||||||
|
@ -21,8 +27,12 @@ export class GraphhopperGeorouter implements GeorouterPort {
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
@Inject(PARAMS_PROVIDER)
|
@Inject(PARAMS_PROVIDER)
|
||||||
private readonly defaultParamsProvider: DefaultParamsProviderPort,
|
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 (
|
routes = async (
|
||||||
|
@ -31,17 +41,7 @@ export class GraphhopperGeorouter implements GeorouterPort {
|
||||||
): Promise<Route[]> => {
|
): Promise<Route[]> => {
|
||||||
this.setDefaultUrlArgs();
|
this.setDefaultUrlArgs();
|
||||||
this.setSettings(settings);
|
this.setSettings(settings);
|
||||||
return [
|
return this.getRoutes(paths);
|
||||||
{
|
|
||||||
type: PathType.DRIVER,
|
|
||||||
distance: 1000,
|
|
||||||
duration: 1000,
|
|
||||||
fwdAzimuth: 280,
|
|
||||||
backAzimuth: 100,
|
|
||||||
distanceAzimuth: 900,
|
|
||||||
points: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private setDefaultUrlArgs = (): void => {
|
private setDefaultUrlArgs = (): void => {
|
||||||
|
@ -99,7 +99,197 @@ export class GraphhopperGeorouter implements GeorouterPort {
|
||||||
private createRoute = (
|
private createRoute = (
|
||||||
response: AxiosResponse<GraphhopperResponse>,
|
response: AxiosResponse<GraphhopperResponse>,
|
||||||
type: PathType,
|
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 = {
|
type GraphhopperResponse = {
|
||||||
|
@ -113,7 +303,7 @@ type GraphhopperResponse = {
|
||||||
points: GraphhopperCoordinates;
|
points: GraphhopperCoordinates;
|
||||||
snapped_waypoints: GraphhopperCoordinates;
|
snapped_waypoints: GraphhopperCoordinates;
|
||||||
details: {
|
details: {
|
||||||
time: number[];
|
time: [[number, number, number]];
|
||||||
};
|
};
|
||||||
instructions: GraphhopperInstruction[];
|
instructions: GraphhopperInstruction[];
|
||||||
},
|
},
|
||||||
|
@ -121,14 +311,14 @@ type GraphhopperResponse = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type GraphhopperCoordinates = {
|
type GraphhopperCoordinates = {
|
||||||
coordinates: number[];
|
coordinates: [[number, number]];
|
||||||
};
|
};
|
||||||
|
|
||||||
type GraphhopperInstruction = {
|
type GraphhopperInstruction = {
|
||||||
distance: number;
|
distance: number;
|
||||||
heading: number;
|
heading: number;
|
||||||
sign: GraphhopperSign;
|
sign: GraphhopperSign;
|
||||||
interval: number[];
|
interval: [number, number];
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Geodesic } from '@modules/geography/infrastructure/geodesic';
|
||||||
|
|
||||||
|
describe('Matcher geodesic', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
const geodesic: Geodesic = new Geodesic();
|
||||||
|
expect(geodesic).toBeDefined();
|
||||||
|
});
|
||||||
|
it('should get inverse values', () => {
|
||||||
|
const geodesic: Geodesic = new Geodesic();
|
||||||
|
const inv = geodesic.inverse(0, 0, 1, 1);
|
||||||
|
expect(Math.round(inv.azimuth)).toBe(45);
|
||||||
|
expect(Math.round(inv.distance)).toBe(156900);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,33 +1,259 @@
|
||||||
import { DefaultParamsProviderPort } from '@modules/geography/core/application/ports/default-params-provider.port';
|
import { DefaultParamsProviderPort } from '@modules/geography/core/application/ports/default-params-provider.port';
|
||||||
|
import { GeodesicPort } from '@modules/geography/core/application/ports/geodesic.port';
|
||||||
import {
|
import {
|
||||||
Path,
|
GeorouterUnavailableException,
|
||||||
PathType,
|
RouteNotFoundException,
|
||||||
Route,
|
} from '@modules/geography/core/domain/route.errors';
|
||||||
} from '@modules/geography/core/domain/route.types';
|
import { PathType, Route } from '@modules/geography/core/domain/route.types';
|
||||||
import { PARAMS_PROVIDER } from '@modules/geography/geography.di-tokens';
|
import {
|
||||||
|
GEODESIC,
|
||||||
|
PARAMS_PROVIDER,
|
||||||
|
} from '@modules/geography/geography.di-tokens';
|
||||||
import { GraphhopperGeorouter } from '@modules/geography/infrastructure/graphhopper-georouter';
|
import { GraphhopperGeorouter } from '@modules/geography/infrastructure/graphhopper-georouter';
|
||||||
import { HttpService } from '@nestjs/axios';
|
import { HttpService } from '@nestjs/axios';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { of, throwError } from 'rxjs';
|
||||||
|
|
||||||
const driverPath: Path = {
|
const mockHttpService = {
|
||||||
type: PathType.DRIVER,
|
get: jest
|
||||||
points: [
|
.fn()
|
||||||
{
|
.mockImplementationOnce(() => {
|
||||||
lon: 6,
|
return throwError(
|
||||||
lat: 47,
|
() => new AxiosError('Axios error', AxiosError.ERR_BAD_REQUEST),
|
||||||
},
|
);
|
||||||
{
|
})
|
||||||
lon: 6.1,
|
.mockImplementationOnce(() => {
|
||||||
lat: 47.1,
|
return throwError(() => 'Router unavailable');
|
||||||
},
|
})
|
||||||
{
|
.mockImplementationOnce(() => {
|
||||||
lon: 6.2,
|
return of({
|
||||||
lat: 47.2,
|
status: 200,
|
||||||
},
|
data: {
|
||||||
],
|
paths: [
|
||||||
|
{
|
||||||
|
distance: 50000,
|
||||||
|
time: 1800000,
|
||||||
|
snapped_waypoints: {
|
||||||
|
coordinates: [
|
||||||
|
[0, 0],
|
||||||
|
[10, 10],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return of({
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
distance: 50000,
|
||||||
|
time: 1800000,
|
||||||
|
points: {
|
||||||
|
coordinates: [
|
||||||
|
[0, 0],
|
||||||
|
[1, 1],
|
||||||
|
[2, 2],
|
||||||
|
[3, 3],
|
||||||
|
[4, 4],
|
||||||
|
[5, 5],
|
||||||
|
[6, 6],
|
||||||
|
[7, 7],
|
||||||
|
[8, 8],
|
||||||
|
[9, 9],
|
||||||
|
[10, 10],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
snapped_waypoints: {
|
||||||
|
coordinates: [
|
||||||
|
[0, 0],
|
||||||
|
[10, 10],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return of({
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
distance: 50000,
|
||||||
|
time: 1800000,
|
||||||
|
points: {
|
||||||
|
coordinates: [
|
||||||
|
[0, 0],
|
||||||
|
[1, 1],
|
||||||
|
[2, 2],
|
||||||
|
[3, 3],
|
||||||
|
[4, 4],
|
||||||
|
[5, 5],
|
||||||
|
[6, 6],
|
||||||
|
[7, 7],
|
||||||
|
[8, 8],
|
||||||
|
[9, 9],
|
||||||
|
[10, 10],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
time: [
|
||||||
|
[0, 1, 180000],
|
||||||
|
[1, 2, 180000],
|
||||||
|
[2, 3, 180000],
|
||||||
|
[3, 4, 180000],
|
||||||
|
[4, 5, 180000],
|
||||||
|
[5, 6, 180000],
|
||||||
|
[6, 7, 180000],
|
||||||
|
[7, 9, 360000],
|
||||||
|
[9, 10, 180000],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
snapped_waypoints: {
|
||||||
|
coordinates: [
|
||||||
|
[0, 0],
|
||||||
|
[10, 10],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return of({
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
distance: 50000,
|
||||||
|
time: 1800000,
|
||||||
|
points: {
|
||||||
|
coordinates: [
|
||||||
|
[0, 0],
|
||||||
|
[1, 1],
|
||||||
|
[2, 2],
|
||||||
|
[3, 3],
|
||||||
|
[4, 4],
|
||||||
|
[7, 7],
|
||||||
|
[8, 8],
|
||||||
|
[9, 9],
|
||||||
|
[10, 10],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
snapped_waypoints: {
|
||||||
|
coordinates: [
|
||||||
|
[0, 0],
|
||||||
|
[5, 5],
|
||||||
|
[10, 10],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
time: [
|
||||||
|
[0, 1, 180000],
|
||||||
|
[1, 2, 180000],
|
||||||
|
[2, 3, 180000],
|
||||||
|
[3, 4, 180000],
|
||||||
|
[4, 7, 540000],
|
||||||
|
[7, 9, 360000],
|
||||||
|
[9, 10, 180000],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return of({
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
distance: 50000,
|
||||||
|
time: 1800000,
|
||||||
|
points: {
|
||||||
|
coordinates: [
|
||||||
|
[0, 0],
|
||||||
|
[1, 1],
|
||||||
|
[2, 2],
|
||||||
|
[3, 3],
|
||||||
|
[4, 4],
|
||||||
|
[5, 5],
|
||||||
|
[6, 6],
|
||||||
|
[7, 7],
|
||||||
|
[8, 8],
|
||||||
|
[9, 9],
|
||||||
|
[10, 10],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
snapped_waypoints: {
|
||||||
|
coordinates: [
|
||||||
|
[0, 0],
|
||||||
|
[5, 5],
|
||||||
|
[10, 10],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
time: [
|
||||||
|
[0, 1, 180000],
|
||||||
|
[1, 2, 180000],
|
||||||
|
[2, 3, 180000],
|
||||||
|
[3, 4, 180000],
|
||||||
|
[4, 7, 540000],
|
||||||
|
[7, 9, 360000],
|
||||||
|
[9, 10, 180000],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
instructions: [
|
||||||
|
{
|
||||||
|
distance: 25000,
|
||||||
|
sign: 0,
|
||||||
|
interval: [0, 5],
|
||||||
|
text: 'Some instructions',
|
||||||
|
time: 900000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
distance: 0,
|
||||||
|
sign: 5,
|
||||||
|
interval: [5, 5],
|
||||||
|
text: 'Waypoint 1',
|
||||||
|
time: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
distance: 25000,
|
||||||
|
sign: 2,
|
||||||
|
interval: [5, 10],
|
||||||
|
text: 'Some instructions',
|
||||||
|
time: 900000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
distance: 0.0,
|
||||||
|
sign: 4,
|
||||||
|
interval: [10, 10],
|
||||||
|
text: 'Arrive at destination',
|
||||||
|
time: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockHttpService = {};
|
const mockGeodesic: GeodesicPort = {
|
||||||
|
inverse: jest.fn().mockImplementation(() => ({
|
||||||
|
azimuth: 45,
|
||||||
|
distance: 50000,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
const mockDefaultParamsProvider: DefaultParamsProviderPort = {
|
const mockDefaultParamsProvider: DefaultParamsProviderPort = {
|
||||||
getParams: jest.fn().mockImplementation(() => ({
|
getParams: jest.fn().mockImplementation(() => ({
|
||||||
|
@ -51,6 +277,10 @@ describe('Graphhopper Georouter', () => {
|
||||||
provide: PARAMS_PROVIDER,
|
provide: PARAMS_PROVIDER,
|
||||||
useValue: mockDefaultParamsProvider,
|
useValue: mockDefaultParamsProvider,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: GEODESIC,
|
||||||
|
useValue: mockGeodesic,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
@ -62,13 +292,209 @@ describe('Graphhopper Georouter', () => {
|
||||||
expect(graphhopperGeorouter).toBeDefined();
|
expect(graphhopperGeorouter).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return basic driver routes', async () => {
|
it('should fail if route is not found', async () => {
|
||||||
const paths: Path[] = [driverPath];
|
await expect(
|
||||||
const driverRoutes: Route[] = await graphhopperGeorouter.routes(paths, {
|
graphhopperGeorouter.routes(
|
||||||
detailedDistance: false,
|
[
|
||||||
detailedDuration: false,
|
{
|
||||||
points: true,
|
type: PathType.DRIVER,
|
||||||
});
|
points: [
|
||||||
expect(driverRoutes.length).toBe(1);
|
{
|
||||||
|
lon: 0,
|
||||||
|
lat: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 1,
|
||||||
|
lat: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
detailedDistance: false,
|
||||||
|
detailedDuration: false,
|
||||||
|
points: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).rejects.toBeInstanceOf(RouteNotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if georouter is unavailable', async () => {
|
||||||
|
await expect(
|
||||||
|
graphhopperGeorouter.routes(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: PathType.DRIVER,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
lon: 0,
|
||||||
|
lat: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 1,
|
||||||
|
lat: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
detailedDistance: false,
|
||||||
|
detailedDuration: false,
|
||||||
|
points: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).rejects.toBeInstanceOf(GeorouterUnavailableException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a basic route', async () => {
|
||||||
|
const routes: Route[] = await graphhopperGeorouter.routes(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: PathType.DRIVER,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
lon: 0,
|
||||||
|
lat: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 10,
|
||||||
|
lat: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
detailedDistance: false,
|
||||||
|
detailedDuration: false,
|
||||||
|
points: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(routes).toHaveLength(1);
|
||||||
|
expect(routes[0].distance).toBe(50000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create one route with points', async () => {
|
||||||
|
const routes = await graphhopperGeorouter.routes(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: PathType.DRIVER,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
lat: 0,
|
||||||
|
lon: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lat: 10,
|
||||||
|
lon: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
detailedDistance: false,
|
||||||
|
detailedDuration: false,
|
||||||
|
points: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(routes).toHaveLength(1);
|
||||||
|
expect(routes[0].distance).toBe(50000);
|
||||||
|
expect(routes[0].duration).toBe(1800);
|
||||||
|
expect(routes[0].fwdAzimuth).toBe(45);
|
||||||
|
expect(routes[0].backAzimuth).toBe(225);
|
||||||
|
expect(routes[0].points).toHaveLength(11);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create one route with points and time', async () => {
|
||||||
|
const routes = await graphhopperGeorouter.routes(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: PathType.DRIVER,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
lat: 0,
|
||||||
|
lon: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lat: 10,
|
||||||
|
lon: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
detailedDistance: false,
|
||||||
|
detailedDuration: true,
|
||||||
|
points: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(routes).toHaveLength(1);
|
||||||
|
expect(routes[0].spacetimeWaypoints).toHaveLength(2);
|
||||||
|
expect(routes[0].spacetimeWaypoints[1].duration).toBe(1800);
|
||||||
|
expect(routes[0].spacetimeWaypoints[1].distance).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create one route with points and missed waypoints extrapolations', async () => {
|
||||||
|
const routes = await graphhopperGeorouter.routes(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: PathType.DRIVER,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
lat: 0,
|
||||||
|
lon: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lat: 5,
|
||||||
|
lon: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lat: 10,
|
||||||
|
lon: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
detailedDistance: false,
|
||||||
|
detailedDuration: true,
|
||||||
|
points: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(routes).toHaveLength(1);
|
||||||
|
expect(routes[0].spacetimeWaypoints).toHaveLength(3);
|
||||||
|
expect(routes[0].distance).toBe(50000);
|
||||||
|
expect(routes[0].duration).toBe(1800);
|
||||||
|
expect(routes[0].fwdAzimuth).toBe(45);
|
||||||
|
expect(routes[0].backAzimuth).toBe(225);
|
||||||
|
expect(routes[0].points.length).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create one route with points, time and distance', async () => {
|
||||||
|
const routes = await graphhopperGeorouter.routes(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: PathType.DRIVER,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
lat: 0,
|
||||||
|
lon: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lat: 10,
|
||||||
|
lon: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
detailedDistance: true,
|
||||||
|
detailedDuration: true,
|
||||||
|
points: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(routes).toHaveLength(1);
|
||||||
|
expect(routes[0].spacetimeWaypoints.length).toBe(3);
|
||||||
|
expect(routes[0].spacetimeWaypoints[1].duration).toBe(990);
|
||||||
|
expect(routes[0].spacetimeWaypoints[1].distance).toBe(25000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue