1428 lines
54 KiB
C#
1428 lines
54 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 distanciaTotalBus = CalcularDistanciaBusEstado(estadoDestino);
|
|
var ruta = new RutaBusAlternativa
|
|
{
|
|
Tramos = tramosBus,
|
|
NumeroTransbordos = numeroTransbordos,
|
|
CodigosParadasTransbordo = ConstruirCodigosTransbordo(tramosBus),
|
|
DistanciaTotalBus = distanciaTotalBus
|
|
};
|
|
|
|
var duracionEstimada = Math.Max(0, (estadoDestino.Llegada - instanteSalida).TotalSeconds) + destino.TiempoAPie;
|
|
var distanciasPieTransbordos = ObtenerDistanciasPieTransbordos(estadoDestino);
|
|
var distanciasPiePorTramo = ConstruirDistanciasPiePorTramo(
|
|
origen.DistanciaAPie,
|
|
distanciasPieTransbordos,
|
|
destino.DistanciaAPie);
|
|
var puntuacionCoste = UtilidadesTransitoDbus.CalcularCosteConPenalizacionesRankingSegundos(
|
|
duracionEstimada,
|
|
parametros,
|
|
numeroTransbordos,
|
|
distanciasPiePorTramo.ToArray());
|
|
|
|
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,
|
|
DistanciasPieTransbordosPorTramo = distanciasPieTransbordos,
|
|
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),
|
|
parametros);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Conserva candidatas buenas por coste y un cupo por menor caminata.
|
|
/// </summary>
|
|
private static List<ResultadoBusquedaRutaBusDbus> SeleccionarPoolBusqueda(
|
|
IEnumerable<ResultadoBusquedaRutaBusDbus> candidatas,
|
|
int maxPorCoste,
|
|
ParametrosPlanificadorRuta parametros)
|
|
{
|
|
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);
|
|
|
|
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));
|
|
|
|
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 static double CalcularScoreBusquedaEquilibrado(
|
|
ResultadoBusquedaRutaBusDbus resultado,
|
|
ParametrosPlanificadorRuta parametros)
|
|
{
|
|
var distanciaBus = Math.Max(0, resultado.RutaBus.DistanciaTotalBus);
|
|
var distanciaAndando = UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaTotalMetros(
|
|
ConstruirDistanciasPiePorTramo(
|
|
resultado.DistanciaPieInicio,
|
|
resultado.DistanciasPieTransbordosPorTramo,
|
|
resultado.DistanciaPieFin),
|
|
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 List<double> ObtenerDistanciasPieTransbordos(EstadoBusquedaParadaDbus estadoDestino)
|
|
{
|
|
var distancias = new List<double>();
|
|
var cursor = estadoDestino;
|
|
while (cursor is not null)
|
|
{
|
|
if (cursor.Movimiento is MovimientoPieTransbordoDbus movimiento &&
|
|
double.IsFinite(movimiento.DistanciaMetros) &&
|
|
movimiento.DistanciaMetros > 0)
|
|
{
|
|
distancias.Add(movimiento.DistanciaMetros);
|
|
}
|
|
|
|
cursor = cursor.EstadoAnterior;
|
|
}
|
|
|
|
distancias.Reverse();
|
|
return distancias;
|
|
}
|
|
|
|
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 double CalcularDistanciaBusEstado(EstadoBusquedaParadaDbus estadoDestino)
|
|
{
|
|
var distancia = 0.0;
|
|
var cursor = estadoDestino;
|
|
while (cursor is not null)
|
|
{
|
|
if (cursor.Movimiento is MovimientoBusDbus movimiento)
|
|
distancia += CalcularDistanciaBusPatron(movimiento.Patron, movimiento.IndiceSubida, movimiento.IndiceBajada);
|
|
|
|
cursor = cursor.EstadoAnterior;
|
|
}
|
|
|
|
return double.IsFinite(distancia) && distancia > 0
|
|
? distancia
|
|
: 0;
|
|
}
|
|
|
|
private static double CalcularDistanciaBusPatron(PatronTransitoDbus patron, int indiceInicio, int indiceFin)
|
|
{
|
|
if (indiceInicio < 0 ||
|
|
indiceFin <= indiceInicio ||
|
|
indiceFin >= patron.DistanciaAcumuladaPorParadaMetros.Count)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var distancia = patron.DistanciaAcumuladaPorParadaMetros[indiceFin] -
|
|
patron.DistanciaAcumuladaPorParadaMetros[indiceInicio];
|
|
return double.IsFinite(distancia) && distancia > 0
|
|
? distancia
|
|
: 0;
|
|
}
|
|
|
|
/// <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>
|
|
/// Distancias peatonales de cada transbordo, sin agregarlas antes de penalizarlas.
|
|
/// </summary>
|
|
public IReadOnlyList<double> DistanciasPieTransbordosPorTramo { get; init; } = Array.Empty<double>();
|
|
|
|
/// <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;
|
|
}
|