Remove most of the geography module and delegate it to external gRPC microservice
This commit is contained in:
parent
d09bad60f7
commit
96c30cb1cc
|
@ -3,6 +3,8 @@ export const SERVICE_NAME = 'matcher';
|
||||||
|
|
||||||
// grpc
|
// grpc
|
||||||
export const GRPC_PACKAGE_NAME = 'matcher';
|
export const GRPC_PACKAGE_NAME = 'matcher';
|
||||||
|
export const GRPC_GEOGRAPHY_PACKAGE_NAME = 'geocoder';
|
||||||
|
export const GRPC_GEOROUTER_SERVICE_NAME = 'GeorouterService';
|
||||||
|
|
||||||
// messaging output
|
// messaging output
|
||||||
export const MATCHER_AD_CREATED_ROUTING_KEY = 'matcher-ad.created';
|
export const MATCHER_AD_CREATED_ROUTING_KEY = 'matcher-ad.created';
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { MESSAGE_PUBLISHER } from '@modules/messager/messager.di-tokens';
|
||||||
import { HealthModuleOptions } from '@mobicoop/health-module/dist/core/domain/types/health.types';
|
import { HealthModuleOptions } from '@mobicoop/health-module/dist/core/domain/types/health.types';
|
||||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
import { GeographyModule } from '@modules/geography/geography.module';
|
import { GeographyModule } from '@modules/geography/geography.module';
|
||||||
|
import producerServicesConfig from './config/producer-services.config';
|
||||||
import {
|
import {
|
||||||
HEALTH_AD_REPOSITORY,
|
HEALTH_AD_REPOSITORY,
|
||||||
HEALTH_CRITICAL_LOGGING_KEY,
|
HEALTH_CRITICAL_LOGGING_KEY,
|
||||||
|
@ -18,7 +19,7 @@ import {
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true, load: [producerServicesConfig] }),
|
||||||
EventEmitterModule.forRoot(),
|
EventEmitterModule.forRoot(),
|
||||||
RequestContextModule,
|
RequestContextModule,
|
||||||
HealthModule.forRootAsync({
|
HealthModule.forRootAsync({
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('producerServices', () => ({
|
||||||
|
geographyUrl: process.env.GEOGRAPHY_SERVICE_URL ?? 'v3-geocoder-api',
|
||||||
|
geographyPort: process.env.GEOGRAPHY_SERVICE_PORT
|
||||||
|
? parseInt(process.env.GEOGRAPHY_SERVICE_PORT, 10)
|
||||||
|
: 5007,
|
||||||
|
}));
|
|
@ -18,3 +18,4 @@ export const OUTPUT_DATETIME_TRANSFORMER = Symbol(
|
||||||
export const AD_CONFIGURATION_REPOSITORY = Symbol(
|
export const AD_CONFIGURATION_REPOSITORY = Symbol(
|
||||||
'AD_CONFIGURATION_REPOSITORY',
|
'AD_CONFIGURATION_REPOSITORY',
|
||||||
);
|
);
|
||||||
|
export const GEOGRAPHY_PACKAGE = Symbol('GEOGRAPHY_PACKAGE');
|
||||||
|
|
|
@ -5,14 +5,13 @@ import {
|
||||||
AD_REPOSITORY,
|
AD_REPOSITORY,
|
||||||
AD_DIRECTION_ENCODER,
|
AD_DIRECTION_ENCODER,
|
||||||
AD_ROUTE_PROVIDER,
|
AD_ROUTE_PROVIDER,
|
||||||
AD_GET_BASIC_ROUTE_CONTROLLER,
|
|
||||||
TIMEZONE_FINDER,
|
TIMEZONE_FINDER,
|
||||||
TIME_CONVERTER,
|
TIME_CONVERTER,
|
||||||
INPUT_DATETIME_TRANSFORMER,
|
INPUT_DATETIME_TRANSFORMER,
|
||||||
AD_GET_DETAILED_ROUTE_CONTROLLER,
|
|
||||||
OUTPUT_DATETIME_TRANSFORMER,
|
OUTPUT_DATETIME_TRANSFORMER,
|
||||||
MATCHING_REPOSITORY,
|
MATCHING_REPOSITORY,
|
||||||
AD_CONFIGURATION_REPOSITORY,
|
AD_CONFIGURATION_REPOSITORY,
|
||||||
|
GEOGRAPHY_PACKAGE,
|
||||||
} from './ad.di-tokens';
|
} from './ad.di-tokens';
|
||||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||||
import { AdRepository } from './infrastructure/ad.repository';
|
import { AdRepository } from './infrastructure/ad.repository';
|
||||||
|
@ -20,8 +19,6 @@ import { PrismaService } from './infrastructure/prisma.service';
|
||||||
import { AdMapper } from './ad.mapper';
|
import { AdMapper } from './ad.mapper';
|
||||||
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
|
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
|
||||||
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||||
import { GetBasicRouteController } from '@modules/geography/interface/controllers/get-basic-route.controller';
|
|
||||||
import { RouteProvider } from './infrastructure/route-provider';
|
|
||||||
import { GeographyModule } from '@modules/geography/geography.module';
|
import { GeographyModule } from '@modules/geography/geography.module';
|
||||||
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
||||||
import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller';
|
import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller';
|
||||||
|
@ -29,7 +26,6 @@ import { MatchQueryHandler } from './core/application/queries/match/match.query-
|
||||||
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
||||||
import { TimeConverter } from './infrastructure/time-converter';
|
import { TimeConverter } from './infrastructure/time-converter';
|
||||||
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
|
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
|
||||||
import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller';
|
|
||||||
import { MatchMapper } from './match.mapper';
|
import { MatchMapper } from './match.mapper';
|
||||||
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
|
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
|
||||||
import { MatchingRepository } from './infrastructure/matching.repository';
|
import { MatchingRepository } from './infrastructure/matching.repository';
|
||||||
|
@ -44,9 +40,30 @@ import {
|
||||||
} from '@songkeys/nestjs-redis';
|
} from '@songkeys/nestjs-redis';
|
||||||
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
||||||
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
|
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
|
||||||
|
import { GeorouterProvider } from './infrastructure/georouter-provider';
|
||||||
|
import { ClientsModule, Transport } from '@nestjs/microservices';
|
||||||
|
import { GRPC_GEOGRAPHY_PACKAGE_NAME } from '@src/app.constants';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
const imports = [
|
const imports = [
|
||||||
CqrsModule,
|
CqrsModule,
|
||||||
|
ClientsModule.registerAsync([
|
||||||
|
{
|
||||||
|
name: GEOGRAPHY_PACKAGE,
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: async (configService: ConfigService) => ({
|
||||||
|
transport: Transport.GRPC,
|
||||||
|
options: {
|
||||||
|
package: GRPC_GEOGRAPHY_PACKAGE_NAME,
|
||||||
|
protoPath: join(__dirname, '/infrastructure/georouter.proto'),
|
||||||
|
url: `${configService.get<string>(
|
||||||
|
'producerServices.geographyUrl',
|
||||||
|
)}:${configService.get<string>('producerServices.geographyPort')}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
CacheModule.registerAsync<RedisClientOptions>({
|
CacheModule.registerAsync<RedisClientOptions>({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: async (configService: ConfigService) => ({
|
||||||
|
@ -122,15 +139,7 @@ const adapters: Provider[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AD_ROUTE_PROVIDER,
|
provide: AD_ROUTE_PROVIDER,
|
||||||
useClass: RouteProvider,
|
useClass: GeorouterProvider,
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: AD_GET_BASIC_ROUTE_CONTROLLER,
|
|
||||||
useClass: GetBasicRouteController,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: AD_GET_DETAILED_ROUTE_CONTROLLER,
|
|
||||||
useClass: GetDetailedRouteController,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: TIMEZONE_FINDER,
|
provide: TIMEZONE_FINDER,
|
||||||
|
|
|
@ -14,7 +14,6 @@ import {
|
||||||
MessagePublisherPort,
|
MessagePublisherPort,
|
||||||
} from '@mobicoop/ddd-library';
|
} from '@mobicoop/ddd-library';
|
||||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||||
import { RouteProviderPort } from '../../ports/route-provider.port';
|
|
||||||
import { Role } from '@modules/ad/core/domain/ad.types';
|
import { Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import {
|
import {
|
||||||
Path,
|
Path,
|
||||||
|
@ -27,6 +26,7 @@ import { Point as PointValueObject } from '@modules/ad/core/domain/value-objects
|
||||||
import { Point } from '@modules/geography/core/domain/route.types';
|
import { Point } from '@modules/geography/core/domain/route.types';
|
||||||
import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-creation-failed.integration-event';
|
import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-creation-failed.integration-event';
|
||||||
import { MATCHER_AD_CREATION_FAILED_ROUTING_KEY } from '@src/app.constants';
|
import { MATCHER_AD_CREATION_FAILED_ROUTING_KEY } from '@src/app.constants';
|
||||||
|
import { GeorouterProviderPort } from '../../ports/georouter-provider.port';
|
||||||
|
|
||||||
@CommandHandler(CreateAdCommand)
|
@CommandHandler(CreateAdCommand)
|
||||||
export class CreateAdService implements ICommandHandler {
|
export class CreateAdService implements ICommandHandler {
|
||||||
|
@ -36,7 +36,7 @@ export class CreateAdService implements ICommandHandler {
|
||||||
@Inject(AD_REPOSITORY)
|
@Inject(AD_REPOSITORY)
|
||||||
private readonly repository: AdRepositoryPort,
|
private readonly repository: AdRepositoryPort,
|
||||||
@Inject(AD_ROUTE_PROVIDER)
|
@Inject(AD_ROUTE_PROVIDER)
|
||||||
private readonly routeProvider: RouteProviderPort,
|
private readonly routeProvider: GeorouterProviderPort,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(command: CreateAdCommand): Promise<AggregateID> {
|
async execute(command: CreateAdCommand): Promise<AggregateID> {
|
||||||
|
@ -69,7 +69,9 @@ export class CreateAdService implements ICommandHandler {
|
||||||
typedRoutes = await Promise.all(
|
typedRoutes = await Promise.all(
|
||||||
pathCreator.getBasePaths().map(async (path: Path) => ({
|
pathCreator.getBasePaths().map(async (path: Path) => ({
|
||||||
type: path.type,
|
type: path.type,
|
||||||
route: await this.routeProvider.getBasic(path.waypoints),
|
route: await this.routeProvider.getRoute({
|
||||||
|
waypoints: path.waypoints,
|
||||||
|
}),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
export type Point = {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Step = Point & {
|
||||||
|
duration: number;
|
||||||
|
distance?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RouteRequest = {
|
||||||
|
waypoints: Point[];
|
||||||
|
detailsSettings?: { points: boolean; steps: boolean };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RouteResponse = {
|
||||||
|
distance: number;
|
||||||
|
duration: number;
|
||||||
|
fwdAzimuth: number;
|
||||||
|
backAzimuth: number;
|
||||||
|
distanceAzimuth: number;
|
||||||
|
points: Point[];
|
||||||
|
steps?: Step[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export abstract class GeorouterProviderPort {
|
||||||
|
abstract getRoute(request: RouteRequest): Promise<RouteResponse>;
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
import { Point } from '../types/point.type';
|
|
||||||
import { Route } from '../types/route.type';
|
|
||||||
|
|
||||||
export interface RouteProviderPort {
|
|
||||||
/**
|
|
||||||
* Get a basic route :
|
|
||||||
* - simple points (coordinates only)
|
|
||||||
* - overall duration
|
|
||||||
* - overall distance
|
|
||||||
*/
|
|
||||||
getBasic(waypoints: Point[]): Promise<Route>;
|
|
||||||
/**
|
|
||||||
* Get a detailed route :
|
|
||||||
* - detailed points (coordinates and time / distance to reach the point)
|
|
||||||
* - overall duration
|
|
||||||
* - overall distance
|
|
||||||
*/
|
|
||||||
getDetailed(waypoints: Point[]): Promise<Route>;
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@ import { Completer } from './completer.abstract';
|
||||||
import { MatchQuery } from '../match.query';
|
import { MatchQuery } from '../match.query';
|
||||||
import { Step } from '../../../types/step.type';
|
import { Step } from '../../../types/step.type';
|
||||||
import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
|
import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
|
||||||
|
import { RouteResponse } from '../../../ports/georouter-provider.port';
|
||||||
|
|
||||||
export class RouteCompleter extends Completer {
|
export class RouteCompleter extends Completer {
|
||||||
protected readonly type: RouteCompleterType;
|
protected readonly type: RouteCompleterType;
|
||||||
|
@ -18,23 +19,20 @@ export class RouteCompleter extends Completer {
|
||||||
candidates.map(async (candidate: CandidateEntity) => {
|
candidates.map(async (candidate: CandidateEntity) => {
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case RouteCompleterType.BASIC:
|
case RouteCompleterType.BASIC:
|
||||||
const basicCandidateRoute = await this.query.routeProvider.getBasic(
|
const basicCandidateRoute = await this._getRoute(candidate, {
|
||||||
(candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
|
points: true,
|
||||||
(carpoolPathItem: CarpoolPathItem) => carpoolPathItem,
|
steps: false,
|
||||||
),
|
});
|
||||||
);
|
|
||||||
candidate.setMetrics(
|
candidate.setMetrics(
|
||||||
basicCandidateRoute.distance,
|
basicCandidateRoute.distance,
|
||||||
basicCandidateRoute.duration,
|
basicCandidateRoute.duration,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case RouteCompleterType.DETAILED:
|
case RouteCompleterType.DETAILED:
|
||||||
const detailedCandidateRoute =
|
const detailedCandidateRoute = await this._getRoute(candidate, {
|
||||||
await this.query.routeProvider.getDetailed(
|
points: true,
|
||||||
(candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
|
steps: true,
|
||||||
(carpoolPathItem: CarpoolPathItem) => carpoolPathItem,
|
});
|
||||||
),
|
|
||||||
);
|
|
||||||
candidate.setSteps(detailedCandidateRoute.steps as Step[]);
|
candidate.setSteps(detailedCandidateRoute.steps as Step[]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -43,6 +41,17 @@ export class RouteCompleter extends Completer {
|
||||||
);
|
);
|
||||||
return candidates;
|
return candidates;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_getRoute = async (
|
||||||
|
candidate: CandidateEntity,
|
||||||
|
detailsSettings: { points: boolean; steps: boolean },
|
||||||
|
): Promise<RouteResponse> =>
|
||||||
|
this.query.routeProvider.getRoute({
|
||||||
|
waypoints: (candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
|
||||||
|
(carpoolPathItem: CarpoolPathItem) => carpoolPathItem,
|
||||||
|
),
|
||||||
|
detailsSettings: detailsSettings,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RouteCompleterType {
|
export enum RouteCompleterType {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { Waypoint } from '../../types/waypoint.type';
|
||||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
|
import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
|
||||||
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
||||||
import { RouteProviderPort } from '../../ports/route-provider.port';
|
|
||||||
import {
|
import {
|
||||||
Path,
|
Path,
|
||||||
PathCreator,
|
PathCreator,
|
||||||
|
@ -13,6 +12,7 @@ import {
|
||||||
} from '@modules/ad/core/domain/path-creator.service';
|
} from '@modules/ad/core/domain/path-creator.service';
|
||||||
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
|
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||||
import { Route } from '../../types/route.type';
|
import { Route } from '../../types/route.type';
|
||||||
|
import { GeorouterProviderPort } from '../../ports/georouter-provider.port';
|
||||||
|
|
||||||
export class MatchQuery extends QueryBase {
|
export class MatchQuery extends QueryBase {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -40,10 +40,10 @@ export class MatchQuery extends QueryBase {
|
||||||
passengerRoute?: Route;
|
passengerRoute?: Route;
|
||||||
backAzimuth?: number;
|
backAzimuth?: number;
|
||||||
private readonly originWaypoint: Waypoint;
|
private readonly originWaypoint: Waypoint;
|
||||||
routeProvider: RouteProviderPort;
|
routeProvider: GeorouterProviderPort;
|
||||||
|
|
||||||
// TODO: remove MatchRequestDto depency (here core domain depends on interface /!\)
|
// TODO: remove MatchRequestDto depency (here core domain depends on interface /!\)
|
||||||
constructor(props: MatchRequestDto, routeProvider: RouteProviderPort) {
|
constructor(props: MatchRequestDto, routeProvider: GeorouterProviderPort) {
|
||||||
super();
|
super();
|
||||||
this.id = props.id;
|
this.id = props.id;
|
||||||
this.driver = props.driver;
|
this.driver = props.driver;
|
||||||
|
@ -208,7 +208,9 @@ export class MatchQuery extends QueryBase {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pathCreator.getBasePaths().map(async (path: Path) => ({
|
pathCreator.getBasePaths().map(async (path: Path) => ({
|
||||||
type: path.type,
|
type: path.type,
|
||||||
route: await this.routeProvider.getBasic(path.waypoints),
|
route: await this.routeProvider.getRoute({
|
||||||
|
waypoints: path.waypoints,
|
||||||
|
}),
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
).forEach((typedRoute: TypedRoute) => {
|
).forEach((typedRoute: TypedRoute) => {
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Observable, lastValueFrom } from 'rxjs';
|
||||||
|
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { ClientGrpc } from '@nestjs/microservices';
|
||||||
|
import { GRPC_GEOROUTER_SERVICE_NAME } from '@src/app.constants';
|
||||||
|
import {
|
||||||
|
GeorouterProviderPort,
|
||||||
|
RouteRequest,
|
||||||
|
RouteResponse,
|
||||||
|
} from '../core/application/ports/georouter-provider.port';
|
||||||
|
import { GEOGRAPHY_PACKAGE } from '../ad.di-tokens';
|
||||||
|
|
||||||
|
interface GeorouterService {
|
||||||
|
getRoute(request: RouteRequest): Observable<RouteResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GeorouterProvider implements GeorouterProviderPort, OnModuleInit {
|
||||||
|
private georouterService: GeorouterService;
|
||||||
|
|
||||||
|
constructor(@Inject(GEOGRAPHY_PACKAGE) private readonly client: ClientGrpc) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.georouterService = this.client.getService<GeorouterService>(
|
||||||
|
GRPC_GEOROUTER_SERVICE_NAME,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoute = async (request: RouteRequest): Promise<RouteResponse> => {
|
||||||
|
try {
|
||||||
|
return await lastValueFrom(this.georouterService.getRoute(request));
|
||||||
|
} catch (error: any) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package geocoder;
|
||||||
|
|
||||||
|
service GeorouterService {
|
||||||
|
rpc GetRoute(RouteRequest) returns (Route);
|
||||||
|
}
|
||||||
|
|
||||||
|
message RouteRequest {
|
||||||
|
repeated Point waypoints = 1;
|
||||||
|
optional DetailsSettings detailsSettings = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Point {
|
||||||
|
double lon = 1;
|
||||||
|
double lat = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DetailsSettings {
|
||||||
|
bool points = 1;
|
||||||
|
bool steps = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Route {
|
||||||
|
int32 distance = 1;
|
||||||
|
int32 duration = 2;
|
||||||
|
int32 fwdAzimuth = 3;
|
||||||
|
int32 backAzimuth = 4;
|
||||||
|
int32 distanceAzimuth = 5;
|
||||||
|
repeated Point points = 6;
|
||||||
|
repeated Step steps = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Step {
|
||||||
|
double lon = 1;
|
||||||
|
double lat = 2;
|
||||||
|
int32 duration = 3;
|
||||||
|
int32 distance = 4;
|
||||||
|
}
|
|
@ -1,28 +0,0 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import { RouteProviderPort } from '../core/application/ports/route-provider.port';
|
|
||||||
import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port';
|
|
||||||
import {
|
|
||||||
AD_GET_BASIC_ROUTE_CONTROLLER,
|
|
||||||
AD_GET_DETAILED_ROUTE_CONTROLLER,
|
|
||||||
} from '../ad.di-tokens';
|
|
||||||
import { Point, Route } from '@modules/geography/core/domain/route.types';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RouteProvider implements RouteProviderPort {
|
|
||||||
constructor(
|
|
||||||
@Inject(AD_GET_BASIC_ROUTE_CONTROLLER)
|
|
||||||
private readonly getBasicRouteController: GetRouteControllerPort,
|
|
||||||
@Inject(AD_GET_DETAILED_ROUTE_CONTROLLER)
|
|
||||||
private readonly getDetailedRouteController: GetRouteControllerPort,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
getBasic = async (waypoints: Point[]): Promise<Route> =>
|
|
||||||
await this.getBasicRouteController.get({
|
|
||||||
waypoints,
|
|
||||||
});
|
|
||||||
|
|
||||||
getDetailed = async (waypoints: Point[]): Promise<Route> =>
|
|
||||||
await this.getDetailedRouteController.get({
|
|
||||||
waypoints,
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -8,10 +8,10 @@ import { MatchRequestDto } from './dtos/match.request.dto';
|
||||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||||
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
|
||||||
import { MatchMapper } from '@modules/ad/match.mapper';
|
import { MatchMapper } from '@modules/ad/match.mapper';
|
||||||
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||||
import { CacheInterceptor, CacheKey } from '@nestjs/cache-manager';
|
import { CacheInterceptor, CacheKey } from '@nestjs/cache-manager';
|
||||||
|
import { GeorouterProviderPort } from '@modules/ad/core/application/ports/georouter-provider.port';
|
||||||
|
|
||||||
@UsePipes(
|
@UsePipes(
|
||||||
new RpcValidationPipe({
|
new RpcValidationPipe({
|
||||||
|
@ -24,7 +24,7 @@ export class MatchGrpcController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly queryBus: QueryBus,
|
private readonly queryBus: QueryBus,
|
||||||
@Inject(AD_ROUTE_PROVIDER)
|
@Inject(AD_ROUTE_PROVIDER)
|
||||||
private readonly routeProvider: RouteProviderPort,
|
private readonly routeProvider: GeorouterProviderPort,
|
||||||
private readonly matchMapper: MatchMapper,
|
private readonly matchMapper: MatchMapper,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
|
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
|
||||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
|
||||||
import {
|
import {
|
||||||
Algorithm,
|
Algorithm,
|
||||||
Selector,
|
Selector,
|
||||||
|
@ -9,6 +8,7 @@ import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||||
|
import { bareMockGeorouter } from '../georouter.mock';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -29,11 +29,6 @@ const destinationWaypoint: Waypoint = {
|
||||||
country: 'France',
|
country: 'France',
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRouteProvider: RouteProviderPort = {
|
|
||||||
getBasic: jest.fn(),
|
|
||||||
getDetailed: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const matchQuery = new MatchQuery(
|
const matchQuery = new MatchQuery(
|
||||||
{
|
{
|
||||||
frequency: Frequency.PUNCTUAL,
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
@ -46,7 +41,7 @@ const matchQuery = new MatchQuery(
|
||||||
],
|
],
|
||||||
waypoints: [originWaypoint, destinationWaypoint],
|
waypoints: [originWaypoint, destinationWaypoint],
|
||||||
},
|
},
|
||||||
mockRouteProvider,
|
bareMockGeorouter,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mockAdRepository: AdRepositoryPort = {
|
const mockAdRepository: AdRepositoryPort = {
|
||||||
|
|
|
@ -11,8 +11,8 @@ import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
|
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
|
||||||
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
|
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
|
||||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
|
||||||
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||||
|
import { GeorouterProviderPort } from '@modules/ad/core/application/ports/georouter-provider.port';
|
||||||
|
|
||||||
const originWaypoint: PointProps = {
|
const originWaypoint: PointProps = {
|
||||||
lat: 48.689445,
|
lat: 48.689445,
|
||||||
|
@ -62,8 +62,8 @@ const mockAdRepository = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRouteProvider: RouteProviderPort = {
|
const mockRouteProvider: GeorouterProviderPort = {
|
||||||
getBasic: jest
|
getRoute: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
|
@ -97,7 +97,6 @@ const mockRouteProvider: RouteProviderPort = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})),
|
})),
|
||||||
getDetailed: jest.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockMessagePublisher = {
|
const mockMessagePublisher = {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
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 { simpleMockGeorouter } from '../georouter.mock';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -42,24 +43,7 @@ const matchQuery = new MatchQuery(
|
||||||
strict: false,
|
strict: false,
|
||||||
waypoints: [originWaypoint, destinationWaypoint],
|
waypoints: [originWaypoint, destinationWaypoint],
|
||||||
},
|
},
|
||||||
{
|
simpleMockGeorouter,
|
||||||
getBasic: jest.fn().mockImplementation(() => ({
|
|
||||||
distance: 350101,
|
|
||||||
duration: 14422,
|
|
||||||
fwdAzimuth: 273,
|
|
||||||
backAzimuth: 93,
|
|
||||||
distanceAzimuth: 336544,
|
|
||||||
points: [],
|
|
||||||
})),
|
|
||||||
getDetailed: jest.fn().mockImplementation(() => ({
|
|
||||||
distance: 350102,
|
|
||||||
duration: 14423,
|
|
||||||
fwdAzimuth: 273,
|
|
||||||
backAzimuth: 93,
|
|
||||||
distanceAzimuth: 336544,
|
|
||||||
points: [],
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const candidate: CandidateEntity = CandidateEntity.create({
|
const candidate: CandidateEntity = CandidateEntity.create({
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.type
|
||||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
|
import { bareMockGeorouter } from '../georouter.mock';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -40,10 +41,7 @@ const matchQuery = new MatchQuery(
|
||||||
strict: false,
|
strict: false,
|
||||||
waypoints: [originWaypoint, destinationWaypoint],
|
waypoints: [originWaypoint, destinationWaypoint],
|
||||||
},
|
},
|
||||||
{
|
bareMockGeorouter,
|
||||||
getBasic: jest.fn(),
|
|
||||||
getDetailed: jest.fn(),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const candidate: CandidateEntity = CandidateEntity.create({
|
const candidate: CandidateEntity = CandidateEntity.create({
|
||||||
|
|
|
@ -19,7 +19,6 @@ import {
|
||||||
} from '@modules/ad/ad.di-tokens';
|
} from '@modules/ad/ad.di-tokens';
|
||||||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
||||||
import { MatchingRepositoryPort } from '@modules/ad/core/application/ports/matching.repository.port';
|
import { MatchingRepositoryPort } from '@modules/ad/core/application/ports/matching.repository.port';
|
||||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
|
||||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||||
import {
|
import {
|
||||||
MatchQueryHandler,
|
MatchQueryHandler,
|
||||||
|
@ -43,6 +42,7 @@ import {
|
||||||
PAGINATION_CONFIG_PER_PAGE,
|
PAGINATION_CONFIG_PER_PAGE,
|
||||||
} from '@modules/ad/match.constants';
|
} from '@modules/ad/match.constants';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { simpleMockGeorouter } from '../georouter.mock';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -351,17 +351,7 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = {
|
||||||
time: jest.fn(),
|
time: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRouteProvider: RouteProviderPort = {
|
const mockRouteProvider = simpleMockGeorouter;
|
||||||
getBasic: jest.fn().mockImplementation(() => ({
|
|
||||||
distance: 350101,
|
|
||||||
duration: 14422,
|
|
||||||
fwdAzimuth: 273,
|
|
||||||
backAzimuth: 93,
|
|
||||||
distanceAzimuth: 336544,
|
|
||||||
points: [],
|
|
||||||
})),
|
|
||||||
getDetailed: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Match Query Handler', () => {
|
describe('Match Query Handler', () => {
|
||||||
let matchQueryHandler: MatchQueryHandler;
|
let matchQueryHandler: MatchQueryHandler;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
||||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
import { GeorouterProviderPort } from '@modules/ad/core/application/ports/georouter-provider.port';
|
||||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||||
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { simpleMockGeorouter } from '../georouter.mock';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -57,17 +58,10 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = {
|
||||||
time: jest.fn().mockImplementation(() => '23:05'),
|
time: jest.fn().mockImplementation(() => '23:05'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRouteProvider: RouteProviderPort = {
|
const mockRouteProvider: GeorouterProviderPort = {
|
||||||
getBasic: jest
|
getRoute: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementationOnce(() => ({
|
.mockImplementationOnce(simpleMockGeorouter.getRoute)
|
||||||
distance: 350101,
|
|
||||||
duration: 14422,
|
|
||||||
fwdAzimuth: 273,
|
|
||||||
backAzimuth: 93,
|
|
||||||
distanceAzimuth: 336544,
|
|
||||||
points: [],
|
|
||||||
}))
|
|
||||||
.mockImplementationOnce(() => ({
|
.mockImplementationOnce(() => ({
|
||||||
distance: 340102,
|
distance: 340102,
|
||||||
duration: 13423,
|
duration: 13423,
|
||||||
|
@ -76,22 +70,8 @@ const mockRouteProvider: RouteProviderPort = {
|
||||||
distanceAzimuth: 336544,
|
distanceAzimuth: 336544,
|
||||||
points: [],
|
points: [],
|
||||||
}))
|
}))
|
||||||
.mockImplementationOnce(() => ({
|
.mockImplementationOnce(simpleMockGeorouter.getRoute)
|
||||||
distance: 350101,
|
.mockImplementationOnce(simpleMockGeorouter.getRoute)
|
||||||
duration: 14422,
|
|
||||||
fwdAzimuth: 273,
|
|
||||||
backAzimuth: 93,
|
|
||||||
distanceAzimuth: 336544,
|
|
||||||
points: [],
|
|
||||||
}))
|
|
||||||
.mockImplementationOnce(() => ({
|
|
||||||
distance: 350101,
|
|
||||||
duration: 14422,
|
|
||||||
fwdAzimuth: 273,
|
|
||||||
backAzimuth: 93,
|
|
||||||
distanceAzimuth: 336544,
|
|
||||||
points: [],
|
|
||||||
}))
|
|
||||||
.mockImplementationOnce(() => ({
|
.mockImplementationOnce(() => ({
|
||||||
distance: 340102,
|
distance: 340102,
|
||||||
duration: 13423,
|
duration: 13423,
|
||||||
|
@ -103,7 +83,6 @@ const mockRouteProvider: RouteProviderPort = {
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}),
|
}),
|
||||||
getDetailed: jest.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Match Query', () => {
|
describe('Match Query', () => {
|
||||||
|
|
|
@ -42,11 +42,10 @@ const matchQuery = new MatchQuery(
|
||||||
waypoints: [originWaypoint, destinationWaypoint],
|
waypoints: [originWaypoint, destinationWaypoint],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getBasic: jest.fn().mockImplementation(() => ({
|
getRoute: jest.fn().mockImplementation(() => ({
|
||||||
duration: 6500,
|
duration: 6500,
|
||||||
distance: 89745,
|
distance: 89745,
|
||||||
})),
|
})),
|
||||||
getDetailed: jest.fn(),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.type
|
||||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
|
import { bareMockGeorouter } from '../georouter.mock';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -40,10 +41,7 @@ const matchQuery = new MatchQuery(
|
||||||
strict: false,
|
strict: false,
|
||||||
waypoints: [originWaypoint, destinationWaypoint],
|
waypoints: [originWaypoint, destinationWaypoint],
|
||||||
},
|
},
|
||||||
{
|
bareMockGeorouter,
|
||||||
getBasic: jest.fn(),
|
|
||||||
getDetailed: jest.fn(),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const candidates: CandidateEntity[] = [
|
const candidates: CandidateEntity[] = [
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.type
|
||||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
|
import { bareMockGeorouter } from '../georouter.mock';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -40,10 +41,7 @@ const matchQuery = new MatchQuery(
|
||||||
strict: false,
|
strict: false,
|
||||||
waypoints: [originWaypoint, destinationWaypoint],
|
waypoints: [originWaypoint, destinationWaypoint],
|
||||||
},
|
},
|
||||||
{
|
bareMockGeorouter,
|
||||||
getBasic: jest.fn(),
|
|
||||||
getDetailed: jest.fn(),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const candidate: CandidateEntity = CandidateEntity.create({
|
const candidate: CandidateEntity = CandidateEntity.create({
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.type
|
||||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
|
import { bareMockGeorouter } from '../georouter.mock';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -47,10 +48,7 @@ const matchQuery = new MatchQuery(
|
||||||
strict: false,
|
strict: false,
|
||||||
waypoints: [originWaypoint, destinationWaypoint],
|
waypoints: [originWaypoint, destinationWaypoint],
|
||||||
},
|
},
|
||||||
{
|
bareMockGeorouter,
|
||||||
getBasic: jest.fn(),
|
|
||||||
getDetailed: jest.fn(),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
matchQuery.driverRoute = {
|
matchQuery.driverRoute = {
|
||||||
distance: 150120,
|
distance: 150120,
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import {
|
||||||
|
RouteRequest,
|
||||||
|
RouteResponse,
|
||||||
|
} from '@modules/ad/core/application/ports/georouter-provider.port';
|
||||||
import {
|
import {
|
||||||
RouteCompleter,
|
RouteCompleter,
|
||||||
RouteCompleterType,
|
RouteCompleterType,
|
||||||
|
@ -9,6 +13,8 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
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 { Step } from '@modules/geography/core/domain/route.types';
|
||||||
|
import { simpleMockGeorouter } from '../georouter.mock';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -46,23 +52,16 @@ const matchQuery = new MatchQuery(
|
||||||
waypoints: [originWaypoint, destinationWaypoint],
|
waypoints: [originWaypoint, destinationWaypoint],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getBasic: jest.fn().mockImplementation(() => ({
|
getRoute: jest
|
||||||
distance: 350101,
|
.fn()
|
||||||
duration: 14422,
|
.mockImplementation(async (req: RouteRequest): Promise<RouteResponse> => {
|
||||||
fwdAzimuth: 273,
|
const response = await simpleMockGeorouter.getRoute(req);
|
||||||
backAzimuth: 93,
|
if (req.detailsSettings?.steps) {
|
||||||
distanceAzimuth: 336544,
|
const step: Step = { lon: 0, lat: 0, duration: 0 };
|
||||||
points: [],
|
response.steps = [step, step, step, step];
|
||||||
})),
|
}
|
||||||
getDetailed: jest.fn().mockImplementation(() => ({
|
return response;
|
||||||
distance: 350101,
|
}),
|
||||||
duration: 14422,
|
|
||||||
fwdAzimuth: 273,
|
|
||||||
backAzimuth: 93,
|
|
||||||
distanceAzimuth: 336544,
|
|
||||||
points: [],
|
|
||||||
steps: [jest.fn(), jest.fn(), jest.fn(), jest.fn()],
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { GeorouterProviderPort } from '@modules/ad/core/application/ports/georouter-provider.port';
|
||||||
|
|
||||||
|
export const bareMockGeorouter: GeorouterProviderPort = {
|
||||||
|
getRoute: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const simpleMockGeorouter: GeorouterProviderPort = {
|
||||||
|
getRoute: jest.fn().mockImplementation(() => ({
|
||||||
|
distance: 350101,
|
||||||
|
duration: 14422,
|
||||||
|
fwdAzimuth: 273,
|
||||||
|
backAzimuth: 93,
|
||||||
|
distanceAzimuth: 336544,
|
||||||
|
points: [],
|
||||||
|
})),
|
||||||
|
};
|
|
@ -4,7 +4,6 @@ import {
|
||||||
AD_ROUTE_PROVIDER,
|
AD_ROUTE_PROVIDER,
|
||||||
} from '@modules/ad/ad.di-tokens';
|
} from '@modules/ad/ad.di-tokens';
|
||||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
|
||||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||||
|
@ -12,6 +11,7 @@ import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
||||||
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
|
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { bareMockGeorouter } from '../georouter.mock';
|
||||||
|
|
||||||
const mockMessagePublisher = {
|
const mockMessagePublisher = {
|
||||||
publish: jest.fn().mockImplementation(),
|
publish: jest.fn().mockImplementation(),
|
||||||
|
@ -73,11 +73,6 @@ const mockDirectionEncoder: DirectionEncoderPort = {
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRouteProvider: RouteProviderPort = {
|
|
||||||
getBasic: jest.fn(),
|
|
||||||
getDetailed: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockPrismaService = {
|
const mockPrismaService = {
|
||||||
$queryRawUnsafe: jest
|
$queryRawUnsafe: jest
|
||||||
.fn()
|
.fn()
|
||||||
|
@ -239,7 +234,7 @@ describe('Ad repository', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AD_ROUTE_PROVIDER,
|
provide: AD_ROUTE_PROVIDER,
|
||||||
useValue: mockRouteProvider,
|
useValue: bareMockGeorouter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AD_MESSAGE_PUBLISHER,
|
provide: AD_MESSAGE_PUBLISHER,
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
import {
|
|
||||||
AD_GET_BASIC_ROUTE_CONTROLLER,
|
|
||||||
AD_GET_DETAILED_ROUTE_CONTROLLER,
|
|
||||||
} from '@modules/ad/ad.di-tokens';
|
|
||||||
import { Point } from '@modules/ad/core/application/types/point.type';
|
|
||||||
import { Route } from '@modules/ad/core/application/types/route.type';
|
|
||||||
import { RouteProvider } from '@modules/ad/infrastructure/route-provider';
|
|
||||||
import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
|
|
||||||
const originPoint: Point = {
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
};
|
|
||||||
const destinationPoint: Point = {
|
|
||||||
lat: 48.8566,
|
|
||||||
lon: 2.3522,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockGetBasicRouteController: GetRouteControllerPort = {
|
|
||||||
get: jest.fn().mockImplementationOnce(() => ({
|
|
||||||
distance: 350101,
|
|
||||||
duration: 14422,
|
|
||||||
fwdAzimuth: 273,
|
|
||||||
backAzimuth: 93,
|
|
||||||
distanceAzimuth: 336544,
|
|
||||||
points: [
|
|
||||||
{
|
|
||||||
lon: 6.1765102,
|
|
||||||
lat: 48.689445,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 4.984578,
|
|
||||||
lat: 48.725687,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 2.3522,
|
|
||||||
lat: 48.8566,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockGetDetailedRouteController: GetRouteControllerPort = {
|
|
||||||
get: jest.fn().mockImplementationOnce(() => ({
|
|
||||||
distance: 350102,
|
|
||||||
duration: 14423,
|
|
||||||
fwdAzimuth: 273,
|
|
||||||
backAzimuth: 93,
|
|
||||||
distanceAzimuth: 336544,
|
|
||||||
points: [
|
|
||||||
{
|
|
||||||
lon: 6.1765102,
|
|
||||||
lat: 48.689445,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 4.984578,
|
|
||||||
lat: 48.725687,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 2.3522,
|
|
||||||
lat: 48.8566,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Route provider', () => {
|
|
||||||
let routeProvider: RouteProvider;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
RouteProvider,
|
|
||||||
{
|
|
||||||
provide: AD_GET_BASIC_ROUTE_CONTROLLER,
|
|
||||||
useValue: mockGetBasicRouteController,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: AD_GET_DETAILED_ROUTE_CONTROLLER,
|
|
||||||
useValue: mockGetDetailedRouteController,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
routeProvider = module.get<RouteProvider>(RouteProvider);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(routeProvider).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should provide a basic route', async () => {
|
|
||||||
const route: Route = await routeProvider.getBasic([
|
|
||||||
originPoint,
|
|
||||||
destinationPoint,
|
|
||||||
]);
|
|
||||||
expect(route.distance).toBe(350101);
|
|
||||||
expect(route.duration).toBe(14422);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should provide a detailed route', async () => {
|
|
||||||
const route: Route = await routeProvider.getDetailed([
|
|
||||||
originPoint,
|
|
||||||
destinationPoint,
|
|
||||||
]);
|
|
||||||
expect(route.distance).toBe(350102);
|
|
||||||
expect(route.duration).toBe(14423);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
|
||||||
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||||
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
|
@ -17,6 +16,7 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
import { QueryBus } from '@nestjs/cqrs';
|
import { QueryBus } from '@nestjs/cqrs';
|
||||||
import { RpcException } from '@nestjs/microservices';
|
import { RpcException } from '@nestjs/microservices';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { bareMockGeorouter } from '../georouter.mock';
|
||||||
|
|
||||||
const originWaypoint: WaypointDto = {
|
const originWaypoint: WaypointDto = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -183,11 +183,6 @@ const mockQueryBus = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRouteProvider: RouteProviderPort = {
|
|
||||||
getBasic: jest.fn(),
|
|
||||||
getDetailed: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMatchMapper = {
|
const mockMatchMapper = {
|
||||||
toResponse: jest.fn().mockImplementation(() => ({
|
toResponse: jest.fn().mockImplementation(() => ({
|
||||||
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
|
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
|
||||||
|
@ -286,7 +281,7 @@ describe('Match Grpc Controller', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AD_ROUTE_PROVIDER,
|
provide: AD_ROUTE_PROVIDER,
|
||||||
useValue: mockRouteProvider,
|
useValue: bareMockGeorouter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: MatchMapper,
|
provide: MatchMapper,
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
export interface GeodesicPort {
|
|
||||||
inverse(
|
|
||||||
lon1: number,
|
|
||||||
lat1: number,
|
|
||||||
lon2: number,
|
|
||||||
lat2: number,
|
|
||||||
): {
|
|
||||||
azimuth: number;
|
|
||||||
distance: number;
|
|
||||||
};
|
|
||||||
distance(lon1: number, lat1: number, lon2: number, lat2: number): number;
|
|
||||||
azimuth(lon1: number, lat1: number, lon2: number, lat2: number): number;
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { Route, Point } from '../../domain/route.types';
|
|
||||||
import { GeorouterSettings } from '../types/georouter-settings.type';
|
|
||||||
|
|
||||||
export interface GeorouterPort {
|
|
||||||
route(waypoints: Point[], settings: GeorouterSettings): Promise<Route>;
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { GetRouteRequestDto } from '@modules/geography/interface/controllers/dtos/get-route.request.dto';
|
|
||||||
import { RouteResponseDto } from '@modules/geography/interface/dtos/route.response.dto';
|
|
||||||
|
|
||||||
export interface GetRouteControllerPort {
|
|
||||||
get(data: GetRouteRequestDto): Promise<RouteResponseDto>;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
|
||||||
import { GetRouteQuery } from './get-route.query';
|
|
||||||
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
|
|
||||||
import { Inject } from '@nestjs/common';
|
|
||||||
import { GEOROUTER } from '@modules/geography/geography.di-tokens';
|
|
||||||
import { GeorouterPort } from '../../ports/georouter.port';
|
|
||||||
|
|
||||||
@QueryHandler(GetRouteQuery)
|
|
||||||
export class GetRouteQueryHandler implements IQueryHandler {
|
|
||||||
constructor(@Inject(GEOROUTER) private readonly georouter: GeorouterPort) {}
|
|
||||||
|
|
||||||
execute = async (query: GetRouteQuery): Promise<RouteEntity> =>
|
|
||||||
await RouteEntity.create({
|
|
||||||
waypoints: query.waypoints,
|
|
||||||
georouter: this.georouter,
|
|
||||||
georouterSettings: query.georouterSettings,
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { QueryBase } from '@mobicoop/ddd-library';
|
|
||||||
import { GeorouterSettings } from '../../types/georouter-settings.type';
|
|
||||||
import { Point } from '@modules/geography/core/domain/route.types';
|
|
||||||
|
|
||||||
export class GetRouteQuery extends QueryBase {
|
|
||||||
readonly waypoints: Point[];
|
|
||||||
readonly georouterSettings: GeorouterSettings;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
waypoints: Point[],
|
|
||||||
georouterSettings: GeorouterSettings = {
|
|
||||||
detailedDistance: false,
|
|
||||||
detailedDuration: false,
|
|
||||||
points: true,
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
this.waypoints = waypoints;
|
|
||||||
this.georouterSettings = georouterSettings;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
export type GeorouterSettings = {
|
|
||||||
points: boolean;
|
|
||||||
detailedDuration: boolean;
|
|
||||||
detailedDistance: boolean;
|
|
||||||
};
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
|
||||||
import { CreateRouteProps, RouteProps, Route } from './route.types';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import { RouteNotFoundException } from './route.errors';
|
|
||||||
|
|
||||||
export class RouteEntity extends AggregateRoot<RouteProps> {
|
|
||||||
protected readonly _id: AggregateID;
|
|
||||||
|
|
||||||
static create = async (create: CreateRouteProps): Promise<RouteEntity> => {
|
|
||||||
const route: Route = await create.georouter.route(
|
|
||||||
create.waypoints,
|
|
||||||
create.georouterSettings,
|
|
||||||
);
|
|
||||||
if (!route) throw new RouteNotFoundException();
|
|
||||||
const routeProps: RouteProps = {
|
|
||||||
distance: route.distance,
|
|
||||||
duration: route.duration,
|
|
||||||
fwdAzimuth: route.fwdAzimuth,
|
|
||||||
backAzimuth: route.backAzimuth,
|
|
||||||
distanceAzimuth: route.distanceAzimuth,
|
|
||||||
points: route.points,
|
|
||||||
steps: route.steps,
|
|
||||||
};
|
|
||||||
return new RouteEntity({
|
|
||||||
id: v4(),
|
|
||||||
props: routeProps,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
validate(): void {
|
|
||||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { ExceptionBase } from '@mobicoop/ddd-library';
|
|
||||||
|
|
||||||
export class RouteNotFoundException extends ExceptionBase {
|
|
||||||
static readonly message = 'Route not found';
|
|
||||||
|
|
||||||
public readonly code = 'ROUTE.NOT_FOUND';
|
|
||||||
|
|
||||||
constructor(cause?: Error, metadata?: unknown) {
|
|
||||||
super(RouteNotFoundException.message, cause, metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GeorouterUnavailableException extends ExceptionBase {
|
|
||||||
static readonly message = 'Georouter unavailable';
|
|
||||||
|
|
||||||
public readonly code = 'GEOROUTER.UNAVAILABLE';
|
|
||||||
|
|
||||||
constructor(cause?: Error, metadata?: unknown) {
|
|
||||||
super(GeorouterUnavailableException.message, cause, metadata);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +1,3 @@
|
||||||
import { GeorouterPort } from '../application/ports/georouter.port';
|
|
||||||
import { GeorouterSettings } from '../application/types/georouter-settings.type';
|
|
||||||
import { PointProps } from './value-objects/point.value-object';
|
|
||||||
import { StepProps } from './value-objects/step.value-object';
|
|
||||||
|
|
||||||
// All properties that a Route has
|
|
||||||
export interface RouteProps {
|
|
||||||
distance: number;
|
|
||||||
duration: number;
|
|
||||||
fwdAzimuth: number;
|
|
||||||
backAzimuth: number;
|
|
||||||
distanceAzimuth: number;
|
|
||||||
points: PointProps[];
|
|
||||||
steps?: StepProps[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Properties that are needed for a Route creation
|
|
||||||
export interface CreateRouteProps {
|
|
||||||
waypoints: PointProps[];
|
|
||||||
georouter: GeorouterPort;
|
|
||||||
georouterSettings: GeorouterSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Types used outside the domain
|
// Types used outside the domain
|
||||||
export type Route = {
|
export type Route = {
|
||||||
distance: number;
|
distance: number;
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
import {
|
|
||||||
ArgumentOutOfRangeException,
|
|
||||||
ValueObject,
|
|
||||||
} from '@mobicoop/ddd-library';
|
|
||||||
|
|
||||||
/** Note:
|
|
||||||
* Value Objects with multiple properties can contain
|
|
||||||
* other Value Objects inside if needed.
|
|
||||||
* */
|
|
||||||
|
|
||||||
export interface PointProps {
|
|
||||||
lon: number;
|
|
||||||
lat: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Point extends ValueObject<PointProps> {
|
|
||||||
get lon(): number {
|
|
||||||
return this.props.lon;
|
|
||||||
}
|
|
||||||
|
|
||||||
get lat(): number {
|
|
||||||
return this.props.lat;
|
|
||||||
}
|
|
||||||
|
|
||||||
validate(props: PointProps): void {
|
|
||||||
if (props.lon > 180 || props.lon < -180)
|
|
||||||
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
|
|
||||||
if (props.lat > 90 || props.lat < -90)
|
|
||||||
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
|
|
||||||
import { Point, PointProps } from './point.value-object';
|
|
||||||
|
|
||||||
/** Note:
|
|
||||||
* Value Objects with multiple properties can contain
|
|
||||||
* other Value Objects inside if needed.
|
|
||||||
* */
|
|
||||||
|
|
||||||
export interface StepProps extends PointProps {
|
|
||||||
duration: number;
|
|
||||||
distance?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Step extends ValueObject<StepProps> {
|
|
||||||
get duration(): number {
|
|
||||||
return this.props.duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
get distance(): number | undefined {
|
|
||||||
return this.props.distance;
|
|
||||||
}
|
|
||||||
|
|
||||||
get lon(): number {
|
|
||||||
return this.props.lon;
|
|
||||||
}
|
|
||||||
|
|
||||||
get lat(): number {
|
|
||||||
return this.props.lat;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected validate(props: StepProps): void {
|
|
||||||
// validate point props
|
|
||||||
new Point({
|
|
||||||
lon: props.lon,
|
|
||||||
lat: props.lat,
|
|
||||||
});
|
|
||||||
if (props.duration < 0)
|
|
||||||
throw new ArgumentInvalidException(
|
|
||||||
'duration must be greater than or equal to 0',
|
|
||||||
);
|
|
||||||
if (props.distance !== undefined && props.distance < 0)
|
|
||||||
throw new ArgumentInvalidException(
|
|
||||||
'distance must be greater than or equal to 0',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { KeyType, Type } from '@mobicoop/configuration-module';
|
|
||||||
|
|
||||||
export const GEOGRAPHY_CONFIG_GEOROUTER_TYPE = 'georouterType';
|
|
||||||
export const GEOGRAPHY_CONFIG_GEOROUTER_URL = 'georouterUrl';
|
|
||||||
|
|
||||||
export const GeographyKeyTypes: KeyType[] = [
|
|
||||||
{
|
|
||||||
key: GEOGRAPHY_CONFIG_GEOROUTER_TYPE,
|
|
||||||
type: Type.STRING,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: GEOGRAPHY_CONFIG_GEOROUTER_URL,
|
|
||||||
type: Type.STRING,
|
|
||||||
},
|
|
||||||
];
|
|
|
@ -1,6 +1,4 @@
|
||||||
export const DIRECTION_ENCODER = Symbol('DIRECTION_ENCODER');
|
export const DIRECTION_ENCODER = Symbol('DIRECTION_ENCODER');
|
||||||
export const GEOROUTER = Symbol('GEOROUTER');
|
|
||||||
export const GEODESIC = Symbol('GEODESIC');
|
|
||||||
export const GEOGRAPHY_CONFIGURATION_REPOSITORY = Symbol(
|
export const GEOGRAPHY_CONFIGURATION_REPOSITORY = Symbol(
|
||||||
'GEOGRAPHY_CONFIGURATION_REPOSITORY',
|
'GEOGRAPHY_CONFIGURATION_REPOSITORY',
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,24 +2,12 @@ import { Module, Provider } from '@nestjs/common';
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
import { CqrsModule } from '@nestjs/cqrs';
|
||||||
import {
|
import {
|
||||||
DIRECTION_ENCODER,
|
DIRECTION_ENCODER,
|
||||||
GEODESIC,
|
|
||||||
GEOGRAPHY_CONFIGURATION_REPOSITORY,
|
GEOGRAPHY_CONFIGURATION_REPOSITORY,
|
||||||
GEOROUTER,
|
|
||||||
} from './geography.di-tokens';
|
} from './geography.di-tokens';
|
||||||
import { PostgresDirectionEncoder } from './infrastructure/postgres-direction-encoder';
|
import { PostgresDirectionEncoder } from './infrastructure/postgres-direction-encoder';
|
||||||
import { GetBasicRouteController } from './interface/controllers/get-basic-route.controller';
|
|
||||||
import { RouteMapper } from './route.mapper';
|
|
||||||
import { Geodesic } from './infrastructure/geodesic';
|
|
||||||
import { GraphhopperGeorouter } from './infrastructure/graphhopper-georouter';
|
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { GetRouteQueryHandler } from './core/application/queries/get-route/get-route.query-handler';
|
|
||||||
import { GetDetailedRouteController } from './interface/controllers/get-detailed-route.controller';
|
|
||||||
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
||||||
|
|
||||||
const queryHandlers: Provider[] = [GetRouteQueryHandler];
|
|
||||||
|
|
||||||
const mappers: Provider[] = [RouteMapper];
|
|
||||||
|
|
||||||
const adapters: Provider[] = [
|
const adapters: Provider[] = [
|
||||||
{
|
{
|
||||||
provide: GEOGRAPHY_CONFIGURATION_REPOSITORY,
|
provide: GEOGRAPHY_CONFIGURATION_REPOSITORY,
|
||||||
|
@ -29,26 +17,11 @@ const adapters: Provider[] = [
|
||||||
provide: DIRECTION_ENCODER,
|
provide: DIRECTION_ENCODER,
|
||||||
useClass: PostgresDirectionEncoder,
|
useClass: PostgresDirectionEncoder,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: GEOROUTER,
|
|
||||||
useClass: GraphhopperGeorouter,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: GEODESIC,
|
|
||||||
useClass: Geodesic,
|
|
||||||
},
|
|
||||||
GetBasicRouteController,
|
|
||||||
GetDetailedRouteController,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [CqrsModule, HttpModule],
|
imports: [CqrsModule, HttpModule],
|
||||||
providers: [...queryHandlers, ...mappers, ...adapters],
|
providers: [...adapters],
|
||||||
exports: [
|
exports: [DIRECTION_ENCODER],
|
||||||
RouteMapper,
|
|
||||||
DIRECTION_ENCODER,
|
|
||||||
GetBasicRouteController,
|
|
||||||
GetDetailedRouteController,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class GeographyModule {}
|
export class GeographyModule {}
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Geodesic as Geolib, GeodesicClass } from 'geographiclib-geodesic';
|
|
||||||
import { GeodesicPort } from '../core/application/ports/geodesic.port';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class Geodesic implements GeodesicPort {
|
|
||||||
private geod: GeodesicClass;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.geod = Geolib.WGS84;
|
|
||||||
}
|
|
||||||
|
|
||||||
inverse = (
|
|
||||||
lon1: number,
|
|
||||||
lat1: number,
|
|
||||||
lon2: number,
|
|
||||||
lat2: number,
|
|
||||||
): { azimuth: number; distance: number } => {
|
|
||||||
const { azi2: azimuth, s12: distance } = this.geod.Inverse(
|
|
||||||
lat1,
|
|
||||||
lon1,
|
|
||||||
lat2,
|
|
||||||
lon2,
|
|
||||||
);
|
|
||||||
if (!azimuth || !distance)
|
|
||||||
throw new Error(
|
|
||||||
`Inverse not found for coordinates ${lon1} ${lat1} / ${lon2} ${lat2}`,
|
|
||||||
);
|
|
||||||
return { azimuth, distance };
|
|
||||||
};
|
|
||||||
|
|
||||||
azimuth = (
|
|
||||||
lon1: number,
|
|
||||||
lat1: number,
|
|
||||||
lon2: number,
|
|
||||||
lat2: number,
|
|
||||||
): number => {
|
|
||||||
const { azi2: azimuth } = this.geod.Inverse(lat1, lon1, lat2, lon2);
|
|
||||||
if (!azimuth)
|
|
||||||
throw new Error(
|
|
||||||
`Azimuth not found for coordinates ${lon1} ${lat1} / ${lon2} ${lat2}`,
|
|
||||||
);
|
|
||||||
return azimuth;
|
|
||||||
};
|
|
||||||
|
|
||||||
distance = (
|
|
||||||
lon1: number,
|
|
||||||
lat1: number,
|
|
||||||
lon2: number,
|
|
||||||
lat2: number,
|
|
||||||
): number => {
|
|
||||||
const { s12: distance } = this.geod.Inverse(lat1, lon1, lat2, lon2);
|
|
||||||
if (!distance)
|
|
||||||
throw new Error(
|
|
||||||
`Distance not found for coordinates ${lon1} ${lat1} / ${lon2} ${lat2}`,
|
|
||||||
);
|
|
||||||
return distance;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,342 +0,0 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import { HttpService } from '@nestjs/axios';
|
|
||||||
import { GeorouterPort } from '../core/application/ports/georouter.port';
|
|
||||||
import { GeorouterSettings } from '../core/application/types/georouter-settings.type';
|
|
||||||
import { Route, Step, Point } from '../core/domain/route.types';
|
|
||||||
import {
|
|
||||||
GEODESIC,
|
|
||||||
GEOGRAPHY_CONFIGURATION_REPOSITORY,
|
|
||||||
} from '../geography.di-tokens';
|
|
||||||
import { catchError, lastValueFrom, map } from 'rxjs';
|
|
||||||
import { AxiosError, AxiosResponse } from 'axios';
|
|
||||||
import {
|
|
||||||
GeorouterUnavailableException,
|
|
||||||
RouteNotFoundException,
|
|
||||||
} from '../core/domain/route.errors';
|
|
||||||
import { GeodesicPort } from '../core/application/ports/geodesic.port';
|
|
||||||
import {
|
|
||||||
Domain,
|
|
||||||
Configurator,
|
|
||||||
GetConfigurationRepositoryPort,
|
|
||||||
} from '@mobicoop/configuration-module';
|
|
||||||
import {
|
|
||||||
GEOGRAPHY_CONFIG_GEOROUTER_URL,
|
|
||||||
GeographyKeyTypes,
|
|
||||||
} from '../geography.constants';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GraphhopperGeorouter implements GeorouterPort {
|
|
||||||
private url: string;
|
|
||||||
private urlArgs: string[];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly httpService: HttpService,
|
|
||||||
@Inject(GEOGRAPHY_CONFIGURATION_REPOSITORY)
|
|
||||||
private readonly configurationRepository: GetConfigurationRepositoryPort,
|
|
||||||
@Inject(GEODESIC) private readonly geodesic: GeodesicPort,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
route = async (
|
|
||||||
waypoints: Point[],
|
|
||||||
settings: GeorouterSettings,
|
|
||||||
): Promise<Route> => {
|
|
||||||
const geographyConfigurator: Configurator =
|
|
||||||
await this.configurationRepository.mget(
|
|
||||||
Domain.GEOGRAPHY,
|
|
||||||
GeographyKeyTypes,
|
|
||||||
);
|
|
||||||
this.url = [
|
|
||||||
geographyConfigurator.get<string>(GEOGRAPHY_CONFIG_GEOROUTER_URL),
|
|
||||||
'/route?',
|
|
||||||
].join('');
|
|
||||||
this._setDefaultUrlArgs();
|
|
||||||
this._setSettings(settings);
|
|
||||||
return this._getRoute(waypoints);
|
|
||||||
};
|
|
||||||
|
|
||||||
private _setDefaultUrlArgs = (): void => {
|
|
||||||
this.urlArgs = ['profile=car', 'points_encoded=false'];
|
|
||||||
};
|
|
||||||
|
|
||||||
private _setSettings = (settings: GeorouterSettings): void => {
|
|
||||||
if (settings.detailedDuration) {
|
|
||||||
this.urlArgs.push('details=time');
|
|
||||||
}
|
|
||||||
if (settings.detailedDistance) {
|
|
||||||
this.urlArgs.push('instructions=true');
|
|
||||||
} else {
|
|
||||||
this.urlArgs.push('instructions=false');
|
|
||||||
}
|
|
||||||
if (!settings.points) {
|
|
||||||
this.urlArgs.push('calc_points=false');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private _getRoute = async (waypoints: Point[]): Promise<Route> => {
|
|
||||||
const url: string = [
|
|
||||||
this.getUrl(),
|
|
||||||
'&point=',
|
|
||||||
waypoints
|
|
||||||
.map((point: Point) => [point.lat, point.lon].join('%2C'))
|
|
||||||
.join('&point='),
|
|
||||||
].join('');
|
|
||||||
return await lastValueFrom(
|
|
||||||
this.httpService.get(url).pipe(
|
|
||||||
map((response) => {
|
|
||||||
if (response.data) return this.createRoute(response);
|
|
||||||
throw new Error();
|
|
||||||
}),
|
|
||||||
catchError((error: AxiosError) => {
|
|
||||||
if (error.code == AxiosError.ERR_BAD_REQUEST) {
|
|
||||||
throw new RouteNotFoundException(
|
|
||||||
error,
|
|
||||||
'No route found for given coordinates',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw new GeorouterUnavailableException(error);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private getUrl = (): string => [this.url, this.urlArgs.join('&')].join('');
|
|
||||||
|
|
||||||
private createRoute = (
|
|
||||||
response: AxiosResponse<GraphhopperResponse>,
|
|
||||||
): Route => {
|
|
||||||
const route = {} as Route;
|
|
||||||
if (response.data.paths && response.data.paths[0]) {
|
|
||||||
const shortestPath = response.data.paths[0];
|
|
||||||
route.distance = shortestPath.distance ?? 0;
|
|
||||||
route.duration = shortestPath.time ? shortestPath.time / 1000 : 0;
|
|
||||||
if (shortestPath.points && shortestPath.points.coordinates) {
|
|
||||||
route.points = shortestPath.points.coordinates.map((coordinate) => ({
|
|
||||||
lon: coordinate[0],
|
|
||||||
lat: coordinate[1],
|
|
||||||
}));
|
|
||||||
const inverse = this.geodesic.inverse(
|
|
||||||
route.points[0].lon,
|
|
||||||
route.points[0].lat,
|
|
||||||
route.points[route.points.length - 1].lon,
|
|
||||||
route.points[route.points.length - 1].lat,
|
|
||||||
);
|
|
||||||
route.fwdAzimuth =
|
|
||||||
inverse.azimuth >= 0
|
|
||||||
? inverse.azimuth
|
|
||||||
: 360 - Math.abs(inverse.azimuth);
|
|
||||||
route.backAzimuth =
|
|
||||||
route.fwdAzimuth > 180
|
|
||||||
? route.fwdAzimuth - 180
|
|
||||||
: route.fwdAzimuth + 180;
|
|
||||||
route.distanceAzimuth = inverse.distance;
|
|
||||||
if (
|
|
||||||
shortestPath.details &&
|
|
||||||
shortestPath.details.time &&
|
|
||||||
shortestPath.snapped_waypoints &&
|
|
||||||
shortestPath.snapped_waypoints.coordinates
|
|
||||||
) {
|
|
||||||
let instructions: GraphhopperInstruction[] = [];
|
|
||||||
if (shortestPath.instructions)
|
|
||||||
instructions = shortestPath.instructions;
|
|
||||||
route.steps = this.generateSteps(
|
|
||||||
shortestPath.points.coordinates,
|
|
||||||
shortestPath.snapped_waypoints.coordinates,
|
|
||||||
shortestPath.details.time,
|
|
||||||
instructions,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return route;
|
|
||||||
};
|
|
||||||
|
|
||||||
private generateSteps = (
|
|
||||||
points: [[number, number]],
|
|
||||||
snappedWaypoints: [[number, number]],
|
|
||||||
durations: [[number, number, number]],
|
|
||||||
instructions: GraphhopperInstruction[],
|
|
||||||
): Step[] => {
|
|
||||||
const indices = this.getIndices(points, snappedWaypoints);
|
|
||||||
const times = this.getTimes(durations, indices);
|
|
||||||
const distances = this.getDistances(instructions, indices);
|
|
||||||
return indices.map((index) => {
|
|
||||||
const duration = times.find((time) => time.index == index);
|
|
||||||
if (!duration)
|
|
||||||
throw new Error(`Duration not found for waypoint #${index}`);
|
|
||||||
const distance = distances.find((distance) => distance.index == index);
|
|
||||||
if (!distance && instructions.length > 0)
|
|
||||||
throw new Error(`Distance not found for waypoint #${index}`);
|
|
||||||
return {
|
|
||||||
lon: points[index][1],
|
|
||||||
lat: points[index][0],
|
|
||||||
distance: distance?.distance,
|
|
||||||
duration: duration.duration,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private getIndices = (
|
|
||||||
points: [[number, number]],
|
|
||||||
snappedWaypoints: [[number, number]],
|
|
||||||
): number[] => {
|
|
||||||
const indices: number[] = snappedWaypoints.map(
|
|
||||||
(waypoint: [number, number]) =>
|
|
||||||
points.findIndex(
|
|
||||||
(point) => point[0] == waypoint[0] && point[1] == waypoint[1],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (indices.find((index: number) => index == -1) === undefined)
|
|
||||||
return indices;
|
|
||||||
const missedWaypoints = indices
|
|
||||||
.map(
|
|
||||||
(value, index) =>
|
|
||||||
<
|
|
||||||
{
|
|
||||||
index: number;
|
|
||||||
originIndex: number;
|
|
||||||
waypoint: number[];
|
|
||||||
nearest?: number;
|
|
||||||
distance: number;
|
|
||||||
}
|
|
||||||
>{
|
|
||||||
index: value,
|
|
||||||
originIndex: index,
|
|
||||||
waypoint: snappedWaypoints[index],
|
|
||||||
nearest: undefined,
|
|
||||||
distance: 999999999,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.filter((element) => element.index == -1);
|
|
||||||
for (const index in points) {
|
|
||||||
for (const missedWaypoint of missedWaypoints) {
|
|
||||||
const distance = this.geodesic.distance(
|
|
||||||
missedWaypoint.waypoint[0],
|
|
||||||
missedWaypoint.waypoint[1],
|
|
||||||
points[index][0],
|
|
||||||
points[index][1],
|
|
||||||
);
|
|
||||||
if (distance < missedWaypoint.distance) {
|
|
||||||
missedWaypoint.distance = distance;
|
|
||||||
missedWaypoint.nearest = parseInt(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const missedWaypoint of missedWaypoints) {
|
|
||||||
indices[missedWaypoint.originIndex] = missedWaypoint.nearest as number;
|
|
||||||
}
|
|
||||||
return indices;
|
|
||||||
};
|
|
||||||
|
|
||||||
private getTimes = (
|
|
||||||
durations: [[number, number, number]],
|
|
||||||
indices: number[],
|
|
||||||
): Array<{ index: number; duration: number }> => {
|
|
||||||
const times: Array<{ index: number; duration: number }> = [];
|
|
||||||
let duration = 0;
|
|
||||||
for (const [origin, destination, stepDuration] of durations) {
|
|
||||||
let indexFound = false;
|
|
||||||
const indexAsOrigin = indices.find((index) => index == origin);
|
|
||||||
if (
|
|
||||||
indexAsOrigin !== undefined &&
|
|
||||||
times.find((time) => origin == time.index) == undefined
|
|
||||||
) {
|
|
||||||
times.push({
|
|
||||||
index: indexAsOrigin,
|
|
||||||
duration: Math.round(stepDuration / 1000),
|
|
||||||
});
|
|
||||||
indexFound = true;
|
|
||||||
}
|
|
||||||
if (!indexFound) {
|
|
||||||
const indexAsDestination = indices.find(
|
|
||||||
(index) => index == destination,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
indexAsDestination !== undefined &&
|
|
||||||
times.find((time) => destination == time.index) == undefined
|
|
||||||
) {
|
|
||||||
times.push({
|
|
||||||
index: indexAsDestination,
|
|
||||||
duration: Math.round((duration + stepDuration) / 1000),
|
|
||||||
});
|
|
||||||
indexFound = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!indexFound) {
|
|
||||||
const indexInBetween = indices.find(
|
|
||||||
(index) => origin < index && index < destination,
|
|
||||||
);
|
|
||||||
if (indexInBetween !== undefined) {
|
|
||||||
times.push({
|
|
||||||
index: indexInBetween,
|
|
||||||
duration: Math.round((duration + stepDuration / 2) / 1000),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
duration += stepDuration;
|
|
||||||
}
|
|
||||||
return times;
|
|
||||||
};
|
|
||||||
|
|
||||||
private getDistances = (
|
|
||||||
instructions: GraphhopperInstruction[],
|
|
||||||
indices: number[],
|
|
||||||
): Array<{ index: number; distance: number }> => {
|
|
||||||
let distance = 0;
|
|
||||||
const distances: Array<{ index: number; distance: number }> = [
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
distance,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
for (const instruction of instructions) {
|
|
||||||
distance += instruction.distance;
|
|
||||||
if (
|
|
||||||
(instruction.sign == GraphhopperSign.SIGN_WAYPOINT ||
|
|
||||||
instruction.sign == GraphhopperSign.SIGN_FINISH) &&
|
|
||||||
indices.find((index) => index == instruction.interval[0]) !== undefined
|
|
||||||
) {
|
|
||||||
distances.push({
|
|
||||||
index: instruction.interval[0],
|
|
||||||
distance: Math.round(distance),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return distances;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type GraphhopperResponse = {
|
|
||||||
paths: [
|
|
||||||
{
|
|
||||||
distance: number;
|
|
||||||
weight: number;
|
|
||||||
time: number;
|
|
||||||
points_encoded: boolean;
|
|
||||||
bbox: number[];
|
|
||||||
points: GraphhopperCoordinates;
|
|
||||||
snapped_waypoints: GraphhopperCoordinates;
|
|
||||||
details: {
|
|
||||||
time: [[number, number, number]];
|
|
||||||
};
|
|
||||||
instructions: GraphhopperInstruction[];
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
type GraphhopperCoordinates = {
|
|
||||||
coordinates: [[number, number]];
|
|
||||||
};
|
|
||||||
|
|
||||||
type GraphhopperInstruction = {
|
|
||||||
distance: number;
|
|
||||||
heading: number;
|
|
||||||
sign: GraphhopperSign;
|
|
||||||
interval: [number, number];
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum GraphhopperSign {
|
|
||||||
SIGN_START = 0,
|
|
||||||
SIGN_FINISH = 4,
|
|
||||||
SIGN_WAYPOINT = 5,
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { Point } from '@modules/geography/core/domain/route.types';
|
|
||||||
|
|
||||||
export type GetRouteRequestDto = {
|
|
||||||
waypoints: Point[];
|
|
||||||
};
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { QueryBus } from '@nestjs/cqrs';
|
|
||||||
import { RouteResponseDto } from '../dtos/route.response.dto';
|
|
||||||
import { GetRouteRequestDto } from './dtos/get-route.request.dto';
|
|
||||||
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
|
|
||||||
import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query';
|
|
||||||
import { RouteMapper } from '@modules/geography/route.mapper';
|
|
||||||
import { Controller } from '@nestjs/common';
|
|
||||||
import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port';
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class GetBasicRouteController implements GetRouteControllerPort {
|
|
||||||
constructor(
|
|
||||||
private readonly queryBus: QueryBus,
|
|
||||||
private readonly mapper: RouteMapper,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async get(data: GetRouteRequestDto): Promise<RouteResponseDto> {
|
|
||||||
const route: RouteEntity = await this.queryBus.execute(
|
|
||||||
new GetRouteQuery(data.waypoints),
|
|
||||||
);
|
|
||||||
return this.mapper.toResponse(route);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { QueryBus } from '@nestjs/cqrs';
|
|
||||||
import { RouteResponseDto } from '../dtos/route.response.dto';
|
|
||||||
import { GetRouteRequestDto } from './dtos/get-route.request.dto';
|
|
||||||
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
|
|
||||||
import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query';
|
|
||||||
import { RouteMapper } from '@modules/geography/route.mapper';
|
|
||||||
import { Controller } from '@nestjs/common';
|
|
||||||
import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port';
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class GetDetailedRouteController implements GetRouteControllerPort {
|
|
||||||
constructor(
|
|
||||||
private readonly queryBus: QueryBus,
|
|
||||||
private readonly mapper: RouteMapper,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async get(data: GetRouteRequestDto): Promise<RouteResponseDto> {
|
|
||||||
const route: RouteEntity = await this.queryBus.execute(
|
|
||||||
new GetRouteQuery(data.waypoints, {
|
|
||||||
detailedDistance: true,
|
|
||||||
detailedDuration: true,
|
|
||||||
points: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return this.mapper.toResponse(route);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { Point, Step } from '@modules/geography/core/domain/route.types';
|
|
||||||
|
|
||||||
export class RouteResponseDto {
|
|
||||||
distance: number;
|
|
||||||
duration: number;
|
|
||||||
fwdAzimuth: number;
|
|
||||||
backAzimuth: number;
|
|
||||||
distanceAzimuth: number;
|
|
||||||
points: Point[];
|
|
||||||
steps?: Step[];
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { Mapper } from '@mobicoop/ddd-library';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { RouteEntity } from './core/domain/route.entity';
|
|
||||||
import { RouteResponseDto } from './interface/dtos/route.response.dto';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapper constructs objects that are used in different layers:
|
|
||||||
* Record is an object that is stored in a database,
|
|
||||||
* Entity is an object that is used in application domain layer,
|
|
||||||
* and a ResponseDTO is an object returned to a user (usually as json).
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RouteMapper
|
|
||||||
implements Mapper<RouteEntity, undefined, undefined, RouteResponseDto>
|
|
||||||
{
|
|
||||||
toResponse = (entity: RouteEntity): RouteResponseDto => {
|
|
||||||
const response = new RouteResponseDto();
|
|
||||||
response.distance = Math.round(entity.getProps().distance);
|
|
||||||
response.duration = Math.round(entity.getProps().duration);
|
|
||||||
response.fwdAzimuth = Math.round(entity.getProps().fwdAzimuth);
|
|
||||||
response.backAzimuth = Math.round(entity.getProps().backAzimuth);
|
|
||||||
response.distanceAzimuth = Math.round(entity.getProps().distanceAzimuth);
|
|
||||||
response.points = entity.getProps().points;
|
|
||||||
response.steps = entity.getProps().steps;
|
|
||||||
return response;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
import { GeorouterPort } from '@modules/geography/core/application/ports/georouter.port';
|
|
||||||
import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query';
|
|
||||||
import { GetRouteQueryHandler } from '@modules/geography/core/application/queries/get-route/get-route.query-handler';
|
|
||||||
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
|
|
||||||
import { Point } from '@modules/geography/core/domain/route.types';
|
|
||||||
import { GEOROUTER } from '@modules/geography/geography.di-tokens';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
|
|
||||||
const originWaypoint: Point = {
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
};
|
|
||||||
const destinationWaypoint: Point = {
|
|
||||||
lat: 48.8566,
|
|
||||||
lon: 2.3522,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockGeorouter: GeorouterPort = {
|
|
||||||
route: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Get route query handler', () => {
|
|
||||||
let getRoutequeryHandler: GetRouteQueryHandler;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: GEOROUTER,
|
|
||||||
useValue: mockGeorouter,
|
|
||||||
},
|
|
||||||
GetRouteQueryHandler,
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
getRoutequeryHandler =
|
|
||||||
module.get<GetRouteQueryHandler>(GetRouteQueryHandler);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(getRoutequeryHandler).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execution', () => {
|
|
||||||
it('should get a route', async () => {
|
|
||||||
const getRoutequery = new GetRouteQuery(
|
|
||||||
[originWaypoint, destinationWaypoint],
|
|
||||||
{
|
|
||||||
detailedDistance: false,
|
|
||||||
detailedDuration: false,
|
|
||||||
points: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
RouteEntity.create = jest.fn().mockReturnValue({
|
|
||||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
|
||||||
});
|
|
||||||
const result = await getRoutequeryHandler.execute(getRoutequery);
|
|
||||||
expect(result.id).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library';
|
|
||||||
import { Point } from '@modules/geography/core/domain/value-objects/point.value-object';
|
|
||||||
|
|
||||||
describe('Point value object', () => {
|
|
||||||
it('should create a point value object', () => {
|
|
||||||
const pointVO = new Point({
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
});
|
|
||||||
expect(pointVO.lat).toBe(48.689445);
|
|
||||||
expect(pointVO.lon).toBe(6.17651);
|
|
||||||
});
|
|
||||||
it('should throw an exception if longitude is invalid', () => {
|
|
||||||
expect(() => {
|
|
||||||
new Point({
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 186.17651,
|
|
||||||
});
|
|
||||||
}).toThrow(ArgumentOutOfRangeException);
|
|
||||||
expect(() => {
|
|
||||||
new Point({
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: -186.17651,
|
|
||||||
});
|
|
||||||
}).toThrow(ArgumentOutOfRangeException);
|
|
||||||
});
|
|
||||||
it('should throw an exception if latitude is invalid', () => {
|
|
||||||
expect(() => {
|
|
||||||
new Point({
|
|
||||||
lat: 148.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
});
|
|
||||||
}).toThrow(ArgumentOutOfRangeException);
|
|
||||||
expect(() => {
|
|
||||||
new Point({
|
|
||||||
lat: -148.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
});
|
|
||||||
}).toThrow(ArgumentOutOfRangeException);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,70 +0,0 @@
|
||||||
import { GeorouterPort } from '@modules/geography/core/application/ports/georouter.port';
|
|
||||||
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
|
|
||||||
import { RouteNotFoundException } from '@modules/geography/core/domain/route.errors';
|
|
||||||
import {
|
|
||||||
Point,
|
|
||||||
CreateRouteProps,
|
|
||||||
} from '@modules/geography/core/domain/route.types';
|
|
||||||
|
|
||||||
const originPoint: Point = {
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
};
|
|
||||||
const destinationPoint: Point = {
|
|
||||||
lat: 48.8566,
|
|
||||||
lon: 2.3522,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockGeorouter: GeorouterPort = {
|
|
||||||
route: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => ({
|
|
||||||
distance: 350101,
|
|
||||||
duration: 14422,
|
|
||||||
fwdAzimuth: 273,
|
|
||||||
backAzimuth: 93,
|
|
||||||
distanceAzimuth: 336544,
|
|
||||||
points: [
|
|
||||||
{
|
|
||||||
lon: 6.1765102,
|
|
||||||
lat: 48.689445,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 4.984578,
|
|
||||||
lat: 48.725687,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 2.3522,
|
|
||||||
lat: 48.8566,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
steps: [],
|
|
||||||
}))
|
|
||||||
.mockImplementationOnce(() => []),
|
|
||||||
};
|
|
||||||
|
|
||||||
const createRouteProps: CreateRouteProps = {
|
|
||||||
waypoints: [originPoint, destinationPoint],
|
|
||||||
georouter: mockGeorouter,
|
|
||||||
georouterSettings: {
|
|
||||||
points: true,
|
|
||||||
detailedDistance: false,
|
|
||||||
detailedDuration: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Route entity create', () => {
|
|
||||||
it('should create a new entity', async () => {
|
|
||||||
const route: RouteEntity = await RouteEntity.create(createRouteProps);
|
|
||||||
expect(route.id.length).toBe(36);
|
|
||||||
expect(route.getProps().duration).toBe(14422);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an exception if route is not found', async () => {
|
|
||||||
try {
|
|
||||||
await RouteEntity.create(createRouteProps);
|
|
||||||
} catch (e: any) {
|
|
||||||
expect(e).toBeInstanceOf(RouteNotFoundException);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,76 +0,0 @@
|
||||||
import {
|
|
||||||
ArgumentInvalidException,
|
|
||||||
ArgumentOutOfRangeException,
|
|
||||||
} from '@mobicoop/ddd-library';
|
|
||||||
import { Step } from '@modules/geography/core/domain/value-objects/step.value-object';
|
|
||||||
|
|
||||||
describe('Step value object', () => {
|
|
||||||
it('should create a step value object', () => {
|
|
||||||
const stepVO = new Step({
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
duration: 150,
|
|
||||||
distance: 12000,
|
|
||||||
});
|
|
||||||
expect(stepVO.duration).toBe(150);
|
|
||||||
expect(stepVO.distance).toBe(12000);
|
|
||||||
expect(stepVO.lat).toBe(48.689445);
|
|
||||||
expect(stepVO.lon).toBe(6.17651);
|
|
||||||
});
|
|
||||||
it('should throw an exception if longitude is invalid', () => {
|
|
||||||
expect(() => {
|
|
||||||
new Step({
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 186.17651,
|
|
||||||
duration: 150,
|
|
||||||
distance: 12000,
|
|
||||||
});
|
|
||||||
}).toThrow(ArgumentOutOfRangeException);
|
|
||||||
expect(() => {
|
|
||||||
new Step({
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: -186.17651,
|
|
||||||
duration: 150,
|
|
||||||
distance: 12000,
|
|
||||||
});
|
|
||||||
}).toThrow(ArgumentOutOfRangeException);
|
|
||||||
});
|
|
||||||
it('should throw an exception if latitude is invalid', () => {
|
|
||||||
expect(() => {
|
|
||||||
new Step({
|
|
||||||
lat: 248.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
duration: 150,
|
|
||||||
distance: 12000,
|
|
||||||
});
|
|
||||||
}).toThrow(ArgumentOutOfRangeException);
|
|
||||||
expect(() => {
|
|
||||||
new Step({
|
|
||||||
lat: -148.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
duration: 150,
|
|
||||||
distance: 12000,
|
|
||||||
});
|
|
||||||
}).toThrow(ArgumentOutOfRangeException);
|
|
||||||
});
|
|
||||||
it('should throw an exception if distance is invalid', () => {
|
|
||||||
expect(() => {
|
|
||||||
new Step({
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
duration: 150,
|
|
||||||
distance: -12000,
|
|
||||||
});
|
|
||||||
}).toThrow(ArgumentInvalidException);
|
|
||||||
});
|
|
||||||
it('should throw an exception if duration is invalid', () => {
|
|
||||||
expect(() => {
|
|
||||||
new Step({
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
duration: -150,
|
|
||||||
distance: 12000,
|
|
||||||
});
|
|
||||||
}).toThrow(ArgumentInvalidException);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { Geodesic } from '@modules/geography/infrastructure/geodesic';
|
|
||||||
|
|
||||||
describe('Matcher geodesic', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
const geodesic: Geodesic = new Geodesic();
|
|
||||||
expect(geodesic).toBeDefined();
|
|
||||||
});
|
|
||||||
it('should get inverse values', () => {
|
|
||||||
const geodesic: Geodesic = new Geodesic();
|
|
||||||
const inv = geodesic.inverse(0, 0, 1, 1);
|
|
||||||
expect(Math.round(inv.azimuth as number)).toBe(45);
|
|
||||||
expect(Math.round(inv.distance as number)).toBe(156900);
|
|
||||||
});
|
|
||||||
it('should get azimuth value', () => {
|
|
||||||
const geodesic: Geodesic = new Geodesic();
|
|
||||||
const azimuth = geodesic.azimuth(0, 0, 1, 1);
|
|
||||||
expect(Math.round(azimuth as number)).toBe(45);
|
|
||||||
});
|
|
||||||
it('should get distance value', () => {
|
|
||||||
const geodesic: Geodesic = new Geodesic();
|
|
||||||
const distance = geodesic.distance(0, 0, 1, 1);
|
|
||||||
expect(Math.round(distance as number)).toBe(156900);
|
|
||||||
});
|
|
||||||
it('should throw an exception if inverse fails', () => {
|
|
||||||
const geodesic: Geodesic = new Geodesic();
|
|
||||||
expect(() => {
|
|
||||||
geodesic.inverse(7.74547, 48.583035, 7.74547, 48.583036);
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
it('should throw an exception if azimuth fails', () => {
|
|
||||||
const geodesic: Geodesic = new Geodesic();
|
|
||||||
expect(() => {
|
|
||||||
geodesic.azimuth(7.74547, 48.583035, 7.74547, 48.583036);
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,508 +0,0 @@
|
||||||
import {
|
|
||||||
Domain,
|
|
||||||
KeyType,
|
|
||||||
Configurator,
|
|
||||||
GetConfigurationRepositoryPort,
|
|
||||||
} from '@mobicoop/configuration-module';
|
|
||||||
import { GeodesicPort } from '@modules/geography/core/application/ports/geodesic.port';
|
|
||||||
import {
|
|
||||||
GeorouterUnavailableException,
|
|
||||||
RouteNotFoundException,
|
|
||||||
} from '@modules/geography/core/domain/route.errors';
|
|
||||||
import { Route, Step } from '@modules/geography/core/domain/route.types';
|
|
||||||
import { GEOGRAPHY_CONFIG_GEOROUTER_URL } from '@modules/geography/geography.constants';
|
|
||||||
import {
|
|
||||||
GEODESIC,
|
|
||||||
GEOGRAPHY_CONFIGURATION_REPOSITORY,
|
|
||||||
} from '@modules/geography/geography.di-tokens';
|
|
||||||
import { GraphhopperGeorouter } from '@modules/geography/infrastructure/graphhopper-georouter';
|
|
||||||
import { HttpService } from '@nestjs/axios';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { of, throwError } from 'rxjs';
|
|
||||||
|
|
||||||
const mockHttpService = {
|
|
||||||
get: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return throwError(
|
|
||||||
() => new AxiosError('Axios error', AxiosError.ERR_BAD_REQUEST),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return throwError(() => 'Router unavailable');
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return of({
|
|
||||||
status: 200,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return of({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
paths: [
|
|
||||||
{
|
|
||||||
distance: 50000,
|
|
||||||
time: 1800000,
|
|
||||||
snapped_waypoints: {
|
|
||||||
coordinates: [
|
|
||||||
[0, 0],
|
|
||||||
[10, 10],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return of({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
paths: [
|
|
||||||
{
|
|
||||||
distance: 50000,
|
|
||||||
time: 1800000,
|
|
||||||
points: {
|
|
||||||
coordinates: [
|
|
||||||
[0, 0],
|
|
||||||
[1, 1],
|
|
||||||
[2, 2],
|
|
||||||
[3, 3],
|
|
||||||
[4, 4],
|
|
||||||
[5, 5],
|
|
||||||
[6, 6],
|
|
||||||
[7, 7],
|
|
||||||
[8, 8],
|
|
||||||
[9, 9],
|
|
||||||
[10, 10],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
snapped_waypoints: {
|
|
||||||
coordinates: [
|
|
||||||
[0, 0],
|
|
||||||
[10, 10],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return of({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
paths: [
|
|
||||||
{
|
|
||||||
distance: 50000,
|
|
||||||
time: 1800000,
|
|
||||||
points: {
|
|
||||||
coordinates: [
|
|
||||||
[0, 0],
|
|
||||||
[1, 1],
|
|
||||||
[2, 2],
|
|
||||||
[3, 3],
|
|
||||||
[4, 4],
|
|
||||||
[5, 5],
|
|
||||||
[6, 6],
|
|
||||||
[7, 7],
|
|
||||||
[8, 8],
|
|
||||||
[9, 9],
|
|
||||||
[10, 10],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
details: {
|
|
||||||
time: [
|
|
||||||
[0, 1, 180000],
|
|
||||||
[1, 2, 180000],
|
|
||||||
[2, 3, 180000],
|
|
||||||
[3, 4, 180000],
|
|
||||||
[4, 5, 180000],
|
|
||||||
[5, 6, 180000],
|
|
||||||
[6, 7, 180000],
|
|
||||||
[7, 9, 360000],
|
|
||||||
[9, 10, 180000],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
snapped_waypoints: {
|
|
||||||
coordinates: [
|
|
||||||
[0, 0],
|
|
||||||
[10, 10],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return of({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
paths: [
|
|
||||||
{
|
|
||||||
distance: 50000,
|
|
||||||
time: 1800000,
|
|
||||||
points: {
|
|
||||||
coordinates: [
|
|
||||||
[0, 0],
|
|
||||||
[1, 1],
|
|
||||||
[2, 2],
|
|
||||||
[3, 3],
|
|
||||||
[4, 4],
|
|
||||||
[7, 7],
|
|
||||||
[8, 8],
|
|
||||||
[9, 9],
|
|
||||||
[10, 10],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
snapped_waypoints: {
|
|
||||||
coordinates: [
|
|
||||||
[0, 0],
|
|
||||||
[5, 5],
|
|
||||||
[10, 10],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
details: {
|
|
||||||
time: [
|
|
||||||
[0, 1, 180000],
|
|
||||||
[1, 2, 180000],
|
|
||||||
[2, 3, 180000],
|
|
||||||
[3, 4, 180000],
|
|
||||||
[4, 7, 540000],
|
|
||||||
[7, 9, 360000],
|
|
||||||
[9, 10, 180000],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return of({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
paths: [
|
|
||||||
{
|
|
||||||
distance: 50000,
|
|
||||||
time: 1800000,
|
|
||||||
points: {
|
|
||||||
coordinates: [
|
|
||||||
[0, 0],
|
|
||||||
[1, 1],
|
|
||||||
[2, 2],
|
|
||||||
[3, 3],
|
|
||||||
[4, 4],
|
|
||||||
[5, 5],
|
|
||||||
[6, 6],
|
|
||||||
[7, 7],
|
|
||||||
[8, 8],
|
|
||||||
[9, 9],
|
|
||||||
[10, 10],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
snapped_waypoints: {
|
|
||||||
coordinates: [
|
|
||||||
[0, 0],
|
|
||||||
[5, 5],
|
|
||||||
[10, 10],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
details: {
|
|
||||||
time: [
|
|
||||||
[0, 1, 180000],
|
|
||||||
[1, 2, 180000],
|
|
||||||
[2, 3, 180000],
|
|
||||||
[3, 4, 180000],
|
|
||||||
[4, 7, 540000],
|
|
||||||
[7, 9, 360000],
|
|
||||||
[9, 10, 180000],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
instructions: [
|
|
||||||
{
|
|
||||||
distance: 25000,
|
|
||||||
sign: 0,
|
|
||||||
interval: [0, 5],
|
|
||||||
text: 'Some instructions',
|
|
||||||
time: 900000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
distance: 0,
|
|
||||||
sign: 5,
|
|
||||||
interval: [5, 5],
|
|
||||||
text: 'Waypoint 1',
|
|
||||||
time: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
distance: 25000,
|
|
||||||
sign: 2,
|
|
||||||
interval: [5, 10],
|
|
||||||
text: 'Some instructions',
|
|
||||||
time: 900000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
distance: 0.0,
|
|
||||||
sign: 4,
|
|
||||||
interval: [10, 10],
|
|
||||||
text: 'Arrive at destination',
|
|
||||||
time: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockGeodesic: GeodesicPort = {
|
|
||||||
inverse: jest.fn().mockImplementation(() => ({
|
|
||||||
azimuth: 45,
|
|
||||||
distance: 50000,
|
|
||||||
})),
|
|
||||||
azimuth: jest.fn().mockImplementation(() => 45),
|
|
||||||
distance: jest.fn().mockImplementation(() => 50000),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockConfigurationRepository: GetConfigurationRepositoryPort = {
|
|
||||||
get: jest.fn(),
|
|
||||||
mget: jest.fn().mockImplementation(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
(domain: Domain, keyTypes: KeyType[]) => {
|
|
||||||
switch (domain) {
|
|
||||||
case Domain.GEOGRAPHY:
|
|
||||||
return new Configurator(Domain.GEOGRAPHY, [
|
|
||||||
{
|
|
||||||
domain: Domain.GEOGRAPHY,
|
|
||||||
key: GEOGRAPHY_CONFIG_GEOROUTER_URL,
|
|
||||||
value: 'http://localhost:8989',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Graphhopper Georouter', () => {
|
|
||||||
let graphhopperGeorouter: GraphhopperGeorouter;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [],
|
|
||||||
providers: [
|
|
||||||
GraphhopperGeorouter,
|
|
||||||
{
|
|
||||||
provide: HttpService,
|
|
||||||
useValue: mockHttpService,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: GEOGRAPHY_CONFIGURATION_REPOSITORY,
|
|
||||||
useValue: mockConfigurationRepository,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: GEODESIC,
|
|
||||||
useValue: mockGeodesic,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
graphhopperGeorouter =
|
|
||||||
module.get<GraphhopperGeorouter>(GraphhopperGeorouter);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(graphhopperGeorouter).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail if route is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
graphhopperGeorouter.route(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
lon: 0,
|
|
||||||
lat: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 1,
|
|
||||||
lat: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
detailedDistance: false,
|
|
||||||
detailedDuration: false,
|
|
||||||
points: false,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(RouteNotFoundException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail if georouter is unavailable', async () => {
|
|
||||||
await expect(
|
|
||||||
graphhopperGeorouter.route(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
lon: 0,
|
|
||||||
lat: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 1,
|
|
||||||
lat: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
detailedDistance: false,
|
|
||||||
detailedDuration: false,
|
|
||||||
points: false,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(GeorouterUnavailableException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail if georouter response is corrupted', async () => {
|
|
||||||
await expect(
|
|
||||||
graphhopperGeorouter.route(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
lon: 0,
|
|
||||||
lat: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 1,
|
|
||||||
lat: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
detailedDistance: false,
|
|
||||||
detailedDuration: false,
|
|
||||||
points: false,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(GeorouterUnavailableException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a basic route', async () => {
|
|
||||||
const route: Route = await graphhopperGeorouter.route(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
lon: 0,
|
|
||||||
lat: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 10,
|
|
||||||
lat: 10,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
detailedDistance: false,
|
|
||||||
detailedDuration: false,
|
|
||||||
points: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(route.distance).toBe(50000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a route with points', async () => {
|
|
||||||
const route: Route = await graphhopperGeorouter.route(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
lon: 0,
|
|
||||||
lat: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 10,
|
|
||||||
lat: 10,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
detailedDistance: false,
|
|
||||||
detailedDuration: false,
|
|
||||||
points: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(route.distance).toBe(50000);
|
|
||||||
expect(route.duration).toBe(1800);
|
|
||||||
expect(route.fwdAzimuth).toBe(45);
|
|
||||||
expect(route.backAzimuth).toBe(225);
|
|
||||||
expect(route.points).toHaveLength(11);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a route with points and time', async () => {
|
|
||||||
const route: Route = await graphhopperGeorouter.route(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
lon: 0,
|
|
||||||
lat: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 10,
|
|
||||||
lat: 10,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
detailedDistance: false,
|
|
||||||
detailedDuration: true,
|
|
||||||
points: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(route.steps).toHaveLength(2);
|
|
||||||
expect((route.steps as Step[])[1].duration).toBe(1800);
|
|
||||||
expect((route.steps as Step[])[1].distance).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create one route with points and missed waypoints extrapolations', async () => {
|
|
||||||
const route: Route = await graphhopperGeorouter.route(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
lon: 0,
|
|
||||||
lat: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 5,
|
|
||||||
lat: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 10,
|
|
||||||
lat: 10,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
detailedDistance: false,
|
|
||||||
detailedDuration: true,
|
|
||||||
points: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(route.steps).toHaveLength(3);
|
|
||||||
expect(route.distance).toBe(50000);
|
|
||||||
expect(route.duration).toBe(1800);
|
|
||||||
expect(route.fwdAzimuth).toBe(45);
|
|
||||||
expect(route.backAzimuth).toBe(225);
|
|
||||||
expect(route.points.length).toBe(9);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a route with points, time and distance', async () => {
|
|
||||||
const route: Route = await graphhopperGeorouter.route(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
lon: 0,
|
|
||||||
lat: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 10,
|
|
||||||
lat: 10,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
detailedDistance: true,
|
|
||||||
detailedDuration: true,
|
|
||||||
points: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(route.steps).toHaveLength(3);
|
|
||||||
expect((route.steps as Step[])[1].duration).toBe(990);
|
|
||||||
expect((route.steps as Step[])[1].distance).toBe(25000);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,63 +0,0 @@
|
||||||
import { GetBasicRouteController } from '@modules/geography/interface/controllers/get-basic-route.controller';
|
|
||||||
import { RouteMapper } from '@modules/geography/route.mapper';
|
|
||||||
import { QueryBus } from '@nestjs/cqrs';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
|
|
||||||
const mockQueryBus = {
|
|
||||||
execute: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockRouteMapper = {
|
|
||||||
toPersistence: jest.fn(),
|
|
||||||
toDomain: jest.fn(),
|
|
||||||
toResponse: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Get Basic Route Controller', () => {
|
|
||||||
let getBasicRouteController: GetBasicRouteController;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: QueryBus,
|
|
||||||
useValue: mockQueryBus,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: RouteMapper,
|
|
||||||
useValue: mockRouteMapper,
|
|
||||||
},
|
|
||||||
GetBasicRouteController,
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
getBasicRouteController = module.get<GetBasicRouteController>(
|
|
||||||
GetBasicRouteController,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(getBasicRouteController).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get a route', async () => {
|
|
||||||
jest.spyOn(mockQueryBus, 'execute');
|
|
||||||
await getBasicRouteController.get({
|
|
||||||
waypoints: [
|
|
||||||
{
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lat: 48.8566,
|
|
||||||
lon: 2.3522,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,63 +0,0 @@
|
||||||
import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller';
|
|
||||||
import { RouteMapper } from '@modules/geography/route.mapper';
|
|
||||||
import { QueryBus } from '@nestjs/cqrs';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
|
|
||||||
const mockQueryBus = {
|
|
||||||
execute: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockRouteMapper = {
|
|
||||||
toPersistence: jest.fn(),
|
|
||||||
toDomain: jest.fn(),
|
|
||||||
toResponse: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Get Detailed Route Controller', () => {
|
|
||||||
let getDetailedRouteController: GetDetailedRouteController;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: QueryBus,
|
|
||||||
useValue: mockQueryBus,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: RouteMapper,
|
|
||||||
useValue: mockRouteMapper,
|
|
||||||
},
|
|
||||||
GetDetailedRouteController,
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
getDetailedRouteController = module.get<GetDetailedRouteController>(
|
|
||||||
GetDetailedRouteController,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(getDetailedRouteController).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get a route', async () => {
|
|
||||||
jest.spyOn(mockQueryBus, 'execute');
|
|
||||||
await getDetailedRouteController.get({
|
|
||||||
waypoints: [
|
|
||||||
{
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lat: 48.8566,
|
|
||||||
lon: 2.3522,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,45 +0,0 @@
|
||||||
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
|
|
||||||
import { RouteMapper } from '@modules/geography/route.mapper';
|
|
||||||
import { Test } from '@nestjs/testing';
|
|
||||||
|
|
||||||
describe('Route Mapper', () => {
|
|
||||||
let routeMapper: RouteMapper;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module = await Test.createTestingModule({
|
|
||||||
providers: [RouteMapper],
|
|
||||||
}).compile();
|
|
||||||
routeMapper = module.get<RouteMapper>(RouteMapper);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(routeMapper).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should map domain entity to response', async () => {
|
|
||||||
const now = new Date();
|
|
||||||
const routeEntity: RouteEntity = new RouteEntity({
|
|
||||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
props: {
|
|
||||||
distance: 23000,
|
|
||||||
duration: 900,
|
|
||||||
fwdAzimuth: 283,
|
|
||||||
backAzimuth: 93,
|
|
||||||
distanceAzimuth: 19840,
|
|
||||||
points: [
|
|
||||||
{
|
|
||||||
lon: 6.1765103,
|
|
||||||
lat: 48.689446,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 2.3523,
|
|
||||||
lat: 48.8567,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(routeMapper.toResponse(routeEntity).distance).toBe(23000);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in New Issue