Merge branch 'matcherEngine' into 'main'

Matcher engine

See merge request v3/service/matcher!2
This commit is contained in:
Sylvain Briat 2023-04-24 09:55:24 +00:00
commit 36c0f0fe11
49 changed files with 1421 additions and 303 deletions

View File

@ -13,7 +13,7 @@ DEFAULT_TIMEZONE=Europe/Paris
# default number of seats proposed as driver # default number of seats proposed as driver
DEFAULT_SEATS=3 DEFAULT_SEATS=3
# algorithm type # algorithm type
ALGORITHM=classic ALGORITHM=CLASSIC
# strict algorithm (if relevant with the algorithm type) # strict algorithm (if relevant with the algorithm type)
# if set to true, matches are made so that # if set to true, matches are made so that
# punctual ads match only with punctual ads and # punctual ads match only with punctual ads and
@ -38,7 +38,6 @@ VALIDITY_DURATION=365
MAX_DETOUR_DISTANCE_RATIO=0.3 MAX_DETOUR_DISTANCE_RATIO=0.3
MAX_DETOUR_DURATION_RATIO=0.3 MAX_DETOUR_DURATION_RATIO=0.3
# PRISMA # PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher" DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher"

View File

@ -23,6 +23,7 @@
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose", "test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose",
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/'", "test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/'",
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage", "test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
"test:cov:watch": "jest --testPathPattern 'tests/unit/' --coverage --watch",
"test:e2e": "jest --config ./test/jest-e2e.json", "test:e2e": "jest --config ./test/jest-e2e.json",
"generate": "docker exec v3-matcher-api sh -c 'npx prisma generate'", "generate": "docker exec v3-matcher-api sh -c 'npx prisma generate'",
"migrate": "docker exec v3-matcher-api sh -c 'npx prisma migrate dev'", "migrate": "docker exec v3-matcher-api sh -c 'npx prisma migrate dev'",

View File

