265 lines
8.7 KiB
TypeScript
265 lines
8.7 KiB
TypeScript
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<CandidateProps> {
|
|
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 => {
|
|
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());
|
|
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) =>
|
|
<ScheduleItemRange>{
|
|
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;
|
|
};
|