222 lines
8.7 KiB
C#
222 lines
8.7 KiB
C#
using AutobusApi.Application.Common.Exceptions;
|
|
using AutobusApi.Application.Common.Interfaces;
|
|
using AutoMapper;
|
|
using AutoMapper.QueryableExtensions;
|
|
using FluentValidation.Results;
|
|
using MediatR;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using QuikGraph;
|
|
|
|
namespace AutobusApi.Application.RouteSearch;
|
|
|
|
public class RouteSearchQueryHandler : IRequestHandler<RouteSearchQuery, List<RouteWithTransfersDto>>
|
|
{
|
|
private readonly IApplicationDbContext _dbContext;
|
|
private readonly IMapper _mapper;
|
|
|
|
public RouteSearchQueryHandler(
|
|
IApplicationDbContext dbContext,
|
|
IMapper mapper)
|
|
{
|
|
_dbContext = dbContext;
|
|
_mapper = mapper;
|
|
}
|
|
|
|
public async Task<List<RouteWithTransfersDto>> Handle(
|
|
RouteSearchQuery request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var departureEnrollmentAddressVertex = await GetEnrollmentAddressVertexAsync(request.DepartureAddressId);
|
|
var arrivalEnrollmentAddressVertex = await GetEnrollmentAddressVertexAsync(request.ArrivalAddressId);
|
|
|
|
var graph = await InitializeGraphAsync(request.DepartureDate, cancellationToken);
|
|
|
|
var paths = FindAllPaths(graph, departureEnrollmentAddressVertex, arrivalEnrollmentAddressVertex,
|
|
request.MinTransferTime, request.MaxTransferTime, request.MaxTransferDistanceInMeters);
|
|
|
|
if (paths.Count == 0)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
return paths.AsQueryable().ProjectTo<RouteWithTransfersDto>(_mapper.ConfigurationProvider).ToList();
|
|
}
|
|
|
|
private async Task<AdjacencyGraph<EnrollmentAddressVertex, SEquatableEdge<EnrollmentAddressVertex>>> InitializeGraphAsync(
|
|
DateOnly departureDate,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var graph = new AdjacencyGraph<EnrollmentAddressVertex, SEquatableEdge<EnrollmentAddressVertex>>();
|
|
|
|
var vehicleEnrollmentsArray = await _dbContext.VehicleEnrollments
|
|
.Include(ve => ve.Route)
|
|
.ThenInclude(r => r.RouteAddresses)
|
|
.ThenInclude(ra => ra.Address)
|
|
.ThenInclude(a => a.City)
|
|
.ThenInclude(c => c.Region)
|
|
.ThenInclude(r => r.Country)
|
|
.Include(ve => ve.Route)
|
|
.ThenInclude(r => r.RouteAddresses)
|
|
.ThenInclude(ra => ra.RouteAddressDetails)
|
|
.Include(ve => ve.Tickets)
|
|
.ThenInclude(t => t.TicketGroup)
|
|
.Include(ve => ve.Vehicle)
|
|
.ThenInclude(v => v.Company)
|
|
.Where(ve =>
|
|
DateOnly.FromDateTime(ve.DepartureDateTimeUtc) >= departureDate &&
|
|
DateOnly.FromDateTime(ve.DepartureDateTimeUtc) < departureDate.AddDays(1))
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
|
|
foreach (var enrollment in vehicleEnrollmentsArray)
|
|
{
|
|
var routeAddressesArray = enrollment.Route.RouteAddresses.OrderBy(ra => ra.Order).ToArray();
|
|
|
|
var previousVertex = new EnrollmentAddressVertex(enrollment, routeAddressesArray[0].Address);
|
|
graph.AddVertex(previousVertex);
|
|
|
|
for (int i = 1; i < routeAddressesArray.Length; i++)
|
|
{
|
|
var currentVertex = new EnrollmentAddressVertex(enrollment, routeAddressesArray[i].Address);
|
|
graph.AddVertex(currentVertex);
|
|
graph.AddEdge(new SEquatableEdge<EnrollmentAddressVertex>(previousVertex, currentVertex));
|
|
|
|
previousVertex = currentVertex;
|
|
}
|
|
}
|
|
|
|
return graph;
|
|
}
|
|
|
|
private async Task<EnrollmentAddressVertex> GetEnrollmentAddressVertexAsync(int addressId)
|
|
{
|
|
var routeAddress = await _dbContext.VehicleEnrollments
|
|
.Include(ve => ve.Route)
|
|
.ThenInclude(r => r.RouteAddresses)
|
|
.ThenInclude(ra => ra.Address)
|
|
.ThenInclude(a => a.City)
|
|
.ThenInclude(c => c.Region)
|
|
.ThenInclude(r => r.Country)
|
|
.Include(ve => ve.Route)
|
|
.ThenInclude(r => r.RouteAddresses)
|
|
.ThenInclude(ra => ra.RouteAddressDetails)
|
|
.Include(ve => ve.Tickets)
|
|
.ThenInclude(t => t.TicketGroup)
|
|
.Include(ve => ve.Vehicle)
|
|
.ThenInclude(v => v.Company)
|
|
.SelectMany(ve => ve.Route.RouteAddresses)
|
|
.FirstOrDefaultAsync(ra => ra.Address.Id == addressId);
|
|
|
|
if (routeAddress == null)
|
|
{
|
|
throw new ValidationException(new ValidationFailure[]
|
|
{
|
|
new ValidationFailure("AddressId", "Departure or Arrival Address with given Id is not found")
|
|
});
|
|
}
|
|
|
|
return new EnrollmentAddressVertex(null, routeAddress.Address);
|
|
}
|
|
|
|
private List<List<EnrollmentAddressVertex>> FindAllPaths(
|
|
AdjacencyGraph<EnrollmentAddressVertex, SEquatableEdge<EnrollmentAddressVertex>> graph,
|
|
EnrollmentAddressVertex startVertex,
|
|
EnrollmentAddressVertex endVertex,
|
|
TimeSpan minTransferTime,
|
|
TimeSpan maxTransferTime,
|
|
double maxTransferDistanceInMeters)
|
|
{
|
|
var paths = new List<List<EnrollmentAddressVertex>>();
|
|
var visited = new HashSet<EnrollmentAddressVertex>();
|
|
var currentPath = new List<EnrollmentAddressVertex>();
|
|
|
|
FindAllPathsDFS(graph, startVertex, endVertex, visited, currentPath, paths, minTransferTime,
|
|
maxTransferTime, maxTransferDistanceInMeters);
|
|
|
|
return paths;
|
|
}
|
|
|
|
private void FindAllPathsDFS(
|
|
AdjacencyGraph<EnrollmentAddressVertex, SEquatableEdge<EnrollmentAddressVertex>> graph,
|
|
EnrollmentAddressVertex current,
|
|
EnrollmentAddressVertex endVertex,
|
|
HashSet<EnrollmentAddressVertex> visited,
|
|
List<EnrollmentAddressVertex> currentPath,
|
|
List<List<EnrollmentAddressVertex>> paths,
|
|
TimeSpan minTransferTime,
|
|
TimeSpan maxTransferTime,
|
|
double maxTransferDistanceInMeters)
|
|
{
|
|
visited.Add(current);
|
|
currentPath.Add(current);
|
|
|
|
if (current.Equals(endVertex))
|
|
{
|
|
paths.Add(new List<EnrollmentAddressVertex>(currentPath));
|
|
}
|
|
else
|
|
{
|
|
foreach (var edge in graph.OutEdges(current))
|
|
{
|
|
if (!visited.Contains(edge.Target))
|
|
{
|
|
if (current.VehicleEnrollment == null)
|
|
{
|
|
current.VehicleEnrollment = edge.Target.VehicleEnrollment;
|
|
}
|
|
|
|
// If transfering to other vehicle enrollment
|
|
if (current.VehicleEnrollment.Id != edge.Target.VehicleEnrollment.Id)
|
|
{
|
|
// Skip paths where there is a singular vertex from any Vehicle Enrollment
|
|
// A - A - A - _B_ <- - C - C
|
|
|
|
var threeLast = currentPath.TakeLast(3).ToArray();
|
|
|
|
if (currentPath.Count >= 4 &&
|
|
threeLast[0].VehicleEnrollment.Id != threeLast[1].VehicleEnrollment.Id &&
|
|
threeLast[1].VehicleEnrollment.Id != threeLast[2].VehicleEnrollment.Id)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Filter by amount of free time between enrollment 1 arrives and enrollment 2 departures
|
|
|
|
var freeTime = edge.Target.GetDepartureDateTimeUtc() - current.GetArrivalDateTimeUtc();
|
|
|
|
if (freeTime < minTransferTime ||
|
|
freeTime > maxTransferTime)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Filter by distance between transfer locations
|
|
|
|
var currentLocation = current.Address.Location;
|
|
var targetLocation = edge.Target.Address.Location;
|
|
|
|
const double metersInDegree = 111_111.111;
|
|
|
|
var distanceInDegrees = Math.Sqrt(
|
|
Math.Pow(currentLocation.Latitude - targetLocation.Latitude, 2) +
|
|
Math.Pow(currentLocation.Longitude - targetLocation.Longitude, 2));
|
|
|
|
var distanceInMeters = distanceInDegrees * metersInDegree;
|
|
|
|
if (distanceInMeters > maxTransferDistanceInMeters)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
FindAllPathsDFS(graph, edge.Target, endVertex, visited, currentPath, paths,
|
|
minTransferTime, maxTransferTime, maxTransferDistanceInMeters);
|
|
}
|
|
}
|
|
}
|
|
|
|
visited.Remove(current);
|
|
currentPath.RemoveAt(currentPath.Count - 1);
|
|
}
|
|
}
|