@ -4,10 +4,10 @@ import { Geodesic, GeodesicClass } from 'geographiclib-geodesic';
@Injectable() @Injectable()
export class MatcherGeodesic implements IGeodesic { export class MatcherGeodesic implements IGeodesic {
_geod: GeodesicClass; private geod: GeodesicClass;
constructor() { constructor() {
this._geod = Geodesic.WGS84; this.geod = Geodesic.WGS84;
} }
inverse = ( inverse = (
@ -16,7 +16,7 @@ export class MatcherGeodesic implements IGeodesic {
lon2: number, lon2: number,
lat2: number, lat2: number,
): { azimuth: number; distance: number } => { ): { azimuth: number; distance: number } => {
const { azi2: azimuth, s12: distance } = this._geod.Inverse( const { azi2: azimuth, s12: distance } = this.geod.Inverse(
lat1, lat1,
lon1, lon1,
lat2, lat2,

View File

@ -4,6 +4,10 @@ import { IGeorouter } from '../../domain/interfaces/georouter.interface';
import { GraphhopperGeorouter } from './graphhopper-georouter'; import { GraphhopperGeorouter } from './graphhopper-georouter';
import { HttpService } from '@nestjs/axios'; import { HttpService } from '@nestjs/axios';
import { MatcherGeodesic } from './geodesic'; import { MatcherGeodesic } from './geodesic';
import {
MatcherException,
MatcherExceptionCode,
} from '../../exceptions/matcher.exception';
@Injectable() @Injectable()
export class GeorouterCreator implements ICreateGeorouter { export class GeorouterCreator implements ICreateGeorouter {
@ -17,7 +21,10 @@ export class GeorouterCreator implements ICreateGeorouter {
case 'graphhopper': case 'graphhopper':
return new GraphhopperGeorouter(url, this.httpService, this.geodesic); return new GraphhopperGeorouter(url, this.httpService, this.geodesic);
default: default:
throw new Error('Unknown geocoder'); throw new MatcherException(
MatcherExceptionCode.INVALID_ARGUMENT,
'Unknown geocoder',
);
} }
}; };
} }

View File

@ -9,82 +9,85 @@ import { IGeodesic } from '../../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 { Route } from '../../domain/entities/ecosystem/route';
import { SpacetimePoint } from '../../domain/entities/ecosystem/spacetime-point'; import { SpacetimePoint } from '../../domain/entities/ecosystem/spacetime-point';
import {
MatcherException,
MatcherExceptionCode,
} from '../../exceptions/matcher.exception';
@Injectable() @Injectable()
export class GraphhopperGeorouter implements IGeorouter { export class GraphhopperGeorouter implements IGeorouter {
_url: string; private url: string;
_urlArgs: Array<string>; private urlArgs: Array<string>;
_withTime: boolean; private withTime: boolean;
_withPoints: boolean; private withPoints: boolean;
_withDistance: boolean; private withDistance: boolean;
_paths: Array<Path>; private paths: Array<Path>;
_httpService: HttpService; private httpService: HttpService;
_geodesic: IGeodesic; private geodesic: IGeodesic;
constructor(url: string, httpService: HttpService, geodesic: IGeodesic) { constructor(url: string, httpService: HttpService, geodesic: IGeodesic) {
this._url = url + '/route?'; this.url = url + '/route?';
this._httpService = httpService; this.httpService = httpService;
this._geodesic = geodesic; this.geodesic = geodesic;
} }
route = async ( route = async (
paths: Array<Path>, paths: Array<Path>,
settings: GeorouterSettings, settings: GeorouterSettings,
): Promise<Array<NamedRoute>> => { ): Promise<Array<NamedRoute>> => {
this._setDefaultUrlArgs(); this.setDefaultUrlArgs();
this._setWithTime(settings.withTime); this.setWithTime(settings.withTime);
this._setWithPoints(settings.withPoints); this.setWithPoints(settings.withPoints);
this._setWithDistance(settings.withDistance); this.setWithDistance(settings.withDistance);
this._paths = paths; this.paths = paths;
return await this._getRoutes(); return await this.getRoutes();
}; };
_setDefaultUrlArgs = (): void => { private setDefaultUrlArgs = (): void => {
this._urlArgs = [ this.urlArgs = ['vehicle=car', 'weighting=fastest', 'points_encoded=false'];
'vehicle=car',
'weighting=fastest',
'points_encoded=false',
];
}; };
_setWithTime = (withTime: boolean): void => { private setWithTime = (withTime: boolean): void => {
this._withTime = withTime; this.withTime = withTime;
if (withTime) { if (withTime) {
this._urlArgs.push('details=time'); this.urlArgs.push('details=time');
} }
}; };
_setWithPoints = (withPoints: boolean): void => { private setWithPoints = (withPoints: boolean): void => {
this._withPoints = withPoints; this.withPoints = withPoints;
if (!withPoints) { if (!withPoints) {
this._urlArgs.push('calc_points=false'); this.urlArgs.push('calc_points=false');
} }
}; };
_setWithDistance = (withDistance: boolean): void => { private setWithDistance = (withDistance: boolean): void => {
this._withDistance = withDistance; this.withDistance = withDistance;
if (withDistance) { if (withDistance) {
this._urlArgs.push('instructions=true'); this.urlArgs.push('instructions=true');
} else { } else {
this._urlArgs.push('instructions=false'); this.urlArgs.push('instructions=false');
} }
}; };
_getRoutes = async (): Promise<Array<NamedRoute>> => { private getRoutes = async (): Promise<Array<NamedRoute>> => {
const routes = Promise.all( const routes = Promise.all(
this._paths.map(async (path) => { this.paths.map(async (path) => {
const url: string = [ const url: string = [
this._getUrl(), this.getUrl(),
'&point=', '&point=',
path.points path.points
.map((point) => [point.lat, point.lon].join()) .map((point) => [point.lat, point.lon].join())
.join('&point='), .join('&point='),
].join(''); ].join('');
const route = await lastValueFrom( const route = await lastValueFrom(
this._httpService.get(url).pipe( this.httpService.get(url).pipe(
map((res) => (res.data ? this._createRoute(res) : undefined)), map((res) => (res.data ? this.createRoute(res) : undefined)),
catchError((error: AxiosError) => { catchError((error: AxiosError) => {
throw new Error('Georouter unavailable : ' + error.message); throw new MatcherException(
MatcherExceptionCode.INTERNAL,
'Georouter unavailable : ' + error.message,
);
}), }),
), ),
); );
@ -97,12 +100,14 @@ export class GraphhopperGeorouter implements IGeorouter {
return routes; return routes;
}; };
_getUrl = (): string => { private getUrl = (): string => {
return [this._url, this._urlArgs.join('&')].join(''); return [this.url, this.urlArgs.join('&')].join('');
}; };
_createRoute = (response: AxiosResponse<GraphhopperResponse>): Route => { private createRoute = (
const route = new Route(this._geodesic); response: AxiosResponse<GraphhopperResponse>,
): Route => {
const route = new Route(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;
@ -124,7 +129,7 @@ export class GraphhopperGeorouter implements IGeorouter {
if (shortestPath.instructions) if (shortestPath.instructions)
instructions = shortestPath.instructions; instructions = shortestPath.instructions;
route.setSpacetimePoints( route.setSpacetimePoints(
this._generateSpacetimePoints( this.generateSpacetimePoints(
shortestPath.points.coordinates, shortestPath.points.coordinates,
shortestPath.snapped_waypoints.coordinates, shortestPath.snapped_waypoints.coordinates,
shortestPath.details.time, shortestPath.details.time,
@ -137,15 +142,15 @@ export class GraphhopperGeorouter implements IGeorouter {
return route; return route;
}; };
_generateSpacetimePoints = ( private generateSpacetimePoints = (
points: Array<Array<number>>, points: Array<Array<number>>,
snappedWaypoints: Array<Array<number>>, snappedWaypoints: Array<Array<number>>,
durations: Array<Array<number>>, durations: Array<Array<number>>,
instructions: Array<GraphhopperInstruction>, instructions: Array<GraphhopperInstruction>,
): Array<SpacetimePoint> => { ): Array<SpacetimePoint> => {
const indices = this._getIndices(points, snappedWaypoints); const indices = this.getIndices(points, snappedWaypoints);
const times = this._getTimes(durations, indices); const times = this.getTimes(durations, indices);
const distances = this._getDistances(instructions, indices); const distances = this.getDistances(instructions, indices);
return indices.map( return indices.map(
(index) => (index) =>
new SpacetimePoint( new SpacetimePoint(
@ -156,7 +161,7 @@ export class GraphhopperGeorouter implements IGeorouter {
); );
}; };
_getIndices = ( private getIndices = (
points: Array<Array<number>>, points: Array<Array<number>>,
snappedWaypoints: Array<Array<number>>, snappedWaypoints: Array<Array<number>>,
): Array<number> => { ): Array<number> => {
@ -188,7 +193,7 @@ export class GraphhopperGeorouter implements IGeorouter {
.filter((element) => element.index == -1); .filter((element) => element.index == -1);
for (const index in points) { for (const index in points) {
for (const missedWaypoint of missedWaypoints) { for (const missedWaypoint of missedWaypoints) {
const inverse = this._geodesic.inverse( const inverse = this.geodesic.inverse(
missedWaypoint.waypoint[0], missedWaypoint.waypoint[0],
missedWaypoint.waypoint[1], missedWaypoint.waypoint[1],
points[index][0], points[index][0],
@ -206,7 +211,7 @@ export class GraphhopperGeorouter implements IGeorouter {
return indices; return indices;
}; };
_getTimes = ( private getTimes = (
durations: Array<Array<number>>, durations: Array<Array<number>>,
indices: Array<number>, indices: Array<number>,
): Array<{ index: number; duration: number }> => { ): Array<{ index: number; duration: number }> => {
@ -256,7 +261,7 @@ export class GraphhopperGeorouter implements IGeorouter {
return times; return times;
}; };
_getDistances = ( private getDistances = (
instructions: Array<GraphhopperInstruction>, instructions: Array<GraphhopperInstruction>,
indices: Array<number>, indices: Array<number>,
): Array<{ index: number; distance: number }> => { ): Array<{ index: number; distance: number }> => {

View File

@ -13,7 +13,7 @@ import { AutoMap } from '@automapper/classes';
import { Point } from '../types/point.type'; import { Point } from '../types/point.type';
import { Schedule } from '../types/schedule.type'; import { Schedule } from '../types/schedule.type';
import { MarginDurations } from '../types/margin-durations.type'; import { MarginDurations } from '../types/margin-durations.type';
import { Algorithm } from '../types/algorithm.enum'; import { AlgorithmType } from '../types/algorithm.enum';
import { IRequestTime } from '../interfaces/time-request.interface'; import { IRequestTime } from '../interfaces/time-request.interface';
import { IRequestPerson } from '../interfaces/person-request.interface'; import { IRequestPerson } from '../interfaces/person-request.interface';
import { IRequestGeography } from '../interfaces/geography-request.interface'; import { IRequestGeography } from '../interfaces/geography-request.interface';
@ -89,9 +89,9 @@ export class MatchRequest
strict: boolean; strict: boolean;
@IsOptional() @IsOptional()
@IsEnum(Algorithm) @IsEnum(AlgorithmType)
@AutoMap() @AutoMap()
algorithm: Algorithm; algorithm: AlgorithmType;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()

View File

@ -1,14 +1,14 @@
import { IRequestAlgorithmSettings } from '../../interfaces/algorithm-settings-request.interface'; import { IRequestAlgorithmSettings } from '../../interfaces/algorithm-settings-request.interface';
import { DefaultAlgorithmSettings } from '../../types/default-algorithm-settings.type'; import { DefaultAlgorithmSettings } from '../../types/default-algorithm-settings.type';
import { Algorithm } from '../../types/algorithm.enum'; import { AlgorithmType } from '../../types/algorithm.enum';
import { TimingFrequency } from '../../types/timing'; import { TimingFrequency } from '../../types/timing';
import { ICreateGeorouter } from '../../interfaces/georouter-creator.interface'; import { ICreateGeorouter } from '../../interfaces/georouter-creator.interface';
import { IGeorouter } from '../../interfaces/georouter.interface'; import { IGeorouter } from '../../interfaces/georouter.interface';
export class AlgorithmSettings { export class AlgorithmSettings {
_algorithmSettingsRequest: IRequestAlgorithmSettings; private algorithmSettingsRequest: IRequestAlgorithmSettings;
_strict: boolean; private strict: boolean;
algorithm: Algorithm; algorithmType: AlgorithmType;
restrict: TimingFrequency; restrict: TimingFrequency;
remoteness: number; remoteness: number;
useProportion: boolean; useProportion: boolean;
@ -25,10 +25,10 @@ export class AlgorithmSettings {
frequency: TimingFrequency, frequency: TimingFrequency,
georouterCreator: ICreateGeorouter, georouterCreator: ICreateGeorouter,
) { ) {
this._algorithmSettingsRequest = algorithmSettingsRequest; this.algorithmSettingsRequest = algorithmSettingsRequest;
this.algorithm = 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)
@ -55,7 +55,7 @@ export class AlgorithmSettings {
defaultAlgorithmSettings.georouterType, defaultAlgorithmSettings.georouterType,
defaultAlgorithmSettings.georouterUrl, defaultAlgorithmSettings.georouterUrl,
); );
if (this._strict) { if (this.strict) {
this.restrict = frequency; this.restrict = frequency;
} }
} }

View File

@ -1,4 +1,7 @@
import { MatcherException } from '../../../exceptions/matcher.exception'; import {
MatcherException,
MatcherExceptionCode,
} from '../../../exceptions/matcher.exception';
import { IRequestGeography } from '../../interfaces/geography-request.interface'; import { IRequestGeography } from '../../interfaces/geography-request.interface';
import { PointType } from '../../types/geography.enum'; import { PointType } from '../../types/geography.enum';
import { Point } from '../../types/point.type'; import { Point } from '../../types/point.type';
@ -13,9 +16,9 @@ import { Step } from '../../types/step.enum';
import { Path } from '../../types/path.type'; import { Path } from '../../types/path.type';
export class Geography { export class Geography {
_geographyRequest: IRequestGeography; private geographyRequest: IRequestGeography;
_person: Person; private person: Person;
_points: Array<Point>; private points: Array<Point>;
originType: PointType; originType: PointType;
destinationType: PointType; destinationType: PointType;
timezones: Array<string>; timezones: Array<string>;
@ -27,18 +30,18 @@ export class Geography {
defaultTimezone: string, defaultTimezone: string,
person: Person, person: Person,
) { ) {
this._geographyRequest = geographyRequest; this.geographyRequest = geographyRequest;
this._person = person; this.person = person;
this._points = []; this.points = [];
this.originType = undefined; this.originType = undefined;
this.destinationType = undefined; this.destinationType = undefined;
this.timezones = [defaultTimezone]; this.timezones = [defaultTimezone];
} }
init = (): void => { init = (): void => {
this._validateWaypoints(); this.validateWaypoints();
this._setTimezones(); this.setTimezones();
this._setPointTypes(); this.setPointTypes();
}; };
createRoutes = async ( createRoutes = async (
@ -49,14 +52,14 @@ export class Geography {
let passengerWaypoints: Array<Waypoint> = []; let passengerWaypoints: Array<Waypoint> = [];
const paths: Array<Path> = []; const paths: Array<Path> = [];
if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) { if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
if (this._points.length == 2) { if (this.points.length == 2) {
// 2 points => same route for driver and passenger // 2 points => same route for driver and passenger
const commonPath: Path = { const commonPath: Path = {
key: RouteKey.COMMON, key: RouteKey.COMMON,
points: this._points, points: this.points,
}; };
driverWaypoints = this._createWaypoints(commonPath.points, Role.DRIVER); driverWaypoints = this.createWaypoints(commonPath.points, Role.DRIVER);
passengerWaypoints = this._createWaypoints( passengerWaypoints = this.createWaypoints(
commonPath.points, commonPath.points,
Role.PASSENGER, Role.PASSENGER,
); );
@ -64,14 +67,14 @@ export class Geography {
} else { } else {
const driverPath: Path = { const driverPath: Path = {
key: RouteKey.DRIVER, key: RouteKey.DRIVER,
points: this._points, points: this.points,
}; };
driverWaypoints = this._createWaypoints(driverPath.points, Role.DRIVER); driverWaypoints = this.createWaypoints(driverPath.points, Role.DRIVER);
const passengerPath: Path = { const passengerPath: Path = {
key: RouteKey.PASSENGER, key: RouteKey.PASSENGER,
points: [this._points[0], this._points[this._points.length - 1]], points: [this.points[0], this.points[this.points.length - 1]],
}; };
passengerWaypoints = this._createWaypoints( passengerWaypoints = this.createWaypoints(
passengerPath.points, passengerPath.points,
Role.PASSENGER, Role.PASSENGER,
); );
@ -80,16 +83,16 @@ export class Geography {
} else if (roles.includes(Role.DRIVER)) { } else if (roles.includes(Role.DRIVER)) {
const driverPath: Path = { const driverPath: Path = {
key: RouteKey.DRIVER, key: RouteKey.DRIVER,
points: this._points, points: this.points,
}; };
driverWaypoints = this._createWaypoints(driverPath.points, Role.DRIVER); driverWaypoints = this.createWaypoints(driverPath.points, Role.DRIVER);
paths.push(driverPath); paths.push(driverPath);
} else if (roles.includes(Role.PASSENGER)) { } else if (roles.includes(Role.PASSENGER)) {
const passengerPath: Path = { const passengerPath: Path = {
key: RouteKey.PASSENGER, key: RouteKey.PASSENGER,
points: [this._points[0], this._points[this._points.length - 1]], points: [this.points[0], this.points[this.points.length - 1]],
}; };
passengerWaypoints = this._createWaypoints( passengerWaypoints = this.createWaypoints(
passengerPath.points, passengerPath.points,
Role.PASSENGER, Role.PASSENGER,
); );
@ -125,55 +128,61 @@ export class Geography {
} }
}; };
_validateWaypoints = (): void => { private validateWaypoints = (): void => {
if (this._geographyRequest.waypoints.length < 2) { if (this.geographyRequest.waypoints.length < 2) {
throw new MatcherException(3, 'At least 2 waypoints are required'); throw new MatcherException(
MatcherExceptionCode.INVALID_ARGUMENT,
'At least 2 waypoints are required',
);
} }
this._geographyRequest.waypoints.map((point) => { this.geographyRequest.waypoints.map((point) => {
if (!this._isValidPoint(point)) { if (!this.isValidPoint(point)) {
throw new MatcherException( throw new MatcherException(
3, MatcherExceptionCode.INVALID_ARGUMENT,
`Waypoint { Lon: ${point.lon}, Lat: ${point.lat} } is not valid`, `Waypoint { Lon: ${point.lon}, Lat: ${point.lat} } is not valid`,
); );
} }
this._points.push(point); this.points.push(point);
}); });
}; };
_setTimezones = (): void => { private setTimezones = (): void => {
this.timezones = find( this.timezones = find(
this._geographyRequest.waypoints[0].lat, this.geographyRequest.waypoints[0].lat,
this._geographyRequest.waypoints[0].lon, this.geographyRequest.waypoints[0].lon,
); );
}; };
_setPointTypes = (): void => { private setPointTypes = (): void => {
this.originType = this.originType =
this._geographyRequest.waypoints[0].type ?? PointType.OTHER; this.geographyRequest.waypoints[0].type ?? PointType.OTHER;
this.destinationType = this.destinationType =
this._geographyRequest.waypoints[ this.geographyRequest.waypoints[
this._geographyRequest.waypoints.length - 1 this.geographyRequest.waypoints.length - 1
].type ?? PointType.OTHER; ].type ?? PointType.OTHER;
}; };
_isValidPoint = (point: Point): boolean => private isValidPoint = (point: Point): boolean =>
this._isValidLongitude(point.lon) && this._isValidLatitude(point.lat); this.isValidLongitude(point.lon) && this.isValidLatitude(point.lat);
_isValidLongitude = (longitude: number): boolean => private isValidLongitude = (longitude: number): boolean =>
longitude >= -180 && longitude <= 180; longitude >= -180 && longitude <= 180;
_isValidLatitude = (latitude: number): boolean => private isValidLatitude = (latitude: number): boolean =>
latitude >= -90 && latitude <= 90; latitude >= -90 && latitude <= 90;
_createWaypoints = (points: Array<Point>, role: Role): Array<Waypoint> => { private createWaypoints = (
points: Array<Point>,
role: Role,
): Array<Waypoint> => {
return points.map((point, index) => { return points.map((point, index) => {
const waypoint = new Waypoint(point); const waypoint = new Waypoint(point);
if (index == 0) { if (index == 0) {
waypoint.addActor(new Actor(this._person, role, Step.START)); waypoint.addActor(new Actor(this.person, role, Step.START));
} else if (index == points.length - 1) { } else if (index == points.length - 1) {
waypoint.addActor(new Actor(this._person, role, Step.FINISH)); waypoint.addActor(new Actor(this.person, role, Step.FINISH));
} else { } else {
waypoint.addActor(new Actor(this._person, role, Step.INTERMEDIATE)); waypoint.addActor(new Actor(this.person, role, Step.INTERMEDIATE));
} }
return waypoint; return waypoint;
}); });

View File

@ -1,9 +1,9 @@
import { IRequestPerson } from '../../interfaces/person-request.interface'; import { IRequestPerson } from '../../interfaces/person-request.interface';
export class Person { export class Person {
_personRequest: IRequestPerson; private personRequest: IRequestPerson;
_defaultIdentifier: number; private defaultIdentifier: number;
_defaultMarginDuration: number; private defaultMarginDuration: number;
identifier: number; identifier: number;
marginDurations: Array<number>; marginDurations: Array<number>;
@ -12,23 +12,21 @@ export class Person {
defaultIdentifier: number, defaultIdentifier: number,
defaultMarginDuration: number, defaultMarginDuration: number,
) { ) {
this._personRequest = personRequest; this.personRequest = personRequest;
this._defaultIdentifier = defaultIdentifier; this.defaultIdentifier = defaultIdentifier;
this._defaultMarginDuration = defaultMarginDuration; this.defaultMarginDuration = defaultMarginDuration;
} }
init = (): void => { init = (): void => {
this.setIdentifier( this.setIdentifier(this.personRequest.identifier ?? this.defaultIdentifier);
this._personRequest.identifier ?? this._defaultIdentifier,
);
this.setMarginDurations([ this.setMarginDurations([
this._defaultMarginDuration, this.defaultMarginDuration,
this._defaultMarginDuration, this.defaultMarginDuration,
this._defaultMarginDuration, this.defaultMarginDuration,
this._defaultMarginDuration, this.defaultMarginDuration,
this._defaultMarginDuration, this.defaultMarginDuration,
this._defaultMarginDuration, this.defaultMarginDuration,
this._defaultMarginDuration, this.defaultMarginDuration,
]); ]);
}; };

View File

@ -1,12 +1,12 @@
import { IRequestRequirement } from '../../interfaces/requirement-request.interface'; import { IRequestRequirement } from '../../interfaces/requirement-request.interface';
export class Requirement { export class Requirement {
_requirementRequest: IRequestRequirement; private requirementRequest: IRequestRequirement;
seatsDriver: number; seatsDriver: number;
seatsPassenger: number; seatsPassenger: number;
constructor(requirementRequest: IRequestRequirement, defaultSeats: number) { constructor(requirementRequest: IRequestRequirement, defaultSeats: number) {
this._requirementRequest = requirementRequest; this.requirementRequest = requirementRequest;
this.seatsDriver = requirementRequest.seatsDriver ?? defaultSeats; this.seatsDriver = requirementRequest.seatsDriver ?? defaultSeats;
this.seatsPassenger = requirementRequest.seatsPassenger ?? 1; this.seatsPassenger = requirementRequest.seatsPassenger ?? 1;
} }

View File

@ -12,7 +12,7 @@ export class Route {
waypoints: Array<Waypoint>; waypoints: Array<Waypoint>;
points: Array<Point>; points: Array<Point>;
spacetimePoints: Array<SpacetimePoint>; spacetimePoints: Array<SpacetimePoint>;
_geodesic: IGeodesic; private geodesic: IGeodesic;
constructor(geodesic: IGeodesic) { constructor(geodesic: IGeodesic) {
this.distance = undefined; this.distance = undefined;
@ -23,25 +23,25 @@ export class Route {
this.waypoints = []; this.waypoints = [];
this.points = []; this.points = [];
this.spacetimePoints = []; this.spacetimePoints = [];
this._geodesic = geodesic; this.geodesic = geodesic;
} }
setWaypoints = (waypoints: Array<Waypoint>): void => { setWaypoints = (waypoints: Array<Waypoint>): void => {
this.waypoints = waypoints; this.waypoints = waypoints;
this._setAzimuth(waypoints.map((waypoint) => waypoint.point)); this.setAzimuth(waypoints.map((waypoint) => waypoint.point));
}; };
setPoints = (points: Array<Point>): void => { setPoints = (points: Array<Point>): void => {
this.points = points; this.points = points;
this._setAzimuth(points); this.setAzimuth(points);
}; };
setSpacetimePoints = (spacetimePoints: Array<SpacetimePoint>): void => { setSpacetimePoints = (spacetimePoints: Array<SpacetimePoint>): void => {
this.spacetimePoints = spacetimePoints; this.spacetimePoints = spacetimePoints;
}; };
_setAzimuth = (points: Array<Point>): void => { private setAzimuth = (points: Array<Point>): void => {
const inverse = this._geodesic.inverse( const inverse = this.geodesic.inverse(
points[0].lon, points[0].lon,
points[0].lat, points[0].lat,
points[points.length - 1].lon, points[points.length - 1].lon,

View File

@ -1,13 +1,16 @@
import { MatcherException } from '../../../exceptions/matcher.exception'; import {
MatcherException,
MatcherExceptionCode,
} from '../../../exceptions/matcher.exception';
import { MarginDurations } from '../../types/margin-durations.type'; import { MarginDurations } from '../../types/margin-durations.type';
import { IRequestTime } from '../../interfaces/time-request.interface'; import { IRequestTime } from '../../interfaces/time-request.interface';
import { TimingDays, TimingFrequency, Days } from '../../types/timing'; import { TimingDays, TimingFrequency, Days } from '../../types/timing';
import { Schedule } from '../../types/schedule.type'; import { Schedule } from '../../types/schedule.type';
export class Time { export class Time {
_timeRequest: IRequestTime; private timeRequest: IRequestTime;
_defaultMarginDuration: number; private defaultMarginDuration: number;
_defaultValidityDuration: number; private defaultValidityDuration: number;
frequency: TimingFrequency; frequency: TimingFrequency;
fromDate: Date; fromDate: Date;
toDate: Date; toDate: Date;
@ -19,9 +22,9 @@ export class Time {
defaultMarginDuration: number, defaultMarginDuration: number,
defaultValidityDuration: number, defaultValidityDuration: number,
) { ) {
this._timeRequest = timeRequest; this.timeRequest = timeRequest;
this._defaultMarginDuration = defaultMarginDuration; this.defaultMarginDuration = defaultMarginDuration;
this._defaultValidityDuration = defaultValidityDuration; this.defaultValidityDuration = defaultValidityDuration;
this.schedule = {}; this.schedule = {};
this.marginDurations = { this.marginDurations = {
mon: defaultMarginDuration, mon: defaultMarginDuration,
@ -35,99 +38,120 @@ export class Time {
} }
init = (): void => { init = (): void => {
this._validateBaseDate(); this.validateBaseDate();
this._validatePunctualRequest(); this.validatePunctualRequest();
this._validateRecurrentRequest(); this.validateRecurrentRequest();
this._setPunctualRequest(); this.setPunctualRequest();
this._setRecurrentRequest(); this.setRecurrentRequest();
this._setMargindurations(); this.setMargindurations();
}; };
_validateBaseDate = (): void => { private validateBaseDate = (): void => {
if (!this._timeRequest.departure && !this._timeRequest.fromDate) { if (!this.timeRequest.departure && !this.timeRequest.fromDate) {
throw new MatcherException(3, 'departure or fromDate is required'); throw new MatcherException(
MatcherExceptionCode.INVALID_ARGUMENT,
'departure or fromDate is required',
);
} }
}; };
_validatePunctualRequest = (): void => { private validatePunctualRequest = (): void => {
if (this._timeRequest.departure) { if (this.timeRequest.departure) {
this.fromDate = this.toDate = new Date(this._timeRequest.departure); this.fromDate = this.toDate = new Date(this.timeRequest.departure);
if (!this._isDate(this.fromDate)) { if (!this.isDate(this.fromDate)) {
throw new MatcherException(3, 'Wrong departure date'); throw new MatcherException(
MatcherExceptionCode.INVALID_ARGUMENT,
'Wrong departure date',
);
} }
} }
}; };
_validateRecurrentRequest = (): void => { private validateRecurrentRequest = (): void => {
if (this._timeRequest.fromDate) { if (this.timeRequest.fromDate) {
this.fromDate = new Date(this._timeRequest.fromDate); this.fromDate = new Date(this.timeRequest.fromDate);
if (!this._isDate(this.fromDate)) { if (!this.isDate(this.fromDate)) {
throw new MatcherException(3, 'Wrong fromDate'); throw new MatcherException(
MatcherExceptionCode.INVALID_ARGUMENT,
'Wrong fromDate',
);
} }
} }
if (this._timeRequest.toDate) { if (this.timeRequest.toDate) {
this.toDate = new Date(this._timeRequest.toDate); this.toDate = new Date(this.timeRequest.toDate);
if (!this._isDate(this.toDate)) { if (!this.isDate(this.toDate)) {
throw new MatcherException(3, 'Wrong toDate'); throw new MatcherException(
MatcherExceptionCode.INVALID_ARGUMENT,
'Wrong toDate',
);
} }
if (this.toDate < this.fromDate) { if (this.toDate < this.fromDate) {
throw new MatcherException(3, 'toDate must be after fromDate'); throw new MatcherException(
MatcherExceptionCode.INVALID_ARGUMENT,
'toDate must be after fromDate',
);
} }
} }
if (this._timeRequest.fromDate) { if (this.timeRequest.fromDate) {
this._validateSchedule(); this.validateSchedule();
} }
}; };
_validateSchedule = (): void => { private validateSchedule = (): void => {
if (!this._timeRequest.schedule) { if (!this.timeRequest.schedule) {
throw new MatcherException(3, 'Schedule is required'); throw new MatcherException(
MatcherExceptionCode.INVALID_ARGUMENT,
'Schedule is required',
);
} }
if ( if (
!Object.keys(this._timeRequest.schedule).some((elem) => !Object.keys(this.timeRequest.schedule).some((elem) =>
Days.includes(elem), Days.includes(elem),
) )
) { ) {
throw new MatcherException(3, 'No valid day in the given schedule'); throw new MatcherException(
MatcherExceptionCode.INVALID_ARGUMENT,
'No valid day in the given schedule',
);
} }
Object.keys(this._timeRequest.schedule).map((day) => { Object.keys(this.timeRequest.schedule).map((day) => {
const time = new Date('1970-01-01 ' + this._timeRequest.schedule[day]); const time = new Date('1970-01-01 ' + this.timeRequest.schedule[day]);
if (!this._isDate(time)) { if (!this.isDate(time)) {
throw new MatcherException(3, `Wrong time for ${day} in schedule`); throw new MatcherException(
MatcherExceptionCode.INVALID_ARGUMENT,
`Wrong time for ${day} in schedule`,
);
} }
}); });
}; };
_setPunctualRequest = (): void => { private setPunctualRequest = (): void => {
if (this._timeRequest.departure) { if (this.timeRequest.departure) {
this.frequency = TimingFrequency.FREQUENCY_PUNCTUAL; this.frequency = TimingFrequency.FREQUENCY_PUNCTUAL;
this.schedule[TimingDays[this.fromDate.getDay()]] = this.schedule[TimingDays[this.fromDate.getDay()]] =
this.fromDate.getHours() + ':' + this.fromDate.getMinutes(); this.fromDate.getHours() + ':' + this.fromDate.getMinutes();
} }
}; };
_setRecurrentRequest = (): void => { private setRecurrentRequest = (): void => {
if (this._timeRequest.fromDate) { if (this.timeRequest.fromDate) {
this.frequency = TimingFrequency.FREQUENCY_RECURRENT; this.frequency = TimingFrequency.FREQUENCY_RECURRENT;
if (!this.toDate) { if (!this.toDate) {
this.toDate = this._addDays( this.toDate = this.addDays(this.fromDate, this.defaultValidityDuration);
this.fromDate,
this._defaultValidityDuration,
);
} }
this._setSchedule(); this.setSchedule();
} }
}; };
_setSchedule = (): void => { private setSchedule = (): void => {
Object.keys(this._timeRequest.schedule).map((day) => { Object.keys(this.timeRequest.schedule).map((day) => {
this.schedule[day] = this._timeRequest.schedule[day]; this.schedule[day] = this.timeRequest.schedule[day];
}); });
}; };
_setMargindurations = (): void => { private setMargindurations = (): void => {
if (this._timeRequest.marginDuration) { if (this.timeRequest.marginDuration) {
const duration = Math.abs(this._timeRequest.marginDuration); const duration = Math.abs(this.timeRequest.marginDuration);
this.marginDurations = { this.marginDurations = {
mon: duration, mon: duration,
tue: duration, tue: duration,
@ -138,30 +162,30 @@ export class Time {
sun: duration, sun: duration,
}; };
} }
if (this._timeRequest.marginDurations) { if (this.timeRequest.marginDurations) {
if ( if (
!Object.keys(this._timeRequest.marginDurations).some((elem) => !Object.keys(this.timeRequest.marginDurations).some((elem) =>
Days.includes(elem), Days.includes(elem),
) )
) { ) {
throw new MatcherException( throw new MatcherException(
3, MatcherExceptionCode.INVALID_ARGUMENT,
'No valid day in the given margin durations', 'No valid day in the given margin durations',
); );
} }
Object.keys(this._timeRequest.marginDurations).map((day) => { Object.keys(this.timeRequest.marginDurations).map((day) => {
this.marginDurations[day] = Math.abs( this.marginDurations[day] = Math.abs(
this._timeRequest.marginDurations[day], this.timeRequest.marginDurations[day],
); );
}); });
} }
}; };
_isDate = (date: Date): boolean => { private isDate = (date: Date): boolean => {
return date instanceof Date && isFinite(+date); return date instanceof Date && isFinite(+date);
}; };
_addDays = (date: Date, days: number): Date => { private addDays = (date: Date, days: number): Date => {
const result = new Date(date); const result = new Date(date);
result.setDate(result.getDate() + days); result.setDate(result.getDate() + days);
return result; return result;

View File

@ -0,0 +1,18 @@
import { MatchQuery } from 'src/modules/matcher/queries/match.query';
import { AlgorithmType } from '../../../types/algorithm.enum';
import { AlgorithmFactory } from './algorithm-factory.abstract';
import { ClassicAlgorithmFactory } from './classic';
import { Injectable } from '@nestjs/common';
@Injectable()
export class AlgorithmFactoryCreator {
create = (matchQuery: MatchQuery): AlgorithmFactory => {
let algorithmFactory: AlgorithmFactory;
switch (matchQuery.algorithmSettings.algorithmType) {
case AlgorithmType.CLASSIC:
algorithmFactory = new ClassicAlgorithmFactory(matchQuery);
break;
}
return algorithmFactory;
};
}

View File

@ -1,15 +1,17 @@
import { MatchQuery } from 'src/modules/matcher/queries/match.query'; import { MatchQuery } from 'src/modules/matcher/queries/match.query';
import { Processor } from '../processor.abstract'; import { Processor } from '../processor/processor.abstract';
import { Candidate } from '../candidate'; import { Candidate } from '../candidate';
import { Selector } from '../selector/selector.abstract';
export abstract class AlgorithmFactory { export abstract class AlgorithmFactory {
_matchQuery: MatchQuery; protected matchQuery: MatchQuery;
_candidates: Array<Candidate>; private candidates: Array<Candidate>;
constructor(matchQuery: MatchQuery) { constructor(matchQuery: MatchQuery) {
this._matchQuery = matchQuery; this.matchQuery = matchQuery;
this._candidates = []; this.candidates = [];
} }
abstract createSelector(): Selector;
abstract createProcessors(): Array<Processor>; abstract createProcessors(): Array<Processor>;
} }

View File

@ -1,9 +1,21 @@
import { AlgorithmFactory } from './algorithm-factory.abstract'; import { AlgorithmFactory } from './algorithm-factory.abstract';
import { Processor } from '../processor.abstract';
import { ClassicWaypointsCompleter } from '../processor/completer/classic-waypoint.completer.processor'; import { ClassicWaypointsCompleter } from '../processor/completer/classic-waypoint.completer.processor';
import { RouteCompleter } from '../processor/completer/route.completer.processor';
import { ClassicGeoFilter } from '../processor/filter/geofilter/classic.filter.processor';
import { JourneyCompleter } from '../processor/completer/journey.completer.processor';
import { ClassicTimeFilter } from '../processor/filter/timefilter/classic.filter.processor';
import { Processor } from '../processor/processor.abstract';
import { Selector } from '../selector/selector.abstract';
import { ClassicSelector } from '../selector/classic.selector';
export class ClassicAlgorithmFactory extends AlgorithmFactory { export class ClassicAlgorithmFactory extends AlgorithmFactory {
createProcessors(): Array<Processor> { createSelector = (): Selector => new ClassicSelector(this.matchQuery);
return [new ClassicWaypointsCompleter(this._matchQuery)]; createProcessors = (): Array<Processor> => [
} new ClassicWaypointsCompleter(this.matchQuery),
new RouteCompleter(this.matchQuery, true, true, true),
new ClassicGeoFilter(this.matchQuery),
new RouteCompleter(this.matchQuery),
new JourneyCompleter(this.matchQuery),
new ClassicTimeFilter(this.matchQuery),
];
} }

View File

@ -1,21 +1,27 @@
import { Injectable } from '@nestjs/common';
import { MatchQuery } from '../../../queries/match.query'; import { MatchQuery } from '../../../queries/match.query';
import { Algorithm } from '../../types/algorithm.enum';
import { Match } from '../ecosystem/match'; import { Match } from '../ecosystem/match';
import { Candidate } from './candidate'; import { Candidate } from './candidate';
import { AlgorithmFactory } from './factory/algorithm-factory.abstract'; import { AlgorithmFactory } from './factory/algorithm-factory.abstract';
import { ClassicAlgorithmFactory } from './factory/classic'; import { AlgorithmFactoryCreator } from './factory/algorithm-factory-creator';
@Injectable()
export class Matcher { export class Matcher {
match = (matchQuery: MatchQuery): Array<Match> => { constructor(
let algorithm: AlgorithmFactory; private readonly algorithmFactoryCreator: AlgorithmFactoryCreator,
switch (matchQuery.algorithmSettings.algorithm) { ) {}
case Algorithm.CLASSIC:
algorithm = new ClassicAlgorithmFactory(matchQuery); match = async (matchQuery: MatchQuery): Promise<Array<Match>> => {
} const algorithmFactory: AlgorithmFactory =
let candidates: Array<Candidate> = []; this.algorithmFactoryCreator.create(matchQuery);
for (const processor of algorithm.createProcessors()) { let candidates: Array<Candidate> = await algorithmFactory
.createSelector()
.select();
for (const processor of algorithmFactory.createProcessors()) {
candidates = processor.execute(candidates); candidates = processor.execute(candidates);
} }
return []; const match = new Match();
match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85';
return [match];
}; };
} }

View File

@ -2,7 +2,7 @@ import { Candidate } from '../../candidate';
import { Completer } from './completer.abstract'; import { Completer } from './completer.abstract';
export class ClassicWaypointsCompleter extends Completer { export class ClassicWaypointsCompleter extends Completer {
complete(candidates: Array<Candidate>): Array<Candidate> { complete = (candidates: Array<Candidate>): Array<Candidate> => {
return []; return candidates;
} };
} }

View File

@ -1,5 +1,5 @@
import { Candidate } from '../../candidate'; import { Candidate } from '../../candidate';
import { Processor } from '../../processor.abstract'; import { Processor } from '../processor.abstract';
export abstract class Completer extends Processor { export abstract class Completer extends Processor {
execute = (candidates: Array<Candidate>): Array<Candidate> => execute = (candidates: Array<Candidate>): Array<Candidate> =>

View File

@ -0,0 +1,8 @@
import { Candidate } from '../../candidate';
import { Completer } from './completer.abstract';
export class JourneyCompleter extends Completer {
complete = (candidates: Array<Candidate>): Array<Candidate> => {
return candidates;
};
}

View File

@ -0,0 +1,25 @@
import { MatchQuery } from 'src/modules/matcher/queries/match.query';
import { Candidate } from '../../candidate';
import { Completer } from './completer.abstract';
export class RouteCompleter extends Completer {
private withPoints: boolean;
private withTime: boolean;
private withDistance: boolean;
constructor(
matchQuery: MatchQuery,
withPoints = false,
withTime = false,
withDistance = false,
) {
super(matchQuery);
this.withPoints = withPoints;
this.withTime = withTime;
this.withDistance = withDistance;
}
complete = (candidates: Array<Candidate>): Array<Candidate> => {
return candidates;
};
}

View File

@ -0,0 +1,9 @@
import { Candidate } from '../../candidate';
import { Processor } from '../processor.abstract';
export abstract class Filter extends Processor {
execute = (candidates: Array<Candidate>): Array<Candidate> =>
this.filter(candidates);
abstract filter(candidates: Array<Candidate>): Array<Candidate>;
}

View File

@ -0,0 +1,8 @@
import { Candidate } from '../../../candidate';
import { Filter } from '../filter.abstract';
export class ClassicGeoFilter extends Filter {
filter = (candidates: Array<Candidate>): Array<Candidate> => {
return candidates;
};
}

View File

@ -0,0 +1,8 @@
import { Candidate } from '../../../candidate';
import { Filter } from '../filter.abstract';
export class ClassicTimeFilter extends Filter {
filter = (candidates: Array<Candidate>): Array<Candidate> => {
return candidates;
};
}

View File

@ -1,11 +1,11 @@
import { MatchQuery } from 'src/modules/matcher/queries/match.query'; import { MatchQuery } from 'src/modules/matcher/queries/match.query';
import { Candidate } from './candidate'; import { Candidate } from '../candidate';
export abstract class Processor { export abstract class Processor {
_matchQuery: MatchQuery; private matchQuery: MatchQuery;
constructor(matchQuery: MatchQuery) { constructor(matchQuery: MatchQuery) {
this._matchQuery = matchQuery; this.matchQuery = matchQuery;
} }
abstract execute(candidates: Array<Candidate>): Array<Candidate>; abstract execute(candidates: Array<Candidate>): Array<Candidate>;

View File

@ -0,0 +1,8 @@
import { Candidate } from '../candidate';
import { Selector } from './selector.abstract';
export class ClassicSelector extends Selector {
select = async (): Promise<Array<Candidate>> => {
return [];
};
}

View File

@ -0,0 +1,12 @@
import { MatchQuery } from 'src/modules/matcher/queries/match.query';
import { Candidate } from '../candidate';
export abstract class Selector {
private matchQuery: MatchQuery;
constructor(matchQuery: MatchQuery) {
this.matchQuery = matchQuery;
}
abstract select(): Promise<Array<Candidate>>;
}

View File

@ -1,7 +1,7 @@
import { Algorithm } from '../types/algorithm.enum'; import { AlgorithmType } from '../types/algorithm.enum';
export interface IRequestAlgorithmSettings { export interface IRequestAlgorithmSettings {
algorithm: Algorithm; algorithm: AlgorithmType;
strict: boolean; strict: boolean;
remoteness: number; remoteness: number;
useProportion: boolean; useProportion: boolean;

View File

@ -1,3 +1,3 @@
export enum Algorithm { export enum AlgorithmType {
CLASSIC = 'CLASSIC', CLASSIC = 'CLASSIC',
} }

View File

@ -1,7 +1,7 @@
import { Algorithm } from './algorithm.enum'; import { AlgorithmType } from './algorithm.enum';
export type DefaultAlgorithmSettings = { export type DefaultAlgorithmSettings = {
algorithm: Algorithm; algorithm: AlgorithmType;
strict: boolean; strict: boolean;
remoteness: number; remoteness: number;
useProportion: boolean; useProportion: boolean;

View File

@ -3,64 +3,25 @@ import { InjectMapper } from '@automapper/nestjs';
import { QueryHandler } from '@nestjs/cqrs'; 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 { AdRepository } from '../../adapters/secondaries/ad.repository';
import { Match } from '../entities/ecosystem/match'; import { Match } from '../entities/ecosystem/match';
import { ICollection } from '../../../database/src/interfaces/collection.interface'; import { ICollection } from '../../../database/src/interfaces/collection.interface';
import { Matcher } from '../entities/engine/matcher';
@QueryHandler(MatchQuery) @QueryHandler(MatchQuery)
export class MatchUseCase { export class MatchUseCase {
constructor( constructor(
private readonly _repository: AdRepository, 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 paths = []; const data: Array<Match> = await this._matcher.match(matchQuery);
// for (let i = 0; i < 1; i++) {
// paths.push({
// key: 'route' + i,
// points: [
// {
// lat: 48.110899,
// lon: -1.68365,
// },
// {
// lat: 48.131105,
// lon: -1.690067,
// },
// {
// lat: 48.534769,
// lon: -1.894032,
// },
// {
// lat: 48.56516,
// lon: -1.923553,
// },
// {
// lat: 48.622813,
// lon: -1.997177,
// },
// {
// lat: 48.67846,
// lon: -1.8554,
// },
// ],
// });
// }
// const routes = await matchQuery.algorithmSettings.georouter.route(paths, {
// withDistance: false,
// withPoints: true,
// withTime: true,
// });
// routes.map((route) => console.log(route.route.spacetimePoints));
const match = new Match();
match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85';
this._messager.publish('matcher.match', 'match !'); this._messager.publish('matcher.match', 'match !');
return { return {
data: [match], data,
total: 1, total: data.length,
}; };
} catch (error) { } catch (error) {
const err: Error = error; const err: Error = error;
@ -75,3 +36,42 @@ export class MatchUseCase {
} }
}; };
} }
// const paths = [];
// for (let i = 0; i < 1; i++) {
// paths.push({
// key: 'route' + i,
// points: [
// {
// lat: 48.110899,
// lon: -1.68365,
// },
// {
// lat: 48.131105,
// lon: -1.690067,
// },
// {
// lat: 48.534769,
// lon: -1.894032,
// },
// {
// lat: 48.56516,
// lon: -1.923553,
// },
// {
// lat: 48.622813,
// lon: -1.997177,
// },
// {
// lat: 48.67846,
// lon: -1.8554,
// },
// ],
// });
// }
// const routes = await matchQuery.algorithmSettings.georouter.route(paths, {
// withDistance: false,
// withPoints: true,
// withTime: true,
// });
// routes.map((route) => console.log(route.route.spacetimePoints));

View File

@ -11,3 +11,23 @@ export class MatcherException implements Error {
return this._code; return this._code;
} }
} }
export enum MatcherExceptionCode {
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,
}

View File

@ -15,6 +15,8 @@ import { DefaultParamsProvider } from './adapters/secondaries/default-params.pro
import { GeorouterCreator } from './adapters/secondaries/georouter-creator'; import { GeorouterCreator } from './adapters/secondaries/georouter-creator';
import { HttpModule } from '@nestjs/axios'; import { HttpModule } from '@nestjs/axios';
import { MatcherGeodesic } from './adapters/secondaries/geodesic'; import { MatcherGeodesic } from './adapters/secondaries/geodesic';
import { Matcher } from './domain/entities/engine/matcher';
import { AlgorithmFactoryCreator } from './domain/entities/engine/factory/algorithm-factory-creator';
@Module({ @Module({
imports: [ imports: [
@ -57,6 +59,8 @@ import { MatcherGeodesic } from './adapters/secondaries/geodesic';
MatchUseCase, MatchUseCase,
GeorouterCreator, GeorouterCreator,
MatcherGeodesic, MatcherGeodesic,
Matcher,
AlgorithmFactoryCreator,
], ],
exports: [], exports: [],
}) })

View File

@ -107,7 +107,6 @@ describe('Geography entity', () => {
person, person,
); );
geography.init(); geography.init();
expect(geography._points.length).toBe(2);
expect(geography.originType).toBe(PointType.LOCALITY); expect(geography.originType).toBe(PointType.LOCALITY);
expect(geography.destinationType).toBe(PointType.LOCALITY); expect(geography.destinationType).toBe(PointType.LOCALITY);
}); });

View File

@ -0,0 +1,61 @@
import { AlgorithmFactoryCreator } from '../../../../domain/entities/engine/factory/algorithm-factory-creator';
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
import { ClassicAlgorithmFactory } from '../../../../domain/entities/engine/factory/classic';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
describe('AlgorithmFactoryCreator', () => {
it('should be defined', () => {
expect(new AlgorithmFactoryCreator()).toBeDefined();
});
it('should create a classic algorithm factory', () => {
expect(new AlgorithmFactoryCreator().create(matchQuery)).toBeInstanceOf(
ClassicAlgorithmFactory,
);
});
});

View File

@ -0,0 +1,78 @@
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { Candidate } from '../../../../domain/entities/engine/candidate';
import { AlgorithmFactory } from '../../../../domain/entities/engine/factory/algorithm-factory.abstract';
import { Processor } from '../../../../domain/entities/engine/processor/processor.abstract';
import { Selector } from '../../../../domain/entities/engine/selector/selector.abstract';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
class FakeSelector extends Selector {
select = (): Promise<Candidate[]> => {
return Promise.resolve([new Candidate()]);
};
}
class FakeProcessor extends Processor {
execute = (candidates: Candidate[]): Candidate[] => {
return candidates;
};
}
class FakeAlgorithmFactory extends AlgorithmFactory {
createSelector = (): Selector => {
return new FakeSelector(matchQuery);
};
createProcessors = (): Processor[] => {
return [new FakeProcessor(matchQuery)];
};
}
describe('AlgorithmFactory', () => {
it('should create an extended class', () => {
expect(new FakeAlgorithmFactory(matchQuery)).toBeDefined();
});
});

View File

@ -0,0 +1,69 @@
import { ClassicSelector } from '../../../../domain/entities/engine/selector/classic.selector';
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { ClassicAlgorithmFactory } from '../../../../domain/entities/engine/factory/classic';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
describe('ClassicAlgorithmFactory', () => {
it('should be defined', () => {
expect(new ClassicAlgorithmFactory(matchQuery)).toBeDefined();
});
it('should create a classic selector', () => {
const classicAlgorithmFactory: ClassicAlgorithmFactory =
new ClassicAlgorithmFactory(matchQuery);
expect(classicAlgorithmFactory.createSelector()).toBeInstanceOf(
ClassicSelector,
);
});
it('should create processors', () => {
const classicAlgorithmFactory: ClassicAlgorithmFactory =
new ClassicAlgorithmFactory(matchQuery);
expect(classicAlgorithmFactory.createProcessors().length).toBe(6);
});
});

View File

@ -0,0 +1,63 @@
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
import { Candidate } from '../../../../domain/entities/engine/candidate';
import { ClassicGeoFilter } from '../../../../domain/entities/engine/processor/filter/geofilter/classic.filter.processor';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
describe('ClassicGeoFilter', () => {
it('should be defined', () => {
expect(new ClassicGeoFilter(matchQuery)).toBeDefined();
});
it('should filter candidates', () => {
const candidates = [new Candidate(), new Candidate()];
const classicWaypointCompleter: ClassicGeoFilter = new ClassicGeoFilter(
matchQuery,
);
expect(classicWaypointCompleter.filter(candidates).length).toBe(2);
});
});

View File

@ -0,0 +1,63 @@
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
import { Candidate } from '../../../../domain/entities/engine/candidate';
import { ClassicTimeFilter } from '../../../../domain/entities/engine/processor/filter/timefilter/classic.filter.processor';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
describe('ClassicTimeFilter', () => {
it('should be defined', () => {
expect(new ClassicTimeFilter(matchQuery)).toBeDefined();
});
it('should filter candidates', () => {
const candidates = [new Candidate(), new Candidate()];
const classicWaypointCompleter: ClassicTimeFilter = new ClassicTimeFilter(
matchQuery,
);
expect(classicWaypointCompleter.filter(candidates).length).toBe(2);
});
});

View File

@ -0,0 +1,62 @@
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
import { ClassicWaypointsCompleter } from '../../../../domain/entities/engine/processor/completer/classic-waypoint.completer.processor';
import { Candidate } from '../../../../domain/entities/engine/candidate';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
describe('ClassicWaypointCompleter', () => {
it('should be defined', () => {
expect(new ClassicWaypointsCompleter(matchQuery)).toBeDefined();
});
it('should complete candidates', () => {
const candidates = [new Candidate(), new Candidate()];
const classicWaypointCompleter: ClassicWaypointsCompleter =
new ClassicWaypointsCompleter(matchQuery);
expect(classicWaypointCompleter.complete(candidates).length).toBe(2);
});
});

View File

@ -0,0 +1,61 @@
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
import { Candidate } from '../../../../domain/entities/engine/candidate';
import { ClassicSelector } from '../../../../domain/entities/engine/selector/classic.selector';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
describe('ClassicSelector', () => {
it('should be defined', () => {
expect(new ClassicSelector(matchQuery)).toBeDefined();
});
it('should select candidates', async () => {
const classicSelector: ClassicSelector = new ClassicSelector(matchQuery);
const candidates: Candidate[] = await classicSelector.select();
expect(candidates.length).toBe(0);
});
});

View File

@ -0,0 +1,68 @@
import { Completer } from '../../../../domain/entities/engine/processor/completer/completer.abstract';
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { Candidate } from '../../../../domain/entities/engine/candidate';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
class FakeCompleter extends Completer {
complete = (candidates: Candidate[]): Candidate[] => {
return candidates;
};
}
describe('Completer', () => {
it('should create an extended class', () => {
expect(new FakeCompleter(matchQuery)).toBeDefined();
});
it('should call complete method', () => {
const fakeCompleter: Completer = new FakeCompleter(matchQuery);
const completerSpy = jest.spyOn(fakeCompleter, 'complete');
fakeCompleter.execute([new Candidate()]);
expect(completerSpy).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,68 @@
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { Candidate } from '../../../../domain/entities/engine/candidate';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
import { Filter } from '../../../../domain/entities/engine/processor/filter/filter.abstract';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
class FakeFilter extends Filter {
filter = (candidates: Candidate[]): Candidate[] => {
return candidates;
};
}
describe('Filter', () => {
it('should create an extended class', () => {
expect(new FakeFilter(matchQuery)).toBeDefined();
});
it('should call complete method', () => {
const fakeFilter: Filter = new FakeFilter(matchQuery);
const filterSpy = jest.spyOn(fakeFilter, 'filter');
fakeFilter.execute([new Candidate()]);
expect(filterSpy).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,61 @@
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
import { Candidate } from '../../../../domain/entities/engine/candidate';
import { JourneyCompleter } from '../../../../domain/entities/engine/processor/completer/journey.completer.processor';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
describe('JourneyCompleter', () => {
it('should be defined', () => {
expect(new JourneyCompleter(matchQuery)).toBeDefined();
});
it('should complete candidates', () => {
const candidates = [new Candidate(), new Candidate()];
const journeyCompleter: JourneyCompleter = new JourneyCompleter(matchQuery);
expect(journeyCompleter.complete(candidates).length).toBe(2);
});
});

View File

@ -0,0 +1,73 @@
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { MatchQuery } from '../../../../queries/match.query';
import { Matcher } from '../../../../domain/entities/engine/matcher';
const mockAlgorithmFactoryCreator = {
create: jest.fn().mockReturnValue({
createSelector: jest.fn().mockReturnValue({
select: jest.fn(),
}),
createProcessors: jest.fn().mockReturnValue([
{
execute: jest.fn(),
},
]),
}),
};
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
describe('Matcher', () => {
it('should be defined', () => {
expect(new Matcher(mockAlgorithmFactoryCreator)).toBeDefined();
});
it('should return matches', async () => {
const matcher = new Matcher(mockAlgorithmFactoryCreator);
const matches = await matcher.match(matchQuery);
expect(matches.length).toBe(1);
});
});

View File

@ -0,0 +1,61 @@
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { Candidate } from '../../../../domain/entities/engine/candidate';
import { Processor } from '../../../../domain/entities/engine/processor/processor.abstract';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
class FakeProcessor extends Processor {
execute = (candidates: Candidate[]): Candidate[] => {
return candidates;
};
}
describe('Processor', () => {
it('should create an extended class', () => {
expect(new FakeProcessor(matchQuery)).toBeDefined();
});
});

View File

@ -0,0 +1,61 @@
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
import { Candidate } from '../../../../domain/entities/engine/candidate';
import { RouteCompleter } from '../../../../domain/entities/engine/processor/completer/route.completer.processor';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
describe('RouteCompleter', () => {
it('should be defined', () => {
expect(new RouteCompleter(matchQuery)).toBeDefined();
});
it('should complete candidates', () => {
const candidates = [new Candidate(), new Candidate()];
const routeCompleter: RouteCompleter = new RouteCompleter(matchQuery);
expect(routeCompleter.complete(candidates).length).toBe(2);
});
});

View File

@ -0,0 +1,61 @@
import { MatchRequest } from '../../../../domain/dtos/match.request';
import { Candidate } from '../../../../domain/entities/engine/candidate';
import { AlgorithmType } from '../../../../domain/types/algorithm.enum';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
import { MatchQuery } from '../../../../queries/match.query';
import { Selector } from '../../../../domain/entities/engine/selector/selector.abstract';
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
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',
},
};
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
class FakeSelector extends Selector {
select = (): Promise<Candidate[]> => {
return Promise.resolve([new Candidate()]);
};
}
describe('Selector', () => {
it('should create an extended class', () => {
expect(new FakeSelector(matchQuery)).toBeDefined();
});
});

View File

@ -3,13 +3,28 @@ import { Messager } from '../../../adapters/secondaries/messager';
import { MatchUseCase } from '../../../domain/usecases/match.usecase'; import { MatchUseCase } from '../../../domain/usecases/match.usecase';
import { MatchRequest } from '../../../domain/dtos/match.request'; import { MatchRequest } from '../../../domain/dtos/match.request';
import { MatchQuery } from '../../../queries/match.query'; import { MatchQuery } from '../../../queries/match.query';
import { AdRepository } from '../../../adapters/secondaries/ad.repository';
import { AutomapperModule } from '@automapper/nestjs'; import { AutomapperModule } from '@automapper/nestjs';
import { classes } from '@automapper/classes'; import { classes } from '@automapper/classes';
import { IDefaultParams } from '../../../domain/types/default-params.type'; import { IDefaultParams } from '../../../domain/types/default-params.type';
import { Algorithm } from '../../../domain/types/algorithm.enum'; import { AlgorithmType } from '../../../domain/types/algorithm.enum';
import { Matcher } from '../../../domain/entities/engine/matcher';
import { Match } from '../../../domain/entities/ecosystem/match';
import {
MatcherException,
MatcherExceptionCode,
} from '../../../exceptions/matcher.exception';
const mockAdRepository = {}; const mockMatcher = {
match: jest
.fn()
.mockImplementationOnce(() => [new Match(), new Match(), new Match()])
.mockImplementationOnce(() => {
throw new MatcherException(
MatcherExceptionCode.INTERNAL,
'Something terrible happened !',
);
}),
};
const mockMessager = { const mockMessager = {
publish: jest.fn().mockImplementation(), publish: jest.fn().mockImplementation(),
@ -26,7 +41,7 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: Algorithm.CLASSIC, algorithm: AlgorithmType.CLASSIC,
strict: false, strict: false,
remoteness: 15000, remoteness: 15000,
useProportion: true, useProportion: true,
@ -40,6 +55,19 @@ const defaultParams: IDefaultParams = {
}, },
}; };
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.waypoints = [
{
lon: 1.093912,
lat: 49.440041,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
matchRequest.departure = '2023-04-01 12:23:00';
describe('MatchUseCase', () => { describe('MatchUseCase', () => {
let matchUseCase: MatchUseCase; let matchUseCase: MatchUseCase;
@ -47,14 +75,14 @@ describe('MatchUseCase', () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
providers: [ providers: [
{
provide: AdRepository,
useValue: mockAdRepository,
},
{ {
provide: Messager, provide: Messager,
useValue: mockMessager, useValue: mockMessager,
}, },
{
provide: Matcher,
useValue: mockMatcher,
},
MatchUseCase, MatchUseCase,
], ],
}).compile(); }).compile();
@ -68,22 +96,18 @@ describe('MatchUseCase', () => {
describe('execute', () => { describe('execute', () => {
it('should return matches', async () => { it('should return matches', async () => {
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.waypoints = [
{
lon: 1.093912,
lat: 49.440041,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
matchRequest.departure = '2023-04-01 12:23:00';
const matches = await matchUseCase.execute( const matches = await matchUseCase.execute(
new MatchQuery(matchRequest, defaultParams, mockGeorouterCreator), new MatchQuery(matchRequest, defaultParams, mockGeorouterCreator),
); );
expect(matches.total).toBe(1); expect(matches.total).toBe(3);
});
it('should throw an exception when error occurs', async () => {
await expect(
matchUseCase.execute(
new MatchQuery(matchRequest, defaultParams, mockGeorouterCreator),
),
).rejects.toBeInstanceOf(MatcherException);
}); });
}); });
}); });

View File

@ -3,7 +3,7 @@ import { Role } from '../../../domain/types/role.enum';
import { TimingFrequency } from '../../../domain/types/timing'; import { TimingFrequency } from '../../../domain/types/timing';
import { IDefaultParams } from '../../../domain/types/default-params.type'; import { IDefaultParams } from '../../../domain/types/default-params.type';
import { MatchQuery } from '../../../queries/match.query'; import { MatchQuery } from '../../../queries/match.query';
import { Algorithm } from '../../../domain/types/algorithm.enum'; import { AlgorithmType } from '../../../domain/types/algorithm.enum';
const defaultParams: IDefaultParams = { const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0, DEFAULT_IDENTIFIER: 0,
@ -12,7 +12,7 @@ const defaultParams: IDefaultParams = {
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3, DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: { DEFAULT_ALGORITHM_SETTINGS: {
algorithm: Algorithm.CLASSIC, algorithm: AlgorithmType.CLASSIC,
strict: false, strict: false,
remoteness: 15000, remoteness: 15000,
useProportion: true, useProportion: true,
@ -181,7 +181,7 @@ describe('Match query', () => {
lon: 3.045432, lon: 3.045432,
}, },
]; ];
matchRequest.algorithm = Algorithm.CLASSIC; matchRequest.algorithm = AlgorithmType.CLASSIC;
matchRequest.strict = true; matchRequest.strict = true;
matchRequest.useProportion = true; matchRequest.useProportion = true;
matchRequest.proportion = 0.45; matchRequest.proportion = 0.45;
@ -195,7 +195,9 @@ describe('Match query', () => {
defaultParams, defaultParams,
mockGeorouterCreator, mockGeorouterCreator,
); );
expect(matchQuery.algorithmSettings.algorithm).toBe(Algorithm.CLASSIC); expect(matchQuery.algorithmSettings.algorithmType).toBe(
AlgorithmType.CLASSIC,
);
expect(matchQuery.algorithmSettings.restrict).toBe( expect(matchQuery.algorithmSettings.restrict).toBe(
TimingFrequency.FREQUENCY_PUNCTUAL, TimingFrequency.FREQUENCY_PUNCTUAL,
); );