Files
RutasDBUS/RutasDBUS/Servicios/Planificacion/BuscadorRutasTemporalesDbus.cs
2026-07-07 14:18:51 +02:00

1314 lines
49 KiB
C#

using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus;
namespace RutasDBUS.Servicios.Planificacion;
/// <summary>
/// Busca rutas de bus mediante rondas sobre patrones y expediciones.
/// </summary>
public sealed class BuscadorRutasTemporalesDbus
{
private const int MaxEstadosPorParada = 4;
private const int MaxContinuidadesCircularesExtra = 2;
/// <summary>
/// Busca alternativas de bus entre grupos de paradas candidatas.
/// </summary>
public IReadOnlyList<ResultadoBusquedaRutaBusDbus> BuscarAlternativas(
IndiceTransitoDbus indice,
IReadOnlyList<ParadaCandidata> candidatasOrigen,
IReadOnlyList<ParadaCandidata> candidatasDestino,
DateTime instanteSalida,
ParametrosPlanificadorRuta parametros,
IReadOnlySet<string>? calendariosActivos,
CancellationToken cancellationToken = default)
{
var origenPorCodigo = candidatasOrigen
.GroupBy(candidata => candidata.Parada.Codigo, StringComparer.OrdinalIgnoreCase)
.ToDictionary(grupo => grupo.Key, grupo => grupo.OrderBy(x => x.TiempoAPie).First(), StringComparer.OrdinalIgnoreCase);
var destinoPorCodigo = candidatasDestino
.GroupBy(candidata => candidata.Parada.Codigo, StringComparer.OrdinalIgnoreCase)
.ToDictionary(grupo => grupo.Key, grupo => grupo.OrderBy(x => x.TiempoAPie).First(), StringComparer.OrdinalIgnoreCase);
var rondas = new List<Dictionary<string, EstadoBusquedaParadaDbus>>();
var rondaInicial = new Dictionary<string, EstadoBusquedaParadaDbus>(StringComparer.OrdinalIgnoreCase);
foreach (var candidata in candidatasOrigen.OrderBy(x => x.TiempoAPie))
{
var estado = new EstadoBusquedaParadaDbus
{
CodigoParada = candidata.Parada.Codigo,
Llegada = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, candidata.TiempoAPie),
DistanciaCaminadaTotal = candidata.DistanciaAPie,
DistanciaPieTransbordos = 0,
TiempoEsperaInicialSegundos = 0,
TiempoEsperaTransbordosSegundos = 0,
TransbordosReales = 0,
BusesTomados = 0,
Movimiento = new MovimientoAccesoDbus
{
DistanciaMetros = candidata.DistanciaAPie,
DuracionSegundos = candidata.TiempoAPie
}
};
IntentarActualizarEstado(rondaInicial, estado);
}
rondas.Add(rondaInicial);
var marcadas = new HashSet<string>(rondaInicial.Values.Select(estado => estado.CodigoParada), StringComparer.OrdinalIgnoreCase);
var maximoBuses = ResolverMaximoBuses(parametros);
var codigosParadaRelevantes = CrearFiltroParadasRelevantes(indice, candidatasDestino, parametros);
for (var ronda = 1; ronda <= maximoBuses && marcadas.Count > 0; ronda++)
{
cancellationToken.ThrowIfCancellationRequested();
var estadosActuales = new Dictionary<string, EstadoBusquedaParadaDbus>(StringComparer.OrdinalIgnoreCase);
var estadosPreviosPorParada = IndexarEstadosPorParada(rondas[ronda - 1].Values);
var patronesMarcados = new HashSet<PatronTransitoDbus>();
foreach (var codigoParada in marcadas)
{
if (!indice.PatronesPorParada.TryGetValue(codigoParada, out var referencias))
continue;
foreach (var referencia in referencias)
{
if (PatronPermitido(referencia.Patron, parametros))
patronesMarcados.Add(referencia.Patron);
}
}
var bloqueoEstados = new object();
Parallel.ForEach(patronesMarcados, new ParallelOptions
{
CancellationToken = cancellationToken,
MaxDegreeOfParallelism = Math.Clamp(Environment.ProcessorCount, 1, 4)
}, patron =>
{
var estadosLocales = new Dictionary<string, EstadoBusquedaParadaDbus>(StringComparer.OrdinalIgnoreCase);
if (parametros.UsarHorariosReales)
{
var cacheExpediciones = new Dictionary<string, (ExpedicionTransitoDbus? expedicion, DateTime? salida)>(StringComparer.Ordinal);
EscanearPatronConHorarios(
patron,
estadosPreviosPorParada,
estadosLocales,
instanteSalida.Date,
parametros,
calendariosActivos,
cacheExpediciones,
codigosParadaRelevantes,
cancellationToken);
}
else
{
EscanearPatronAproximado(
patron,
estadosPreviosPorParada,
estadosLocales,
parametros,
codigosParadaRelevantes,
cancellationToken);
}
if (estadosLocales.Count == 0)
return;
lock (bloqueoEstados)
{
foreach (var estado in estadosLocales.Values)
IntentarActualizarEstado(estadosActuales, estado);
}
});
RelajarTransferencias(indice, estadosActuales, parametros, codigosParadaRelevantes, cancellationToken);
PodarEstadosPorParada(estadosActuales);
rondas.Add(estadosActuales);
marcadas = new HashSet<string>(estadosActuales.Values.Select(estado => estado.CodigoParada), StringComparer.OrdinalIgnoreCase);
}
return ConstruirResultados(rondas, origenPorCodigo, destinoPorCodigo, instanteSalida, parametros);
}
/// <summary>
/// Calcula el maximo de buses a considerar.
/// </summary>
private static int ResolverMaximoBuses(ParametrosPlanificadorRuta parametros)
=> Math.Max(2, Math.Max(0, parametros.MaxTransbordosPermitidos) + 1 + MaxContinuidadesCircularesExtra);
/// <summary>
/// Recorre un patron en modo horario real.
/// </summary>
private static void EscanearPatronConHorarios(
PatronTransitoDbus patron,
IReadOnlyDictionary<string, List<EstadoBusquedaParadaDbus>> estadosPrevios,
IDictionary<string, EstadoBusquedaParadaDbus> estadosActuales,
DateTime fechaBase,
ParametrosPlanificadorRuta parametros,
IReadOnlySet<string>? calendariosActivos,
IDictionary<string, (ExpedicionTransitoDbus? expedicion, DateTime? salida)> cacheExpediciones,
IReadOnlySet<string>? codigosParadaRelevantes,
CancellationToken cancellationToken)
{
if (calendariosActivos is null || calendariosActivos.Count == 0)
return;
for (var indiceSubida = 0; indiceSubida < patron.CodigosParada.Count - 1; indiceSubida++)
{
cancellationToken.ThrowIfCancellationRequested();
var codigoParada = patron.CodigosParada[indiceSubida];
foreach (var estadoPrevio in ObtenerEstadosEnParada(estadosPrevios, codigoParada))
{
var expedicion = EncontrarPrimeraExpedicionValida(
patron,
indiceSubida,
estadoPrevio,
fechaBase,
parametros,
calendariosActivos,
cacheExpediciones,
out var esTransbordoReal,
out var salida);
if (expedicion is null || salida is null)
continue;
var transbordosReales = estadoPrevio.TransbordosReales + (esTransbordoReal ? 1 : 0);
if (!TransbordosEnRangoParcial(transbordosReales, parametros))
continue;
var esperaSegundos = Math.Max(0, (salida.Value - estadoPrevio.Llegada).TotalSeconds);
var esperaInicial = estadoPrevio.TiempoEsperaInicialSegundos + (!esTransbordoReal ? esperaSegundos : 0);
var esperaTransbordos = estadoPrevio.TiempoEsperaTransbordosSegundos + (esTransbordoReal ? esperaSegundos : 0);
for (var indiceBajada = indiceSubida + 1; indiceBajada < patron.CodigosParada.Count; indiceBajada++)
{
if (!ParadaEsRelevante(patron.CodigosParada[indiceBajada], codigosParadaRelevantes))
continue;
var llegada = ObtenerFechaDesdeSegundos(fechaBase, expedicion.LlegadasSegundos[indiceBajada]);
var nuevoEstado = new EstadoBusquedaParadaDbus
{
CodigoParada = patron.CodigosParada[indiceBajada],
Llegada = llegada,
DistanciaCaminadaTotal = estadoPrevio.DistanciaCaminadaTotal,
DistanciaPieTransbordos = estadoPrevio.DistanciaPieTransbordos,
TiempoEsperaInicialSegundos = esperaInicial,
TiempoEsperaTransbordosSegundos = esperaTransbordos,
TransbordosReales = transbordosReales,
BusesTomados = estadoPrevio.BusesTomados + 1,
EstadoAnterior = estadoPrevio,
Movimiento = new MovimientoBusDbus
{
Patron = patron,
IndiceSubida = indiceSubida,
IndiceBajada = indiceBajada,
Salida = salida.Value,
Llegada = llegada
}
};
IntentarActualizarEstado(estadosActuales, nuevoEstado);
}
}
}
}
/// <summary>
/// Recorre un patron en modo aproximado sin horarios.
/// </summary>
private static void EscanearPatronAproximado(
PatronTransitoDbus patron,
IReadOnlyDictionary<string, List<EstadoBusquedaParadaDbus>> estadosPrevios,
IDictionary<string, EstadoBusquedaParadaDbus> estadosActuales,
ParametrosPlanificadorRuta parametros,
IReadOnlySet<string>? codigosParadaRelevantes,
CancellationToken cancellationToken)
{
for (var indiceSubida = 0; indiceSubida < patron.CodigosParada.Count - 1; indiceSubida++)
{
cancellationToken.ThrowIfCancellationRequested();
var codigoParada = patron.CodigosParada[indiceSubida];
foreach (var estadoPrevio in ObtenerEstadosEnParada(estadosPrevios, codigoParada))
{
if (!PuedeTomarPatronDesdeEstado(estadoPrevio, patron, out var esTransbordoReal))
continue;
var salida = esTransbordoReal
? estadoPrevio.Llegada.AddMinutes(parametros.MinutosPorTransbordo)
: estadoPrevio.Llegada;
var transbordosReales = estadoPrevio.TransbordosReales + (esTransbordoReal ? 1 : 0);
if (!TransbordosEnRangoParcial(transbordosReales, parametros))
continue;
for (var indiceBajada = indiceSubida + 1; indiceBajada < patron.CodigosParada.Count; indiceBajada++)
{
if (!ParadaEsRelevante(patron.CodigosParada[indiceBajada], codigosParadaRelevantes))
continue;
var distanciaBus = patron.DistanciaAcumuladaPorParadaMetros[indiceBajada] - patron.DistanciaAcumuladaPorParadaMetros[indiceSubida];
var duracionBus = UtilidadesTransitoDbus.EstimarDuracionBusSegundos(distanciaBus, parametros) +
((indiceBajada - indiceSubida) * Math.Max(0, parametros.SegundosPorParadaBus));
var llegada = UtilidadesTransitoDbus.AddSecondsSafe(salida, duracionBus);
var nuevoEstado = new EstadoBusquedaParadaDbus
{
CodigoParada = patron.CodigosParada[indiceBajada],
Llegada = llegada,
DistanciaCaminadaTotal = estadoPrevio.DistanciaCaminadaTotal,
DistanciaPieTransbordos = estadoPrevio.DistanciaPieTransbordos,
TiempoEsperaInicialSegundos = estadoPrevio.TiempoEsperaInicialSegundos,
TiempoEsperaTransbordosSegundos = estadoPrevio.TiempoEsperaTransbordosSegundos + (esTransbordoReal ? parametros.MinutosPorTransbordo * 60.0 : 0),
TransbordosReales = transbordosReales,
BusesTomados = estadoPrevio.BusesTomados + 1,
EstadoAnterior = estadoPrevio,
Movimiento = new MovimientoBusDbus
{
Patron = patron,
IndiceSubida = indiceSubida,
IndiceBajada = indiceBajada,
Salida = salida,
Llegada = llegada
}
};
IntentarActualizarEstado(estadosActuales, nuevoEstado);
}
}
}
}
/// <summary>
/// Propaga transferencias peatonales dentro de la misma ronda.
/// </summary>
private static void RelajarTransferencias(
IndiceTransitoDbus indice,
IDictionary<string, EstadoBusquedaParadaDbus> estadosActuales,
ParametrosPlanificadorRuta parametros,
IReadOnlySet<string>? codigosParadaRelevantes,
CancellationToken cancellationToken)
{
var cola = new Queue<EstadoBusquedaParadaDbus>(estadosActuales.Values);
while (cola.Count > 0)
{
cancellationToken.ThrowIfCancellationRequested();
var estado = cola.Dequeue();
if (!indice.TransferenciasPorParada.TryGetValue(estado.CodigoParada, out var transferencias))
continue;
foreach (var transferencia in transferencias)
{
if (!ParadaEsRelevante(transferencia.CodigoParadaDestino, codigosParadaRelevantes))
continue;
var duracion = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(transferencia.DistanciaMetros, parametros);
var nuevoEstado = new EstadoBusquedaParadaDbus
{
CodigoParada = transferencia.CodigoParadaDestino,
Llegada = UtilidadesTransitoDbus.AddSecondsSafe(estado.Llegada, duracion),
DistanciaCaminadaTotal = estado.DistanciaCaminadaTotal + transferencia.DistanciaMetros,
DistanciaPieTransbordos = estado.DistanciaPieTransbordos + transferencia.DistanciaMetros,
TiempoEsperaInicialSegundos = estado.TiempoEsperaInicialSegundos,
TiempoEsperaTransbordosSegundos = estado.TiempoEsperaTransbordosSegundos,
TransbordosReales = estado.TransbordosReales,
BusesTomados = estado.BusesTomados,
EstadoAnterior = estado,
Movimiento = new MovimientoPieTransbordoDbus
{
DistanciaMetros = transferencia.DistanciaMetros,
DuracionSegundos = duracion
}
};
if (IntentarActualizarEstado(estadosActuales, nuevoEstado))
cola.Enqueue(nuevoEstado);
}
}
}
/// <summary>
/// Encuentra la primera expedicion utilizable desde una parada del patron.
/// </summary>
private static ExpedicionTransitoDbus? EncontrarPrimeraExpedicionValida(
PatronTransitoDbus patron,
int indiceSubida,
EstadoBusquedaParadaDbus estadoPrevio,
DateTime fechaBase,
ParametrosPlanificadorRuta parametros,
IReadOnlySet<string> calendariosActivos,
IDictionary<string, (ExpedicionTransitoDbus? expedicion, DateTime? salida)> cacheExpediciones,
out bool esTransbordoReal,
out DateTime? salida)
{
salida = null;
esTransbordoReal = false;
if (!PuedeTomarPatronDesdeEstado(estadoPrevio, patron, out esTransbordoReal))
return null;
var margenTransbordo = esTransbordoReal
? Math.Clamp(parametros.MargenTransbordoSegundos, 0, 1800)
: 0;
var primeraSalidaAceptable = estadoPrevio.Llegada.AddSeconds(margenTransbordo);
var claveCache = CrearClaveExpedicion(patron, indiceSubida, fechaBase, primeraSalidaAceptable);
if (!cacheExpediciones.TryGetValue(claveCache, out var cacheado))
{
cacheado = EncontrarPrimeraExpedicionDesde(
patron,
indiceSubida,
fechaBase,
calendariosActivos,
primeraSalidaAceptable);
cacheExpediciones[claveCache] = cacheado;
}
if (cacheado.expedicion is null || cacheado.salida is null)
return null;
var esperaCacheadaSegundos = Math.Max(0, (cacheado.salida.Value - estadoPrevio.Llegada).TotalSeconds);
if (!EsEsperaValida(estadoPrevio, esperaCacheadaSegundos, esTransbordoReal, parametros))
return null;
salida = cacheado.salida;
return cacheado.expedicion;
}
private static (ExpedicionTransitoDbus? expedicion, DateTime? salida) EncontrarPrimeraExpedicionDesde(
PatronTransitoDbus patron,
int indiceSubida,
DateTime fechaBase,
IReadOnlySet<string> calendariosActivos,
DateTime primeraSalidaAceptable)
{
var expediciones = patron.Expediciones;
if (expediciones.Count == 0)
return (null, null);
var segundosObjetivo = (int)Math.Ceiling((primeraSalidaAceptable - fechaBase.Date).TotalSeconds);
var indiceInicial = BuscarPrimerIndiceSalidaDesde(expediciones, indiceSubida, segundosObjetivo);
if (indiceInicial < 0)
return (null, null);
for (var indiceExpedicion = indiceInicial; indiceExpedicion < expediciones.Count; indiceExpedicion++)
{
var expedicion = expediciones[indiceExpedicion];
if (!calendariosActivos.Contains(expedicion.IdCalendario))
continue;
var salidaExpedicion = ObtenerFechaDesdeSegundos(fechaBase, expedicion.SalidasSegundos[indiceSubida]);
if (salidaExpedicion < primeraSalidaAceptable)
continue;
return (expedicion, salidaExpedicion);
}
return (null, null);
}
private static int BuscarPrimerIndiceSalidaDesde(
IReadOnlyList<ExpedicionTransitoDbus> expediciones,
int indiceSubida,
int segundosObjetivo)
{
var izquierda = 0;
var derecha = expediciones.Count - 1;
var encontrado = -1;
while (izquierda <= derecha)
{
var medio = izquierda + ((derecha - izquierda) / 2);
var salida = expediciones[medio].SalidasSegundos[indiceSubida];
if (salida >= segundosObjetivo)
{
encontrado = medio;
derecha = medio - 1;
}
else
{
izquierda = medio + 1;
}
}
return encontrado;
}
private static string CrearClaveExpedicion(
PatronTransitoDbus patron,
int indiceSubida,
DateTime fechaBase,
DateTime primeraSalidaAceptable)
{
var ticksSegundo = primeraSalidaAceptable.Ticks - primeraSalidaAceptable.Ticks % TimeSpan.TicksPerSecond;
return $"{patron.Id}|{indiceSubida}|{fechaBase:yyyyMMdd}|{ticksSegundo}";
}
/// <summary>
/// Comprueba si una espera respeta las restricciones configuradas.
/// </summary>
private static bool EsEsperaValida(
EstadoBusquedaParadaDbus estadoPrevio,
double esperaSegundos,
bool esTransbordoReal,
ParametrosPlanificadorRuta parametros)
{
if (estadoPrevio.BusesTomados == 0 &&
parametros.MaxEsperaInicialMinutos > 0 &&
esperaSegundos > parametros.MaxEsperaInicialMinutos * 60.0)
{
return false;
}
if (esTransbordoReal &&
parametros.MaxEsperaTransbordoMinutos > 0 &&
esperaSegundos > parametros.MaxEsperaTransbordoMinutos * 60.0)
{
return false;
}
var esperaTotal = estadoPrevio.TiempoEsperaInicialSegundos + estadoPrevio.TiempoEsperaTransbordosSegundos + esperaSegundos;
if (parametros.MaxEsperaTotalMinutos > 0 &&
esperaTotal > parametros.MaxEsperaTotalMinutos * 60.0)
{
return false;
}
return true;
}
private static bool TransbordosEnRangoParcial(int transbordosReales, ParametrosPlanificadorRuta parametros)
{
if (!parametros.ActivarRangoTransbordos)
return true;
var maximo = Math.Max(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
return transbordosReales <= maximo;
}
private static HashSet<string>? CrearFiltroParadasRelevantes(
IndiceTransitoDbus indice,
IReadOnlyList<ParadaCandidata> candidatasDestino,
ParametrosPlanificadorRuta parametros)
{
if (!parametros.ActivarRangoTransbordos)
return null;
var maximoTransbordos = Math.Max(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
if (maximoTransbordos > 1)
return null;
var relevantes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var subidasHaciaDestino = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var destino in candidatasDestino)
{
relevantes.Add(destino.Parada.Codigo);
if (!indice.PatronesPorParada.TryGetValue(destino.Parada.Codigo, out var referenciasDestino))
continue;
foreach (var referencia in referenciasDestino)
{
var patron = referencia.Patron;
if (!PatronPermitido(patron, parametros))
continue;
var indiceDestino = referencia.IndiceParada;
if (indiceDestino <= 0 || indiceDestino >= patron.CodigosParada.Count)
continue;
for (var indiceSubida = 0; indiceSubida < indiceDestino; indiceSubida++)
subidasHaciaDestino.Add(patron.CodigosParada[indiceSubida]);
}
}
foreach (var codigoSubida in subidasHaciaDestino)
relevantes.Add(codigoSubida);
foreach (var transferenciaOrigen in indice.TransferenciasPorParada)
{
if (transferenciaOrigen.Value.Any(transferencia =>
subidasHaciaDestino.Contains(transferencia.CodigoParadaDestino)))
{
relevantes.Add(transferenciaOrigen.Key);
}
}
foreach (var referenciaPorParada in indice.PatronesPorParada)
{
if (EsPivoteCircular(referenciaPorParada.Value))
relevantes.Add(referenciaPorParada.Key);
}
return relevantes;
}
private static bool ParadaEsRelevante(string codigoParada, IReadOnlySet<string>? codigosParadaRelevantes)
=> codigosParadaRelevantes is null || codigosParadaRelevantes.Contains(codigoParada);
private static bool EsPivoteCircular(IReadOnlyList<ReferenciaPatronParadaDbus> referencias)
{
for (var i = 0; i < referencias.Count; i++)
{
var izquierda = referencias[i].Patron;
for (var j = i + 1; j < referencias.Count; j++)
{
if (EsContinuidadCircular(izquierda, referencias[j].Patron))
return true;
}
}
return false;
}
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 EsTransbordoRealDesdeEstado(EstadoBusquedaParadaDbus estadoPrevio, PatronTransitoDbus patron)
{
if (estadoPrevio.BusesTomados == 0)
return false;
if (HayTransbordoPeatonalDesdeUltimoBus(estadoPrevio))
return true;
var movimientoBusAnterior = BuscarMovimientoBusAnterior(estadoPrevio);
return movimientoBusAnterior is null ||
UtilidadesTransitoDbus.EsTransbordoReal(movimientoBusAnterior.Patron.Variante, patron.Variante);
}
private static bool PuedeTomarPatronDesdeEstado(
EstadoBusquedaParadaDbus estadoPrevio,
PatronTransitoDbus patron,
out bool esTransbordoReal)
{
esTransbordoReal = EsTransbordoRealDesdeEstado(estadoPrevio, patron);
if (estadoPrevio.BusesTomados == 0 || esTransbordoReal)
return true;
return ContarContinuidadesNoRealesConsecutivas(estadoPrevio) < MaxContinuidadesCircularesExtra;
}
private static bool HayTransbordoPeatonalDesdeUltimoBus(EstadoBusquedaParadaDbus estado)
{
var cursor = estado;
while (cursor is not null)
{
if (cursor.Movimiento is MovimientoBusDbus)
return false;
if (cursor.Movimiento is MovimientoPieTransbordoDbus)
return true;
cursor = cursor.EstadoAnterior;
}
return false;
}
private static int ContarContinuidadesNoRealesConsecutivas(EstadoBusquedaParadaDbus estado)
{
var continuidades = 0;
MovimientoBusDbus? busPosterior = null;
var cursor = estado;
while (cursor is not null)
{
if (cursor.Movimiento is MovimientoPieTransbordoDbus)
return continuidades;
if (cursor.Movimiento is MovimientoBusDbus bus)
{
if (busPosterior is not null)
{
if (UtilidadesTransitoDbus.EsTransbordoReal(bus.Patron.Variante, busPosterior.Patron.Variante))
return continuidades;
continuidades++;
}
busPosterior = bus;
}
cursor = cursor.EstadoAnterior;
}
return continuidades;
}
private static MovimientoBusDbus? BuscarMovimientoBusAnterior(EstadoBusquedaParadaDbus? estado)
{
var cursor = estado;
while (cursor is not null)
{
if (cursor.Movimiento is MovimientoBusDbus bus)
return bus;
cursor = cursor.EstadoAnterior;
}
return null;
}
/// <summary>
/// Convierte rondas exactas en rutas candidatas deduplicadas.
/// </summary>
private static IReadOnlyList<ResultadoBusquedaRutaBusDbus> ConstruirResultados(
IReadOnlyList<Dictionary<string, EstadoBusquedaParadaDbus>> rondas,
IReadOnlyDictionary<string, ParadaCandidata> origenPorCodigo,
IReadOnlyDictionary<string, ParadaCandidata> destinoPorCodigo,
DateTime instanteSalida,
ParametrosPlanificadorRuta parametros)
{
var mejores = new Dictionary<string, ResultadoBusquedaRutaBusDbus>(StringComparer.OrdinalIgnoreCase);
foreach (var estadosRonda in rondas.Skip(1))
{
var estadosPorParada = IndexarEstadosPorParada(estadosRonda.Values);
foreach (var destino in destinoPorCodigo.Values)
{
foreach (var estadoDestino in ObtenerEstadosEnParada(estadosPorParada, destino.Parada.Codigo))
{
var tramosBus = NormalizarTramosBus(ConstruirTramosBus(estadoDestino));
if (tramosBus.Count == 0)
continue;
if (UtilidadesTransitoDbus.TieneContinuidadesNoRealesConsecutivasExcesivas(
tramosBus,
MaxContinuidadesCircularesExtra))
{
continue;
}
var numeroTransbordos = UtilidadesTransitoDbus.ContarTransbordosReales(tramosBus);
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 primeraParada = tramosBus[0].CodigoParadaInicio;
if (!origenPorCodigo.TryGetValue(primeraParada, out var origen))
continue;
var ruta = new RutaBusAlternativa
{
Tramos = tramosBus,
NumeroTransbordos = numeroTransbordos,
CodigosParadasTransbordo = ConstruirCodigosTransbordo(tramosBus)
};
var duracionEstimada = Math.Max(0, (estadoDestino.Llegada - instanteSalida).TotalSeconds) + destino.TiempoAPie;
var puntuacionCoste = UtilidadesTransitoDbus.CalcularCosteConPenalizacionesRankingSegundos(
duracionEstimada,
parametros,
numeroTransbordos,
origen.DistanciaAPie,
estadoDestino.DistanciaPieTransbordos,
destino.DistanciaAPie);
var resultado = new ResultadoBusquedaRutaBusDbus
{
ParadaOrigen = origen.Parada,
ParadaDestino = destino.Parada,
DistanciaPieInicio = origen.DistanciaAPie,
TiempoPieInicio = origen.TiempoAPie,
DistanciaPieFin = destino.DistanciaAPie,
TiempoPieFin = destino.TiempoAPie,
DistanciaPieTransbordos = estadoDestino.DistanciaPieTransbordos,
TiempoEsperaInicialSegundos = estadoDestino.TiempoEsperaInicialSegundos,
TiempoEsperaTransbordosSegundos = estadoDestino.TiempoEsperaTransbordosSegundos,
DuracionEstimadaSegundos = duracionEstimada,
PuntuacionCoste = puntuacionCoste,
RutaBus = ruta
};
var firma = UtilidadesTransitoDbus.FirmaRutaBus(ruta);
if (!mejores.TryGetValue(firma, out var actual) ||
ResultadoEsMejor(resultado, actual))
{
mejores[firma] = resultado;
}
}
}
}
return SeleccionarPoolBusqueda(
mejores.Values,
Math.Max(12, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 8));
}
/// <summary>
/// Conserva candidatas buenas por coste y un cupo por menor caminata.
/// </summary>
private static List<ResultadoBusquedaRutaBusDbus> SeleccionarPoolBusqueda(
IEnumerable<ResultadoBusquedaRutaBusDbus> candidatas,
int maxPorCoste)
{
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);
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));
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);
}
}
}
/// <summary>
/// Reconstruye los tramos de bus de una ruta.
/// </summary>
private static List<TramoBus> ConstruirTramosBus(EstadoBusquedaParadaDbus estadoDestino)
{
var movimientos = new Stack<MovimientoBusDbus>();
var cursor = estadoDestino;
while (cursor is not null)
{
if (cursor.Movimiento is MovimientoBusDbus movimientoBus)
movimientos.Push(movimientoBus);
cursor = cursor.EstadoAnterior;
}
var tramos = new List<TramoBus>(movimientos.Count);
while (movimientos.Count > 0)
{
var movimiento = movimientos.Pop();
tramos.Add(new TramoBus
{
Variante = movimiento.Patron.Variante,
IndiceInicio = movimiento.IndiceSubida,
IndiceFin = movimiento.IndiceBajada
});
}
return tramos;
}
/// <summary>
/// Fusiona tramos consecutivos que realmente pertenecen a la misma variante.
/// </summary>
private static List<TramoBus> NormalizarTramosBus(List<TramoBus> tramosOriginales)
{
if (tramosOriginales.Count <= 1)
return tramosOriginales;
var resultado = new List<TramoBus>(tramosOriginales.Count);
foreach (var tramo in tramosOriginales)
{
if (!tramo.EsValido)
continue;
if (resultado.Count == 0)
{
resultado.Add(tramo);
continue;
}
var ultimo = resultado[^1];
if (SonMismaVariante(ultimo, tramo) && tramo.IndiceInicio <= ultimo.IndiceFin)
{
ultimo.IndiceFin = Math.Max(ultimo.IndiceFin, tramo.IndiceFin);
continue;
}
if (SonMismaVariante(ultimo, tramo) && tramo.IndiceInicio >= ultimo.IndiceFin)
{
ultimo.IndiceFin = Math.Max(ultimo.IndiceFin, tramo.IndiceFin);
continue;
}
resultado.Add(tramo);
}
return resultado;
}
/// <summary>
/// Indica si dos tramos recorren la misma variante operativa.
/// </summary>
private static bool SonMismaVariante(TramoBus izquierda, TramoBus derecha)
{
return string.Equals(
UtilidadesTransitoDbus.LineaCodigoStr(izquierda.Variante),
UtilidadesTransitoDbus.LineaCodigoStr(derecha.Variante),
StringComparison.OrdinalIgnoreCase) &&
izquierda.Variante.CodigoItinerario == derecha.Variante.CodigoItinerario &&
string.Equals(
UtilidadesTransitoDbus.NormalizarSentidoCodigo(izquierda.Variante.Sentido?.Codigo),
UtilidadesTransitoDbus.NormalizarSentidoCodigo(derecha.Variante.Sentido?.Codigo),
StringComparison.OrdinalIgnoreCase) &&
izquierda.Variante.Variante.Codigo == derecha.Variante.Variante.Codigo;
}
/// <summary>
/// Reconstruye las paradas de transbordo visibles.
/// </summary>
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>
/// Decide si un patron entra en los filtros 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>
/// Intenta almacenar un estado si mejora al existente.
/// </summary>
private static bool IntentarActualizarEstado(
IDictionary<string, EstadoBusquedaParadaDbus> estados,
EstadoBusquedaParadaDbus nuevoEstado)
{
var clave = CrearClaveEstado(nuevoEstado);
if (!estados.TryGetValue(clave, out var actual) ||
EstadoEsMejor(nuevoEstado, actual))
{
estados[clave] = nuevoEstado;
return true;
}
return false;
}
private static void PodarEstadosPorParada(IDictionary<string, EstadoBusquedaParadaDbus> estados)
{
var conservar = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var grupo in estados.GroupBy(par => par.Value.CodigoParada, StringComparer.OrdinalIgnoreCase))
{
var seleccionados = new List<KeyValuePair<string, EstadoBusquedaParadaDbus>>(MaxEstadosPorParada);
var lista = grupo.ToList();
Agregar(lista
.OrderBy(par => par.Value.Llegada)
.ThenBy(par => par.Value.TiempoEsperaTotalSegundos)
.ThenBy(par => par.Value.DistanciaCaminadaTotal)
.First());
Agregar(lista
.OrderBy(par => par.Value.DistanciaCaminadaTotal)
.ThenBy(par => par.Value.Llegada)
.ThenBy(par => par.Value.TiempoEsperaTotalSegundos)
.First());
foreach (var representanteContexto in lista
.GroupBy(par => CrearContextoBusEstado(par.Value), StringComparer.OrdinalIgnoreCase)
.Select(contexto => contexto
.OrderBy(par => par.Value.Llegada)
.ThenBy(par => par.Value.TiempoEsperaTotalSegundos)
.ThenBy(par => par.Value.DistanciaCaminadaTotal)
.First())
.OrderBy(par => par.Value.Llegada)
.ThenBy(par => par.Value.DistanciaCaminadaTotal))
{
Agregar(representanteContexto);
if (seleccionados.Count >= MaxEstadosPorParada)
break;
}
foreach (var candidato in lista
.OrderBy(par => par.Value.Llegada)
.ThenBy(par => par.Value.TiempoEsperaTotalSegundos)
.ThenBy(par => par.Value.DistanciaCaminadaTotal))
{
Agregar(candidato);
if (seleccionados.Count >= MaxEstadosPorParada)
break;
}
foreach (var seleccionado in seleccionados)
conservar.Add(seleccionado.Key);
void Agregar(KeyValuePair<string, EstadoBusquedaParadaDbus> candidato)
{
if (seleccionados.Count >= MaxEstadosPorParada)
return;
if (seleccionados.Any(par => string.Equals(par.Key, candidato.Key, StringComparison.OrdinalIgnoreCase)))
return;
seleccionados.Add(candidato);
}
}
var eliminar = estados.Keys
.Where(clave => !conservar.Contains(clave))
.ToList();
foreach (var clave in eliminar)
estados.Remove(clave);
}
private static IEnumerable<EstadoBusquedaParadaDbus> ObtenerEstadosEnParada(
IReadOnlyDictionary<string, List<EstadoBusquedaParadaDbus>> estados,
string codigoParada)
{
return estados.TryGetValue(codigoParada, out var lista)
? lista
: Enumerable.Empty<EstadoBusquedaParadaDbus>();
}
private static Dictionary<string, List<EstadoBusquedaParadaDbus>> IndexarEstadosPorParada(
IEnumerable<EstadoBusquedaParadaDbus> estados)
{
var indice = new Dictionary<string, List<EstadoBusquedaParadaDbus>>(StringComparer.OrdinalIgnoreCase);
foreach (var estado in estados)
{
if (!indice.TryGetValue(estado.CodigoParada, out var lista))
{
lista = new List<EstadoBusquedaParadaDbus>();
indice[estado.CodigoParada] = lista;
}
lista.Add(estado);
}
return indice;
}
private static string CrearClaveEstado(EstadoBusquedaParadaDbus estado)
{
return $"{estado.CodigoParada}|b:{estado.BusesTomados}|ctx:{CrearContextoBusEstado(estado)}";
}
private static string CrearContextoBusEstado(EstadoBusquedaParadaDbus estado)
{
var movimientoBus = BuscarMovimientoBusAnterior(estado);
if (movimientoBus is null)
return "sin-bus";
var variante = movimientoBus.Patron.Variante;
var sentido = UtilidadesTransitoDbus.NormalizarSentidoCodigo(variante.Sentido?.Codigo);
return $"{UtilidadesTransitoDbus.LineaCodigoStr(variante)}.{variante.CodigoItinerario}-{sentido}|v:{variante.Variante.Codigo}";
}
/// <summary>
/// Compara dos estados de parada.
/// </summary>
private static bool EstadoEsMejor(EstadoBusquedaParadaDbus candidato, EstadoBusquedaParadaDbus actual)
{
var comparacionLlegada = candidato.Llegada.CompareTo(actual.Llegada);
if (comparacionLlegada != 0)
return comparacionLlegada < 0;
var comparacionEspera = candidato.TiempoEsperaTotalSegundos.CompareTo(actual.TiempoEsperaTotalSegundos);
if (comparacionEspera != 0)
return comparacionEspera < 0;
var comparacionTransbordos = candidato.TransbordosReales.CompareTo(actual.TransbordosReales);
if (comparacionTransbordos != 0)
return comparacionTransbordos < 0;
return candidato.DistanciaCaminadaTotal < actual.DistanciaCaminadaTotal;
}
/// <summary>
/// Compara dos resultados ya construidos.
/// </summary>
private static bool ResultadoEsMejor(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>
/// Obtiene una fecha a partir de segundos desde el inicio del dia.
/// </summary>
private static DateTime ObtenerFechaDesdeSegundos(DateTime fechaBase, int segundos)
=> fechaBase.Date.AddSeconds(segundos);
}
/// <summary>
/// Estado de una parada en la busqueda por rondas.
/// </summary>
public sealed class EstadoBusquedaParadaDbus
{
/// <summary>
/// Codigo de la parada.
/// </summary>
public string CodigoParada { get; init; } = "";
/// <summary>
/// Momento de llegada a la parada.
/// </summary>
public DateTime Llegada { get; init; }
/// <summary>
/// Distancia total caminada hasta este punto.
/// </summary>
public double DistanciaCaminadaTotal { get; init; }
/// <summary>
/// Distancia caminada solo en transbordos.
/// </summary>
public double DistanciaPieTransbordos { get; init; }
/// <summary>
/// Espera inicial acumulada.
/// </summary>
public double TiempoEsperaInicialSegundos { get; init; }
/// <summary>
/// Espera acumulada en transbordos.
/// </summary>
public double TiempoEsperaTransbordosSegundos { get; init; }
/// <summary>
/// Numero de buses tomados hasta llegar aqui.
/// </summary>
public int BusesTomados { get; init; }
/// <summary>
/// Numero de transbordos reales acumulados hasta llegar aqui.
/// </summary>
public int TransbordosReales { get; init; }
/// <summary>
/// Estado previo del que cuelga la reconstruccion.
/// </summary>
public EstadoBusquedaParadaDbus? EstadoAnterior { get; init; }
/// <summary>
/// Movimiento aplicado para llegar a este estado.
/// </summary>
public MovimientoBusquedaDbus? Movimiento { get; init; }
/// <summary>
/// Espera total acumulada.
/// </summary>
public double TiempoEsperaTotalSegundos => TiempoEsperaInicialSegundos + TiempoEsperaTransbordosSegundos;
}
/// <summary>
/// Movimiento abstracto dentro de la reconstruccion.
/// </summary>
public abstract class MovimientoBusquedaDbus
{
}
/// <summary>
/// Movimiento inicial a pie desde el origen.
/// </summary>
public sealed class MovimientoAccesoDbus : MovimientoBusquedaDbus
{
/// <summary>
/// Distancia del acceso.
/// </summary>
public double DistanciaMetros { get; init; }
/// <summary>
/// Duracion del acceso.
/// </summary>
public double DuracionSegundos { get; init; }
}
/// <summary>
/// Movimiento a pie entre dos paradas.
/// </summary>
public sealed class MovimientoPieTransbordoDbus : MovimientoBusquedaDbus
{
/// <summary>
/// Distancia del transbordo.
/// </summary>
public double DistanciaMetros { get; init; }
/// <summary>
/// Duracion del transbordo.
/// </summary>
public double DuracionSegundos { get; init; }
}
/// <summary>
/// Movimiento en bus sobre un patron concreto.
/// </summary>
public sealed class MovimientoBusDbus : MovimientoBusquedaDbus
{
/// <summary>
/// Patron recorrido.
/// </summary>
public PatronTransitoDbus Patron { get; init; } = default!;
/// <summary>
/// Indice de subida.
/// </summary>
public int IndiceSubida { get; init; }
/// <summary>
/// Indice de bajada.
/// </summary>
public int IndiceBajada { get; init; }
/// <summary>
/// Hora de salida del tramo.
/// </summary>
public DateTime Salida { get; init; }
/// <summary>
/// Hora de llegada del tramo.
/// </summary>
public DateTime Llegada { get; init; }
}
/// <summary>
/// Resultado intermedio del buscador temporal.
/// </summary>
public sealed class ResultadoBusquedaRutaBusDbus
{
/// <summary>
/// Parada de acceso inicial.
/// </summary>
public Parada ParadaOrigen { get; init; } = default!;
/// <summary>
/// Parada de salida final del bus.
/// </summary>
public Parada ParadaDestino { get; init; } = default!;
/// <summary>
/// Distancia a pie inicial.
/// </summary>
public double DistanciaPieInicio { get; init; }
/// <summary>
/// Tiempo a pie inicial.
/// </summary>
public double TiempoPieInicio { get; init; }
/// <summary>
/// Distancia a pie final.
/// </summary>
public double DistanciaPieFin { get; init; }
/// <summary>
/// Tiempo a pie final.
/// </summary>
public double TiempoPieFin { get; init; }
/// <summary>
/// Distancia peatonal en transbordos.
/// </summary>
public double DistanciaPieTransbordos { get; init; }
/// <summary>
/// Espera inicial acumulada.
/// </summary>
public double TiempoEsperaInicialSegundos { get; init; }
/// <summary>
/// Espera acumulada en transbordos.
/// </summary>
public double TiempoEsperaTransbordosSegundos { get; init; }
/// <summary>
/// Duracion estimada total desde el origen real.
/// </summary>
public double DuracionEstimadaSegundos { get; init; }
/// <summary>
/// Coste interno con penalizacion progresiva de caminata.
/// </summary>
public double PuntuacionCoste { get; init; }
/// <summary>
/// Ruta de bus reconstruida.
/// </summary>
public RutaBusAlternativa RutaBus { get; init; } = default!;
/// <summary>
/// Distancia peatonal total.
/// </summary>
public double DistanciaPieTotal => DistanciaPieInicio + DistanciaPieTransbordos + DistanciaPieFin;
}