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());
|
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;
|
|
||||||
// }
|
|
||||||
|
|
|
@ -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({
|
||||||
lon: waypoint.lon,
|
point: new Point({
|
||||||
lat: waypoint.lat,
|
lon: waypoint.lon,
|
||||||
|
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({
|
||||||
lon: passengerWaypoint.lon,
|
point: new Point({
|
||||||
lat: passengerWaypoint.lat,
|
lon: passengerWaypoint.lon,
|
||||||
|
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(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[] {
|
||||||
|
|
|
@ -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 { 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({
|
||||||
lat: 48.689445,
|
point: new Point({
|
||||||
lon: 6.17651,
|
lat: 48.689445,
|
||||||
|
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({
|
||||||
lat: 48.689445,
|
point: new Point({
|
||||||
lon: 6.17651,
|
lat: 48.689445,
|
||||||
|
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({
|
||||||
lat: 48.689445,
|
point: new Point({
|
||||||
lon: 6.17651,
|
lat: 48.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
}),
|
||||||
actors: [
|
actors: [
|
||||||
new Actor({
|
new Actor({
|
||||||
role: Role.DRIVER,
|
role: Role.DRIVER,
|
||||||
|
|
Loading…
Reference in New Issue