extract carpool informations from geography module
This commit is contained in:
		
							parent
							
								
									57fe8d417f
								
							
						
					
					
						commit
						d1a314f011
					
				| 
						 | 
				
			
			@ -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})`,
 | 
			
		||||
          ),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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[];
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
export type Coordinates = {
 | 
			
		||||
export type Point = {
 | 
			
		||||
  lon: number;
 | 
			
		||||
  lat: number;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
      waypoints,
 | 
			
		||||
    });
 | 
			
		||||
  ): 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',
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
    backAzimuth: 93,
 | 
			
		||||
    distanceAzimuth: 19840,
 | 
			
		||||
    points: [
 | 
			
		||||
      {
 | 
			
		||||
        lon: 6.1765103,
 | 
			
		||||
        lat: 48.689446,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        lon: 2.3523,
 | 
			
		||||
        lat: 48.8567,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  })),
 | 
			
		||||
  get: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => ({
 | 
			
		||||
      distance: 350101,
 | 
			
		||||
      duration: 14422,
 | 
			
		||||
      fwdAzimuth: 273,
 | 
			
		||||
      backAzimuth: 93,
 | 
			
		||||
      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);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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[];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
  // });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			@ -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',
 | 
			
		||||
| 
						 | 
				
			
			@ -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,46 +57,39 @@ export class GraphhopperGeorouter implements GeorouterPort {
 | 
			
		|||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  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('');
 | 
			
		||||
        return await lastValueFrom(
 | 
			
		||||
          this.httpService.get(url).pipe(
 | 
			
		||||
            map((response) => {
 | 
			
		||||
              if (response.data) return this.createRoute(response, path.type);
 | 
			
		||||
              throw new Error();
 | 
			
		||||
            }),
 | 
			
		||||
            catchError((error: AxiosError) => {
 | 
			
		||||
              if (error.code == AxiosError.ERR_BAD_REQUEST) {
 | 
			
		||||
                throw new RouteNotFoundException(
 | 
			
		||||
                  error,
 | 
			
		||||
                  'No route found for given coordinates',
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
              throw new GeorouterUnavailableException(error);
 | 
			
		||||
            }),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      }),
 | 
			
		||||
  private _getRoute = async (waypoints: Waypoint[]): Promise<Route> => {
 | 
			
		||||
    const url: string = [
 | 
			
		||||
      this.getUrl(),
 | 
			
		||||
      '&point=',
 | 
			
		||||
      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);
 | 
			
		||||
          throw new Error();
 | 
			
		||||
        }),
 | 
			
		||||
        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 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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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[];
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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[];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
      });
 | 
			
		||||
| 
						 | 
				
			
			@ -2,205 +2,56 @@ 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,
 | 
			
		||||
        distance: 350101,
 | 
			
		||||
        duration: 14422,
 | 
			
		||||
        fwdAzimuth: 273,
 | 
			
		||||
        backAzimuth: 93,
 | 
			
		||||
        distanceAzimuth: 336544,
 | 
			
		||||
        points: [
 | 
			
		||||
          {
 | 
			
		||||
            lon: 6.1765102,
 | 
			
		||||
            lat: 48.689445,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            lon: 4.984578,
 | 
			
		||||
            lat: 48.725687,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            lon: 2.3522,
 | 
			
		||||
            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: [],
 | 
			
		||||
      },
 | 
			
		||||
    ])
 | 
			
		||||
    .mockImplementationOnce(() => ({
 | 
			
		||||
      distance: 350101,
 | 
			
		||||
      duration: 14422,
 | 
			
		||||
      fwdAzimuth: 273,
 | 
			
		||||
      backAzimuth: 93,
 | 
			
		||||
      distanceAzimuth: 336544,
 | 
			
		||||
      points: [
 | 
			
		||||
        {
 | 
			
		||||
          lon: 6.1765102,
 | 
			
		||||
          lat: 48.689445,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          lon: 4.984578,
 | 
			
		||||
          lat: 48.725687,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          lon: 2.3522,
 | 
			
		||||
          lat: 48.8566,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      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);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			@ -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,20 +294,17 @@ describe('Graphhopper Georouter', () => {
 | 
			
		|||
 | 
			
		||||
  it('should fail if route is not found', async () => {
 | 
			
		||||
    await expect(
 | 
			
		||||
      graphhopperGeorouter.routes(
 | 
			
		||||
      graphhopperGeorouter.route(
 | 
			
		||||
        [
 | 
			
		||||
          {
 | 
			
		||||
            type: PathType.DRIVER,
 | 
			
		||||
            points: [
 | 
			
		||||
              {
 | 
			
		||||
                lon: 0,
 | 
			
		||||
                lat: 0,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                lon: 1,
 | 
			
		||||
                lat: 1,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
            position: 0,
 | 
			
		||||
            lon: 0,
 | 
			
		||||
            lat: 0,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            position: 1,
 | 
			
		||||
            lon: 1,
 | 
			
		||||
            lat: 1,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        {
 | 
			
		||||
| 
						 | 
				
			
			@ -321,20 +318,17 @@ describe('Graphhopper Georouter', () => {
 | 
			
		|||
 | 
			
		||||
  it('should fail if georouter is unavailable', async () => {
 | 
			
		||||
    await expect(
 | 
			
		||||
      graphhopperGeorouter.routes(
 | 
			
		||||
      graphhopperGeorouter.route(
 | 
			
		||||
        [
 | 
			
		||||
          {
 | 
			
		||||
            type: PathType.DRIVER,
 | 
			
		||||
            points: [
 | 
			
		||||
              {
 | 
			
		||||
                lon: 0,
 | 
			
		||||
                lat: 0,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                lon: 1,
 | 
			
		||||
                lat: 1,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
            position: 0,
 | 
			
		||||
            lon: 0,
 | 
			
		||||
            lat: 0,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            position: 1,
 | 
			
		||||
            lon: 1,
 | 
			
		||||
            lat: 1,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        {
 | 
			
		||||
| 
						 | 
				
			
			@ -347,20 +341,17 @@ 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: [
 | 
			
		||||
            {
 | 
			
		||||
              lon: 0,
 | 
			
		||||
              lat: 0,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              lon: 10,
 | 
			
		||||
              lat: 10,
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          position: 0,
 | 
			
		||||
          lon: 0,
 | 
			
		||||
          lat: 0,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          position: 1,
 | 
			
		||||
          lon: 10,
 | 
			
		||||
          lat: 10,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      {
 | 
			
		||||
| 
						 | 
				
			
			@ -369,25 +360,21 @@ describe('Graphhopper Georouter', () => {
 | 
			
		|||
        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,
 | 
			
		||||
              lon: 0,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              lat: 10,
 | 
			
		||||
              lon: 10,
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          position: 0,
 | 
			
		||||
          lon: 0,
 | 
			
		||||
          lat: 0,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          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,
 | 
			
		||||
              lon: 0,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              lat: 10,
 | 
			
		||||
              lon: 10,
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          position: 0,
 | 
			
		||||
          lon: 0,
 | 
			
		||||
          lat: 0,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          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,
 | 
			
		||||
              lon: 0,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              lat: 5,
 | 
			
		||||
              lon: 5,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              lat: 10,
 | 
			
		||||
              lon: 10,
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          position: 0,
 | 
			
		||||
          lon: 0,
 | 
			
		||||
          lat: 0,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          position: 1,
 | 
			
		||||
          lon: 5,
 | 
			
		||||
          lat: 5,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          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,
 | 
			
		||||
              lon: 0,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              lat: 10,
 | 
			
		||||
              lon: 10,
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          position: 0,
 | 
			
		||||
          lon: 0,
 | 
			
		||||
          lat: 0,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          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);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue