add graphhopper georouter
This commit is contained in:
parent
a6836b168c
commit
158b12b150
|
@ -1,4 +1,4 @@
|
||||||
import { Coordinates } from '../types/coordinates.type';
|
import { Coordinates } from '../../domain/route.types';
|
||||||
|
|
||||||
export interface DirectionEncoderPort {
|
export interface DirectionEncoderPort {
|
||||||
encode(coordinates: Coordinates[]): string;
|
encode(coordinates: Coordinates[]): string;
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { GeorouterPort } from './georouter.port';
|
|
||||||
|
|
||||||
export interface GeorouterCreatorPort {
|
|
||||||
create(type: string, url: string): GeorouterPort;
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { QueryBase } from '@mobicoop/ddd-library';
|
import { QueryBase } from '@mobicoop/ddd-library';
|
||||||
import { Role } from '@modules/geography/core/domain/route.types';
|
import { Role, Waypoint } from '@modules/geography/core/domain/route.types';
|
||||||
import { Waypoint } from '../../types/waypoint.type';
|
|
||||||
import { GeorouterSettings } from '../../types/georouter-settings.type';
|
import { GeorouterSettings } from '../../types/georouter-settings.type';
|
||||||
|
|
||||||
export class GetRouteQuery extends QueryBase {
|
export class GetRouteQuery extends QueryBase {
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
export type Coordinates = {
|
|
||||||
lon: number;
|
|
||||||
lat: number;
|
|
||||||
};
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { PathType } from '../../domain/route.types';
|
|
||||||
import { Coordinates } from './coordinates.type';
|
|
||||||
|
|
||||||
export type Path = {
|
|
||||||
type: PathType;
|
|
||||||
points: Coordinates[];
|
|
||||||
};
|
|
|
@ -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[];
|
|
||||||
};
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { Coordinates } from './coordinates.type';
|
|
||||||
|
|
||||||
export type SpacetimePoint = Coordinates & {
|
|
||||||
duration: number;
|
|
||||||
distance: number;
|
|
||||||
};
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { Coordinates } from './coordinates.type';
|
|
||||||
|
|
||||||
export type Waypoint = Coordinates & {
|
|
||||||
position: number;
|
|
||||||
};
|
|
|
@ -5,10 +5,9 @@ import {
|
||||||
Role,
|
Role,
|
||||||
RouteProps,
|
RouteProps,
|
||||||
PathType,
|
PathType,
|
||||||
Direction,
|
Route,
|
||||||
} from './route.types';
|
} from './route.types';
|
||||||
import { WaypointProps } from './value-objects/waypoint.value-object';
|
import { WaypointProps } from './value-objects/waypoint.value-object';
|
||||||
import { Route } from '../application/types/route.type';
|
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { RouteNotFoundException } from './route.errors';
|
import { RouteNotFoundException } from './route.errors';
|
||||||
|
|
||||||
|
@ -16,33 +15,30 @@ export class RouteEntity extends AggregateRoot<RouteProps> {
|
||||||
protected readonly _id: AggregateID;
|
protected readonly _id: AggregateID;
|
||||||
|
|
||||||
static create = async (create: CreateRouteProps): Promise<RouteEntity> => {
|
static create = async (create: CreateRouteProps): Promise<RouteEntity> => {
|
||||||
let directions: Direction[];
|
let routes: Route[];
|
||||||
try {
|
try {
|
||||||
directions = await create.georouter.routes(
|
routes = await create.georouter.routes(
|
||||||
this.getPaths(create.roles, create.waypoints),
|
this.getPaths(create.roles, create.waypoints),
|
||||||
create.georouterSettings,
|
create.georouterSettings,
|
||||||
);
|
);
|
||||||
if (!directions || directions.length == 0)
|
if (!routes || routes.length == 0) throw new RouteNotFoundException();
|
||||||
throw new RouteNotFoundException();
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
let driverRoute: Route;
|
let driverRoute: Route;
|
||||||
let passengerRoute: Route;
|
let passengerRoute: Route;
|
||||||
if (directions.some((route: Route) => route.type == PathType.GENERIC)) {
|
if (routes.some((route: Route) => route.type == PathType.GENERIC)) {
|
||||||
driverRoute = passengerRoute = directions.find(
|
driverRoute = passengerRoute = routes.find(
|
||||||
(route: Route) => route.type == PathType.GENERIC,
|
(route: Route) => route.type == PathType.GENERIC,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
driverRoute = directions.some(
|
driverRoute = routes.some((route: Route) => route.type == PathType.DRIVER)
|
||||||
(route: Route) => route.type == PathType.DRIVER,
|
? routes.find((route: Route) => route.type == PathType.DRIVER)
|
||||||
)
|
|
||||||
? directions.find((route: Route) => route.type == PathType.DRIVER)
|
|
||||||
: undefined;
|
: undefined;
|
||||||
passengerRoute = directions.some(
|
passengerRoute = routes.some(
|
||||||
(route: Route) => route.type == PathType.PASSENGER,
|
(route: Route) => route.type == PathType.PASSENGER,
|
||||||
)
|
)
|
||||||
? directions.find((route: Route) => route.type == PathType.PASSENGER)
|
? routes.find((route: Route) => route.type == PathType.PASSENGER)
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
const routeProps: RouteProps = {
|
const routeProps: RouteProps = {
|
||||||
|
|
|
@ -9,3 +9,13 @@ export class RouteNotFoundException extends ExceptionBase {
|
||||||
super(RouteNotFoundException.message, cause, metadata);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -25,27 +25,31 @@ export interface CreateRouteProps {
|
||||||
georouterSettings: GeorouterSettings;
|
georouterSettings: GeorouterSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Direction = {
|
export type Route = {
|
||||||
type: PathType;
|
type: PathType;
|
||||||
distance: number;
|
distance: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
fwdAzimuth: number;
|
fwdAzimuth: number;
|
||||||
backAzimuth: number;
|
backAzimuth: number;
|
||||||
distanceAzimuth: number;
|
distanceAzimuth: number;
|
||||||
points: SpacetimePoint[] | Point[];
|
points: SpacetimePoint[] | Coordinates[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Path = {
|
export type Path = {
|
||||||
type: PathType;
|
type: PathType;
|
||||||
points: Point[];
|
points: Coordinates[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Point = {
|
export type Coordinates = {
|
||||||
lon: number;
|
lon: number;
|
||||||
lat: number;
|
lat: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SpacetimePoint = Point & {
|
export type Waypoint = Coordinates & {
|
||||||
|
position: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpacetimePoint = Coordinates & {
|
||||||
duration: number;
|
duration: number;
|
||||||
distance: number;
|
distance: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Coordinates } from '../core/application/types/coordinates.type';
|
|
||||||
import { DirectionEncoderPort } from '../core/application/ports/direction-encoder.port';
|
import { DirectionEncoderPort } from '../core/application/ports/direction-encoder.port';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Coordinates } from '../core/domain/route.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PostgresDirectionEncoder implements DirectionEncoderPort {
|
export class PostgresDirectionEncoder implements DirectionEncoderPort {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Waypoint } from '@modules/geography/core/application/types/waypoint.type';
|
import { Role, Waypoint } from '@modules/geography/core/domain/route.types';
|
||||||
import { Role } from '@modules/geography/core/domain/route.types';
|
|
||||||
|
|
||||||
export type GetRouteRequestDto = {
|
export type GetRouteRequestDto = {
|
||||||
roles: Role[];
|
roles: Role[];
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { Coordinates } from '@modules/geography/core/application/types/coordinates.type';
|
import {
|
||||||
import { SpacetimePoint } from '@modules/geography/core/application/types/spacetime-point.type';
|
Coordinates,
|
||||||
|
SpacetimePoint,
|
||||||
|
} from '@modules/geography/core/domain/route.types';
|
||||||
|
|
||||||
export class RouteResponseDto {
|
export class RouteResponseDto {
|
||||||
driverDistance?: number;
|
driverDistance?: number;
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { GeorouterPort } from '@modules/geography/core/application/ports/georouter.port';
|
import { GeorouterPort } from '@modules/geography/core/application/ports/georouter.port';
|
||||||
import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query';
|
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 { 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 { 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 { GEOROUTER } from '@modules/geography/geography.di-tokens';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { GeorouterPort } from '@modules/geography/core/application/ports/georouter.port';
|
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 { RouteEntity } from '@modules/geography/core/domain/route.entity';
|
||||||
import { RouteNotFoundException } from '@modules/geography/core/domain/route.errors';
|
import { RouteNotFoundException } from '@modules/geography/core/domain/route.errors';
|
||||||
import {
|
import {
|
||||||
|
Coordinates,
|
||||||
CreateRouteProps,
|
CreateRouteProps,
|
||||||
PathType,
|
PathType,
|
||||||
Role,
|
Role,
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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';
|
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||||
|
|
||||||
describe('Postgres direction encoder', () => {
|
describe('Postgres direction encoder', () => {
|
||||||
|
|
Loading…
Reference in New Issue