Files
RutasDBUS/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs
Pedro 6145fb635a - andar equilibraod separado del calculo
- comienzo de penalizacoin en 200m
- centrado de origen
2026-07-29 14:45:59 +02:00

3729 lines
157 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}