This commit is contained in:
sbriat 2023-05-11 17:47:55 +02:00
parent 2a2cfa5c0f
commit da96f52c1e
69 changed files with 1700 additions and 492 deletions

View File

@ -1,65 +0,0 @@
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "postgis";
-- Required to use postgis extension :
-- set the search_path to both public (where is postgis) AND the current schema
SET search_path TO matcher, public;
-- CreateTable
CREATE TABLE "ad" (
"uuid" UUID NOT NULL,
"driver" BOOLEAN NOT NULL,
"passenger" BOOLEAN NOT NULL,
"frequency" INTEGER NOT NULL,
"fromDate" DATE NOT NULL,
"toDate" DATE NOT NULL,
"monTime" TIMESTAMPTZ NOT NULL,
"tueTime" TIMESTAMPTZ NOT NULL,
"wedTime" TIMESTAMPTZ NOT NULL,
"thuTime" TIMESTAMPTZ NOT NULL,
"friTime" TIMESTAMPTZ NOT NULL,
"satTime" TIMESTAMPTZ NOT NULL,
"sunTime" TIMESTAMPTZ NOT NULL,
"monMargin" INTEGER NOT NULL,
"tueMargin" INTEGER NOT NULL,
"wedMargin" INTEGER NOT NULL,
"thuMargin" INTEGER NOT NULL,
"friMargin" INTEGER NOT NULL,
"satMargin" INTEGER NOT NULL,
"sunMargin" INTEGER NOT NULL,
"driverDuration" INTEGER NOT NULL,
"driverDistance" INTEGER NOT NULL,
"passengerDuration" INTEGER NOT NULL,
"passengerDistance" INTEGER NOT NULL,
"originType" SMALLINT NOT NULL,
"destinationType" SMALLINT NOT NULL,
"waypoints" geography(LINESTRING),
"direction" geography(LINESTRING),
"fwdAzimuth" INTEGER NOT NULL,
"backAzimuth" INTEGER NOT NULL,
"seatsDriver" SMALLINT NOT NULL,
"seatsPassenger" SMALLINT NOT NULL,
"seatsUsed" SMALLINT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ad_pkey" PRIMARY KEY ("uuid")
);
-- CreateIndex
CREATE INDEX "ad_driver_idx" ON "ad"("driver");
-- CreateIndex
CREATE INDEX "ad_passenger_idx" ON "ad"("passenger");
-- CreateIndex
CREATE INDEX "ad_fromDate_idx" ON "ad"("fromDate");
-- CreateIndex
CREATE INDEX "ad_toDate_idx" ON "ad"("toDate");
-- CreateIndex
CREATE INDEX "ad_fwdAzimuth_idx" ON "ad"("fwdAzimuth");
-- CreateIndex
CREATE INDEX "direction_idx" ON "ad" USING GIST ("direction");

