extract carpool informations from geography module

This commit is contained in:
sbriat 2023-09-07 14:30:07 +02:00
parent 57fe8d417f
commit d1a314f011
28 changed files with 586 additions and 611 deletions

View File

@ -4,7 +4,7 @@ import { Selector } from '../algorithm.abstract';
import { AdReadModel } from '@modules/ad/infrastructure/ad.repository';
import { ScheduleItem } from '../match.query';
import { Waypoint } from '../../../types/waypoint.type';
import { Coordinates } from '../../../types/coordinates.type';
import { Point } from '../../../types/point.type';
export class PassengerOrientedSelector extends Selector {
select = async (): Promise<Candidate[]> => {
@ -238,7 +238,7 @@ export class PassengerOrientedSelector extends Selector {
${this.query.remoteness}`;
case Role.DRIVER:
const lineStringPoints: string[] = [];
this.query.carpoolRoute?.points.forEach((point: Coordinates) =>
this.query.carpoolRoute?.points.forEach((point: Point) =>
lineStringPoints.push(
`public.st_makepoint(${point.lon},${point.lat})`,
),

View File

@ -1,4 +1,4 @@
import { Coordinates } from './coordinates.type';
import { Point } from './point.type';
export type Address = {
name?: string;
@ -7,4 +7,4 @@ export type Address = {
locality?: string;
postalCode?: string;
country?: string;
} & Coordinates;
} & Point;

View File

@ -1,4 +1,4 @@
import { Coordinates } from './coordinates.type';
import { Point } from './point.type';
/**
* A carpool route is a route with distance and duration as driver and / or passenger
@ -10,5 +10,5 @@ export type CarpoolRoute = {
passengerDuration?: number;
fwdAzimuth: number;
backAzimuth: number;
points: Coordinates[];
points: Point[];
};

View File

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

View File

@ -5,6 +5,7 @@ import { Waypoint } from '../core/application/types/waypoint.type';
import { Role } from '../core/domain/ad.types';
import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port';
import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens';
import { Route } from '@modules/geography/core/domain/route.types';
@Injectable()
export class CarpoolRouteProvider implements CarpoolRouteProviderPort {
@ -16,9 +17,116 @@ export class CarpoolRouteProvider implements CarpoolRouteProviderPort {
getBasic = async (
roles: Role[],
waypoints: Waypoint[],
): Promise<CarpoolRoute> =>
await this.getBasicRouteController.get({
roles,
): Promise<CarpoolRoute> => {
const paths: Path[] = this.getPaths(roles, waypoints);
const typeRoutes: TypeRoute[] = await Promise.all(
paths.map(
async (path: Path) =>
<TypeRoute>{
type: path.type,
route: await this.getBasicRouteController.get({
waypoints,
}),
},
),
);
return this._toCarpoolRoute(typeRoutes);
};
private _toCarpoolRoute = (typeRoutes: TypeRoute[]): CarpoolRoute => {
let baseRoute: Route;
let driverRoute: Route | undefined;
let passengerRoute: Route | undefined;
if (
typeRoutes.some(
(typeRoute: TypeRoute) => typeRoute.type == PathType.GENERIC,
)
) {
driverRoute = passengerRoute = typeRoutes.find(
(typeRoute: TypeRoute) => typeRoute.type == PathType.GENERIC,
)?.route;
} else {
driverRoute = typeRoutes.some(
(typeRoute: TypeRoute) => typeRoute.type == PathType.DRIVER,
)
? typeRoutes.find(
(typeRoute: TypeRoute) => typeRoute.type == PathType.DRIVER,
)?.route
: undefined;
passengerRoute = typeRoutes.some(
(typeRoute: TypeRoute) => typeRoute.type == PathType.PASSENGER,
)
? typeRoutes.find(
(typeRoute: TypeRoute) => typeRoute.type == PathType.PASSENGER,
)?.route
: undefined;
}
if (driverRoute) {
baseRoute = driverRoute;
} else {
baseRoute = passengerRoute as Route;
}
return {
driverDistance: driverRoute?.distance,
driverDuration: driverRoute?.duration,
passengerDistance: passengerRoute?.distance,
passengerDuration: passengerRoute?.duration,
fwdAzimuth: baseRoute.fwdAzimuth,
backAzimuth: baseRoute.backAzimuth,
points: baseRoute.points,
};
};
private getPaths = (roles: Role[], waypoints: Waypoint[]): Path[] => {
const paths: Path[] = [];
if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
if (waypoints.length == 2) {
// 2 points => same route for driver and passenger
paths.push(this.createGenericPath(waypoints));
} else {
paths.push(
this.createDriverPath(waypoints),
this.createPassengerPath(waypoints),
);
}
} else if (roles.includes(Role.DRIVER)) {
paths.push(this.createDriverPath(waypoints));
} else if (roles.includes(Role.PASSENGER)) {
paths.push(this.createPassengerPath(waypoints));
}
return paths;
};
private createGenericPath = (waypoints: Waypoint[]): Path =>
this.createPath(waypoints, PathType.GENERIC);
private createDriverPath = (waypoints: Waypoint[]): Path =>
this.createPath(waypoints, PathType.DRIVER);
private createPassengerPath = (waypoints: Waypoint[]): Path =>
this.createPath(
[waypoints[0], waypoints[waypoints.length - 1]],
PathType.PASSENGER,
);
private createPath = (waypoints: Waypoint[], type: PathType): Path => ({
type,
waypoints,
});
}
type Path = {
type: PathType;
waypoints: Waypoint[];
};
type TypeRoute = {
type: PathType;
route: Route;
};
enum PathType {
GENERIC = 'generic',
DRIVER = 'driver',
PASSENGER = 'passenger',
}

View File

@ -1,30 +1,133 @@
import { AD_GET_BASIC_ROUTE_CONTROLLER } from '@modules/ad/ad.di-tokens';
import { CarpoolRoute } from '@modules/ad/core/application/types/carpool-route.type';
import { Point } from '@modules/ad/core/application/types/point.type';
import { Role } from '@modules/ad/core/domain/ad.types';
import { CarpoolRouteProvider } from '@modules/ad/infrastructure/carpool-route-provider';
import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port';
import { Test, TestingModule } from '@nestjs/testing';
const originPoint: Point = {
lat: 48.689445,
lon: 6.17651,
};
const destinationPoint: Point = {
lat: 48.8566,
lon: 2.3522,
};
const additionalPoint: Point = {
lon: 48.7566,
lat: 4.4498,
};
const mockGetBasicRouteController: GetBasicRouteControllerPort = {
get: jest.fn().mockImplementation(() => ({
driverDistance: 23000,
driverDuration: 900,
passengerDistance: 23000,
passengerDuration: 900,
fwdAzimuth: 283,
get: jest
.fn()
.mockImplementationOnce(() => ({
distance: 350101,
duration: 14422,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 19840,
distanceAzimuth: 336544,
points: [
{
lon: 6.1765102,
lat: 48.689445,
},
{
lon: 4.984578,
lat: 48.725687,
},
{
lon: 2.3522,
lat: 48.8566,
},
],
}))
.mockImplementationOnce(() => ({
distance: 350102,
duration: 14423,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336545,
points: [
{
lon: 6.1765103,
lat: 48.689446,
},
{
lon: 4.984579,
lat: 48.725688,
},
{
lon: 2.3523,
lat: 48.8567,
},
],
})),
}))
.mockImplementationOnce(() => ({
distance: 350100,
duration: 14421,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336543,
points: [
{
lon: 6.1765101,
lat: 48.689444,
},
{
lon: 4.984577,
lat: 48.725686,
},
{
lon: 2.3521,
lat: 48.8565,
},
],
}))
.mockImplementationOnce(() => ({
distance: 350107,
duration: 14427,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336548,
points: [
{
lon: 6.1765101,
lat: 48.689444,
},
{
lon: 4.984577,
lat: 48.725686,
},
{
lon: 2.3521,
lat: 48.8565,
},
],
}))
.mockImplementationOnce(() => ({
distance: 350108,
duration: 14428,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336548,
points: [
{
lon: 6.1765101,
lat: 48.689444,
},
{
lon: 4.984577,
lat: 48.725686,
},
{
lon: 2.3521,
lat: 48.8565,
},
],
}))
.mockImplementationOnce(() => []),
};
describe('Carpool route provider', () => {
@ -49,22 +152,79 @@ describe('Carpool route provider', () => {
expect(carpoolRouteProvider).toBeDefined();
});
it('should provide a carpool route', async () => {
it('should provide a carpool route for a driver only', async () => {
const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic(
[Role.DRIVER],
[
{
position: 0,
lat: 48.689445,
lon: 6.1765102,
...originPoint,
},
{
position: 1,
lat: 48.8566,
lon: 2.3522,
...destinationPoint,
},
],
);
expect(carpoolRoute.driverDistance).toBe(23000);
expect(carpoolRoute.driverDistance).toBe(350101);
expect(carpoolRoute.passengerDuration).toBeUndefined();
});
it('should provide a carpool route for a passenger only', async () => {
const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic(
[Role.PASSENGER],
[
{
position: 0,
...originPoint,
},
{
position: 1,
...destinationPoint,
},
],
);
expect(carpoolRoute.passengerDistance).toBe(350102);
expect(carpoolRoute.driverDuration).toBeUndefined();
});
it('should provide a simple carpool route for a driver and passenger', async () => {
const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic(
[Role.DRIVER, Role.PASSENGER],
[
{
position: 0,
...originPoint,
},
{
position: 1,
...destinationPoint,
},
],
);
expect(carpoolRoute.driverDuration).toBe(14421);
expect(carpoolRoute.passengerDistance).toBe(350100);
});
it('should provide a complex carpool route for a driver and passenger', async () => {
const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic(
[Role.DRIVER, Role.PASSENGER],
[
{
position: 0,
...originPoint,
},
{
position: 1,
...additionalPoint,
},
{
position: 2,
...destinationPoint,
},
],
);
expect(carpoolRoute.driverDistance).toBe(350107);
expect(carpoolRoute.passengerDuration).toBe(14428);
});
});

View File

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

View File

@ -1,6 +1,6 @@
import { Path, Route } from '../../domain/route.types';
import { Route, Waypoint } from '../../domain/route.types';
import { GeorouterSettings } from '../types/georouter-settings.type';
export interface GeorouterPort {
routes(paths: Path[], settings: GeorouterSettings): Promise<Route[]>;
route(waypoints: Waypoint[], settings: GeorouterSettings): Promise<Route>;
}

View File

@ -11,7 +11,6 @@ export class GetRouteQueryHandler implements IQueryHandler {
execute = async (query: GetRouteQuery): Promise<RouteEntity> =>
await RouteEntity.create({
roles: query.roles,
waypoints: query.waypoints,
georouter: this.georouter,
georouterSettings: query.georouterSettings,

View File

@ -1,19 +1,13 @@
import { QueryBase } from '@mobicoop/ddd-library';
import { Role, Waypoint } from '@modules/geography/core/domain/route.types';
import { Waypoint } from '@modules/geography/core/domain/route.types';
import { GeorouterSettings } from '../../types/georouter-settings.type';
export class GetRouteQuery extends QueryBase {
readonly roles: Role[];
readonly waypoints: Waypoint[];
readonly georouterSettings: GeorouterSettings;
constructor(
roles: Role[],
waypoints: Waypoint[],
georouterSettings: GeorouterSettings,
) {
constructor(waypoints: Waypoint[], georouterSettings: GeorouterSettings) {
super();
this.roles = roles;
this.waypoints = waypoints;
this.georouterSettings = georouterSettings;
}

View File

@ -1,13 +1,5 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import {
CreateRouteProps,
Path,
Role,
RouteProps,
PathType,
Route,
} from './route.types';
import { WaypointProps } from './value-objects/waypoint.value-object';
import { CreateRouteProps, RouteProps, Route } from './route.types';
import { v4 } from 'uuid';
import { RouteNotFoundException } from './route.errors';
@ -15,43 +7,18 @@ export class RouteEntity extends AggregateRoot<RouteProps> {
protected readonly _id: AggregateID;
static create = async (create: CreateRouteProps): Promise<RouteEntity> => {
const routes: Route[] = await create.georouter.routes(
this.getPaths(create.roles, create.waypoints),
const route: Route = await create.georouter.route(
create.waypoints,
create.georouterSettings,
);
if (!routes || routes.length == 0) throw new RouteNotFoundException();
let baseRoute: Route;
let driverRoute: Route | undefined;
let passengerRoute: Route | undefined;
if (routes.some((route: Route) => route.type == PathType.GENERIC)) {
driverRoute = passengerRoute = routes.find(
(route: Route) => route.type == PathType.GENERIC,
);
} else {
driverRoute = routes.some((route: Route) => route.type == PathType.DRIVER)
? routes.find((route: Route) => route.type == PathType.DRIVER)
: undefined;
passengerRoute = routes.some(
(route: Route) => route.type == PathType.PASSENGER,
)
? routes.find((route: Route) => route.type == PathType.PASSENGER)
: undefined;
}
if (driverRoute) {
baseRoute = driverRoute;
} else {
baseRoute = passengerRoute as Route;
}
if (!route) throw new RouteNotFoundException();
const routeProps: RouteProps = {
driverDistance: driverRoute?.distance,
driverDuration: driverRoute?.duration,
passengerDistance: passengerRoute?.distance,
passengerDuration: passengerRoute?.duration,
fwdAzimuth: baseRoute.fwdAzimuth,
backAzimuth: baseRoute.backAzimuth,
distanceAzimuth: baseRoute.distanceAzimuth,
waypoints: create.waypoints,
points: baseRoute.points,
distance: route.distance,
duration: route.duration,
fwdAzimuth: route.fwdAzimuth,
backAzimuth: route.backAzimuth,
distanceAzimuth: route.distanceAzimuth,
points: route.points,
};
return new RouteEntity({
id: v4(),
@ -63,46 +30,46 @@ export class RouteEntity extends AggregateRoot<RouteProps> {
// entity business rules validation to protect it's invariant before saving entity to a database
}
private static getPaths = (
roles: Role[],
waypoints: WaypointProps[],
): Path[] => {
const paths: Path[] = [];
if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
if (waypoints.length == 2) {
// 2 points => same route for driver and passenger
paths.push(this.createGenericPath(waypoints));
} else {
paths.push(
this.createDriverPath(waypoints),
this.createPassengerPath(waypoints),
);
}
} else if (roles.includes(Role.DRIVER)) {
paths.push(this.createDriverPath(waypoints));
} else if (roles.includes(Role.PASSENGER)) {
paths.push(this.createPassengerPath(waypoints));
}
return paths;
};
// private static getPaths = (
// roles: Role[],
// waypoints: WaypointProps[],
// ): Path[] => {
// const paths: Path[] = [];
// if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
// if (waypoints.length == 2) {
// // 2 points => same route for driver and passenger
// paths.push(this.createGenericPath(waypoints));
// } else {
// paths.push(
// this.createDriverPath(waypoints),
// this.createPassengerPath(waypoints),
// );
// }
// } else if (roles.includes(Role.DRIVER)) {
// paths.push(this.createDriverPath(waypoints));
// } else if (roles.includes(Role.PASSENGER)) {
// paths.push(this.createPassengerPath(waypoints));
// }
// return paths;
// };
private static createGenericPath = (waypoints: WaypointProps[]): Path =>
this.createPath(waypoints, PathType.GENERIC);
// private static createGenericPath = (waypoints: WaypointProps[]): Path =>
// this.createPath(waypoints, PathType.GENERIC);
private static createDriverPath = (waypoints: WaypointProps[]): Path =>
this.createPath(waypoints, PathType.DRIVER);
// private static createDriverPath = (waypoints: WaypointProps[]): Path =>
// this.createPath(waypoints, PathType.DRIVER);
private static createPassengerPath = (waypoints: WaypointProps[]): Path =>
this.createPath(
[waypoints[0], waypoints[waypoints.length - 1]],
PathType.PASSENGER,
);
// private static createPassengerPath = (waypoints: WaypointProps[]): Path =>
// this.createPath(
// [waypoints[0], waypoints[waypoints.length - 1]],
// PathType.PASSENGER,
// );
private static createPath = (
points: WaypointProps[],
type: PathType,
): Path => ({
type,
points,
});
// private static createPath = (
// points: WaypointProps[],
// type: PathType,
// ): Path => ({
// type,
// points,
// });
}

View File

@ -1,67 +1,49 @@
import { GeorouterPort } from '../application/ports/georouter.port';
import { GeorouterSettings } from '../application/types/georouter-settings.type';
import { CoordinatesProps } from './value-objects/coordinates.value-object';
import { SpacetimePointProps } from './value-objects/spacetime-point.value-object';
import { PointProps } from './value-objects/point.value-object';
import { WaypointProps } from './value-objects/waypoint.value-object';
// All properties that a Route has
export interface RouteProps {
driverDistance?: number;
driverDuration?: number;
passengerDistance?: number;
passengerDuration?: number;
distance: number;
duration: number;
fwdAzimuth: number;
backAzimuth: number;
distanceAzimuth: number;
waypoints: WaypointProps[];
points: SpacetimePointProps[] | CoordinatesProps[];
points: PointProps[];
}
// Properties that are needed for a Route creation
export interface CreateRouteProps {
roles: Role[];
waypoints: WaypointProps[];
georouter: GeorouterPort;
georouterSettings: GeorouterSettings;
}
export type Route = {
type: PathType;
distance: number;
duration: number;
fwdAzimuth: number;
backAzimuth: number;
distanceAzimuth: number;
points: Coordinates[];
spacetimeWaypoints: SpacetimePoint[];
points: Point[];
steps: Step[];
};
export type Path = {
type: PathType;
points: Coordinates[];
};
export type Coordinates = {
export type Point = {
lon: number;
lat: number;
};
export type Waypoint = Coordinates & {
export type Waypoint = Point & {
position: number;
};
export type SpacetimePoint = Coordinates & {
export type Spacetime = {
duration: number;
distance?: number;
};
export enum Role {
DRIVER = 'DRIVER',
PASSENGER = 'PASSENGER',
}
export type Step = Point & Spacetime;
export enum PathType {
GENERIC = 'generic',
DRIVER = 'driver',
PASSENGER = 'passenger',
}
export type Waystep = Waypoint & Spacetime;

View File

@ -8,12 +8,12 @@ import {
* other Value Objects inside if needed.
* */
export interface CoordinatesProps {
export interface PointProps {
lon: number;
lat: number;
}
export class Coordinates extends ValueObject<CoordinatesProps> {
export class Point extends ValueObject<PointProps> {
get lon(): number {
return this.props.lon;
}
@ -22,7 +22,7 @@ export class Coordinates extends ValueObject<CoordinatesProps> {
return this.props.lat;
}
protected validate(props: CoordinatesProps): void {
protected validate(props: PointProps): void {
if (props.lon > 180 || props.lon < -180)
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
if (props.lat > 90 || props.lat < -90)

View File

@ -9,14 +9,14 @@ import {
* other Value Objects inside if needed.
* */
export interface SpacetimePointProps {
export interface StepProps {
lon: number;
lat: number;
duration: number;
distance: number;
}
export class SpacetimePoint extends ValueObject<SpacetimePointProps> {
export class Step extends ValueObject<StepProps> {
get lon(): number {
return this.props.lon;
}
@ -33,7 +33,7 @@ export class SpacetimePoint extends ValueObject<SpacetimePointProps> {
return this.props.distance;
}
protected validate(props: SpacetimePointProps): void {
protected validate(props: StepProps): void {
if (props.duration < 0)
throw new ArgumentInvalidException(
'duration must be greater than or equal to 0',

View File

@ -2,12 +2,7 @@ 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,
SpacetimePoint,
} from '../core/domain/route.types';
import { Route, Step, Waypoint } from '../core/domain/route.types';
import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port';
import { GEODESIC, PARAMS_PROVIDER } from '../geography.di-tokens';
import { catchError, lastValueFrom, map } from 'rxjs';
@ -35,13 +30,13 @@ export class GraphhopperGeorouter implements GeorouterPort {
].join('');
}
routes = async (
paths: Path[],
route = async (
waypoints: Waypoint[],
settings: GeorouterSettings,
): Promise<Route[]> => {
): Promise<Route> => {
this._setDefaultUrlArgs();
this._setSettings(settings);
return this._getRoutes(paths);
return this._getRoute(waypoints);
};
private _setDefaultUrlArgs = (): void => {
@ -62,20 +57,18 @@ export class GraphhopperGeorouter implements GeorouterPort {
}
};
private _getRoutes = async (paths: Path[]): Promise<Route[]> => {
const routes = Promise.all(
paths.map(async (path) => {
private _getRoute = async (waypoints: Waypoint[]): Promise<Route> => {
const url: string = [
this.getUrl(),
'&point=',
path.points
.map((point) => [point.lat, point.lon].join('%2C'))
waypoints
.map((waypoint: Waypoint) => [waypoint.lat, waypoint.lon].join('%2C'))
.join('&point='),
].join('');
return await lastValueFrom(
this.httpService.get(url).pipe(
map((response) => {
if (response.data) return this.createRoute(response, path.type);
if (response.data) return this.createRoute(response);
throw new Error();
}),
catchError((error: AxiosError) => {
@ -89,19 +82,14 @@ export class GraphhopperGeorouter implements GeorouterPort {
}),
),
);
}),
);
return routes;
};
private getUrl = (): string => [this.url, this.urlArgs.join('&')].join('');
private createRoute = (
response: AxiosResponse<GraphhopperResponse>,
type: PathType,
): 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;
@ -135,7 +123,7 @@ export class GraphhopperGeorouter implements GeorouterPort {
let instructions: GraphhopperInstruction[] = [];
if (shortestPath.instructions)
instructions = shortestPath.instructions;
route.spacetimeWaypoints = this.generateSpacetimePoints(
route.steps = this.generateSteps(
shortestPath.points.coordinates,
shortestPath.snapped_waypoints.coordinates,
shortestPath.details.time,
@ -147,12 +135,12 @@ export class GraphhopperGeorouter implements GeorouterPort {
return route;
};
private generateSpacetimePoints = (
private generateSteps = (
points: [[number, number]],
snappedWaypoints: [[number, number]],
durations: [[number, number, number]],
instructions: GraphhopperInstruction[],
): SpacetimePoint[] => {
): Step[] => {
const indices = this.getIndices(points, snappedWaypoints);
const times = this.getTimes(durations, indices);
const distances = this.getDistances(instructions, indices);

View File

@ -1,16 +1,16 @@
import { DirectionEncoderPort } from '../core/application/ports/direction-encoder.port';
import { Injectable } from '@nestjs/common';
import { Coordinates } from '../core/domain/route.types';
import { Point } from '../core/domain/route.types';
@Injectable()
export class PostgresDirectionEncoder implements DirectionEncoderPort {
encode = (coordinates: Coordinates[]): string =>
encode = (coordinates: Point[]): string =>
[
"'LINESTRING(",
coordinates.map((point) => [point.lon, point.lat].join(' ')).join(),
")'",
].join('');
decode = (direction: string): Coordinates[] =>
decode = (direction: string): Point[] =>
direction
.split('(')[1]
.split(')')[0]

View File

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

View File

@ -16,7 +16,7 @@ export class GetBasicRouteController implements GetBasicRouteControllerPort {
async get(data: GetRouteRequestDto): Promise<RouteResponseDto> {
const route: RouteEntity = await this.queryBus.execute(
new GetRouteQuery(data.roles, data.waypoints, {
new GetRouteQuery(data.waypoints, {
detailedDistance: false,
detailedDuration: false,
points: true,

View File

@ -1,15 +1,10 @@
import {
Coordinates,
SpacetimePoint,
} from '@modules/geography/core/domain/route.types';
import { Point, Step } from '@modules/geography/core/domain/route.types';
export class RouteResponseDto {
driverDistance?: number;
driverDuration?: number;
passengerDistance?: number;
passengerDuration?: number;
distance?: number;
duration?: number;
fwdAzimuth: number;
backAzimuth: number;
distanceAzimuth: number;
points: SpacetimePoint[] | Coordinates[];
points: Step[] | Point[];
}

View File

@ -16,17 +16,11 @@ export class RouteMapper
{
toResponse = (entity: RouteEntity): RouteResponseDto => {
const response = new RouteResponseDto();
response.driverDistance = entity.getProps().driverDistance
? Math.round(entity.getProps().driverDistance as number)
response.distance = entity.getProps().distance
? Math.round(entity.getProps().distance as number)
: undefined;
response.driverDuration = entity.getProps().driverDuration
? Math.round(entity.getProps().driverDuration as number)
: undefined;
response.passengerDistance = entity.getProps().passengerDistance
? Math.round(entity.getProps().passengerDistance as number)
: undefined;
response.passengerDuration = entity.getProps().passengerDuration
? Math.round(entity.getProps().passengerDuration as number)
response.duration = entity.getProps().duration
? Math.round(entity.getProps().duration as number)
: undefined;
response.fwdAzimuth = Math.round(entity.getProps().fwdAzimuth);
response.backAzimuth = Math.round(entity.getProps().backAzimuth);

View File

@ -2,7 +2,7 @@ import { GeorouterPort } from '@modules/geography/core/application/ports/georout
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 { RouteEntity } from '@modules/geography/core/domain/route.entity';
import { Role, Waypoint } from '@modules/geography/core/domain/route.types';
import { Waypoint } from '@modules/geography/core/domain/route.types';
import { GEOROUTER } from '@modules/geography/geography.di-tokens';
import { Test, TestingModule } from '@nestjs/testing';
@ -18,7 +18,7 @@ const destinationWaypoint: Waypoint = {
};
const mockGeorouter: GeorouterPort = {
routes: jest.fn(),
route: jest.fn(),
};
describe('Get route query handler', () => {
@ -44,9 +44,8 @@ describe('Get route query handler', () => {
});
describe('execution', () => {
it('should get a route for a driver only', async () => {
it('should get a route', async () => {
const getRoutequery = new GetRouteQuery(
[Role.DRIVER],
[originWaypoint, destinationWaypoint],
{
detailedDistance: false,

View File

@ -1,18 +1,18 @@
import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library';
import { Coordinates } from '@modules/geography/core/domain/value-objects/coordinates.value-object';
import { Point } from '@modules/geography/core/domain/value-objects/point.value-object';
describe('Waypoint value object', () => {
it('should create a waypoint value object', () => {
const coordinatesVO = new Coordinates({
describe('Point value object', () => {
it('should create a point value object', () => {
const pointVO = new Point({
lat: 48.689445,
lon: 6.17651,
});
expect(coordinatesVO.lat).toBe(48.689445);
expect(coordinatesVO.lon).toBe(6.17651);
expect(pointVO.lat).toBe(48.689445);
expect(pointVO.lon).toBe(6.17651);
});
it('should throw an exception if longitude is invalid', () => {
try {
new Coordinates({
new Point({
lat: 48.689445,
lon: 186.17651,
});
@ -20,7 +20,7 @@ describe('Waypoint value object', () => {
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
}
try {
new Coordinates({
new Point({
lat: 48.689445,
lon: -186.17651,
});
@ -30,7 +30,7 @@ describe('Waypoint value object', () => {
});
it('should throw an exception if latitude is invalid', () => {
try {
new Coordinates({
new Point({
lat: 148.689445,
lon: 6.17651,
});
@ -38,7 +38,7 @@ describe('Waypoint value object', () => {
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
}
try {
new Coordinates({
new Point({
lat: -148.689445,
lon: 6.17651,
});

View File

@ -2,31 +2,23 @@ import { GeorouterPort } from '@modules/geography/core/application/ports/georout
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
import { RouteNotFoundException } from '@modules/geography/core/domain/route.errors';
import {
Coordinates,
Point,
CreateRouteProps,
PathType,
Role,
} from '@modules/geography/core/domain/route.types';
const originCoordinates: Coordinates = {
const originPoint: Point = {
lat: 48.689445,
lon: 6.17651,
};
const destinationCoordinates: Coordinates = {
const destinationPoint: Point = {
lat: 48.8566,
lon: 2.3522,
};
const additionalCoordinates: Coordinates = {
lon: 48.7566,
lat: 4.4498,
};
const mockGeorouter: GeorouterPort = {
routes: jest
route: jest
.fn()
.mockImplementationOnce(() => [
{
type: PathType.DRIVER,
.mockImplementationOnce(() => ({
distance: 350101,
duration: 14422,
fwdAzimuth: 273,
@ -46,161 +38,20 @@ const mockGeorouter: GeorouterPort = {
lat: 48.8566,
},
],
spacetimePoints: [],
},
])
.mockImplementationOnce(() => [
{
type: PathType.PASSENGER,
distance: 350102,
duration: 14423,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336545,
points: [
{
lon: 6.1765103,
lat: 48.689446,
},
{
lon: 4.984579,
lat: 48.725688,
},
{
lon: 2.3523,
lat: 48.8567,
},
],
spacetimePoints: [],
},
])
.mockImplementationOnce(() => [
{
type: PathType.GENERIC,
distance: 350100,
duration: 14421,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336543,
points: [
{
lon: 6.1765101,
lat: 48.689444,
},
{
lon: 4.984577,
lat: 48.725686,
},
{
lon: 2.3521,
lat: 48.8565,
},
],
spacetimePoints: [],
},
])
.mockImplementationOnce(() => [
{
type: PathType.GENERIC,
distance: 350108,
duration: 14428,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336548,
points: [
{
lon: 6.1765101,
lat: 48.689444,
},
{
lon: 4.984577,
lat: 48.725686,
},
{
lon: 2.3521,
lat: 48.8565,
},
],
spacetimePoints: [],
},
])
steps: [],
}))
.mockImplementationOnce(() => []),
};
const createDriverRouteProps: CreateRouteProps = {
roles: [Role.DRIVER],
const createRouteProps: CreateRouteProps = {
waypoints: [
{
position: 0,
...originCoordinates,
...originPoint,
},
{
position: 1,
...destinationCoordinates,
},
],
georouter: mockGeorouter,
georouterSettings: {
points: true,
detailedDistance: false,
detailedDuration: false,
},
};
const createPassengerRouteProps: CreateRouteProps = {
roles: [Role.PASSENGER],
waypoints: [
{
position: 0,
...originCoordinates,
},
{
position: 1,
...destinationCoordinates,
},
],
georouter: mockGeorouter,
georouterSettings: {
points: true,
detailedDistance: false,
detailedDuration: false,
},
};
const createSimpleDriverAndPassengerRouteProps: CreateRouteProps = {
roles: [Role.DRIVER, Role.PASSENGER],
waypoints: [
{
position: 0,
...originCoordinates,
},
{
position: 1,
...destinationCoordinates,
},
],
georouter: mockGeorouter,
georouterSettings: {
points: true,
detailedDistance: false,
detailedDuration: false,
},
};
const createComplexDriverAndPassengerRouteProps: CreateRouteProps = {
roles: [Role.DRIVER, Role.PASSENGER],
waypoints: [
{
position: 0,
...originCoordinates,
},
{
position: 1,
...additionalCoordinates,
},
{
position: 2,
...destinationCoordinates,
...destinationPoint,
},
],
georouter: mockGeorouter,
@ -212,43 +63,15 @@ const createComplexDriverAndPassengerRouteProps: CreateRouteProps = {
};
describe('Route entity create', () => {
it('should create a new entity for a driver only', async () => {
const route: RouteEntity = await RouteEntity.create(createDriverRouteProps);
it('should create a new entity', async () => {
const route: RouteEntity = await RouteEntity.create(createRouteProps);
expect(route.id.length).toBe(36);
expect(route.getProps().driverDuration).toBe(14422);
expect(route.getProps().passengerDistance).toBeUndefined();
});
it('should create a new entity for a passenger only', async () => {
const route: RouteEntity = await RouteEntity.create(
createPassengerRouteProps,
);
expect(route.id.length).toBe(36);
expect(route.getProps().passengerDuration).toBe(14423);
expect(route.getProps().driverDistance).toBeUndefined();
});
it('should create a new entity for a simple driver and passenger route', async () => {
const route: RouteEntity = await RouteEntity.create(
createSimpleDriverAndPassengerRouteProps,
);
expect(route.id.length).toBe(36);
expect(route.getProps().driverDuration).toBe(14421);
expect(route.getProps().driverDistance).toBe(350100);
expect(route.getProps().passengerDuration).toBe(14421);
expect(route.getProps().passengerDistance).toBe(350100);
});
it('should create a new entity for a complex driver and passenger route', async () => {
const route: RouteEntity = await RouteEntity.create(
createComplexDriverAndPassengerRouteProps,
);
expect(route.id.length).toBe(36);
expect(route.getProps().driverDuration).toBe(14428);
expect(route.getProps().driverDistance).toBe(350108);
expect(route.getProps().passengerDuration).toBe(14428);
expect(route.getProps().passengerDistance).toBe(350108);
expect(route.getProps().duration).toBe(14422);
});
it('should throw an exception if route is not found', async () => {
try {
await RouteEntity.create(createDriverRouteProps);
await RouteEntity.create(createRouteProps);
} catch (e: any) {
expect(e).toBeInstanceOf(RouteNotFoundException);
}

View File

@ -2,24 +2,24 @@ import {
ArgumentInvalidException,
ArgumentOutOfRangeException,
} from '@mobicoop/ddd-library';
import { SpacetimePoint } from '@modules/geography/core/domain/value-objects/spacetime-point.value-object';
import { Step } from '@modules/geography/core/domain/value-objects/step.value-object';
describe('Timepoint value object', () => {
it('should create a timepoint value object', () => {
const timepointVO = new SpacetimePoint({
describe('Step value object', () => {
it('should create a step value object', () => {
const stepVO = new Step({
lat: 48.689445,
lon: 6.17651,
duration: 150,
distance: 12000,
});
expect(timepointVO.duration).toBe(150);
expect(timepointVO.distance).toBe(12000);
expect(timepointVO.lat).toBe(48.689445);
expect(timepointVO.lon).toBe(6.17651);
expect(stepVO.duration).toBe(150);
expect(stepVO.distance).toBe(12000);
expect(stepVO.lat).toBe(48.689445);
expect(stepVO.lon).toBe(6.17651);
});
it('should throw an exception if longitude is invalid', () => {
try {
new SpacetimePoint({
new Step({
lat: 48.689445,
lon: 186.17651,
duration: 150,
@ -29,7 +29,7 @@ describe('Timepoint value object', () => {
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
}
try {
new SpacetimePoint({
new Step({
lon: 48.689445,
lat: -186.17651,
duration: 150,
@ -41,7 +41,7 @@ describe('Timepoint value object', () => {
});
it('should throw an exception if latitude is invalid', () => {
try {
new SpacetimePoint({
new Step({
lat: 248.689445,
lon: 6.17651,
duration: 150,
@ -51,7 +51,7 @@ describe('Timepoint value object', () => {
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
}
try {
new SpacetimePoint({
new Step({
lon: -148.689445,
lat: 6.17651,
duration: 150,
@ -63,7 +63,7 @@ describe('Timepoint value object', () => {
});
it('should throw an exception if distance is invalid', () => {
try {
new SpacetimePoint({
new Step({
lat: 48.689445,
lon: 6.17651,
duration: 150,
@ -75,7 +75,7 @@ describe('Timepoint value object', () => {
});
it('should throw an exception if duration is invalid', () => {
try {
new SpacetimePoint({
new Step({
lat: 48.689445,
lon: 6.17651,
duration: -150,

View File

@ -4,7 +4,7 @@ import {
GeorouterUnavailableException,
RouteNotFoundException,
} from '@modules/geography/core/domain/route.errors';
import { PathType, Route } from '@modules/geography/core/domain/route.types';
import { Route } from '@modules/geography/core/domain/route.types';
import {
GEODESIC,
PARAMS_PROVIDER,
@ -294,22 +294,19 @@ describe('Graphhopper Georouter', () => {
it('should fail if route is not found', async () => {
await expect(
graphhopperGeorouter.routes(
graphhopperGeorouter.route(
[
{
type: PathType.DRIVER,
points: [
{
position: 0,
lon: 0,
lat: 0,
},
{
position: 1,
lon: 1,
lat: 1,
},
],
},
],
{
detailedDistance: false,
detailedDuration: false,
@ -321,22 +318,19 @@ describe('Graphhopper Georouter', () => {
it('should fail if georouter is unavailable', async () => {
await expect(
graphhopperGeorouter.routes(
graphhopperGeorouter.route(
[
{
type: PathType.DRIVER,
points: [
{
position: 0,
lon: 0,
lat: 0,
},
{
position: 1,
lon: 1,
lat: 1,
},
],
},
],
{
detailedDistance: false,
detailedDuration: false,
@ -347,47 +341,40 @@ describe('Graphhopper Georouter', () => {
});
it('should create a basic route', async () => {
const routes: Route[] = await graphhopperGeorouter.routes(
const route: Route = await graphhopperGeorouter.route(
[
{
type: PathType.DRIVER,
points: [
{
position: 0,
lon: 0,
lat: 0,
},
{
position: 1,
lon: 10,
lat: 10,
},
],
},
],
{
detailedDistance: false,
detailedDuration: false,
points: false,
},
);
expect(routes).toHaveLength(1);
expect(routes[0].distance).toBe(50000);
expect(route.distance).toBe(50000);
});
it('should create one route with points', async () => {
const routes = await graphhopperGeorouter.routes(
it('should create a route with points', async () => {
const route: Route = await graphhopperGeorouter.route(
[
{
type: PathType.DRIVER,
points: [
{
lat: 0,
position: 0,
lon: 0,
lat: 0,
},
{
lat: 10,
position: 1,
lon: 10,
},
],
lat: 10,
},
],
{
@ -396,29 +383,25 @@ describe('Graphhopper Georouter', () => {
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);
expect(route.distance).toBe(50000);
expect(route.duration).toBe(1800);
expect(route.fwdAzimuth).toBe(45);
expect(route.backAzimuth).toBe(225);
expect(route.points).toHaveLength(11);
});
it('should create one route with points and time', async () => {
const routes = await graphhopperGeorouter.routes(
it('should create a route with points and time', async () => {
const route: Route = await graphhopperGeorouter.route(
[
{
type: PathType.DRIVER,
points: [
{
lat: 0,
position: 0,
lon: 0,
lat: 0,
},
{
lat: 10,
position: 1,
lon: 10,
},
],
lat: 10,
},
],
{
@ -427,31 +410,28 @@ describe('Graphhopper Georouter', () => {
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();
expect(route.steps).toHaveLength(2);
expect(route.steps[1].duration).toBe(1800);
expect(route.steps[1].distance).toBeUndefined();
});
it('should create one route with points and missed waypoints extrapolations', async () => {
const routes = await graphhopperGeorouter.routes(
const route: Route = await graphhopperGeorouter.route(
[
{
type: PathType.DRIVER,
points: [
{
lat: 0,
position: 0,
lon: 0,
lat: 0,
},
{
lat: 5,
position: 1,
lon: 5,
lat: 5,
},
{
lat: 10,
position: 2,
lon: 10,
},
],
lat: 10,
},
],
{
@ -460,30 +440,26 @@ describe('Graphhopper Georouter', () => {
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);
expect(route.steps).toHaveLength(3);
expect(route.distance).toBe(50000);
expect(route.duration).toBe(1800);
expect(route.fwdAzimuth).toBe(45);
expect(route.backAzimuth).toBe(225);
expect(route.points.length).toBe(9);
});
it('should create one route with points, time and distance', async () => {
const routes = await graphhopperGeorouter.routes(
it('should create a route with points, time and distance', async () => {
const route: Route = await graphhopperGeorouter.route(
[
{
type: PathType.DRIVER,
points: [
{
lat: 0,
position: 0,
lon: 0,
lat: 0,
},
{
lat: 10,
position: 1,
lon: 10,
},
],
lat: 10,
},
],
{
@ -492,9 +468,8 @@ describe('Graphhopper Georouter', () => {
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);
expect(route.steps.length).toBe(3);
expect(route.steps[1].duration).toBe(990);
expect(route.steps[1].distance).toBe(25000);
});
});

View File

@ -1,4 +1,4 @@
import { Coordinates } from '@modules/geography/core/domain/route.types';
import { Point } from '@modules/geography/core/domain/route.types';
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
describe('Postgres direction encoder', () => {
@ -7,10 +7,10 @@ describe('Postgres direction encoder', () => {
new PostgresDirectionEncoder();
expect(postgresDirectionEncoder).toBeDefined();
});
it('should encode coordinates to a postgres direction', () => {
it('should encode points to a postgres direction', () => {
const postgresDirectionEncoder: PostgresDirectionEncoder =
new PostgresDirectionEncoder();
const coordinates: Coordinates[] = [
const points: Point[] = [
{
lon: 6,
lat: 47,
@ -24,18 +24,17 @@ describe('Postgres direction encoder', () => {
lat: 47.2,
},
];
const direction = postgresDirectionEncoder.encode(coordinates);
const direction = postgresDirectionEncoder.encode(points);
expect(direction).toBe("'LINESTRING(6 47,6.1 47.1,6.2 47.2)'");
});
it('should decode a postgres direction to coordinates', () => {
const postgresDirectionEncoder: PostgresDirectionEncoder =
new PostgresDirectionEncoder();
const direction = "'LINESTRING(6 47,6.1 47.1,6.2 47.2)'";
const coordinates: Coordinates[] =
postgresDirectionEncoder.decode(direction);
expect(coordinates.length).toBe(3);
expect(coordinates[0].lat).toBe(47);
expect(coordinates[1].lon).toBe(6.1);
expect(coordinates[2].lat).toBe(47.2);
const points: Point[] = postgresDirectionEncoder.decode(direction);
expect(points.length).toBe(3);
expect(points[0].lat).toBe(47);
expect(points[1].lon).toBe(6.1);
expect(points[2].lat).toBe(47.2);
});
});

View File

@ -1,4 +1,3 @@
import { Role } from '@modules/geography/core/domain/route.types';
import { GetBasicRouteController } from '@modules/geography/interface/controllers/get-basic-route.controller';
import { RouteMapper } from '@modules/geography/route.mapper';
import { QueryBus } from '@nestjs/cqrs';
@ -48,7 +47,6 @@ describe('Get Basic Route Controller', () => {
it('should get a route', async () => {
jest.spyOn(mockQueryBus, 'execute');
await getBasicRouteController.get({
roles: [Role.DRIVER],
waypoints: [
{
position: 0,

View File

@ -23,28 +23,23 @@ describe('Route Mapper', () => {
createdAt: now,
updatedAt: now,
props: {
driverDistance: 23000,
driverDuration: 900,
passengerDistance: 23000,
passengerDuration: 900,
distance: 23000,
duration: 900,
fwdAzimuth: 283,
backAzimuth: 93,
distanceAzimuth: 19840,
points: [],
waypoints: [
points: [
{
position: 0,
lon: 6.1765103,
lat: 48.689446,
},
{
position: 1,
lon: 2.3523,
lat: 48.8567,
},
],
},
});
expect(routeMapper.toResponse(routeEntity).driverDistance).toBe(23000);
expect(routeMapper.toResponse(routeEntity).distance).toBe(23000);
});
});