diff --git a/src/modules/ad/core/application/ports/route-provider.port.ts b/src/modules/ad/core/application/ports/route-provider.port.ts index ca06709..2087fba 100644 --- a/src/modules/ad/core/application/ports/route-provider.port.ts +++ b/src/modules/ad/core/application/ports/route-provider.port.ts @@ -1,5 +1,5 @@ -import { Route } from '@modules/geography/core/domain/route.types'; import { Point } from '../types/point.type'; +import { Route } from '../types/route.type'; export interface RouteProviderPort { /** diff --git a/src/modules/ad/core/application/queries/match/completer/journey.completer.ts b/src/modules/ad/core/application/queries/match/completer/journey.completer.ts new file mode 100644 index 0000000..ac4ae5f --- /dev/null +++ b/src/modules/ad/core/application/queries/match/completer/journey.completer.ts @@ -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 => + candidates.map((candidate: CandidateEntity) => candidate.createJourney()); +} diff --git a/src/modules/ad/core/application/queries/match/completer/route.completer.ts b/src/modules/ad/core/application/queries/match/completer/route.completer.ts index 3a61a20..96dc9f1 100644 --- a/src/modules/ad/core/application/queries/match/completer/route.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/route.completer.ts @@ -1,7 +1,8 @@ import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Completer } from './completer.abstract'; 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 { protected readonly type: RouteCompleterType; @@ -18,8 +19,8 @@ export class RouteCompleter extends Completer { switch (this.type) { case RouteCompleterType.BASIC: const basicCandidateRoute = await this.query.routeProvider.getBasic( - (candidate.getProps().carpoolSteps as WayStep[]).map( - (wayStep: WayStep) => wayStep.point, + (candidate.getProps().carpoolSteps as CarpoolStep[]).map( + (carpoolStep: CarpoolStep) => carpoolStep.point, ), ); candidate.setMetrics( @@ -30,14 +31,11 @@ export class RouteCompleter extends Completer { case RouteCompleterType.DETAILED: const detailedCandidateRoute = await this.query.routeProvider.getDetailed( - (candidate.getProps().carpoolSteps as WayStep[]).map( - (wayStep: WayStep) => wayStep.point, + (candidate.getProps().carpoolSteps as CarpoolStep[]).map( + (carpoolStep: CarpoolStep) => carpoolStep.point, ), ); - candidate.setMetrics( - detailedCandidateRoute.distance, - detailedCandidateRoute.duration, - ); + candidate.setSteps(detailedCandidateRoute.steps as Step[]); break; } return candidate; diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index 8f96427..337ae7c 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -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 { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; import { RouteProviderPort } from '../../ports/route-provider.port'; -import { Route } from '@modules/geography/core/domain/route.types'; import { Path, PathCreator, @@ -13,6 +12,7 @@ import { TypedRoute, } from '@modules/ad/core/domain/path-creator.service'; import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; +import { Route } from '../../types/route.type'; export class MatchQuery extends QueryBase { driver?: boolean; diff --git a/src/modules/ad/core/application/types/route.type.ts b/src/modules/ad/core/application/types/route.type.ts new file mode 100644 index 0000000..297bc71 --- /dev/null +++ b/src/modules/ad/core/application/types/route.type.ts @@ -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[]; +}; diff --git a/src/modules/ad/core/application/types/step.type.ts b/src/modules/ad/core/application/types/step.type.ts new file mode 100644 index 0000000..c9e9b7b --- /dev/null +++ b/src/modules/ad/core/application/types/step.type.ts @@ -0,0 +1,6 @@ +import { Point } from './point.type'; + +export type Step = Point & { + duration: number; + distance?: number; +}; diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index 89bcd84..7e4b1ad 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -1,6 +1,7 @@ import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; 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 { protected readonly _id: AggregateID; @@ -10,8 +11,8 @@ export class CandidateEntity extends AggregateRoot { return new CandidateEntity({ id: create.id, props }); }; - setCarpoolPath = (waySteps: WayStepProps[]): CandidateEntity => { - this.props.carpoolSteps = waySteps; + setCarpoolPath = (carpoolSteps: CarpoolStepProps[]): CandidateEntity => { + this.props.carpoolSteps = carpoolSteps; return this; }; @@ -21,9 +22,16 @@ export class CandidateEntity extends AggregateRoot { return this; }; + setSteps = (steps: StepProps[]): CandidateEntity => { + this.props.steps = steps; + return this; + }; + isDetourValid = (): boolean => this._validateDistanceDetour() && this._validateDurationDetour(); + createJourney = (): CandidateEntity => this; + private _validateDurationDetour = (): boolean => this.props.duration ? this.props.duration <= diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index c387860..3a68d06 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -1,7 +1,9 @@ import { Role } from './ad.types'; import { PointProps } from './value-objects/point.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 export interface CandidateProps { @@ -10,11 +12,13 @@ export interface CandidateProps { passengerWaypoints: PointProps[]; driverDistance: number; driverDuration: number; - carpoolSteps?: WayStepProps[]; // carpool path for the crew (driver + passenger) + carpoolSteps?: CarpoolStepProps[]; distance?: number; duration?: number; + steps?: StepProps[]; driverSchedule: ScheduleItemProps[]; passengerSchedule: ScheduleItemProps[]; + journeys?: JourneyProps[]; spacetimeDetourRatio: SpacetimeDetourRatio; } diff --git a/src/modules/ad/core/domain/carpool-path-creator.service.ts b/src/modules/ad/core/domain/carpool-path-creator.service.ts index bf1f5ae..43f61dd 100644 --- a/src/modules/ad/core/domain/carpool-path-creator.service.ts +++ b/src/modules/ad/core/domain/carpool-path-creator.service.ts @@ -3,7 +3,7 @@ import { Target } from './candidate.types'; import { CarpoolPathCreatorException } from './match.errors'; import { Actor } from './value-objects/actor.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 { 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 of the driver waypoints Inspired by : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment */ - public carpoolPath = (): WayStep[] => + public carpoolPath = (): CarpoolStep[] => this._consolidate( - this._mixedWaysteps(this._driverWaysteps(), this._passengerWaysteps()), + this._mixedCarpoolSteps( + this._driverCarpoolSteps(), + this._passengerCarpoolSteps(), + ), ); - private _mixedWaysteps = ( - driverWaysteps: WayStep[], - passengerWaysteps: WayStep[], - ): WayStep[] => - driverWaysteps.length == 2 - ? this._simpleMixedWaysteps(driverWaysteps, passengerWaysteps) - : this._complexMixedWaysteps(driverWaysteps, passengerWaysteps); + private _mixedCarpoolSteps = ( + driverCarpoolSteps: CarpoolStep[], + passengerCarpoolSteps: CarpoolStep[], + ): CarpoolStep[] => + driverCarpoolSteps.length == 2 + ? this._simpleMixedCarpoolSteps(driverCarpoolSteps, passengerCarpoolSteps) + : this._complexMixedCarpoolSteps( + driverCarpoolSteps, + passengerCarpoolSteps, + ); - private _driverWaysteps = (): WayStep[] => + private _driverCarpoolSteps = (): CarpoolStep[] => this.driverWaypoints.map( (waypoint: Point, index: number) => - new WayStep({ + new CarpoolStep({ point: new Point({ lon: waypoint.lon, 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[] => { - const waysteps: WayStep[] = []; + private _passengerCarpoolSteps = (): CarpoolStep[] => { + const carpoolSteps: CarpoolStep[] = []; this.passengerWaypoints.forEach( (passengerWaypoint: Point, index: number) => { - const waystep: WayStep = new WayStep({ + const carpoolStep: CarpoolStep = new CarpoolStep({ point: new Point({ lon: passengerWaypoint.lon, lat: passengerWaypoint.lat, @@ -83,73 +89,78 @@ export class CarpoolPathCreator { passengerWaypoint.isSame(driverWaypoint), ).length == 0 ) { - waystep.actors.push( + carpoolStep.actors.push( new Actor({ role: Role.DRIVER, target: Target.NEUTRAL, }), ); } - waysteps.push(waystep); + carpoolSteps.push(carpoolStep); }, ); - return waysteps; + return carpoolSteps; }; - private _simpleMixedWaysteps = ( - driverWaysteps: WayStep[], - passengerWaysteps: WayStep[], - ): WayStep[] => [driverWaysteps[0], ...passengerWaysteps, driverWaysteps[1]]; + private _simpleMixedCarpoolSteps = ( + driverCarpoolSteps: CarpoolStep[], + passengerCarpoolSteps: CarpoolStep[], + ): CarpoolStep[] => [ + driverCarpoolSteps[0], + ...passengerCarpoolSteps, + driverCarpoolSteps[1], + ]; - private _complexMixedWaysteps = ( - driverWaysteps: WayStep[], - passengerWaysteps: WayStep[], - ): WayStep[] => { - let mixedWaysteps: WayStep[] = [...driverWaysteps]; + private _complexMixedCarpoolSteps = ( + driverCarpoolSteps: CarpoolStep[], + passengerCarpoolSteps: CarpoolStep[], + ): CarpoolStep[] => { + let mixedCarpoolSteps: CarpoolStep[] = [...driverCarpoolSteps]; const originInsertIndex: number = this._insertIndex( - passengerWaysteps[0], - driverWaysteps, + passengerCarpoolSteps[0], + driverCarpoolSteps, ); - mixedWaysteps = [ - ...mixedWaysteps.slice(0, originInsertIndex), - passengerWaysteps[0], - ...mixedWaysteps.slice(originInsertIndex), + mixedCarpoolSteps = [ + ...mixedCarpoolSteps.slice(0, originInsertIndex), + passengerCarpoolSteps[0], + ...mixedCarpoolSteps.slice(originInsertIndex), ]; const destinationInsertIndex: number = this._insertIndex( - passengerWaysteps[passengerWaysteps.length - 1], - driverWaysteps, + passengerCarpoolSteps[passengerCarpoolSteps.length - 1], + driverCarpoolSteps, ) + 1; - mixedWaysteps = [ - ...mixedWaysteps.slice(0, destinationInsertIndex), - passengerWaysteps[passengerWaysteps.length - 1], - ...mixedWaysteps.slice(destinationInsertIndex), + mixedCarpoolSteps = [ + ...mixedCarpoolSteps.slice(0, destinationInsertIndex), + passengerCarpoolSteps[passengerCarpoolSteps.length - 1], + ...mixedCarpoolSteps.slice(destinationInsertIndex), ]; - return mixedWaysteps; + return mixedCarpoolSteps; }; private _insertIndex = ( - targetWaystep: WayStep, - waysteps: WayStep[], + targetCarpoolStep: CarpoolStep, + carpoolSteps: CarpoolStep[], ): number => - this._closestSegmentIndex(targetWaystep, this._segments(waysteps)) + 1; + this._closestSegmentIndex(targetCarpoolStep, this._segments(carpoolSteps)) + + 1; - private _segments = (waysteps: WayStep[]): WayStep[][] => { - const segments: WayStep[][] = []; - waysteps.forEach((waystep: WayStep, index: number) => { - if (index < waysteps.length - 1) - segments.push([waystep, waysteps[index + 1]]); + private _segments = (carpoolSteps: CarpoolStep[]): CarpoolStep[][] => { + const segments: CarpoolStep[][] = []; + carpoolSteps.forEach((carpoolStep: CarpoolStep, index: number) => { + if (index < carpoolSteps.length - 1) + segments.push([carpoolStep, carpoolSteps[index + 1]]); }); return segments; }; private _closestSegmentIndex = ( - waystep: WayStep, - segments: WayStep[][], + carpoolStep: CarpoolStep, + segments: CarpoolStep[][], ): number => { const distances: Map = new Map(); - segments.forEach((segment: WayStep[], index: number) => { - distances.set(index, this._distanceToSegment(waystep, segment)); + segments.forEach((segment: CarpoolStep[], index: number) => { + distances.set(index, this._distanceToSegment(carpoolStep, segment)); }); const sortedDistances: Map = new Map( [...distances.entries()].sort((a, b) => a[1] - b[1]), @@ -158,30 +169,33 @@ export class CarpoolPathCreator { return closestSegmentIndex; }; - private _distanceToSegment = (waystep: WayStep, segment: WayStep[]): number => + private _distanceToSegment = ( + carpoolStep: CarpoolStep, + segment: CarpoolStep[], + ): number => parseFloat( - Math.sqrt(this._distanceToSegmentSquared(waystep, segment)).toFixed( + Math.sqrt(this._distanceToSegmentSquared(carpoolStep, segment)).toFixed( this.PRECISION, ), ); private _distanceToSegmentSquared = ( - waystep: WayStep, - segment: WayStep[], + carpoolStep: CarpoolStep, + segment: CarpoolStep[], ): number => { const length2: number = this._distanceSquared( segment[0].point, segment[1].point, ); if (length2 == 0) - return this._distanceSquared(waystep.point, segment[0].point); + return this._distanceSquared(carpoolStep.point, segment[0].point); const length: number = Math.max( 0, Math.min( 1, - ((waystep.point.lon - segment[0].point.lon) * + ((carpoolStep.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)) / length2, ), @@ -194,7 +208,7 @@ export class CarpoolPathCreator { 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 => @@ -213,29 +227,31 @@ export class CarpoolPathCreator { : 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[] = []; - waysteps.forEach((waystep: WayStep) => { + carpoolSteps.forEach((carpoolStep: CarpoolStep) => { if ( - uniquePoints.find((point: Point) => point.isSame(waystep.point)) === + uniquePoints.find((point: Point) => point.isSame(carpoolStep.point)) === undefined ) uniquePoints.push( new Point({ - lon: waystep.point.lon, - lat: waystep.point.lat, + lon: carpoolStep.point.lon, + lat: carpoolStep.point.lat, }), ); }); return uniquePoints.map( (point: Point) => - new WayStep({ + new CarpoolStep({ point, - actors: waysteps - .filter((waystep: WayStep) => waystep.point.isSame(point)) - .map((waystep: WayStep) => waystep.actors) + actors: carpoolSteps + .filter((carpoolStep: CarpoolStep) => + carpoolStep.point.isSame(point), + ) + .map((carpoolStep: CarpoolStep) => carpoolStep.actors) .flat(), }), ); diff --git a/src/modules/ad/core/domain/path-creator.service.ts b/src/modules/ad/core/domain/path-creator.service.ts index 36114b6..ed34c0c 100644 --- a/src/modules/ad/core/domain/path-creator.service.ts +++ b/src/modules/ad/core/domain/path-creator.service.ts @@ -1,7 +1,7 @@ -import { Route } from '@modules/geography/core/domain/route.types'; import { Role } from './ad.types'; import { Point } from './value-objects/point.value-object'; import { PathCreatorException } from './match.errors'; +import { Route } from '../application/types/route.type'; export class PathCreator { constructor( diff --git a/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts b/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts new file mode 100644 index 0000000..100532c --- /dev/null +++ b/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts @@ -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 { + 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`); + } +} diff --git a/src/modules/ad/core/domain/value-objects/waystep.value-object.ts b/src/modules/ad/core/domain/value-objects/carpool-step.value-object.ts similarity index 78% rename from src/modules/ad/core/domain/value-objects/waystep.value-object.ts rename to src/modules/ad/core/domain/value-objects/carpool-step.value-object.ts index bfc1f52..95382cf 100644 --- a/src/modules/ad/core/domain/value-objects/waystep.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/carpool-step.value-object.ts @@ -11,12 +11,12 @@ import { Point } from './point.value-object'; * other Value Objects inside if needed. * */ -export interface WayStepProps { +export interface CarpoolStepProps { point: Point; actors: Actor[]; } -export class WayStep extends ValueObject { +export class CarpoolStep extends ValueObject { get point(): Point { return this.props.point; } @@ -25,7 +25,7 @@ export class WayStep extends ValueObject { return this.props.actors; } - protected validate(props: WayStepProps): void { + protected validate(props: CarpoolStepProps): void { if (props.actors.length <= 0) throw new ArgumentOutOfRangeException('at least one actor is required'); if ( @@ -33,7 +33,7 @@ export class WayStep extends ValueObject { 1 ) throw new ArgumentOutOfRangeException( - 'a waystep can contain only one driver', + 'a carpoolStep can contain only one driver', ); } } diff --git a/src/modules/ad/core/domain/value-objects/journey.value-object.ts b/src/modules/ad/core/domain/value-objects/journey.value-object.ts new file mode 100644 index 0000000..44bd626 --- /dev/null +++ b/src/modules/ad/core/domain/value-objects/journey.value-object.ts @@ -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 { + 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', + ); + } +} diff --git a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts index 97fb87f..eb32016 100644 --- a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts @@ -28,7 +28,6 @@ export class ScheduleItem extends ValueObject { return this.props.margin; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars protected validate(props: ScheduleItemProps): void { if (props.day < 0 || props.day > 6) throw new ArgumentOutOfRangeException('day must be between 0 and 6'); diff --git a/src/modules/ad/core/domain/value-objects/step.value-object.ts b/src/modules/ad/core/domain/value-objects/step.value-object.ts new file mode 100644 index 0000000..0c6d8df --- /dev/null +++ b/src/modules/ad/core/domain/value-objects/step.value-object.ts @@ -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 { + 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', + ); + } +} diff --git a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts index 0c49df7..b9a8294 100644 --- a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts @@ -1,7 +1,7 @@ import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service'; import { CarpoolPathCreatorException } from '@modules/ad/core/domain/match.errors'; 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({ lat: 0, @@ -34,71 +34,71 @@ describe('Carpool Path Creator Service', () => { [waypoint1, waypoint6], [waypoint2, waypoint5], ); - const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - expect(waysteps).toHaveLength(4); - expect(waysteps[0].actors.length).toBe(1); + const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); + expect(carpoolSteps).toHaveLength(4); + expect(carpoolSteps[0].actors.length).toBe(1); }); it('should create a simple carpool path with same destination for driver and passenger', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint6], [waypoint2, waypoint6], ); - const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - expect(waysteps).toHaveLength(3); - expect(waysteps[0].actors.length).toBe(1); - expect(waysteps[1].actors.length).toBe(2); - expect(waysteps[2].actors.length).toBe(2); + const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); + expect(carpoolSteps).toHaveLength(3); + expect(carpoolSteps[0].actors.length).toBe(1); + expect(carpoolSteps[1].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', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint6], [waypoint1, waypoint6], ); - const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - expect(waysteps).toHaveLength(2); - expect(waysteps[0].actors.length).toBe(2); - expect(waysteps[1].actors.length).toBe(2); + const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); + expect(carpoolSteps).toHaveLength(2); + expect(carpoolSteps[0].actors.length).toBe(2); + expect(carpoolSteps[1].actors.length).toBe(2); }); it('should create a complex carpool path with 3 driver waypoints', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint3, waypoint6], [waypoint2, waypoint5], ); - const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - expect(waysteps).toHaveLength(5); - expect(waysteps[0].actors.length).toBe(1); - expect(waysteps[1].actors.length).toBe(2); - expect(waysteps[2].actors.length).toBe(1); - expect(waysteps[3].actors.length).toBe(2); - expect(waysteps[4].actors.length).toBe(1); + const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); + expect(carpoolSteps).toHaveLength(5); + expect(carpoolSteps[0].actors.length).toBe(1); + expect(carpoolSteps[1].actors.length).toBe(2); + expect(carpoolSteps[2].actors.length).toBe(1); + expect(carpoolSteps[3].actors.length).toBe(2); + expect(carpoolSteps[4].actors.length).toBe(1); }); it('should create a complex carpool path with 4 driver waypoints', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint3, waypoint4, waypoint6], [waypoint2, waypoint5], ); - const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - expect(waysteps).toHaveLength(6); - expect(waysteps[0].actors.length).toBe(1); - expect(waysteps[1].actors.length).toBe(2); - expect(waysteps[2].actors.length).toBe(1); - expect(waysteps[3].actors.length).toBe(1); - expect(waysteps[4].actors.length).toBe(2); - expect(waysteps[5].actors.length).toBe(1); + const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); + expect(carpoolSteps).toHaveLength(6); + expect(carpoolSteps[0].actors.length).toBe(1); + expect(carpoolSteps[1].actors.length).toBe(2); + expect(carpoolSteps[2].actors.length).toBe(1); + expect(carpoolSteps[3].actors.length).toBe(1); + expect(carpoolSteps[4].actors.length).toBe(2); + expect(carpoolSteps[5].actors.length).toBe(1); }); it('should create a alternate complex carpool path with 4 driver waypoints', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint2, waypoint5, waypoint6], [waypoint3, waypoint4], ); - const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - expect(waysteps).toHaveLength(6); - expect(waysteps[0].actors.length).toBe(1); - expect(waysteps[1].actors.length).toBe(1); - expect(waysteps[2].actors.length).toBe(2); - expect(waysteps[3].actors.length).toBe(2); - expect(waysteps[4].actors.length).toBe(1); - expect(waysteps[5].actors.length).toBe(1); + const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); + expect(carpoolSteps).toHaveLength(6); + expect(carpoolSteps[0].actors.length).toBe(1); + expect(carpoolSteps[1].actors.length).toBe(1); + expect(carpoolSteps[2].actors.length).toBe(2); + expect(carpoolSteps[3].actors.length).toBe(2); + expect(carpoolSteps[4].actors.length).toBe(1); + expect(carpoolSteps[5].actors.length).toBe(1); }); it('should throw an exception if less than 2 driver waypoints are given', () => { try { diff --git a/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts b/src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts similarity index 76% rename from src/modules/ad/tests/unit/core/waystep.value-object.spec.ts rename to src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts index c71f530..cf65a5c 100644 --- a/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts @@ -3,11 +3,11 @@ import { Role } from '@modules/ad/core/domain/ad.types'; 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'; -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', () => { - it('should create a waystep value object', () => { - const wayStepVO = new WayStep({ +describe('CarpoolStep value object', () => { + it('should create a carpoolStep value object', () => { + const carpoolStepVO = new CarpoolStep({ point: new Point({ lat: 48.689445, lon: 6.17651, @@ -23,13 +23,13 @@ describe('WayStep value object', () => { }), ], }); - expect(wayStepVO.point.lon).toBe(6.17651); - expect(wayStepVO.point.lat).toBe(48.689445); - expect(wayStepVO.actors).toHaveLength(2); + expect(carpoolStepVO.point.lon).toBe(6.17651); + expect(carpoolStepVO.point.lat).toBe(48.689445); + expect(carpoolStepVO.actors).toHaveLength(2); }); it('should throw an exception if actors is empty', () => { try { - new WayStep({ + new CarpoolStep({ point: new Point({ lat: 48.689445, lon: 6.17651, @@ -42,7 +42,7 @@ describe('WayStep value object', () => { }); it('should throw an exception if actors contains more than one driver', () => { try { - new WayStep({ + new CarpoolStep({ point: new Point({ lat: 48.689445, lon: 6.17651, diff --git a/src/modules/ad/tests/unit/core/journey.completer.spec.ts b/src/modules/ad/tests/unit/core/journey.completer.spec.ts new file mode 100644 index 0000000..5adbeaa --- /dev/null +++ b/src/modules/ad/tests/unit/core/journey.completer.spec.ts @@ -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); + }); +}); diff --git a/src/modules/ad/tests/unit/core/route.completer.spec.ts b/src/modules/ad/tests/unit/core/route.completer.spec.ts index b64cdf1..80291da 100644 --- a/src/modules/ad/tests/unit/core/route.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/route.completer.spec.ts @@ -56,12 +56,13 @@ const matchQuery = new MatchQuery( points: [], })), getDetailed: jest.fn().mockImplementation(() => ({ - distance: 350102, - duration: 14423, + distance: 350101, + duration: 14422, fwdAzimuth: 273, backAzimuth: 93, distanceAzimuth: 336544, points: [], + steps: [jest.fn(), jest.fn(), jest.fn(), jest.fn()], })), }, ); @@ -163,6 +164,6 @@ describe('Route completer', () => { const completedCandidates: CandidateEntity[] = await routeCompleter.complete([candidate]); expect(completedCandidates.length).toBe(1); - expect(completedCandidates[0].getProps().distance).toBe(350102); + expect(completedCandidates[0].getProps().steps).toHaveLength(4); }); }); diff --git a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts index 6e8ec9a..9d2bf51 100644 --- a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts @@ -3,9 +3,9 @@ import { AD_GET_DETAILED_ROUTE_CONTROLLER, } from '@modules/ad/ad.di-tokens'; 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 { 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'; const originPoint: Point = {