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

View File

@ -10,11 +10,17 @@ import { CqrsModule } from '@nestjs/cqrs';
import { Messager } from './adapters/secondaries/messager';
import { TimezoneFinder } from './adapters/secondaries/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({
imports: [
GeographyModule,
DatabaseModule,
CqrsModule,
HttpModule,
RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
@ -46,6 +52,8 @@ import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezon
TimezoneFinder,
GeoTimezoneFinder,
CreateAdUseCase,
DefaultParamsProvider,
GeorouterCreator,
],
exports: [],
})

View File

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

View File

@ -2,10 +2,11 @@ import { Injectable } from '@nestjs/common';
import { MatcherRepository } from '../../../database/domain/matcher-repository';
import { Ad } from '../../domain/entities/ad';
import { DatabaseException } from '../../../database/exceptions/database.exception';
import { Frequency } from '../../domain/types/frequency.enum';
@Injectable()
export class AdRepository extends MatcherRepository<Ad> {
protected _model = 'ad';
protected model = 'ad';
async createAd(ad: Partial<Ad>): Promise<Ad> {
try {
@ -44,7 +45,7 @@ type AdFields = {
uuid: string;
driver: string;
passenger: string;
frequency: number;
frequency: Frequency;
fromDate: string;
toDate: 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()
export class Messager extends MessageBroker {
constructor(
private readonly _amqpConnection: AmqpConnection,
private readonly amqpConnection: AmqpConnection,
configService: ConfigService,
) {
super(configService.get<string>('RMQ_EXCHANGE'));
}
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 { 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 {
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.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,
IsArray,
IsBoolean,
IsDate,
IsEnum,
IsMilitaryTime,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
import { PointType } from '../../../geography/domain/types/point-type.enum';
import { Frequency } from '../types/frequency.enum';
import { Coordinates } from '../../../geography/domain/entities/coordinates';
import { Type } from 'class-transformer';
export class CreateAdRequest {
@IsString()
@ -19,6 +21,11 @@ export class CreateAdRequest {
@AutoMap()
uuid: string;
@IsString()
@IsNotEmpty()
@AutoMap()
userUuid: string;
@IsBoolean()
@AutoMap()
driver: boolean;
@ -27,53 +34,54 @@ export class CreateAdRequest {
@AutoMap()
passenger: boolean;
@IsNotEmpty()
@IsEnum(Frequency)
@AutoMap()
frequency: Frequency;
@IsString()
@Type(() => Date)
@IsDate()
@AutoMap()
fromDate: string;
fromDate: Date;
@IsString()
@Type(() => Date)
@IsDate()
@AutoMap()
toDate: string;
toDate: Date;
@IsOptional()
@IsString()
@IsMilitaryTime()
@AutoMap()
monTime?: string | null;
monTime?: string;
@IsOptional()
@IsString()
@IsMilitaryTime()
@AutoMap()
tueTime?: string | null;
tueTime?: string;
@IsOptional()
@IsString()
@IsMilitaryTime()
@AutoMap()
wedTime?: string | null;
wedTime?: string;
@IsOptional()
@IsString()
@IsMilitaryTime()
@AutoMap()
thuTime?: string | null;
thuTime?: string;
@IsOptional()
@IsString()
@IsMilitaryTime()
@AutoMap()
friTime?: string | null;
friTime?: string;
@IsOptional()
@IsString()
@IsMilitaryTime()
@AutoMap()
satTime?: string | null;
satTime?: string;
@IsOptional()
@IsString()
@IsMilitaryTime()
@AutoMap()
sunTime?: string | null;
sunTime?: string;
@IsNumber()
@AutoMap()
@ -103,19 +111,33 @@ export class CreateAdRequest {
@AutoMap()
sunMargin: number;
@IsEnum(PointType)
@AutoMap()
originType: PointType;
@IsEnum(PointType)
@AutoMap()
destinationType: PointType;
@Type(() => Coordinates)
@IsArray()
@ArrayMinSize(2)
@AutoMap(() => [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()
@AutoMap()
seatsDriver: number;
@ -129,13 +151,9 @@ export class CreateAdRequest {
@AutoMap()
seatsUsed?: number;
@IsString()
@IsBoolean()
@AutoMap()
createdAt: string;
@IsString()
@AutoMap()
updatedAt: string;
strict: boolean;
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 { PointType } from '../../../geography/domain/types/point-type.enum';
import { Coordinates } from '../../../geography/domain/entities/coordinates';
import { Frequency } from '../types/frequency.enum';
export class Ad {
@AutoMap()
@ -13,7 +13,7 @@ export class Ad {
passenger: boolean;
@AutoMap()
frequency: number;
frequency: Frequency;
@AutoMap()
fromDate: Date;
@ -64,22 +64,16 @@ export class Ad {
sunMargin: number;
@AutoMap()
driverDuration: number;
driverDuration?: number;
@AutoMap()
driverDistance: number;
driverDistance?: number;
@AutoMap()
passengerDuration: number;
passengerDuration?: number;
@AutoMap()
passengerDistance: number;
@AutoMap()
originType: PointType;
@AutoMap()
destinationType: PointType;
passengerDistance?: number;
@AutoMap(() => [Coordinates])
waypoints: Coordinates[];
@ -102,6 +96,9 @@ export class Ad {
@AutoMap()
seatsUsed: number;
@AutoMap()
strict: boolean;
@AutoMap()
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';
export class TimeConverter {
static toUtcDatetime = (
date: string,
time: string,
timezone: string,
): Date => {
static toUtcDatetime = (date: Date, time: string, timezone: string): Date => {
try {
if (!date || !time || !timezone) throw new Error();
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'))
.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 {
PUNCTUAL = 1,
RECURRENT = 2,
PUNCTUAL = 'PUNCTUAL',
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,
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 await this.adRepository.createAd(adToCreate);
} catch (error) {

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ Array.from({ length: 10 }).forEach(() => {
@Injectable()
class FakePrismaRepository extends PrismaRepository<FakeEntity> {
protected _model = 'fake';
protected model = 'fake';
}
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 { Point } from '../../../../geography/domain/types/point.type';
import { IGeodesic } from '../interfaces/geodesic.interface';
import { Point } from '../types/point.type';
import { SpacetimePoint } from './spacetime-point';
import { Waypoint } from './waypoint';
export class Route {
distance: number;
@ -9,7 +8,6 @@ export class Route {
fwdAzimuth: number;
backAzimuth: number;
distanceAzimuth: number;
waypoints: Waypoint[];
points: Point[];
spacetimePoints: SpacetimePoint[];
private geodesic: IGeodesic;
@ -20,17 +18,11 @@ export class Route {
this.fwdAzimuth = undefined;
this.backAzimuth = undefined;
this.distanceAzimuth = undefined;
this.waypoints = [];
this.points = [];
this.spacetimePoints = [];
this.geodesic = geodesic;
}
setWaypoints = (waypoints: Waypoint[]): void => {
this.waypoints = waypoints;
this.setAzimuth(waypoints.map((waypoint) => waypoint.point));
};
setPoints = (points: Point[]): void => {
this.points = points;
this.setAzimuth(points);
@ -40,7 +32,7 @@ export class Route {
this.spacetimePoints = spacetimePoints;
};
private setAzimuth = (points: Point[]): void => {
protected setAzimuth = (points: Point[]): void => {
const inverse = this.geodesic.inverse(
points[0].lon,
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(
private readonly queryBus: QueryBus,
private readonly defaultParamsProvider: DefaultParamsProvider,
@InjectMapper() private readonly _mapper: Mapper,
@InjectMapper() private readonly mapper: Mapper,
private readonly georouterCreator: GeorouterCreator,
private readonly timezoneFinder: GeoTimezoneFinder,
private readonly timeConverter: TimeConverter,
@ -45,7 +45,7 @@ export class MatcherController {
);
return Promise.resolve({
data: matchCollection.data.map((match: Match) =>
this._mapper.map(match, Match, MatchPresenter),
this.mapper.map(match, Match, MatchPresenter),
),
total: matchCollection.total,
});

View File

@ -7,29 +7,28 @@ service MatcherService {
}
message MatchRequest {
Mode mode = 1;
string uuid = 2;
repeated Coordinates waypoints = 3;
string departure = 4;
string fromDate = 5;
Schedule schedule = 6;
bool driver = 7;
bool passenger = 8;
string toDate = 9;
int32 marginDuration = 10;
MarginDurations marginDurations = 11;
int32 seatsPassenger = 12;
int32 seatsDriver = 13;
bool strict = 14;
Algorithm algorithm = 15;
int32 remoteness = 16;
bool useProportion = 17;
int32 proportion = 18;
bool useAzimuth = 19;
int32 azimuthMargin = 20;
float maxDetourDistanceRatio = 21;
float maxDetourDurationRatio = 22;
repeated int32 exclusions = 23;
string uuid = 1;
repeated Coordinates waypoints = 2;
string departure = 3;
string fromDate = 4;
Schedule schedule = 5;
bool driver = 6;
bool passenger = 7;
string toDate = 8;
int32 marginDuration = 9;
MarginDurations marginDurations = 10;
int32 seatsPassenger = 11;
int32 seatsDriver = 12;
bool strict = 13;
Algorithm algorithm = 14;
int32 remoteness = 15;
bool useProportion = 16;
int32 proportion = 17;
bool useAzimuth = 18;
int32 azimuthMargin = 19;
float maxDetourDistanceRatio = 20;
float maxDetourDurationRatio = 21;
repeated int32 exclusions = 22;
}
message Coordinates {
@ -69,9 +68,3 @@ message Matches {
repeated Match data = 1;
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_SEATS: parseInt(this.configService.get('DEFAULT_SEATS')),
DEFAULT_ALGORITHM_SETTINGS: {
algorithm: this.configService.get('ALGORITHM'),
strict: !!parseInt(this.configService.get('STRICT_ALGORITHM')),
remoteness: parseInt(this.configService.get('REMOTENESS')),
useProportion: !!parseInt(this.configService.get('USE_PROPORTION')),
proportion: parseInt(this.configService.get('PROPORTION')),
useAzimuth: !!parseInt(this.configService.get('USE_AZIMUTH')),
azimuthMargin: parseInt(this.configService.get('AZIMUTH_MARGIN')),
maxDetourDistanceRatio: parseFloat(
ALGORITHM: this.configService.get('ALGORITHM'),
STRICT: !!parseInt(this.configService.get('STRICT_ALGORITHM')),
REMOTENESS: parseInt(this.configService.get('REMOTENESS')),
USE_PROPORTION: !!parseInt(this.configService.get('USE_PROPORTION')),
PROPORTION: parseInt(this.configService.get('PROPORTION')),
USE_AZIMUTH: !!parseInt(this.configService.get('USE_AZIMUTH')),
AZIMUTH_MARGIN: parseInt(this.configService.get('AZIMUTH_MARGIN')),
MAX_DETOUR_DISTANCE_RATIO: parseFloat(
this.configService.get('MAX_DETOUR_DISTANCE_RATIO'),
),
maxDetourDurationRatio: parseFloat(
MAX_DETOUR_DURATION_RATIO: parseFloat(
this.configService.get('MAX_DETOUR_DURATION_RATIO'),
),
georouterType: this.configService.get('GEOROUTER_TYPE'),
georouterUrl: this.configService.get('GEOROUTER_URL'),
GEOROUTER_TYPE: this.configService.get('GEOROUTER_TYPE'),
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 { IGeodesic } from '../../../geography/domain/interfaces/geodesic.interface';
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 {
MatcherException,
@ -106,8 +106,8 @@ export class GraphhopperGeorouter implements IGeorouter {
private createRoute = (
response: AxiosResponse<GraphhopperResponse>,
): Route => {
const route = new Route(this.geodesic);
): MatcherRoute => {
const route = new MatcherRoute(this.geodesic);
if (response.data.paths && response.data.paths[0]) {
const shortestPath = response.data.paths[0];
route.distance = shortestPath.distance ?? 0;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import {
} from '../../../../domain/entities/ecosystem/geography';
import { Role } from '../../../../domain/types/role.enum';
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 { PointType } from '../../../../../geography/domain/types/point-type.enum';
@ -31,7 +31,7 @@ const mockGeorouter = {
return [
<NamedRoute>{
key: RouteKey.COMMON,
route: new Route(mockGeodesic),
route: new MatcherRoute(mockGeodesic),
},
];
})
@ -39,11 +39,11 @@ const mockGeorouter = {
return [
<NamedRoute>{
key: RouteKey.DRIVER,
route: new Route(mockGeodesic),
route: new MatcherRoute(mockGeodesic),
},
<NamedRoute>{
key: RouteKey.PASSENGER,
route: new Route(mockGeodesic),
route: new MatcherRoute(mockGeodesic),
},
];
})
@ -51,7 +51,7 @@ const mockGeorouter = {
return [
<NamedRoute>{
key: RouteKey.DRIVER,
route: new Route(mockGeodesic),
route: new MatcherRoute(mockGeodesic),
},
];
})
@ -59,7 +59,7 @@ const mockGeorouter = {
return [
<NamedRoute>{
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 { Waypoint } from '../../../../domain/entities/ecosystem/waypoint';
@ -17,13 +17,13 @@ const mockGeodesic = {
}),
};
describe('Route entity', () => {
describe('Matcher route entity', () => {
it('should be defined', () => {
const route = new Route(mockGeodesic);
const route = new MatcherRoute(mockGeodesic);
expect(route).toBeDefined();
});
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({
lon: 0,
lat: 0,
@ -39,7 +39,7 @@ describe('Route entity', () => {
expect(route.distanceAzimuth).toBe(50000);
});
it('should set points and geodesic values for a route', () => {
const route = new Route(mockGeodesic);
const route = new MatcherRoute(mockGeodesic);
route.setPoints([
{
lon: 10,
@ -56,7 +56,7 @@ describe('Route entity', () => {
expect(route.distanceAzimuth).toBe(60000);
});
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 spacetimePoint2 = new SpacetimePoint({ lon: 10, lat: 10 }, 500, 5000);
route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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