autobus-api_old/AutobusApi.Application/RouteSearch/RouteSearchQueryHandler.cs

221 lines
8.9 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);
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));
var vertexFromDifferentRoute = graph.Vertices.FirstOrDefault(v => v.Address.Id == currentVertex.Address.Id);
if (vertexFromDifferentRoute != null)
{
graph.AddEdge(new SEquatableEdge<EnrollmentAddressVertex>(currentVertex, vertexFromDifferentRoute));
}
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);
}
}