carpool path creator

This commit is contained in:
sbriat 2023-09-15 17:02:52 +02:00
parent a7c73080a7
commit 37fd74d6d3
5 changed files with 291 additions and 45 deletions

View File

@ -48,18 +48,9 @@ export class PassengerOrientedCarpoolPathCompleter extends Completer {
}), }),
), ),
); );
candidate.setCarpoolPath(carpoolPathCreator.createCarpoolPath()); candidate.setCarpoolPath(carpoolPathCreator.carpoolPath());
console.log(JSON.stringify(candidate, null, 2)); // console.log(JSON.stringify(candidate, null, 2));
}); });
return candidates; return candidates;
}; };
} }
// complete = async (candidates: Candidate[]): Promise<Candidate[]> => {
// candidates.forEach( (candidate: Candidate) => {
// if (candidate.role == Role.DRIVER) {
// candidate.driverWaypoints = th
// }
// return candidates;
// }

View File

@ -5,23 +5,41 @@ import { Point } from './value-objects/point.value-object';
import { WayStep } from './value-objects/waystep.value-object'; import { WayStep } from './value-objects/waystep.value-object';
export class CarpoolPathCreator { export class CarpoolPathCreator {
private PRECISION = 5;
constructor( constructor(
private readonly driverWaypoints: Point[], private readonly driverWaypoints: Point[],
private readonly passengerWaypoints: Point[], private readonly passengerWaypoints: Point[],
) {} ) {}
public createCarpoolPath = (): WayStep[] => /**
this._createMixedWaysteps( * Creates a path (a list of waysteps) between driver waypoints
this._createDriverWaysteps(), and passenger waypoints respecting the order
this._createPassengerWaysteps(), of the driver waypoints
Inspired by :
https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
*/
public carpoolPath = (): WayStep[] =>
this._consolidate(
this._mixedWaysteps(this._driverWaysteps(), this._passengerWaysteps()),
); );
private _createDriverWaysteps = (): WayStep[] => private _mixedWaysteps = (
driverWaysteps: WayStep[],
passengerWaysteps: WayStep[],
): WayStep[] =>
driverWaysteps.length == 2
? this._simpleMixedWaysteps(driverWaysteps, passengerWaysteps)
: this._complexMixedWaysteps(driverWaysteps, passengerWaysteps);
private _driverWaysteps = (): WayStep[] =>
this.driverWaypoints.map( this.driverWaypoints.map(
(waypoint: Point, index: number) => (waypoint: Point, index: number) =>
new WayStep({ new WayStep({
point: new Point({
lon: waypoint.lon, lon: waypoint.lon,
lat: waypoint.lat, lat: waypoint.lat,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,
@ -31,13 +49,18 @@ export class CarpoolPathCreator {
}), }),
); );
private _createPassengerWaysteps = (): WayStep[] => { /**
* Creates the passenger waysteps with original passenger waypoints, adding driver waypoints that are the same
*/
private _passengerWaysteps = (): WayStep[] => {
const waysteps: WayStep[] = []; const waysteps: WayStep[] = [];
this.passengerWaypoints.forEach( this.passengerWaypoints.forEach(
(passengerWaypoint: Point, index: number) => { (passengerWaypoint: Point, index: number) => {
const waystep: WayStep = new WayStep({ const waystep: WayStep = new WayStep({
point: new Point({
lon: passengerWaypoint.lon, lon: passengerWaypoint.lon,
lat: passengerWaypoint.lat, lat: passengerWaypoint.lat,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.PASSENGER, role: Role.PASSENGER,
@ -48,7 +71,7 @@ export class CarpoolPathCreator {
if ( if (
this.driverWaypoints.filter((driverWaypoint: Point) => this.driverWaypoints.filter((driverWaypoint: Point) =>
passengerWaypoint.isSame(driverWaypoint), passengerWaypoint.isSame(driverWaypoint),
).length > 0 ).length == 0
) { ) {
waystep.actors.push( waystep.actors.push(
new Actor({ new Actor({
@ -63,18 +86,114 @@ export class CarpoolPathCreator {
return waysteps; return waysteps;
}; };
private _createMixedWaysteps = ( private _simpleMixedWaysteps = (
driverWaysteps: WayStep[], driverWaysteps: WayStep[],
passengerWaysteps: WayStep[], passengerWaysteps: WayStep[],
): WayStep[] => ): WayStep[] => [driverWaysteps[0], ...passengerWaysteps, driverWaysteps[1]];
driverWaysteps.length == 2
? [driverWaysteps[0], ...passengerWaysteps, driverWaysteps[1]]
: this._createComplexMixedWaysteps(driverWaysteps, passengerWaysteps);
private _createComplexMixedWaysteps = ( private _complexMixedWaysteps = (
driverWaysteps: WayStep[], driverWaysteps: WayStep[],
passengerWaysteps: WayStep[], passengerWaysteps: WayStep[],
): WayStep[] => []; ): WayStep[] => {
let mixedWaysteps: WayStep[] = [...driverWaysteps];
const originInsertIndex: number = this._insertIndex(
passengerWaysteps[0],
driverWaysteps,
);
mixedWaysteps = [
...mixedWaysteps.slice(0, originInsertIndex),
passengerWaysteps[0],
...mixedWaysteps.slice(originInsertIndex),
];
const destinationInsertIndex: number =
this._insertIndex(
passengerWaysteps[passengerWaysteps.length - 1],
driverWaysteps,
) + 1;
mixedWaysteps = [
...mixedWaysteps.slice(0, destinationInsertIndex),
passengerWaysteps[passengerWaysteps.length - 1],
...mixedWaysteps.slice(destinationInsertIndex),
];
return mixedWaysteps;
};
private _insertIndex = (
targetWaystep: WayStep,
waysteps: WayStep[],
): number =>
this._closestSegmentIndex(targetWaystep, this._segments(waysteps)) + 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]]);
});
return segments;
};
private _closestSegmentIndex = (
waystep: WayStep,
segments: WayStep[][],
): number => {
const distances: Map<number, number> = new Map();
segments.forEach((segment: WayStep[], index: number) => {
distances.set(index, this._distanceToSegment(waystep, segment));
});
const sortedDistances: Map<number, number> = new Map(
[...distances.entries()].sort((a, b) => a[1] - b[1]),
);
const [closestSegmentIndex] = sortedDistances.keys();
return closestSegmentIndex;
};
private _distanceToSegment = (waystep: WayStep, segment: WayStep[]): number =>
parseFloat(
Math.sqrt(this._distanceToSegmentSquared(waystep, segment)).toFixed(
this.PRECISION,
),
);
private _distanceToSegmentSquared = (
waystep: WayStep,
segment: WayStep[],
): number => {
const length2: number = this._distanceSquared(
segment[0].point,
segment[1].point,
);
if (length2 == 0)
return this._distanceSquared(waystep.point, segment[0].point);
const length: number = Math.max(
0,
Math.min(
1,
((waystep.point.lon - segment[0].point.lon) *
(segment[1].point.lon - segment[0].point.lon) +
(waystep.point.lat - segment[0].point.lat) *
(segment[1].point.lat - segment[0].point.lat)) /
length2,
),
);
const newPoint: Point = new Point({
lon:
segment[0].point.lon +
length * (segment[1].point.lon - segment[0].point.lon),
lat:
segment[0].point.lat +
length * (segment[1].point.lat - segment[0].point.lat),
});
return this._distanceSquared(waystep.point, newPoint);
};
private _distanceSquared = (point1: Point, point2: Point): number =>
parseFloat(
(
Math.pow(point1.lon - point2.lon, 2) +
Math.pow(point1.lat - point2.lat, 2)
).toFixed(this.PRECISION),
);
private _getTarget = (index: number, waypoints: Point[]): Target => private _getTarget = (index: number, waypoints: Point[]): Target =>
index == 0 index == 0
@ -82,4 +201,33 @@ export class CarpoolPathCreator {
: index == waypoints.length - 1 : index == waypoints.length - 1
? Target.FINISH ? Target.FINISH
: Target.INTERMEDIATE; : Target.INTERMEDIATE;
/**
* Consolidate waysteps by removing duplicate actors (eg. driver with neutral and start or finish target)
*/
private _consolidate = (waysteps: WayStep[]): WayStep[] => {
const uniquePoints: Point[] = [];
waysteps.forEach((waystep: WayStep) => {
if (
uniquePoints.find((point: Point) => point.isSame(waystep.point)) ===
undefined
)
uniquePoints.push(
new Point({
lon: waystep.point.lon,
lat: waystep.point.lat,
}),
);
});
return uniquePoints.map(
(point: Point) =>
new WayStep({
point,
actors: waysteps
.filter((waystep: WayStep) => waystep.point.isSame(point))
.map((waystep: WayStep) => waystep.actors)
.flat(),
}),
);
};
} }

View File

@ -4,24 +4,21 @@ import {
} from '@mobicoop/ddd-library'; } from '@mobicoop/ddd-library';
import { Actor } from './actor.value-object'; import { Actor } from './actor.value-object';
import { Role } from '../ad.types'; import { Role } from '../ad.types';
import { PointProps } from './point.value-object'; import { Point } from './point.value-object';
/** Note: /** Note:
* Value Objects with multiple properties can contain * Value Objects with multiple properties can contain
* other Value Objects inside if needed. * other Value Objects inside if needed.
* */ * */
export interface WayStepProps extends PointProps { export interface WayStepProps {
point: Point;
actors: Actor[]; actors: Actor[];
} }
export class WayStep extends ValueObject<WayStepProps> { export class WayStep extends ValueObject<WayStepProps> {
get lon(): number { get point(): Point {
return this.props.lon; return this.props.point;
}
get lat(): number {
return this.props.lat;
} }
get actors(): Actor[] { get actors(): Actor[] {

View File

@ -0,0 +1,103 @@
import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service';
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object';
const waypoint1: Point = new Point({
lat: 0,
lon: 0,
});
const waypoint2: Point = new Point({
lat: 2,
lon: 2,
});
const waypoint3: Point = new Point({
lat: 5,
lon: 5,
});
const waypoint4: Point = new Point({
lat: 6,
lon: 6,
});
const waypoint5: Point = new Point({
lat: 8,
lon: 8,
});
const waypoint6: Point = new Point({
lat: 10,
lon: 10,
});
describe('Carpool Path Creator Service', () => {
it('should create a simple carpool path', () => {
const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
[waypoint1, waypoint6],
[waypoint2, waypoint5],
);
const waysteps: WayStep[] = carpoolPathCreator.carpoolPath();
expect(waysteps).toHaveLength(4);
expect(waysteps[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);
});
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);
});
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);
});
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);
});
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();
// console.log(JSON.stringify(waysteps, null, 2));
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);
});
});

View File

@ -2,13 +2,16 @@ import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library';
import { Role } from '@modules/ad/core/domain/ad.types'; 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 { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object';
describe('WayStep value object', () => { describe('WayStep value object', () => {
it('should create a waystep value object', () => { it('should create a waystep value object', () => {
const wayStepVO = new WayStep({ const wayStepVO = new WayStep({
point: new Point({
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,
@ -20,15 +23,17 @@ describe('WayStep value object', () => {
}), }),
], ],
}); });
expect(wayStepVO.lon).toBe(6.17651); expect(wayStepVO.point.lon).toBe(6.17651);
expect(wayStepVO.lat).toBe(48.689445); expect(wayStepVO.point.lat).toBe(48.689445);
expect(wayStepVO.actors).toHaveLength(2); expect(wayStepVO.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 WayStep({
point: new Point({
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,
}),
actors: [], actors: [],
}); });
} catch (e: any) { } catch (e: any) {
@ -38,8 +43,10 @@ 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 WayStep({
point: new Point({
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,
}),
actors: [ actors: [
new Actor({ new Actor({
role: Role.DRIVER, role: Role.DRIVER,