mirror of
https://gitlab.com/mobicoop/v3/service/matcher.git
synced 2025-12-31 02:42:41 +00:00
refactor to ddh, first commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { IFindTimezone } from '../../domain/interfaces/timezone-finder.interface';
|
||||
import { find } from 'geo-tz';
|
||||
|
||||
@Injectable()
|
||||
export class GeoTimezoneFinder implements IFindTimezone {
|
||||
timezones = (lon: number, lat: number): string[] => find(lat, lon);
|
||||
}
|
||||
27
old/modules/geography/adapters/secondaries/geodesic.ts
Normal file
27
old/modules/geography/adapters/secondaries/geodesic.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Geodesic as Geolib, GeodesicClass } from 'geographiclib-geodesic';
|
||||
import { IGeodesic } from '../../domain/interfaces/geodesic.interface';
|
||||
|
||||
@Injectable()
|
||||
export class Geodesic implements IGeodesic {
|
||||
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,
|
||||
);
|
||||
return { azimuth, distance };
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ICreateGeorouter } from '../../domain/interfaces/georouter-creator.interface';
|
||||
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
|
||||
import { GraphhopperGeorouter } from './graphhopper-georouter';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { Geodesic } from './geodesic';
|
||||
import { GeographyException } from '../../exceptions/geography.exception';
|
||||
import { ExceptionCode } from '../../../utils/exception-code.enum';
|
||||
|
||||
@Injectable()
|
||||
export class GeorouterCreator implements ICreateGeorouter {
|
||||
constructor(
|
||||
private readonly httpService: HttpService,
|
||||
private readonly geodesic: Geodesic,
|
||||
) {}
|
||||
|
||||
create = (type: string, url: string): IGeorouter => {
|
||||
switch (type) {
|
||||
case 'graphhopper':
|
||||
return new GraphhopperGeorouter(url, this.httpService, this.geodesic);
|
||||
default:
|
||||
throw new GeographyException(
|
||||
ExceptionCode.INVALID_ARGUMENT,
|
||||
'Unknown geocoder',
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { catchError, lastValueFrom, map } from 'rxjs';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { IGeodesic } from '../../domain/interfaces/geodesic.interface';
|
||||
import { GeorouterSettings } from '../../domain/types/georouter-settings.type';
|
||||
import { Path } from '../../domain/types/path.type';
|
||||
import { NamedRoute } from '../../domain/types/named-route';
|
||||
import { GeographyException } from '../../exceptions/geography.exception';
|
||||
import { ExceptionCode } from '../../../utils/exception-code.enum';
|
||||
import { Route } from '../../domain/entities/route';
|
||||
import { SpacetimePoint } from '../../domain/entities/spacetime-point';
|
||||
|
||||
@Injectable()
|
||||
export class GraphhopperGeorouter implements IGeorouter {
|
||||
private url: string;
|
||||
private urlArgs: string[];
|
||||
private withTime: boolean;
|
||||
private withPoints: boolean;
|
||||
private withDistance: boolean;
|
||||
private paths: Path[];
|
||||
private httpService: HttpService;
|
||||
private geodesic: IGeodesic;
|
||||
|
||||
constructor(url: string, httpService: HttpService, geodesic: IGeodesic) {
|
||||
this.url = url + '/route?';
|
||||
this.httpService = httpService;
|
||||
this.geodesic = geodesic;
|
||||
}
|
||||
|
||||
route = async (
|
||||
paths: Path[],
|
||||
settings: GeorouterSettings,
|
||||
): Promise<NamedRoute[]> => {
|
||||
this.setDefaultUrlArgs();
|
||||
this.setWithTime(settings.withTime);
|
||||
this.setWithPoints(settings.withPoints);
|
||||
this.setWithDistance(settings.withDistance);
|
||||
this.paths = paths;
|
||||
return await this.getRoutes();
|
||||
};
|
||||
|
||||
private setDefaultUrlArgs = (): void => {
|
||||
this.urlArgs = ['vehicle=car', 'weighting=fastest', 'points_encoded=false'];
|
||||
};
|
||||
|
||||
private setWithTime = (withTime: boolean): void => {
|
||||
this.withTime = withTime;
|
||||
if (withTime) {
|
||||
this.urlArgs.push('details=time');
|
||||
}
|
||||
};
|
||||
|
||||
private setWithPoints = (withPoints: boolean): void => {
|
||||
this.withPoints = withPoints;
|
||||
if (!withPoints) {
|
||||
this.urlArgs.push('calc_points=false');
|
||||
}
|
||||
};
|
||||
|
||||
private setWithDistance = (withDistance: boolean): void => {
|
||||
this.withDistance = withDistance;
|
||||
if (withDistance) {
|
||||
this.urlArgs.push('instructions=true');
|
||||
} else {
|
||||
this.urlArgs.push('instructions=false');
|
||||
}
|
||||
};
|
||||
|
||||
private getRoutes = async (): Promise<NamedRoute[]> => {
|
||||
const routes = Promise.all(
|
||||
this.paths.map(async (path) => {
|
||||
const url: string = [
|
||||
this.getUrl(),
|
||||
'&point=',
|
||||
path.points
|
||||
.map((point) => [point.lat, point.lon].join('%2C'))
|
||||
.join('&point='),
|
||||
].join('');
|
||||
const route = await lastValueFrom(
|
||||
this.httpService.get(url).pipe(
|
||||
map((res) => (res.data ? this.createRoute(res) : undefined)),
|
||||
catchError((error: AxiosError) => {
|
||||
if (error.code == AxiosError.ERR_BAD_REQUEST) {
|
||||
throw new GeographyException(
|
||||
ExceptionCode.OUT_OF_RANGE,
|
||||
'No route found for given coordinates',
|
||||
);
|
||||
}
|
||||
throw new GeographyException(
|
||||
ExceptionCode.UNAVAILABLE,
|
||||
'Georouter unavailable : ' + error.message,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
return <NamedRoute>{
|
||||
key: path.key,
|
||||
route,
|
||||
};
|
||||
}),
|
||||
);
|
||||
return routes;
|
||||
};
|
||||
|
||||
private getUrl = (): string => {
|
||||
return [this.url, this.urlArgs.join('&')].join('');
|
||||
};
|
||||
|
||||
private createRoute = (
|
||||
response: AxiosResponse<GraphhopperResponse>,
|
||||
): Route => {
|
||||
const route = new Route(this.geodesic);
|
||||
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.setPoints(
|
||||
shortestPath.points.coordinates.map((coordinate) => ({
|
||||
lon: coordinate[0],
|
||||
lat: coordinate[1],
|
||||
})),
|
||||
);
|
||||
if (
|
||||
shortestPath.details &&
|
||||
shortestPath.details.time &&
|
||||
shortestPath.snapped_waypoints &&
|
||||
shortestPath.snapped_waypoints.coordinates
|
||||
) {
|
||||
let instructions: GraphhopperInstruction[] = [];
|
||||
if (shortestPath.instructions)
|
||||
instructions = shortestPath.instructions;
|
||||
route.setSpacetimePoints(
|
||||
this.generateSpacetimePoints(
|
||||
shortestPath.points.coordinates,
|
||||
shortestPath.snapped_waypoints.coordinates,
|
||||
shortestPath.details.time,
|
||||
instructions,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return route;
|
||||
};
|
||||
|
||||
private generateSpacetimePoints = (
|
||||
points: Array<number[]>,
|
||||
snappedWaypoints: Array<number[]>,
|
||||
durations: Array<number[]>,
|
||||
instructions: GraphhopperInstruction[],
|
||||
): SpacetimePoint[] => {
|
||||
const indices = this.getIndices(points, snappedWaypoints);
|
||||
const times = this.getTimes(durations, indices);
|
||||
const distances = this.getDistances(instructions, indices);
|
||||
return indices.map(
|
||||
(index) =>
|
||||
new SpacetimePoint(
|
||||
{ lon: points[index][1], lat: points[index][0] },
|
||||
times.find((time) => time.index == index)?.duration,
|
||||
distances.find((distance) => distance.index == index)?.distance,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
private getIndices = (
|
||||
points: Array<number[]>,
|
||||
snappedWaypoints: Array<number[]>,
|
||||
): number[] => {
|
||||
const indices = snappedWaypoints.map((waypoint) =>
|
||||
points.findIndex(
|
||||
(point) => point[0] == waypoint[0] && point[1] == waypoint[1],
|
||||
),
|
||||
);
|
||||
if (indices.find((index) => 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 inverse = this.geodesic.inverse(
|
||||
missedWaypoint.waypoint[0],
|
||||
missedWaypoint.waypoint[1],
|
||||
points[index][0],
|
||||
points[index][1],
|
||||
);
|
||||
if (inverse.distance < missedWaypoint.distance) {
|
||||
missedWaypoint.distance = inverse.distance;
|
||||
missedWaypoint.nearest = parseInt(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const missedWaypoint of missedWaypoints) {
|
||||
indices[missedWaypoint.originIndex] = missedWaypoint.nearest;
|
||||
}
|
||||
return indices;
|
||||
};
|
||||
|
||||
private getTimes = (
|
||||
durations: Array<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: Array<number[]>;
|
||||
};
|
||||
instructions: GraphhopperInstruction[];
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
type GraphhopperCoordinates = {
|
||||
coordinates: Array<number[]>;
|
||||
};
|
||||
|
||||
type GraphhopperInstruction = {
|
||||
distance: number;
|
||||
heading: number;
|
||||
sign: GraphhopperSign;
|
||||
interval: number[];
|
||||
text: string;
|
||||
};
|
||||
|
||||
enum GraphhopperSign {
|
||||
SIGN_START = 0,
|
||||
SIGN_FINISH = 4,
|
||||
SIGN_WAYPOINT = 5,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Coordinate } from '../../domain/entities/coordinate';
|
||||
import { IEncodeDirection } from '../../domain/interfaces/direction-encoder.interface';
|
||||
|
||||
export class PostgresDirectionEncoder implements IEncodeDirection {
|
||||
encode = (coordinates: Coordinate[]): string =>
|
||||
[
|
||||
"'LINESTRING(",
|
||||
coordinates.map((point) => [point.lon, point.lat].join(' ')).join(),
|
||||
")'",
|
||||
].join('');
|
||||
}
|
||||
Reference in New Issue
Block a user