import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; import { CandidateProps, CreateCandidateProps, Target, } from './candidate.types'; import { CarpoolPathItem, CarpoolPathItemProps, } from './value-objects/carpool-path-item.value-object'; import { Step, StepProps } from './value-objects/step.value-object'; import { ScheduleItem } from './value-objects/schedule-item.value-object'; import { Journey } from './value-objects/journey.value-object'; import { CalendarTools } from './calendar-tools.service'; import { JourneyItem } from './value-objects/journey-item.value-object'; import { Actor } from './value-objects/actor.value-object'; import { ActorTime } from './value-objects/actor-time.value-object'; import { Role } from './ad.types'; export class CandidateEntity extends AggregateRoot { protected readonly _id: AggregateID; static create = (create: CreateCandidateProps): CandidateEntity => { const props: CandidateProps = { ...create }; return new CandidateEntity({ id: create.id, props }); }; setCarpoolPath = (carpoolPath: CarpoolPathItemProps[]): CandidateEntity => { this.props.carpoolPath = carpoolPath; return this; }; setMetrics = (distance: number, duration: number): CandidateEntity => { this.props.distance = distance; this.props.duration = duration; return this; }; setSteps = (steps: StepProps[]): CandidateEntity => { this.props.steps = steps; return this; }; isDetourValid = (): boolean => this._validateDistanceDetour() && this._validateDurationDetour(); hasJourneys = (): boolean => this.getProps().journeys !== undefined && (this.getProps().journeys as Journey[]).length > 0; /** * Create the journeys based on the driver schedule (the driver 'drives' the carpool !) * This is a tedious process : additional information can be found in deeper methods ! */ createJourneys = (): CandidateEntity => { try { this.props.journeys = this.props.driverSchedule // first we create the journeys .map((driverScheduleItem: ScheduleItem) => this._createJourney(driverScheduleItem), ) // then we filter the ones with invalid pickups .filter((journey: Journey) => journey.hasValidPickUp()); } catch (e) { // irrelevant journeys fall here // eg. no available day for the given date range } return this; }; private _validateDurationDetour = (): boolean => this.props.duration ? this.props.duration <= this.props.driverDuration * (1 + this.props.spacetimeDetourRatio.maxDurationDetourRatio) : false; private _validateDistanceDetour = (): boolean => this.props.distance ? this.props.distance <= this.props.driverDistance * (1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio) : false; private _createJourney = (driverScheduleItem: ScheduleItem): Journey => new Journey({ firstDate: CalendarTools.firstDate( driverScheduleItem.day, this.props.dateInterval, ), lastDate: CalendarTools.lastDate( driverScheduleItem.day, this.props.dateInterval, ), journeyItems: this._createJourneyItems(driverScheduleItem), }); private _createJourneyItems = ( driverScheduleItem: ScheduleItem, ): JourneyItem[] => this.props.carpoolPath?.map( (carpoolPathItem: CarpoolPathItem, index: number) => this._createJourneyItem(carpoolPathItem, index, driverScheduleItem), ) as JourneyItem[]; /** * Create a journey item based on a carpool path item and driver schedule item * The stepIndex is used to get the duration to reach the carpool path item * from the steps prop (computed previously by a georouter) * There MUST be a one/one relation between the carpool path items indexes * and the steps indexes. */ private _createJourneyItem = ( carpoolPathItem: CarpoolPathItem, stepIndex: number, driverScheduleItem: ScheduleItem, ): JourneyItem => new JourneyItem({ lon: carpoolPathItem.lon, lat: carpoolPathItem.lat, duration: ((this.props.steps as Step[])[stepIndex] as Step).duration, distance: ((this.props.steps as Step[])[stepIndex] as Step).distance, actorTimes: carpoolPathItem.actors.map((actor: Actor) => this._createActorTime( actor, driverScheduleItem, ((this.props.steps as Step[])[stepIndex] as Step).duration, ), ), }); private _createActorTime = ( actor: Actor, driverScheduleItem: ScheduleItem, duration: number, ): ActorTime => { const scheduleItem: ScheduleItem = actor.role == Role.PASSENGER && actor.target == Target.START ? this._closestPassengerScheduleItem(driverScheduleItem) : driverScheduleItem; const effectiveDuration = (actor.role == Role.PASSENGER && actor.target == Target.START) || actor.target == Target.START ? 0 : duration; const firstDate: Date = CalendarTools.firstDate( scheduleItem.day, this.props.dateInterval, ); const lastDate: Date = CalendarTools.lastDate( scheduleItem.day, this.props.dateInterval, ); return new ActorTime({ role: actor.role, target: actor.target, firstDatetime: CalendarTools.datetimeWithSeconds( firstDate, scheduleItem.time, effectiveDuration, ), firstMinDatetime: CalendarTools.datetimeWithSeconds( firstDate, scheduleItem.time, -scheduleItem.margin + effectiveDuration, ), firstMaxDatetime: CalendarTools.datetimeWithSeconds( firstDate, scheduleItem.time, scheduleItem.margin + effectiveDuration, ), lastDatetime: CalendarTools.datetimeWithSeconds( lastDate, scheduleItem.time, effectiveDuration, ), lastMinDatetime: CalendarTools.datetimeWithSeconds( lastDate, scheduleItem.time, -scheduleItem.margin + effectiveDuration, ), lastMaxDatetime: CalendarTools.datetimeWithSeconds( lastDate, scheduleItem.time, scheduleItem.margin + effectiveDuration, ), }); }; /** * Get the closest (in time) passenger schedule item for a given driver schedule item * This is mandatory as we can't rely only on the day of the schedule item : * items on different days can match when playing with margins around midnight */ private _closestPassengerScheduleItem = ( driverScheduleItem: ScheduleItem, ): ScheduleItem => CalendarTools.epochDaysFromTime( driverScheduleItem.day, driverScheduleItem.time, ) .map((driverDate: Date) => this._minPassengerScheduleItemGapForDate(driverDate), ) .reduce( ( previousScheduleItemGap: ScheduleItemGap, currentScheduleItemGap: ScheduleItemGap, ) => previousScheduleItemGap.gap < currentScheduleItemGap.gap ? previousScheduleItemGap : currentScheduleItemGap, ).scheduleItem; /** * Find the passenger schedule item with the minimum duration between a given date and the dates of the passenger schedule */ private _minPassengerScheduleItemGapForDate = (date: Date): ScheduleItemGap => this.props.passengerSchedule // first map the passenger schedule to "real" dates (we use unix epoch date as base) .map( (scheduleItem: ScheduleItem) => { scheduleItem, range: CalendarTools.epochDaysFromTime( scheduleItem.day, scheduleItem.time, ), }, ) // then compute the duration in seconds to the given date // for each "real" date computed in step 1 .map((scheduleItemRange: ScheduleItemRange) => ({ scheduleItem: scheduleItemRange.scheduleItem, gap: scheduleItemRange.range // compute the duration .map((scheduleDate: Date) => Math.round(Math.abs(scheduleDate.getTime() - date.getTime())), ) // keep the lowest duration .reduce((previousGap: number, currentGap: number) => previousGap < currentGap ? previousGap : currentGap, ), })) // finally, keep the passenger schedule item with the lowest duration .reduce( ( previousScheduleItemGap: ScheduleItemGap, currentScheduleItemGap: ScheduleItemGap, ) => previousScheduleItemGap.gap < currentScheduleItemGap.gap ? previousScheduleItemGap : currentScheduleItemGap, ); validate(): void { // entity business rules validation to protect it's invariant before saving entity to a database } } type ScheduleItemRange = { scheduleItem: ScheduleItem; range: Date[]; }; type ScheduleItemGap = { scheduleItem: ScheduleItem; gap: number; };