carpool path creator
This commit is contained in:
parent
a7c73080a7
commit
37fd74d6d3
|
@ -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;
|
||||
// }
|
||||
|
|
|
@ -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({
|
||||
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({
|
||||
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(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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[] {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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({
|
||||
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({
|
||||
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({
|
||||
point: new Point({
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
}),
|
||||
actors: [
|
||||
new Actor({
|
||||
role: Role.DRIVER,
|
||||
|
|
Loading…
Reference in New Issue