add actorTime

This commit is contained in:
sbriat 2023-09-19 16:49:35 +02:00
parent 996759d001
commit 6b6a169dee
20 changed files with 481 additions and 143 deletions

View File

@ -1,5 +1,5 @@
import { Route } from '@modules/geography/core/domain/route.types';
import { Point } from '../types/point.type'; import { Point } from '../types/point.type';
import { Route } from '../types/route.type';
export interface RouteProviderPort { export interface RouteProviderPort {
/** /**

View File

@ -0,0 +1,9 @@
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Completer } from './completer.abstract';
export class JourneyCompleter extends Completer {
complete = async (
candidates: CandidateEntity[],
): Promise<CandidateEntity[]> =>
candidates.map((candidate: CandidateEntity) => candidate.createJourney());
}

View File

@ -1,7 +1,8 @@
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Completer } from './completer.abstract'; import { Completer } from './completer.abstract';
import { MatchQuery } from '../match.query'; import { MatchQuery } from '../match.query';
import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object';
import { Step } from '../../../types/step.type';
export class RouteCompleter extends Completer { export class RouteCompleter extends Completer {
protected readonly type: RouteCompleterType; protected readonly type: RouteCompleterType;
@ -18,8 +19,8 @@ export class RouteCompleter extends Completer {
switch (this.type) { switch (this.type) {
case RouteCompleterType.BASIC: case RouteCompleterType.BASIC:
const basicCandidateRoute = await this.query.routeProvider.getBasic( const basicCandidateRoute = await this.query.routeProvider.getBasic(
(candidate.getProps().carpoolSteps as WayStep[]).map( (candidate.getProps().carpoolSteps as CarpoolStep[]).map(
(wayStep: WayStep) => wayStep.point, (carpoolStep: CarpoolStep) => carpoolStep.point,
), ),
); );
candidate.setMetrics( candidate.setMetrics(
@ -30,14 +31,11 @@ export class RouteCompleter extends Completer {
case RouteCompleterType.DETAILED: case RouteCompleterType.DETAILED:
const detailedCandidateRoute = const detailedCandidateRoute =
await this.query.routeProvider.getDetailed( await this.query.routeProvider.getDetailed(
(candidate.getProps().carpoolSteps as WayStep[]).map( (candidate.getProps().carpoolSteps as CarpoolStep[]).map(
(wayStep: WayStep) => wayStep.point, (carpoolStep: CarpoolStep) => carpoolStep.point,
), ),
); );
candidate.setMetrics( candidate.setSteps(detailedCandidateRoute.steps as Step[]);
detailedCandidateRoute.distance,
detailedCandidateRoute.duration,
);
break; break;
} }
return candidate; return candidate;

View File

@ -5,7 +5,6 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
import { RouteProviderPort } from '../../ports/route-provider.port'; import { RouteProviderPort } from '../../ports/route-provider.port';
import { Route } from '@modules/geography/core/domain/route.types';
import { import {
Path, Path,
PathCreator, PathCreator,
@ -13,6 +12,7 @@ import {
TypedRoute, TypedRoute,
} from '@modules/ad/core/domain/path-creator.service'; } from '@modules/ad/core/domain/path-creator.service';
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
import { Route } from '../../types/route.type';
export class MatchQuery extends QueryBase { export class MatchQuery extends QueryBase {
driver?: boolean; driver?: boolean;

View File

@ -0,0 +1,12 @@
import { Point } from './point.type';
import { Step } from './step.type';
export type Route = {
distance: number;
duration: number;
fwdAzimuth: number;
backAzimuth: number;
distanceAzimuth: number;
points: Point[];
steps?: Step[];
};

View File

@ -0,0 +1,6 @@
import { Point } from './point.type';
export type Step = Point & {
duration: number;
distance?: number;
};

View File

@ -1,6 +1,7 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { CandidateProps, CreateCandidateProps } from './candidate.types'; import { CandidateProps, CreateCandidateProps } from './candidate.types';
import { WayStepProps } from './value-objects/waystep.value-object'; import { CarpoolStepProps } from './value-objects/carpool-step.value-object';
import { StepProps } from './value-objects/step.value-object';
export class CandidateEntity extends AggregateRoot<CandidateProps> { export class CandidateEntity extends AggregateRoot<CandidateProps> {
protected readonly _id: AggregateID; protected readonly _id: AggregateID;
@ -10,8 +11,8 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
return new CandidateEntity({ id: create.id, props }); return new CandidateEntity({ id: create.id, props });
}; };
setCarpoolPath = (waySteps: WayStepProps[]): CandidateEntity => { setCarpoolPath = (carpoolSteps: CarpoolStepProps[]): CandidateEntity => {
this.props.carpoolSteps = waySteps; this.props.carpoolSteps = carpoolSteps;
return this; return this;
}; };
@ -21,9 +22,16 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
return this; return this;
}; };
setSteps = (steps: StepProps[]): CandidateEntity => {
this.props.steps = steps;
return this;
};
isDetourValid = (): boolean => isDetourValid = (): boolean =>
this._validateDistanceDetour() && this._validateDurationDetour(); this._validateDistanceDetour() && this._validateDurationDetour();
createJourney = (): CandidateEntity => this;
private _validateDurationDetour = (): boolean => private _validateDurationDetour = (): boolean =>
this.props.duration this.props.duration
? this.props.duration <= ? this.props.duration <=

View File

@ -1,7 +1,9 @@
import { Role } from './ad.types'; import { Role } from './ad.types';
import { PointProps } from './value-objects/point.value-object'; import { PointProps } from './value-objects/point.value-object';
import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
import { WayStepProps } from './value-objects/waystep.value-object'; import { CarpoolStepProps } from './value-objects/carpool-step.value-object';
import { JourneyProps } from './value-objects/journey.value-object';
import { StepProps } from './value-objects/step.value-object';
// All properties that a Candidate has // All properties that a Candidate has
export interface CandidateProps { export interface CandidateProps {
@ -10,11 +12,13 @@ export interface CandidateProps {
passengerWaypoints: PointProps[]; passengerWaypoints: PointProps[];
driverDistance: number; driverDistance: number;
driverDuration: number; driverDuration: number;
carpoolSteps?: WayStepProps[]; // carpool path for the crew (driver + passenger) carpoolSteps?: CarpoolStepProps[];
distance?: number; distance?: number;
duration?: number; duration?: number;
steps?: StepProps[];
driverSchedule: ScheduleItemProps[]; driverSchedule: ScheduleItemProps[];
passengerSchedule: ScheduleItemProps[]; passengerSchedule: ScheduleItemProps[];
journeys?: JourneyProps[];
spacetimeDetourRatio: SpacetimeDetourRatio; spacetimeDetourRatio: SpacetimeDetourRatio;
} }

View File

@ -3,7 +3,7 @@ import { Target } from './candidate.types';
import { CarpoolPathCreatorException } from './match.errors'; import { CarpoolPathCreatorException } from './match.errors';
import { Actor } from './value-objects/actor.value-object'; import { Actor } from './value-objects/actor.value-object';
import { Point } from './value-objects/point.value-object'; import { Point } from './value-objects/point.value-object';
import { WayStep } from './value-objects/waystep.value-object'; import { CarpoolStep } from './value-objects/carpool-step.value-object';
export class CarpoolPathCreator { export class CarpoolPathCreator {
private PRECISION = 5; private PRECISION = 5;
@ -23,29 +23,35 @@ export class CarpoolPathCreator {
} }
/** /**
* Creates a path (a list of waysteps) between driver waypoints * Creates a path (a list of carpoolSteps) between driver waypoints
and passenger waypoints respecting the order and passenger waypoints respecting the order
of the driver waypoints of the driver waypoints
Inspired by : Inspired by :
https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
*/ */
public carpoolPath = (): WayStep[] => public carpoolPath = (): CarpoolStep[] =>
this._consolidate( this._consolidate(
this._mixedWaysteps(this._driverWaysteps(), this._passengerWaysteps()), this._mixedCarpoolSteps(
this._driverCarpoolSteps(),
this._passengerCarpoolSteps(),
),
); );
private _mixedWaysteps = ( private _mixedCarpoolSteps = (
driverWaysteps: WayStep[], driverCarpoolSteps: CarpoolStep[],
passengerWaysteps: WayStep[], passengerCarpoolSteps: CarpoolStep[],
): WayStep[] => ): CarpoolStep[] =>
driverWaysteps.length == 2 driverCarpoolSteps.length == 2
? this._simpleMixedWaysteps(driverWaysteps, passengerWaysteps) ? this._simpleMixedCarpoolSteps(driverCarpoolSteps, passengerCarpoolSteps)
: this._complexMixedWaysteps(driverWaysteps, passengerWaysteps); : this._complexMixedCarpoolSteps(
driverCarpoolSteps,
passengerCarpoolSteps,
);
private _driverWaysteps = (): WayStep[] => private _driverCarpoolSteps = (): CarpoolStep[] =>
this.driverWaypoints.map( this.driverWaypoints.map(
(waypoint: Point, index: number) => (waypoint: Point, index: number) =>
new WayStep({ new CarpoolStep({
point: new Point({ point: new Point({
lon: waypoint.lon, lon: waypoint.lon,
lat: waypoint.lat, lat: waypoint.lat,
@ -60,13 +66,13 @@ export class CarpoolPathCreator {
); );
/** /**
* Creates the passenger waysteps with original passenger waypoints, adding driver waypoints that are the same * Creates the passenger carpoolSteps with original passenger waypoints, adding driver waypoints that are the same
*/ */
private _passengerWaysteps = (): WayStep[] => { private _passengerCarpoolSteps = (): CarpoolStep[] => {
const waysteps: WayStep[] = []; const carpoolSteps: CarpoolStep[] = [];
this.passengerWaypoints.forEach( this.passengerWaypoints.forEach(
(passengerWaypoint: Point, index: number) => { (passengerWaypoint: Point, index: number) => {
const waystep: WayStep = new WayStep({ const carpoolStep: CarpoolStep = new CarpoolStep({
point: new Point({ point: new Point({
lon: passengerWaypoint.lon, lon: passengerWaypoint.lon,
lat: passengerWaypoint.lat, lat: passengerWaypoint.lat,
@ -83,73 +89,78 @@ export class CarpoolPathCreator {
passengerWaypoint.isSame(driverWaypoint), passengerWaypoint.isSame(driverWaypoint),
).length == 0 ).length == 0
) { ) {
waystep.actors.push( carpoolStep.actors.push(
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,
target: Target.NEUTRAL, target: Target.NEUTRAL,
}), }),
); );
} }
waysteps.push(waystep); carpoolSteps.push(carpoolStep);
}, },
); );
return waysteps; return carpoolSteps;
}; };
private _simpleMixedWaysteps = ( private _simpleMixedCarpoolSteps = (
driverWaysteps: WayStep[], driverCarpoolSteps: CarpoolStep[],
passengerWaysteps: WayStep[], passengerCarpoolSteps: CarpoolStep[],
): WayStep[] => [driverWaysteps[0], ...passengerWaysteps, driverWaysteps[1]]; ): CarpoolStep[] => [
driverCarpoolSteps[0],
...passengerCarpoolSteps,
driverCarpoolSteps[1],
];
private _complexMixedWaysteps = ( private _complexMixedCarpoolSteps = (
driverWaysteps: WayStep[], driverCarpoolSteps: CarpoolStep[],
passengerWaysteps: WayStep[], passengerCarpoolSteps: CarpoolStep[],
): WayStep[] => { ): CarpoolStep[] => {
let mixedWaysteps: WayStep[] = [...driverWaysteps]; let mixedCarpoolSteps: CarpoolStep[] = [...driverCarpoolSteps];
const originInsertIndex: number = this._insertIndex( const originInsertIndex: number = this._insertIndex(
passengerWaysteps[0], passengerCarpoolSteps[0],
driverWaysteps, driverCarpoolSteps,
); );
mixedWaysteps = [ mixedCarpoolSteps = [
...mixedWaysteps.slice(0, originInsertIndex), ...mixedCarpoolSteps.slice(0, originInsertIndex),
passengerWaysteps[0], passengerCarpoolSteps[0],
...mixedWaysteps.slice(originInsertIndex), ...mixedCarpoolSteps.slice(originInsertIndex),
]; ];
const destinationInsertIndex: number = const destinationInsertIndex: number =
this._insertIndex( this._insertIndex(
passengerWaysteps[passengerWaysteps.length - 1], passengerCarpoolSteps[passengerCarpoolSteps.length - 1],
driverWaysteps, driverCarpoolSteps,
) + 1; ) + 1;
mixedWaysteps = [ mixedCarpoolSteps = [
...mixedWaysteps.slice(0, destinationInsertIndex), ...mixedCarpoolSteps.slice(0, destinationInsertIndex),
passengerWaysteps[passengerWaysteps.length - 1], passengerCarpoolSteps[passengerCarpoolSteps.length - 1],
...mixedWaysteps.slice(destinationInsertIndex), ...mixedCarpoolSteps.slice(destinationInsertIndex),
]; ];
return mixedWaysteps; return mixedCarpoolSteps;
}; };
private _insertIndex = ( private _insertIndex = (
targetWaystep: WayStep, targetCarpoolStep: CarpoolStep,
waysteps: WayStep[], carpoolSteps: CarpoolStep[],
): number => ): number =>
this._closestSegmentIndex(targetWaystep, this._segments(waysteps)) + 1; this._closestSegmentIndex(targetCarpoolStep, this._segments(carpoolSteps)) +
1;
private _segments = (waysteps: WayStep[]): WayStep[][] => { private _segments = (carpoolSteps: CarpoolStep[]): CarpoolStep[][] => {
const segments: WayStep[][] = []; const segments: CarpoolStep[][] = [];
waysteps.forEach((waystep: WayStep, index: number) => { carpoolSteps.forEach((carpoolStep: CarpoolStep, index: number) => {
if (index < waysteps.length - 1) if (index < carpoolSteps.length - 1)
segments.push([waystep, waysteps[index + 1]]); segments.push([carpoolStep, carpoolSteps[index + 1]]);
}); });
return segments; return segments;
}; };
private _closestSegmentIndex = ( private _closestSegmentIndex = (
waystep: WayStep, carpoolStep: CarpoolStep,
segments: WayStep[][], segments: CarpoolStep[][],
): number => { ): number => {
const distances: Map<number, number> = new Map(); const distances: Map<number, number> = new Map();
segments.forEach((segment: WayStep[], index: number) => { segments.forEach((segment: CarpoolStep[], index: number) => {
distances.set(index, this._distanceToSegment(waystep, segment)); distances.set(index, this._distanceToSegment(carpoolStep, segment));
}); });
const sortedDistances: Map<number, number> = new Map( const sortedDistances: Map<number, number> = new Map(
[...distances.entries()].sort((a, b) => a[1] - b[1]), [...distances.entries()].sort((a, b) => a[1] - b[1]),
@ -158,30 +169,33 @@ export class CarpoolPathCreator {
return closestSegmentIndex; return closestSegmentIndex;
}; };
private _distanceToSegment = (waystep: WayStep, segment: WayStep[]): number => private _distanceToSegment = (
carpoolStep: CarpoolStep,
segment: CarpoolStep[],
): number =>
parseFloat( parseFloat(
Math.sqrt(this._distanceToSegmentSquared(waystep, segment)).toFixed( Math.sqrt(this._distanceToSegmentSquared(carpoolStep, segment)).toFixed(
this.PRECISION, this.PRECISION,
), ),
); );
private _distanceToSegmentSquared = ( private _distanceToSegmentSquared = (
waystep: WayStep, carpoolStep: CarpoolStep,
segment: WayStep[], segment: CarpoolStep[],
): number => { ): number => {
const length2: number = this._distanceSquared( const length2: number = this._distanceSquared(
segment[0].point, segment[0].point,
segment[1].point, segment[1].point,
); );
if (length2 == 0) if (length2 == 0)
return this._distanceSquared(waystep.point, segment[0].point); return this._distanceSquared(carpoolStep.point, segment[0].point);
const length: number = Math.max( const length: number = Math.max(
0, 0,
Math.min( Math.min(
1, 1,
((waystep.point.lon - segment[0].point.lon) * ((carpoolStep.point.lon - segment[0].point.lon) *
(segment[1].point.lon - segment[0].point.lon) + (segment[1].point.lon - segment[0].point.lon) +
(waystep.point.lat - segment[0].point.lat) * (carpoolStep.point.lat - segment[0].point.lat) *
(segment[1].point.lat - segment[0].point.lat)) / (segment[1].point.lat - segment[0].point.lat)) /
length2, length2,
), ),
@ -194,7 +208,7 @@ export class CarpoolPathCreator {
segment[0].point.lat + segment[0].point.lat +
length * (segment[1].point.lat - segment[0].point.lat), length * (segment[1].point.lat - segment[0].point.lat),
}); });
return this._distanceSquared(waystep.point, newPoint); return this._distanceSquared(carpoolStep.point, newPoint);
}; };
private _distanceSquared = (point1: Point, point2: Point): number => private _distanceSquared = (point1: Point, point2: Point): number =>
@ -213,29 +227,31 @@ export class CarpoolPathCreator {
: Target.INTERMEDIATE; : Target.INTERMEDIATE;
/** /**
* Consolidate waysteps by removing duplicate actors (eg. driver with neutral and start or finish target) * Consolidate carpoolSteps by removing duplicate actors (eg. driver with neutral and start or finish target)
*/ */
private _consolidate = (waysteps: WayStep[]): WayStep[] => { private _consolidate = (carpoolSteps: CarpoolStep[]): CarpoolStep[] => {
const uniquePoints: Point[] = []; const uniquePoints: Point[] = [];
waysteps.forEach((waystep: WayStep) => { carpoolSteps.forEach((carpoolStep: CarpoolStep) => {
if ( if (
uniquePoints.find((point: Point) => point.isSame(waystep.point)) === uniquePoints.find((point: Point) => point.isSame(carpoolStep.point)) ===
undefined undefined
) )
uniquePoints.push( uniquePoints.push(
new Point({ new Point({
lon: waystep.point.lon, lon: carpoolStep.point.lon,
lat: waystep.point.lat, lat: carpoolStep.point.lat,
}), }),
); );
}); });
return uniquePoints.map( return uniquePoints.map(
(point: Point) => (point: Point) =>
new WayStep({ new CarpoolStep({
point, point,
actors: waysteps actors: carpoolSteps
.filter((waystep: WayStep) => waystep.point.isSame(point)) .filter((carpoolStep: CarpoolStep) =>
.map((waystep: WayStep) => waystep.actors) carpoolStep.point.isSame(point),
)
.map((carpoolStep: CarpoolStep) => carpoolStep.actors)
.flat(), .flat(),
}), }),
); );

View File

@ -1,7 +1,7 @@
import { Route } from '@modules/geography/core/domain/route.types';
import { Role } from './ad.types'; import { Role } from './ad.types';
import { Point } from './value-objects/point.value-object'; import { Point } from './value-objects/point.value-object';
import { PathCreatorException } from './match.errors'; import { PathCreatorException } from './match.errors';
import { Route } from '../application/types/route.type';
export class PathCreator { export class PathCreator {
constructor( constructor(

View File

@ -0,0 +1,51 @@
import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
import { Role } from '../ad.types';
import { Target } from '../candidate.types';
import { ActorProps } from './actor.value-object';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface ActorTimeProps extends ActorProps {
time: string;
minTime: string;
maxTime: string;
}
export class ActorTime extends ValueObject<ActorTimeProps> {
get time(): string {
return this.props.time;
}
get minTime(): string {
return this.props.minTime;
}
get maxTime(): string {
return this.props.maxTime;
}
get role(): Role {
return this.props.role;
}
get target(): Target {
return this.props.target;
}
protected validate(props: ActorTimeProps): void {
this._validateTime(props.time, 'time');
this._validateTime(props.minTime, 'minTime');
this._validateTime(props.maxTime, 'maxTime');
}
private _validateTime(time: string, property: string): void {
if (time.split(':').length != 2)
throw new ArgumentInvalidException(`${property} is invalid`);
if (parseInt(time.split(':')[0]) < 0 || parseInt(time.split(':')[0]) > 23)
throw new ArgumentInvalidException(`${property} is invalid`);
if (parseInt(time.split(':')[1]) < 0 || parseInt(time.split(':')[1]) > 59)
throw new ArgumentInvalidException(`${property} is invalid`);
}
}

View File

@ -11,12 +11,12 @@ import { Point } from './point.value-object';
* other Value Objects inside if needed. * other Value Objects inside if needed.
* */ * */
export interface WayStepProps { export interface CarpoolStepProps {
point: Point; point: Point;
actors: Actor[]; actors: Actor[];
} }
export class WayStep extends ValueObject<WayStepProps> { export class CarpoolStep extends ValueObject<CarpoolStepProps> {
get point(): Point { get point(): Point {
return this.props.point; return this.props.point;
} }
@ -25,7 +25,7 @@ export class WayStep extends ValueObject<WayStepProps> {
return this.props.actors; return this.props.actors;
} }
protected validate(props: WayStepProps): void { protected validate(props: CarpoolStepProps): void {
if (props.actors.length <= 0) if (props.actors.length <= 0)
throw new ArgumentOutOfRangeException('at least one actor is required'); throw new ArgumentOutOfRangeException('at least one actor is required');
if ( if (
@ -33,7 +33,7 @@ export class WayStep extends ValueObject<WayStepProps> {
1 1
) )
throw new ArgumentOutOfRangeException( throw new ArgumentOutOfRangeException(
'a waystep can contain only one driver', 'a carpoolStep can contain only one driver',
); );
} }
} }

View File

@ -0,0 +1,42 @@
import {
ArgumentOutOfRangeException,
ValueObject,
} from '@mobicoop/ddd-library';
import { ScheduleItemProps } from './schedule-item.value-object';
import { ActorTime } from './actor-time.value-object';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface JourneyProps extends ScheduleItemProps {
firstDate: Date;
lastDate: Date;
actorTimes: ActorTime[];
}
export class Journey extends ValueObject<JourneyProps> {
get firstDate(): Date {
return this.props.firstDate;
}
get lastDate(): Date {
return this.props.lastDate;
}
get actorTimes(): ActorTime[] {
return this.props.actorTimes;
}
protected validate(props: JourneyProps): void {
if (props.firstDate > props.lastDate)
throw new ArgumentOutOfRangeException(
'firstDate must be before lastDate',
);
if (props.actorTimes.length <= 0)
throw new ArgumentOutOfRangeException(
'at least one actorTime is required',
);
}
}

View File

@ -28,7 +28,6 @@ export class ScheduleItem extends ValueObject<ScheduleItemProps> {
return this.props.margin; return this.props.margin;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected validate(props: ScheduleItemProps): void { protected validate(props: ScheduleItemProps): void {
if (props.day < 0 || props.day > 6) if (props.day < 0 || props.day > 6)
throw new ArgumentOutOfRangeException('day must be between 0 and 6'); throw new ArgumentOutOfRangeException('day must be between 0 and 6');

View File

@ -0,0 +1,41 @@
import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
import { PointProps } from './point.value-object';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface StepProps extends PointProps {
duration: number;
distance?: number;
}
export class Step extends ValueObject<StepProps> {
get duration(): number {
return this.props.duration;
}
get distance(): number | undefined {
return this.props.distance;
}
get lon(): number {
return this.props.lon;
}
get lat(): number {
return this.props.lat;
}
protected validate(props: StepProps): void {
if (props.duration < 0)
throw new ArgumentInvalidException(
'duration must be greater than or equal to 0',
);
if (props.distance !== undefined && props.distance < 0)
throw new ArgumentInvalidException(
'distance must be greater than or equal to 0',
);
}
}

View File

@ -1,7 +1,7 @@
import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service'; import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service';
import { CarpoolPathCreatorException } from '@modules/ad/core/domain/match.errors'; import { CarpoolPathCreatorException } from '@modules/ad/core/domain/match.errors';
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object';
const waypoint1: Point = new Point({ const waypoint1: Point = new Point({
lat: 0, lat: 0,
@ -34,71 +34,71 @@ describe('Carpool Path Creator Service', () => {
[waypoint1, waypoint6], [waypoint1, waypoint6],
[waypoint2, waypoint5], [waypoint2, waypoint5],
); );
const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath();
expect(waysteps).toHaveLength(4); expect(carpoolSteps).toHaveLength(4);
expect(waysteps[0].actors.length).toBe(1); expect(carpoolSteps[0].actors.length).toBe(1);
}); });
it('should create a simple carpool path with same destination for driver and passenger', () => { it('should create a simple carpool path with same destination for driver and passenger', () => {
const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
[waypoint1, waypoint6], [waypoint1, waypoint6],
[waypoint2, waypoint6], [waypoint2, waypoint6],
); );
const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath();
expect(waysteps).toHaveLength(3); expect(carpoolSteps).toHaveLength(3);
expect(waysteps[0].actors.length).toBe(1); expect(carpoolSteps[0].actors.length).toBe(1);
expect(waysteps[1].actors.length).toBe(2); expect(carpoolSteps[1].actors.length).toBe(2);
expect(waysteps[2].actors.length).toBe(2); expect(carpoolSteps[2].actors.length).toBe(2);
}); });
it('should create a simple carpool path with same waypoints for driver and passenger', () => { it('should create a simple carpool path with same waypoints for driver and passenger', () => {
const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
[waypoint1, waypoint6], [waypoint1, waypoint6],
[waypoint1, waypoint6], [waypoint1, waypoint6],
); );
const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath();
expect(waysteps).toHaveLength(2); expect(carpoolSteps).toHaveLength(2);
expect(waysteps[0].actors.length).toBe(2); expect(carpoolSteps[0].actors.length).toBe(2);
expect(waysteps[1].actors.length).toBe(2); expect(carpoolSteps[1].actors.length).toBe(2);
}); });
it('should create a complex carpool path with 3 driver waypoints', () => { it('should create a complex carpool path with 3 driver waypoints', () => {
const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
[waypoint1, waypoint3, waypoint6], [waypoint1, waypoint3, waypoint6],
[waypoint2, waypoint5], [waypoint2, waypoint5],
); );
const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath();
expect(waysteps).toHaveLength(5); expect(carpoolSteps).toHaveLength(5);
expect(waysteps[0].actors.length).toBe(1); expect(carpoolSteps[0].actors.length).toBe(1);
expect(waysteps[1].actors.length).toBe(2); expect(carpoolSteps[1].actors.length).toBe(2);
expect(waysteps[2].actors.length).toBe(1); expect(carpoolSteps[2].actors.length).toBe(1);
expect(waysteps[3].actors.length).toBe(2); expect(carpoolSteps[3].actors.length).toBe(2);
expect(waysteps[4].actors.length).toBe(1); expect(carpoolSteps[4].actors.length).toBe(1);
}); });
it('should create a complex carpool path with 4 driver waypoints', () => { it('should create a complex carpool path with 4 driver waypoints', () => {
const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
[waypoint1, waypoint3, waypoint4, waypoint6], [waypoint1, waypoint3, waypoint4, waypoint6],
[waypoint2, waypoint5], [waypoint2, waypoint5],
); );
const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath();
expect(waysteps).toHaveLength(6); expect(carpoolSteps).toHaveLength(6);
expect(waysteps[0].actors.length).toBe(1); expect(carpoolSteps[0].actors.length).toBe(1);
expect(waysteps[1].actors.length).toBe(2); expect(carpoolSteps[1].actors.length).toBe(2);
expect(waysteps[2].actors.length).toBe(1); expect(carpoolSteps[2].actors.length).toBe(1);
expect(waysteps[3].actors.length).toBe(1); expect(carpoolSteps[3].actors.length).toBe(1);
expect(waysteps[4].actors.length).toBe(2); expect(carpoolSteps[4].actors.length).toBe(2);
expect(waysteps[5].actors.length).toBe(1); expect(carpoolSteps[5].actors.length).toBe(1);
}); });
it('should create a alternate complex carpool path with 4 driver waypoints', () => { it('should create a alternate complex carpool path with 4 driver waypoints', () => {
const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
[waypoint1, waypoint2, waypoint5, waypoint6], [waypoint1, waypoint2, waypoint5, waypoint6],
[waypoint3, waypoint4], [waypoint3, waypoint4],
); );
const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath();
expect(waysteps).toHaveLength(6); expect(carpoolSteps).toHaveLength(6);
expect(waysteps[0].actors.length).toBe(1); expect(carpoolSteps[0].actors.length).toBe(1);
expect(waysteps[1].actors.length).toBe(1); expect(carpoolSteps[1].actors.length).toBe(1);
expect(waysteps[2].actors.length).toBe(2); expect(carpoolSteps[2].actors.length).toBe(2);
expect(waysteps[3].actors.length).toBe(2); expect(carpoolSteps[3].actors.length).toBe(2);
expect(waysteps[4].actors.length).toBe(1); expect(carpoolSteps[4].actors.length).toBe(1);
expect(waysteps[5].actors.length).toBe(1); expect(carpoolSteps[5].actors.length).toBe(1);
}); });
it('should throw an exception if less than 2 driver waypoints are given', () => { it('should throw an exception if less than 2 driver waypoints are given', () => {
try { try {

View File

@ -3,11 +3,11 @@ import { Role } from '@modules/ad/core/domain/ad.types';
import { Target } from '@modules/ad/core/domain/candidate.types'; import { Target } from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object';
describe('WayStep value object', () => { describe('CarpoolStep value object', () => {
it('should create a waystep value object', () => { it('should create a carpoolStep value object', () => {
const wayStepVO = new WayStep({ const carpoolStepVO = new CarpoolStep({
point: new Point({ point: new Point({
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,
@ -23,13 +23,13 @@ describe('WayStep value object', () => {
}), }),
], ],
}); });
expect(wayStepVO.point.lon).toBe(6.17651); expect(carpoolStepVO.point.lon).toBe(6.17651);
expect(wayStepVO.point.lat).toBe(48.689445); expect(carpoolStepVO.point.lat).toBe(48.689445);
expect(wayStepVO.actors).toHaveLength(2); expect(carpoolStepVO.actors).toHaveLength(2);
}); });
it('should throw an exception if actors is empty', () => { it('should throw an exception if actors is empty', () => {
try { try {
new WayStep({ new CarpoolStep({
point: new Point({ point: new Point({
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,
@ -42,7 +42,7 @@ describe('WayStep value object', () => {
}); });
it('should throw an exception if actors contains more than one driver', () => { it('should throw an exception if actors contains more than one driver', () => {
try { try {
new WayStep({ new CarpoolStep({
point: new Point({ point: new Point({
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,

View File

@ -0,0 +1,151 @@
import { JourneyCompleter } from '@modules/ad/core/application/queries/match/completer/journey.completer';
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Target } from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
const originWaypoint: Waypoint = {
position: 0,
lat: 48.689445,
lon: 6.17651,
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
};
const destinationWaypoint: Waypoint = {
position: 1,
lat: 48.8566,
lon: 2.3522,
locality: 'Paris',
postalCode: '75000',
country: 'France',
};
const matchQuery = new MatchQuery(
{
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
driver: true,
passenger: true,
frequency: Frequency.PUNCTUAL,
fromDate: '2023-08-28',
toDate: '2023-08-28',
schedule: [
{
time: '07:05',
},
],
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
},
{
getBasic: jest.fn().mockImplementation(() => ({
distance: 350101,
duration: 14422,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336544,
points: [],
})),
getDetailed: jest.fn().mockImplementation(() => ({
distance: 350102,
duration: 14423,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336544,
points: [],
})),
},
);
const candidate: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER,
driverWaypoints: [
{
lat: 48.678454,
lon: 6.189745,
},
{
lat: 48.84877,
lon: 2.398457,
},
],
passengerWaypoints: [
{
lat: 48.689445,
lon: 6.17651,
},
{
lat: 48.8566,
lon: 2.3522,
},
],
driverDistance: 350145,
driverDuration: 13548,
driverSchedule: [
{
day: 0,
time: '07:00',
margin: 900,
},
],
passengerSchedule: [
{
day: 0,
time: '07:10',
margin: 900,
},
],
spacetimeDetourRatio: {
maxDistanceDetourRatio: 0.3,
maxDurationDetourRatio: 0.3,
},
}).setCarpoolPath([
{
point: new Point({
lat: 48.689445,
lon: 6.17651,
}),
actors: [
new Actor({
role: Role.DRIVER,
target: Target.START,
}),
new Actor({
role: Role.PASSENGER,
target: Target.START,
}),
],
},
{
point: new Point({
lat: 48.8566,
lon: 2.3522,
}),
actors: [
new Actor({
role: Role.DRIVER,
target: Target.FINISH,
}),
new Actor({
role: Role.PASSENGER,
target: Target.FINISH,
}),
],
},
]);
describe('Journey completer', () => {
it('should complete candidates with their journey', async () => {
const journeyCompleter: JourneyCompleter = new JourneyCompleter(matchQuery);
const completedCandidates: CandidateEntity[] =
await journeyCompleter.complete([candidate]);
expect(completedCandidates.length).toBe(1);
});
});

View File

@ -56,12 +56,13 @@ const matchQuery = new MatchQuery(
points: [], points: [],
})), })),
getDetailed: jest.fn().mockImplementation(() => ({ getDetailed: jest.fn().mockImplementation(() => ({
distance: 350102, distance: 350101,
duration: 14423, duration: 14422,
fwdAzimuth: 273, fwdAzimuth: 273,
backAzimuth: 93, backAzimuth: 93,
distanceAzimuth: 336544, distanceAzimuth: 336544,
points: [], points: [],
steps: [jest.fn(), jest.fn(), jest.fn(), jest.fn()],
})), })),
}, },
); );
@ -163,6 +164,6 @@ describe('Route completer', () => {
const completedCandidates: CandidateEntity[] = const completedCandidates: CandidateEntity[] =
await routeCompleter.complete([candidate]); await routeCompleter.complete([candidate]);
expect(completedCandidates.length).toBe(1); expect(completedCandidates.length).toBe(1);
expect(completedCandidates[0].getProps().distance).toBe(350102); expect(completedCandidates[0].getProps().steps).toHaveLength(4);
}); });
}); });

View File

@ -3,9 +3,9 @@ import {
AD_GET_DETAILED_ROUTE_CONTROLLER, AD_GET_DETAILED_ROUTE_CONTROLLER,
} from '@modules/ad/ad.di-tokens'; } from '@modules/ad/ad.di-tokens';
import { Point } from '@modules/ad/core/application/types/point.type'; import { Point } from '@modules/ad/core/application/types/point.type';
import { Route } from '@modules/ad/core/application/types/route.type';
import { RouteProvider } from '@modules/ad/infrastructure/route-provider'; import { RouteProvider } from '@modules/ad/infrastructure/route-provider';
import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port'; import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port';
import { Route } from '@modules/geography/core/domain/route.types';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
const originPoint: Point = { const originPoint: Point = {