diff --git a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts index ccba94c..201c5ac 100644 --- a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts @@ -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 => { -// candidates.forEach( (candidate: Candidate) => { -// if (candidate.role == Role.DRIVER) { -// candidate.driverWaypoints = th -// } - -// return candidates; -// } diff --git a/src/modules/ad/core/domain/carpool-path-creator.service.ts b/src/modules/ad/core/domain/carpool-path-creator.service.ts index aa615a2..252a503 100644 --- a/src/modules/ad/core/domain/carpool-path-creator.service.ts +++ b/src/modules/ad/core/domain/carpool-path-creator.service.ts @@ -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 = new Map(); + segments.forEach((segment: WayStep[], index: number) => { + distances.set(index, this._distanceToSegment(waystep, segment)); + }); + const sortedDistances: Map = 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(), + }), + ); + }; } diff --git a/src/modules/ad/core/domain/value-objects/waystep.value-object.ts b/src/modules/ad/core/domain/value-objects/waystep.value-object.ts index 7797525..bfc1f52 100644 --- a/src/modules/ad/core/domain/value-objects/waystep.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/waystep.value-object.ts @@ -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 { - get lon(): number { - return this.props.lon; - } - - get lat(): number { - return this.props.lat; + get point(): Point { + return this.props.point; } get actors(): Actor[] { diff --git a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts new file mode 100644 index 0000000..4379d95 --- /dev/null +++ b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts @@ -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); + }); +}); diff --git a/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts b/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts index 5791bd3..c71f530 100644 --- a/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts @@ -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,