View File

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@ -13,19 +13,20 @@ datasource db {
} }
model Ad { model Ad {
uuid String @id @default(uuid()) @db.Uuid uuid String @id @db.Uuid
userUuid String @db.Uuid
driver Boolean driver Boolean
passenger Boolean passenger Boolean
frequency Int frequency Frequency
fromDate DateTime @db.Date fromDate DateTime @db.Date
toDate DateTime @db.Date toDate DateTime @db.Date
monTime DateTime @db.Timestamptz() monTime DateTime? @db.Timestamptz()
tueTime DateTime @db.Timestamptz() tueTime DateTime? @db.Timestamptz()
wedTime DateTime @db.Timestamptz() wedTime DateTime? @db.Timestamptz()
thuTime DateTime @db.Timestamptz() thuTime DateTime? @db.Timestamptz()
friTime DateTime @db.Timestamptz() friTime DateTime? @db.Timestamptz()
satTime DateTime @db.Timestamptz() satTime DateTime? @db.Timestamptz()
sunTime DateTime @db.Timestamptz() sunTime DateTime? @db.Timestamptz()
monMargin Int monMargin Int
tueMargin Int tueMargin Int
wedMargin Int wedMargin Int
@ -33,12 +34,10 @@ model Ad {
friMargin Int friMargin Int
satMargin Int satMargin Int
sunMargin Int sunMargin Int
driverDuration Int driverDuration Int?
driverDistance Int driverDistance Int?
passengerDuration Int passengerDuration Int?
passengerDistance Int passengerDistance Int?
originType Int @db.SmallInt
destinationType Int @db.SmallInt
waypoints Unsupported("geography(LINESTRING)")? waypoints Unsupported("geography(LINESTRING)")?
direction Unsupported("geography(LINESTRING)")? direction Unsupported("geography(LINESTRING)")?
fwdAzimuth Int fwdAzimuth Int
@ -46,6 +45,7 @@ model Ad {
seatsDriver Int @db.SmallInt seatsDriver Int @db.SmallInt
seatsPassenger Int @db.SmallInt seatsPassenger Int @db.SmallInt
seatsUsed Int @db.SmallInt seatsUsed Int @db.SmallInt
strict Boolean
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
@ -57,3 +57,8 @@ model Ad {
@@index([direction], name: "direction_idx", type: Gist) @@index([direction], name: "direction_idx", type: Gist)
@@map("ad") @@map("ad")
} }
enum Frequency {
PUNCTUAL
RECURRENT
}

View File

@ -10,11 +10,17 @@ import { CqrsModule } from '@nestjs/cqrs';
import { Messager } from './adapters/secondaries/messager'; import { Messager } from './adapters/secondaries/messager';
import { TimezoneFinder } from './adapters/secondaries/timezone-finder'; import { TimezoneFinder } from './adapters/secondaries/timezone-finder';
import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezone-finder'; import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezone-finder';
import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider';
import { GeorouterCreator } from '../geography/adapters/secondaries/georouter-creator';
import { GeographyModule } from '../geography/geography.module';
import { HttpModule } from '@nestjs/axios';
@Module({ @Module({
imports: [ imports: [
GeographyModule,
DatabaseModule, DatabaseModule,
CqrsModule, CqrsModule,
HttpModule,
RabbitMQModule.forRootAsync(RabbitMQModule, { RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule], imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
@ -46,6 +52,8 @@ import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezon
TimezoneFinder, TimezoneFinder,
GeoTimezoneFinder, GeoTimezoneFinder,
CreateAdUseCase, CreateAdUseCase,
DefaultParamsProvider,
GeorouterCreator,
], ],
exports: [], exports: [],
}) })

View File

@ -8,7 +8,10 @@ import { CreateAdCommand } from '../../commands/create-ad.command';
import { CreateAdRequest } from '../../domain/dtos/create-ad.request'; import { CreateAdRequest } from '../../domain/dtos/create-ad.request';
import { validateOrReject } from 'class-validator'; import { validateOrReject } from 'class-validator';
import { Messager } from '../secondaries/messager'; import { Messager } from '../secondaries/messager';
import { GeoTimezoneFinder } from 'src/modules/geography/adapters/secondaries/geo-timezone-finder'; import { GeoTimezoneFinder } from '../../../geography/adapters/secondaries/geo-timezone-finder';
import { plainToInstance } from 'class-transformer';
import { DefaultParamsProvider } from '../secondaries/default-params.provider';
import { GeorouterCreator } from '../../../geography/adapters/secondaries/georouter-creator';
@Controller() @Controller()
export class AdMessagerController { export class AdMessagerController {
@ -16,6 +19,8 @@ export class AdMessagerController {
private readonly messager: Messager, private readonly messager: Messager,
private readonly commandBus: CommandBus, private readonly commandBus: CommandBus,
@InjectMapper() private readonly mapper: Mapper, @InjectMapper() private readonly mapper: Mapper,
private readonly defaultParamsProvider: DefaultParamsProvider,
private readonly georouterCreator: GeorouterCreator,
private readonly timezoneFinder: GeoTimezoneFinder, private readonly timezoneFinder: GeoTimezoneFinder,
) {} ) {}
@ -23,15 +28,10 @@ export class AdMessagerController {
name: 'adCreated', name: 'adCreated',
}) })
async adCreatedHandler(message: string): Promise<void> { async adCreatedHandler(message: string): Promise<void> {
let createAdRequest: CreateAdRequest;
try { try {
// parse message to conform to CreateAdRequest (not a real instance yet) const createAdRequest: CreateAdRequest = plainToInstance(
const parsedMessage: CreateAdRequest = JSON.parse(message);
// create a real instance of CreateAdRequest from parsed message
createAdRequest = this.mapper.map(
parsedMessage,
CreateAdRequest,
CreateAdRequest, CreateAdRequest,
JSON.parse(message),
); );
// validate instance // validate instance
await validateOrReject(createAdRequest); await validateOrReject(createAdRequest);
@ -48,14 +48,20 @@ export class AdMessagerController {
createAdRequest.waypoints[0].lat, createAdRequest.waypoints[0].lat,
)[0]; )[0];
const ad: Ad = await this.commandBus.execute( const ad: Ad = await this.commandBus.execute(
new CreateAdCommand(createAdRequest), new CreateAdCommand(
createAdRequest,
this.defaultParamsProvider.getParams(),
this.georouterCreator,
this.timezoneFinder,
),
); );
console.log(ad); console.log(ad);
} catch (e) { } catch (e) {
console.log(e);
this.messager.publish( this.messager.publish(
'logging.matcher.ad.crit', 'logging.matcher.ad.crit',
JSON.stringify({ JSON.stringify({
createAdRequest, message,
error: e, error: e,
}), }),
); );

View File

@ -2,10 +2,11 @@ import { Injectable } from '@nestjs/common';
import { MatcherRepository } from '../../../database/domain/matcher-repository'; import { MatcherRepository } from '../../../database/domain/matcher-repository';
import { Ad } from '../../domain/entities/ad'; import { Ad } from '../../domain/entities/ad';
import { DatabaseException } from '../../../database/exceptions/database.exception'; import { DatabaseException } from '../../../database/exceptions/database.exception';
import { Frequency } from '../../domain/types/frequency.enum';
@Injectable() @Injectable()
export class AdRepository extends MatcherRepository<Ad> { export class AdRepository extends MatcherRepository<Ad> {
protected _model = 'ad'; protected model = 'ad';
async createAd(ad: Partial<Ad>): Promise<Ad> { async createAd(ad: Partial<Ad>): Promise<Ad> {
try { try {
@ -44,7 +45,7 @@ type AdFields = {
uuid: string; uuid: string;
driver: string; driver: string;
passenger: string; passenger: string;
frequency: number; frequency: Frequency;
fromDate: string; fromDate: string;
toDate: string; toDate: string;
monTime: string; monTime: string;

View File

@ -0,0 +1,16 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IDefaultParams } from '../../domain/types/default-params.type';
@Injectable()
export class DefaultParamsProvider {
constructor(private readonly configService: ConfigService) {}
getParams = (): IDefaultParams => {
return {
DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'),
GEOROUTER_TYPE: this.configService.get('GEOROUTER_TYPE'),
GEOROUTER_URL: this.configService.get('GEOROUTER_URL'),
};
};
}

View File

@ -6,13 +6,13 @@ import { MessageBroker } from './message-broker';
@Injectable() @Injectable()
export class Messager extends MessageBroker { export class Messager extends MessageBroker {
constructor( constructor(
private readonly _amqpConnection: AmqpConnection, private readonly amqpConnection: AmqpConnection,
configService: ConfigService, configService: ConfigService,
) { ) {
super(configService.get<string>('RMQ_EXCHANGE')); super(configService.get<string>('RMQ_EXCHANGE'));
} }
publish = (routingKey: string, message: string): void => { publish = (routingKey: string, message: string): void => {
this._amqpConnection.publish(this.exchange, routingKey, message); this.amqpConnection.publish(this.exchange, routingKey, message);
}; };
} }

View File

@ -1,9 +1,54 @@
import { IGeorouter } from '../../geography/domain/interfaces/georouter.interface';
import { ICreateGeorouter } from '../../geography/domain/interfaces/georouter-creator.interface';
import { CreateAdRequest } from '../domain/dtos/create-ad.request'; import { CreateAdRequest } from '../domain/dtos/create-ad.request';
import { Geography } from '../domain/entities/geography';
import { IDefaultParams } from '../domain/types/default-params.type';
import { Role } from '../domain/types/role.enum';
import { IFindTimezone } from '../../geography/domain/interfaces/timezone-finder.interface';
export class CreateAdCommand { export class CreateAdCommand {
readonly createAdRequest: CreateAdRequest; readonly createAdRequest: CreateAdRequest;
private readonly defaultParams: IDefaultParams;
private readonly georouter: IGeorouter;
private readonly timezoneFinder: IFindTimezone;
roles: Role[];
geography: Geography;
timezone: string;
constructor(request: CreateAdRequest) { constructor(
request: CreateAdRequest,
defaultParams: IDefaultParams,
georouterCreator: ICreateGeorouter,
timezoneFinder: IFindTimezone,
) {
this.createAdRequest = request; this.createAdRequest = request;
this.defaultParams = defaultParams;
this.georouter = georouterCreator.create(
defaultParams.GEOROUTER_TYPE,
defaultParams.GEOROUTER_URL,
);
this.timezoneFinder = timezoneFinder;
this.setRoles();
this.setGeography();
} }
private setRoles = (): void => {
this.roles = [];
if (this.createAdRequest.driver) this.roles.push(Role.DRIVER);
if (this.createAdRequest.passenger) this.roles.push(Role.PASSENGER);
};
private setGeography = async (): Promise<void> => {
this.geography = new Geography(this.createAdRequest.waypoints, {
timezone: this.defaultParams.DEFAULT_TIMEZONE,
finder: this.timezoneFinder,
});
if (this.geography.timezones.length > 0)
this.createAdRequest.timezone = this.geography.timezones[0];
try {
await this.geography.createRoutes(this.roles, this.georouter);
} catch (e) {
console.log(e);
}
};
} }

View File

@ -3,15 +3,17 @@ import {
ArrayMinSize, ArrayMinSize,
IsArray, IsArray,
IsBoolean, IsBoolean,
IsDate,
IsEnum, IsEnum,
IsMilitaryTime,
IsNotEmpty, IsNotEmpty,
IsNumber, IsNumber,
IsOptional, IsOptional,
IsString, IsString,
} from 'class-validator'; } from 'class-validator';
import { PointType } from '../../../geography/domain/types/point-type.enum';
import { Frequency } from '../types/frequency.enum'; import { Frequency } from '../types/frequency.enum';
import { Coordinates } from '../../../geography/domain/entities/coordinates'; import { Coordinates } from '../../../geography/domain/entities/coordinates';
import { Type } from 'class-transformer';
export class CreateAdRequest { export class CreateAdRequest {
@IsString() @IsString()
@ -19,6 +21,11 @@ export class CreateAdRequest {
@AutoMap() @AutoMap()
uuid: string; uuid: string;
@IsString()
@IsNotEmpty()
@AutoMap()
userUuid: string;
@IsBoolean() @IsBoolean()
@AutoMap() @AutoMap()
driver: boolean; driver: boolean;
@ -27,53 +34,54 @@ export class CreateAdRequest {
@AutoMap() @AutoMap()
passenger: boolean; passenger: boolean;
@IsNotEmpty()
@IsEnum(Frequency) @IsEnum(Frequency)
@AutoMap() @AutoMap()
frequency: Frequency; frequency: Frequency;
@IsString() @Type(() => Date)
@IsDate()
@AutoMap() @AutoMap()
fromDate: string; fromDate: Date;
@IsString() @Type(() => Date)
@IsDate()
@AutoMap() @AutoMap()
toDate: string; toDate: Date;
@IsOptional() @IsOptional()
@IsString() @IsMilitaryTime()
@AutoMap() @AutoMap()
monTime?: string | null; monTime?: string;
@IsOptional() @IsOptional()
@IsString() @IsMilitaryTime()
@AutoMap() @AutoMap()
tueTime?: string | null; tueTime?: string;
@IsOptional() @IsOptional()
@IsString() @IsMilitaryTime()
@AutoMap() @AutoMap()
wedTime?: string | null; wedTime?: string;
@IsOptional() @IsOptional()
@IsString() @IsMilitaryTime()
@AutoMap() @AutoMap()
thuTime?: string | null; thuTime?: string;
@IsOptional() @IsOptional()
@IsString() @IsMilitaryTime()
@AutoMap() @AutoMap()
friTime?: string | null; friTime?: string;
@IsOptional() @IsOptional()
@IsString() @IsMilitaryTime()
@AutoMap() @AutoMap()
satTime?: string | null; satTime?: string;
@IsOptional() @IsOptional()
@IsString() @IsMilitaryTime()
@AutoMap() @AutoMap()
sunTime?: string | null; sunTime?: string;
@IsNumber() @IsNumber()
@AutoMap() @AutoMap()
@ -103,19 +111,33 @@ export class CreateAdRequest {
@AutoMap() @AutoMap()
sunMargin: number; sunMargin: number;
@IsEnum(PointType) @Type(() => Coordinates)
@AutoMap()
originType: PointType;
@IsEnum(PointType)
@AutoMap()
destinationType: PointType;
@IsArray() @IsArray()
@ArrayMinSize(2) @ArrayMinSize(2)
@AutoMap(() => [Coordinates]) @AutoMap(() => [Coordinates])
waypoints: Coordinates[]; waypoints: Coordinates[];
@AutoMap()
driverDuration?: number;
@AutoMap()
driverDistance?: number;
@AutoMap()
passengerDuration?: number;
@AutoMap()
passengerDistance?: number;
@AutoMap()
direction: string;
@AutoMap()
fwdAzimuth: number;
@AutoMap()
backAzimuth: number;
@IsNumber() @IsNumber()
@AutoMap() @AutoMap()
seatsDriver: number; seatsDriver: number;
@ -129,13 +151,9 @@ export class CreateAdRequest {
@AutoMap() @AutoMap()
seatsUsed?: number; seatsUsed?: number;
@IsString() @IsBoolean()
@AutoMap() @AutoMap()
createdAt: string; strict: boolean;
@IsString()
@AutoMap()
updatedAt: string;
timezone?: string; timezone?: string;
} }

View File

@ -0,0 +1,7 @@
import { Ad } from './ad';
export class AdCompleter {
complete = async (ad: Ad): Promise<Ad> => {
return ad;
};
}

View File

@ -1,6 +1,6 @@
import { AutoMap } from '@automapper/classes'; import { AutoMap } from '@automapper/classes';
import { PointType } from '../../../geography/domain/types/point-type.enum';
import { Coordinates } from '../../../geography/domain/entities/coordinates'; import { Coordinates } from '../../../geography/domain/entities/coordinates';
import { Frequency } from '../types/frequency.enum';
export class Ad { export class Ad {
@AutoMap() @AutoMap()
@ -13,7 +13,7 @@ export class Ad {
passenger: boolean; passenger: boolean;
@AutoMap() @AutoMap()
frequency: number; frequency: Frequency;
@AutoMap() @AutoMap()
fromDate: Date; fromDate: Date;
@ -64,22 +64,16 @@ export class Ad {
sunMargin: number; sunMargin: number;
@AutoMap() @AutoMap()
driverDuration: number; driverDuration?: number;
@AutoMap() @AutoMap()
driverDistance: number; driverDistance?: number;
@AutoMap() @AutoMap()
passengerDuration: number; passengerDuration?: number;
@AutoMap() @AutoMap()
passengerDistance: number; passengerDistance?: number;
@AutoMap()
originType: PointType;
@AutoMap()
destinationType: PointType;
@AutoMap(() => [Coordinates]) @AutoMap(() => [Coordinates])
waypoints: Coordinates[]; waypoints: Coordinates[];
@ -102,6 +96,9 @@ export class Ad {
@AutoMap() @AutoMap()
seatsUsed: number; seatsUsed: number;
@AutoMap()
strict: boolean;
@AutoMap() @AutoMap()
createdAt: Date; createdAt: Date;

View File

@ -0,0 +1,98 @@
import { Coordinates } from '../../../geography/domain/entities/coordinates';
import { Route } from '../../../geography/domain/entities/route';
import { IFindTimezone } from '../../../geography/domain/interfaces/timezone-finder.interface';
import { Role } from '../types/role.enum';
import { IGeorouter } from '../../../geography/domain/interfaces/georouter.interface';
import { Path } from '../../../geography/domain/types/path.type';
import { Timezoner } from '../../../geography/domain/types/timezoner';
export class Geography {
private points: Coordinates[];
timezones: string[];
driverRoute: Route;
passengerRoute: Route;
timezoneFinder: IFindTimezone;
constructor(points: Coordinates[], timezoner: Timezoner) {
this.points = points;
this.timezones = [timezoner.timezone];
this.timezoneFinder = timezoner.finder;
this.setTimezones();
}
createRoutes = async (
roles: Role[],
georouter: IGeorouter,
): Promise<void> => {
const paths: Path[] = [];
if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
if (this.points.length == 2) {
// 2 points => same route for driver and passenger
const commonPath: Path = {
key: RouteKey.COMMON,
points: this.points,
};
paths.push(commonPath);
} else {
const driverPath: Path = {
key: RouteKey.DRIVER,
points: this.points,
};
const passengerPath: Path = {
key: RouteKey.PASSENGER,
points: [this.points[0], this.points[this.points.length - 1]],
};
paths.push(driverPath, passengerPath);
}
} else if (roles.includes(Role.DRIVER)) {
const driverPath: Path = {
key: RouteKey.DRIVER,
points: this.points,
};
paths.push(driverPath);
} else if (roles.includes(Role.PASSENGER)) {
const passengerPath: Path = {
key: RouteKey.PASSENGER,
points: [this.points[0], this.points[this.points.length - 1]],
};
paths.push(passengerPath);
}
const routes = await georouter.route(paths, {
withDistance: false,
withPoints: false,
withTime: false,
});
if (routes.some((route) => route.key == RouteKey.COMMON)) {
this.driverRoute = routes.find(
(route) => route.key == RouteKey.COMMON,
).route;
this.passengerRoute = routes.find(
(route) => route.key == RouteKey.COMMON,
).route;
} else {
if (routes.some((route) => route.key == RouteKey.DRIVER)) {
this.driverRoute = routes.find(
(route) => route.key == RouteKey.DRIVER,
).route;
}
if (routes.some((route) => route.key == RouteKey.PASSENGER)) {
this.passengerRoute = routes.find(
(route) => route.key == RouteKey.PASSENGER,
).route;
}
}
};
private setTimezones = (): void => {
this.timezones = this.timezoneFinder.timezones(
this.points[0].lat,
this.points[0].lon,
);
};
}
export enum RouteKey {
COMMON = 'common',
DRIVER = 'driver',
PASSENGER = 'passenger',
}

View File

@ -1,14 +1,14 @@
import { DateTime, TimeZone } from 'timezonecomplete'; import { DateTime, TimeZone } from 'timezonecomplete';
export class TimeConverter { export class TimeConverter {
static toUtcDatetime = ( static toUtcDatetime = (date: Date, time: string, timezone: string): Date => {
date: string,
time: string,
timezone: string,
): Date => {
try { try {
if (!date || !time || !timezone) throw new Error();
return new Date( return new Date(
new DateTime(`${date}T${time}:00`, TimeZone.zone(timezone, false)) new DateTime(
`${date.toISOString().split('T')[0]}T${time}:00`,
TimeZone.zone(timezone, false),
)
.convert(TimeZone.zone('UTC')) .convert(TimeZone.zone('UTC'))
.toIsoString(), .toIsoString(),
); );

View File

@ -0,0 +1,5 @@
export type IDefaultParams = {
DEFAULT_TIMEZONE: string;
GEOROUTER_TYPE: string;
GEOROUTER_URL: string;
};

View File

@ -1,4 +1,4 @@
export enum Frequency { export enum Frequency {
PUNCTUAL = 1, PUNCTUAL = 'PUNCTUAL',
RECURRENT = 2, RECURRENT = 'RECURRENT',
} }

View File

@ -0,0 +1,4 @@
export enum Role {
DRIVER = 'DRIVER',
PASSENGER = 'PASSENGER',
}

View File

@ -20,6 +20,16 @@ export class CreateAdUseCase {
CreateAdRequest, CreateAdRequest,
Ad, Ad,
); );
adToCreate.driverDistance = command.geography.driverRoute?.distance;
adToCreate.driverDuration = command.geography.driverRoute?.duration;
adToCreate.passengerDistance = command.geography.passengerRoute?.distance;
adToCreate.passengerDuration = command.geography.passengerRoute?.duration;
adToCreate.fwdAzimuth = command.geography.driverRoute
? command.geography.driverRoute.fwdAzimuth
: command.geography.passengerRoute.fwdAzimuth;
adToCreate.backAzimuth = command.geography.driverRoute
? command.geography.driverRoute.backAzimuth
: command.geography.passengerRoute.backAzimuth;
return adToCreate; return adToCreate;
// return await this.adRepository.createAd(adToCreate); // return await this.adRepository.createAd(adToCreate);
} catch (error) { } catch (error) {

View File

@ -4,7 +4,6 @@ import { Injectable } from '@nestjs/common';
import { Ad } from '../domain/entities/ad'; import { Ad } from '../domain/entities/ad';
import { AdPresenter } from '../adapters/primaries/ad.presenter'; import { AdPresenter } from '../adapters/primaries/ad.presenter';
import { CreateAdRequest } from '../domain/dtos/create-ad.request'; import { CreateAdRequest } from '../domain/dtos/create-ad.request';
import { Coordinates } from '../../geography/domain/entities/coordinates';
import { TimeConverter } from '../domain/entities/time-converter'; import { TimeConverter } from '../domain/entities/time-converter';
@Injectable() @Injectable()
@ -16,43 +15,10 @@ export class AdProfile extends AutomapperProfile {
override get profile() { override get profile() {
return (mapper: any) => { return (mapper: any) => {
createMap(mapper, Ad, AdPresenter); createMap(mapper, Ad, AdPresenter);
createMap(
mapper,
CreateAdRequest,
CreateAdRequest,
forMember(
(dest) => dest.waypoints,
mapFrom((source) =>
source.waypoints.map(
(waypoint) =>
new Coordinates(
waypoint.lon ?? undefined,
waypoint.lat ?? undefined,
),
),
),
),
);
createMap( createMap(
mapper, mapper,
CreateAdRequest, CreateAdRequest,
Ad, Ad,
forMember(
(dest) => dest.fromDate,
mapFrom((source) => new Date(source.fromDate)),
),
forMember(
(dest) => dest.toDate,
mapFrom((source) => new Date(source.toDate)),
),
forMember(
(dest) => dest.createdAt,
mapFrom((source) => new Date(source.createdAt)),
),
forMember(
(dest) => dest.updatedAt,
mapFrom((source) => new Date(source.updatedAt)),
),
forMember( forMember(
(dest) => dest.monTime, (dest) => dest.monTime,
mapFrom(({ monTime: time, fromDate: date, timezone }) => mapFrom(({ monTime: time, fromDate: date, timezone }) =>

View File

@ -0,0 +1,38 @@
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { DefaultParamsProvider } from '../../../../adapters/secondaries/default-params.provider';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
const mockConfigService = {
get: jest.fn().mockImplementation(() => 'some_default_value'),
};
describe('DefaultParamsProvider', () => {
let defaultParamsProvider: DefaultParamsProvider;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
DefaultParamsProvider,
{
provide: ConfigService,
useValue: mockConfigService,
},
],
}).compile();
defaultParamsProvider = module.get<DefaultParamsProvider>(
DefaultParamsProvider,
);
});
it('should be defined', () => {
expect(defaultParamsProvider).toBeDefined();
});
it('should provide default params', async () => {
const params: IDefaultParams = defaultParamsProvider.getParams();
expect(params.GEOROUTER_URL).toBe('some_default_value');
});
});

View File

@ -0,0 +1,42 @@
import { Frequency } from '../../../domain/types/frequency.enum';
import { Ad } from '../../../domain/entities/ad';
import { AdCompleter } from '../../../domain/entities/ad.completer';
import { Coordinates } from '../../../../geography/domain/entities/coordinates';
const ad: Ad = new Ad();
ad.driver = true;
ad.passenger = false;
ad.frequency = Frequency.RECURRENT;
ad.fromDate = new Date('2023-05-01');
ad.toDate = new Date('2024-05-01');
ad.monTime = new Date('2023-05-01T06:00:00.000Z');
ad.tueTime = new Date('2023-05-01T06:00:00.000Z');
ad.wedTime = new Date('2023-05-01T06:00:00.000Z');
ad.thuTime = new Date('2023-05-01T06:00:00.000Z');
ad.friTime = new Date('2023-05-01T06:00:00.000Z');
ad.monMargin = 900;
ad.tueMargin = 900;
ad.wedMargin = 900;
ad.thuMargin = 900;
ad.friMargin = 900;
ad.satMargin = 900;
ad.sunMargin = 900;
ad.waypoints = [new Coordinates(6.18, 48.69), new Coordinates(6.44, 48.85)];
ad.seatsDriver = 3;
ad.seatsPassenger = 1;
ad.strict = false;
describe('AdCompleter', () => {
it('should be defined', () => {
expect(new AdCompleter()).toBeDefined();
});
describe('complete', () => {
it('should complete an ad', async () => {
const ad: Ad = new Ad();
const adCompleter: AdCompleter = new AdCompleter();
const completedAd: Ad = await adCompleter.complete(ad);
expect(completedAd.fwdAzimuth).toBe(45);
});
});
});

View File

@ -1,5 +1,4 @@
import { CreateAdRequest } from '../../../domain/dtos/create-ad.request'; import { CreateAdRequest } from '../../../domain/dtos/create-ad.request';
import { PointType } from '../../../../geography/domain/types/point-type.enum';
import { CreateAdUseCase } from '../../../domain/usecases/create-ad.usecase'; import { CreateAdUseCase } from '../../../domain/usecases/create-ad.usecase';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { AutomapperModule } from '@automapper/nestjs'; import { AutomapperModule } from '@automapper/nestjs';
@ -8,16 +7,28 @@ import { AdRepository } from '../../../adapters/secondaries/ad.repository';
import { CreateAdCommand } from '../../../commands/create-ad.command'; import { CreateAdCommand } from '../../../commands/create-ad.command';
import { Ad } from '../../../domain/entities/ad'; import { Ad } from '../../../domain/entities/ad';
import { AdProfile } from '../../../mappers/ad.profile'; import { AdProfile } from '../../../mappers/ad.profile';
import { Frequency } from '../../../domain/types/frequency.enum';
import { IDefaultParams } from '../../../domain/types/default-params.type';
const mockAdRepository = {}; const mockAdRepository = {};
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
GEOROUTER_TYPE: 'graphhopper',
GEOROUTER_URL: 'http://localhost',
};
const createAdRequest: CreateAdRequest = { const createAdRequest: CreateAdRequest = {
uuid: '77c55dfc-c28b-4026-942e-f94e95401fb1', uuid: '77c55dfc-c28b-4026-942e-f94e95401fb1',
userUuid: 'dfd993f6-7889-4876-9570-5e1d7b6e3f42',
driver: true, driver: true,
passenger: false, passenger: false,
frequency: 2, frequency: Frequency.RECURRENT,
fromDate: '2023-04-26', fromDate: new Date('2023-04-26'),
toDate: '2024-04-25', toDate: new Date('2024-04-25'),
monTime: '07:00', monTime: '07:00',
tueTime: '07:00', tueTime: '07:00',
wedTime: '07:00', wedTime: '07:00',
@ -32,12 +43,9 @@ const createAdRequest: CreateAdRequest = {
friMargin: 900, friMargin: 900,
satMargin: 900, satMargin: 900,
sunMargin: 900, sunMargin: 900,
originType: PointType.OTHER,
destinationType: PointType.OTHER,
seatsDriver: 3, seatsDriver: 3,
seatsPassenger: 1, seatsPassenger: 1,
createdAt: '2023-04-01 12:45', strict: false,
updatedAt: '2023-04-01 12:45',
waypoints: [ waypoints: [
{ lon: 6, lat: 45 }, { lon: 6, lat: 45 },
{ lon: 6.5, lat: 45.5 }, { lon: 6.5, lat: 45.5 },
@ -70,7 +78,11 @@ describe('CreateAdUseCase', () => {
describe('execute', () => { describe('execute', () => {
it('should create an ad', async () => { it('should create an ad', async () => {
const ad = await createAdUseCase.execute( const ad = await createAdUseCase.execute(
new CreateAdCommand(createAdRequest), new CreateAdCommand(
createAdRequest,
defaultParams,
mockGeorouterCreator,
),
); );
expect(ad).toBeInstanceOf(Ad); expect(ad).toBeInstanceOf(Ad);
}); });

View File

@ -8,7 +8,7 @@ describe('TimeConverter', () => {
it('should convert a Europe/Paris datetime to utc datetime', () => { it('should convert a Europe/Paris datetime to utc datetime', () => {
expect( expect(
TimeConverter.toUtcDatetime( TimeConverter.toUtcDatetime(
'2023-05-01', new Date('2023-05-01'),
'07:00', '07:00',
'Europe/Paris', 'Europe/Paris',
).getUTCHours(), ).getUTCHours(),
@ -20,16 +20,28 @@ describe('TimeConverter', () => {
TimeConverter.toUtcDatetime(undefined, '07:00', 'Europe/Paris'), TimeConverter.toUtcDatetime(undefined, '07:00', 'Europe/Paris'),
).toBeUndefined(); ).toBeUndefined();
expect( expect(
TimeConverter.toUtcDatetime('2023-13-01', '07:00', 'Europe/Paris'), TimeConverter.toUtcDatetime(
new Date('2023-13-01'),
'07:00',
'Europe/Paris',
),
).toBeUndefined(); ).toBeUndefined();
expect( expect(
TimeConverter.toUtcDatetime('2023-05-01', undefined, 'Europe/Paris'), TimeConverter.toUtcDatetime(
new Date('2023-05-01'),
undefined,
'Europe/Paris',
),
).toBeUndefined(); ).toBeUndefined();
expect( expect(
TimeConverter.toUtcDatetime('2023-05-01', 'a', 'Europe/Paris'), TimeConverter.toUtcDatetime(new Date('2023-05-01'), 'a', 'Europe/Paris'),
).toBeUndefined(); ).toBeUndefined();
expect( expect(
TimeConverter.toUtcDatetime('2023-13-01', '07:00', 'OlympusMons/Mars'), TimeConverter.toUtcDatetime(
new Date('2023-13-01'),
'07:00',
'OlympusMons/Mars',
),
).toBeUndefined(); ).toBeUndefined();
}); });
}); });

View File

@ -10,7 +10,7 @@ import { PrismaService } from './prisma-service';
*/ */
@Injectable() @Injectable()
export abstract class PrismaRepository<T> implements IRepository<T> { export abstract class PrismaRepository<T> implements IRepository<T> {
protected _model: string; protected model: string;
constructor(protected readonly _prisma: PrismaService) {} constructor(protected readonly _prisma: PrismaService) {}
@ -21,13 +21,13 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
include?: any, include?: any,
): Promise<ICollection<T>> { ): Promise<ICollection<T>> {
const [data, total] = await this._prisma.$transaction([ const [data, total] = await this._prisma.$transaction([
this._prisma[this._model].findMany({ this._prisma[this.model].findMany({
where, where,
include, include,
skip: (page - 1) * perPage, skip: (page - 1) * perPage,
take: perPage, take: perPage,
}), }),
this._prisma[this._model].count({ this._prisma[this.model].count({
where, where,
}), }),
]); ]);
@ -39,7 +39,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
async findOneByUuid(uuid: string): Promise<T> { async findOneByUuid(uuid: string): Promise<T> {
try { try {
const entity = await this._prisma[this._model].findUnique({ const entity = await this._prisma[this.model].findUnique({
where: { uuid }, where: { uuid },
}); });
@ -59,7 +59,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
async findOne(where: any, include?: any): Promise<T> { async findOne(where: any, include?: any): Promise<T> {
try { try {
const entity = await this._prisma[this._model].findFirst({ const entity = await this._prisma[this.model].findFirst({
where: where, where: where,
include: include, include: include,
}); });
@ -81,7 +81,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
// TODO : Refactor for good clean architecture ? // TODO : Refactor for good clean architecture ?
async create(entity: Partial<T> | any, include?: any): Promise<T> { async create(entity: Partial<T> | any, include?: any): Promise<T> {
try { try {
const res = await this._prisma[this._model].create({ const res = await this._prisma[this.model].create({
data: entity, data: entity,
include: include, include: include,
}); });
@ -102,7 +102,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
async update(uuid: string, entity: Partial<T>): Promise<T> { async update(uuid: string, entity: Partial<T>): Promise<T> {
try { try {
const updatedEntity = await this._prisma[this._model].update({ const updatedEntity = await this._prisma[this.model].update({
where: { uuid }, where: { uuid },
data: entity, data: entity,
}); });
@ -126,7 +126,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
include?: any, include?: any,
): Promise<T> { ): Promise<T> {
try { try {
const updatedEntity = await this._prisma[this._model].update({ const updatedEntity = await this._prisma[this.model].update({
where: where, where: where,
data: entity, data: entity,
include: include, include: include,
@ -148,7 +148,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
async delete(uuid: string): Promise<T> { async delete(uuid: string): Promise<T> {
try { try {
const entity = await this._prisma[this._model].delete({ const entity = await this._prisma[this.model].delete({
where: { uuid }, where: { uuid },
}); });
@ -168,7 +168,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
async deleteMany(where: any): Promise<void> { async deleteMany(where: any): Promise<void> {
try { try {
const entity = await this._prisma[this._model].deleteMany({ const entity = await this._prisma[this.model].deleteMany({
where: where, where: where,
}); });
@ -191,7 +191,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
where: string[], where: string[],
): Promise<ICollection<T>> { ): Promise<ICollection<T>> {
const query = `SELECT ${include.join(',')} FROM ${ const query = `SELECT ${include.join(',')} FROM ${
this._model this.model
} WHERE ${where.join(' AND ')}`; } WHERE ${where.join(' AND ')}`;
const data: T[] = await this._prisma.$queryRawUnsafe(query); const data: T[] = await this._prisma.$queryRawUnsafe(query);
return Promise.resolve({ return Promise.resolve({
@ -202,7 +202,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
async createWithFields(fields: object): Promise<number> { async createWithFields(fields: object): Promise<number> {
try { try {
const command = `INSERT INTO ${this._model} ("${Object.keys(fields).join( const command = `INSERT INTO ${this.model} ("${Object.keys(fields).join(
'","', '","',
)}") VALUES (${Object.values(fields).join(',')})`; )}") VALUES (${Object.values(fields).join(',')})`;
return await this._prisma.$executeRawUnsafe(command); return await this._prisma.$executeRawUnsafe(command);
@ -223,7 +223,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
entity['"updatedAt"'] = `to_timestamp(${Date.now()} / 1000.0)`; entity['"updatedAt"'] = `to_timestamp(${Date.now()} / 1000.0)`;
const values = Object.keys(entity).map((key) => `${key} = ${entity[key]}`); const values = Object.keys(entity).map((key) => `${key} = ${entity[key]}`);
try { try {
const command = `UPDATE ${this._model} SET ${values.join( const command = `UPDATE ${this.model} SET ${values.join(
', ', ', ',
)} WHERE uuid = '${uuid}'`; )} WHERE uuid = '${uuid}'`;
return await this._prisma.$executeRawUnsafe(command); return await this._prisma.$executeRawUnsafe(command);

View File

@ -41,7 +41,7 @@ Array.from({ length: 10 }).forEach(() => {
@Injectable() @Injectable()
class FakePrismaRepository extends PrismaRepository<FakeEntity> { class FakePrismaRepository extends PrismaRepository<FakeEntity> {
protected _model = 'fake'; protected model = 'fake';
} }
class FakePrismaService extends PrismaService { class FakePrismaService extends PrismaService {

View File

@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { ICreateGeorouter } from '../../domain/interfaces/georouter-creator.interface';
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
import { GraphhopperGeorouter } from './graphhopper-georouter';
import { HttpService } from '@nestjs/axios';
import { Geodesic } from './geodesic';
import { GeographyException } from '../../exceptions/geography.exception';
import { ExceptionCode } from '../../..//utils/exception-code.enum';
@Injectable()
export class GeorouterCreator implements ICreateGeorouter {
constructor(
private readonly httpService: HttpService,
private readonly geodesic: Geodesic,
) {}
create = (type: string, url: string): IGeorouter => {
switch (type) {
case 'graphhopper':
return new GraphhopperGeorouter(url, this.httpService, this.geodesic);
default:
throw new GeographyException(
ExceptionCode.INVALID_ARGUMENT,
'Unknown geocoder',
);
}
};
}

View File

@ -0,0 +1,324 @@
import { HttpService } from '@nestjs/axios';
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
import { Injectable } from '@nestjs/common';
import { catchError, lastValueFrom, map } from 'rxjs';
import { AxiosError, AxiosResponse } from 'axios';
import { IGeodesic } from '../../../geography/domain/interfaces/geodesic.interface';
import { GeorouterSettings } from '../../domain/types/georouter-settings.type';
import { Path } from '../../domain/types/path.type';
import { NamedRoute } from '../../domain/types/named-route';
import { GeographyException } from '../../exceptions/geography.exception';
import { ExceptionCode } from '../../..//utils/exception-code.enum';
import { Route } from '../../domain/entities/route';
import { SpacetimePoint } from '../../domain/entities/spacetime-point';
@Injectable()
export class GraphhopperGeorouter implements IGeorouter {
private url: string;
private urlArgs: string[];
private withTime: boolean;
private withPoints: boolean;
private withDistance: boolean;
private paths: Path[];
private httpService: HttpService;
private geodesic: IGeodesic;
constructor(url: string, httpService: HttpService, geodesic: IGeodesic) {
this.url = url + '/route?';
this.httpService = httpService;
this.geodesic = geodesic;
}
route = async (
paths: Path[],
settings: GeorouterSettings,
): Promise<NamedRoute[]> => {
this.setDefaultUrlArgs();
this.setWithTime(settings.withTime);
this.setWithPoints(settings.withPoints);
this.setWithDistance(settings.withDistance);
this.paths = paths;
return await this.getRoutes();
};
private setDefaultUrlArgs = (): void => {
this.urlArgs = ['vehicle=car', 'weighting=fastest', 'points_encoded=false'];
};
private setWithTime = (withTime: boolean): void => {
this.withTime = withTime;
if (withTime) {
this.urlArgs.push('details=time');
}
};
private setWithPoints = (withPoints: boolean): void => {
this.withPoints = withPoints;
if (!withPoints) {
this.urlArgs.push('calc_points=false');
}
};
private setWithDistance = (withDistance: boolean): void => {
this.withDistance = withDistance;
if (withDistance) {
this.urlArgs.push('instructions=true');
} else {
this.urlArgs.push('instructions=false');
}
};
private getRoutes = async (): Promise<NamedRoute[]> => {
const routes = Promise.all(
this.paths.map(async (path) => {
const url: string = [
this.getUrl(),
'&point=',
path.points
.map((point) => [point.lat, point.lon].join())
.join('&point='),
].join('');
const route = await lastValueFrom(
this.httpService.get(url).pipe(
map((res) => (res.data ? this.createRoute(res) : undefined)),
catchError((error: AxiosError) => {
throw new GeographyException(
ExceptionCode.INTERNAL,
'Georouter unavailable : ' + error.message,
);
}),
),
);
return <NamedRoute>{
key: path.key,
route,
};
}),
);
return routes;
};
private getUrl = (): string => {
return [this.url, this.urlArgs.join('&')].join('');
};
private createRoute = (
response: AxiosResponse<GraphhopperResponse>,
): Route => {
const route = new Route(this.geodesic);
if (response.data.paths && response.data.paths[0]) {
const shortestPath = response.data.paths[0];
route.distance = shortestPath.distance ?? 0;
route.duration = shortestPath.time ? shortestPath.time / 1000 : 0;
if (shortestPath.points && shortestPath.points.coordinates) {
route.setPoints(
shortestPath.points.coordinates.map((coordinate) => ({
lon: coordinate[0],
lat: coordinate[1],
})),
);
if (
shortestPath.details &&
shortestPath.details.time &&
shortestPath.snapped_waypoints &&
shortestPath.snapped_waypoints.coordinates
) {
let instructions: GraphhopperInstruction[] = [];
if (shortestPath.instructions)
instructions = shortestPath.instructions;
route.setSpacetimePoints(
this.generateSpacetimePoints(
shortestPath.points.coordinates,
shortestPath.snapped_waypoints.coordinates,
shortestPath.details.time,
instructions,
),
);
}
}
}
return route;
};
private generateSpacetimePoints = (
points: Array<number[]>,
snappedWaypoints: Array<number[]>,
durations: Array<number[]>,
instructions: GraphhopperInstruction[],
): SpacetimePoint[] => {
const indices = this.getIndices(points, snappedWaypoints);
const times = this.getTimes(durations, indices);
const distances = this.getDistances(instructions, indices);
return indices.map(
(index) =>
new SpacetimePoint(
{ lon: points[index][1], lat: points[index][0] },
times.find((time) => time.index == index)?.duration,
distances.find((distance) => distance.index == index)?.distance,
),
);
};
private getIndices = (
points: Array<number[]>,
snappedWaypoints: Array<number[]>,
): number[] => {
const indices = snappedWaypoints.map((waypoint) =>
points.findIndex(
(point) => point[0] == waypoint[0] && point[1] == waypoint[1],
),
);
if (indices.find((index) => index == -1) === undefined) return indices;
const missedWaypoints = indices
.map(
(value, index) =>
<
{
index: number;
originIndex: number;
waypoint: number[];
nearest: number;
distance: number;
}
>{
index: value,
originIndex: index,
waypoint: snappedWaypoints[index],
nearest: undefined,
distance: 999999999,
},
)
.filter((element) => element.index == -1);
for (const index in points) {
for (const missedWaypoint of missedWaypoints) {
const inverse = this.geodesic.inverse(
missedWaypoint.waypoint[0],
missedWaypoint.waypoint[1],
points[index][0],
points[index][1],
);
if (inverse.distance < missedWaypoint.distance) {
missedWaypoint.distance = inverse.distance;
missedWaypoint.nearest = parseInt(index);
}
}
}
for (const missedWaypoint of missedWaypoints) {
indices[missedWaypoint.originIndex] = missedWaypoint.nearest;
}
return indices;
};
private getTimes = (
durations: Array<number[]>,
indices: number[],
): Array<{ index: number; duration: number }> => {
const times: Array<{ index: number; duration: number }> = [];
let duration = 0;
for (const [origin, destination, stepDuration] of durations) {
let indexFound = false;
const indexAsOrigin = indices.find((index) => index == origin);
if (
indexAsOrigin !== undefined &&
times.find((time) => origin == time.index) == undefined
) {
times.push({
index: indexAsOrigin,
duration: Math.round(stepDuration / 1000),
});
indexFound = true;
}
if (!indexFound) {
const indexAsDestination = indices.find(
(index) => index == destination,
);
if (
indexAsDestination !== undefined &&
times.find((time) => destination == time.index) == undefined
) {
times.push({
index: indexAsDestination,
duration: Math.round((duration + stepDuration) / 1000),
});
indexFound = true;
}
}
if (!indexFound) {
const indexInBetween = indices.find(
(index) => origin < index && index < destination,
);
if (indexInBetween !== undefined) {
times.push({
index: indexInBetween,
duration: Math.round((duration + stepDuration / 2) / 1000),
});
}
}
duration += stepDuration;
}
return times;
};
private getDistances = (
instructions: GraphhopperInstruction[],
indices: number[],
): Array<{ index: number; distance: number }> => {
let distance = 0;
const distances: Array<{ index: number; distance: number }> = [
{
index: 0,
distance,
},
];
for (const instruction of instructions) {
distance += instruction.distance;
if (
(instruction.sign == GraphhopperSign.SIGN_WAYPOINT ||
instruction.sign == GraphhopperSign.SIGN_FINISH) &&
indices.find((index) => index == instruction.interval[0]) !== undefined
) {
distances.push({
index: instruction.interval[0],
distance: Math.round(distance),
});
}
}
return distances;
};
}
type GraphhopperResponse = {
paths: [
{
distance: number;
weight: number;
time: number;
points_encoded: boolean;
bbox: number[];
points: GraphhopperCoordinates;
snapped_waypoints: GraphhopperCoordinates;
details: {
time: Array<number[]>;
};
instructions: GraphhopperInstruction[];
},
];
};
type GraphhopperCoordinates = {
coordinates: Array<number[]>;
};
type GraphhopperInstruction = {
distance: number;
heading: number;
sign: GraphhopperSign;
interval: number[];
text: string;
};
enum GraphhopperSign {
SIGN_START = 0,
SIGN_FINISH = 4,
SIGN_WAYPOINT = 5,
}

View File

@ -1,7 +1,6 @@
import { IGeodesic } from '../../../../geography/domain/interfaces/geodesic.interface'; import { IGeodesic } from '../interfaces/geodesic.interface';
import { Point } from '../../../../geography/domain/types/point.type'; import { Point } from '../types/point.type';
import { SpacetimePoint } from './spacetime-point'; import { SpacetimePoint } from './spacetime-point';
import { Waypoint } from './waypoint';
export class Route { export class Route {
distance: number; distance: number;
@ -9,7 +8,6 @@ export class Route {
fwdAzimuth: number; fwdAzimuth: number;
backAzimuth: number; backAzimuth: number;
distanceAzimuth: number; distanceAzimuth: number;
waypoints: Waypoint[];
points: Point[]; points: Point[];
spacetimePoints: SpacetimePoint[]; spacetimePoints: SpacetimePoint[];
private geodesic: IGeodesic; private geodesic: IGeodesic;
@ -20,17 +18,11 @@ export class Route {
this.fwdAzimuth = undefined; this.fwdAzimuth = undefined;
this.backAzimuth = undefined; this.backAzimuth = undefined;
this.distanceAzimuth = undefined; this.distanceAzimuth = undefined;
this.waypoints = [];
this.points = []; this.points = [];
this.spacetimePoints = []; this.spacetimePoints = [];
this.geodesic = geodesic; this.geodesic = geodesic;
} }
setWaypoints = (waypoints: Waypoint[]): void => {
this.waypoints = waypoints;
this.setAzimuth(waypoints.map((waypoint) => waypoint.point));
};
setPoints = (points: Point[]): void => { setPoints = (points: Point[]): void => {
this.points = points; this.points = points;
this.setAzimuth(points); this.setAzimuth(points);
@ -40,7 +32,7 @@ export class Route {
this.spacetimePoints = spacetimePoints; this.spacetimePoints = spacetimePoints;
}; };
private setAzimuth = (points: Point[]): void => { protected setAzimuth = (points: Point[]): void => {
const inverse = this.geodesic.inverse( const inverse = this.geodesic.inverse(
points[0].lon, points[0].lon,
points[0].lat, points[0].lat,

View File

@ -0,0 +1,13 @@
import { Coordinates } from './coordinates';
export class SpacetimePoint {
coordinates: Coordinates;
duration: number;
distance: number;
constructor(coordinates: Coordinates, duration: number, distance: number) {
this.coordinates = coordinates;
this.duration = duration;
this.distance = distance;
}
}

View File

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

View File

@ -0,0 +1,7 @@
import { GeorouterSettings } from '../types/georouter-settings.type';
import { NamedRoute } from '../types/named-route';
import { Path } from '../types/path.type';
export interface IGeorouter {
route(paths: Path[], settings: GeorouterSettings): Promise<NamedRoute[]>;
}

View File

@ -0,0 +1,5 @@
export type GeorouterSettings = {
withPoints: boolean;
withTime: boolean;
withDistance: boolean;
};

View File

@ -0,0 +1,6 @@
import { Route } from '../entities/route';
export type NamedRoute = {
key: string;
route: Route;
};

View File

@ -0,0 +1,6 @@
import { Point } from '../../../geography/domain/types/point.type';
export type Path = {
key: string;
points: Point[];
};

View File

@ -0,0 +1,6 @@
import { IFindTimezone } from '../interfaces/timezone-finder.interface';
export type Timezoner = {
timezone: string;
finder: IFindTimezone;
};

View File

@ -0,0 +1,13 @@
export class GeographyException implements Error {
name: string;
message: string;
constructor(private _code: number, private _message: string) {
this.name = 'GeographyException';
this.message = _message;
}
get code(): number {
return this._code;
}
}

View File

@ -0,0 +1,47 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HttpService } from '@nestjs/axios';
import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator';
import { Geodesic } from '../../adapters/secondaries/geodesic';
import { GraphhopperGeorouter } from '../../adapters/secondaries/graphhopper-georouter';
const mockHttpService = jest.fn();
const mockGeodesic = jest.fn();
describe('Georouter creator', () => {
let georouterCreator: GeorouterCreator;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
GeorouterCreator,
{
provide: HttpService,
useValue: mockHttpService,
},
{
provide: Geodesic,
useValue: mockGeodesic,
},
],
}).compile();
georouterCreator = module.get<GeorouterCreator>(GeorouterCreator);
});
it('should be defined', () => {
expect(georouterCreator).toBeDefined();
});
it('should create a graphhopper georouter', () => {
const georouter = georouterCreator.create(
'graphhopper',
'http://localhost',
);
expect(georouter).toBeInstanceOf(GraphhopperGeorouter);
});
it('should throw an exception if georouter type is unknown', () => {
expect(() =>
georouterCreator.create('unknown', 'http://localhost'),
).toThrow();
});
});

View File

@ -0,0 +1,456 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HttpService } from '@nestjs/axios';
import { of } from 'rxjs';
import { AxiosError } from 'axios';
import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator';
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
import { Geodesic } from '../../adapters/secondaries/geodesic';
const mockHttpService = {
get: jest
.fn()
.mockImplementationOnce(() => {
throw new AxiosError('Axios error !');
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
snapped_waypoints: {
coordinates: [
[0, 0],
[10, 10],
],
},
},
],
},
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
points: {
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
[6, 6],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
],
},
snapped_waypoints: {
coordinates: [
[0, 0],
[10, 10],
],
},
},
],
},
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
points: {
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
[6, 6],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
],
},
details: {
time: [
[0, 1, 180000],
[1, 2, 180000],
[2, 3, 180000],
[3, 4, 180000],
[4, 5, 180000],
[5, 6, 180000],
[6, 7, 180000],
[7, 9, 360000],
[9, 10, 180000],
],
},
snapped_waypoints: {
coordinates: [
[0, 0],
[10, 10],
],
},
},
],
},
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
points: {
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
],
},
snapped_waypoints: {
coordinates: [
[0, 0],
[5, 5],
[10, 10],
],
},
details: {
time: [
[0, 1, 180000],
[1, 2, 180000],
[2, 3, 180000],
[3, 4, 180000],
[4, 7, 540000],
[7, 9, 360000],
[9, 10, 180000],
],
},
},
],
},
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
points: {
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
[6, 6],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
],
},
snapped_waypoints: {
coordinates: [
[0, 0],
[5, 5],
[10, 10],
],
},
details: {
time: [
[0, 1, 180000],
[1, 2, 180000],
[2, 3, 180000],
[3, 4, 180000],
[4, 7, 540000],
[7, 9, 360000],
[9, 10, 180000],
],
},
instructions: [
{
distance: 25000,
sign: 0,
interval: [0, 5],
text: 'Some instructions',
time: 900000,
},
{
distance: 0,
sign: 5,
interval: [5, 5],
text: 'Waypoint 1',
time: 0,
},
{
distance: 25000,
sign: 2,
interval: [5, 10],
text: 'Some instructions',
time: 900000,
},
{
distance: 0.0,
sign: 4,
interval: [10, 10],
text: 'Arrive at destination',
time: 0,
},
],
},
],
},
});
}),
};
const mockGeodesic = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
inverse: jest.fn().mockImplementation(() => ({
azimuth: 45,
distance: 50000,
})),
};
describe('Graphhopper Georouter', () => {
let georouterCreator: GeorouterCreator;
let graphhopperGeorouter: IGeorouter;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
GeorouterCreator,
{
provide: HttpService,
useValue: mockHttpService,
},
{
provide: Geodesic,
useValue: mockGeodesic,
},
],
}).compile();
georouterCreator = module.get<GeorouterCreator>(GeorouterCreator);
graphhopperGeorouter = georouterCreator.create(
'graphhopper',
'http://localhost',
);
});
it('should be defined', () => {
expect(graphhopperGeorouter).toBeDefined();
});
describe('route function', () => {
it('should fail on axios error', async () => {
await expect(
graphhopperGeorouter.route(
[
{
key: 'route1',
points: [
{
lat: 0,
lon: 0,
},
{
lat: 1,
lon: 1,
},
],
},
],
{
withDistance: false,
withPoints: false,
withTime: false,
},
),
).rejects.toBeInstanceOf(Error);
});
it('should create one route with all settings to false', async () => {
const routes = await graphhopperGeorouter.route(
[
{
key: 'route1',
points: [
{
lat: 0,
lon: 0,
},
{
lat: 10,
lon: 10,
},
],
},
],
{
withDistance: false,
withPoints: false,
withTime: false,
},
);
expect(routes).toHaveLength(1);
expect(routes[0].route.distance).toBe(50000);
});
it('should create one route with points', async () => {
const routes = await graphhopperGeorouter.route(
[
{
key: 'route1',
points: [
{
lat: 0,
lon: 0,
},
{
lat: 10,
lon: 10,
},
],
},
],
{
withDistance: false,
withPoints: true,
withTime: false,
},
);
expect(routes).toHaveLength(1);
expect(routes[0].route.distance).toBe(50000);
expect(routes[0].route.duration).toBe(1800);
expect(routes[0].route.fwdAzimuth).toBe(45);
expect(routes[0].route.backAzimuth).toBe(225);
expect(routes[0].route.points.length).toBe(11);
});
it('should create one route with points and time', async () => {
const routes = await graphhopperGeorouter.route(
[
{
key: 'route1',
points: [
{
lat: 0,
lon: 0,
},
{
lat: 10,
lon: 10,
},
],
},
],
{
withDistance: false,
withPoints: true,
withTime: true,
},
);
expect(routes).toHaveLength(1);
expect(routes[0].route.spacetimePoints.length).toBe(2);
expect(routes[0].route.spacetimePoints[1].duration).toBe(1800);
expect(routes[0].route.spacetimePoints[1].distance).toBeUndefined();
});
it('should create one route with points and missed waypoints extrapolations', async () => {
const routes = await graphhopperGeorouter.route(
[
{
key: 'route1',
points: [
{
lat: 0,
lon: 0,
},
{
lat: 5,
lon: 5,
},
{
lat: 10,
lon: 10,
},
],
},
],
{
withDistance: false,
withPoints: true,
withTime: true,
},
);
expect(routes).toHaveLength(1);
expect(routes[0].route.spacetimePoints.length).toBe(3);
expect(routes[0].route.distance).toBe(50000);
expect(routes[0].route.duration).toBe(1800);
expect(routes[0].route.fwdAzimuth).toBe(45);
expect(routes[0].route.backAzimuth).toBe(225);
expect(routes[0].route.points.length).toBe(9);
});
it('should create one route with points, time and distance', async () => {
const routes = await graphhopperGeorouter.route(
[
{
key: 'route1',
points: [
{
lat: 0,
lon: 0,
},
{
lat: 10,
lon: 10,
},
],
},
],
{
withDistance: true,
withPoints: true,
withTime: true,
},
);
expect(routes).toHaveLength(1);
expect(routes[0].route.spacetimePoints.length).toBe(3);
expect(routes[0].route.spacetimePoints[1].duration).toBe(990);
expect(routes[0].route.spacetimePoints[1].distance).toBe(25000);
});
});
});

View File

@ -0,0 +1,48 @@
import { Route } from '../../domain/entities/route';
import { SpacetimePoint } from '../../domain/entities/spacetime-point';
const mockGeodesic = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
inverse: jest.fn().mockImplementation((lon1, lat1, lon2, lat2) => {
return lon1 == 0
? {
azimuth: 45,
distance: 50000,
}
: {
azimuth: -45,
distance: 60000,
};
}),
};
describe('Route entity', () => {
it('should be defined', () => {
const route = new Route(mockGeodesic);
expect(route).toBeDefined();
});
it('should set points and geodesic values for a route', () => {
const route = new Route(mockGeodesic);
route.setPoints([
{
lon: 10,
lat: 10,
},
{
lon: 20,
lat: 20,
},
]);
expect(route.points.length).toBe(2);
expect(route.fwdAzimuth).toBe(315);
expect(route.backAzimuth).toBe(135);
expect(route.distanceAzimuth).toBe(60000);
});
it('should set spacetimePoints for a route', () => {
const route = new Route(mockGeodesic);
const spacetimePoint1 = new SpacetimePoint({ lon: 0, lat: 0 }, 0, 0);
const spacetimePoint2 = new SpacetimePoint({ lon: 10, lat: 10 }, 500, 5000);
route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]);
expect(route.spacetimePoints.length).toBe(2);
});
});

View File

@ -25,7 +25,7 @@ export class MatcherController {
constructor( constructor(
private readonly queryBus: QueryBus, private readonly queryBus: QueryBus,
private readonly defaultParamsProvider: DefaultParamsProvider, private readonly defaultParamsProvider: DefaultParamsProvider,
@InjectMapper() private readonly _mapper: Mapper, @InjectMapper() private readonly mapper: Mapper,
private readonly georouterCreator: GeorouterCreator, private readonly georouterCreator: GeorouterCreator,
private readonly timezoneFinder: GeoTimezoneFinder, private readonly timezoneFinder: GeoTimezoneFinder,
private readonly timeConverter: TimeConverter, private readonly timeConverter: TimeConverter,
@ -45,7 +45,7 @@ export class MatcherController {
); );
return Promise.resolve({ return Promise.resolve({
data: matchCollection.data.map((match: Match) => data: matchCollection.data.map((match: Match) =>
this._mapper.map(match, Match, MatchPresenter), this.mapper.map(match, Match, MatchPresenter),
), ),
total: matchCollection.total, total: matchCollection.total,
}); });

View File

@ -7,29 +7,28 @@ service MatcherService {
} }
message MatchRequest { message MatchRequest {
Mode mode = 1; string uuid = 1;
string uuid = 2; repeated Coordinates waypoints = 2;
repeated Coordinates waypoints = 3; string departure = 3;
string departure = 4; string fromDate = 4;
string fromDate = 5; Schedule schedule = 5;
Schedule schedule = 6; bool driver = 6;
bool driver = 7; bool passenger = 7;
bool passenger = 8; string toDate = 8;
string toDate = 9; int32 marginDuration = 9;
int32 marginDuration = 10; MarginDurations marginDurations = 10;
MarginDurations marginDurations = 11; int32 seatsPassenger = 11;
int32 seatsPassenger = 12; int32 seatsDriver = 12;
int32 seatsDriver = 13; bool strict = 13;
bool strict = 14; Algorithm algorithm = 14;
Algorithm algorithm = 15; int32 remoteness = 15;
int32 remoteness = 16; bool useProportion = 16;
bool useProportion = 17; int32 proportion = 17;
int32 proportion = 18; bool useAzimuth = 18;
bool useAzimuth = 19; int32 azimuthMargin = 19;
int32 azimuthMargin = 20; float maxDetourDistanceRatio = 20;
float maxDetourDistanceRatio = 21; float maxDetourDurationRatio = 21;
float maxDetourDurationRatio = 22; repeated int32 exclusions = 22;
repeated int32 exclusions = 23;
} }
message Coordinates { message Coordinates {
@ -69,9 +68,3 @@ message Matches {
repeated Match data = 1; repeated Match data = 1;
int32 total = 2; int32 total = 2;
} }
enum Mode {
MATCH = 0;
PUBLISH = 1;
PUBLISH_AND_MATCH = 2;
}

View File

@ -14,21 +14,21 @@ export class DefaultParamsProvider {
DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'), DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'),
DEFAULT_SEATS: parseInt(this.configService.get('DEFAULT_SEATS')), DEFAULT_SEATS: parseInt(this.configService.get('DEFAULT_SEATS')),
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: this.configService.get('ALGORITHM'), ALGORITHM: this.configService.get('ALGORITHM'),
strict: !!parseInt(this.configService.get('STRICT_ALGORITHM')), STRICT: !!parseInt(this.configService.get('STRICT_ALGORITHM')),
remoteness: parseInt(this.configService.get('REMOTENESS')), REMOTENESS: parseInt(this.configService.get('REMOTENESS')),
useProportion: !!parseInt(this.configService.get('USE_PROPORTION')), USE_PROPORTION: !!parseInt(this.configService.get('USE_PROPORTION')),
proportion: parseInt(this.configService.get('PROPORTION')), PROPORTION: parseInt(this.configService.get('PROPORTION')),
useAzimuth: !!parseInt(this.configService.get('USE_AZIMUTH')), USE_AZIMUTH: !!parseInt(this.configService.get('USE_AZIMUTH')),
azimuthMargin: parseInt(this.configService.get('AZIMUTH_MARGIN')), AZIMUTH_MARGIN: parseInt(this.configService.get('AZIMUTH_MARGIN')),
maxDetourDistanceRatio: parseFloat( MAX_DETOUR_DISTANCE_RATIO: parseFloat(
this.configService.get('MAX_DETOUR_DISTANCE_RATIO'), this.configService.get('MAX_DETOUR_DISTANCE_RATIO'),
), ),
maxDetourDurationRatio: parseFloat( MAX_DETOUR_DURATION_RATIO: parseFloat(
this.configService.get('MAX_DETOUR_DURATION_RATIO'), this.configService.get('MAX_DETOUR_DURATION_RATIO'),
), ),
georouterType: this.configService.get('GEOROUTER_TYPE'), GEOROUTER_TYPE: this.configService.get('GEOROUTER_TYPE'),
georouterUrl: this.configService.get('GEOROUTER_URL'), GEOROUTER_URL: this.configService.get('GEOROUTER_URL'),
}, },
}; };
}; };

View File

@ -7,7 +7,7 @@ import { catchError, lastValueFrom, map } from 'rxjs';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import { IGeodesic } from '../../../geography/domain/interfaces/geodesic.interface'; import { IGeodesic } from '../../../geography/domain/interfaces/geodesic.interface';
import { NamedRoute } from '../../domain/entities/ecosystem/named-route'; import { NamedRoute } from '../../domain/entities/ecosystem/named-route';
import { Route } from '../../domain/entities/ecosystem/route'; import { MatcherRoute } from '../../domain/entities/ecosystem/matcher-route';
import { SpacetimePoint } from '../../domain/entities/ecosystem/spacetime-point'; import { SpacetimePoint } from '../../domain/entities/ecosystem/spacetime-point';
import { import {
MatcherException, MatcherException,
@ -106,8 +106,8 @@ export class GraphhopperGeorouter implements IGeorouter {
private createRoute = ( private createRoute = (
response: AxiosResponse<GraphhopperResponse>, response: AxiosResponse<GraphhopperResponse>,
): Route => { ): MatcherRoute => {
const route = new Route(this.geodesic); const route = new MatcherRoute(this.geodesic);
if (response.data.paths && response.data.paths[0]) { if (response.data.paths && response.data.paths[0]) {
const shortestPath = response.data.paths[0]; const shortestPath = response.data.paths[0];
route.distance = shortestPath.distance ?? 0; route.distance = shortestPath.distance ?? 0;

View File

@ -27,33 +27,33 @@ export class AlgorithmSettings {
) { ) {
this.algorithmSettingsRequest = algorithmSettingsRequest; this.algorithmSettingsRequest = algorithmSettingsRequest;
this.algorithmType = this.algorithmType =
algorithmSettingsRequest.algorithm ?? defaultAlgorithmSettings.algorithm; algorithmSettingsRequest.algorithm ?? defaultAlgorithmSettings.ALGORITHM;
this.strict = this.strict =
algorithmSettingsRequest.strict ?? defaultAlgorithmSettings.strict; algorithmSettingsRequest.strict ?? defaultAlgorithmSettings.STRICT;
this.remoteness = algorithmSettingsRequest.remoteness this.remoteness = algorithmSettingsRequest.remoteness
? Math.abs(algorithmSettingsRequest.remoteness) ? Math.abs(algorithmSettingsRequest.remoteness)
: defaultAlgorithmSettings.remoteness; : defaultAlgorithmSettings.REMOTENESS;
this.useProportion = this.useProportion =
algorithmSettingsRequest.useProportion ?? algorithmSettingsRequest.useProportion ??
defaultAlgorithmSettings.useProportion; defaultAlgorithmSettings.USE_PROPORTION;
this.proportion = algorithmSettingsRequest.proportion this.proportion = algorithmSettingsRequest.proportion
? Math.abs(algorithmSettingsRequest.proportion) ? Math.abs(algorithmSettingsRequest.proportion)
: defaultAlgorithmSettings.proportion; : defaultAlgorithmSettings.PROPORTION;
this.useAzimuth = this.useAzimuth =
algorithmSettingsRequest.useAzimuth ?? algorithmSettingsRequest.useAzimuth ??
defaultAlgorithmSettings.useAzimuth; defaultAlgorithmSettings.USE_AZIMUTH;
this.azimuthMargin = algorithmSettingsRequest.azimuthMargin this.azimuthMargin = algorithmSettingsRequest.azimuthMargin
? Math.abs(algorithmSettingsRequest.azimuthMargin) ? Math.abs(algorithmSettingsRequest.azimuthMargin)
: defaultAlgorithmSettings.azimuthMargin; : defaultAlgorithmSettings.AZIMUTH_MARGIN;
this.maxDetourDistanceRatio = this.maxDetourDistanceRatio =
algorithmSettingsRequest.maxDetourDistanceRatio ?? algorithmSettingsRequest.maxDetourDistanceRatio ??
defaultAlgorithmSettings.maxDetourDistanceRatio; defaultAlgorithmSettings.MAX_DETOUR_DISTANCE_RATIO;
this.maxDetourDurationRatio = this.maxDetourDurationRatio =
algorithmSettingsRequest.maxDetourDurationRatio ?? algorithmSettingsRequest.maxDetourDurationRatio ??
defaultAlgorithmSettings.maxDetourDurationRatio; defaultAlgorithmSettings.MAX_DETOUR_DURATION_RATIO;
this.georouter = georouterCreator.create( this.georouter = georouterCreator.create(
defaultAlgorithmSettings.georouterType, defaultAlgorithmSettings.GEOROUTER_TYPE,
defaultAlgorithmSettings.georouterUrl, defaultAlgorithmSettings.GEOROUTER_URL,
); );
if (this.strict) { if (this.strict) {
this.restrict = frequency; this.restrict = frequency;

View File

@ -5,7 +5,7 @@ import {
import { IRequestGeography } from '../../interfaces/geography-request.interface'; import { IRequestGeography } from '../../interfaces/geography-request.interface';
import { PointType } from '../../../../geography/domain/types/point-type.enum'; import { PointType } from '../../../../geography/domain/types/point-type.enum';
import { Point } from '../../../../geography/domain/types/point.type'; import { Point } from '../../../../geography/domain/types/point.type';
import { Route } from './route'; import { MatcherRoute } from './matcher-route';
import { Role } from '../../types/role.enum'; import { Role } from '../../types/role.enum';
import { IGeorouter } from '../../interfaces/georouter.interface'; import { IGeorouter } from '../../interfaces/georouter.interface';
import { Waypoint } from './waypoint'; import { Waypoint } from './waypoint';
@ -23,8 +23,8 @@ export class Geography {
originType: PointType; originType: PointType;
destinationType: PointType; destinationType: PointType;
timezones: string[]; timezones: string[];
driverRoute: Route; driverRoute: MatcherRoute;
passengerRoute: Route; passengerRoute: MatcherRoute;
timezoneFinder: IFindTimezone; timezoneFinder: IFindTimezone;
constructor( constructor(

View File

@ -0,0 +1,16 @@
import { Route } from '../../../../geography/domain/entities/route';
import { IGeodesic } from '../../../../geography/domain/interfaces/geodesic.interface';
import { Waypoint } from './waypoint';
export class MatcherRoute extends Route {
waypoints: Waypoint[];
constructor(geodesic: IGeodesic) {
super(geodesic);
}
setWaypoints = (waypoints: Waypoint[]): void => {
this.waypoints = waypoints;
this.setAzimuth(waypoints.map((waypoint) => waypoint.point));
};
}

View File

@ -1,6 +1,6 @@
import { Route } from './route'; import { MatcherRoute } from './matcher-route';
export type NamedRoute = { export type NamedRoute = {
key: string; key: string;
route: Route; route: MatcherRoute;
}; };

View File

@ -1,15 +1,15 @@
import { AlgorithmType } from './algorithm.enum'; import { AlgorithmType } from './algorithm.enum';
export type DefaultAlgorithmSettings = { export type DefaultAlgorithmSettings = {
algorithm: AlgorithmType; ALGORITHM: AlgorithmType;
strict: boolean; STRICT: boolean;
remoteness: number; REMOTENESS: number;
useProportion: boolean; USE_PROPORTION: boolean;
proportion: number; PROPORTION: number;
useAzimuth: boolean; USE_AZIMUTH: boolean;
azimuthMargin: number; AZIMUTH_MARGIN: number;
maxDetourDistanceRatio: number; MAX_DETOUR_DISTANCE_RATIO: number;
maxDetourDurationRatio: number; MAX_DETOUR_DURATION_RATIO: number;
georouterType: string; GEOROUTER_TYPE: string;
georouterUrl: string; GEOROUTER_URL: string;
}; };

View File

@ -4,28 +4,28 @@ import { QueryHandler } from '@nestjs/cqrs';
import { Messager } from '../../adapters/secondaries/messager'; import { Messager } from '../../adapters/secondaries/messager';
import { MatchQuery } from '../../queries/match.query'; import { MatchQuery } from '../../queries/match.query';
import { Match } from '../entities/ecosystem/match'; import { Match } from '../entities/ecosystem/match';
import { ICollection } from '../../../database/src/interfaces/collection.interface'; import { ICollection } from '../../../database/interfaces/collection.interface';
import { Matcher } from '../entities/engine/matcher'; import { Matcher } from '../entities/engine/matcher';
@QueryHandler(MatchQuery) @QueryHandler(MatchQuery)
export class MatchUseCase { export class MatchUseCase {
constructor( constructor(
private readonly _matcher: Matcher, private readonly matcher: Matcher,
private readonly _messager: Messager, private readonly messager: Messager,
@InjectMapper() private readonly _mapper: Mapper, @InjectMapper() private readonly mapper: Mapper,
) {} ) {}
execute = async (matchQuery: MatchQuery): Promise<ICollection<Match>> => { execute = async (matchQuery: MatchQuery): Promise<ICollection<Match>> => {
try { try {
const data: Match[] = await this._matcher.match(matchQuery); const data: Match[] = await this.matcher.match(matchQuery);
this._messager.publish('matcher.match', 'match !'); this.messager.publish('matcher.match', 'match !');
return { return {
data, data,
total: data.length, total: data.length,
}; };
} catch (error) { } catch (error) {
const err: Error = error; const err: Error = error;
this._messager.publish( this.messager.publish(
'logging.matcher.match.crit', 'logging.matcher.match.crit',
JSON.stringify({ JSON.stringify({
matchQuery, matchQuery,

View File

@ -19,6 +19,7 @@ import { AlgorithmFactoryCreator } from './domain/entities/engine/factory/algori
import { TimezoneFinder } from './adapters/secondaries/timezone-finder'; import { TimezoneFinder } from './adapters/secondaries/timezone-finder';
import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezone-finder'; import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezone-finder';
import { GeographyModule } from '../geography/geography.module'; import { GeographyModule } from '../geography/geography.module';
import { TimeConverter } from './adapters/secondaries/time-converter';
@Module({ @Module({
imports: [ imports: [
@ -62,6 +63,7 @@ import { GeographyModule } from '../geography/geography.module';
GeorouterCreator, GeorouterCreator,
MatcherGeodesic, MatcherGeodesic,
TimezoneFinder, TimezoneFinder,
TimeConverter,
Matcher, Matcher,
AlgorithmFactoryCreator, AlgorithmFactoryCreator,
GeoTimezoneFinder, GeoTimezoneFinder,

View File

@ -5,7 +5,7 @@ import {
} from '../../../../domain/entities/ecosystem/geography'; } from '../../../../domain/entities/ecosystem/geography';
import { Role } from '../../../../domain/types/role.enum'; import { Role } from '../../../../domain/types/role.enum';
import { NamedRoute } from '../../../../domain/entities/ecosystem/named-route'; import { NamedRoute } from '../../../../domain/entities/ecosystem/named-route';
import { Route } from '../../../../domain/entities/ecosystem/route'; import { MatcherRoute } from '../../../../domain/entities/ecosystem/matcher-route';
import { IGeodesic } from '../../../../../geography/domain/interfaces/geodesic.interface'; import { IGeodesic } from '../../../../../geography/domain/interfaces/geodesic.interface';
import { PointType } from '../../../../../geography/domain/types/point-type.enum'; import { PointType } from '../../../../../geography/domain/types/point-type.enum';
@ -31,7 +31,7 @@ const mockGeorouter = {
return [ return [
<NamedRoute>{ <NamedRoute>{
key: RouteKey.COMMON, key: RouteKey.COMMON,
route: new Route(mockGeodesic), route: new MatcherRoute(mockGeodesic),
}, },
]; ];
}) })
@ -39,11 +39,11 @@ const mockGeorouter = {
return [ return [
<NamedRoute>{ <NamedRoute>{
key: RouteKey.DRIVER, key: RouteKey.DRIVER,
route: new Route(mockGeodesic), route: new MatcherRoute(mockGeodesic),
}, },
<NamedRoute>{ <NamedRoute>{
key: RouteKey.PASSENGER, key: RouteKey.PASSENGER,
route: new Route(mockGeodesic), route: new MatcherRoute(mockGeodesic),
}, },
]; ];
}) })
@ -51,7 +51,7 @@ const mockGeorouter = {
return [ return [
<NamedRoute>{ <NamedRoute>{
key: RouteKey.DRIVER, key: RouteKey.DRIVER,
route: new Route(mockGeodesic), route: new MatcherRoute(mockGeodesic),
}, },
]; ];
}) })
@ -59,7 +59,7 @@ const mockGeorouter = {
return [ return [
<NamedRoute>{ <NamedRoute>{
key: RouteKey.PASSENGER, key: RouteKey.PASSENGER,
route: new Route(mockGeodesic), route: new MatcherRoute(mockGeodesic),
}, },
]; ];
}), }),

View File

@ -1,4 +1,4 @@
import { Route } from '../../../../domain/entities/ecosystem/route'; import { MatcherRoute } from '../../../../domain/entities/ecosystem/matcher-route';
import { SpacetimePoint } from '../../../../domain/entities/ecosystem/spacetime-point'; import { SpacetimePoint } from '../../../../domain/entities/ecosystem/spacetime-point';
import { Waypoint } from '../../../../domain/entities/ecosystem/waypoint'; import { Waypoint } from '../../../../domain/entities/ecosystem/waypoint';
@ -17,13 +17,13 @@ const mockGeodesic = {
}), }),
}; };
describe('Route entity', () => { describe('Matcher route entity', () => {
it('should be defined', () => { it('should be defined', () => {
const route = new Route(mockGeodesic); const route = new MatcherRoute(mockGeodesic);
expect(route).toBeDefined(); expect(route).toBeDefined();
}); });
it('should set waypoints and geodesic values for a route', () => { it('should set waypoints and geodesic values for a route', () => {
const route = new Route(mockGeodesic); const route = new MatcherRoute(mockGeodesic);
const waypoint1: Waypoint = new Waypoint({ const waypoint1: Waypoint = new Waypoint({
lon: 0, lon: 0,
lat: 0, lat: 0,
@ -39,7 +39,7 @@ describe('Route entity', () => {
expect(route.distanceAzimuth).toBe(50000); expect(route.distanceAzimuth).toBe(50000);
}); });
it('should set points and geodesic values for a route', () => { it('should set points and geodesic values for a route', () => {
const route = new Route(mockGeodesic); const route = new MatcherRoute(mockGeodesic);
route.setPoints([ route.setPoints([
{ {
lon: 10, lon: 10,
@ -56,7 +56,7 @@ describe('Route entity', () => {
expect(route.distanceAzimuth).toBe(60000); expect(route.distanceAzimuth).toBe(60000);
}); });
it('should set spacetimePoints for a route', () => { it('should set spacetimePoints for a route', () => {
const route = new Route(mockGeodesic); const route = new MatcherRoute(mockGeodesic);
const spacetimePoint1 = new SpacetimePoint({ lon: 0, lat: 0 }, 0, 0); const spacetimePoint1 = new SpacetimePoint({ lon: 0, lat: 0 }, 0, 0);
const spacetimePoint2 = new SpacetimePoint({ lon: 10, lat: 10 }, 500, 5000); const spacetimePoint2 = new SpacetimePoint({ lon: 10, lat: 10 }, 500, 5000);
route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]); route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]);

View File

@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -26,17 +26,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -36,17 +36,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -49,17 +49,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -13,17 +13,17 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: AlgorithmType.CLASSIC, ALGORITHM: AlgorithmType.CLASSIC,
strict: false, STRICT: false,
remoteness: 15000, REMOTENESS: 15000,
useProportion: true, USE_PROPORTION: true,
proportion: 0.3, PROPORTION: 0.3,
useAzimuth: true, USE_AZIMUTH: true,
azimuthMargin: 10, AZIMUTH_MARGIN: 10,
maxDetourDistanceRatio: 0.3, MAX_DETOUR_DISTANCE_RATIO: 0.3,
maxDetourDurationRatio: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3,
georouterType: 'graphhopper', GEOROUTER_TYPE: 'graphhopper',
georouterUrl: 'http://localhost', GEOROUTER_URL: 'http://localhost',
}, },
}; };

View File

@ -0,0 +1,19 @@
export enum ExceptionCode {
OK = 0,
CANCELLED = 1,
UNKNOWN = 2,
INVALID_ARGUMENT = 3,
DEADLINE_EXCEEDED = 4,
NOT_FOUND = 5,
ALREADY_EXISTS = 6,
PERMISSION_DENIED = 7,
RESOURCE_EXHAUSTED = 8,
FAILED_PRECONDITION = 9,
ABORTED = 10,
OUT_OF_RANGE = 11,
UNIMPLEMENTED = 12,
INTERNAL = 13,
UNAVAILABLE = 14,
DATA_LOSS = 15,
UNAUTHENTICATED = 16,
}