diff --git a/src/modules/geography/core/application/ports/direction-encoder.port.ts b/src/modules/geography/core/application/ports/direction-encoder.port.ts index 3251915..737456a 100644 --- a/src/modules/geography/core/application/ports/direction-encoder.port.ts +++ b/src/modules/geography/core/application/ports/direction-encoder.port.ts @@ -1,4 +1,4 @@ -import { Coordinates } from '../types/coordinates.type'; +import { Coordinates } from '../../domain/route.types'; export interface DirectionEncoderPort { encode(coordinates: Coordinates[]): string; diff --git a/src/modules/geography/core/application/ports/georouter-creator.port.ts b/src/modules/geography/core/application/ports/georouter-creator.port.ts deleted file mode 100644 index 0f3957d..0000000 --- a/src/modules/geography/core/application/ports/georouter-creator.port.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { GeorouterPort } from './georouter.port'; - -export interface GeorouterCreatorPort { - create(type: string, url: string): GeorouterPort; -} diff --git a/src/modules/geography/core/application/queries/get-route/get-route.query.ts b/src/modules/geography/core/application/queries/get-route/get-route.query.ts index 4b8c6fd..eef3ed1 100644 --- a/src/modules/geography/core/application/queries/get-route/get-route.query.ts +++ b/src/modules/geography/core/application/queries/get-route/get-route.query.ts @@ -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 { diff --git a/src/modules/geography/core/application/types/coordinates.type.ts b/src/modules/geography/core/application/types/coordinates.type.ts deleted file mode 100644 index 8e149ed..0000000 --- a/src/modules/geography/core/application/types/coordinates.type.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Coordinates = { - lon: number; - lat: number; -}; diff --git a/src/modules/geography/core/application/types/path.type.ts b/src/modules/geography/core/application/types/path.type.ts deleted file mode 100644 index 2f8e46e..0000000 --- a/src/modules/geography/core/application/types/path.type.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { PathType } from '../../domain/route.types'; -import { Coordinates } from './coordinates.type'; - -export type Path = { - type: PathType; - points: Coordinates[]; -}; diff --git a/src/modules/geography/core/application/types/route.type.ts b/src/modules/geography/core/application/types/route.type.ts deleted file mode 100644 index 791a00e..0000000 --- a/src/modules/geography/core/application/types/route.type.ts +++ /dev/null @@ -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[]; -}; diff --git a/src/modules/geography/core/application/types/spacetime-point.type.ts b/src/modules/geography/core/application/types/spacetime-point.type.ts deleted file mode 100644 index 9bed0c5..0000000 --- a/src/modules/geography/core/application/types/spacetime-point.type.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Coordinates } from './coordinates.type'; - -export type SpacetimePoint = Coordinates & { - duration: number; - distance: number; -}; diff --git a/src/modules/geography/core/application/types/waypoint.type.ts b/src/modules/geography/core/application/types/waypoint.type.ts deleted file mode 100644 index 3c635d5..0000000 --- a/src/modules/geography/core/application/types/waypoint.type.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Coordinates } from './coordinates.type'; - -export type Waypoint = Coordinates & { - position: number; -}; diff --git a/src/modules/geography/core/domain/route.entity.ts b/src/modules/geography/core/domain/route.entity.ts index dc81161..46177b7 100644 --- a/src/modules/geography/core/domain/route.entity.ts +++ b/src/modules/geography/core/domain/route.entity.ts @@ -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 { protected readonly _id: AggregateID; static create = async (create: CreateRouteProps): Promise => { - 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 = { diff --git a/src/modules/geography/core/domain/route.errors.ts b/src/modules/geography/core/domain/route.errors.ts index a0f484b..420f096 100644 --- a/src/modules/geography/core/domain/route.errors.ts +++ b/src/modules/geography/core/domain/route.errors.ts @@ -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); + } +} diff --git a/src/modules/geography/core/domain/route.types.ts b/src/modules/geography/core/domain/route.types.ts index 7fe7776..ba47f02 100644 --- a/src/modules/geography/core/domain/route.types.ts +++ b/src/modules/geography/core/domain/route.types.ts @@ -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; }; diff --git a/src/modules/geography/infrastructure/graphhopper-georouter.ts b/src/modules/geography/infrastructure/graphhopper-georouter.ts new file mode 100644 index 0000000..a6f6f3a --- /dev/null +++ b/src/modules/geography/infrastructure/graphhopper-georouter.ts @@ -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 => { + 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 => { + 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, + 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, +} diff --git a/src/modules/geography/infrastructure/postgres-direction-encoder.ts b/src/modules/geography/infrastructure/postgres-direction-encoder.ts index b4c8001..d6cb0b6 100644 --- a/src/modules/geography/infrastructure/postgres-direction-encoder.ts +++ b/src/modules/geography/infrastructure/postgres-direction-encoder.ts @@ -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 { diff --git a/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts b/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts index c6948df..7790bba 100644 --- a/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts +++ b/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts @@ -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[]; diff --git a/src/modules/geography/interface/dtos/route.response.dto.ts b/src/modules/geography/interface/dtos/route.response.dto.ts index 58d3c52..714fb68 100644 --- a/src/modules/geography/interface/dtos/route.response.dto.ts +++ b/src/modules/geography/interface/dtos/route.response.dto.ts @@ -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; diff --git a/src/modules/geography/tests/unit/core/get-route.query-handler.spec.ts b/src/modules/geography/tests/unit/core/get-route.query-handler.spec.ts index d9c142e..d8df206 100644 --- a/src/modules/geography/tests/unit/core/get-route.query-handler.spec.ts +++ b/src/modules/geography/tests/unit/core/get-route.query-handler.spec.ts @@ -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'; diff --git a/src/modules/geography/tests/unit/core/route.entity.spec.ts b/src/modules/geography/tests/unit/core/route.entity.spec.ts index 5c0eaf3..edbe7a9 100644 --- a/src/modules/geography/tests/unit/core/route.entity.spec.ts +++ b/src/modules/geography/tests/unit/core/route.entity.spec.ts @@ -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, diff --git a/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts b/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts new file mode 100644 index 0000000..adda4e8 --- /dev/null +++ b/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts @@ -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); + }); + + 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); + }); +}); diff --git a/src/modules/geography/tests/unit/infrastructure/postgres-direction-encoder.spec.ts b/src/modules/geography/tests/unit/infrastructure/postgres-direction-encoder.spec.ts index 6f12ed5..fd4cbab 100644 --- a/src/modules/geography/tests/unit/infrastructure/postgres-direction-encoder.spec.ts +++ b/src/modules/geography/tests/unit/infrastructure/postgres-direction-encoder.spec.ts @@ -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', () => {