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());
console.log(JSON.stringify(candidate, null, 2));
candidate.setCarpoolPath(carpoolPathCreator.carpoolPath());
// console.log(JSON.stringify(candidate, null, 2));
});
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';
export class CarpoolPathCreator {
private PRECISION = 5;
constructor(
private readonly driverWaypoints: Point[],
private readonly passengerWaypoints: Point[],
) {}
public createCarpoolPath = (): WayStep[] =>
this._createMixedWaysteps(
this._createDriverWaysteps(),
this._createPassengerWaysteps(),
/**
* Creates a path (a list of waysteps) 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[] =>
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(
(waypoint: Point, index: number) =>
new WayStep({
lon: waypoint.lon,
lat: waypoint.lat,
point: new Point({
lon: waypoint.lon,
lat: waypoint.lat,
}),
actors: [
new Actor({
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[] = [];
this.passengerWaypoints.forEach(
(passengerWaypoint: Point, index: number) => {
const waystep: WayStep = new WayStep({
lon: passengerWaypoint.lon,
lat: passengerWaypoint.lat,
point: new Point({
lon: passengerWaypoint.lon,
lat: passengerWaypoint.lat,
}),
actors: [
new Actor({
role: Role.PASSENGER,
@ -48,7 +71,7 @@ export class CarpoolPathCreator {
if (
this.driverWaypoints.filter((driverWaypoint: Point) =>
passengerWaypoint.isSame(driverWaypoint),
).length > 0
).length == 0
) {
waystep.actors.push(
new Actor({
@ -63,18 +86,114 @@ export class CarpoolPathCreator {
return waysteps;
};
private _createMixedWaysteps = (
private _simpleMixedWaysteps = (
driverWaysteps: WayStep[],
passengerWaysteps: WayStep[],
): WayStep[] =>
driverWaysteps.length == 2
? [driverWaysteps[0], ...passengerWaysteps, driverWaysteps[1]]
: this._createComplexMixedWaysteps(driverWaysteps, passengerWaysteps);
): WayStep[] => [driverWaysteps[0], ...passengerWaysteps, driverWaysteps[1]];
private _createComplexMixedWaysteps = (
private _complexMixedWaysteps = (
driverWaysteps: 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 =>
index == 0
@ -82,4 +201,33 @@ export class CarpoolPathCreator {
: index == waypoints.length - 1
? Target.FINISH
: 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';
import { Actor } from './actor.value-object';
import { Role } from '../ad.types';
import { PointProps } from './point.value-object';
import { Point } from './point.value-object';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface WayStepProps extends PointProps {
export interface WayStepProps {
point: Point;
actors: Actor[];
}
export class WayStep extends ValueObject<WayStepProps> {
get lon(): number {
return this.props.lon;
}
get lat(): number {
return this.props.lat;
get point(): Point {
return this.props.point;
}
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 { 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';
describe('WayStep value object', () => {
it('should create a waystep value object', () => {
const wayStepVO = new WayStep({
lat: 48.689445,
lon: 6.17651,
point: new Point({
lat: 48.689445,
lon: 6.17651,
}),
actors: [
new Actor({
role: Role.DRIVER,
@ -20,15 +23,17 @@ describe('WayStep value object', () => {
}),
],
});
expect(wayStepVO.lon).toBe(6.17651);
expect(wayStepVO.lat).toBe(48.689445);
expect(wayStepVO.point.lon).toBe(6.17651);
expect(wayStepVO.point.lat).toBe(48.689445);
expect(wayStepVO.actors).toHaveLength(2);
});
it('should throw an exception if actors is empty', () => {
try {
new WayStep({
lat: 48.689445,
lon: 6.17651,
point: new Point({
lat: 48.689445,
lon: 6.17651,
}),
actors: [],
});
} catch (e: any) {
@ -38,8 +43,10 @@ describe('WayStep value object', () => {
it('should throw an exception if actors contains more than one driver', () => {
try {
new WayStep({
lat: 48.689445,
lon: 6.17651,
point: new Point({
lat: 48.689445,
lon: 6.17651,
}),
actors: [
new Actor({
role: Role.DRIVER,