3729 lines
157 KiB
C#
3729 lines
157 KiB
C#
using System.Collections.Concurrent;
|
||
using System.Globalization;
|
||
using Microsoft.AspNetCore.Hosting;
|
||
using RutasDBUS.Modelos.Planificacion;
|
||
using RutasDBUS.Modelos.RedBus;
|
||
using RutasDBUS.Servicios.Caminata;
|
||
using RutasDBUS.Servicios.Horarios;
|
||
using RutasDBUS.Servicios.RedBus;
|
||
|
||
namespace RutasDBUS.Servicios.Planificacion;
|
||
|
||
/// <summary>
|
||
/// Orquesta la seleccion de candidatas, el motor temporal y la evaluacion final.
|
||
/// </summary>
|
||
public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||
{
|
||
private readonly IRedBusServicio _redBusServicio;
|
||
private readonly IClienteOsrm _clienteOsrm;
|
||
private readonly IClienteValhalla _clienteValhalla;
|
||
private readonly ServicioHorariosDbus _servicioHorarios;
|
||
private readonly IWebHostEnvironment _entorno;
|
||
private readonly ServicioIndiceTransitoDbus _servicioIndiceTransito;
|
||
private readonly BuscadorRutasTemporalesDbus _buscadorRutasTemporales;
|
||
|
||
private static readonly ConcurrentDictionary<string, (double[,]? linea, double distancia, double duracion)> _cacheCaminatas =
|
||
new(StringComparer.OrdinalIgnoreCase);
|
||
private static readonly ConcurrentDictionary<string, double> _cacheDuracionBusRefinamiento =
|
||
new(StringComparer.OrdinalIgnoreCase);
|
||
private static readonly ConcurrentDictionary<string, (bool ok, DateTime salida, DateTime llegada)> _cacheSegmentosHorarios =
|
||
new(StringComparer.OrdinalIgnoreCase);
|
||
private static readonly ConcurrentDictionary<string, (List<double[,]> lineas, double distancia)> _cacheLineasBusPlanificacion =
|
||
new(StringComparer.OrdinalIgnoreCase);
|
||
private readonly ConcurrentDictionary<string, Parada> _cacheParadasEstimacion =
|
||
new(StringComparer.OrdinalIgnoreCase);
|
||
private const double UmbralCosturaVisualMetros = 12.0;
|
||
private const double UmbralUnionPeatonalAutomaticaMetros = 250.0;
|
||
private const double ToleranciaEmpateRapidezSegundos = 5.0;
|
||
private const double MejoraMinimaAccesoExtensionCircularMetros = 20.0;
|
||
private const double MaxExcesoAccesoDirectoCircularMetros = 120.0;
|
||
private const double ToleranciaMismoBusCircularSegundos = 5.0;
|
||
private const int MaxContinuidadesCircularesConsecutivas = 2;
|
||
private const int VentanaRefinamientoParadaInicio = 4;
|
||
private const int VentanaRefinamientoParadaFin = 4;
|
||
private const int MaxConcurrenciaRefinamientoParadas = 4;
|
||
private const int MaxOrigenesTransbordoRobusto = 20;
|
||
private const int MaxBajadasPrimeraPiernaTransbordoRobusto = 256;
|
||
private const double UmbralActivacionTransbordoRobustoMetros = 400.0;
|
||
private const double UmbralAccesoDestinoTransbordoRobustoMetros = 350.0;
|
||
private const double ToleranciaDominanciaFinalSegundos = 45.0;
|
||
private const double ToleranciaDominanciaFinalMetros = 40.0;
|
||
private const double ToleranciaDominanciaFinalBusMetros = 120.0;
|
||
private const double MejoraClaraDominanciaFinalSegundos = 90.0;
|
||
private const double MejoraClaraDominanciaFinalMetros = 120.0;
|
||
private const double AhorroClaroAndandoFinalMetros = 250.0;
|
||
private const double FactorAhorroClaroAndandoFinal = 0.75;
|
||
private const double UmbralAccesoCercanoRefinamientoInicioMetros = 120.0;
|
||
private const double MaxIncrementoAccesoCercanoRefinamientoInicioMetros = 60.0;
|
||
|
||
/// <summary>
|
||
/// Inicializa una nueva instancia del planificador.
|
||
/// </summary>
|
||
public PlanificadorRutasServicio(
|
||
IRedBusServicio redBusServicio,
|
||
IClienteOsrm clienteOsrm,
|
||
IClienteValhalla clienteValhalla,
|
||
ServicioHorariosDbus servicioHorarios,
|
||
IWebHostEnvironment entorno,
|
||
ServicioIndiceTransitoDbus servicioIndiceTransito,
|
||
BuscadorRutasTemporalesDbus buscadorRutasTemporales)
|
||
{
|
||
_redBusServicio = redBusServicio;
|
||
_clienteOsrm = clienteOsrm;
|
||
_clienteValhalla = clienteValhalla;
|
||
_servicioHorarios = servicioHorarios;
|
||
_entorno = entorno;
|
||
_servicioIndiceTransito = servicioIndiceTransito;
|
||
_buscadorRutasTemporales = buscadorRutasTemporales;
|
||
}
|
||
|
||
public void InvalidarCachesModelo()
|
||
{
|
||
_cacheDuracionBusRefinamiento.Clear();
|
||
_cacheSegmentosHorarios.Clear();
|
||
_cacheLineasBusPlanificacion.Clear();
|
||
_cacheParadasEstimacion.Clear();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Calcula alternativas de ruta entre origen y destino.
|
||
/// </summary>
|
||
public async Task<ResultadoPlanificadorRuta> CalcularAsync(
|
||
double[] origenLatLon,
|
||
double[] destinoLatLon,
|
||
ParametrosPlanificadorRuta parametros,
|
||
CancellationToken ct = default)
|
||
{
|
||
await _redBusServicio.CargarRedAsync(ct);
|
||
if (_redBusServicio.Paradas.Count == 0)
|
||
{
|
||
return new ResultadoPlanificadorRuta
|
||
{
|
||
Ok = false,
|
||
Mensaje = "No hay red de paradas cargada.",
|
||
Alternativas = new(),
|
||
IndicePreferido = 0
|
||
};
|
||
}
|
||
|
||
var indiceTransito = await _servicioIndiceTransito.ObtenerAsync(ct);
|
||
if (parametros.UsarHorariosReales)
|
||
{
|
||
try
|
||
{
|
||
await _servicioHorarios.AsegurarCargaAsync(_entorno.ContentRootPath, ct);
|
||
}
|
||
catch
|
||
{
|
||
parametros.UsarHorariosReales = false;
|
||
}
|
||
}
|
||
|
||
var candidatasOrigen = await SeleccionarCandidatasAsync(indiceTransito, origenLatLon[0], origenLatLon[1], parametros, ct);
|
||
var candidatasDestino = await SeleccionarCandidatasAsync(indiceTransito, destinoLatLon[0], destinoLatLon[1], parametros, ct);
|
||
|
||
if (candidatasOrigen.Count == 0 || candidatasDestino.Count == 0)
|
||
{
|
||
return new ResultadoPlanificadorRuta
|
||
{
|
||
Ok = false,
|
||
Mensaje = "No hay paradas validas cerca del origen o del destino con los filtros actuales.",
|
||
Alternativas = new(),
|
||
IndicePreferido = 0
|
||
};
|
||
}
|
||
|
||
var alternativasSoloPie = await CrearAlternativasSoloPieAsync(origenLatLon, destinoLatLon, parametros);
|
||
var instanteSalida = UtilidadesTransitoDbus.ResolverInstanteSalida(parametros);
|
||
var calendariosActivos = parametros.UsarHorariosReales
|
||
? _servicioHorarios.GetActiveCalendars(DateOnly.FromDateTime(instanteSalida))
|
||
: null;
|
||
|
||
var usarBusquedaDirigida = parametros.UsarHorariosReales && DebeUsarBusquedaDirigidaUnaTransbordo(parametros);
|
||
var candidatasBus = usarBusquedaDirigida
|
||
? new List<ResultadoBusquedaRutaBusDbus>()
|
||
: _buscadorRutasTemporales.BuscarAlternativas(
|
||
indiceTransito,
|
||
candidatasOrigen,
|
||
candidatasDestino,
|
||
instanteSalida,
|
||
parametros,
|
||
calendariosActivos,
|
||
ct).ToList();
|
||
|
||
if (parametros.UsarHorariosReales)
|
||
{
|
||
var directasRobustas = BuscarDirectasRobustasConHorario(
|
||
indiceTransito,
|
||
candidatasOrigen,
|
||
candidatasDestino,
|
||
instanteSalida,
|
||
parametros);
|
||
var continuidadesCirculares = BuscarContinuidadesCircularesConHorario(
|
||
indiceTransito,
|
||
candidatasOrigen,
|
||
candidatasDestino,
|
||
instanteSalida,
|
||
parametros);
|
||
var transbordosRobustos = usarBusquedaDirigida || DebeBuscarTransbordosRobustos(
|
||
candidatasBus,
|
||
candidatasOrigen,
|
||
candidatasDestino)
|
||
? BuscarTransbordosRobustosConHorario(
|
||
indiceTransito,
|
||
candidatasOrigen,
|
||
candidatasDestino,
|
||
instanteSalida,
|
||
parametros,
|
||
incluirPrimeraPiernaDirecta: usarBusquedaDirigida || candidatasBus.Count == 0)
|
||
: new List<ResultadoBusquedaRutaBusDbus>();
|
||
|
||
if (directasRobustas.Count > 0 ||
|
||
continuidadesCirculares.Count > 0 ||
|
||
transbordosRobustos.Count > 0)
|
||
{
|
||
var porFirma = new Dictionary<string, ResultadoBusquedaRutaBusDbus>(StringComparer.OrdinalIgnoreCase);
|
||
foreach (var candidata in candidatasBus)
|
||
{
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(candidata.RutaBus);
|
||
porFirma[firma] = candidata;
|
||
}
|
||
|
||
foreach (var directa in directasRobustas)
|
||
{
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(directa.RutaBus);
|
||
if (!porFirma.TryGetValue(firma, out var actual) || ResultadoBusquedaEsMejor(directa, actual))
|
||
porFirma[firma] = directa;
|
||
}
|
||
|
||
foreach (var circular in continuidadesCirculares)
|
||
{
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(circular.RutaBus);
|
||
if (!porFirma.TryGetValue(firma, out var actual) || ResultadoBusquedaEsMejor(circular, actual))
|
||
porFirma[firma] = circular;
|
||
}
|
||
|
||
foreach (var transbordo in transbordosRobustos)
|
||
{
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(transbordo.RutaBus);
|
||
if (!porFirma.TryGetValue(firma, out var actual) || ResultadoBusquedaEsMejor(transbordo, actual))
|
||
porFirma[firma] = transbordo;
|
||
}
|
||
|
||
candidatasBus = SeleccionarPoolBusqueda(
|
||
porFirma.Values,
|
||
Math.Max(16, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 10),
|
||
parametros);
|
||
}
|
||
|
||
var extensionesCircularesInicio = BuscarExtensionesCircularesInicialesConHorario(
|
||
indiceTransito,
|
||
candidatasOrigen,
|
||
candidatasBus,
|
||
instanteSalida,
|
||
parametros);
|
||
|
||
if (extensionesCircularesInicio.Count > 0)
|
||
{
|
||
var porFirma = new Dictionary<string, ResultadoBusquedaRutaBusDbus>(StringComparer.OrdinalIgnoreCase);
|
||
foreach (var candidata in candidatasBus)
|
||
{
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(candidata.RutaBus);
|
||
porFirma[firma] = candidata;
|
||
}
|
||
|
||
foreach (var extension in extensionesCircularesInicio)
|
||
{
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(extension.RutaBus);
|
||
if (!porFirma.TryGetValue(firma, out var actual) || ResultadoBusquedaEsMejor(extension, actual))
|
||
porFirma[firma] = extension;
|
||
}
|
||
|
||
candidatasBus = SeleccionarPoolBusqueda(
|
||
porFirma.Values,
|
||
Math.Max(16, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 10),
|
||
parametros);
|
||
}
|
||
|
||
var iniciosCircularesDirectos = BuscarIniciosDirectosTrasCircularConHorario(
|
||
candidatasBus,
|
||
candidatasOrigen,
|
||
instanteSalida,
|
||
parametros);
|
||
|
||
if (iniciosCircularesDirectos.Count > 0)
|
||
{
|
||
var porFirma = candidatasBus.ToDictionary(
|
||
candidata => UtilidadesTransitoDbus.FirmaRutaBus(candidata.RutaBus),
|
||
StringComparer.OrdinalIgnoreCase);
|
||
|
||
foreach (var inicioDirecto in iniciosCircularesDirectos)
|
||
{
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(inicioDirecto.RutaBus);
|
||
if (!porFirma.TryGetValue(firma, out var actual) ||
|
||
ResultadoBusquedaEsMejor(inicioDirecto, actual))
|
||
{
|
||
porFirma[firma] = inicioDirecto;
|
||
}
|
||
}
|
||
|
||
// Estos recortes ya proceden del pool seleccionado. No se vuelve a podar para que
|
||
// una familia con muchos itinerarios equivalentes no los elimine otra vez.
|
||
candidatasBus = porFirma.Values.ToList();
|
||
}
|
||
}
|
||
|
||
candidatasBus = candidatasBus
|
||
.Where(candidata => !UtilidadesTransitoDbus.TieneContinuidadesNoRealesConsecutivasExcesivas(
|
||
candidata.RutaBus.Tramos,
|
||
MaxContinuidadesCircularesConsecutivas))
|
||
.ToList();
|
||
|
||
var alternativasBus = new List<AlternativaRuta>();
|
||
foreach (var candidataBus in candidatasBus)
|
||
{
|
||
ct.ThrowIfCancellationRequested();
|
||
|
||
var rutaBus = ClonarRutaBus(candidataBus.RutaBus);
|
||
|
||
var alternativa = new AlternativaRuta
|
||
{
|
||
EsSoloAPie = false,
|
||
OrigenParada = candidataBus.ParadaOrigen,
|
||
DestinoParada = candidataBus.ParadaDestino,
|
||
AlternativaBus = rutaBus,
|
||
DistanciaPieInicioAprox = candidataBus.DistanciaPieInicio,
|
||
DistanciaPieFinAprox = candidataBus.DistanciaPieFin,
|
||
DistanciaPieTransbordosAprox = candidataBus.DistanciaPieTransbordos,
|
||
DistanciasPiePorTramoAprox = ConstruirDistanciasPiePorTramo(candidataBus),
|
||
TiempoPieInicio = candidataBus.TiempoPieInicio,
|
||
TiempoPieFin = candidataBus.TiempoPieFin,
|
||
TiempoEsperaInicialSegundos = candidataBus.TiempoEsperaInicialSegundos,
|
||
TiempoEsperaTransbordosSegundos = candidataBus.TiempoEsperaTransbordosSegundos,
|
||
NumeroTransbordosReales = UtilidadesTransitoDbus.ContarTransbordosReales(rutaBus.Tramos),
|
||
UsaHorariosReales = parametros.UsarHorariosReales,
|
||
HorarioCompletoDisponible = parametros.UsarHorariosReales,
|
||
DuracionTotalAproxSegundos = candidataBus.DuracionEstimadaSegundos,
|
||
PuntuacionCoste = candidataBus.PuntuacionCoste
|
||
};
|
||
|
||
var duracionExacta = await RecalcularDuracionExactaAsync(origenLatLon, destinoLatLon, alternativa, parametros, ct);
|
||
if (!double.IsFinite(duracionExacta))
|
||
continue;
|
||
|
||
alternativa.DuracionTotalAproxSegundos = duracionExacta;
|
||
alternativa.PuntuacionCoste = CalcularPuntuacionCoste(alternativa, parametros);
|
||
alternativasBus.Add(alternativa);
|
||
}
|
||
|
||
alternativasBus = alternativasBus
|
||
.GroupBy(alternativa => UtilidadesTransitoDbus.FirmaRutaBus(alternativa.AlternativaBus!), StringComparer.OrdinalIgnoreCase)
|
||
.Select(grupo => grupo.OrderBy(x => x.PuntuacionCoste).First())
|
||
.ToList();
|
||
|
||
alternativasBus = SeleccionarPoolFinal(DeduplicarAlternativasEquivalentes(alternativasBus), parametros);
|
||
|
||
var finales = SeleccionarAlternativasFinales(alternativasBus, alternativasSoloPie, parametros);
|
||
if (finales.Count == 0)
|
||
{
|
||
return new ResultadoPlanificadorRuta
|
||
{
|
||
Ok = false,
|
||
Mensaje = alternativasSoloPie.Count > 0
|
||
? "No hay salidas viables en bus para la hora indicada."
|
||
: "No he encontrado una combinacion valida de bus para ese origen y destino.",
|
||
Alternativas = alternativasSoloPie.OrderBy(x => x.PuntuacionCoste).ToList(),
|
||
IndicePreferido = 0
|
||
};
|
||
}
|
||
|
||
var indicePreferido = 0;
|
||
if (!string.IsNullOrWhiteSpace(parametros.PreferenciaEtiquetaRuta))
|
||
{
|
||
var encontrado = finales.FindIndex(x =>
|
||
string.Equals(x.Etiqueta, parametros.PreferenciaEtiquetaRuta, StringComparison.OrdinalIgnoreCase));
|
||
if (encontrado >= 0)
|
||
indicePreferido = encontrado;
|
||
}
|
||
|
||
return new ResultadoPlanificadorRuta
|
||
{
|
||
Ok = true,
|
||
Mensaje = "Ok",
|
||
Alternativas = finales,
|
||
IndicePreferido = indicePreferido
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Inyecta candidatas directas (sin transbordos) usando horarios reales para evitar perder
|
||
/// servicios claramente mejores por podas tempranas del buscador principal.
|
||
/// </summary>
|
||
private List<ResultadoBusquedaRutaBusDbus> BuscarDirectasRobustasConHorario(
|
||
IndiceTransitoDbus indiceTransito,
|
||
IReadOnlyList<ParadaCandidata> candidatasOrigen,
|
||
IReadOnlyList<ParadaCandidata> candidatasDestino,
|
||
DateTime instanteSalida,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (candidatasOrigen.Count == 0 || candidatasDestino.Count == 0)
|
||
return new();
|
||
|
||
if (parametros.ActivarRangoTransbordos &&
|
||
(parametros.MinTransbordosPermitidos > 0 || parametros.MaxTransbordosPermitidos < 0))
|
||
{
|
||
return new();
|
||
}
|
||
|
||
var destinoPorCodigo = candidatasDestino
|
||
.GroupBy(x => x.Parada.Codigo, StringComparer.OrdinalIgnoreCase)
|
||
.ToDictionary(g => g.Key, g => g.OrderBy(x => x.TiempoAPie).First(), StringComparer.OrdinalIgnoreCase);
|
||
|
||
var mejores = new Dictionary<string, ResultadoBusquedaRutaBusDbus>(StringComparer.OrdinalIgnoreCase);
|
||
var maxResultados = Math.Max(24, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 12);
|
||
|
||
foreach (var origen in candidatasOrigen.OrderBy(x => x.TiempoAPie))
|
||
{
|
||
if (!indiceTransito.PatronesPorParada.TryGetValue(origen.Parada.Codigo, out var referencias) || referencias.Count == 0)
|
||
continue;
|
||
|
||
var llegadaParada = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, origen.TiempoAPie);
|
||
|
||
foreach (var referencia in referencias)
|
||
{
|
||
var patron = referencia.Patron;
|
||
if (!PatronPermitido(patron, parametros))
|
||
continue;
|
||
|
||
var codigoLinea = UtilidadesTransitoDbus.LineaCodigoStr(patron.Variante);
|
||
var codigoItinerario = patron.CodigoItinerario;
|
||
var sentido = UtilidadesTransitoDbus.NormalizarSentidoCodigo(patron.Sentido);
|
||
var codigos = patron.CodigosParada;
|
||
|
||
var indiceSubida = referencia.IndiceParada;
|
||
if (indiceSubida < 0 || indiceSubida >= codigos.Count - 1)
|
||
continue;
|
||
|
||
for (var indiceBajada = indiceSubida + 1; indiceBajada < codigos.Count; indiceBajada++)
|
||
{
|
||
if (!destinoPorCodigo.TryGetValue(codigos[indiceBajada], out var destino))
|
||
continue;
|
||
|
||
if (!ResolverSegmentoHorario(
|
||
origen.Parada.Codigo,
|
||
destino.Parada.Codigo,
|
||
codigoLinea,
|
||
codigoItinerario,
|
||
sentido,
|
||
llegadaParada,
|
||
out var dep,
|
||
out var arr))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var esperaInicial = Math.Max(0, (dep - llegadaParada).TotalSeconds);
|
||
if (parametros.MaxEsperaInicialMinutos > 0 &&
|
||
esperaInicial > parametros.MaxEsperaInicialMinutos * 60.0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (parametros.MaxEsperaTotalMinutos > 0 &&
|
||
esperaInicial > parametros.MaxEsperaTotalMinutos * 60.0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var duracionTotal = Math.Max(0, (arr - instanteSalida).TotalSeconds) + destino.TiempoAPie;
|
||
var ruta = new RutaBusAlternativa
|
||
{
|
||
Tramos = new List<TramoBus>
|
||
{
|
||
new()
|
||
{
|
||
Variante = patron.Variante,
|
||
IndiceInicio = indiceSubida,
|
||
IndiceFin = indiceBajada
|
||
}
|
||
},
|
||
NumeroTransbordos = 0,
|
||
CodigosParadasTransbordo = new List<string>()
|
||
};
|
||
|
||
var candidata = new ResultadoBusquedaRutaBusDbus
|
||
{
|
||
ParadaOrigen = origen.Parada,
|
||
ParadaDestino = destino.Parada,
|
||
DistanciaPieInicio = origen.DistanciaAPie,
|
||
TiempoPieInicio = origen.TiempoAPie,
|
||
DistanciaPieFin = destino.DistanciaAPie,
|
||
TiempoPieFin = destino.TiempoAPie,
|
||
DistanciaPieTransbordos = 0,
|
||
TiempoEsperaInicialSegundos = esperaInicial,
|
||
TiempoEsperaTransbordosSegundos = 0,
|
||
DuracionEstimadaSegundos = duracionTotal,
|
||
PuntuacionCoste = UtilidadesTransitoDbus.CalcularCosteConPenalizacionPieSegundos(
|
||
duracionTotal,
|
||
parametros,
|
||
origen.DistanciaAPie,
|
||
0,
|
||
destino.DistanciaAPie),
|
||
RutaBus = ruta
|
||
};
|
||
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(ruta);
|
||
if (!mejores.TryGetValue(firma, out var actual) || ResultadoBusquedaEsMejor(candidata, actual))
|
||
mejores[firma] = candidata;
|
||
|
||
if (mejores.Count > maxResultados * 3)
|
||
{
|
||
mejores = SeleccionarPoolBusqueda(mejores.Values, maxResultados * 2, parametros)
|
||
.ToDictionary(x => UtilidadesTransitoDbus.FirmaRutaBus(x.RutaBus), StringComparer.OrdinalIgnoreCase);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return SeleccionarPoolBusqueda(mejores.Values, maxResultados, parametros);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Inyecta continuidades circulares de la misma linea/itinerario que cambian de sentido sin transbordo real.
|
||
/// </summary>
|
||
private List<ResultadoBusquedaRutaBusDbus> BuscarContinuidadesCircularesConHorario(
|
||
IndiceTransitoDbus indiceTransito,
|
||
IReadOnlyList<ParadaCandidata> candidatasOrigen,
|
||
IReadOnlyList<ParadaCandidata> candidatasDestino,
|
||
DateTime instanteSalida,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (candidatasOrigen.Count == 0 || candidatasDestino.Count == 0)
|
||
return new();
|
||
|
||
if (parametros.ActivarRangoTransbordos &&
|
||
(parametros.MinTransbordosPermitidos > 0 || parametros.MaxTransbordosPermitidos < 0))
|
||
{
|
||
return new();
|
||
}
|
||
|
||
var destinoPorCodigo = candidatasDestino
|
||
.GroupBy(x => x.Parada.Codigo, StringComparer.OrdinalIgnoreCase)
|
||
.ToDictionary(g => g.Key, g => g.OrderBy(x => x.TiempoAPie).First(), StringComparer.OrdinalIgnoreCase);
|
||
|
||
var mejores = new Dictionary<string, ResultadoBusquedaRutaBusDbus>(StringComparer.OrdinalIgnoreCase);
|
||
var maxResultados = Math.Max(24, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 12);
|
||
|
||
foreach (var origen in candidatasOrigen.OrderBy(x => x.TiempoAPie))
|
||
{
|
||
if (!indiceTransito.PatronesPorParada.TryGetValue(origen.Parada.Codigo, out var referenciasOrigen))
|
||
continue;
|
||
|
||
var llegadaParadaOrigen = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, origen.TiempoAPie);
|
||
|
||
foreach (var referenciaOrigen in referenciasOrigen)
|
||
{
|
||
var primerPatron = referenciaOrigen.Patron;
|
||
if (!PatronPermitido(primerPatron, parametros))
|
||
continue;
|
||
|
||
var indiceSubida = referenciaOrigen.IndiceParada;
|
||
if (indiceSubida < 0 || indiceSubida >= primerPatron.CodigosParada.Count - 1)
|
||
continue;
|
||
|
||
for (var indiceIntermedioPrimero = indiceSubida + 1;
|
||
indiceIntermedioPrimero < primerPatron.CodigosParada.Count;
|
||
indiceIntermedioPrimero++)
|
||
{
|
||
var codigoIntermedio = primerPatron.CodigosParada[indiceIntermedioPrimero];
|
||
if (!indiceTransito.PatronesPorParada.TryGetValue(codigoIntermedio, out var referenciasIntermedio))
|
||
continue;
|
||
|
||
foreach (var referenciaIntermedio in referenciasIntermedio)
|
||
{
|
||
var segundoPatron = referenciaIntermedio.Patron;
|
||
if (!EsContinuidadCircular(primerPatron, segundoPatron) ||
|
||
!PatronPermitido(segundoPatron, parametros))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var indiceIntermedioSegundo = referenciaIntermedio.IndiceParada;
|
||
if (indiceIntermedioSegundo < 0 || indiceIntermedioSegundo >= segundoPatron.CodigosParada.Count - 1)
|
||
continue;
|
||
|
||
for (var indiceBajada = indiceIntermedioSegundo + 1;
|
||
indiceBajada < segundoPatron.CodigosParada.Count;
|
||
indiceBajada++)
|
||
{
|
||
if (!destinoPorCodigo.TryGetValue(segundoPatron.CodigosParada[indiceBajada], out var destino))
|
||
continue;
|
||
|
||
if (!ResolverSegmentoHorario(
|
||
primerPatron,
|
||
origen.Parada.Codigo,
|
||
codigoIntermedio,
|
||
llegadaParadaOrigen,
|
||
out var salidaPrimera,
|
||
out var llegadaIntermedia))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!ResolverSegmentoHorario(
|
||
segundoPatron,
|
||
codigoIntermedio,
|
||
destino.Parada.Codigo,
|
||
llegadaIntermedia,
|
||
out _,
|
||
out var llegadaDestinoBus))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var esperaInicial = Math.Max(0, (salidaPrimera - llegadaParadaOrigen).TotalSeconds);
|
||
if (parametros.MaxEsperaInicialMinutos > 0 &&
|
||
esperaInicial > parametros.MaxEsperaInicialMinutos * 60.0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var duracionTotal = Math.Max(0, (llegadaDestinoBus - instanteSalida).TotalSeconds) + destino.TiempoAPie;
|
||
if (parametros.MaxEsperaTotalMinutos > 0 &&
|
||
esperaInicial > parametros.MaxEsperaTotalMinutos * 60.0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var ruta = new RutaBusAlternativa
|
||
{
|
||
Tramos = new List<TramoBus>
|
||
{
|
||
new()
|
||
{
|
||
Variante = primerPatron.Variante,
|
||
IndiceInicio = indiceSubida,
|
||
IndiceFin = indiceIntermedioPrimero
|
||
},
|
||
new()
|
||
{
|
||
Variante = segundoPatron.Variante,
|
||
IndiceInicio = indiceIntermedioSegundo,
|
||
IndiceFin = indiceBajada
|
||
}
|
||
},
|
||
NumeroTransbordos = 0,
|
||
CodigosParadasTransbordo = new List<string>()
|
||
};
|
||
|
||
var candidata = new ResultadoBusquedaRutaBusDbus
|
||
{
|
||
ParadaOrigen = origen.Parada,
|
||
ParadaDestino = destino.Parada,
|
||
DistanciaPieInicio = origen.DistanciaAPie,
|
||
TiempoPieInicio = origen.TiempoAPie,
|
||
DistanciaPieFin = destino.DistanciaAPie,
|
||
TiempoPieFin = destino.TiempoAPie,
|
||
DistanciaPieTransbordos = 0,
|
||
TiempoEsperaInicialSegundos = esperaInicial,
|
||
TiempoEsperaTransbordosSegundos = 0,
|
||
DuracionEstimadaSegundos = duracionTotal,
|
||
PuntuacionCoste = UtilidadesTransitoDbus.CalcularCosteConPenalizacionPieSegundos(
|
||
duracionTotal,
|
||
parametros,
|
||
origen.DistanciaAPie,
|
||
0,
|
||
destino.DistanciaAPie),
|
||
RutaBus = ruta
|
||
};
|
||
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(ruta);
|
||
if (!mejores.TryGetValue(firma, out var actual) || ResultadoBusquedaEsMejor(candidata, actual))
|
||
mejores[firma] = candidata;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return SeleccionarPoolBusqueda(mejores.Values, maxResultados, parametros);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Refuerza combinaciones de un transbordo que pueden quedar tapadas por podas del buscador principal,
|
||
/// especialmente cuando una continuidad circular permite llegar al transbordo andando muy poco.
|
||
/// </summary>
|
||
private List<ResultadoBusquedaRutaBusDbus> BuscarTransbordosRobustosConHorario(
|
||
IndiceTransitoDbus indiceTransito,
|
||
IReadOnlyList<ParadaCandidata> candidatasOrigen,
|
||
IReadOnlyList<ParadaCandidata> candidatasDestino,
|
||
DateTime instanteSalida,
|
||
ParametrosPlanificadorRuta parametros,
|
||
bool incluirPrimeraPiernaDirecta)
|
||
{
|
||
if (candidatasOrigen.Count == 0 || candidatasDestino.Count == 0)
|
||
return new();
|
||
|
||
if (parametros.ActivarRangoTransbordos)
|
||
{
|
||
var minimo = Math.Min(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
|
||
var maximo = Math.Max(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
|
||
if (minimo > 1 || maximo < 1)
|
||
return new();
|
||
}
|
||
|
||
var destinoPorCodigo = candidatasDestino
|
||
.GroupBy(x => x.Parada.Codigo, StringComparer.OrdinalIgnoreCase)
|
||
.ToDictionary(g => g.Key, g => g.OrderBy(x => x.TiempoAPie).First(), StringComparer.OrdinalIgnoreCase);
|
||
var segundasPiernasPorSubida = ConstruirSegundasPiernasPorSubida();
|
||
if (segundasPiernasPorSubida.Count == 0)
|
||
return new();
|
||
|
||
var mejores = new Dictionary<string, ResultadoBusquedaRutaBusDbus>(StringComparer.OrdinalIgnoreCase);
|
||
var transferenciasUtilesPorBajada = new Dictionary<string, List<(string CodigoParada, double DistanciaMetros)>>(StringComparer.OrdinalIgnoreCase);
|
||
var maxResultados = Math.Max(32, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 8);
|
||
var origenes = candidatasOrigen
|
||
.OrderBy(x => x.TiempoAPie)
|
||
.ThenBy(x => x.DistanciaAPie)
|
||
.Take(Math.Min(candidatasOrigen.Count, MaxOrigenesTransbordoRobusto))
|
||
.ToList();
|
||
var margenTransbordo = Math.Clamp(parametros.MargenTransbordoSegundos, 0, 1800);
|
||
|
||
foreach (var origen in origenes)
|
||
{
|
||
if (!indiceTransito.PatronesPorParada.TryGetValue(origen.Parada.Codigo, out var referenciasOrigen))
|
||
continue;
|
||
|
||
var llegadaParadaOrigen = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, origen.TiempoAPie);
|
||
|
||
foreach (var referenciaOrigen in referenciasOrigen)
|
||
{
|
||
var primerPatron = referenciaOrigen.Patron;
|
||
if (!PatronPermitido(primerPatron, parametros))
|
||
continue;
|
||
|
||
var indiceSubida = referenciaOrigen.IndiceParada;
|
||
if (indiceSubida < 0 || indiceSubida >= primerPatron.CodigosParada.Count - 1)
|
||
continue;
|
||
|
||
if (incluirPrimeraPiernaDirecta)
|
||
ExplorarPrimeraPiernaDirecta(origen, primerPatron, indiceSubida, llegadaParadaOrigen);
|
||
ExplorarPrimeraPiernaCircular(origen, primerPatron, indiceSubida, llegadaParadaOrigen);
|
||
|
||
if (mejores.Count > maxResultados * 3)
|
||
PodarMejores();
|
||
}
|
||
}
|
||
|
||
return SeleccionarPoolBusqueda(mejores.Values, maxResultados, parametros);
|
||
|
||
void ExplorarPrimeraPiernaDirecta(
|
||
ParadaCandidata origen,
|
||
PatronTransitoDbus primerPatron,
|
||
int indiceSubida,
|
||
DateTime llegadaParadaOrigen)
|
||
{
|
||
var limiteBajadas = Math.Min(
|
||
primerPatron.CodigosParada.Count,
|
||
indiceSubida + 1 + MaxBajadasPrimeraPiernaTransbordoRobusto);
|
||
|
||
for (var indiceBajada = indiceSubida + 1; indiceBajada < limiteBajadas; indiceBajada++)
|
||
{
|
||
var codigoBajada = primerPatron.CodigosParada[indiceBajada];
|
||
if (!TieneTransferenciaUtil(codigoBajada))
|
||
continue;
|
||
|
||
if (!ResolverSegmentoHorario(
|
||
primerPatron,
|
||
origen.Parada.Codigo,
|
||
codigoBajada,
|
||
llegadaParadaOrigen,
|
||
out var salidaPrimera,
|
||
out var llegadaBajada))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var esperaInicial = Math.Max(0, (salidaPrimera - llegadaParadaOrigen).TotalSeconds);
|
||
var tramosPrimeraPierna = new List<TramoBus>
|
||
{
|
||
new()
|
||
{
|
||
Variante = primerPatron.Variante,
|
||
IndiceInicio = indiceSubida,
|
||
IndiceFin = indiceBajada
|
||
}
|
||
};
|
||
|
||
ProcesarBajadaPrimeraPierna(
|
||
origen,
|
||
tramosPrimeraPierna,
|
||
codigoBajada,
|
||
llegadaBajada,
|
||
esperaInicial);
|
||
}
|
||
}
|
||
|
||
void ExplorarPrimeraPiernaCircular(
|
||
ParadaCandidata origen,
|
||
PatronTransitoDbus primerPatron,
|
||
int indiceSubida,
|
||
DateTime llegadaParadaOrigen)
|
||
{
|
||
var limiteIntermedias = Math.Min(
|
||
primerPatron.CodigosParada.Count,
|
||
indiceSubida + 1 + MaxBajadasPrimeraPiernaTransbordoRobusto);
|
||
|
||
for (var indiceIntermedioPrimero = indiceSubida + 1;
|
||
indiceIntermedioPrimero < limiteIntermedias;
|
||
indiceIntermedioPrimero++)
|
||
{
|
||
var codigoIntermedio = primerPatron.CodigosParada[indiceIntermedioPrimero];
|
||
if (!indiceTransito.PatronesPorParada.TryGetValue(codigoIntermedio, out var referenciasIntermedio))
|
||
continue;
|
||
|
||
var continuidadesIntermedio = referenciasIntermedio
|
||
.Where(referencia =>
|
||
PatronPermitido(referencia.Patron, parametros) &&
|
||
EsContinuidadCircular(primerPatron, referencia.Patron) &&
|
||
referencia.IndiceParada >= 0 &&
|
||
referencia.IndiceParada < referencia.Patron.CodigosParada.Count - 1)
|
||
.ToList();
|
||
if (continuidadesIntermedio.Count == 0)
|
||
continue;
|
||
|
||
if (!ResolverSegmentoHorario(
|
||
primerPatron,
|
||
origen.Parada.Codigo,
|
||
codigoIntermedio,
|
||
llegadaParadaOrigen,
|
||
out var salidaPrimera,
|
||
out var llegadaIntermedia))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var esperaPrimera = Math.Max(0, (salidaPrimera - llegadaParadaOrigen).TotalSeconds);
|
||
|
||
foreach (var referenciaIntermedio in continuidadesIntermedio)
|
||
{
|
||
var segundoPatronCircular = referenciaIntermedio.Patron;
|
||
var indiceIntermedioSegundo = referenciaIntermedio.IndiceParada;
|
||
|
||
var limiteBajadas = Math.Min(
|
||
segundoPatronCircular.CodigosParada.Count,
|
||
indiceIntermedioSegundo + 1 + MaxBajadasPrimeraPiernaTransbordoRobusto);
|
||
|
||
for (var indiceBajadaSegundo = indiceIntermedioSegundo + 1;
|
||
indiceBajadaSegundo < limiteBajadas;
|
||
indiceBajadaSegundo++)
|
||
{
|
||
var codigoBajada = segundoPatronCircular.CodigosParada[indiceBajadaSegundo];
|
||
if (!TieneTransferenciaUtil(codigoBajada))
|
||
continue;
|
||
|
||
if (!ResolverSegmentoHorario(
|
||
segundoPatronCircular,
|
||
codigoIntermedio,
|
||
codigoBajada,
|
||
llegadaIntermedia,
|
||
out var salidaSegunda,
|
||
out var llegadaBajada))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var esperaCircular = Math.Max(0, (salidaSegunda - llegadaIntermedia).TotalSeconds);
|
||
var tramosPrimeraPierna = new List<TramoBus>
|
||
{
|
||
new()
|
||
{
|
||
Variante = primerPatron.Variante,
|
||
IndiceInicio = indiceSubida,
|
||
IndiceFin = indiceIntermedioPrimero
|
||
},
|
||
new()
|
||
{
|
||
Variante = segundoPatronCircular.Variante,
|
||
IndiceInicio = indiceIntermedioSegundo,
|
||
IndiceFin = indiceBajadaSegundo
|
||
}
|
||
};
|
||
|
||
ProcesarBajadaPrimeraPierna(
|
||
origen,
|
||
tramosPrimeraPierna,
|
||
codigoBajada,
|
||
llegadaBajada,
|
||
esperaPrimera + esperaCircular);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void ProcesarBajadaPrimeraPierna(
|
||
ParadaCandidata origen,
|
||
IReadOnlyList<TramoBus> tramosPrimeraPierna,
|
||
string codigoBajada,
|
||
DateTime llegadaBajada,
|
||
double esperaInicial)
|
||
{
|
||
if (tramosPrimeraPierna.Count == 0 || !EsEsperaInicialValida(esperaInicial))
|
||
return;
|
||
|
||
var ultimoTramoPrimeraPierna = tramosPrimeraPierna[^1];
|
||
foreach (var transferencia in ObtenerTransferenciasTransbordo(codigoBajada))
|
||
{
|
||
if (!segundasPiernasPorSubida.TryGetValue(transferencia.CodigoParada, out var opcionesSegundaPierna))
|
||
continue;
|
||
|
||
var duracionTransbordo = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(transferencia.DistanciaMetros, parametros);
|
||
var llegadaParadaTransbordo = UtilidadesTransitoDbus.AddSecondsSafe(llegadaBajada, duracionTransbordo);
|
||
var primeraSalidaSegundaPierna = llegadaParadaTransbordo.AddSeconds(margenTransbordo);
|
||
|
||
foreach (var opcionSegunda in opcionesSegundaPierna)
|
||
{
|
||
var segundoPatron = opcionSegunda.PrimerPatron;
|
||
if (!UtilidadesTransitoDbus.EsTransbordoReal(ultimoTramoPrimeraPierna.Variante, segundoPatron.Variante))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (opcionSegunda.IndiceSubidaPrimerPatron < 0 ||
|
||
opcionSegunda.IndiceSubidaPrimerPatron >= segundoPatron.CodigosParada.Count ||
|
||
opcionSegunda.IndiceFinPrimerPatron <= opcionSegunda.IndiceSubidaPrimerPatron ||
|
||
opcionSegunda.IndiceFinPrimerPatron >= segundoPatron.CodigosParada.Count)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var destino = opcionSegunda.Destino;
|
||
var codigoSubidaSegundo = segundoPatron.CodigosParada[opcionSegunda.IndiceSubidaPrimerPatron];
|
||
if (!string.Equals(codigoSubidaSegundo, transferencia.CodigoParada, StringComparison.OrdinalIgnoreCase))
|
||
continue;
|
||
|
||
var codigoFinPrimerSegmento = segundoPatron.CodigosParada[opcionSegunda.IndiceFinPrimerPatron];
|
||
if (!ResolverSegmentoHorario(
|
||
segundoPatron,
|
||
codigoSubidaSegundo,
|
||
codigoFinPrimerSegmento,
|
||
primeraSalidaSegundaPierna,
|
||
out var salidaSegunda,
|
||
out var llegadaFinPrimerSegmento))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var esperaTransbordo = Math.Max(0, (salidaSegunda - llegadaParadaTransbordo).TotalSeconds);
|
||
var llegadaDestinoBus = llegadaFinPrimerSegmento;
|
||
var esperaCircularSegundaPierna = 0.0;
|
||
|
||
if (opcionSegunda.PatronContinuacion is { } patronContinuacion)
|
||
{
|
||
if (opcionSegunda.IndiceInicioContinuacion < 0 ||
|
||
opcionSegunda.IndiceInicioContinuacion >= patronContinuacion.CodigosParada.Count ||
|
||
opcionSegunda.IndiceDestinoContinuacion <= opcionSegunda.IndiceInicioContinuacion ||
|
||
opcionSegunda.IndiceDestinoContinuacion >= patronContinuacion.CodigosParada.Count)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var codigoInicioContinuacion = patronContinuacion.CodigosParada[opcionSegunda.IndiceInicioContinuacion];
|
||
if (!string.Equals(codigoInicioContinuacion, codigoFinPrimerSegmento, StringComparison.OrdinalIgnoreCase))
|
||
continue;
|
||
|
||
var codigoDestinoContinuacion = patronContinuacion.CodigosParada[opcionSegunda.IndiceDestinoContinuacion];
|
||
if (!ResolverSegmentoHorario(
|
||
patronContinuacion,
|
||
codigoInicioContinuacion,
|
||
codigoDestinoContinuacion,
|
||
llegadaFinPrimerSegmento,
|
||
out var salidaContinuacion,
|
||
out llegadaDestinoBus))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
esperaCircularSegundaPierna = Math.Max(0, (salidaContinuacion - llegadaFinPrimerSegmento).TotalSeconds);
|
||
}
|
||
|
||
var esperaTotalTransbordos = esperaTransbordo + esperaCircularSegundaPierna;
|
||
if (!SonEsperasValidas(esperaInicial, esperaTotalTransbordos))
|
||
continue;
|
||
|
||
var tramos = tramosPrimeraPierna.Select(tramo => new TramoBus
|
||
{
|
||
Variante = tramo.Variante,
|
||
IndiceInicio = tramo.IndiceInicio,
|
||
IndiceFin = tramo.IndiceFin
|
||
}).ToList();
|
||
tramos.Add(new TramoBus
|
||
{
|
||
Variante = segundoPatron.Variante,
|
||
IndiceInicio = opcionSegunda.IndiceSubidaPrimerPatron,
|
||
IndiceFin = opcionSegunda.IndiceFinPrimerPatron
|
||
});
|
||
if (opcionSegunda.PatronContinuacion is { } patronContinuacionTramo)
|
||
{
|
||
tramos.Add(new TramoBus
|
||
{
|
||
Variante = patronContinuacionTramo.Variante,
|
||
IndiceInicio = opcionSegunda.IndiceInicioContinuacion,
|
||
IndiceFin = opcionSegunda.IndiceDestinoContinuacion
|
||
});
|
||
}
|
||
|
||
var numeroTransbordos = UtilidadesTransitoDbus.ContarTransbordosReales(tramos);
|
||
if (numeroTransbordos != 1 || !TransbordosEnRango(numeroTransbordos))
|
||
continue;
|
||
|
||
var ruta = new RutaBusAlternativa
|
||
{
|
||
Tramos = tramos,
|
||
NumeroTransbordos = numeroTransbordos,
|
||
CodigosParadasTransbordo = ConstruirCodigosTransbordo(tramos)
|
||
};
|
||
var duracionTotal = Math.Max(0, (llegadaDestinoBus - instanteSalida).TotalSeconds) + destino.TiempoAPie;
|
||
var candidata = new ResultadoBusquedaRutaBusDbus
|
||
{
|
||
ParadaOrigen = origen.Parada,
|
||
ParadaDestino = destino.Parada,
|
||
DistanciaPieInicio = origen.DistanciaAPie,
|
||
TiempoPieInicio = origen.TiempoAPie,
|
||
DistanciaPieFin = destino.DistanciaAPie,
|
||
TiempoPieFin = destino.TiempoAPie,
|
||
DistanciaPieTransbordos = transferencia.DistanciaMetros,
|
||
DistanciasPieTransbordosPorTramo = new[] { transferencia.DistanciaMetros },
|
||
TiempoEsperaInicialSegundos = esperaInicial,
|
||
TiempoEsperaTransbordosSegundos = esperaTotalTransbordos,
|
||
DuracionEstimadaSegundos = duracionTotal,
|
||
PuntuacionCoste = UtilidadesTransitoDbus.CalcularCosteConPenalizacionesRankingSegundos(
|
||
duracionTotal,
|
||
parametros,
|
||
numeroTransbordos,
|
||
origen.DistanciaAPie,
|
||
transferencia.DistanciaMetros,
|
||
destino.DistanciaAPie),
|
||
RutaBus = ruta
|
||
};
|
||
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(ruta);
|
||
if (!mejores.TryGetValue(firma, out var actual) || ResultadoBusquedaEsMejor(candidata, actual))
|
||
mejores[firma] = candidata;
|
||
}
|
||
}
|
||
}
|
||
|
||
List<(string CodigoParada, double DistanciaMetros)> ObtenerTransferenciasTransbordo(string codigoParada)
|
||
{
|
||
if (transferenciasUtilesPorBajada.TryGetValue(codigoParada, out var cacheadas))
|
||
return cacheadas;
|
||
|
||
var mejoresTransferencias = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase)
|
||
{ };
|
||
|
||
if (segundasPiernasPorSubida.ContainsKey(codigoParada))
|
||
mejoresTransferencias[codigoParada] = 0;
|
||
|
||
if (indiceTransito.TransferenciasPorParada.TryGetValue(codigoParada, out var transferencias))
|
||
{
|
||
foreach (var transferencia in transferencias)
|
||
{
|
||
if (!segundasPiernasPorSubida.ContainsKey(transferencia.CodigoParadaDestino))
|
||
continue;
|
||
|
||
if (!mejoresTransferencias.TryGetValue(transferencia.CodigoParadaDestino, out var actual) ||
|
||
transferencia.DistanciaMetros < actual)
|
||
{
|
||
mejoresTransferencias[transferencia.CodigoParadaDestino] = transferencia.DistanciaMetros;
|
||
}
|
||
}
|
||
}
|
||
|
||
var resultado = mejoresTransferencias
|
||
.Select(par => (CodigoParada: par.Key, DistanciaMetros: par.Value))
|
||
.OrderBy(par => par.DistanciaMetros)
|
||
.ToList();
|
||
|
||
transferenciasUtilesPorBajada[codigoParada] = resultado;
|
||
return resultado;
|
||
}
|
||
|
||
bool TieneTransferenciaUtil(string codigoParada)
|
||
=> ObtenerTransferenciasTransbordo(codigoParada).Count > 0;
|
||
|
||
Dictionary<string, List<SegundaPiernaTransbordoRobusta>> ConstruirSegundasPiernasPorSubida()
|
||
{
|
||
var porSubida = new Dictionary<string, List<SegundaPiernaTransbordoRobusta>>(StringComparer.OrdinalIgnoreCase);
|
||
var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
|
||
foreach (var destino in destinoPorCodigo.Values)
|
||
{
|
||
if (!indiceTransito.PatronesPorParada.TryGetValue(destino.Parada.Codigo, out var referenciasDestino))
|
||
continue;
|
||
|
||
foreach (var referenciaDestino in referenciasDestino)
|
||
{
|
||
var patron = referenciaDestino.Patron;
|
||
if (!PatronPermitido(patron, parametros))
|
||
continue;
|
||
|
||
var indiceDestino = referenciaDestino.IndiceParada;
|
||
if (indiceDestino <= 0 || indiceDestino >= patron.CodigosParada.Count)
|
||
continue;
|
||
|
||
var indiceMinimoSubida = Math.Max(0, indiceDestino - MaxBajadasPrimeraPiernaTransbordoRobusto);
|
||
for (var indiceSubida = indiceMinimoSubida; indiceSubida < indiceDestino; indiceSubida++)
|
||
{
|
||
var codigoSubida = patron.CodigosParada[indiceSubida];
|
||
AgregarOpcion(
|
||
codigoSubida,
|
||
new SegundaPiernaTransbordoRobusta(
|
||
patron,
|
||
indiceSubida,
|
||
indiceDestino,
|
||
null,
|
||
-1,
|
||
-1,
|
||
destino));
|
||
}
|
||
|
||
for (var indiceIntermedioDestino = 0; indiceIntermedioDestino < indiceDestino; indiceIntermedioDestino++)
|
||
{
|
||
var codigoIntermedio = patron.CodigosParada[indiceIntermedioDestino];
|
||
if (!indiceTransito.PatronesPorParada.TryGetValue(codigoIntermedio, out var referenciasIntermedio))
|
||
continue;
|
||
|
||
foreach (var referenciaIntermedio in referenciasIntermedio)
|
||
{
|
||
var patronInicialCircular = referenciaIntermedio.Patron;
|
||
if (!PatronPermitido(patronInicialCircular, parametros) ||
|
||
!EsContinuidadCircular(patronInicialCircular, patron))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var indiceIntermedioInicial = referenciaIntermedio.IndiceParada;
|
||
if (indiceIntermedioInicial <= 0 ||
|
||
indiceIntermedioInicial >= patronInicialCircular.CodigosParada.Count)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var indiceMinimoSubidaCircular = Math.Max(
|
||
0,
|
||
indiceIntermedioInicial - MaxBajadasPrimeraPiernaTransbordoRobusto);
|
||
for (var indiceSubidaCircular = indiceMinimoSubidaCircular;
|
||
indiceSubidaCircular < indiceIntermedioInicial;
|
||
indiceSubidaCircular++)
|
||
{
|
||
var codigoSubida = patronInicialCircular.CodigosParada[indiceSubidaCircular];
|
||
AgregarOpcion(
|
||
codigoSubida,
|
||
new SegundaPiernaTransbordoRobusta(
|
||
patronInicialCircular,
|
||
indiceSubidaCircular,
|
||
indiceIntermedioInicial,
|
||
patron,
|
||
indiceIntermedioDestino,
|
||
indiceDestino,
|
||
destino));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach (var codigo in porSubida.Keys.ToList())
|
||
{
|
||
porSubida[codigo] = porSubida[codigo]
|
||
.OrderBy(x => x.Destino.TiempoAPie)
|
||
.ThenBy(x => x.NumeroTramos)
|
||
.ThenBy(x => (x.IndiceFinPrimerPatron - x.IndiceSubidaPrimerPatron) +
|
||
Math.Max(0, x.IndiceDestinoContinuacion - x.IndiceInicioContinuacion))
|
||
.Take(20)
|
||
.ToList();
|
||
}
|
||
|
||
return porSubida;
|
||
|
||
void AgregarOpcion(string codigoSubida, SegundaPiernaTransbordoRobusta opcion)
|
||
{
|
||
var firma = string.Join("|",
|
||
opcion.PrimerPatron.Id,
|
||
opcion.IndiceSubidaPrimerPatron.ToString(CultureInfo.InvariantCulture),
|
||
opcion.IndiceFinPrimerPatron.ToString(CultureInfo.InvariantCulture),
|
||
opcion.PatronContinuacion?.Id ?? "-",
|
||
opcion.IndiceInicioContinuacion.ToString(CultureInfo.InvariantCulture),
|
||
opcion.IndiceDestinoContinuacion.ToString(CultureInfo.InvariantCulture),
|
||
opcion.Destino.Parada.Codigo);
|
||
if (!firmas.Add(firma))
|
||
return;
|
||
|
||
if (!porSubida.TryGetValue(codigoSubida, out var lista))
|
||
{
|
||
lista = new List<SegundaPiernaTransbordoRobusta>();
|
||
porSubida[codigoSubida] = lista;
|
||
}
|
||
|
||
lista.Add(opcion);
|
||
}
|
||
}
|
||
|
||
bool EsEsperaInicialValida(double esperaInicial)
|
||
{
|
||
return parametros.MaxEsperaInicialMinutos <= 0 ||
|
||
esperaInicial <= parametros.MaxEsperaInicialMinutos * 60.0;
|
||
}
|
||
|
||
bool SonEsperasValidas(double esperaInicial, double esperaTransbordo)
|
||
{
|
||
if (!EsEsperaInicialValida(esperaInicial))
|
||
return false;
|
||
|
||
if (parametros.MaxEsperaTransbordoMinutos > 0 &&
|
||
esperaTransbordo > parametros.MaxEsperaTransbordoMinutos * 60.0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var esperaTotal = esperaInicial + esperaTransbordo;
|
||
return parametros.MaxEsperaTotalMinutos <= 0 ||
|
||
esperaTotal <= parametros.MaxEsperaTotalMinutos * 60.0;
|
||
}
|
||
|
||
bool TransbordosEnRango(int numeroTransbordos)
|
||
{
|
||
if (!parametros.ActivarRangoTransbordos)
|
||
return true;
|
||
|
||
var minimo = Math.Min(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
|
||
var maximo = Math.Max(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
|
||
return numeroTransbordos >= minimo && numeroTransbordos <= maximo;
|
||
}
|
||
|
||
void PodarMejores()
|
||
{
|
||
mejores = SeleccionarPoolBusqueda(mejores.Values, maxResultados * 2, parametros)
|
||
.ToDictionary(
|
||
x => UtilidadesTransitoDbus.FirmaRutaBus(x.RutaBus),
|
||
StringComparer.OrdinalIgnoreCase);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Extiende rutas que empiezan en una circular para poder coger antes el sentido contrario
|
||
/// cuando eso evita una caminata inicial claramente peor.
|
||
/// </summary>
|
||
private List<ResultadoBusquedaRutaBusDbus> BuscarExtensionesCircularesInicialesConHorario(
|
||
IndiceTransitoDbus indiceTransito,
|
||
IReadOnlyList<ParadaCandidata> candidatasOrigen,
|
||
IReadOnlyList<ResultadoBusquedaRutaBusDbus> candidatasBase,
|
||
DateTime instanteSalida,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (candidatasOrigen.Count == 0 || candidatasBase.Count == 0)
|
||
return new();
|
||
|
||
var mejores = new Dictionary<string, ResultadoBusquedaRutaBusDbus>(StringComparer.OrdinalIgnoreCase);
|
||
var maxResultados = Math.Max(24, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 12);
|
||
var basesExtension = SeleccionarPoolBusqueda(
|
||
candidatasBase,
|
||
Math.Max(12, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 4),
|
||
parametros);
|
||
|
||
foreach (var baseRuta in basesExtension)
|
||
{
|
||
if (baseRuta.RutaBus.Tramos.Count == 0)
|
||
continue;
|
||
|
||
var primerTramoBase = baseRuta.RutaBus.Tramos[0];
|
||
var segundoPatron = ResolverPatronDesdeTramo(indiceTransito, primerTramoBase);
|
||
if (segundoPatron is null)
|
||
continue;
|
||
|
||
foreach (var origen in candidatasOrigen.OrderBy(x => x.TiempoAPie))
|
||
{
|
||
if (origen.DistanciaAPie + MejoraMinimaAccesoExtensionCircularMetros >= baseRuta.DistanciaPieInicio)
|
||
continue;
|
||
|
||
if (!indiceTransito.PatronesPorParada.TryGetValue(origen.Parada.Codigo, out var referenciasOrigen))
|
||
continue;
|
||
|
||
var llegadaParadaOrigen = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, origen.TiempoAPie);
|
||
foreach (var referenciaOrigen in referenciasOrigen)
|
||
{
|
||
var primerPatron = referenciaOrigen.Patron;
|
||
if (!PatronPermitido(primerPatron, parametros) ||
|
||
!EsContinuidadCircular(primerPatron, segundoPatron))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var indiceSubida = referenciaOrigen.IndiceParada;
|
||
if (indiceSubida < 0 || indiceSubida >= primerPatron.CodigosParada.Count - 1)
|
||
continue;
|
||
|
||
for (var indiceIntermedioPrimero = indiceSubida + 1;
|
||
indiceIntermedioPrimero < primerPatron.CodigosParada.Count;
|
||
indiceIntermedioPrimero++)
|
||
{
|
||
var codigoIntermedio = primerPatron.CodigosParada[indiceIntermedioPrimero];
|
||
var indicesSegundo = IndicesParadaAntesDe(segundoPatron, codigoIntermedio, primerTramoBase.IndiceFin);
|
||
foreach (var indiceIntermedioSegundo in indicesSegundo)
|
||
{
|
||
if (!ResolverSegmentoHorario(
|
||
primerPatron,
|
||
origen.Parada.Codigo,
|
||
codigoIntermedio,
|
||
llegadaParadaOrigen,
|
||
out var salidaPrimera,
|
||
out var llegadaIntermedia))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!ResolverSegmentoHorario(
|
||
segundoPatron,
|
||
codigoIntermedio,
|
||
primerTramoBase.CodigoParadaFin,
|
||
llegadaIntermedia,
|
||
out var salidaSegunda,
|
||
out _))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var tramos = new List<TramoBus>
|
||
{
|
||
new()
|
||
{
|
||
Variante = primerPatron.Variante,
|
||
IndiceInicio = indiceSubida,
|
||
IndiceFin = indiceIntermedioPrimero
|
||
},
|
||
new()
|
||
{
|
||
Variante = segundoPatron.Variante,
|
||
IndiceInicio = indiceIntermedioSegundo,
|
||
IndiceFin = primerTramoBase.IndiceFin
|
||
}
|
||
};
|
||
|
||
tramos.AddRange(baseRuta.RutaBus.Tramos.Skip(1).Select(tramo => new TramoBus
|
||
{
|
||
Variante = tramo.Variante,
|
||
IndiceInicio = tramo.IndiceInicio,
|
||
IndiceFin = tramo.IndiceFin
|
||
}));
|
||
|
||
var numeroTransbordos = UtilidadesTransitoDbus.ContarTransbordosReales(tramos);
|
||
if (parametros.ActivarRangoTransbordos)
|
||
{
|
||
var minimo = Math.Min(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
|
||
var maximo = Math.Max(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
|
||
if (numeroTransbordos < minimo || numeroTransbordos > maximo)
|
||
continue;
|
||
}
|
||
|
||
var ruta = new RutaBusAlternativa
|
||
{
|
||
Tramos = tramos,
|
||
NumeroTransbordos = numeroTransbordos,
|
||
CodigosParadasTransbordo = ConstruirCodigosTransbordo(tramos)
|
||
};
|
||
|
||
var esperaInicial = Math.Max(0, (salidaPrimera - llegadaParadaOrigen).TotalSeconds) +
|
||
Math.Max(0, (salidaSegunda - llegadaIntermedia).TotalSeconds);
|
||
var duracionTotal = baseRuta.DuracionEstimadaSegundos;
|
||
var candidata = new ResultadoBusquedaRutaBusDbus
|
||
{
|
||
ParadaOrigen = origen.Parada,
|
||
ParadaDestino = baseRuta.ParadaDestino,
|
||
DistanciaPieInicio = origen.DistanciaAPie,
|
||
TiempoPieInicio = origen.TiempoAPie,
|
||
DistanciaPieFin = baseRuta.DistanciaPieFin,
|
||
TiempoPieFin = baseRuta.TiempoPieFin,
|
||
DistanciaPieTransbordos = baseRuta.DistanciaPieTransbordos,
|
||
DistanciasPieTransbordosPorTramo = baseRuta.DistanciasPieTransbordosPorTramo.ToArray(),
|
||
TiempoEsperaInicialSegundos = esperaInicial,
|
||
TiempoEsperaTransbordosSegundos = baseRuta.TiempoEsperaTransbordosSegundos,
|
||
DuracionEstimadaSegundos = duracionTotal,
|
||
PuntuacionCoste = UtilidadesTransitoDbus.CalcularCosteConPenalizacionesRankingSegundos(
|
||
duracionTotal,
|
||
parametros,
|
||
numeroTransbordos,
|
||
ConstruirDistanciasPiePorTramo(
|
||
origen.DistanciaAPie,
|
||
ObtenerDistanciasPieTransbordosPorTramo(baseRuta),
|
||
baseRuta.DistanciaPieFin).ToArray()),
|
||
RutaBus = ruta
|
||
};
|
||
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(ruta);
|
||
if (!mejores.TryGetValue(firma, out var actual) || ResultadoBusquedaEsMejor(candidata, actual))
|
||
mejores[firma] = candidata;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return SeleccionarPoolBusqueda(mejores.Values, maxResultados, parametros);
|
||
}
|
||
|
||
private static PatronTransitoDbus? ResolverPatronDesdeTramo(IndiceTransitoDbus indiceTransito, TramoBus tramo)
|
||
{
|
||
if (!tramo.EsValido)
|
||
return null;
|
||
|
||
if (indiceTransito.PatronesPorParada.TryGetValue(tramo.CodigoParadaInicio, out var referencias))
|
||
{
|
||
var encontrado = referencias
|
||
.Where(referencia => referencia.IndiceParada == tramo.IndiceInicio)
|
||
.Select(referencia => referencia.Patron)
|
||
.FirstOrDefault(patron => PatronCoincideConTramo(patron, tramo));
|
||
if (encontrado is not null)
|
||
return encontrado;
|
||
}
|
||
|
||
return indiceTransito.Patrones.FirstOrDefault(patron => PatronCoincideConTramo(patron, tramo));
|
||
}
|
||
|
||
private static bool PatronCoincideConTramo(PatronTransitoDbus patron, TramoBus tramo)
|
||
{
|
||
var linea = UtilidadesTransitoDbus.LineaCodigoStr(tramo.Variante);
|
||
var sentido = UtilidadesTransitoDbus.NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo);
|
||
return
|
||
ReferenceEquals(patron.Variante, tramo.Variante) ||
|
||
(string.Equals(patron.CodigoLinea, linea, StringComparison.OrdinalIgnoreCase) &&
|
||
patron.CodigoItinerario == tramo.Variante.CodigoItinerario &&
|
||
string.Equals(
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(patron.Sentido),
|
||
sentido,
|
||
StringComparison.OrdinalIgnoreCase) &&
|
||
patron.CodigosParada.Count == tramo.Variante.CodigosParadas.Count &&
|
||
patron.CodigosParada.SequenceEqual(tramo.Variante.CodigosParadas, StringComparer.OrdinalIgnoreCase));
|
||
}
|
||
|
||
private static IEnumerable<int> IndicesParadaAntesDe(PatronTransitoDbus patron, string codigoParada, int indiceLimiteFin)
|
||
{
|
||
var limite = Math.Min(indiceLimiteFin, patron.CodigosParada.Count);
|
||
for (var indice = 0; indice < limite; indice++)
|
||
{
|
||
if (string.Equals(patron.CodigosParada[indice], codigoParada, StringComparison.OrdinalIgnoreCase))
|
||
yield return indice;
|
||
}
|
||
}
|
||
|
||
private static List<string> ConstruirCodigosTransbordo(IReadOnlyList<TramoBus> tramos)
|
||
{
|
||
var codigos = new List<string>();
|
||
for (var indice = 1; indice < tramos.Count; indice++)
|
||
{
|
||
if (!UtilidadesTransitoDbus.EsTransbordoReal(tramos[indice - 1], tramos[indice]))
|
||
continue;
|
||
|
||
var codigo = tramos[indice].CodigoParadaInicio;
|
||
if (!codigos.Contains(codigo, StringComparer.OrdinalIgnoreCase))
|
||
codigos.Add(codigo);
|
||
}
|
||
|
||
return codigos;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Conserva candidatas buenas por coste y tambien un cupo por menor caminata.
|
||
/// </summary>
|
||
private List<ResultadoBusquedaRutaBusDbus> SeleccionarPoolBusqueda(
|
||
IEnumerable<ResultadoBusquedaRutaBusDbus> candidatas,
|
||
int maxPorCoste,
|
||
ParametrosPlanificadorRuta? parametros = null)
|
||
{
|
||
var lista = candidatas.ToList();
|
||
if (lista.Count == 0)
|
||
return new();
|
||
|
||
var seleccionadas = new List<ResultadoBusquedaRutaBusDbus>();
|
||
var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
var limiteCoste = Math.Max(1, maxPorCoste);
|
||
var limiteAndar = Math.Max(8, limiteCoste / 2);
|
||
var limiteEquilibrado = Math.Max(8, limiteCoste / 2);
|
||
var duracionMinima = lista
|
||
.Select(x => x.DuracionEstimadaSegundos)
|
||
.Where(double.IsFinite)
|
||
.DefaultIfEmpty(double.PositiveInfinity)
|
||
.Min();
|
||
var limiteDuracionAndarRazonable = double.IsFinite(duracionMinima)
|
||
? duracionMinima + Math.Max(15 * 60.0, duracionMinima * 0.5)
|
||
: double.PositiveInfinity;
|
||
|
||
Agregar(lista
|
||
.OrderBy(x => x.PuntuacionCoste)
|
||
.ThenBy(x => x.DuracionEstimadaSegundos)
|
||
.ThenBy(x => x.RutaBus.NumeroTransbordos)
|
||
.ThenBy(x => x.DistanciaPieTotal)
|
||
.Take(limiteCoste));
|
||
|
||
Agregar(lista
|
||
.OrderBy(x => x.DistanciaPieTotal)
|
||
.ThenBy(x => x.DuracionEstimadaSegundos)
|
||
.ThenBy(x => x.PuntuacionCoste)
|
||
.ThenBy(x => x.RutaBus.NumeroTransbordos)
|
||
.Take(limiteAndar));
|
||
|
||
Agregar(lista
|
||
.OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
|
||
.ThenBy(x => x.DuracionEstimadaSegundos)
|
||
.ThenBy(x => x.RutaBus.NumeroTransbordos)
|
||
.ThenBy(x => x.PuntuacionCoste)
|
||
.Take(limiteEquilibrado));
|
||
|
||
Agregar(lista
|
||
.Where(x => x.DuracionEstimadaSegundos <= limiteDuracionAndarRazonable)
|
||
.OrderBy(x => x.DistanciaPieTotal)
|
||
.ThenBy(x => x.DuracionEstimadaSegundos)
|
||
.ThenBy(x => x.PuntuacionCoste)
|
||
.ThenBy(x => x.RutaBus.NumeroTransbordos)
|
||
.Take(Math.Max(8, limiteCoste / 2)));
|
||
|
||
Agregar(lista
|
||
.OrderBy(x => x.DuracionEstimadaSegundos)
|
||
.ThenBy(x => x.PuntuacionCoste)
|
||
.ThenBy(x => x.DistanciaPieTotal)
|
||
.ThenBy(x => x.RutaBus.NumeroTransbordos)
|
||
.Take(Math.Max(6, limiteCoste / 3)));
|
||
|
||
return seleccionadas
|
||
.OrderBy(x => x.PuntuacionCoste)
|
||
.ThenBy(x => x.DuracionEstimadaSegundos)
|
||
.ThenBy(x => x.RutaBus.NumeroTransbordos)
|
||
.ThenBy(x => x.DistanciaPieTotal)
|
||
.ToList();
|
||
|
||
void Agregar(IEnumerable<ResultadoBusquedaRutaBusDbus> origen)
|
||
{
|
||
foreach (var candidata in origen)
|
||
{
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(candidata.RutaBus);
|
||
if (firmas.Add(firma))
|
||
seleccionadas.Add(candidata);
|
||
}
|
||
}
|
||
}
|
||
|
||
private double CalcularScoreBusquedaEquilibrado(
|
||
ResultadoBusquedaRutaBusDbus resultado,
|
||
ParametrosPlanificadorRuta? parametros)
|
||
{
|
||
var distanciaBus = Math.Max(0, resultado.RutaBus.DistanciaTotalBus);
|
||
if (distanciaBus <= 0 && resultado.RutaBus.Tramos.Count > 0)
|
||
distanciaBus = CalcularDistanciaBusCandidata(resultado.RutaBus.Tramos);
|
||
|
||
var distanciaAndando = parametros is null
|
||
? Math.Max(0, resultado.DistanciaPieTotal)
|
||
: UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaTotalMetros(
|
||
ConstruirDistanciasPiePorTramo(resultado),
|
||
parametros);
|
||
var factor = parametros is not null && double.IsFinite(parametros.FactorEquilibradoAndarBus)
|
||
? Math.Max(0, parametros.FactorEquilibradoAndarBus)
|
||
: 12.0;
|
||
|
||
var score = distanciaBus + (distanciaAndando * factor);
|
||
return double.IsFinite(score) && score >= 0
|
||
? score
|
||
: double.PositiveInfinity;
|
||
}
|
||
|
||
private static List<double> ConstruirDistanciasPiePorTramo(ResultadoBusquedaRutaBusDbus resultado)
|
||
=> ConstruirDistanciasPiePorTramo(
|
||
resultado.DistanciaPieInicio,
|
||
ObtenerDistanciasPieTransbordosPorTramo(resultado),
|
||
resultado.DistanciaPieFin);
|
||
|
||
private static IReadOnlyList<double> ObtenerDistanciasPieTransbordosPorTramo(
|
||
ResultadoBusquedaRutaBusDbus resultado)
|
||
{
|
||
if (resultado.DistanciasPieTransbordosPorTramo.Count > 0)
|
||
return resultado.DistanciasPieTransbordosPorTramo;
|
||
|
||
return double.IsFinite(resultado.DistanciaPieTransbordos) && resultado.DistanciaPieTransbordos > 0
|
||
? new[] { resultado.DistanciaPieTransbordos }
|
||
: Array.Empty<double>();
|
||
}
|
||
|
||
private static List<double> ConstruirDistanciasPiePorTramo(
|
||
double distanciaInicio,
|
||
IEnumerable<double> distanciasTransbordos,
|
||
double distanciaFin)
|
||
{
|
||
var distancias = new List<double>();
|
||
AgregarDistancia(distanciaInicio);
|
||
foreach (var distancia in distanciasTransbordos)
|
||
AgregarDistancia(distancia);
|
||
AgregarDistancia(distanciaFin);
|
||
return distancias;
|
||
|
||
void AgregarDistancia(double distancia)
|
||
{
|
||
if (double.IsFinite(distancia) && distancia > 0)
|
||
distancias.Add(distancia);
|
||
}
|
||
}
|
||
|
||
private static bool DebeBuscarTransbordosRobustos(
|
||
IReadOnlyList<ResultadoBusquedaRutaBusDbus> candidatasBus,
|
||
IReadOnlyList<ParadaCandidata> candidatasOrigen,
|
||
IReadOnlyList<ParadaCandidata> candidatasDestino)
|
||
{
|
||
if (candidatasOrigen.Count == 0 || candidatasDestino.Count == 0)
|
||
return false;
|
||
|
||
if (candidatasBus.Count == 0)
|
||
return true;
|
||
|
||
var mejorPieActual = candidatasBus
|
||
.Select(x => x.DistanciaPieTotal)
|
||
.Where(double.IsFinite)
|
||
.DefaultIfEmpty(double.PositiveInfinity)
|
||
.Min();
|
||
if (mejorPieActual <= UmbralActivacionTransbordoRobustoMetros)
|
||
return false;
|
||
|
||
var mejorAccesoExtremos = candidatasOrigen.Min(x => x.DistanciaAPie) +
|
||
candidatasDestino.Min(x => x.DistanciaAPie);
|
||
return mejorAccesoExtremos <= UmbralAccesoDestinoTransbordoRobustoMetros;
|
||
}
|
||
|
||
private static bool DebeUsarBusquedaDirigidaUnaTransbordo(ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (!parametros.ActivarRangoTransbordos)
|
||
return false;
|
||
|
||
var minimo = Math.Min(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
|
||
return minimo <= 1;
|
||
}
|
||
|
||
private bool ResolverSegmentoHorario(
|
||
PatronTransitoDbus patron,
|
||
string codigoParadaInicio,
|
||
string codigoParadaFin,
|
||
DateTime primeraSalidaAceptable,
|
||
out DateTime salida,
|
||
out DateTime llegada)
|
||
{
|
||
return ResolverSegmentoHorario(
|
||
codigoParadaInicio,
|
||
codigoParadaFin,
|
||
UtilidadesTransitoDbus.LineaCodigoStr(patron.Variante),
|
||
patron.CodigoItinerario,
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(patron.Sentido),
|
||
primeraSalidaAceptable,
|
||
out salida,
|
||
out llegada);
|
||
}
|
||
|
||
private bool ResolverSegmentoHorario(
|
||
string codigoParadaInicio,
|
||
string codigoParadaFin,
|
||
string codigoLinea,
|
||
int codigoItinerario,
|
||
string sentido,
|
||
DateTime primeraSalidaAceptable,
|
||
out DateTime salida,
|
||
out DateTime llegada)
|
||
{
|
||
var primeraSalidaNormalizada = RedondearAlSegundoSuperior(primeraSalidaAceptable);
|
||
var fecha = DateOnly.FromDateTime(primeraSalidaNormalizada);
|
||
var hora = TimeOnly.FromDateTime(primeraSalidaNormalizada);
|
||
var clave = string.Join("|",
|
||
codigoParadaInicio,
|
||
codigoParadaFin,
|
||
codigoLinea,
|
||
codigoItinerario.ToString(CultureInfo.InvariantCulture),
|
||
sentido,
|
||
fecha.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||
hora.ToString("HH:mm:ss", CultureInfo.InvariantCulture));
|
||
|
||
if (_cacheSegmentosHorarios.TryGetValue(clave, out var cacheado))
|
||
{
|
||
salida = cacheado.salida;
|
||
llegada = cacheado.llegada;
|
||
return cacheado.ok;
|
||
}
|
||
|
||
var ok = _servicioHorarios.TryGetNextBusSegment(
|
||
codigoParadaInicio,
|
||
codigoParadaFin,
|
||
codigoLinea,
|
||
codigoItinerario,
|
||
sentido,
|
||
fecha,
|
||
hora,
|
||
out salida,
|
||
out llegada,
|
||
out _);
|
||
|
||
_cacheSegmentosHorarios[clave] = (ok, salida, llegada);
|
||
return ok;
|
||
}
|
||
|
||
private static DateTime RedondearAlSegundoSuperior(DateTime fecha)
|
||
{
|
||
var resto = fecha.Ticks % TimeSpan.TicksPerSecond;
|
||
return resto == 0
|
||
? fecha
|
||
: new DateTime(fecha.Ticks + TimeSpan.TicksPerSecond - resto, fecha.Kind);
|
||
}
|
||
|
||
private static bool EsContinuidadCircular(PatronTransitoDbus primero, PatronTransitoDbus segundo)
|
||
{
|
||
return string.Equals(primero.CodigoLinea, segundo.CodigoLinea, StringComparison.OrdinalIgnoreCase) &&
|
||
primero.CodigoItinerario == segundo.CodigoItinerario &&
|
||
!string.Equals(
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(primero.Sentido),
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(segundo.Sentido),
|
||
StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
private static bool EsContinuidadCircular(InfoVariante primero, InfoVariante segundo)
|
||
{
|
||
return string.Equals(
|
||
UtilidadesTransitoDbus.LineaCodigoStr(primero),
|
||
UtilidadesTransitoDbus.LineaCodigoStr(segundo),
|
||
StringComparison.OrdinalIgnoreCase) &&
|
||
primero.CodigoItinerario == segundo.CodigoItinerario &&
|
||
!string.Equals(
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(primero.Sentido?.Codigo),
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(segundo.Sentido?.Codigo),
|
||
StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Compara dos candidatas de bus de forma consistente con el buscador temporal.
|
||
/// </summary>
|
||
private static bool ResultadoBusquedaEsMejor(ResultadoBusquedaRutaBusDbus candidato, ResultadoBusquedaRutaBusDbus actual)
|
||
{
|
||
var comparacionCoste = candidato.PuntuacionCoste.CompareTo(actual.PuntuacionCoste);
|
||
if (comparacionCoste != 0)
|
||
return comparacionCoste < 0;
|
||
|
||
var comparacionDuracion = candidato.DuracionEstimadaSegundos.CompareTo(actual.DuracionEstimadaSegundos);
|
||
if (comparacionDuracion != 0)
|
||
return comparacionDuracion < 0;
|
||
|
||
var comparacionTrans = candidato.RutaBus.NumeroTransbordos.CompareTo(actual.RutaBus.NumeroTransbordos);
|
||
if (comparacionTrans != 0)
|
||
return comparacionTrans < 0;
|
||
|
||
return candidato.DistanciaPieTotal < actual.DistanciaPieTotal;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deduplica alternativas tecnicamente distintas pero equivalentes para la persona usuaria.
|
||
/// </summary>
|
||
private static List<AlternativaRuta> DeduplicarAlternativasEquivalentes(IReadOnlyList<AlternativaRuta> alternativas)
|
||
{
|
||
var mejoresPorFirma = new Dictionary<string, AlternativaRuta>(StringComparer.OrdinalIgnoreCase);
|
||
|
||
foreach (var alternativa in alternativas)
|
||
{
|
||
if (alternativa.AlternativaBus is null || alternativa.AlternativaBus.Tramos.Count == 0)
|
||
continue;
|
||
|
||
var firma = CrearFirmaAlternativaEquivalente(alternativa);
|
||
if (!mejoresPorFirma.TryGetValue(firma, out var actual) ||
|
||
AlternativaEquivalenteEsMejor(alternativa, actual))
|
||
{
|
||
mejoresPorFirma[firma] = alternativa;
|
||
}
|
||
}
|
||
|
||
return mejoresPorFirma.Values
|
||
.OrderBy(x => x.PuntuacionCoste)
|
||
.ThenBy(x => x.DuracionTotalAproxSegundos)
|
||
.ThenBy(x => x.NumeroTransbordosReales)
|
||
.ThenBy(x => x.DistanciaPieTotalAprox)
|
||
.ToList();
|
||
}
|
||
|
||
private static List<AlternativaRuta> SeleccionarPoolFinal(
|
||
IReadOnlyList<AlternativaRuta> alternativas,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
var seleccionadas = new List<AlternativaRuta>();
|
||
var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
|
||
Agregar(alternativas
|
||
.OrderBy(x => CalcularScoreEquilibrado(x, parametros))
|
||
.ThenBy(x => x.PuntuacionCoste)
|
||
.ThenBy(x => x.DuracionTotalAproxSegundos)
|
||
.Take(16));
|
||
|
||
Agregar(alternativas
|
||
.OrderBy(x => x.PuntuacionCoste)
|
||
.ThenBy(x => x.DuracionTotalAproxSegundos)
|
||
.Take(12));
|
||
|
||
Agregar(alternativas
|
||
.OrderBy(x => x.DistanciaPieTotalAprox)
|
||
.ThenBy(x => x.DuracionTotalAproxSegundos)
|
||
.Take(8));
|
||
|
||
return seleccionadas
|
||
.OrderBy(x => x.PuntuacionCoste)
|
||
.ThenBy(x => x.DuracionTotalAproxSegundos)
|
||
.ToList();
|
||
|
||
void Agregar(IEnumerable<AlternativaRuta> candidatas)
|
||
{
|
||
foreach (var alternativa in candidatas)
|
||
{
|
||
if (alternativa.AlternativaBus is null)
|
||
continue;
|
||
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(alternativa.AlternativaBus);
|
||
if (firmas.Add(firma))
|
||
seleccionadas.Add(alternativa);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Firma de equivalencia orientada a resultado visible (mismos tramos/paradas y tiempos practicos).
|
||
/// </summary>
|
||
private static string CrearFirmaAlternativaEquivalente(AlternativaRuta alternativa)
|
||
{
|
||
var tramos = alternativa.AlternativaBus!.Tramos
|
||
.Select(tramo =>
|
||
{
|
||
var linea = UtilidadesTransitoDbus.LineaCodigoStr(tramo.Variante);
|
||
var sentido = UtilidadesTransitoDbus.NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo);
|
||
return $"{linea}-{sentido}-{tramo.CodigoParadaInicio}>{tramo.CodigoParadaFin}";
|
||
});
|
||
|
||
return string.Join("|", new[]
|
||
{
|
||
string.Join(";", tramos),
|
||
$"tp0:{BucketSegundos(alternativa.TiempoPieInicio)}",
|
||
$"te0:{BucketSegundos(alternativa.TiempoEsperaInicialSegundos)}",
|
||
$"tet:{BucketSegundos(alternativa.TiempoEsperaTransbordosSegundos)}",
|
||
$"tpf:{BucketSegundos(alternativa.TiempoPieFin)}",
|
||
$"tt:{BucketSegundos(alternativa.DuracionTotalAproxSegundos)}"
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Compara dos alternativas equivalentes y decide cual conservar.
|
||
/// </summary>
|
||
private static bool AlternativaEquivalenteEsMejor(AlternativaRuta candidato, AlternativaRuta actual)
|
||
{
|
||
var comparacionCoste = candidato.PuntuacionCoste.CompareTo(actual.PuntuacionCoste);
|
||
if (comparacionCoste != 0)
|
||
return comparacionCoste < 0;
|
||
|
||
var comparacionDuracion = candidato.DuracionTotalAproxSegundos.CompareTo(actual.DuracionTotalAproxSegundos);
|
||
if (comparacionDuracion != 0)
|
||
return comparacionDuracion < 0;
|
||
|
||
var comparacionTransbordos = candidato.NumeroTransbordosReales.CompareTo(actual.NumeroTransbordosReales);
|
||
if (comparacionTransbordos != 0)
|
||
return comparacionTransbordos < 0;
|
||
|
||
var comparacionPie = candidato.DistanciaPieTotalAprox.CompareTo(actual.DistanciaPieTotalAprox);
|
||
if (comparacionPie != 0)
|
||
return comparacionPie < 0;
|
||
|
||
return CodigoItinerarioMinimo(candidato) < CodigoItinerarioMinimo(actual);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Agrupa tiempos en cubos de 30 segundos para evitar ruido numerico.
|
||
/// </summary>
|
||
private static int BucketSegundos(double segundos)
|
||
{
|
||
if (!double.IsFinite(segundos) || segundos <= 0)
|
||
return 0;
|
||
|
||
return (int)Math.Round(segundos / 30.0, MidpointRounding.AwayFromZero);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Devuelve el codigo de itinerario mas bajo de la alternativa para desempatar.
|
||
/// </summary>
|
||
private static int CodigoItinerarioMinimo(AlternativaRuta alternativa)
|
||
{
|
||
var tramos = alternativa.AlternativaBus?.Tramos;
|
||
if (tramos is null || tramos.Count == 0)
|
||
return int.MaxValue;
|
||
|
||
return tramos
|
||
.Select(t => t.Variante.CodigoItinerario)
|
||
.DefaultIfEmpty(int.MaxValue)
|
||
.Min();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Selecciona las paradas cercanas mas prometedoras.
|
||
/// </summary>
|
||
private async Task<List<ParadaCandidata>> SeleccionarCandidatasAsync(
|
||
IndiceTransitoDbus indiceTransito,
|
||
double latitud,
|
||
double longitud,
|
||
ParametrosPlanificadorRuta parametros,
|
||
CancellationToken ct)
|
||
{
|
||
var radio = Math.Clamp(parametros.RadioParadasCandidatasMetros, 200, 3500);
|
||
var maximo = Math.Clamp(parametros.MaxParadasCandidatasPorLado, 3, 20);
|
||
var pool = indiceTransito.QueryParadasEnRadio(latitud, longitud, radio);
|
||
var candidatas = new List<ParadaCandidata>(pool.Count);
|
||
|
||
foreach (var parada in pool)
|
||
{
|
||
ct.ThrowIfCancellationRequested();
|
||
|
||
if ((parametros.ActivarRangoLineas || parametros.ActivarRangoItinerarios) &&
|
||
!ParadaTienePatronPermitido(indiceTransito, parada.Codigo, parametros))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var distancia = UtilidadesTransitoDbus.CalcularDistanciaMetros(latitud, longitud, parada.Latitud, parada.Longitud);
|
||
if (distancia > radio * 1.9)
|
||
continue;
|
||
|
||
candidatas.Add(new ParadaCandidata
|
||
{
|
||
Parada = parada,
|
||
DistanciaEuclidea = distancia,
|
||
DistanciaAPie = distancia,
|
||
TiempoAPie = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distancia, parametros)
|
||
});
|
||
}
|
||
|
||
candidatas = candidatas
|
||
.OrderBy(x => x.DistanciaEuclidea)
|
||
.Take(Math.Min(64, Math.Max(maximo + 20, maximo * 4)))
|
||
.ToList();
|
||
|
||
if (!parametros.UsarOsrmParaRanking || parametros.ActivarRangoLineas || parametros.ActivarRangoItinerarios)
|
||
return SeleccionarCandidatasConCoberturaPatrones(candidatas, maximo, indiceTransito, parametros);
|
||
|
||
var semaforo = new SemaphoreSlim(4, 4);
|
||
var tareas = candidatas.Select(async candidata =>
|
||
{
|
||
await semaforo.WaitAsync(ct);
|
||
try
|
||
{
|
||
var caminata = await WalkOsrmCached(latitud, longitud, candidata.Parada.Latitud, candidata.Parada.Longitud);
|
||
if (caminata.distancia > 0)
|
||
{
|
||
candidata.DistanciaAPie = caminata.distancia;
|
||
candidata.TiempoAPie = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(caminata.distancia, parametros);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
finally
|
||
{
|
||
semaforo.Release();
|
||
}
|
||
});
|
||
|
||
await Task.WhenAll(tareas);
|
||
|
||
var candidatasOrdenadas = candidatas
|
||
.OrderBy(x => x.TiempoAPie)
|
||
.ThenBy(x => x.DistanciaEuclidea)
|
||
.ToList();
|
||
|
||
return SeleccionarCandidatasConCoberturaPatrones(candidatasOrdenadas, maximo, indiceTransito, parametros);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Comprueba si una parada tiene al menos un patron permitido.
|
||
/// </summary>
|
||
private static bool ParadaTienePatronPermitido(
|
||
IndiceTransitoDbus indiceTransito,
|
||
string codigoParada,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (!indiceTransito.PatronesPorParada.TryGetValue(codigoParada, out var referencias))
|
||
return false;
|
||
|
||
foreach (var referencia in referencias)
|
||
{
|
||
if (PatronPermitido(referencia.Patron, parametros))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Selecciona candidatas manteniendo las mejores por distancia/tiempo y añade algunas extra
|
||
/// para cubrir lineas/itinerarios que no entren en el recorte principal.
|
||
/// </summary>
|
||
private static List<ParadaCandidata> SeleccionarCandidatasConCoberturaPatrones(
|
||
IReadOnlyList<ParadaCandidata> candidatasOrdenadas,
|
||
int maximo,
|
||
IndiceTransitoDbus indiceTransito,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (candidatasOrdenadas.Count <= maximo)
|
||
return candidatasOrdenadas.ToList();
|
||
|
||
var seleccion = candidatasOrdenadas.Take(maximo).ToList();
|
||
var firmasCubiertas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
|
||
foreach (var candidata in seleccion)
|
||
AgregarFirmasPatronesPermitidos(candidata.Parada.Codigo, firmasCubiertas, indiceTransito, parametros);
|
||
|
||
var extrasMaximos = Math.Min(10, Math.Max(4, maximo / 2));
|
||
foreach (var candidata in candidatasOrdenadas.Skip(maximo))
|
||
{
|
||
if (extrasMaximos <= 0)
|
||
break;
|
||
|
||
if (!ParadaAportaCoberturaNueva(candidata.Parada.Codigo, firmasCubiertas, indiceTransito, parametros))
|
||
continue;
|
||
|
||
seleccion.Add(candidata);
|
||
AgregarFirmasPatronesPermitidos(candidata.Parada.Codigo, firmasCubiertas, indiceTransito, parametros);
|
||
extrasMaximos--;
|
||
}
|
||
|
||
return seleccion;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Indica si una parada aporta al menos una firma de patron que todavia no estaba cubierta.
|
||
/// </summary>
|
||
private static bool ParadaAportaCoberturaNueva(
|
||
string codigoParada,
|
||
ISet<string> firmasCubiertas,
|
||
IndiceTransitoDbus indiceTransito,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (!indiceTransito.PatronesPorParada.TryGetValue(codigoParada, out var referencias))
|
||
return false;
|
||
|
||
foreach (var referencia in referencias)
|
||
{
|
||
var patron = referencia.Patron;
|
||
if (!PatronPermitido(patron, parametros))
|
||
continue;
|
||
|
||
var firma = FirmaCoberturaPatron(patron);
|
||
if (!firmasCubiertas.Contains(firma))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Registra en el set las firmas permitidas que pasan por una parada.
|
||
/// </summary>
|
||
private static void AgregarFirmasPatronesPermitidos(
|
||
string codigoParada,
|
||
ISet<string> firmasCubiertas,
|
||
IndiceTransitoDbus indiceTransito,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (!indiceTransito.PatronesPorParada.TryGetValue(codigoParada, out var referencias))
|
||
return;
|
||
|
||
foreach (var referencia in referencias)
|
||
{
|
||
var patron = referencia.Patron;
|
||
if (!PatronPermitido(patron, parametros))
|
||
continue;
|
||
|
||
firmasCubiertas.Add(FirmaCoberturaPatron(patron));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Firma de cobertura para ampliar variedad de alternativas sin duplicar exceso.
|
||
/// </summary>
|
||
private static string FirmaCoberturaPatron(PatronTransitoDbus patron)
|
||
=> $"{patron.CodigoLinea}|{patron.CodigoItinerario}|{patron.Sentido}";
|
||
|
||
/// <summary>
|
||
/// Crea las dos alternativas de solo a pie.
|
||
/// </summary>
|
||
private async Task<List<AlternativaRuta>> CrearAlternativasSoloPieAsync(
|
||
double[] origenLatLon,
|
||
double[] destinoLatLon,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
var alternativas = new List<AlternativaRuta>(2);
|
||
|
||
var osrm = await WalkOsrmCached(origenLatLon[0], origenLatLon[1], destinoLatLon[0], destinoLatLon[1]);
|
||
if (osrm.linea is not null && osrm.distancia > 0)
|
||
{
|
||
var duracion = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(osrm.distancia, parametros);
|
||
var puntuacion = UtilidadesTransitoDbus.CalcularCosteConPenalizacionPieSegundos(duracion, parametros, osrm.distancia);
|
||
alternativas.Add(new AlternativaRuta
|
||
{
|
||
EsSoloAPie = true,
|
||
Etiqueta = "Solo a pie (v1)",
|
||
ProveedorRuta = "v1",
|
||
LineaSoloAPie = osrm.linea,
|
||
LineasPieCalculoElevacion = new List<double[,]> { osrm.linea },
|
||
DistanciasPiePorTramoAprox = new List<double> { osrm.distancia },
|
||
DistanciaSoloAPie = osrm.distancia,
|
||
DuracionSoloAPie = duracion,
|
||
DuracionTotalAproxSegundos = duracion,
|
||
PuntuacionCoste = puntuacion
|
||
});
|
||
}
|
||
|
||
var valhalla = await WalkValhallaCached(origenLatLon[0], origenLatLon[1], destinoLatLon[0], destinoLatLon[1]);
|
||
if (valhalla.linea is not null && valhalla.distancia > 0)
|
||
{
|
||
var duracion = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(valhalla.distancia, parametros);
|
||
var puntuacion = UtilidadesTransitoDbus.CalcularCosteConPenalizacionPieSegundos(duracion, parametros, valhalla.distancia);
|
||
alternativas.Add(new AlternativaRuta
|
||
{
|
||
EsSoloAPie = true,
|
||
Etiqueta = "Solo a pie (v2)",
|
||
ProveedorRuta = "v2",
|
||
LineaSoloAPie = valhalla.linea,
|
||
LineasPieCalculoElevacion = new List<double[,]> { valhalla.linea },
|
||
DistanciasPiePorTramoAprox = new List<double> { valhalla.distancia },
|
||
DistanciaSoloAPie = valhalla.distancia,
|
||
DuracionSoloAPie = duracion,
|
||
DuracionTotalAproxSegundos = duracion,
|
||
PuntuacionCoste = puntuacion
|
||
});
|
||
}
|
||
|
||
return alternativas;
|
||
}
|
||
|
||
private static void AgregarLineaPieCalculoElevacion(
|
||
AlternativaRuta alternativa,
|
||
double[,]? linea,
|
||
double latitudOrigen,
|
||
double longitudOrigen,
|
||
double latitudDestino,
|
||
double longitudDestino)
|
||
{
|
||
if (linea is not null && linea.GetLength(0) >= 2)
|
||
{
|
||
alternativa.LineasPieCalculoElevacion.Add(linea);
|
||
return;
|
||
}
|
||
|
||
var distanciaDirecta = UtilidadesTransitoDbus.CalcularDistanciaMetros(
|
||
latitudOrigen,
|
||
longitudOrigen,
|
||
latitudDestino,
|
||
longitudDestino);
|
||
if (!double.IsFinite(distanciaDirecta) || distanciaDirecta <= 0.5)
|
||
return;
|
||
|
||
alternativa.LineasPieCalculoElevacion.Add(new double[,]
|
||
{
|
||
{ latitudOrigen, longitudOrigen },
|
||
{ latitudDestino, longitudDestino }
|
||
});
|
||
}
|
||
|
||
private static void AgregarDistanciaPiePorTramo(AlternativaRuta alternativa, double distanciaMetros)
|
||
{
|
||
if (double.IsFinite(distanciaMetros) && distanciaMetros > 0)
|
||
alternativa.DistanciasPiePorTramoAprox.Add(distanciaMetros);
|
||
}
|
||
|
||
private static void AcumularDistanciaPieEnPrimerTramo(AlternativaRuta alternativa, double distanciaMetros)
|
||
{
|
||
if (!double.IsFinite(distanciaMetros) || distanciaMetros <= 0)
|
||
return;
|
||
|
||
if (alternativa.DistanciasPiePorTramoAprox.Count == 0)
|
||
alternativa.DistanciasPiePorTramoAprox.Add(distanciaMetros);
|
||
else
|
||
alternativa.DistanciasPiePorTramoAprox[0] += distanciaMetros;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Recalcula una alternativa de bus usando caminatas exactas y horarios reales.
|
||
/// </summary>
|
||
private async Task<double> RecalcularDuracionExactaAsync(
|
||
double[] origenLatLon,
|
||
double[] destinoLatLon,
|
||
AlternativaRuta alternativa,
|
||
ParametrosPlanificadorRuta parametros,
|
||
CancellationToken ct)
|
||
{
|
||
if (alternativa.AlternativaBus is null || alternativa.AlternativaBus.Tramos.Count == 0)
|
||
return double.PositiveInfinity;
|
||
|
||
alternativa.LineasPieCalculoElevacion.Clear();
|
||
alternativa.DistanciasPiePorTramoAprox.Clear();
|
||
alternativa.EsfuerzoExtraPendienteMetros = 0;
|
||
|
||
var tramosBus = alternativa.AlternativaBus.Tramos;
|
||
var instanteSalida = UtilidadesTransitoDbus.ResolverInstanteSalida(parametros);
|
||
|
||
await RecortarInicioCircularRedundanteAsync(origenLatLon, alternativa, parametros, instanteSalida, ct);
|
||
await RefinarParadaInicioAsync(origenLatLon, tramosBus, parametros, instanteSalida, ct);
|
||
await RecortarInicioCircularRedundanteAsync(origenLatLon, alternativa, parametros, instanteSalida, ct);
|
||
|
||
var lineasBus = ConstruirLineasBusPlanificacionCached(tramosBus, out var distanciaBus);
|
||
alternativa.AlternativaBus.DistanciaTotalBus = distanciaBus;
|
||
|
||
var paradaInicioBus = alternativa.OrigenParada;
|
||
var paradaFinBus = alternativa.DestinoParada;
|
||
|
||
if (lineasBus.Count > 0)
|
||
{
|
||
var primerTramo = tramosBus[0];
|
||
var ultimoTramo = tramosBus[^1];
|
||
|
||
paradaInicioBus = ResolverParadaFisica(
|
||
primerTramo.CodigoParadaInicio,
|
||
ObtenerPrimerPunto(lineasBus[0]),
|
||
alternativa.OrigenParada);
|
||
paradaFinBus = ResolverParadaFisica(
|
||
ultimoTramo.CodigoParadaFin,
|
||
ObtenerUltimoPunto(lineasBus[^1]),
|
||
alternativa.DestinoParada);
|
||
}
|
||
|
||
alternativa.OrigenParada = paradaInicioBus;
|
||
alternativa.DestinoParada = paradaFinBus;
|
||
alternativa.TiempoEsperaInicialSegundos = 0;
|
||
alternativa.TiempoEsperaTransbordosSegundos = 0;
|
||
alternativa.HorarioCompletoDisponible = parametros.UsarHorariosReales;
|
||
|
||
var total = 0.0;
|
||
var cursor = instanteSalida;
|
||
|
||
var acceso = await WalkOsrmCached(origenLatLon[0], origenLatLon[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
|
||
AgregarLineaPieCalculoElevacion(
|
||
alternativa,
|
||
acceso.linea,
|
||
origenLatLon[0],
|
||
origenLatLon[1],
|
||
paradaInicioBus.Latitud,
|
||
paradaInicioBus.Longitud);
|
||
alternativa.DistanciaPieInicioAprox = acceso.distancia > 0
|
||
? acceso.distancia
|
||
: UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
|
||
AgregarDistanciaPiePorTramo(alternativa, alternativa.DistanciaPieInicioAprox);
|
||
alternativa.TiempoPieInicio = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(alternativa.DistanciaPieInicioAprox, parametros);
|
||
total += alternativa.TiempoPieInicio;
|
||
cursor = UtilidadesTransitoDbus.AddSecondsSafe(cursor, alternativa.TiempoPieInicio);
|
||
|
||
alternativa.DistanciaPieTransbordosAprox = 0;
|
||
|
||
var paradaUltimaBajada = paradaInicioBus;
|
||
var puntoUltimaLlegada = new[] { paradaInicioBus.Latitud, paradaInicioBus.Longitud };
|
||
for (var indice = 0; indice < tramosBus.Count; indice++)
|
||
{
|
||
ct.ThrowIfCancellationRequested();
|
||
|
||
if (indice == tramosBus.Count - 1)
|
||
{
|
||
await RefinarParadaFinAsync(destinoLatLon, tramosBus, parametros, cursor, ct);
|
||
lineasBus = ConstruirLineasBusPlanificacionCached(tramosBus, out distanciaBus);
|
||
alternativa.AlternativaBus.DistanciaTotalBus = distanciaBus;
|
||
}
|
||
|
||
var tramo = tramosBus[indice];
|
||
var esTransbordoReal = indice > 0 && UtilidadesTransitoDbus.EsTransbordoReal(tramosBus[indice - 1], tramo);
|
||
var lineaTramo = lineasBus[indice];
|
||
var puntoInicioTramo = ObtenerPrimerPunto(lineaTramo);
|
||
var puntoFinTramo = ObtenerUltimoPunto(lineaTramo);
|
||
var paradaSubida = ResolverParadaFisica(
|
||
tramo.CodigoParadaInicio,
|
||
puntoInicioTramo,
|
||
indice == 0 ? paradaInicioBus : paradaUltimaBajada);
|
||
var paradaBajada = ResolverParadaFisica(
|
||
tramo.CodigoParadaFin,
|
||
puntoFinTramo,
|
||
indice == tramosBus.Count - 1 ? paradaFinBus : paradaUltimaBajada);
|
||
|
||
var distanciaUnion = CalcularDistanciaEntrePuntos(puntoUltimaLlegada, puntoInicioTramo);
|
||
if (indice > 0 &&
|
||
esTransbordoReal &&
|
||
distanciaUnion > UmbralUnionPeatonalAutomaticaMetros)
|
||
{
|
||
return double.PositiveInfinity;
|
||
}
|
||
|
||
if ((indice == 0 || esTransbordoReal) &&
|
||
distanciaUnion > UmbralCosturaVisualMetros &&
|
||
distanciaUnion <= UmbralUnionPeatonalAutomaticaMetros)
|
||
{
|
||
var transferencia = await WalkOsrmCached(
|
||
puntoUltimaLlegada[0],
|
||
puntoUltimaLlegada[1],
|
||
puntoInicioTramo![0],
|
||
puntoInicioTramo[1]);
|
||
|
||
AgregarLineaPieCalculoElevacion(
|
||
alternativa,
|
||
transferencia.linea,
|
||
puntoUltimaLlegada[0],
|
||
puntoUltimaLlegada[1],
|
||
puntoInicioTramo[0],
|
||
puntoInicioTramo[1]);
|
||
|
||
var distanciaTransferencia = transferencia.distancia > 0
|
||
? transferencia.distancia
|
||
: distanciaUnion;
|
||
|
||
if (indice > 0 &&
|
||
esTransbordoReal &&
|
||
distanciaTransferencia > UmbralUnionPeatonalAutomaticaMetros)
|
||
{
|
||
return double.PositiveInfinity;
|
||
}
|
||
|
||
if (indice == 0)
|
||
{
|
||
alternativa.DistanciaPieInicioAprox += distanciaTransferencia;
|
||
alternativa.TiempoPieInicio += UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaTransferencia, parametros);
|
||
AcumularDistanciaPieEnPrimerTramo(alternativa, distanciaTransferencia);
|
||
}
|
||
else
|
||
{
|
||
alternativa.DistanciaPieTransbordosAprox += distanciaTransferencia;
|
||
AgregarDistanciaPiePorTramo(alternativa, distanciaTransferencia);
|
||
}
|
||
|
||
var duracionTransferencia = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaTransferencia, parametros);
|
||
total += duracionTransferencia;
|
||
cursor = UtilidadesTransitoDbus.AddSecondsSafe(cursor, duracionTransferencia);
|
||
}
|
||
|
||
var resueltoConHorario = false;
|
||
if (parametros.UsarHorariosReales)
|
||
{
|
||
var primeraSalidaAceptable = esTransbordoReal
|
||
? cursor.AddSeconds(Math.Clamp(parametros.MargenTransbordoSegundos, 0, 1800))
|
||
: cursor;
|
||
|
||
if (ResolverSegmentoHorario(
|
||
tramo.CodigoParadaInicio,
|
||
tramo.CodigoParadaFin,
|
||
UtilidadesTransitoDbus.LineaCodigoStr(tramo.Variante),
|
||
tramo.Variante.CodigoItinerario,
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo),
|
||
primeraSalidaAceptable,
|
||
out var salidaReal,
|
||
out var llegadaReal))
|
||
{
|
||
var espera = Math.Max(0, (salidaReal - cursor).TotalSeconds);
|
||
if (espera > 0)
|
||
{
|
||
if (indice == 0 || !esTransbordoReal)
|
||
{
|
||
alternativa.TiempoEsperaInicialSegundos += espera;
|
||
if (parametros.MaxEsperaInicialMinutos > 0 &&
|
||
indice == 0 &&
|
||
espera > parametros.MaxEsperaInicialMinutos * 60.0)
|
||
{
|
||
return double.PositiveInfinity;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
alternativa.TiempoEsperaTransbordosSegundos += espera;
|
||
if (parametros.MaxEsperaTransbordoMinutos > 0 &&
|
||
espera > parametros.MaxEsperaTransbordoMinutos * 60.0)
|
||
{
|
||
return double.PositiveInfinity;
|
||
}
|
||
}
|
||
|
||
if (parametros.MaxEsperaTotalMinutos > 0 &&
|
||
alternativa.TiempoEsperaTotalSegundos > parametros.MaxEsperaTotalMinutos * 60.0)
|
||
{
|
||
return double.PositiveInfinity;
|
||
}
|
||
|
||
total += espera;
|
||
cursor = salidaReal;
|
||
}
|
||
|
||
var duracionBusReal = Math.Max(0, (llegadaReal - salidaReal).TotalSeconds);
|
||
total += duracionBusReal;
|
||
cursor = llegadaReal;
|
||
resueltoConHorario = true;
|
||
}
|
||
else
|
||
{
|
||
alternativa.HorarioCompletoDisponible = false;
|
||
if (parametros.DescartarAlternativasSinHorario)
|
||
return double.PositiveInfinity;
|
||
}
|
||
}
|
||
|
||
if (!resueltoConHorario)
|
||
{
|
||
var linea = lineasBus[indice];
|
||
var distanciaTramo = 0.0;
|
||
for (var p = 1; p < linea.GetLength(0); p++)
|
||
{
|
||
distanciaTramo += UtilidadesTransitoDbus.CalcularDistanciaMetros(
|
||
linea[p - 1, 0],
|
||
linea[p - 1, 1],
|
||
linea[p, 0],
|
||
linea[p, 1]);
|
||
}
|
||
|
||
var paradasTramo = Math.Max(0, tramo.IndiceFin - tramo.IndiceInicio);
|
||
var duracionBusAprox = UtilidadesTransitoDbus.EstimarDuracionBusSegundos(distanciaTramo, parametros) +
|
||
(paradasTramo * Math.Max(0, parametros.SegundosPorParadaBus));
|
||
|
||
total += duracionBusAprox;
|
||
cursor = UtilidadesTransitoDbus.AddSecondsSafe(cursor, duracionBusAprox);
|
||
}
|
||
|
||
paradaUltimaBajada = paradaBajada;
|
||
puntoUltimaLlegada = puntoFinTramo ?? new[] { paradaBajada.Latitud, paradaBajada.Longitud };
|
||
}
|
||
|
||
paradaFinBus = ResolverParadaFisica(
|
||
tramosBus[^1].CodigoParadaFin,
|
||
ObtenerUltimoPunto(lineasBus[^1]),
|
||
paradaFinBus);
|
||
alternativa.DestinoParada = paradaFinBus;
|
||
|
||
var salidaFinal = await WalkOsrmCached(puntoUltimaLlegada[0], puntoUltimaLlegada[1], destinoLatLon[0], destinoLatLon[1]);
|
||
AgregarLineaPieCalculoElevacion(
|
||
alternativa,
|
||
salidaFinal.linea,
|
||
puntoUltimaLlegada[0],
|
||
puntoUltimaLlegada[1],
|
||
destinoLatLon[0],
|
||
destinoLatLon[1]);
|
||
alternativa.DistanciaPieFinAprox = salidaFinal.distancia > 0
|
||
? salidaFinal.distancia
|
||
: UtilidadesTransitoDbus.CalcularDistanciaMetros(puntoUltimaLlegada[0], puntoUltimaLlegada[1], destinoLatLon[0], destinoLatLon[1]);
|
||
AgregarDistanciaPiePorTramo(alternativa, alternativa.DistanciaPieFinAprox);
|
||
alternativa.TiempoPieFin = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(alternativa.DistanciaPieFinAprox, parametros);
|
||
total += alternativa.TiempoPieFin;
|
||
|
||
if (!parametros.UsarHorariosReales || !alternativa.HorarioCompletoDisponible)
|
||
total += alternativa.NumeroTransbordosReales * (parametros.MinutosPorTransbordo * 60.0);
|
||
|
||
return total;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Conserva una variante que entra directamente en el segundo sentido de una circular cuando
|
||
/// una parada inmediata permite coger el mismo servicio con mejor score equilibrado.
|
||
/// </summary>
|
||
private List<ResultadoBusquedaRutaBusDbus> BuscarIniciosDirectosTrasCircularConHorario(
|
||
IReadOnlyList<ResultadoBusquedaRutaBusDbus> candidatasBase,
|
||
IReadOnlyList<ParadaCandidata> candidatasOrigen,
|
||
DateTime instanteSalida,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (candidatasBase.Count == 0 || candidatasOrigen.Count == 0)
|
||
return new();
|
||
|
||
var origenesPorCodigo = candidatasOrigen
|
||
.GroupBy(x => x.Parada.Codigo, StringComparer.OrdinalIgnoreCase)
|
||
.ToDictionary(
|
||
grupo => grupo.Key,
|
||
grupo => grupo.OrderBy(x => x.TiempoAPie).ThenBy(x => x.DistanciaAPie).First(),
|
||
StringComparer.OrdinalIgnoreCase);
|
||
var mejores = new Dictionary<string, ResultadoBusquedaRutaBusDbus>(StringComparer.OrdinalIgnoreCase);
|
||
|
||
foreach (var candidataBase in candidatasBase)
|
||
{
|
||
var tramosBase = candidataBase.RutaBus.Tramos;
|
||
if (tramosBase.Count < 2)
|
||
continue;
|
||
|
||
var primero = tramosBase[0];
|
||
var segundo = tramosBase[1];
|
||
if (!primero.EsValido ||
|
||
!segundo.EsValido ||
|
||
!EsContinuidadCircular(primero.Variante, segundo.Variante) ||
|
||
!string.Equals(primero.CodigoParadaFin, segundo.CodigoParadaInicio, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var llegadaPrimeraParada = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, candidataBase.TiempoPieInicio);
|
||
if (!ResolverSegmentoHorario(
|
||
primero.CodigoParadaInicio,
|
||
primero.CodigoParadaFin,
|
||
UtilidadesTransitoDbus.LineaCodigoStr(primero.Variante),
|
||
primero.Variante.CodigoItinerario,
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(primero.Variante.Sentido?.Codigo),
|
||
llegadaPrimeraParada,
|
||
out _,
|
||
out var llegadaIntermedia) ||
|
||
!ResolverSegmentoHorario(
|
||
segundo.CodigoParadaInicio,
|
||
segundo.CodigoParadaFin,
|
||
UtilidadesTransitoDbus.LineaCodigoStr(segundo.Variante),
|
||
segundo.Variante.CodigoItinerario,
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(segundo.Variante.Sentido?.Codigo),
|
||
llegadaIntermedia,
|
||
out _,
|
||
out var llegadaOriginalSegundo))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
ResultadoBusquedaRutaBusDbus? mejorRecorte = null;
|
||
var indiceMaximo = Math.Min(
|
||
segundo.IndiceFin - 1,
|
||
segundo.IndiceInicio + VentanaRefinamientoParadaInicio);
|
||
|
||
for (var indiceSubida = segundo.IndiceInicio + 1; indiceSubida <= indiceMaximo; indiceSubida++)
|
||
{
|
||
var codigoSubida = segundo.Variante.CodigosParadas[indiceSubida];
|
||
if (!origenesPorCodigo.TryGetValue(codigoSubida, out var origenDirecto) ||
|
||
origenDirecto.DistanciaAPie > candidataBase.DistanciaPieInicio + MaxExcesoAccesoDirectoCircularMetros)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var llegadaDirectaParada = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, origenDirecto.TiempoAPie);
|
||
if (!ResolverSegmentoHorario(
|
||
codigoSubida,
|
||
segundo.CodigoParadaFin,
|
||
UtilidadesTransitoDbus.LineaCodigoStr(segundo.Variante),
|
||
segundo.Variante.CodigoItinerario,
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(segundo.Variante.Sentido?.Codigo),
|
||
llegadaDirectaParada,
|
||
out var salidaDirecta,
|
||
out var llegadaDirectaSegundo) ||
|
||
llegadaDirectaSegundo > llegadaOriginalSegundo.AddSeconds(ToleranciaMismoBusCircularSegundos))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var rutaDirecta = ClonarRutaBus(candidataBase.RutaBus);
|
||
rutaDirecta.Tramos.RemoveAt(0);
|
||
rutaDirecta.Tramos[0].IndiceInicio = indiceSubida;
|
||
rutaDirecta.NumeroTransbordos = UtilidadesTransitoDbus.ContarTransbordosReales(rutaDirecta.Tramos);
|
||
rutaDirecta.CodigosParadasTransbordo = ConstruirCodigosTransbordo(rutaDirecta.Tramos);
|
||
rutaDirecta.DistanciaTotalBus = 0;
|
||
|
||
var diferenciaLlegada = (llegadaDirectaSegundo - llegadaOriginalSegundo).TotalSeconds;
|
||
var duracionEstimada = Math.Max(0, candidataBase.DuracionEstimadaSegundos + diferenciaLlegada);
|
||
var esperaInicial = Math.Max(0, (salidaDirecta - llegadaDirectaParada).TotalSeconds);
|
||
var recorte = new ResultadoBusquedaRutaBusDbus
|
||
{
|
||
ParadaOrigen = origenDirecto.Parada,
|
||
ParadaDestino = candidataBase.ParadaDestino,
|
||
DistanciaPieInicio = origenDirecto.DistanciaAPie,
|
||
TiempoPieInicio = origenDirecto.TiempoAPie,
|
||
DistanciaPieFin = candidataBase.DistanciaPieFin,
|
||
TiempoPieFin = candidataBase.TiempoPieFin,
|
||
DistanciaPieTransbordos = candidataBase.DistanciaPieTransbordos,
|
||
DistanciasPieTransbordosPorTramo = candidataBase.DistanciasPieTransbordosPorTramo.ToArray(),
|
||
TiempoEsperaInicialSegundos = esperaInicial,
|
||
TiempoEsperaTransbordosSegundos = candidataBase.TiempoEsperaTransbordosSegundos,
|
||
DuracionEstimadaSegundos = duracionEstimada,
|
||
PuntuacionCoste = UtilidadesTransitoDbus.CalcularCosteConPenalizacionesRankingSegundos(
|
||
duracionEstimada,
|
||
parametros,
|
||
rutaDirecta.NumeroTransbordos,
|
||
ConstruirDistanciasPiePorTramo(
|
||
origenDirecto.DistanciaAPie,
|
||
ObtenerDistanciasPieTransbordosPorTramo(candidataBase),
|
||
candidataBase.DistanciaPieFin).ToArray()),
|
||
RutaBus = rutaDirecta
|
||
};
|
||
|
||
if (CalcularScoreBusquedaEquilibrado(recorte, parametros) + 1e-6 >=
|
||
CalcularScoreBusquedaEquilibrado(candidataBase, parametros))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (mejorRecorte is null ||
|
||
CalcularScoreBusquedaEquilibrado(recorte, parametros) + 1e-6 <
|
||
CalcularScoreBusquedaEquilibrado(mejorRecorte, parametros))
|
||
{
|
||
mejorRecorte = recorte;
|
||
}
|
||
}
|
||
|
||
if (mejorRecorte is null)
|
||
continue;
|
||
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(mejorRecorte.RutaBus);
|
||
if (!mejores.TryGetValue(firma, out var actual) ||
|
||
CalcularScoreBusquedaEquilibrado(mejorRecorte, parametros) + 1e-6 <
|
||
CalcularScoreBusquedaEquilibrado(actual, parametros))
|
||
{
|
||
mejores[firma] = mejorRecorte;
|
||
}
|
||
}
|
||
|
||
return mejores.Values.ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Evita empezar una circular yendo hacia atras si se puede coger directamente el tramo siguiente.
|
||
/// </summary>
|
||
private async Task RecortarInicioCircularRedundanteAsync(
|
||
double[] origenLatLon,
|
||
AlternativaRuta alternativa,
|
||
ParametrosPlanificadorRuta parametros,
|
||
DateTime instanteSalida,
|
||
CancellationToken ct)
|
||
{
|
||
ct.ThrowIfCancellationRequested();
|
||
|
||
if (!parametros.UsarHorariosReales || alternativa.AlternativaBus is null)
|
||
return;
|
||
|
||
var tramosBus = alternativa.AlternativaBus.Tramos;
|
||
if (tramosBus.Count < 2)
|
||
return;
|
||
|
||
var primero = tramosBus[0];
|
||
var segundo = tramosBus[1];
|
||
if (!primero.EsValido ||
|
||
!segundo.EsValido ||
|
||
!EsContinuidadCircular(primero.Variante, segundo.Variante) ||
|
||
!string.Equals(primero.CodigoParadaFin, segundo.CodigoParadaInicio, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var paradaPrimera = ResolverParadaFisica(primero.CodigoParadaInicio);
|
||
var paradaDirecta = ResolverParadaFisica(segundo.CodigoParadaInicio);
|
||
if (string.IsNullOrWhiteSpace(paradaPrimera.Codigo) || string.IsNullOrWhiteSpace(paradaDirecta.Codigo))
|
||
return;
|
||
|
||
var accesoPrimero = await WalkOsrmCached(origenLatLon[0], origenLatLon[1], paradaPrimera.Latitud, paradaPrimera.Longitud);
|
||
var distanciaPrimero = accesoPrimero.distancia > 0
|
||
? accesoPrimero.distancia
|
||
: UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaPrimera.Latitud, paradaPrimera.Longitud);
|
||
|
||
var accesoDirecto = await WalkOsrmCached(origenLatLon[0], origenLatLon[1], paradaDirecta.Latitud, paradaDirecta.Longitud);
|
||
var distanciaDirecta = accesoDirecto.distancia > 0
|
||
? accesoDirecto.distancia
|
||
: UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaDirecta.Latitud, paradaDirecta.Longitud);
|
||
|
||
if (distanciaDirecta > distanciaPrimero + MaxExcesoAccesoDirectoCircularMetros)
|
||
return;
|
||
|
||
var llegadaPrimera = UtilidadesTransitoDbus.AddSecondsSafe(
|
||
instanteSalida,
|
||
UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaPrimero, parametros));
|
||
if (!ResolverSegmentoHorario(
|
||
primero.CodigoParadaInicio,
|
||
primero.CodigoParadaFin,
|
||
UtilidadesTransitoDbus.LineaCodigoStr(primero.Variante),
|
||
primero.Variante.CodigoItinerario,
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(primero.Variante.Sentido?.Codigo),
|
||
llegadaPrimera,
|
||
out _,
|
||
out var llegadaIntermedia))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!ResolverSegmentoHorario(
|
||
segundo.CodigoParadaInicio,
|
||
segundo.CodigoParadaFin,
|
||
UtilidadesTransitoDbus.LineaCodigoStr(segundo.Variante),
|
||
segundo.Variante.CodigoItinerario,
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(segundo.Variante.Sentido?.Codigo),
|
||
llegadaIntermedia,
|
||
out var salidaOriginalSegundo,
|
||
out var llegadaOriginalSegundo))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var llegadaDirecta = UtilidadesTransitoDbus.AddSecondsSafe(
|
||
instanteSalida,
|
||
UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaDirecta, parametros));
|
||
if (!ResolverSegmentoHorario(
|
||
segundo.CodigoParadaInicio,
|
||
segundo.CodigoParadaFin,
|
||
UtilidadesTransitoDbus.LineaCodigoStr(segundo.Variante),
|
||
segundo.Variante.CodigoItinerario,
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(segundo.Variante.Sentido?.Codigo),
|
||
llegadaDirecta,
|
||
out var salidaDirectaSegundo,
|
||
out var llegadaDirectaSegundo))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (salidaDirectaSegundo > salidaOriginalSegundo.AddSeconds(ToleranciaMismoBusCircularSegundos) ||
|
||
llegadaDirectaSegundo > llegadaOriginalSegundo.AddSeconds(ToleranciaMismoBusCircularSegundos))
|
||
{
|
||
return;
|
||
}
|
||
|
||
tramosBus.RemoveAt(0);
|
||
alternativa.OrigenParada = paradaDirecta;
|
||
alternativa.AlternativaBus.NumeroTransbordos = UtilidadesTransitoDbus.ContarTransbordosReales(tramosBus);
|
||
alternativa.AlternativaBus.CodigosParadasTransbordo = ConstruirCodigosTransbordo(tramosBus);
|
||
alternativa.NumeroTransbordosReales = alternativa.AlternativaBus.NumeroTransbordos;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Refina la parada de subida del primer tramo probando paradas vecinas de la misma variante.
|
||
/// </summary>
|
||
private async Task RefinarParadaInicioAsync(
|
||
double[] origenLatLon,
|
||
IReadOnlyList<TramoBus> tramosBus,
|
||
ParametrosPlanificadorRuta parametros,
|
||
DateTime instanteSalida,
|
||
CancellationToken ct)
|
||
{
|
||
if (tramosBus.Count == 0)
|
||
return;
|
||
|
||
var primerTramo = tramosBus[0];
|
||
if (!primerTramo.EsValido)
|
||
return;
|
||
|
||
var indiceOriginal = primerTramo.IndiceInicio;
|
||
var indiceMaximo = Math.Min(primerTramo.IndiceFin - 1, indiceOriginal + VentanaRefinamientoParadaInicio);
|
||
if (indiceMaximo <= indiceOriginal)
|
||
return;
|
||
|
||
var mejorIndice = indiceOriginal;
|
||
var mejorScore = double.PositiveInfinity;
|
||
var mejorDistancia = double.PositiveInfinity;
|
||
|
||
using var semaforo = new SemaphoreSlim(MaxConcurrenciaRefinamientoParadas, MaxConcurrenciaRefinamientoParadas);
|
||
var tareas = Enumerable.Range(indiceOriginal, indiceMaximo - indiceOriginal + 1)
|
||
.Select(async indice =>
|
||
{
|
||
await semaforo.WaitAsync(ct);
|
||
try
|
||
{
|
||
ct.ThrowIfCancellationRequested();
|
||
var evaluacion = await EvaluarSubidaPrimerTramoAsync(
|
||
origenLatLon,
|
||
primerTramo,
|
||
indice,
|
||
parametros,
|
||
instanteSalida,
|
||
ct);
|
||
|
||
return (indice, evaluacion);
|
||
}
|
||
finally
|
||
{
|
||
semaforo.Release();
|
||
}
|
||
})
|
||
.ToArray();
|
||
|
||
var resultados = await Task.WhenAll(tareas);
|
||
var evaluacionOriginal = resultados
|
||
.FirstOrDefault(x => x.indice == indiceOriginal)
|
||
.evaluacion;
|
||
foreach (var (indice, evaluacion) in resultados)
|
||
{
|
||
if (evaluacion is null)
|
||
continue;
|
||
|
||
if (indice != indiceOriginal &&
|
||
evaluacionOriginal is not null &&
|
||
!MovimientoInicioRefinadoPermitido(evaluacionOriginal, evaluacion))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (evaluacion.Score + 1e-6 < mejorScore ||
|
||
(Math.Abs(evaluacion.Score - mejorScore) <= 1e-6 && evaluacion.DistanciaWalk + 1e-6 < mejorDistancia))
|
||
{
|
||
mejorIndice = indice;
|
||
mejorScore = evaluacion.Score;
|
||
mejorDistancia = evaluacion.DistanciaWalk;
|
||
}
|
||
}
|
||
|
||
primerTramo.IndiceInicio = mejorIndice;
|
||
}
|
||
|
||
private static bool MovimientoInicioRefinadoPermitido(
|
||
EvaluacionRefinamientoParada original,
|
||
EvaluacionRefinamientoParada candidata)
|
||
{
|
||
if (original.DistanciaWalk > UmbralAccesoCercanoRefinamientoInicioMetros)
|
||
return true;
|
||
|
||
return candidata.DistanciaWalk <= original.DistanciaWalk + MaxIncrementoAccesoCercanoRefinamientoInicioMetros;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Refina la parada de bajada del ultimo tramo probando paradas vecinas de la misma variante.
|
||
/// </summary>
|
||
private async Task RefinarParadaFinAsync(
|
||
double[] destinoLatLon,
|
||
IReadOnlyList<TramoBus> tramosBus,
|
||
ParametrosPlanificadorRuta parametros,
|
||
DateTime instanteDisponible,
|
||
CancellationToken ct)
|
||
{
|
||
if (tramosBus.Count == 0)
|
||
return;
|
||
|
||
var ultimoTramo = tramosBus[^1];
|
||
if (!ultimoTramo.EsValido)
|
||
return;
|
||
|
||
var indiceOriginal = ultimoTramo.IndiceFin;
|
||
var indiceMinimo = Math.Max(ultimoTramo.IndiceInicio + 1, indiceOriginal - VentanaRefinamientoParadaFin);
|
||
var indiceMaximo = Math.Min(ultimoTramo.Variante.CodigosParadas.Count - 1, indiceOriginal + VentanaRefinamientoParadaFin);
|
||
if (indiceMaximo < indiceMinimo)
|
||
return;
|
||
|
||
var mejorIndice = indiceOriginal;
|
||
var mejorScore = double.PositiveInfinity;
|
||
var mejorDistancia = double.PositiveInfinity;
|
||
|
||
using var semaforo = new SemaphoreSlim(MaxConcurrenciaRefinamientoParadas, MaxConcurrenciaRefinamientoParadas);
|
||
var tareas = Enumerable.Range(indiceMinimo, indiceMaximo - indiceMinimo + 1)
|
||
.Select(async indice =>
|
||
{
|
||
await semaforo.WaitAsync(ct);
|
||
try
|
||
{
|
||
ct.ThrowIfCancellationRequested();
|
||
var evaluacion = await EvaluarBajadaUltimoTramoAsync(
|
||
destinoLatLon,
|
||
ultimoTramo,
|
||
indice,
|
||
parametros,
|
||
instanteDisponible,
|
||
ct);
|
||
|
||
return (indice, evaluacion);
|
||
}
|
||
finally
|
||
{
|
||
semaforo.Release();
|
||
}
|
||
})
|
||
.ToArray();
|
||
|
||
var resultados = await Task.WhenAll(tareas);
|
||
foreach (var (indice, evaluacion) in resultados)
|
||
{
|
||
if (evaluacion is null)
|
||
continue;
|
||
|
||
if (evaluacion.Score + 1e-6 < mejorScore ||
|
||
(Math.Abs(evaluacion.Score - mejorScore) <= 1e-6 && evaluacion.DistanciaWalk + 1e-6 < mejorDistancia))
|
||
{
|
||
mejorIndice = indice;
|
||
mejorScore = evaluacion.Score;
|
||
mejorDistancia = evaluacion.DistanciaWalk;
|
||
}
|
||
}
|
||
|
||
ultimoTramo.IndiceFin = mejorIndice;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Evalua una parada candidata de subida para el primer tramo.
|
||
/// </summary>
|
||
private async Task<EvaluacionRefinamientoParada?> EvaluarSubidaPrimerTramoAsync(
|
||
double[] origenLatLon,
|
||
TramoBus tramo,
|
||
int indiceSubida,
|
||
ParametrosPlanificadorRuta parametros,
|
||
DateTime instanteSalida,
|
||
CancellationToken ct)
|
||
{
|
||
ct.ThrowIfCancellationRequested();
|
||
if (indiceSubida < 0 || indiceSubida >= tramo.IndiceFin || indiceSubida >= tramo.Variante.CodigosParadas.Count)
|
||
return null;
|
||
|
||
var codigoParada = tramo.Variante.CodigosParadas[indiceSubida];
|
||
var parada = ResolverParadaFisica(codigoParada);
|
||
if (parada is null)
|
||
return null;
|
||
|
||
var acceso = await WalkOsrmCached(origenLatLon[0], origenLatLon[1], parada.Latitud, parada.Longitud);
|
||
var distanciaWalk = acceso.distancia > 0
|
||
? acceso.distancia
|
||
: UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], parada.Latitud, parada.Longitud);
|
||
var duracionWalk = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaWalk, parametros);
|
||
|
||
double costeTotal;
|
||
if (parametros.UsarHorariosReales)
|
||
{
|
||
var llegadaParada = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, duracionWalk);
|
||
if (!ResolverSegmentoHorario(
|
||
codigoParada,
|
||
tramo.CodigoParadaFin,
|
||
UtilidadesTransitoDbus.LineaCodigoStr(tramo.Variante),
|
||
tramo.Variante.CodigoItinerario,
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo),
|
||
llegadaParada,
|
||
out _,
|
||
out var llegadaReal))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
costeTotal = Math.Max(0, (llegadaReal - instanteSalida).TotalSeconds);
|
||
}
|
||
else
|
||
{
|
||
var duracionBus = EstimarDuracionBusEntreIndices(tramo, indiceSubida, tramo.IndiceFin, parametros);
|
||
costeTotal = duracionWalk + duracionBus;
|
||
}
|
||
|
||
return new EvaluacionRefinamientoParada(
|
||
UtilidadesTransitoDbus.CalcularCosteConPenalizacionPieSegundos(costeTotal, parametros, distanciaWalk),
|
||
distanciaWalk);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Evalua una parada candidata de bajada para el ultimo tramo.
|
||
/// </summary>
|
||
private async Task<EvaluacionRefinamientoParada?> EvaluarBajadaUltimoTramoAsync(
|
||
double[] destinoLatLon,
|
||
TramoBus tramo,
|
||
int indiceBajada,
|
||
ParametrosPlanificadorRuta parametros,
|
||
DateTime instanteDisponible,
|
||
CancellationToken ct)
|
||
{
|
||
ct.ThrowIfCancellationRequested();
|
||
if (indiceBajada <= tramo.IndiceInicio || indiceBajada >= tramo.Variante.CodigosParadas.Count)
|
||
return null;
|
||
|
||
var codigoParada = tramo.Variante.CodigosParadas[indiceBajada];
|
||
var parada = ResolverParadaFisica(codigoParada);
|
||
if (parada is null)
|
||
return null;
|
||
|
||
var salidaFinal = await WalkOsrmCached(parada.Latitud, parada.Longitud, destinoLatLon[0], destinoLatLon[1]);
|
||
var distanciaWalk = salidaFinal.distancia > 0
|
||
? salidaFinal.distancia
|
||
: UtilidadesTransitoDbus.CalcularDistanciaMetros(parada.Latitud, parada.Longitud, destinoLatLon[0], destinoLatLon[1]);
|
||
var duracionWalk = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaWalk, parametros);
|
||
|
||
double costeTotal;
|
||
if (parametros.UsarHorariosReales)
|
||
{
|
||
if (!ResolverSegmentoHorario(
|
||
tramo.CodigoParadaInicio,
|
||
codigoParada,
|
||
UtilidadesTransitoDbus.LineaCodigoStr(tramo.Variante),
|
||
tramo.Variante.CodigoItinerario,
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo),
|
||
instanteDisponible,
|
||
out _,
|
||
out var llegadaReal))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
costeTotal = Math.Max(0, (llegadaReal - instanteDisponible).TotalSeconds) + duracionWalk;
|
||
}
|
||
else
|
||
{
|
||
var duracionBus = EstimarDuracionBusEntreIndices(tramo, tramo.IndiceInicio, indiceBajada, parametros);
|
||
costeTotal = duracionBus + duracionWalk;
|
||
}
|
||
|
||
return new EvaluacionRefinamientoParada(
|
||
UtilidadesTransitoDbus.CalcularCosteConPenalizacionPieSegundos(costeTotal, parametros, distanciaWalk),
|
||
distanciaWalk);
|
||
}
|
||
|
||
private List<double[,]> ConstruirLineasBusPlanificacionCached(
|
||
IReadOnlyList<TramoBus> tramos,
|
||
out double distanciaTotalBus)
|
||
{
|
||
if (tramos.Count == 0)
|
||
{
|
||
distanciaTotalBus = 0;
|
||
return new List<double[,]>();
|
||
}
|
||
|
||
var clave = CrearClaveLineasBus(tramos);
|
||
if (_cacheLineasBusPlanificacion.TryGetValue(clave, out var cacheado))
|
||
{
|
||
distanciaTotalBus = cacheado.distancia;
|
||
return cacheado.lineas;
|
||
}
|
||
|
||
if (_cacheLineasBusPlanificacion.Count > 10_000)
|
||
_cacheLineasBusPlanificacion.Clear();
|
||
|
||
var lineas = _redBusServicio.ConstruirLineasBus(tramos, out distanciaTotalBus);
|
||
_cacheLineasBusPlanificacion[clave] = (lineas, distanciaTotalBus);
|
||
return lineas;
|
||
}
|
||
|
||
private double CalcularDistanciaBusCandidata(IReadOnlyList<TramoBus> tramos)
|
||
{
|
||
if (tramos.Count == 0)
|
||
return 0;
|
||
|
||
var distanciaTotal = 0.0;
|
||
foreach (var tramo in tramos)
|
||
{
|
||
if (!tramo.EsValido)
|
||
continue;
|
||
|
||
var codigos = tramo.Variante.CodigosParadas;
|
||
var indiceInicio = Math.Clamp(tramo.IndiceInicio, 0, codigos.Count - 1);
|
||
var indiceFin = Math.Clamp(tramo.IndiceFin, 0, codigos.Count - 1);
|
||
for (var indice = indiceInicio + 1; indice <= indiceFin; indice++)
|
||
{
|
||
if (!TryResolverParadaEstimacion(codigos[indice - 1], out var origen) ||
|
||
!TryResolverParadaEstimacion(codigos[indice], out var destino))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
distanciaTotal += UtilidadesTransitoDbus.CalcularDistanciaMetros(
|
||
origen.Latitud,
|
||
origen.Longitud,
|
||
destino.Latitud,
|
||
destino.Longitud);
|
||
}
|
||
}
|
||
|
||
return double.IsFinite(distanciaTotal) && distanciaTotal > 0
|
||
? distanciaTotal
|
||
: 0;
|
||
}
|
||
|
||
private bool TryResolverParadaEstimacion(string codigoParada, out Parada parada)
|
||
{
|
||
if (_cacheParadasEstimacion.TryGetValue(codigoParada, out parada!))
|
||
return true;
|
||
|
||
var encontrada = _redBusServicio.Paradas
|
||
.FirstOrDefault(p => p.Codigo.Equals(codigoParada, StringComparison.OrdinalIgnoreCase));
|
||
if (encontrada is null)
|
||
{
|
||
parada = default!;
|
||
return false;
|
||
}
|
||
|
||
_cacheParadasEstimacion[codigoParada] = encontrada;
|
||
parada = encontrada;
|
||
return true;
|
||
}
|
||
|
||
private static string CrearClaveLineasBus(IReadOnlyList<TramoBus> tramos)
|
||
{
|
||
return string.Join(";", tramos.Select(tramo =>
|
||
{
|
||
var variante = tramo.Variante;
|
||
return string.Join(":",
|
||
UtilidadesTransitoDbus.LineaCodigoStr(variante),
|
||
variante.CodigoItinerario.ToString(CultureInfo.InvariantCulture),
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(variante.Sentido?.Codigo),
|
||
Convert.ToString(variante.Variante.Codigo, CultureInfo.InvariantCulture) ?? string.Empty,
|
||
tramo.IndiceInicio.ToString(CultureInfo.InvariantCulture),
|
||
tramo.IndiceFin.ToString(CultureInfo.InvariantCulture));
|
||
}));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Estima la duración aproximada de un tramo de bus entre dos indices concretos.
|
||
/// </summary>
|
||
private double EstimarDuracionBusEntreIndices(
|
||
TramoBus tramoBase,
|
||
int indiceInicio,
|
||
int indiceFin,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (indiceInicio < 0 || indiceFin <= indiceInicio)
|
||
return double.PositiveInfinity;
|
||
|
||
var clave = BuildBusDurationRefinementKey(tramoBase, indiceInicio, indiceFin, parametros);
|
||
if (_cacheDuracionBusRefinamiento.TryGetValue(clave, out var cacheado))
|
||
return cacheado;
|
||
|
||
var tramo = new TramoBus
|
||
{
|
||
Variante = tramoBase.Variante,
|
||
IndiceInicio = indiceInicio,
|
||
IndiceFin = indiceFin
|
||
};
|
||
|
||
var lineas = ConstruirLineasBusPlanificacionCached(new[] { tramo }, out _);
|
||
if (lineas.Count == 0)
|
||
return double.PositiveInfinity;
|
||
|
||
var linea = lineas[0];
|
||
var distancia = 0.0;
|
||
for (var punto = 1; punto < linea.GetLength(0); punto++)
|
||
{
|
||
distancia += UtilidadesTransitoDbus.CalcularDistanciaMetros(
|
||
linea[punto - 1, 0],
|
||
linea[punto - 1, 1],
|
||
linea[punto, 0],
|
||
linea[punto, 1]);
|
||
}
|
||
|
||
var paradasRecorridas = Math.Max(0, indiceFin - indiceInicio);
|
||
var duracion = UtilidadesTransitoDbus.EstimarDuracionBusSegundos(distancia, parametros) +
|
||
(paradasRecorridas * Math.Max(0, parametros.SegundosPorParadaBus));
|
||
|
||
_cacheDuracionBusRefinamiento[clave] = duracion;
|
||
return duracion;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Selecciona y etiqueta las alternativas finales.
|
||
/// </summary>
|
||
private static List<AlternativaRuta> SeleccionarAlternativasFinales(
|
||
IReadOnlyList<AlternativaRuta> alternativasBus,
|
||
IReadOnlyList<AlternativaRuta> alternativasSoloPie,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
var finales = new List<AlternativaRuta>(6);
|
||
var usadas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
var categoriasPorFirma = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
||
var preferenciaEtiquetaRuta = parametros.PreferenciaEtiquetaRuta;
|
||
|
||
var masRapidaBus = ElegirMasRapidaBus(alternativasBus);
|
||
var priorizarTiempo = string.Equals(preferenciaEtiquetaRuta, "M\u00e1s r\u00e1pida", StringComparison.OrdinalIgnoreCase);
|
||
var modoMenosAndar = string.Equals(preferenciaEtiquetaRuta, "Menos andar", StringComparison.OrdinalIgnoreCase);
|
||
var alternativasBusRazonables = FiltrarAlternativasRazonablesFinales(alternativasBus, masRapidaBus);
|
||
var modoEquilibrado = string.IsNullOrWhiteSpace(preferenciaEtiquetaRuta);
|
||
var alternativasBusParaAndar = modoMenosAndar || modoEquilibrado
|
||
? AmpliarAlternativasPorAhorroAndando(alternativasBus, alternativasBusRazonables)
|
||
: alternativasBusRazonables;
|
||
var menosTransbordos = alternativasBusRazonables
|
||
.OrderBy(x => x.NumeroTransbordosReales)
|
||
.ThenBy(x => DuracionVisibleOrdenacion(x))
|
||
.ThenBy(x => x.DistanciaPieTotalAprox)
|
||
.ThenBy(x => x.PuntuacionCoste)
|
||
.FirstOrDefault();
|
||
var menosAndar = ElegirMenosAndarRazonable(
|
||
alternativasBusParaAndar,
|
||
modoMenosAndar ? null : masRapidaBus,
|
||
parametros);
|
||
var priorizarAndar = string.Equals(preferenciaEtiquetaRuta, "Menos andar", StringComparison.OrdinalIgnoreCase);
|
||
var equilibrada = modoEquilibrado
|
||
? ElegirEquilibradaRazonable(alternativasBusParaAndar, parametros)
|
||
: null;
|
||
|
||
if (priorizarAndar)
|
||
{
|
||
AddAlternativaBus(finales, usadas, menosAndar, "Menos andar");
|
||
AddAlternativaBus(finales, usadas, masRapidaBus, "M\u00e1s r\u00e1pida");
|
||
AddAlternativaBus(finales, usadas, menosTransbordos, "Menos transbordos");
|
||
}
|
||
else if (modoEquilibrado)
|
||
{
|
||
AddAlternativaBus(finales, usadas, equilibrada, "Equilibrada");
|
||
AddAlternativaBus(finales, usadas, masRapidaBus, "M\u00e1s r\u00e1pida");
|
||
AddAlternativaBus(finales, usadas, menosAndar, "Menos andar");
|
||
AddAlternativaBus(finales, usadas, menosTransbordos, "Menos transbordos");
|
||
}
|
||
else
|
||
{
|
||
AddAlternativaBus(finales, usadas, masRapidaBus, "M\u00e1s r\u00e1pida");
|
||
AddAlternativaBus(finales, usadas, menosTransbordos, "Menos transbordos");
|
||
AddAlternativaBus(finales, usadas, menosAndar, "Menos andar");
|
||
}
|
||
|
||
var candidatasRelleno = priorizarAndar
|
||
? alternativasBusParaAndar.OrderBy(x => CalcularCosteAndarEquilibradoSegundos(x, parametros)).ThenBy(x => x.PuntuacionCoste)
|
||
: priorizarTiempo
|
||
? alternativasBusRazonables.OrderBy(x => DuracionVisibleOrdenacion(x)).ThenBy(x => x.DistanciaPieTotalAprox).ThenBy(x => x.PuntuacionCoste)
|
||
: alternativasBusParaAndar.OrderBy(x => CalcularScoreEquilibrado(x, parametros))
|
||
.ThenBy(x => x.PuntuacionCoste);
|
||
|
||
foreach (var alternativa in candidatasRelleno)
|
||
{
|
||
if (finales.Count >= 4)
|
||
break;
|
||
|
||
AddAlternativaBus(finales, usadas, alternativa, $"Opci\u00f3n {finales.Count + 1}");
|
||
}
|
||
|
||
foreach (var alternativaPie in alternativasSoloPie.OrderBy(x => x.PuntuacionCoste))
|
||
{
|
||
if (finales.Count >= 6)
|
||
break;
|
||
|
||
finales.Add(alternativaPie);
|
||
}
|
||
|
||
var globalMasRapida = finales
|
||
.OrderBy(x => DuracionVisibleOrdenacion(x))
|
||
.ThenBy(x => x.PuntuacionCoste)
|
||
.FirstOrDefault();
|
||
if (globalMasRapida is not null && globalMasRapida.EsSoloAPie)
|
||
globalMasRapida.Etiqueta = "M\u00e1s r\u00e1pida";
|
||
|
||
return finales;
|
||
}
|
||
|
||
private static AlternativaRuta? ElegirEquilibradaRazonable(
|
||
IReadOnlyList<AlternativaRuta> alternativasBus,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (alternativasBus.Count == 0)
|
||
return null;
|
||
|
||
return alternativasBus
|
||
.OrderBy(x => CalcularScoreEquilibrado(x, parametros))
|
||
.ThenBy(x => x.NumeroTransbordosReales)
|
||
.ThenBy(x => x.TiempoEsperaTotalSegundos)
|
||
.ThenBy(x => x.PuntuacionCoste)
|
||
.FirstOrDefault();
|
||
}
|
||
|
||
private static double CalcularScoreEquilibrado(
|
||
AlternativaRuta alternativa,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (alternativa.AlternativaBus is null)
|
||
return double.PositiveInfinity;
|
||
|
||
var distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus);
|
||
var distanciaAndando = CalcularDistanciaAndandoCorregidaMetros(alternativa, parametros);
|
||
var factor = double.IsFinite(parametros.FactorEquilibradoAndarBus)
|
||
? Math.Max(0, parametros.FactorEquilibradoAndarBus)
|
||
: 12.0;
|
||
|
||
var score = distanciaBus + (distanciaAndando * factor);
|
||
return double.IsFinite(score) && score >= 0
|
||
? score
|
||
: double.PositiveInfinity;
|
||
}
|
||
|
||
private static double CalcularCosteAndarEquilibradoSegundos(
|
||
AlternativaRuta alternativa,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
var distanciaCorregida = CalcularDistanciaAndandoCorregidaMetros(alternativa, parametros);
|
||
|
||
if (!double.IsFinite(distanciaCorregida) || distanciaCorregida < 0)
|
||
return double.PositiveInfinity;
|
||
|
||
return UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaCorregida, parametros);
|
||
}
|
||
|
||
private static double CalcularDistanciaAndandoCorregidaMetros(
|
||
AlternativaRuta alternativa,
|
||
ParametrosPlanificadorRuta parametros)
|
||
=> UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaTotalMetros(
|
||
ObtenerDistanciasPiePorTramo(alternativa),
|
||
parametros);
|
||
|
||
private static IReadOnlyList<double> ObtenerDistanciasPiePorTramo(AlternativaRuta alternativa)
|
||
{
|
||
if (alternativa.DistanciasPiePorTramoAprox.Count > 0)
|
||
return alternativa.DistanciasPiePorTramoAprox;
|
||
|
||
return ConstruirDistanciasPiePorTramo(
|
||
alternativa.DistanciaPieInicioAprox,
|
||
alternativa.DistanciaPieTransbordosAprox > 0
|
||
? new[] { alternativa.DistanciaPieTransbordosAprox }
|
||
: Array.Empty<double>(),
|
||
alternativa.DistanciaPieFinAprox);
|
||
}
|
||
|
||
private static double DuracionVisibleOrdenacion(AlternativaRuta alternativa)
|
||
=> double.IsFinite(alternativa.DuracionTotalAproxSegundos)
|
||
? alternativa.DuracionTotalAproxSegundos
|
||
: double.PositiveInfinity;
|
||
|
||
private static AlternativaRuta? ElegirMasRapidaBus(IReadOnlyList<AlternativaRuta> alternativasBus)
|
||
{
|
||
if (alternativasBus.Count == 0)
|
||
return null;
|
||
|
||
var duracionMinima = alternativasBus.Min(DuracionVisibleOrdenacion);
|
||
return alternativasBus
|
||
.Where(x => DuracionVisibleOrdenacion(x) <= duracionMinima + ToleranciaEmpateRapidezSegundos)
|
||
.OrderBy(x => x.DistanciaPieTotalAprox)
|
||
.ThenBy(x => DuracionVisibleOrdenacion(x))
|
||
.ThenBy(x => x.PuntuacionCoste)
|
||
.FirstOrDefault();
|
||
}
|
||
|
||
private static List<AlternativaRuta> FiltrarAlternativasRazonablesFinales(
|
||
IReadOnlyList<AlternativaRuta> alternativasBus,
|
||
AlternativaRuta? masRapidaBus)
|
||
{
|
||
if (alternativasBus.Count == 0)
|
||
return new();
|
||
|
||
if (masRapidaBus is null)
|
||
return alternativasBus.ToList();
|
||
|
||
var duracionMinima = DuracionVisibleOrdenacion(masRapidaBus);
|
||
var limiteDuracion = CalcularLimiteDuracionRazonable(duracionMinima);
|
||
var razonables = alternativasBus
|
||
.Where(x => DuracionVisibleOrdenacion(x) <= limiteDuracion)
|
||
.ToList();
|
||
|
||
var filtradas = razonables.Count > 0
|
||
? razonables
|
||
: alternativasBus.ToList();
|
||
|
||
return EliminarAlternativasDominadasFinales(filtradas);
|
||
}
|
||
|
||
private static List<AlternativaRuta> AmpliarAlternativasPorAhorroAndando(
|
||
IReadOnlyList<AlternativaRuta> alternativasBus,
|
||
IReadOnlyList<AlternativaRuta> alternativasRazonables)
|
||
{
|
||
if (alternativasBus.Count == 0)
|
||
return new();
|
||
|
||
if (alternativasRazonables.Count == 0)
|
||
return EliminarAlternativasDominadasFinales(alternativasBus);
|
||
|
||
var seleccionadas = new Dictionary<string, AlternativaRuta>(StringComparer.OrdinalIgnoreCase);
|
||
Agregar(alternativasRazonables);
|
||
|
||
var mejorPieRazonable = alternativasRazonables
|
||
.Select(x => x.DistanciaPieTotalAprox)
|
||
.Where(double.IsFinite)
|
||
.DefaultIfEmpty(double.PositiveInfinity)
|
||
.Min();
|
||
if (double.IsFinite(mejorPieRazonable))
|
||
{
|
||
var limiteAhorroClaro = Math.Min(
|
||
mejorPieRazonable - AhorroClaroAndandoFinalMetros,
|
||
mejorPieRazonable * FactorAhorroClaroAndandoFinal);
|
||
if (limiteAhorroClaro > 0)
|
||
{
|
||
Agregar(alternativasBus
|
||
.Where(x => x.DistanciaPieTotalAprox <= limiteAhorroClaro)
|
||
.OrderBy(x => x.DistanciaPieTotalAprox)
|
||
.ThenBy(x => DuracionVisibleOrdenacion(x))
|
||
.ThenBy(x => x.PuntuacionCoste)
|
||
.Take(8));
|
||
}
|
||
}
|
||
|
||
return EliminarAlternativasDominadasFinales(seleccionadas.Values.ToList());
|
||
|
||
void Agregar(IEnumerable<AlternativaRuta> alternativas)
|
||
{
|
||
foreach (var alternativa in alternativas)
|
||
{
|
||
if (alternativa.AlternativaBus is null)
|
||
continue;
|
||
|
||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(alternativa.AlternativaBus);
|
||
seleccionadas.TryAdd(firma, alternativa);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static List<AlternativaRuta> EliminarAlternativasDominadasFinales(
|
||
IReadOnlyList<AlternativaRuta> alternativas)
|
||
{
|
||
if (alternativas.Count <= 1)
|
||
return alternativas.ToList();
|
||
|
||
var resultado = new List<AlternativaRuta>(alternativas.Count);
|
||
foreach (var candidata in alternativas)
|
||
{
|
||
if (!EstaDominadaPorOtraAlternativa(candidata, alternativas))
|
||
resultado.Add(candidata);
|
||
}
|
||
|
||
return resultado.Count > 0
|
||
? resultado
|
||
: alternativas.ToList();
|
||
}
|
||
|
||
private static bool EstaDominadaPorOtraAlternativa(
|
||
AlternativaRuta candidata,
|
||
IReadOnlyList<AlternativaRuta> alternativas)
|
||
{
|
||
var duracionCandidata = DuracionVisibleOrdenacion(candidata);
|
||
var pieCandidata = candidata.DistanciaPieTotalAprox;
|
||
var busCandidata = candidata.AlternativaBus?.DistanciaTotalBus ?? double.PositiveInfinity;
|
||
|
||
foreach (var otra in alternativas)
|
||
{
|
||
if (ReferenceEquals(otra, candidata))
|
||
continue;
|
||
|
||
if (otra.NumeroTransbordosReales > candidata.NumeroTransbordosReales)
|
||
continue;
|
||
|
||
var duracionOtra = DuracionVisibleOrdenacion(otra);
|
||
var pieOtra = otra.DistanciaPieTotalAprox;
|
||
var busOtra = otra.AlternativaBus?.DistanciaTotalBus ?? double.PositiveInfinity;
|
||
var noPierdeTiempo = duracionOtra <= duracionCandidata + ToleranciaDominanciaFinalSegundos;
|
||
var noPierdeAndando = pieOtra <= pieCandidata + ToleranciaDominanciaFinalMetros;
|
||
var noPierdeBus = busOtra <= busCandidata + ToleranciaDominanciaFinalBusMetros;
|
||
if (!noPierdeTiempo || !noPierdeAndando || !noPierdeBus)
|
||
continue;
|
||
|
||
var mejoraTransbordo = otra.NumeroTransbordosReales < candidata.NumeroTransbordosReales;
|
||
var mejoraTiempo = duracionOtra <= duracionCandidata - MejoraClaraDominanciaFinalSegundos;
|
||
var mejoraAndando = pieOtra <= pieCandidata - MejoraClaraDominanciaFinalMetros;
|
||
if (mejoraTransbordo || mejoraTiempo || mejoraAndando)
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static AlternativaRuta? ElegirMenosAndarRazonable(
|
||
IReadOnlyList<AlternativaRuta> alternativasBus,
|
||
AlternativaRuta? masRapidaBus,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (alternativasBus.Count == 0)
|
||
return null;
|
||
|
||
if (masRapidaBus is null)
|
||
{
|
||
return alternativasBus
|
||
.OrderBy(x => CalcularCosteAndarEquilibradoSegundos(x, parametros))
|
||
.ThenBy(x => DuracionVisibleOrdenacion(x))
|
||
.ThenBy(x => x.PuntuacionCoste)
|
||
.FirstOrDefault();
|
||
}
|
||
|
||
var duracionMinima = DuracionVisibleOrdenacion(masRapidaBus);
|
||
var limiteDuracion = CalcularLimiteDuracionRazonable(duracionMinima);
|
||
var candidatasRazonables = alternativasBus
|
||
.Where(x => DuracionVisibleOrdenacion(x) <= limiteDuracion)
|
||
.ToList();
|
||
|
||
if (candidatasRazonables.Count == 0)
|
||
candidatasRazonables = alternativasBus.ToList();
|
||
|
||
return candidatasRazonables
|
||
.OrderBy(x => CalcularCosteAndarEquilibradoSegundos(x, parametros))
|
||
.ThenBy(x => DuracionVisibleOrdenacion(x))
|
||
.ThenBy(x => x.PuntuacionCoste)
|
||
.FirstOrDefault();
|
||
}
|
||
|
||
private static double CalcularLimiteDuracionRazonable(double duracionMinima)
|
||
{
|
||
if (!double.IsFinite(duracionMinima))
|
||
return double.PositiveInfinity;
|
||
|
||
var margenRazonable = Math.Max(15 * 60.0, duracionMinima * 0.5);
|
||
return duracionMinima + margenRazonable;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Calcula el coste interno de una alternativa aplicando penalizacion progresiva a pie.
|
||
/// </summary>
|
||
private static double CalcularPuntuacionCoste(AlternativaRuta alternativa, ParametrosPlanificadorRuta parametros)
|
||
{
|
||
return UtilidadesTransitoDbus.CalcularCosteConPenalizacionesRankingSegundos(
|
||
alternativa.DuracionTotalAproxSegundos,
|
||
parametros,
|
||
alternativa.NumeroTransbordosReales,
|
||
ObtenerDistanciasPiePorTramo(alternativa).ToArray());
|
||
}
|
||
|
||
/// <summary>
|
||
/// Intenta añadir una alternativa de bus a la lista final.
|
||
/// </summary>
|
||
private static void AddAlternativaBus(
|
||
ICollection<AlternativaRuta> finales,
|
||
ISet<string> usadas,
|
||
AlternativaRuta? alternativa,
|
||
string etiqueta)
|
||
{
|
||
if (alternativa is null || alternativa.AlternativaBus is null)
|
||
return;
|
||
|
||
var firma = CrearFirmaFamiliaRutaBus(alternativa.AlternativaBus);
|
||
if (!usadas.Add(firma))
|
||
return;
|
||
|
||
alternativa.Etiqueta = etiqueta;
|
||
finales.Add(alternativa);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Firma orientada a UI: agrupa alternativas que usan la misma familia de lineas aunque cambie la parada exacta.
|
||
/// </summary>
|
||
private static string CrearFirmaFamiliaRutaBus(RutaBusAlternativa alternativa)
|
||
{
|
||
var partes = new List<string>();
|
||
foreach (var tramo in alternativa.Tramos)
|
||
{
|
||
var linea = UtilidadesTransitoDbus.LineaCodigoStr(tramo.Variante);
|
||
var parte = $"{linea}.{tramo.Variante.CodigoItinerario}";
|
||
if (partes.Count == 0 || !string.Equals(partes[^1], parte, StringComparison.OrdinalIgnoreCase))
|
||
partes.Add(parte);
|
||
}
|
||
|
||
return string.Join(">", partes);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Clona una ruta de bus para no compartir instancias mutables.
|
||
/// </summary>
|
||
private static RutaBusAlternativa ClonarRutaBus(RutaBusAlternativa rutaBus)
|
||
{
|
||
return new RutaBusAlternativa
|
||
{
|
||
Tramos = rutaBus.Tramos.Select(tramo => new TramoBus
|
||
{
|
||
Variante = tramo.Variante,
|
||
IndiceInicio = tramo.IndiceInicio,
|
||
IndiceFin = tramo.IndiceFin
|
||
}).ToList(),
|
||
NumeroTransbordos = UtilidadesTransitoDbus.ContarTransbordosReales(rutaBus.Tramos),
|
||
DistanciaTotalBus = rutaBus.DistanciaTotalBus,
|
||
CodigosParadasTransbordo = rutaBus.CodigosParadasTransbordo.ToList()
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Resuelve la parada fisica mas coherente para un codigo y una referencia espacial.
|
||
/// </summary>
|
||
private Parada ResolverParadaFisica(string? codigoParada, double[]? puntoReferencia = null, Parada? paradaPreferida = null)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(codigoParada))
|
||
{
|
||
var candidatas = _redBusServicio.Paradas
|
||
.Where(parada => parada.Codigo.Equals(codigoParada, StringComparison.OrdinalIgnoreCase))
|
||
.ToList();
|
||
|
||
if (candidatas.Count == 1)
|
||
return candidatas[0];
|
||
|
||
if (candidatas.Count > 1)
|
||
{
|
||
if (paradaPreferida is not null &&
|
||
paradaPreferida.Codigo.Equals(codigoParada, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var coincidenciaExacta = candidatas.FirstOrDefault(parada =>
|
||
Math.Abs(parada.Latitud - paradaPreferida.Latitud) < 1e-9 &&
|
||
Math.Abs(parada.Longitud - paradaPreferida.Longitud) < 1e-9);
|
||
|
||
if (coincidenciaExacta is not null)
|
||
return coincidenciaExacta;
|
||
}
|
||
|
||
if (puntoReferencia is not null)
|
||
{
|
||
return candidatas
|
||
.OrderBy(parada => UtilidadesTransitoDbus.CalcularDistanciaMetros(
|
||
parada.Latitud,
|
||
parada.Longitud,
|
||
puntoReferencia[0],
|
||
puntoReferencia[1]))
|
||
.First();
|
||
}
|
||
|
||
if (paradaPreferida is not null &&
|
||
paradaPreferida.Codigo.Equals(codigoParada, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return paradaPreferida;
|
||
}
|
||
|
||
return candidatas[0];
|
||
}
|
||
}
|
||
|
||
return paradaPreferida ?? new Parada();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Devuelve el primer punto de una linea.
|
||
/// </summary>
|
||
private static double[]? ObtenerPrimerPunto(double[,]? linea)
|
||
{
|
||
if (linea is null || linea.GetLength(0) == 0)
|
||
return null;
|
||
|
||
return new[] { linea[0, 0], linea[0, 1] };
|
||
}
|
||
|
||
/// <summary>
|
||
/// Devuelve el ultimo punto de una linea.
|
||
/// </summary>
|
||
private static double[]? ObtenerUltimoPunto(double[,]? linea)
|
||
{
|
||
if (linea is null || linea.GetLength(0) == 0)
|
||
return null;
|
||
|
||
var ultimo = linea.GetLength(0) - 1;
|
||
return new[] { linea[ultimo, 0], linea[ultimo, 1] };
|
||
}
|
||
|
||
/// <summary>
|
||
/// Calcula la distancia entre dos puntos geograficos.
|
||
/// </summary>
|
||
private static double CalcularDistanciaEntrePuntos(double[]? origen, double[]? destino)
|
||
{
|
||
if (origen is null || destino is null || origen.Length < 2 || destino.Length < 2)
|
||
return 0;
|
||
|
||
return UtilidadesTransitoDbus.CalcularDistanciaMetros(origen[0], origen[1], destino[0], destino[1]);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Comprueba si un patron pasa el filtro del usuario.
|
||
/// </summary>
|
||
private static bool PatronPermitido(PatronTransitoDbus patron, ParametrosPlanificadorRuta parametros)
|
||
{
|
||
if (parametros.ActivarRangoLineas)
|
||
{
|
||
if (patron.NumeroLinea is null)
|
||
return false;
|
||
|
||
if (parametros.LineasPermitidas.Count == 0 ||
|
||
!parametros.LineasPermitidas.Contains(patron.NumeroLinea.Value))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (parametros.ActivarRangoItinerarios)
|
||
{
|
||
var minimoItinerario = Math.Min(parametros.ItinerarioMin, parametros.ItinerarioMax);
|
||
var maximoItinerario = Math.Max(parametros.ItinerarioMin, parametros.ItinerarioMax);
|
||
if (patron.CodigoItinerario < minimoItinerario || patron.CodigoItinerario > maximoItinerario)
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Devuelve una caminata desde OSRM con cache.
|
||
/// </summary>
|
||
private async Task<(double[,]? linea, double distancia, double duracion)> WalkOsrmCached(
|
||
double latitudOrigen,
|
||
double longitudOrigen,
|
||
double latitudDestino,
|
||
double longitudDestino)
|
||
{
|
||
var clave = BuildWalkKey("osrm", latitudOrigen, longitudOrigen, latitudDestino, longitudDestino);
|
||
if (_cacheCaminatas.TryGetValue(clave, out var cacheado))
|
||
return cacheado;
|
||
|
||
try
|
||
{
|
||
var (linea, distancia, duracion) = await _clienteOsrm.ObtenerRutaAPieAsync(
|
||
latitudOrigen,
|
||
longitudOrigen,
|
||
latitudDestino,
|
||
longitudDestino);
|
||
|
||
var valor = (linea, distancia, duracion);
|
||
_cacheCaminatas[clave] = valor;
|
||
return valor;
|
||
}
|
||
catch
|
||
{
|
||
var vacio = (linea: (double[,]?)null, distancia: 0.0, duracion: 0.0);
|
||
_cacheCaminatas[clave] = vacio;
|
||
return vacio;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Devuelve una caminata desde Valhalla con cache.
|
||
/// </summary>
|
||
private async Task<(double[,]? linea, double distancia, double duracion)> WalkValhallaCached(
|
||
double latitudOrigen,
|
||
double longitudOrigen,
|
||
double latitudDestino,
|
||
double longitudDestino)
|
||
{
|
||
var clave = BuildWalkKey("valhalla", latitudOrigen, longitudOrigen, latitudDestino, longitudDestino);
|
||
if (_cacheCaminatas.TryGetValue(clave, out var cacheado))
|
||
return cacheado;
|
||
|
||
try
|
||
{
|
||
var (linea, distancia, duracion) = await _clienteValhalla.ObtenerRutaAsync(
|
||
latitudOrigen,
|
||
longitudOrigen,
|
||
latitudDestino,
|
||
longitudDestino,
|
||
costing: "pedestrian");
|
||
|
||
var valor = (linea, distancia, duracion);
|
||
_cacheCaminatas[clave] = valor;
|
||
return valor;
|
||
}
|
||
catch
|
||
{
|
||
var vacio = (linea: (double[,]?)null, distancia: 0.0, duracion: 0.0);
|
||
_cacheCaminatas[clave] = vacio;
|
||
return vacio;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Construye una clave estable de cache para caminatas.
|
||
/// </summary>
|
||
private static string BuildWalkKey(
|
||
string proveedor,
|
||
double latitudOrigen,
|
||
double longitudOrigen,
|
||
double latitudDestino,
|
||
double longitudDestino)
|
||
{
|
||
static string Q(double valor) => Math.Round(valor, 5).ToString("0.00000", CultureInfo.InvariantCulture);
|
||
return $"{proveedor}|{Q(latitudOrigen)},{Q(longitudOrigen)}|{Q(latitudDestino)},{Q(longitudDestino)}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// Construye una clave estable para cachear duraciones aproximadas de bus durante el refinamiento.
|
||
/// </summary>
|
||
private static string BuildBusDurationRefinementKey(
|
||
TramoBus tramoBase,
|
||
int indiceInicio,
|
||
int indiceFin,
|
||
ParametrosPlanificadorRuta parametros)
|
||
{
|
||
static string Q(double valor) => Math.Round(valor, 3).ToString("0.000", CultureInfo.InvariantCulture);
|
||
|
||
return string.Join("|",
|
||
UtilidadesTransitoDbus.LineaCodigoStr(tramoBase.Variante),
|
||
tramoBase.Variante.CodigoItinerario.ToString(CultureInfo.InvariantCulture),
|
||
UtilidadesTransitoDbus.NormalizarSentidoCodigo(tramoBase.Variante.Sentido?.Codigo),
|
||
tramoBase.Variante.Variante.Codigo.ToString(CultureInfo.InvariantCulture),
|
||
indiceInicio.ToString(CultureInfo.InvariantCulture),
|
||
indiceFin.ToString(CultureInfo.InvariantCulture),
|
||
Q(parametros.VelocidadBusKmH),
|
||
parametros.SegundosPorParadaBus.ToString(CultureInfo.InvariantCulture));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Resultado mÃnimo de una evaluación local de parada.
|
||
/// </summary>
|
||
private sealed record SegundaPiernaTransbordoRobusta(
|
||
PatronTransitoDbus PrimerPatron,
|
||
int IndiceSubidaPrimerPatron,
|
||
int IndiceFinPrimerPatron,
|
||
PatronTransitoDbus? PatronContinuacion,
|
||
int IndiceInicioContinuacion,
|
||
int IndiceDestinoContinuacion,
|
||
ParadaCandidata Destino)
|
||
{
|
||
public int NumeroTramos => PatronContinuacion is null ? 1 : 2;
|
||
}
|
||
|
||
private sealed record EvaluacionRefinamientoParada(double Score, double DistanciaWalk);
|
||
}
|