add graphhopper georouter

This commit is contained in:
sbriat 2023-08-23 11:35:48 +02:00
parent a6836b168c
commit 158b12b150
19 changed files with 253 additions and 71 deletions

View File

@ -1,4 +1,4 @@
import { Coordinates } from '../types/coordinates.type';
import { Coordinates } from '../../domain/route.types';
export interface DirectionEncoderPort {
encode(coordinates: Coordinates[]): string;

View File

@ -1,5 +0,0 @@
import { GeorouterPort } from './georouter.port';
export interface GeorouterCreatorPort {
create(type: string, url: string): GeorouterPort;
}

View File

@ -1,6 +1,5 @@
import { QueryBase } from '@mobicoop/ddd-library';
import { Role } from '@modules/geography/core/domain/route.types';
import { Waypoint } from '../../types/waypoint.type';
import { Role, Waypoint } from '@modules/geography/core/domain/route.types';
import { GeorouterSettings } from '../../types/georouter-settings.type';
export class GetRouteQuery extends QueryBase {

View File

@ -1,4 +0,0 @@
export type Coordinates = {
lon: number;
lat: number;
};

View File

@ -1,7 +0,0 @@
import { PathType } from '../../domain/route.types';
import { Coordinates } from './coordinates.type';
export type Path = {
type: PathType;
points: Coordinates[];
};

View File

@ -1,13 +0,0 @@
import { PathType } from '../../domain/route.types';
import { Coordinates } from './coordinates.type';
import { SpacetimePoint } from './spacetime-point.type';
export type Route = {
type: PathType;
distance: number;
duration: number;
fwdAzimuth: number;
backAzimuth: number;
distanceAzimuth: number;
points: Coordinates[] | SpacetimePoint[];
};

View File

@ -1,6 +0,0 @@
import { Coordinates } from './coordinates.type';
export type SpacetimePoint = Coordinates & {
duration: number;
distance: number;
};

View File

@ -1,5 +0,0 @@
import { Coordinates } from './coordinates.type';
export type Waypoint = Coordinates & {
position: number;
};

View File

@ -5,10 +5,9 @@ import {
Role,
RouteProps,
PathType,
Direction,
Route,
} from './route.types';
import { WaypointProps } from './value-objects/waypoint.value-object';
import { Route } from '../application/types/route.type';
import { v4 } from 'uuid';
import { RouteNotFoundException } from './route.errors';
@ -16,33 +15,30 @@ export class RouteEntity extends AggregateRoot<RouteProps> {
protected readonly _id: AggregateID;
static create = async (create: CreateRouteProps): Promise<RouteEntity> => {
let directions: Direction[];
let routes: Route[];
try {
directions = await create.georouter.routes(
routes = await create.georouter.routes(
this.getPaths(create.roles, create.waypoints),
create.georouterSettings,
);
if (!directions || directions.length == 0)
throw new RouteNotFoundException();
if (!routes || routes.length == 0) throw new RouteNotFoundException();
} catch (e: any) {
throw e;
}
let driverRoute: Route;
let passengerRoute: Route;
if (directions.some((route: Route) => route.type == PathType.GENERIC)) {
driverRoute = passengerRoute = directions.find(
if (routes.some((route: Route) => route.type == PathType.GENERIC)) {
driverRoute = passengerRoute = routes.find(
(route: Route) => route.type == PathType.GENERIC,
);
} else {
driverRoute = directions.some(
(route: Route) => route.type == PathType.DRIVER,
)
? directions.find((route: Route) => route.type == PathType.DRIVER)
driverRoute = routes.some((route: Route) => route.type == PathType.DRIVER)
? routes.find((route: Route) => route.type == PathType.DRIVER)
: undefined;
passengerRoute = directions.some(
passengerRoute = routes.some(
(route: Route) => route.type == PathType.PASSENGER,
)
? directions.find((route: Route) => route.type == PathType.PASSENGER)
? routes.find((route: Route) => route.type == PathType.PASSENGER)
: undefined;
}
const routeProps: RouteProps = {

View File

@ -9,3 +9,13 @@ export class RouteNotFoundException extends ExceptionBase {
super(RouteNotFoundException.message, cause, metadata);
}
}
export class GeorouterUnavailableException extends ExceptionBase {
static readonly message = 'Georouter unavailable';
public readonly code = 'GEOROUTER.UNAVAILABLE';
constructor(cause?: Error, metadata?: unknown) {
super(GeorouterUnavailableException.message, cause, metadata);
}
}

View File

@ -25,27 +25,31 @@ export interface CreateRouteProps {
georouterSettings: GeorouterSettings;
}
export type Direction = {
export type Route = {
type: PathType;
distance: number;
duration: number;
fwdAzimuth: number;
backAzimuth: number;
distanceAzimuth: number;
points: SpacetimePoint[] | Point[];
points: SpacetimePoint[] | Coordinates[];
};
export type Path = {
type: PathType;
points: Point[];
points: Coordinates[];
};
export type Point = {
export type Coordinates = {
lon: number;
lat: number;
};
export type SpacetimePoint = Point & {
export type Waypoint = Coordinates & {
position: number;
};
export type SpacetimePoint = Coordinates & {
duration: number;
distance: number;
};

View File

@ -0,0 +1,139 @@
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 { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port';
import { 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';
@Injectable()
export class GraphhopperGeorouter implements GeorouterPort {
private url: string;
private urlArgs: string[];
constructor(
private readonly httpService: HttpService,
@Inject(PARAMS_PROVIDER)
private readonly defaultParamsProvider: DefaultParamsProviderPort,
) {
this.url = defaultParamsProvider.getParams().GEOROUTER_URL;
}
routes = async (
paths: Path[],
settings: GeorouterSettings,
): Promise<Route[]> => {
this.setDefaultUrlArgs();
this.setSettings(settings);
return [
{
type: PathType.DRIVER,
distance: 1000,
duration: 1000,
fwdAzimuth: 280,
backAzimuth: 100,
distanceAzimuth: 900,
points: [],
},
];
};
private setDefaultUrlArgs = (): void => {
this.urlArgs = ['vehicle=car', 'weighting=fastest', 'points_encoded=false'];
};
private setSettings = (settings: GeorouterSettings): void => {
if (settings.detailedDuration) {
this.urlArgs.push('details=time');
}
if (settings.detailedDistance) {
this.urlArgs.push('instructions=true');
} else {
this.urlArgs.push('instructions=false');
}
if (!settings.points) {
this.urlArgs.push('calc_points=false');
}
};
private getRoutes = async (paths: Path[]): Promise<Route[]> => {
const routes = Promise.all(
paths.map(async (path) => {
const url: string = [
this.getUrl(),
'&point=',
path.points
.map((point) => [point.lat, point.lon].join('%2C'))
.join('&point='),
].join('');
const route = await lastValueFrom(
this.httpService.get(url).pipe(
map((res) =>
res.data ? this.createRoute(res, path.type) : undefined,
),
catchError((error: AxiosError) => {
if (error.code == AxiosError.ERR_BAD_REQUEST) {
throw new RouteNotFoundException(
error,
'No route found for given coordinates',
);
}
throw new GeorouterUnavailableException(error);
}),
),
);
return route;
}),
);
return routes;
};
private getUrl = (): string => [this.url, this.urlArgs.join('&')].join('');
private createRoute = (
response: AxiosResponse<GraphhopperResponse>,
type: PathType,
): Route => undefined;
}
type GraphhopperResponse = {
paths: [
{
distance: number;
weight: number;
time: number;
points_encoded: boolean;
bbox: number[];
points: GraphhopperCoordinates;
snapped_waypoints: GraphhopperCoordinates;
details: {
time: number[];
};
instructions: GraphhopperInstruction[];
},
];
};
type GraphhopperCoordinates = {
coordinates: number[];
};
type GraphhopperInstruction = {
distance: number;
heading: number;
sign: GraphhopperSign;
interval: number[];
text: string;
};
enum GraphhopperSign {
SIGN_START = 0,
SIGN_FINISH = 4,
SIGN_WAYPOINT = 5,
}

View File

@ -1,6 +1,6 @@
import { Coordinates } from '../core/application/types/coordinates.type';
import { DirectionEncoderPort } from '../core/application/ports/direction-encoder.port';
import { Injectable } from '@nestjs/common';
import { Coordinates } from '../core/domain/route.types';
@Injectable()
export class PostgresDirectionEncoder implements DirectionEncoderPort {

View File

@ -1,5 +1,4 @@
import { Waypoint } from '@modules/geography/core/application/types/waypoint.type';
import { Role } from '@modules/geography/core/domain/route.types';
import { Role, Waypoint } from '@modules/geography/core/domain/route.types';
export type GetRouteRequestDto = {
roles: Role[];

View File

@ -1,5 +1,7 @@
import { Coordinates } from '@modules/geography/core/application/types/coordinates.type';
import { SpacetimePoint } from '@modules/geography/core/application/types/spacetime-point.type';
import {
Coordinates,
SpacetimePoint,
} from '@modules/geography/core/domain/route.types';
export class RouteResponseDto {
driverDistance?: number;

View File

@ -1,9 +1,8 @@
import { GeorouterPort } from '@modules/geography/core/application/ports/georouter.port';
import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query';
import { GetRouteQueryHandler } from '@modules/geography/core/application/queries/get-route/get-route.query-handler';
import { Waypoint } from '@modules/geography/core/application/types/waypoint.type';
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
import { Role } from '@modules/geography/core/domain/route.types';
import { Role, Waypoint } from '@modules/geography/core/domain/route.types';
import { GEOROUTER } from '@modules/geography/geography.di-tokens';
import { Test, TestingModule } from '@nestjs/testing';

View File

@ -1,8 +1,8 @@
import { GeorouterPort } from '@modules/geography/core/application/ports/georouter.port';
import { Coordinates } from '@modules/geography/core/application/types/coordinates.type';
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
import { RouteNotFoundException } from '@modules/geography/core/domain/route.errors';
import {
Coordinates,
CreateRouteProps,
PathType,
Role,

View File

@ -0,0 +1,74 @@
import { DefaultParamsProviderPort } from '@modules/geography/core/application/ports/default-params-provider.port';
import {
Path,
PathType,
Route,
} from '@modules/geography/core/domain/route.types';
import { PARAMS_PROVIDER } from '@modules/geography/geography.di-tokens';
import { GraphhopperGeorouter } from '@modules/geography/infrastructure/graphhopper-georouter';
import { HttpService } from '@nestjs/axios';
import { Test, TestingModule } from '@nestjs/testing';
const driverPath: Path = {
type: PathType.DRIVER,
points: [
{
lon: 6,
lat: 47,
},
{
lon: 6.1,
lat: 47.1,
},
{
lon: 6.2,
lat: 47.2,
},
],
};
const mockHttpService = {};
const mockDefaultParamsProvider: DefaultParamsProviderPort = {
getParams: jest.fn().mockImplementation(() => ({
GEOROUTER_URL: 'http://localhost:8989',
})),
};
describe('Graphhopper Georouter', () => {
let graphhopperGeorouter: GraphhopperGeorouter;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
GraphhopperGeorouter,
{
provide: HttpService,
useValue: mockHttpService,
},
{
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
},
],
}).compile();
graphhopperGeorouter =
module.get<GraphhopperGeorouter>(GraphhopperGeorouter);
});
it('should be defined', () => {
expect(graphhopperGeorouter).toBeDefined();
});
it('should return basic driver routes', async () => {
const paths: Path[] = [driverPath];
const driverRoutes: Route[] = await graphhopperGeorouter.routes(paths, {
detailedDistance: false,
detailedDuration: false,
points: true,
});
expect(driverRoutes.length).toBe(1);
});
});

View File

@ -1,4 +1,4 @@
import { Coordinates } from '@modules/geography/core/application/types/coordinates.type';
import { Coordinates } from '@modules/geography/core/domain/route.types';
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
describe('Postgres direction encoder', () => {