v20260917 - actual se sustituya y original metido en el modal de derivadas
This commit is contained in:
@@ -238,8 +238,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
|
||||
if (!string.IsNullOrWhiteSpace(_itinerarioClicadoSubId) &&
|
||||
string.Equals(_itinerarioClicadoSubId, id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_itinerarioClicado = null;
|
||||
_itinerarioClicadoSubId = null;
|
||||
await CerrarPopupItinerarioAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +252,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
|
||||
/// </summary>
|
||||
private async Task CerrarTodasLasPreviewsAsync()
|
||||
{
|
||||
await LimpiarResaltadoTramoHorarioAsync();
|
||||
try { await JS.InvokeVoidAsync("rt.clearPreview"); } catch { }
|
||||
|
||||
_itPreviewActivos.Clear();
|
||||
@@ -356,6 +356,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
|
||||
}
|
||||
|
||||
_itinerarioClicado = encontrado;
|
||||
await CargarTramosHorariosItinerarioAsync(GetVarianteRealClicada());
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
{
|
||||
new RealTimeMap.BasemapLayer
|
||||
{
|
||||
url = "http://192.168.41.12/osm_tiles/{z}/{x}/{y}.png",
|
||||
url = "https://osm.tecnosis.net/osm_tiles/{z}/{x}/{y}.png",
|
||||
attribution = "© OpenStreetMap",
|
||||
title = "",
|
||||
detectRetina = true
|
||||
|
||||
@@ -51,8 +51,6 @@ public partial class PlanificadorRutas
|
||||
private ModoConfiguracionHistorial _modoConfiguracionHistorial = ModoConfiguracionHistorial.Actual;
|
||||
private bool _cambiandoParametrosHistorial = false;
|
||||
private string? _historialCompartidoPendienteEliminarId;
|
||||
private ConfiguracionHistorialRuta? _configuracionTemporalPreviaHistorial;
|
||||
private ConfiguracionHistorialRuta? _configuracionParametrosActualesHistorial;
|
||||
private Task<bool>? _tareaCargaHistorialLocal;
|
||||
private bool _historialLocalCargado = false;
|
||||
private bool _historialLocalCargando = false;
|
||||
@@ -150,8 +148,6 @@ public partial class PlanificadorRutas
|
||||
_mostrarInformacionDerivacionHistorial = false;
|
||||
_historialLocalCargando = false;
|
||||
StopRefrescoHistorialCompartido();
|
||||
// Cerrar el panel no debe separar la configuracion visible de la ruta calculada.
|
||||
// La configuracion previa solo se recupera al elegir expresamente "Actual".
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -918,22 +914,9 @@ public partial class PlanificadorRutas
|
||||
{
|
||||
var itemSeleccionado = ObtenerRegistroHistorialSeleccionado();
|
||||
|
||||
if (_modoConfiguracionHistorial == ModoConfiguracionHistorial.Actual &&
|
||||
modo != ModoConfiguracionHistorial.Actual)
|
||||
{
|
||||
CapturarConfiguracionTemporalAntesDeHistorial();
|
||||
_configuracionParametrosActualesHistorial = CrearConfiguracionLocal();
|
||||
}
|
||||
|
||||
_modoConfiguracionHistorial = modo;
|
||||
await PersistirModoConfiguracionHistorialAsync();
|
||||
|
||||
if (modo == ModoConfiguracionHistorial.Actual &&
|
||||
_configuracionParametrosActualesHistorial is not null)
|
||||
{
|
||||
AplicarConfiguracionLocal(_configuracionParametrosActualesHistorial);
|
||||
}
|
||||
|
||||
if (itemSeleccionado is null)
|
||||
return;
|
||||
|
||||
@@ -1072,7 +1055,8 @@ public partial class PlanificadorRutas
|
||||
|
||||
private string TextoReferenciaHistorialDerivado()
|
||||
{
|
||||
if (!_rutaActualDerivadaDeHistorial)
|
||||
var originalDerivacionActivo = EsOriginalDerivacionActivo;
|
||||
if (!_rutaActualDerivadaDeHistorial && !originalDerivacionActivo)
|
||||
return string.Empty;
|
||||
|
||||
var (nombre, indice) = _tipoReferenciaHistorial switch
|
||||
@@ -1087,6 +1071,9 @@ public partial class PlanificadorRutas
|
||||
if (indice < 0)
|
||||
return string.Empty;
|
||||
|
||||
if (originalDerivacionActivo)
|
||||
return $"Ruta original del Historial {nombre} n.º {indice + 1} cargada.";
|
||||
|
||||
var numeroDerivacion = _tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido &&
|
||||
_indicePasoDerivacionActivo >= 0 &&
|
||||
_indicePasoDerivacionActivo < _pasosDerivacionHistorial.Count
|
||||
@@ -1096,11 +1083,18 @@ public partial class PlanificadorRutas
|
||||
}
|
||||
|
||||
private bool PuedeMostrarInformacionDerivacionHistorial =>
|
||||
_rutaActualDerivadaDeHistorial &&
|
||||
(_rutaActualDerivadaDeHistorial || EsOriginalDerivacionActivo) &&
|
||||
_tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido &&
|
||||
_escenarioBaseDerivacionHistorial is not null &&
|
||||
_pasosDerivacionHistorial.Count > 0;
|
||||
|
||||
private bool EsOriginalDerivacionActivo =>
|
||||
!_rutaActualDerivadaDeHistorial &&
|
||||
_indicePasoDerivacionActivo == -1 &&
|
||||
_pasosDerivacionHistorial.Count > 0 &&
|
||||
_escenarioBaseDerivacionHistorial is not null &&
|
||||
ExisteReferenciaHistorialActual();
|
||||
|
||||
private int IndiceHistorialCompartidoReferencia =>
|
||||
_historialCompartido.FindIndex(item => string.Equals(
|
||||
item.Id,
|
||||
@@ -1113,12 +1107,19 @@ public partial class PlanificadorRutas
|
||||
? _pasosDerivacionHistorial[_indicePasoDerivacionSeleccionado]
|
||||
: null;
|
||||
|
||||
private RegistroHistorialRuta? EscenarioDerivacionSeleccionado =>
|
||||
_indicePasoDerivacionSeleccionado == -1
|
||||
? _escenarioBaseDerivacionHistorial
|
||||
: PasoDerivacionSeleccionado?.Escenario;
|
||||
|
||||
private void AbrirInformacionDerivacionHistorial()
|
||||
{
|
||||
if (!PuedeMostrarInformacionDerivacionHistorial)
|
||||
return;
|
||||
|
||||
_indicePasoDerivacionSeleccionado = _indicePasoDerivacionActivo >= 0
|
||||
_indicePasoDerivacionSeleccionado = EsOriginalDerivacionActivo
|
||||
? -1
|
||||
: _indicePasoDerivacionActivo >= 0
|
||||
? _indicePasoDerivacionActivo
|
||||
: _pasosDerivacionHistorial.Count - 1;
|
||||
_mostrarInformacionDerivacionHistorial = true;
|
||||
@@ -1131,7 +1132,7 @@ public partial class PlanificadorRutas
|
||||
|
||||
private void SeleccionarPasoDerivacionHistorial(int indice)
|
||||
{
|
||||
if (indice < 0 || indice >= _pasosDerivacionHistorial.Count)
|
||||
if (indice < -1 || indice >= _pasosDerivacionHistorial.Count)
|
||||
return;
|
||||
|
||||
_indicePasoDerivacionSeleccionado = indice;
|
||||
@@ -1140,28 +1141,46 @@ public partial class PlanificadorRutas
|
||||
private async Task CargarPasoDerivacionHistorialAsync()
|
||||
{
|
||||
if (_cargandoPasoDerivacionHistorial ||
|
||||
_indicePasoDerivacionSeleccionado < 0 ||
|
||||
_indicePasoDerivacionSeleccionado < -1 ||
|
||||
_indicePasoDerivacionSeleccionado >= _pasosDerivacionHistorial.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var indice = _indicePasoDerivacionSeleccionado;
|
||||
var paso = _pasosDerivacionHistorial[indice];
|
||||
var esOriginal = indice == -1;
|
||||
var paso = esOriginal ? null : _pasosDerivacionHistorial[indice];
|
||||
var escenario = esOriginal ? _escenarioBaseDerivacionHistorial : paso?.Escenario;
|
||||
if (escenario is null)
|
||||
return;
|
||||
|
||||
_cargandoPasoDerivacionHistorial = true;
|
||||
try
|
||||
{
|
||||
await AplicarRegistroHistorialAsync(ClonarEscenarioHistorial(paso.Escenario));
|
||||
await AplicarRegistroHistorialAsync(ClonarEscenarioHistorial(escenario));
|
||||
_indicePasoDerivacionActivo = indice;
|
||||
_indicePasoDerivacionSeleccionado = indice;
|
||||
_rutaActualDerivadaDeHistorial = ExisteReferenciaHistorialActual();
|
||||
_rutaActualDerivadaDeHistorial = !esOriginal && ExisteReferenciaHistorialActual();
|
||||
if (esOriginal)
|
||||
{
|
||||
_historialLocalSeleccionadoId = string.Empty;
|
||||
_historialCompartidoSeleccionadoId = _historialReferenciaId;
|
||||
}
|
||||
else
|
||||
{
|
||||
LimpiarSeleccionHistorial();
|
||||
}
|
||||
_mostrarInformacionDerivacionHistorial = false;
|
||||
MostrarMensajeHistorial($"Derivación n.º {paso.Numero} cargada.");
|
||||
MostrarMensajeHistorial(esOriginal
|
||||
? "Ruta original cargada."
|
||||
: $"Derivación n.º {paso!.Numero} cargada.");
|
||||
}
|
||||
catch
|
||||
{
|
||||
MostrarMensajeHistorial(
|
||||
$"No he podido cargar la Derivación n.º {paso.Numero}.",
|
||||
esOriginal
|
||||
? "No he podido cargar la ruta original."
|
||||
: $"No he podido cargar la Derivación n.º {paso!.Numero}.",
|
||||
esError: true);
|
||||
}
|
||||
finally
|
||||
@@ -1558,7 +1577,6 @@ public partial class PlanificadorRutas
|
||||
_aplicandoRegistroHistorial = true;
|
||||
try
|
||||
{
|
||||
CapturarConfiguracionTemporalAntesDeHistorial();
|
||||
var mostrarParadasAntes = _mostrarParadas;
|
||||
|
||||
_omitirRegistroHistorialEnSiguienteCalculo = true;
|
||||
@@ -1626,9 +1644,7 @@ public partial class PlanificadorRutas
|
||||
|
||||
// Los JSON anteriores no incluían una instantánea completa. Conservamos los
|
||||
// parámetros actuales para lo desconocido y superponemos todos los que sí viajaban.
|
||||
var configuracion = (_configuracionParametrosActualesHistorial ??
|
||||
_configuracionTemporalPreviaHistorial ??
|
||||
CrearConfiguracionLocal()).Clonar();
|
||||
var configuracion = CrearConfiguracionLocal();
|
||||
|
||||
configuracion.UsarHorariosTeoricos = item.UsarHorariosTeoricos;
|
||||
configuracion.UsarHoraSimulada = item.UsarHoraSimulada;
|
||||
@@ -1646,15 +1662,6 @@ public partial class PlanificadorRutas
|
||||
private ConfiguracionHistorialRuta CrearConfiguracionPredeterminadaParaHistorial()
|
||||
=> CrearConfiguracionPredeterminadaHistorial();
|
||||
|
||||
private void CapturarConfiguracionTemporalAntesDeHistorial()
|
||||
{
|
||||
if (_configuracionTemporalPreviaHistorial is not null)
|
||||
return;
|
||||
|
||||
_configuracionTemporalPreviaHistorial = CrearConfiguracionLocal();
|
||||
_configuracionParametrosActualesHistorial = CrearConfiguracionLocal();
|
||||
}
|
||||
|
||||
private RegistroHistorialRuta? ConstruirRegistroHistorialActual()
|
||||
{
|
||||
if (_coordenadasOrigen is null || _coordenadasDestino is null)
|
||||
|
||||
@@ -43,10 +43,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
private async Task QuitarTodosItinerarios()
|
||||
{
|
||||
await CerrarTodasLasPreviewsAsync();
|
||||
|
||||
// cerrar popup de itinerario al ocultar todo
|
||||
_itinerarioClicado = null;
|
||||
_itinerarioClicadoSubId = null;
|
||||
await CerrarPopupItinerarioAsync();
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
@@ -1506,7 +1503,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
private bool _elevPopupMinimizado = false;
|
||||
|
||||
|
||||
private const string _versionApp = "(v-20260917a)";
|
||||
private const string _versionApp = "(v-20260918a)";
|
||||
|
||||
|
||||
private double? _paradaElevIdee;
|
||||
@@ -1524,7 +1521,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
/// </summary>
|
||||
private async Task CerrarPopupsAsync()
|
||||
{
|
||||
_itinerarioClicado = null;
|
||||
await CerrarPopupItinerarioAsync();
|
||||
|
||||
await CerrarPopupParadaAsync();
|
||||
await CerrarInspectorAsync();
|
||||
|
||||
@@ -219,14 +219,13 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
|
||||
private List<ProximaSalidaVm> _nextDeps = new();
|
||||
|
||||
private bool HorariosTaxibusSimuladosVisibles
|
||||
=> _nextDeps.Any(salida => TaxibusDbus.EsExpedicionSimulada(salida.ExpId));
|
||||
private bool HorariosTaxibusVisibles
|
||||
=> _nextDeps.Any(salida =>
|
||||
TaxibusDbus.EsLinea(salida.RutaId) ||
|
||||
TaxibusDbus.EsLinea(salida.LineaTxt));
|
||||
|
||||
private string MensajeHorarioTaxibusSimuladoVisible
|
||||
=> TaxibusDbus.ObtenerMensajeHorarioSimulado(
|
||||
_nextDeps
|
||||
.Where(salida => TaxibusDbus.EsExpedicionSimulada(salida.ExpId))
|
||||
.SelectMany(salida => new[] { salida.RutaId, salida.LineaTxt }));
|
||||
private string MensajeHorarioTaxibusVisible
|
||||
=> TaxibusDbus.MensajeReserva;
|
||||
|
||||
private static string ObtenerAvisoTaxibus(AlternativaRuta alternativa)
|
||||
{
|
||||
@@ -239,14 +238,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
if (lineasTaxibus is not { Count: > 0 })
|
||||
return string.Empty;
|
||||
|
||||
var lineasSimuladas = lineasTaxibus
|
||||
.Where(linea => linea.HorarioSimulado)
|
||||
.Select(linea => linea.Codigo)
|
||||
.ToList();
|
||||
|
||||
return lineasSimuladas.Count > 0
|
||||
? TaxibusDbus.ObtenerMensajeHorarioSimulado(lineasSimuladas)
|
||||
: TaxibusDbus.MensajeReserva;
|
||||
return TaxibusDbus.MensajeReserva;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
185
RutasDBUS/Components/Pages/PlanificadorRutas.TramosHorarios.cs
Normal file
185
RutasDBUS/Components/Pages/PlanificadorRutas.TramosHorarios.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.JSInterop;
|
||||
using RutasDBUS.Modelos.RedBus;
|
||||
using RutasDBUS.Servicios.Horarios;
|
||||
|
||||
namespace RutasDBUS.Components.Pages;
|
||||
|
||||
public partial class PlanificadorRutas
|
||||
{
|
||||
private IReadOnlyList<ResumenTramoHorarioDbus> _tramosHorariosItinerario =
|
||||
Array.Empty<ResumenTramoHorarioDbus>();
|
||||
private bool _tramosHorariosItinerarioCargando;
|
||||
private string _tramosHorariosItinerarioMensaje = "";
|
||||
private DateOnly? _tramosHorariosItinerarioFecha;
|
||||
private int _expedicionesTramosHorarios;
|
||||
private int? _indiceTramoHorarioResaltado;
|
||||
private int _versionCargaTramosHorarios;
|
||||
|
||||
private async Task CargarTramosHorariosItinerarioAsync(InfoVariante? variante)
|
||||
{
|
||||
var version = ++_versionCargaTramosHorarios;
|
||||
_tramosHorariosItinerario = Array.Empty<ResumenTramoHorarioDbus>();
|
||||
_tramosHorariosItinerarioMensaje = "";
|
||||
_tramosHorariosItinerarioFecha = null;
|
||||
_expedicionesTramosHorarios = 0;
|
||||
_tramosHorariosItinerarioCargando = variante is not null;
|
||||
await LimpiarResaltadoTramoHorarioAsync();
|
||||
|
||||
if (variante is null)
|
||||
return;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
await Horarios.AsegurarCargaAsync(Env.ContentRootPath);
|
||||
var fecha = DateOnly.FromDateTime(AhoraLocal());
|
||||
var analisis = Horarios.AnalizarTramosItinerario(
|
||||
variante.Linea.IdRuta,
|
||||
variante.Linea.Codigo,
|
||||
variante.CodigoItinerario,
|
||||
NormalizarSentidoCodigo(variante.Sentido?.Codigo),
|
||||
variante.Variante.Codigo,
|
||||
fecha,
|
||||
toleranciaSegundos: 60);
|
||||
|
||||
if (version != _versionCargaTramosHorarios)
|
||||
return;
|
||||
|
||||
_tramosHorariosItinerario = analisis.Tramos;
|
||||
_tramosHorariosItinerarioFecha = analisis.Fecha;
|
||||
_expedicionesTramosHorarios = analisis.ExpedicionesAnalizadas;
|
||||
if (analisis.ExpedicionesAnalizadas == 0)
|
||||
{
|
||||
_tramosHorariosItinerarioMensaje = "No hay expediciones activas para esta fecha.";
|
||||
}
|
||||
else if (analisis.Tramos.Count == 0)
|
||||
{
|
||||
_tramosHorariosItinerarioMensaje =
|
||||
"No se han detectado tiempos repetidos entre paradas con el margen de 1 minuto.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (version == _versionCargaTramosHorarios)
|
||||
_tramosHorariosItinerarioMensaje = "No se han podido analizar los tramos: " + ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (version == _versionCargaTramosHorarios)
|
||||
{
|
||||
_tramosHorariosItinerarioCargando = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AlternarResaltadoTramoHorarioAsync(ResumenTramoHorarioDbus tramo)
|
||||
{
|
||||
if (_indiceTramoHorarioResaltado == tramo.IndiceTramo)
|
||||
{
|
||||
await LimpiarResaltadoTramoHorarioAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var variante = GetVarianteRealClicada();
|
||||
var indiceTrayecto = BuscarIndiceTrayecto(variante, tramo);
|
||||
if (variante is null ||
|
||||
indiceTrayecto < 0 ||
|
||||
indiceTrayecto >= variante.Variante.IdsTrayectos.Count ||
|
||||
!RedBusServicio.Trayectos.TryGetValue(
|
||||
variante.Variante.IdsTrayectos[indiceTrayecto],
|
||||
out var trayecto) ||
|
||||
trayecto.Posiciones.Count < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync(
|
||||
"rt.showItinerarySegmentHighlight",
|
||||
trayecto.Posiciones,
|
||||
new { color = "#facc15", haloColor = "#111827" });
|
||||
_indiceTramoHorarioResaltado = tramo.IndiceTramo;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_indiceTramoHorarioResaltado = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int BuscarIndiceTrayecto(
|
||||
InfoVariante? variante,
|
||||
ResumenTramoHorarioDbus tramo)
|
||||
{
|
||||
if (variante?.CodigosParadas is null || variante.CodigosParadas.Count < 2)
|
||||
return -1;
|
||||
|
||||
bool Coincide(int indice) =>
|
||||
indice >= 0 &&
|
||||
indice + 1 < variante.CodigosParadas.Count &&
|
||||
string.Equals(
|
||||
NormalizarCodigoParada(variante.CodigosParadas[indice]),
|
||||
NormalizarCodigoParada(tramo.CodigoParadaOrigen),
|
||||
StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(
|
||||
NormalizarCodigoParada(variante.CodigosParadas[indice + 1]),
|
||||
NormalizarCodigoParada(tramo.CodigoParadaDestino),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (Coincide(tramo.IndiceTramo))
|
||||
return tramo.IndiceTramo;
|
||||
|
||||
for (var indice = 0; indice < variante.CodigosParadas.Count - 1; indice++)
|
||||
{
|
||||
if (Coincide(indice))
|
||||
return indice;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private async Task LimpiarResaltadoTramoHorarioAsync()
|
||||
{
|
||||
_indiceTramoHorarioResaltado = null;
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("rt.clearItinerarySegmentHighlight");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// El mapa puede no estar inicializado todavía.
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CerrarPopupItinerarioAsync()
|
||||
{
|
||||
_versionCargaTramosHorarios++;
|
||||
await LimpiarResaltadoTramoHorarioAsync();
|
||||
_itinerarioClicado = null;
|
||||
_itinerarioClicadoSubId = null;
|
||||
_tramosHorariosItinerario = Array.Empty<ResumenTramoHorarioDbus>();
|
||||
_tramosHorariosItinerarioMensaje = "";
|
||||
_tramosHorariosItinerarioCargando = false;
|
||||
}
|
||||
|
||||
private static string FormatearTiempoTramoHorario(int segundos)
|
||||
{
|
||||
var tiempo = TimeSpan.FromSeconds(Math.Max(0, segundos));
|
||||
return tiempo.TotalHours >= 1
|
||||
? $"{(int)tiempo.TotalHours}:{tiempo.Minutes:00}:{tiempo.Seconds:00}"
|
||||
: $"{tiempo.Minutes:00}:{tiempo.Seconds:00}";
|
||||
}
|
||||
|
||||
private static string FormatearCodigoParadaTramoHorario(string? codigo)
|
||||
{
|
||||
var normalizado = NormalizarCodigoParada(codigo);
|
||||
return string.IsNullOrWhiteSpace(normalizado) ? "-" : $"P-{normalizado}";
|
||||
}
|
||||
|
||||
private static string FormatearFrecuenciaTramoHorario(ResumenTramoHorarioDbus tramo) =>
|
||||
$"{tramo.Repeticiones}/{tramo.ExpedicionesAnalizadas} " +
|
||||
$"({tramo.FrecuenciaPorcentaje.ToString("0.#", CultureInfo.CurrentCulture)} %)";
|
||||
}
|
||||
@@ -304,7 +304,7 @@
|
||||
aria-pressed="@todosItinerariosParadaPintados"
|
||||
disabled="@_procesandoTodosItinerariosParada"
|
||||
@onclick="ToggleTodosItinerariosParadaAsync">
|
||||
<span aria-hidden="true">👁</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7S2 12 2 12Z" /><circle cx="12" cy="12" r="3" /></svg>
|
||||
<span>@TextoBotonTodosItinerariosParada</span>
|
||||
</button>
|
||||
|
||||
@@ -317,10 +317,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (HorariosTaxibusSimuladosVisibles)
|
||||
@if (HorariosTaxibusVisibles)
|
||||
{
|
||||
<div class="taxibus-notice taxibus-notice--schedule" role="status">
|
||||
<strong>Taxibús:</strong> @MensajeHorarioTaxibusSimuladoVisible
|
||||
<strong>Taxibús:</strong> @MensajeHorarioTaxibusVisible
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@
|
||||
aria-label="@(itinerarioPintado ? "Ocultar itinerario" : "Ver itinerario")"
|
||||
aria-pressed="@itinerarioPintado"
|
||||
@onclick="() => ToggleItinerarioSalidaParadaAsync(x)">
|
||||
<span aria-hidden="true">👁</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7S2 12 2 12Z" /><circle cx="12" cy="12" r="3" /></svg>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
@@ -385,10 +385,66 @@
|
||||
<div><b>Recorrido:</b> @ConstruirTextoRecorridoVariante(vReal, incluirCodigosParada: true)</div>
|
||||
<div><b>Longitud:</b> @FormatearDistanciaVariante(vReal)</div>
|
||||
</div>
|
||||
|
||||
<section class="itinerary-segments">
|
||||
<div class="itinerary-segments__header">
|
||||
<strong>Tramos detectados</strong>
|
||||
<span class="itinerary-segments__tolerance">± 1 min</span>
|
||||
</div>
|
||||
|
||||
@if (_tramosHorariosItinerarioCargando)
|
||||
{
|
||||
<div class="itinerary-segments__status">Analizando horarios...</div>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(_tramosHorariosItinerarioMensaje))
|
||||
{
|
||||
<div class="itinerary-segments__status">@_tramosHorariosItinerarioMensaje</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="itinerary-segments__summary">
|
||||
@_tramosHorariosItinerario.Count tramo(s) · @_expedicionesTramosHorarios expedición(es)
|
||||
@if (_tramosHorariosItinerarioFecha is { } fechaTramos)
|
||||
{
|
||||
<span> · @fechaTramos.ToString("dd/MM/yyyy", CultureInfo.CurrentCulture)</span>
|
||||
}
|
||||
</div>
|
||||
<ol class="itinerary-segments__list">
|
||||
@foreach (var tramoHorario in _tramosHorariosItinerario)
|
||||
{
|
||||
var tramoHorarioActivo = _indiceTramoHorarioResaltado == tramoHorario.IndiceTramo;
|
||||
var tituloTiempoTramo = tramoHorario.DuracionMinimaSegundos == tramoHorario.DuracionMaximaSegundos
|
||||
? $"Tiempo repetido: {FormatearTiempoTramoHorario(tramoHorario.DuracionHabitualSegundos)}"
|
||||
: $"Intervalo agrupado: {FormatearTiempoTramoHorario(tramoHorario.DuracionMinimaSegundos)} - {FormatearTiempoTramoHorario(tramoHorario.DuracionMaximaSegundos)}";
|
||||
<li class="itinerary-segment-row @(tramoHorarioActivo ? "is-active" : "")">
|
||||
<span class="itinerary-segment-row__content">
|
||||
<span class="itinerary-segment-row__stops">
|
||||
@tramoHorario.NombreParadaOrigen (@FormatearCodigoParadaTramoHorario(tramoHorario.CodigoParadaOrigen))
|
||||
<span aria-hidden="true">→</span>
|
||||
@tramoHorario.NombreParadaDestino (@FormatearCodigoParadaTramoHorario(tramoHorario.CodigoParadaDestino))
|
||||
</span>
|
||||
<span class="itinerary-segment-row__meta" title="@tituloTiempoTramo">
|
||||
<span>Tiempo: @FormatearTiempoTramoHorario(tramoHorario.DuracionHabitualSegundos)</span>
|
||||
<span>Frecuencia: @FormatearFrecuenciaTramoHorario(tramoHorario)</span>
|
||||
</span>
|
||||
</span>
|
||||
<button type="button"
|
||||
class="stop-itinerary-toggle itinerary-segment-row__eye @(tramoHorarioActivo ? "is-active" : "")"
|
||||
title="@(tramoHorarioActivo ? "Ocultar tramo" : "Ver tramo")"
|
||||
aria-label="@(tramoHorarioActivo ? "Ocultar tramo" : "Ver tramo")"
|
||||
aria-pressed="@tramoHorarioActivo"
|
||||
@onclick="() => AlternarResaltadoTramoHorarioAsync(tramoHorario)">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7S2 12 2 12Z" /><circle cx="12" cy="12" r="3" /></svg>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button class="popup-parada-cerrar" @onclick="() => { _itinerarioClicado = null; _itinerarioClicadoSubId = null; }">
|
||||
<button class="popup-parada-cerrar" @onclick="CerrarPopupItinerarioAsync">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
@@ -1378,16 +1434,20 @@
|
||||
|
||||
@if (_mostrarInformacionDerivacionHistorial &&
|
||||
_escenarioBaseDerivacionHistorial is not null &&
|
||||
PasoDerivacionSeleccionado is { } pasoDerivacion)
|
||||
EscenarioDerivacionSeleccionado is { } escenarioDerivacionSeleccionado)
|
||||
{
|
||||
var esOriginalDerivacion = _indicePasoDerivacionSeleccionado == -1;
|
||||
var pasoDerivacion = PasoDerivacionSeleccionado;
|
||||
var cambiosRespectoOriginal = ObtenerCambiosDerivacion(
|
||||
_escenarioBaseDerivacionHistorial,
|
||||
pasoDerivacion.Escenario);
|
||||
var escenarioAnterior = ObtenerEscenarioAnteriorDerivacion(pasoDerivacion);
|
||||
var cambiosRespectoAnterior = ObtenerCambiosDerivacion(
|
||||
escenarioAnterior,
|
||||
pasoDerivacion.Escenario);
|
||||
var textoComparacionAnterior = pasoDerivacion.NumeroAnterior is null
|
||||
escenarioDerivacionSeleccionado);
|
||||
var escenarioAnterior = pasoDerivacion is null
|
||||
? null
|
||||
: ObtenerEscenarioAnteriorDerivacion(pasoDerivacion);
|
||||
var cambiosRespectoAnterior = pasoDerivacion is null
|
||||
? Array.Empty<CambioDerivacionHistorial>()
|
||||
: ObtenerCambiosDerivacion(escenarioAnterior, pasoDerivacion.Escenario);
|
||||
var textoComparacionAnterior = pasoDerivacion?.NumeroAnterior is null
|
||||
? "Respecto al Historial Compartido original"
|
||||
: $"Respecto a la Derivación n.º {pasoDerivacion.NumeroAnterior}";
|
||||
|
||||
@@ -1412,6 +1472,12 @@
|
||||
</div>
|
||||
|
||||
<div class="history-derivation-steps" aria-label="Derivaciones registradas">
|
||||
<button type="button"
|
||||
class="history-derivation-step @(_indicePasoDerivacionSeleccionado == -1 ? "history-derivation-step--selected" : "") @(EsOriginalDerivacionActivo ? "history-derivation-step--active" : "")"
|
||||
title="@(EsOriginalDerivacionActivo ? "Ruta original cargada actualmente" : "Ver ruta original")"
|
||||
@onclick="() => SeleccionarPasoDerivacionHistorial(-1)">
|
||||
Original
|
||||
</button>
|
||||
@foreach (var paso in _pasosDerivacionHistorial)
|
||||
{
|
||||
var indicePaso = paso.Numero - 1;
|
||||
@@ -1426,8 +1492,16 @@
|
||||
|
||||
<div class="history-derivation-selected">
|
||||
<div class="history-derivation-selected__summary">
|
||||
@if (esOriginalDerivacion)
|
||||
{
|
||||
<strong>Ruta original</strong>
|
||||
<span>@_escenarioBaseDerivacionHistorial.FechaCreacionUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss")</span>
|
||||
}
|
||||
else if (pasoDerivacion is not null)
|
||||
{
|
||||
<strong>Derivación n.º @pasoDerivacion.Numero</strong>
|
||||
<span>@pasoDerivacion.FechaUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss")</span>
|
||||
}
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn-ghost history-derivation-load"
|
||||
@@ -1437,10 +1511,23 @@
|
||||
? "Cargando..."
|
||||
: _indicePasoDerivacionSeleccionado == _indicePasoDerivacionActivo
|
||||
? "Cargada"
|
||||
: esOriginalDerivacion
|
||||
? "Cargar original"
|
||||
: "Cargar derivación")
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (esOriginalDerivacion)
|
||||
{
|
||||
<section class="history-derivation-comparison">
|
||||
<h6>Escenario original</h6>
|
||||
<div class="history-derivation-no-changes">
|
||||
Punto de partida de todas las derivaciones registradas.
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
else
|
||||
{
|
||||
<section class="history-derivation-comparison">
|
||||
<h6>Respecto al Historial Compartido original</h6>
|
||||
@if (cambiosRespectoOriginal.Count == 0)
|
||||
@@ -1471,8 +1558,9 @@
|
||||
}
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (pasoDerivacion.NumeroAnterior is not null)
|
||||
@if (pasoDerivacion?.NumeroAnterior is not null)
|
||||
{
|
||||
<section class="history-derivation-comparison">
|
||||
<h6>@textoComparacionAnterior</h6>
|
||||
|
||||
@@ -5,6 +5,28 @@ using RutasDBUS.Servicios.Importacion;
|
||||
|
||||
namespace RutasDBUS.Servicios.Horarios
|
||||
{
|
||||
public sealed record ResumenTramoHorarioDbus(
|
||||
int IndiceTramo,
|
||||
string CodigoParadaOrigen,
|
||||
string NombreParadaOrigen,
|
||||
string CodigoParadaDestino,
|
||||
string NombreParadaDestino,
|
||||
int DuracionHabitualSegundos,
|
||||
int DuracionMinimaSegundos,
|
||||
int DuracionMaximaSegundos,
|
||||
int Repeticiones,
|
||||
int ExpedicionesAnalizadas)
|
||||
{
|
||||
public double FrecuenciaPorcentaje => ExpedicionesAnalizadas <= 0
|
||||
? 0
|
||||
: Repeticiones * 100d / ExpedicionesAnalizadas;
|
||||
}
|
||||
|
||||
public sealed record AnalisisTramosHorarioDbus(
|
||||
DateOnly Fecha,
|
||||
int ExpedicionesAnalizadas,
|
||||
IReadOnlyList<ResumenTramoHorarioDbus> Tramos);
|
||||
|
||||
/// <summary>
|
||||
/// Lee el ZIP de importación de dBUS (CSV + opcional gtfs.zip) y expone:
|
||||
/// - Calendarios activos para una fecha
|
||||
@@ -280,6 +302,159 @@ namespace RutasDBUS.Servicios.Horarios
|
||||
return active;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resume los tiempos repetidos entre paradas consecutivas de una variante para una fecha.
|
||||
/// Dos duraciones pertenecen al mismo grupo cuando su diferencia total no supera la tolerancia.
|
||||
/// </summary>
|
||||
public AnalisisTramosHorarioDbus AnalizarTramosItinerario(
|
||||
string? idRuta,
|
||||
string? codigoLinea,
|
||||
int codigoItinerario,
|
||||
string? sentido,
|
||||
int codigoVariante,
|
||||
DateOnly fecha,
|
||||
int toleranciaSegundos = 60)
|
||||
{
|
||||
EnsureLoadedOrThrow();
|
||||
|
||||
toleranciaSegundos = Math.Max(0, toleranciaSegundos);
|
||||
var calendariosActivos = GetActiveCalendars(fecha);
|
||||
var idRutaNormalizado = NormalizarLinea(idRuta);
|
||||
var codigoLineaNormalizado = NormalizarLinea(codigoLinea);
|
||||
var itinerarioBuscado = codigoItinerario.ToString(CultureInfo.InvariantCulture);
|
||||
var sentidoBuscado = DireccionToSentidoBusqueda(sentido);
|
||||
|
||||
var expediciones = _expById.Values
|
||||
.Where(exp => exp.HSalida is not null)
|
||||
.Where(exp => calendariosActivos.Contains(exp.IdCal))
|
||||
.Where(exp => CoincideLineaBusqueda(exp, idRutaNormalizado, codigoLineaNormalizado))
|
||||
.Where(exp => string.Equals(
|
||||
ExtraerItinerarioDesdePatron(exp.IdRuta, exp.IdPatronExp),
|
||||
itinerarioBuscado,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
.Where(exp => sentidoBuscado == "?" || string.Equals(
|
||||
DireccionToSentido(exp.Direccion),
|
||||
sentidoBuscado,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
.Where(exp => CoincideCodigoVariante(exp.IdPatronExp, codigoVariante))
|
||||
.Where(exp => _stopTimesByExp.TryGetValue(exp.IdExp, out var tiempos) && tiempos.Count >= 2)
|
||||
.ToList();
|
||||
|
||||
var duracionesPorTramo = new Dictionary<(int Indice, int Origen, int Destino), List<int>>();
|
||||
foreach (var expedicion in expediciones)
|
||||
{
|
||||
var tiempos = _stopTimesByExp[expedicion.IdExp];
|
||||
for (var indice = 0; indice < tiempos.Count - 1; indice++)
|
||||
{
|
||||
var actual = tiempos[indice];
|
||||
var siguiente = tiempos[indice + 1];
|
||||
var duracion = CalcularDuracionEntreParadas(actual.Salida, siguiente.Llegada);
|
||||
var clave = (indice, actual.StopId, siguiente.StopId);
|
||||
|
||||
if (!duracionesPorTramo.TryGetValue(clave, out var duraciones))
|
||||
duracionesPorTramo[clave] = duraciones = new List<int>();
|
||||
|
||||
duraciones.Add(duracion);
|
||||
}
|
||||
}
|
||||
|
||||
var tramos = new List<ResumenTramoHorarioDbus>();
|
||||
foreach (var entrada in duracionesPorTramo.OrderBy(x => x.Key.Indice))
|
||||
{
|
||||
var grupo = ObtenerGrupoDuracionesDominante(entrada.Value, toleranciaSegundos);
|
||||
if (grupo.Count < 2)
|
||||
continue;
|
||||
|
||||
var origen = ObtenerDatosParada(entrada.Key.Origen);
|
||||
var destino = ObtenerDatosParada(entrada.Key.Destino);
|
||||
tramos.Add(new ResumenTramoHorarioDbus(
|
||||
entrada.Key.Indice,
|
||||
origen.Codigo,
|
||||
origen.Nombre,
|
||||
destino.Codigo,
|
||||
destino.Nombre,
|
||||
grupo.DuracionHabitual,
|
||||
grupo.Minimo,
|
||||
grupo.Maximo,
|
||||
grupo.Count,
|
||||
entrada.Value.Count));
|
||||
}
|
||||
|
||||
return new AnalisisTramosHorarioDbus(fecha, expediciones.Count, tramos);
|
||||
}
|
||||
|
||||
private static bool CoincideCodigoVariante(string? idPatronExp, int codigoVariante)
|
||||
{
|
||||
if (codigoVariante <= 0)
|
||||
return true;
|
||||
|
||||
if (int.TryParse(idPatronExp, NumberStyles.Integer, CultureInfo.InvariantCulture, out var codigoPatron))
|
||||
return codigoPatron == codigoVariante;
|
||||
|
||||
var match = Regex.Match(idPatronExp ?? "", @"(\d+)$");
|
||||
return match.Success &&
|
||||
int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out codigoPatron) &&
|
||||
codigoPatron == codigoVariante;
|
||||
}
|
||||
|
||||
private static int CalcularDuracionEntreParadas(TimeOnly salida, TimeOnly llegada)
|
||||
{
|
||||
var segundos = (int)Math.Round((llegada.ToTimeSpan() - salida.ToTimeSpan()).TotalSeconds);
|
||||
return segundos < 0 ? segundos + 24 * 60 * 60 : segundos;
|
||||
}
|
||||
|
||||
private (string Codigo, string Nombre) ObtenerDatosParada(int stopId)
|
||||
{
|
||||
if (!_stopById.TryGetValue(stopId, out var parada))
|
||||
{
|
||||
var codigoDesconocido = stopId.ToString(CultureInfo.InvariantCulture);
|
||||
return (codigoDesconocido, $"Parada {codigoDesconocido}");
|
||||
}
|
||||
|
||||
var codigo = string.IsNullOrWhiteSpace(parada.CodigoSms)
|
||||
? parada.Id.ToString(CultureInfo.InvariantCulture)
|
||||
: NormalizarCodigoParada(parada.CodigoSms);
|
||||
var nombre = string.IsNullOrWhiteSpace(parada.Nombre) ? $"Parada {codigo}" : parada.Nombre.Trim();
|
||||
return (codigo, nombre);
|
||||
}
|
||||
|
||||
private static (int DuracionHabitual, int Minimo, int Maximo, int Count)
|
||||
ObtenerGrupoDuracionesDominante(IReadOnlyCollection<int> duraciones, int toleranciaSegundos)
|
||||
{
|
||||
if (duraciones.Count == 0)
|
||||
return (0, 0, 0, 0);
|
||||
|
||||
var ordenadas = duraciones.OrderBy(x => x).ToArray();
|
||||
var mejorInicio = 0;
|
||||
var mejorFin = 0;
|
||||
var inicio = 0;
|
||||
|
||||
for (var fin = 0; fin < ordenadas.Length; fin++)
|
||||
{
|
||||
while (ordenadas[fin] - ordenadas[inicio] > toleranciaSegundos)
|
||||
inicio++;
|
||||
|
||||
var cantidadActual = fin - inicio + 1;
|
||||
var cantidadMejor = mejorFin - mejorInicio + 1;
|
||||
var amplitudActual = ordenadas[fin] - ordenadas[inicio];
|
||||
var amplitudMejor = ordenadas[mejorFin] - ordenadas[mejorInicio];
|
||||
if (cantidadActual > cantidadMejor ||
|
||||
(cantidadActual == cantidadMejor && amplitudActual < amplitudMejor))
|
||||
{
|
||||
mejorInicio = inicio;
|
||||
mejorFin = fin;
|
||||
}
|
||||
}
|
||||
|
||||
var cantidad = mejorFin - mejorInicio + 1;
|
||||
var indiceMediana = mejorInicio + (cantidad - 1) / 2;
|
||||
return (
|
||||
ordenadas[indiceMediana],
|
||||
ordenadas[mejorInicio],
|
||||
ordenadas[mejorFin],
|
||||
cantidad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Próximas salidas desde una parada (por stopId interno).
|
||||
/// Devuelve: (horaSalidaEnParada, expId, rutaId)
|
||||
|
||||
677
RutasDBUS/Servicios/Importacion/HorariosOficialesTaxibus.cs
Normal file
677
RutasDBUS/Servicios/Importacion/HorariosOficialesTaxibus.cs
Normal file
@@ -0,0 +1,677 @@
|
||||
using RutasDBUS.Modelos.Importacion;
|
||||
|
||||
namespace RutasDBUS.Servicios.Importacion;
|
||||
|
||||
/// <summary>
|
||||
/// Integra los horarios oficiales publicados por Dbus cuando el ZIP solo contiene
|
||||
/// las plantillas de recorrido de TB6 y TB7.
|
||||
/// Fuentes: https://dbus.eus/es/tb6-tb6-taxibus-ulia/ y
|
||||
/// https://dbus.eus/es/tb7-taxibus-atotxaerreka-paseo-del-faro/ (consultadas el 17/09/2026).
|
||||
/// </summary>
|
||||
internal static class HorariosOficialesTaxibus
|
||||
{
|
||||
private const string PrefijoCalendario = "TAXIBUS_OFICIAL_CAL_";
|
||||
private const string IdCalendarioTodosLosDias = PrefijoCalendario + "TODOS";
|
||||
private const string IdCalendarioLaborables = PrefijoCalendario + "LAB";
|
||||
private const string IdCalendarioSabados = PrefijoCalendario + "SAB";
|
||||
private const string IdCalendarioFestivos = PrefijoCalendario + "FEST";
|
||||
|
||||
private const int Activo = 2;
|
||||
private const int Inactivo = 1;
|
||||
|
||||
public static IReadOnlySet<string> AplicarSiFaltanHorarios(
|
||||
List<FilaCalendarioCsv> calendarios,
|
||||
List<FilaCalendarioExcepcionCsv> calendarioExcepciones,
|
||||
List<FilaExpedicionCsv> expediciones,
|
||||
IReadOnlyList<FilaRutaCsv> rutas,
|
||||
List<FilaTiempoParadaCsv> tiemposParada)
|
||||
{
|
||||
var rutasIntegradas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var calendariosOriginales = calendarios.ToList();
|
||||
var excepcionesOriginales = calendarioExcepciones.ToList();
|
||||
var rangoModelo = ObtenerRangoModelo(calendariosOriginales);
|
||||
var calendariosVerano = ObtenerCalendariosVerano(rangoModelo);
|
||||
var tiemposOriginalesPorExpedicion = tiemposParada
|
||||
.GroupBy(tiempo => tiempo.IdExp, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
grupo => grupo.Key,
|
||||
grupo => grupo.OrderBy(tiempo => tiempo.SecPar).ToList(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var ruta in rutas.Where(EsRutaTaxibus))
|
||||
{
|
||||
var expedicionesRuta = expediciones
|
||||
.Where(expedicion => string.Equals(
|
||||
expedicion.IdRuta,
|
||||
ruta.Id,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(expedicion => expedicion.Id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (expedicionesRuta.Count == 0 ||
|
||||
TieneHorarioOficial(expedicionesRuta, tiemposOriginalesPorExpedicion))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var plantillas = expedicionesRuta
|
||||
.Select(expedicion => new PlantillaTaxibus(
|
||||
expedicion,
|
||||
tiemposOriginalesPorExpedicion.TryGetValue(expedicion.Id, out var tiempos)
|
||||
? tiempos
|
||||
: new List<FilaTiempoParadaCsv>()))
|
||||
.Where(plantilla => plantilla.Tiempos.Count >= 2)
|
||||
.ToList();
|
||||
|
||||
if (plantillas.Count == 0 || !SonPlantillasCompatibles(ruta, plantillas))
|
||||
continue;
|
||||
|
||||
var idsPlantilla = expedicionesRuta
|
||||
.Select(expedicion => expedicion.Id)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
expediciones.RemoveAll(expedicion => idsPlantilla.Contains(expedicion.Id));
|
||||
tiemposParada.RemoveAll(tiempo => idsPlantilla.Contains(tiempo.IdExp));
|
||||
|
||||
if (TaxibusDbus.EsLineaTb6(ruta.Abreviatura) || TaxibusDbus.EsLineaTb6(ruta.Id))
|
||||
{
|
||||
CrearHorariosTb6(
|
||||
ruta,
|
||||
plantillas,
|
||||
calendariosVerano,
|
||||
expediciones,
|
||||
tiemposParada);
|
||||
}
|
||||
else
|
||||
{
|
||||
CrearHorariosTb7(
|
||||
ruta,
|
||||
plantillas,
|
||||
calendariosVerano,
|
||||
expediciones,
|
||||
tiemposParada);
|
||||
}
|
||||
|
||||
rutasIntegradas.Add(ruta.Id);
|
||||
}
|
||||
|
||||
if (rutasIntegradas.Count > 0)
|
||||
{
|
||||
ConfigurarCalendariosOficiales(
|
||||
calendarios,
|
||||
calendarioExcepciones,
|
||||
calendariosOriginales,
|
||||
excepcionesOriginales,
|
||||
rangoModelo,
|
||||
calendariosVerano);
|
||||
}
|
||||
|
||||
return rutasIntegradas;
|
||||
}
|
||||
|
||||
private static bool EsRutaTaxibus(FilaRutaCsv ruta)
|
||||
=> TaxibusDbus.EsLinea(ruta.Abreviatura) || TaxibusDbus.EsLinea(ruta.Id);
|
||||
|
||||
private static bool SonPlantillasCompatibles(
|
||||
FilaRutaCsv ruta,
|
||||
IReadOnlyCollection<PlantillaTaxibus> plantillas)
|
||||
{
|
||||
var primerasParadas = plantillas
|
||||
.Select(plantilla => plantilla.Tiempos[0].IdTiempoPar)
|
||||
.ToHashSet();
|
||||
|
||||
if (TaxibusDbus.EsLineaTb6(ruta.Abreviatura) || TaxibusDbus.EsLineaTb6(ruta.Id))
|
||||
return primerasParadas.SetEquals(new[] { 372, 521 });
|
||||
|
||||
return primerasParadas.SetEquals(new[] { 240, 599, 42, 625 });
|
||||
}
|
||||
|
||||
private static bool TieneHorarioOficial(
|
||||
IReadOnlyList<FilaExpedicionCsv> expediciones,
|
||||
IReadOnlyDictionary<string, List<FilaTiempoParadaCsv>> tiemposPorExpedicion)
|
||||
{
|
||||
foreach (var expedicion in expediciones)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(expedicion.IdCal) &&
|
||||
!expedicion.IdCal.Equals("0000", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (expedicion.HSalida is { } salida && salida != TimeOnly.MinValue)
|
||||
return true;
|
||||
|
||||
if (!tiemposPorExpedicion.TryGetValue(expedicion.Id, out var tiempos))
|
||||
continue;
|
||||
|
||||
if (tiempos.Any(tiempo =>
|
||||
EsHoraReal(tiempo.HoraLlegada) || EsHoraReal(tiempo.HoraSalida)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool EsHoraReal(TimeOnly? hora)
|
||||
=> hora is { } valor && valor != TimeOnly.MinValue;
|
||||
|
||||
private static void CrearHorariosTb6(
|
||||
FilaRutaCsv ruta,
|
||||
IReadOnlyList<PlantillaTaxibus> plantillas,
|
||||
IReadOnlyDictionary<int, string> calendariosVerano,
|
||||
ICollection<FilaExpedicionCsv> expediciones,
|
||||
ICollection<FilaTiempoParadaCsv> tiemposParada)
|
||||
{
|
||||
foreach (var plantilla in plantillas)
|
||||
{
|
||||
var primeraParada = plantilla.Tiempos[0].IdTiempoPar;
|
||||
var plan = ObtenerPlanRegular(primeraParada)!;
|
||||
|
||||
if (primeraParada == 372)
|
||||
{
|
||||
CrearSalidasCadaHora(
|
||||
ruta,
|
||||
plantilla,
|
||||
plan,
|
||||
7 * 60,
|
||||
21 * 60,
|
||||
IdCalendarioTodosLosDias,
|
||||
"REG",
|
||||
expediciones,
|
||||
tiemposParada);
|
||||
|
||||
foreach (var (anio, idCalendario) in calendariosVerano)
|
||||
{
|
||||
CrearExpedicion(
|
||||
ruta,
|
||||
plantilla,
|
||||
plan,
|
||||
22 * 60,
|
||||
idCalendario,
|
||||
$"VER_{anio}",
|
||||
expediciones,
|
||||
tiemposParada);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CrearSalidasCadaHora(
|
||||
ruta,
|
||||
plantilla,
|
||||
plan,
|
||||
7 * 60 + 40,
|
||||
21 * 60 + 40,
|
||||
IdCalendarioTodosLosDias,
|
||||
"REG",
|
||||
expediciones,
|
||||
tiemposParada);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CrearHorariosTb7(
|
||||
FilaRutaCsv ruta,
|
||||
IReadOnlyList<PlantillaTaxibus> plantillas,
|
||||
IReadOnlyDictionary<int, string> calendariosVerano,
|
||||
ICollection<FilaExpedicionCsv> expediciones,
|
||||
ICollection<FilaTiempoParadaCsv> tiemposParada)
|
||||
{
|
||||
foreach (var plantilla in plantillas)
|
||||
{
|
||||
var primeraParada = plantilla.Tiempos[0].IdTiempoPar;
|
||||
var plan = ObtenerPlanRegular(primeraParada)!;
|
||||
|
||||
switch (primeraParada)
|
||||
{
|
||||
case 240:
|
||||
CrearSalidasPorTipoDia(ruta, plantilla, plan, 7, 8, 9, 21, 15, expediciones, tiemposParada);
|
||||
foreach (var (anio, idCalendario) in calendariosVerano)
|
||||
{
|
||||
CrearExpedicion(
|
||||
ruta,
|
||||
plantilla,
|
||||
ObtenerPlanTb7VeranoAtotxaerreka(),
|
||||
22 * 60 + 15,
|
||||
idCalendario,
|
||||
$"VER_{anio}",
|
||||
expediciones,
|
||||
tiemposParada);
|
||||
}
|
||||
break;
|
||||
|
||||
case 599:
|
||||
CrearSalidasPorTipoDia(ruta, plantilla, plan, 7, 8, 9, 21, 30, expediciones, tiemposParada);
|
||||
break;
|
||||
|
||||
case 42:
|
||||
CrearSalidasPorTipoDia(ruta, plantilla, plan, 7, 8, 9, 20, 45, expediciones, tiemposParada);
|
||||
foreach (var (anio, idCalendario) in calendariosVerano)
|
||||
{
|
||||
CrearExpedicion(
|
||||
ruta,
|
||||
plantilla,
|
||||
plan,
|
||||
21 * 60 + 45,
|
||||
idCalendario,
|
||||
$"VER_{anio}",
|
||||
expediciones,
|
||||
tiemposParada);
|
||||
}
|
||||
break;
|
||||
|
||||
case 625:
|
||||
CrearSalidasPorTipoDia(ruta, plantilla, plan, 8, 9, 10, 21, 5, expediciones, tiemposParada);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CrearSalidasPorTipoDia(
|
||||
FilaRutaCsv ruta,
|
||||
PlantillaTaxibus plantilla,
|
||||
PlanHorario plan,
|
||||
int primeraHoraLaborable,
|
||||
int primeraHoraSabado,
|
||||
int primeraHoraFestivo,
|
||||
int ultimaHora,
|
||||
int minuto,
|
||||
ICollection<FilaExpedicionCsv> expediciones,
|
||||
ICollection<FilaTiempoParadaCsv> tiemposParada)
|
||||
{
|
||||
CrearSalidasCadaHora(
|
||||
ruta,
|
||||
plantilla,
|
||||
plan,
|
||||
primeraHoraLaborable * 60 + minuto,
|
||||
ultimaHora * 60 + minuto,
|
||||
IdCalendarioLaborables,
|
||||
"LAB",
|
||||
expediciones,
|
||||
tiemposParada);
|
||||
CrearSalidasCadaHora(
|
||||
ruta,
|
||||
plantilla,
|
||||
plan,
|
||||
primeraHoraSabado * 60 + minuto,
|
||||
ultimaHora * 60 + minuto,
|
||||
IdCalendarioSabados,
|
||||
"SAB",
|
||||
expediciones,
|
||||
tiemposParada);
|
||||
CrearSalidasCadaHora(
|
||||
ruta,
|
||||
plantilla,
|
||||
plan,
|
||||
primeraHoraFestivo * 60 + minuto,
|
||||
ultimaHora * 60 + minuto,
|
||||
IdCalendarioFestivos,
|
||||
"FEST",
|
||||
expediciones,
|
||||
tiemposParada);
|
||||
}
|
||||
|
||||
private static void CrearSalidasCadaHora(
|
||||
FilaRutaCsv ruta,
|
||||
PlantillaTaxibus plantilla,
|
||||
PlanHorario plan,
|
||||
int primerMinuto,
|
||||
int ultimoMinuto,
|
||||
string idCalendario,
|
||||
string serie,
|
||||
ICollection<FilaExpedicionCsv> expediciones,
|
||||
ICollection<FilaTiempoParadaCsv> tiemposParada)
|
||||
{
|
||||
for (var minuto = primerMinuto; minuto <= ultimoMinuto; minuto += 60)
|
||||
{
|
||||
CrearExpedicion(
|
||||
ruta,
|
||||
plantilla,
|
||||
plan,
|
||||
minuto,
|
||||
idCalendario,
|
||||
serie,
|
||||
expediciones,
|
||||
tiemposParada);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CrearExpedicion(
|
||||
FilaRutaCsv ruta,
|
||||
PlantillaTaxibus plantilla,
|
||||
PlanHorario plan,
|
||||
int minutoInicio,
|
||||
string idCalendario,
|
||||
string serie,
|
||||
ICollection<FilaExpedicionCsv> expediciones,
|
||||
ICollection<FilaTiempoParadaCsv> tiemposParada)
|
||||
{
|
||||
var tiemposPlantilla = RecortarTiempos(plantilla.Tiempos, plan.IdUltimaParada);
|
||||
var desplazamientos = CalcularDesplazamientos(tiemposPlantilla, plan);
|
||||
var horaInicio = HoraDesdeSegundos(minutoInicio * 60);
|
||||
var idExpedicion =
|
||||
$"{TaxibusDbus.PrefijoExpedicionHorarioOficial}{ruta.Id}_{plan.IdPrimeraParada}_{serie}_{minutoInicio:0000}";
|
||||
|
||||
expediciones.Add(new FilaExpedicionCsv(
|
||||
idExpedicion,
|
||||
plantilla.Expedicion.IdRuta,
|
||||
plan.Destino,
|
||||
plantilla.Expedicion.Direccion,
|
||||
plantilla.Expedicion.CambioExp,
|
||||
plantilla.Expedicion.IdPatronExp,
|
||||
horaInicio,
|
||||
idCalendario));
|
||||
|
||||
for (var indice = 0; indice < tiemposPlantilla.Count; indice++)
|
||||
{
|
||||
var tiempoPlantilla = tiemposPlantilla[indice];
|
||||
var horaPaso = HoraDesdeSegundos(minutoInicio * 60 + desplazamientos[indice]);
|
||||
|
||||
tiemposParada.Add(new FilaTiempoParadaCsv(
|
||||
idExpedicion,
|
||||
horaPaso,
|
||||
horaPaso,
|
||||
indice == tiemposPlantilla.Count - 1
|
||||
? 0
|
||||
: tiempoPlantilla.DistanciaSiguienteParada,
|
||||
tiempoPlantilla.IdTiempoPar,
|
||||
tiempoPlantilla.SecPar,
|
||||
tiempoPlantilla.TipoSubidaPar,
|
||||
tiempoPlantilla.TipoBajadaPar,
|
||||
tiempoPlantilla.NuevaDirDestinoPar));
|
||||
}
|
||||
}
|
||||
|
||||
private static List<FilaTiempoParadaCsv> RecortarTiempos(
|
||||
IReadOnlyList<FilaTiempoParadaCsv> tiempos,
|
||||
int idUltimaParada)
|
||||
{
|
||||
var indiceFinal = -1;
|
||||
for (var indice = 0; indice < tiempos.Count; indice++)
|
||||
{
|
||||
if (tiempos[indice].IdTiempoPar == idUltimaParada)
|
||||
{
|
||||
indiceFinal = indice;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return indiceFinal >= 0
|
||||
? tiempos.Take(indiceFinal + 1).ToList()
|
||||
: tiempos.ToList();
|
||||
}
|
||||
|
||||
private static int[] CalcularDesplazamientos(
|
||||
IReadOnlyList<FilaTiempoParadaCsv> tiempos,
|
||||
PlanHorario plan)
|
||||
{
|
||||
var acumuladas = new double[tiempos.Count];
|
||||
for (var indice = 1; indice < tiempos.Count; indice++)
|
||||
{
|
||||
acumuladas[indice] = acumuladas[indice - 1] +
|
||||
Math.Max(0, tiempos[indice - 1].DistanciaSiguienteParada);
|
||||
}
|
||||
|
||||
var anclas = new SortedDictionary<int, int>();
|
||||
foreach (var (idParada, minuto) in plan.AnclasMinutos)
|
||||
{
|
||||
for (var indice = 0; indice < tiempos.Count; indice++)
|
||||
{
|
||||
if (tiempos[indice].IdTiempoPar == idParada)
|
||||
{
|
||||
anclas[indice] = minuto * 60;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anclas[0] = 0;
|
||||
anclas[tiempos.Count - 1] = plan.DuracionMinutos * 60;
|
||||
|
||||
var desplazamientos = new int[tiempos.Count];
|
||||
var pares = anclas.ToList();
|
||||
for (var indiceAncla = 0; indiceAncla < pares.Count - 1; indiceAncla++)
|
||||
{
|
||||
var inicio = pares[indiceAncla];
|
||||
var fin = pares[indiceAncla + 1];
|
||||
var distanciaTramo = acumuladas[fin.Key] - acumuladas[inicio.Key];
|
||||
var numeroPasos = Math.Max(1, fin.Key - inicio.Key);
|
||||
|
||||
for (var indice = inicio.Key; indice <= fin.Key; indice++)
|
||||
{
|
||||
var proporcion = distanciaTramo > 0
|
||||
? (acumuladas[indice] - acumuladas[inicio.Key]) / distanciaTramo
|
||||
: (double)(indice - inicio.Key) / numeroPasos;
|
||||
desplazamientos[indice] = inicio.Value + (int)Math.Round(
|
||||
(fin.Value - inicio.Value) * proporcion,
|
||||
MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
|
||||
return desplazamientos;
|
||||
}
|
||||
|
||||
private static PlanHorario? ObtenerPlanRegular(int primeraParada)
|
||||
=> primeraParada switch
|
||||
{
|
||||
372 => new PlanHorario(372, 521, 20, "Albergue Ulia", new Dictionary<int, int>
|
||||
{
|
||||
[372] = 0,
|
||||
[346] = 10,
|
||||
[521] = 20
|
||||
}),
|
||||
521 => new PlanHorario(521, 372, 20, "Colón 21", new Dictionary<int, int>
|
||||
{
|
||||
[521] = 0,
|
||||
[457] = 10,
|
||||
[372] = 20
|
||||
}),
|
||||
240 => new PlanHorario(240, 599, 15, "Añorga", new Dictionary<int, int>
|
||||
{
|
||||
[240] = 0,
|
||||
[617] = 8,
|
||||
[599] = 15
|
||||
}),
|
||||
599 => new PlanHorario(599, 42, 15, "Antiguo", new Dictionary<int, int>
|
||||
{
|
||||
[599] = 0,
|
||||
[343] = 2,
|
||||
[42] = 15
|
||||
}),
|
||||
42 => new PlanHorario(42, 625, 20, "Paseo del Faro", new Dictionary<int, int>
|
||||
{
|
||||
[42] = 0,
|
||||
[625] = 20
|
||||
}),
|
||||
625 => new PlanHorario(625, 240, 10, "Antiguo", new Dictionary<int, int>
|
||||
{
|
||||
[625] = 0,
|
||||
[240] = 10
|
||||
}),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static PlanHorario ObtenerPlanTb7VeranoAtotxaerreka()
|
||||
=> new(240, 620, 11, "Atotxaerreka", new Dictionary<int, int>
|
||||
{
|
||||
[240] = 0,
|
||||
[617] = 8,
|
||||
[620] = 11
|
||||
});
|
||||
|
||||
private static void ConfigurarCalendariosOficiales(
|
||||
List<FilaCalendarioCsv> calendarios,
|
||||
List<FilaCalendarioExcepcionCsv> calendarioExcepciones,
|
||||
IReadOnlyList<FilaCalendarioCsv> calendariosOriginales,
|
||||
IReadOnlyList<FilaCalendarioExcepcionCsv> excepcionesOriginales,
|
||||
RangoFechas rangoModelo,
|
||||
IReadOnlyDictionary<int, string> calendariosVerano)
|
||||
{
|
||||
calendarios.RemoveAll(calendario =>
|
||||
calendario.Id.StartsWith(PrefijoCalendario, StringComparison.OrdinalIgnoreCase));
|
||||
calendarioExcepciones.RemoveAll(excepcion =>
|
||||
excepcion.Id.StartsWith(PrefijoCalendario, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
calendarios.Add(new FilaCalendarioCsv(
|
||||
IdCalendarioTodosLosDias,
|
||||
Activo, Activo, Activo, Activo, Activo, Activo, Activo,
|
||||
rangoModelo.Inicio,
|
||||
rangoModelo.Fin));
|
||||
calendarios.Add(new FilaCalendarioCsv(
|
||||
IdCalendarioLaborables,
|
||||
Activo, Activo, Activo, Activo, Activo, Inactivo, Inactivo,
|
||||
rangoModelo.Inicio,
|
||||
rangoModelo.Fin));
|
||||
calendarios.Add(new FilaCalendarioCsv(
|
||||
IdCalendarioSabados,
|
||||
Inactivo, Inactivo, Inactivo, Inactivo, Inactivo, Activo, Inactivo,
|
||||
rangoModelo.Inicio,
|
||||
rangoModelo.Fin));
|
||||
calendarios.Add(new FilaCalendarioCsv(
|
||||
IdCalendarioFestivos,
|
||||
Inactivo, Inactivo, Inactivo, Inactivo, Inactivo, Inactivo, Activo,
|
||||
rangoModelo.Inicio,
|
||||
rangoModelo.Fin));
|
||||
|
||||
foreach (var (anio, idCalendario) in calendariosVerano)
|
||||
{
|
||||
var inicio = Max(rangoModelo.Inicio, new DateOnly(anio, 6, 23));
|
||||
var fin = Min(rangoModelo.Fin, new DateOnly(anio, 8, 31));
|
||||
calendarios.Add(new FilaCalendarioCsv(
|
||||
idCalendario,
|
||||
Activo, Activo, Activo, Activo, Activo, Activo, Activo,
|
||||
inicio,
|
||||
fin));
|
||||
}
|
||||
|
||||
AgregarExcepcionesFestivos(
|
||||
calendarioExcepciones,
|
||||
calendariosOriginales,
|
||||
excepcionesOriginales,
|
||||
rangoModelo);
|
||||
}
|
||||
|
||||
private static void AgregarExcepcionesFestivos(
|
||||
ICollection<FilaCalendarioExcepcionCsv> destino,
|
||||
IReadOnlyList<FilaCalendarioCsv> calendariosOriginales,
|
||||
IReadOnlyList<FilaCalendarioExcepcionCsv> excepcionesOriginales,
|
||||
RangoFechas rangoModelo)
|
||||
{
|
||||
var calendariosPorId = calendariosOriginales
|
||||
.GroupBy(calendario => calendario.Id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(grupo => grupo.Key, grupo => grupo.First(), StringComparer.OrdinalIgnoreCase);
|
||||
var existentes = destino
|
||||
.Select(excepcion => (excepcion.Id, excepcion.FechaEx, excepcion.TipoExcepcion))
|
||||
.ToHashSet();
|
||||
|
||||
var festivosEntreSemana = excepcionesOriginales
|
||||
.Where(excepcion => excepcion.TipoExcepcion == 1 &&
|
||||
excepcion.FechaEx >= rangoModelo.Inicio &&
|
||||
excepcion.FechaEx <= rangoModelo.Fin)
|
||||
.GroupBy(excepcion => excepcion.FechaEx)
|
||||
.Where(grupo => grupo.Key.DayOfWeek != DayOfWeek.Sunday && grupo.Any(excepcion =>
|
||||
calendariosPorId.TryGetValue(excepcion.Id, out var calendario) &&
|
||||
calendario.Domingo == Activo &&
|
||||
ObtenerFlagDia(calendario, grupo.Key.DayOfWeek) != Activo))
|
||||
.Select(grupo => grupo.Key)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
foreach (var fecha in festivosEntreSemana)
|
||||
{
|
||||
var calendarioBase = fecha.DayOfWeek == DayOfWeek.Saturday
|
||||
? IdCalendarioSabados
|
||||
: IdCalendarioLaborables;
|
||||
AgregarExcepcion(destino, existentes, calendarioBase, fecha, 2);
|
||||
AgregarExcepcion(destino, existentes, IdCalendarioFestivos, fecha, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AgregarExcepcion(
|
||||
ICollection<FilaCalendarioExcepcionCsv> destino,
|
||||
ISet<(string Id, DateOnly FechaEx, int TipoExcepcion)> existentes,
|
||||
string idCalendario,
|
||||
DateOnly fecha,
|
||||
int tipo)
|
||||
{
|
||||
if (!existentes.Add((idCalendario, fecha, tipo)))
|
||||
return;
|
||||
|
||||
destino.Add(new FilaCalendarioExcepcionCsv(idCalendario, fecha, tipo));
|
||||
}
|
||||
|
||||
private static int ObtenerFlagDia(FilaCalendarioCsv calendario, DayOfWeek dia)
|
||||
=> dia switch
|
||||
{
|
||||
DayOfWeek.Monday => calendario.Lunes,
|
||||
DayOfWeek.Tuesday => calendario.Martes,
|
||||
DayOfWeek.Wednesday => calendario.Miercoles,
|
||||
DayOfWeek.Thursday => calendario.Jueves,
|
||||
DayOfWeek.Friday => calendario.Viernes,
|
||||
DayOfWeek.Saturday => calendario.Sabado,
|
||||
DayOfWeek.Sunday => calendario.Domingo,
|
||||
_ => Inactivo
|
||||
};
|
||||
|
||||
private static RangoFechas ObtenerRangoModelo(IReadOnlyList<FilaCalendarioCsv> calendarios)
|
||||
{
|
||||
var validos = calendarios
|
||||
.Where(calendario =>
|
||||
!calendario.Id.Equals("0000", StringComparison.OrdinalIgnoreCase) &&
|
||||
calendario.FechaInicio != default &&
|
||||
calendario.FechaFin >= calendario.FechaInicio)
|
||||
.ToList();
|
||||
|
||||
if (validos.Count == 0)
|
||||
{
|
||||
var anio = DateTime.Today.Year;
|
||||
return new RangoFechas(new DateOnly(anio, 1, 1), new DateOnly(anio, 12, 31));
|
||||
}
|
||||
|
||||
return new RangoFechas(
|
||||
validos.Min(calendario => calendario.FechaInicio),
|
||||
validos.Max(calendario => calendario.FechaFin));
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<int, string> ObtenerCalendariosVerano(RangoFechas rango)
|
||||
{
|
||||
var resultado = new Dictionary<int, string>();
|
||||
for (var anio = rango.Inicio.Year; anio <= rango.Fin.Year; anio++)
|
||||
{
|
||||
var inicioVerano = new DateOnly(anio, 6, 23);
|
||||
var finVerano = new DateOnly(anio, 8, 31);
|
||||
if (finVerano < rango.Inicio || inicioVerano > rango.Fin)
|
||||
continue;
|
||||
|
||||
resultado[anio] = $"{PrefijoCalendario}VERANO_{anio}";
|
||||
}
|
||||
|
||||
return resultado;
|
||||
}
|
||||
|
||||
private static DateOnly Max(DateOnly izquierda, DateOnly derecha)
|
||||
=> izquierda >= derecha ? izquierda : derecha;
|
||||
|
||||
private static DateOnly Min(DateOnly izquierda, DateOnly derecha)
|
||||
=> izquierda <= derecha ? izquierda : derecha;
|
||||
|
||||
private static TimeOnly HoraDesdeSegundos(int segundos)
|
||||
{
|
||||
var segundosDia = ((segundos % (24 * 3600)) + (24 * 3600)) % (24 * 3600);
|
||||
return TimeOnly.FromTimeSpan(TimeSpan.FromSeconds(segundosDia));
|
||||
}
|
||||
|
||||
private sealed record PlantillaTaxibus(
|
||||
FilaExpedicionCsv Expedicion,
|
||||
IReadOnlyList<FilaTiempoParadaCsv> Tiempos);
|
||||
|
||||
private sealed record PlanHorario(
|
||||
int IdPrimeraParada,
|
||||
int IdUltimaParada,
|
||||
int DuracionMinutos,
|
||||
string Destino,
|
||||
IReadOnlyDictionary<int, int> AnclasMinutos);
|
||||
|
||||
private sealed record RangoFechas(DateOnly Inicio, DateOnly Fin);
|
||||
}
|
||||
@@ -109,8 +109,9 @@ public sealed class ServicioArchivoImportacionDbus
|
||||
var tiemposReales = await ReadTiemposRealesAsync(FindEntry(zip, "tiempo_real.csv"), ct);
|
||||
var transbordos = await ReadTransbordosAsync(FindEntry(zip, "transbordos.csv"), ct);
|
||||
var vehiculos = await ReadVehiculosAsync(FindEntry(zip, "vehiculos.csv"), ct);
|
||||
var rutasConHorarioSimulado = SimuladorHorariosTaxibus.AplicarSiFaltanHorarios(
|
||||
HorariosOficialesTaxibus.AplicarSiFaltanHorarios(
|
||||
calendarios,
|
||||
calendarioEx,
|
||||
expediciones,
|
||||
rutas,
|
||||
tiemposParada);
|
||||
@@ -131,7 +132,6 @@ public sealed class ServicioArchivoImportacionDbus
|
||||
TiemposReales = tiemposReales,
|
||||
Transbordos = transbordos,
|
||||
Vehiculos = vehiculos,
|
||||
RutasConHorarioSimulado = rutasConHorarioSimulado,
|
||||
ParadasPorId = paradas.ToDictionary(x => x.Id),
|
||||
RutasPorId = rutas.ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase),
|
||||
ExpedicionesPorId = expediciones.ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase),
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
using RutasDBUS.Modelos.Importacion;
|
||||
|
||||
namespace RutasDBUS.Servicios.Importacion;
|
||||
|
||||
/// <summary>
|
||||
/// Completa las plantillas sin horario de TB6 y TB7 con una frecuencia provisional.
|
||||
/// </summary>
|
||||
internal static class SimuladorHorariosTaxibus
|
||||
{
|
||||
private const string IdCalendario = "TAXIBUS_SIM_TODOS_LOS_DIAS";
|
||||
private const int FrecuenciaMinutosTb6 = 10;
|
||||
private const int FrecuenciaMinutosRestoTaxibus = 30;
|
||||
private const int MinutoPrimeraSalida = 7 * 60;
|
||||
private const int MinutoFinServicioExclusivoTb6 = 22 * 60;
|
||||
private const int MinutoFinServicioExclusivoRestoTaxibus = 24 * 60;
|
||||
private const double VelocidadMetrosSegundo = 16_000.0 / 3_600.0;
|
||||
private const int SegundosMinimosEntreParadas = 30;
|
||||
|
||||
public static IReadOnlySet<string> AplicarSiFaltanHorarios(
|
||||
List<FilaCalendarioCsv> calendarios,
|
||||
List<FilaExpedicionCsv> expediciones,
|
||||
IReadOnlyList<FilaRutaCsv> rutas,
|
||||
List<FilaTiempoParadaCsv> tiemposParada)
|
||||
{
|
||||
var rutasSimuladas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var tiemposOriginalesPorExpedicion = tiemposParada
|
||||
.GroupBy(tiempo => tiempo.IdExp, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
grupo => grupo.Key,
|
||||
grupo => grupo.OrderBy(tiempo => tiempo.SecPar).ToList(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var ruta in rutas.Where(EsRutaTaxibus))
|
||||
{
|
||||
var expedicionesRuta = expediciones
|
||||
.Where(expedicion => string.Equals(
|
||||
expedicion.IdRuta,
|
||||
ruta.Id,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(expedicion => expedicion.Id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (expedicionesRuta.Count == 0 ||
|
||||
TieneHorarioOficial(expedicionesRuta, tiemposOriginalesPorExpedicion))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var plantillas = expedicionesRuta
|
||||
.Select(expedicion => new PlantillaTaxibus(
|
||||
expedicion,
|
||||
tiemposOriginalesPorExpedicion.TryGetValue(expedicion.Id, out var tiempos)
|
||||
? tiempos
|
||||
: new List<FilaTiempoParadaCsv>()))
|
||||
.Where(plantilla => plantilla.Tiempos.Count >= 2)
|
||||
.ToList();
|
||||
|
||||
if (plantillas.Count == 0)
|
||||
continue;
|
||||
|
||||
var idsPlantilla = expedicionesRuta
|
||||
.Select(expedicion => expedicion.Id)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
expediciones.RemoveAll(expedicion => idsPlantilla.Contains(expedicion.Id));
|
||||
tiemposParada.RemoveAll(tiempo => idsPlantilla.Contains(tiempo.IdExp));
|
||||
|
||||
for (var indicePlantilla = 0; indicePlantilla < plantillas.Count; indicePlantilla++)
|
||||
{
|
||||
var esTb6 = TaxibusDbus.EsLineaTb6(ruta.Abreviatura) ||
|
||||
TaxibusDbus.EsLineaTb6(ruta.Id);
|
||||
CrearExpedicionesSimuladas(
|
||||
ruta,
|
||||
plantillas[indicePlantilla],
|
||||
indicePlantilla,
|
||||
esTb6 ? FrecuenciaMinutosTb6 : FrecuenciaMinutosRestoTaxibus,
|
||||
esTb6 ? MinutoFinServicioExclusivoTb6 : MinutoFinServicioExclusivoRestoTaxibus,
|
||||
expediciones,
|
||||
tiemposParada);
|
||||
}
|
||||
|
||||
rutasSimuladas.Add(ruta.Id);
|
||||
}
|
||||
|
||||
if (rutasSimuladas.Count > 0)
|
||||
{
|
||||
calendarios.RemoveAll(calendario =>
|
||||
calendario.Id.Equals(IdCalendario, StringComparison.OrdinalIgnoreCase));
|
||||
calendarios.Add(new FilaCalendarioCsv(
|
||||
IdCalendario,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
new DateOnly(2000, 1, 1),
|
||||
new DateOnly(2099, 12, 31)));
|
||||
}
|
||||
|
||||
return rutasSimuladas;
|
||||
}
|
||||
|
||||
private static bool EsRutaTaxibus(FilaRutaCsv ruta)
|
||||
=> TaxibusDbus.EsLinea(ruta.Abreviatura) || TaxibusDbus.EsLinea(ruta.Id);
|
||||
|
||||
private static bool TieneHorarioOficial(
|
||||
IReadOnlyList<FilaExpedicionCsv> expediciones,
|
||||
IReadOnlyDictionary<string, List<FilaTiempoParadaCsv>> tiemposPorExpedicion)
|
||||
{
|
||||
foreach (var expedicion in expediciones)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(expedicion.IdCal) &&
|
||||
!expedicion.IdCal.Equals("0000", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (expedicion.HSalida is { } salida && salida != TimeOnly.MinValue)
|
||||
return true;
|
||||
|
||||
if (!tiemposPorExpedicion.TryGetValue(expedicion.Id, out var tiempos))
|
||||
continue;
|
||||
|
||||
if (tiempos.Any(tiempo =>
|
||||
EsHoraReal(tiempo.HoraLlegada) || EsHoraReal(tiempo.HoraSalida)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool EsHoraReal(TimeOnly? hora)
|
||||
=> hora is { } valor && valor != TimeOnly.MinValue;
|
||||
|
||||
private static void CrearExpedicionesSimuladas(
|
||||
FilaRutaCsv ruta,
|
||||
PlantillaTaxibus plantilla,
|
||||
int indicePlantilla,
|
||||
int frecuenciaMinutos,
|
||||
int minutoFinServicioExclusivo,
|
||||
ICollection<FilaExpedicionCsv> expediciones,
|
||||
ICollection<FilaTiempoParadaCsv> tiemposParada)
|
||||
{
|
||||
var indiceSalida = 0;
|
||||
for (var minutoInicio = MinutoPrimeraSalida;
|
||||
minutoInicio < minutoFinServicioExclusivo;
|
||||
minutoInicio += frecuenciaMinutos, indiceSalida++)
|
||||
{
|
||||
var segundosInicio = minutoInicio * 60;
|
||||
var horaInicio = HoraDesdeSegundos(segundosInicio);
|
||||
var idExpedicion =
|
||||
$"{TaxibusDbus.PrefijoExpedicionSimulada}{ruta.Id}_{indicePlantilla:00}_{indiceSalida:00}";
|
||||
|
||||
expediciones.Add(new FilaExpedicionCsv(
|
||||
idExpedicion,
|
||||
plantilla.Expedicion.IdRuta,
|
||||
plantilla.Expedicion.DirDestino,
|
||||
plantilla.Expedicion.Direccion,
|
||||
plantilla.Expedicion.CambioExp,
|
||||
plantilla.Expedicion.IdPatronExp,
|
||||
horaInicio,
|
||||
IdCalendario));
|
||||
|
||||
var segundosPaso = segundosInicio;
|
||||
for (var indiceParada = 0; indiceParada < plantilla.Tiempos.Count; indiceParada++)
|
||||
{
|
||||
var tiempoPlantilla = plantilla.Tiempos[indiceParada];
|
||||
var horaPaso = HoraDesdeSegundos(segundosPaso);
|
||||
|
||||
tiemposParada.Add(new FilaTiempoParadaCsv(
|
||||
idExpedicion,
|
||||
horaPaso,
|
||||
horaPaso,
|
||||
tiempoPlantilla.DistanciaSiguienteParada,
|
||||
tiempoPlantilla.IdTiempoPar,
|
||||
tiempoPlantilla.SecPar,
|
||||
tiempoPlantilla.TipoSubidaPar,
|
||||
tiempoPlantilla.TipoBajadaPar,
|
||||
tiempoPlantilla.NuevaDirDestinoPar));
|
||||
|
||||
if (indiceParada < plantilla.Tiempos.Count - 1)
|
||||
segundosPaso += CalcularSegundosEntreParadas(tiempoPlantilla.DistanciaSiguienteParada);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int CalcularSegundosEntreParadas(double distanciaMetros)
|
||||
{
|
||||
if (!double.IsFinite(distanciaMetros) || distanciaMetros <= 0)
|
||||
return SegundosMinimosEntreParadas;
|
||||
|
||||
return Math.Max(
|
||||
SegundosMinimosEntreParadas,
|
||||
(int)Math.Round(
|
||||
distanciaMetros / VelocidadMetrosSegundo,
|
||||
MidpointRounding.AwayFromZero));
|
||||
}
|
||||
|
||||
private static TimeOnly HoraDesdeSegundos(int segundos)
|
||||
{
|
||||
var segundosDia = ((segundos % (24 * 3600)) + (24 * 3600)) % (24 * 3600);
|
||||
return TimeOnly.FromTimeSpan(TimeSpan.FromSeconds(segundosDia));
|
||||
}
|
||||
|
||||
private sealed record PlantillaTaxibus(
|
||||
FilaExpedicionCsv Expedicion,
|
||||
IReadOnlyList<FilaTiempoParadaCsv> Tiempos);
|
||||
}
|
||||
@@ -5,12 +5,9 @@ namespace RutasDBUS.Servicios;
|
||||
/// </summary>
|
||||
public static class TaxibusDbus
|
||||
{
|
||||
public const string PrefijoExpedicionSimulada = "TAXIBUS_SIM_";
|
||||
public const string MensajeReserva = "Esta l\u00ednea requiere solicitud de RESERVA.";
|
||||
public const string MensajeHorarioSimuladoTb6 =
|
||||
"Horario provisional simulado cada 10 min, de 07:00 a 22:00. Esta l\u00ednea requiere solicitud de RESERVA.";
|
||||
public const string MensajeHorarioSimulado =
|
||||
"Horario provisional simulado cada 30 min, de 07:00 a 00:00. Esta l\u00ednea requiere solicitud de RESERVA.";
|
||||
public const string PrefijoExpedicionHorarioOficial = "TAXIBUS_OFICIAL_";
|
||||
public const string MensajeReserva =
|
||||
"Horario oficial. Esta l\u00ednea requiere solicitud de RESERVA.";
|
||||
|
||||
public static bool EsLinea(string? codigo)
|
||||
=> EsLineaTb6(codigo) || EsLineaTb7(codigo);
|
||||
@@ -22,23 +19,7 @@ public static class TaxibusDbus
|
||||
normalizado.Equals("6", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string ObtenerMensajeHorarioSimulado(IEnumerable<string?> codigosLinea)
|
||||
{
|
||||
var codigos = codigosLinea.Where(codigo => !string.IsNullOrWhiteSpace(codigo)).ToList();
|
||||
var mensajes = new List<string>(2);
|
||||
|
||||
if (codigos.Any(EsLineaTb6))
|
||||
mensajes.Add(MensajeHorarioSimuladoTb6);
|
||||
|
||||
if (codigos.Any(codigo => EsLineaTb7(codigo)))
|
||||
mensajes.Add(MensajeHorarioSimulado);
|
||||
|
||||
return mensajes.Count > 0
|
||||
? string.Join(" ", mensajes)
|
||||
: MensajeHorarioSimulado;
|
||||
}
|
||||
|
||||
private static bool EsLineaTb7(string? codigo)
|
||||
public static bool EsLineaTb7(string? codigo)
|
||||
{
|
||||
var normalizado = NormalizarCodigoLinea(codigo);
|
||||
return normalizado.Equals("TB7", StringComparison.OrdinalIgnoreCase) ||
|
||||
@@ -58,7 +39,7 @@ public static class TaxibusDbus
|
||||
return normalizado;
|
||||
}
|
||||
|
||||
public static bool EsExpedicionSimulada(string? idExpedicion)
|
||||
public static bool EsExpedicionHorarioOficial(string? idExpedicion)
|
||||
=> !string.IsNullOrWhiteSpace(idExpedicion) &&
|
||||
idExpedicion.StartsWith(PrefijoExpedicionSimulada, StringComparison.OrdinalIgnoreCase);
|
||||
idExpedicion.StartsWith(PrefijoExpedicionHorarioOficial, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -775,7 +775,93 @@ html, body {
|
||||
/* Itinerario: bajar texto para que no choque con la X */
|
||||
.popup-stack-left .popup-itinerario-body {
|
||||
padding-right: 2.2rem;
|
||||
max-height: min(62vh, 620px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.itinerary-segments {
|
||||
margin-top: .65rem;
|
||||
padding-top: .55rem;
|
||||
border-top: 1px solid rgba(148, 163, 184, .28);
|
||||
}
|
||||
|
||||
.itinerary-segments__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: .5rem;
|
||||
margin-bottom: .18rem;
|
||||
}
|
||||
|
||||
.itinerary-segments__tolerance {
|
||||
flex: 0 0 auto;
|
||||
padding: 2px 5px;
|
||||
border: 1px solid rgba(56, 189, 248, .45);
|
||||
border-radius: 5px;
|
||||
color: #bae6fd;
|
||||
font-size: .68rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.itinerary-segments__summary,
|
||||
.itinerary-segments__status {
|
||||
margin-bottom: .35rem;
|
||||
color: #94a3b8;
|
||||
font-size: .7rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.itinerary-segments__list {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.itinerary-segment-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 26px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 38px;
|
||||
padding: 4px 3px 4px 6px;
|
||||
border-left: 2px solid transparent;
|
||||
border-radius: 5px;
|
||||
background: rgba(30, 41, 59, .52);
|
||||
}
|
||||
|
||||
.itinerary-segment-row.is-active {
|
||||
border-left-color: #facc15;
|
||||
background: rgba(250, 204, 21, .11);
|
||||
}
|
||||
|
||||
.itinerary-segment-row__content,
|
||||
.itinerary-segment-row__stops,
|
||||
.itinerary-segment-row__meta {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.itinerary-segment-row__stops {
|
||||
color: #e2e8f0;
|
||||
font-size: .72rem;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.itinerary-segment-row__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .15rem .55rem;
|
||||
margin-top: 2px;
|
||||
color: #94a3b8;
|
||||
font-size: .67rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.itinerary-segment-row__eye {
|
||||
align-self: center;
|
||||
}
|
||||
.popup-parada-header {
|
||||
font-weight: 600;
|
||||
|
||||
@@ -1149,6 +1149,7 @@ window.rt.clearPreview = function () {
|
||||
if (!window.rt.ensurePreviewLayer()) return;
|
||||
window.rt._previewLayer.clearLayers();
|
||||
window.rt._previewLines = {};
|
||||
window.rt._itinerarySegmentHighlight = null;
|
||||
|
||||
// ✅ también limpiar paradas destacadas
|
||||
if (window.rt._itStopsLayer) {
|
||||
@@ -1157,6 +1158,58 @@ window.rt.clearPreview = function () {
|
||||
}
|
||||
};
|
||||
|
||||
window.rt.clearItinerarySegmentHighlight = function () {
|
||||
if (!window.rt.ensurePreviewLayer()) return;
|
||||
const highlight = window.rt._itinerarySegmentHighlight;
|
||||
if (!highlight) return;
|
||||
|
||||
try { window.rt._previewLayer.removeLayer(highlight); } catch { }
|
||||
window.rt._itinerarySegmentHighlight = null;
|
||||
};
|
||||
|
||||
window.rt.showItinerarySegmentHighlight = function (coords, options) {
|
||||
if (!window.rt.ensurePreviewLayer()) return;
|
||||
window.rt.clearItinerarySegmentHighlight();
|
||||
if (!Array.isArray(coords) || coords.length < 2) return;
|
||||
|
||||
const color = (options && options.color) || "#facc15";
|
||||
const haloColor = (options && options.haloColor) || "#111827";
|
||||
const group = window.L.layerGroup();
|
||||
const halo = window.L.polyline(coords, {
|
||||
color: haloColor,
|
||||
weight: 12,
|
||||
opacity: 0.92,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
interactive: false
|
||||
});
|
||||
const line = window.L.polyline(coords, {
|
||||
color,
|
||||
weight: 6,
|
||||
opacity: 1,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
interactive: false
|
||||
});
|
||||
|
||||
halo.addTo(group);
|
||||
line.addTo(group);
|
||||
[coords[0], coords[coords.length - 1]].forEach(function (point) {
|
||||
window.L.circleMarker(point, {
|
||||
radius: 5,
|
||||
color: haloColor,
|
||||
weight: 2,
|
||||
fillColor: color,
|
||||
fillOpacity: 1,
|
||||
interactive: false
|
||||
}).addTo(group);
|
||||
});
|
||||
|
||||
group.addTo(window.rt._previewLayer);
|
||||
try { halo.bringToFront(); line.bringToFront(); } catch { }
|
||||
window.rt._itinerarySegmentHighlight = group;
|
||||
};
|
||||
|
||||
window.rt.drawPreviewLine = function (id, coords, options) {
|
||||
if (!window.rt.ensurePreviewLayer()) return;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const TILE_CACHE = 'tile-cache-v1';
|
||||
const TILE_PREFIX = 'http://192.168.41.12/osm_tiles/';
|
||||
const TILE_PREFIX = 'https://osm.tecnosis.net/osm_tiles/';
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
self.skipWaiting();
|
||||
|
||||
@@ -5,9 +5,13 @@ using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using RutasDBUS.Components.Pages;
|
||||
using RutasDBUS.Modelos.Historial;
|
||||
using RutasDBUS.Modelos.Importacion;
|
||||
using RutasDBUS.Modelos.Planificacion;
|
||||
using RutasDBUS.Servicios;
|
||||
using RutasDBUS.Servicios.Caminata;
|
||||
using RutasDBUS.Servicios.Configuracion;
|
||||
using RutasDBUS.Servicios.Horarios;
|
||||
using RutasDBUS.Servicios.Importacion;
|
||||
using RutasDBUS.Servicios.Planificacion;
|
||||
|
||||
static void Check(bool result, string message)
|
||||
@@ -20,6 +24,213 @@ var baseMethod = typeof(PlanificadorRutas).GetMethod("CrearConfiguracionBaseHist
|
||||
var defaults = (ConfiguracionHistorialRuta)baseMethod.Invoke(null, null)!;
|
||||
Check(defaults.PesoTransbordoRankingMinutos == 6, "Penalizacion inicial de 6 minutos");
|
||||
|
||||
var calendariosTaxibus = new List<FilaCalendarioCsv>
|
||||
{
|
||||
new("LAB_BASE", 2, 2, 2, 2, 2, 1, 1, new DateOnly(2026, 6, 1), new DateOnly(2026, 12, 31)),
|
||||
new("FEST_BASE", 1, 1, 1, 1, 1, 1, 2, new DateOnly(2026, 6, 1), new DateOnly(2026, 12, 31))
|
||||
};
|
||||
var excepcionesTaxibus = new List<FilaCalendarioExcepcionCsv>
|
||||
{
|
||||
new("FEST_BASE", new DateOnly(2026, 10, 12), 1)
|
||||
};
|
||||
var rutasTaxibus = new List<FilaRutaCsv>
|
||||
{
|
||||
new("6", "TB6", "Ulia", "Ulia", "4", "", "94AF5D"),
|
||||
new("7", "TB7", "Atotxaerreka", "Atotxaerreka", "4", "", "63C600")
|
||||
};
|
||||
var expedicionesTaxibus = new List<FilaExpedicionCsv>
|
||||
{
|
||||
new("TB6_I", "6", "N.A.", 1, "6", "60101", null, "0000"),
|
||||
new("TB6_V", "6", "N.A.", 2, "6", "60102", null, "0000"),
|
||||
new("TB7_701_I", "7", "N.A.", 1, "7", "70101", null, "0000"),
|
||||
new("TB7_701_V", "7", "N.A.", 2, "7", "70102", null, "0000"),
|
||||
new("TB7_702_I", "7", "N.A.", 1, "7", "70201", null, "0000"),
|
||||
new("TB7_702_V", "7", "N.A.", 2, "7", "70202", null, "0000")
|
||||
};
|
||||
var tiemposTaxibus = new List<FilaTiempoParadaCsv>
|
||||
{
|
||||
new("TB6_I", TimeOnly.MinValue, TimeOnly.MinValue, 1000, 372, 1, "", "", ""),
|
||||
new("TB6_I", TimeOnly.MinValue, TimeOnly.MinValue, 1000, 346, 2, "", "", ""),
|
||||
new("TB6_I", TimeOnly.MinValue, TimeOnly.MinValue, 0, 521, 3, "", "", ""),
|
||||
new("TB6_V", TimeOnly.MinValue, TimeOnly.MinValue, 1000, 521, 1, "", "", ""),
|
||||
new("TB6_V", TimeOnly.MinValue, TimeOnly.MinValue, 1000, 457, 2, "", "", ""),
|
||||
new("TB6_V", TimeOnly.MinValue, TimeOnly.MinValue, 0, 372, 3, "", "", ""),
|
||||
new("TB7_701_I", TimeOnly.MinValue, TimeOnly.MinValue, 800, 240, 1, "", "", ""),
|
||||
new("TB7_701_I", TimeOnly.MinValue, TimeOnly.MinValue, 300, 617, 2, "", "", ""),
|
||||
new("TB7_701_I", TimeOnly.MinValue, TimeOnly.MinValue, 400, 620, 3, "", "", ""),
|
||||
new("TB7_701_I", TimeOnly.MinValue, TimeOnly.MinValue, 0, 599, 4, "", "", ""),
|
||||
new("TB7_701_V", TimeOnly.MinValue, TimeOnly.MinValue, 200, 599, 1, "", "", ""),
|
||||
new("TB7_701_V", TimeOnly.MinValue, TimeOnly.MinValue, 1300, 343, 2, "", "", ""),
|
||||
new("TB7_701_V", TimeOnly.MinValue, TimeOnly.MinValue, 0, 42, 3, "", "", ""),
|
||||
new("TB7_702_I", TimeOnly.MinValue, TimeOnly.MinValue, 2000, 42, 1, "", "", ""),
|
||||
new("TB7_702_I", TimeOnly.MinValue, TimeOnly.MinValue, 0, 625, 2, "", "", ""),
|
||||
new("TB7_702_V", TimeOnly.MinValue, TimeOnly.MinValue, 1500, 625, 1, "", "", ""),
|
||||
new("TB7_702_V", TimeOnly.MinValue, TimeOnly.MinValue, 0, 240, 2, "", "", "")
|
||||
};
|
||||
var integrarTaxibus = typeof(TaxibusDbus).Assembly
|
||||
.GetType("RutasDBUS.Servicios.Importacion.HorariosOficialesTaxibus")!
|
||||
.GetMethod("AplicarSiFaltanHorarios", BindingFlags.Static | BindingFlags.Public)!;
|
||||
var rutasTaxibusIntegradas = (IReadOnlySet<string>)integrarTaxibus.Invoke(
|
||||
null,
|
||||
new object[]
|
||||
{
|
||||
calendariosTaxibus,
|
||||
excepcionesTaxibus,
|
||||
expedicionesTaxibus,
|
||||
rutasTaxibus,
|
||||
tiemposTaxibus
|
||||
})!;
|
||||
Check(rutasTaxibusIntegradas.SetEquals(new[] { "6", "7" }),
|
||||
"TB6 y TB7 reciben sus horarios oficiales cuando faltan en el ZIP");
|
||||
Check(expedicionesTaxibus.Any(expedicion =>
|
||||
expedicion.IdRuta == "6" && expedicion.HSalida == new TimeOnly(7, 0)) &&
|
||||
expedicionesTaxibus.Any(expedicion =>
|
||||
expedicion.IdRuta == "6" && expedicion.HSalida == new TimeOnly(7, 40)),
|
||||
"TB6 sale a las horas oficiales desde Colon y Albergue");
|
||||
var salidaTb6Colon = expedicionesTaxibus.Single(expedicion =>
|
||||
expedicion.IdRuta == "6" &&
|
||||
expedicion.IdPatronExp == "60101" &&
|
||||
expedicion.HSalida == new TimeOnly(7, 0));
|
||||
Check(tiemposTaxibus.Any(tiempo =>
|
||||
tiempo.IdExp == salidaTb6Colon.Id &&
|
||||
tiempo.IdTiempoPar == 346 &&
|
||||
tiempo.HoraSalida == new TimeOnly(7, 10)) &&
|
||||
tiemposTaxibus.Any(tiempo =>
|
||||
tiempo.IdExp == salidaTb6Colon.Id &&
|
||||
tiempo.IdTiempoPar == 521 &&
|
||||
tiempo.HoraSalida == new TimeOnly(7, 20)) &&
|
||||
expedicionesTaxibus.Any(expedicion =>
|
||||
expedicion.IdPatronExp == "60101" &&
|
||||
expedicion.HSalida == new TimeOnly(22, 0)),
|
||||
"TB6 respeta los tiempos publicados y la salida adicional de verano");
|
||||
var salidaTb7Laborable = expedicionesTaxibus.Single(expedicion =>
|
||||
expedicion.IdRuta == "7" &&
|
||||
expedicion.IdPatronExp == "70101" &&
|
||||
expedicion.IdCal.EndsWith("LAB", StringComparison.Ordinal) &&
|
||||
expedicion.HSalida == new TimeOnly(7, 15));
|
||||
Check(tiemposTaxibus.Any(tiempo =>
|
||||
tiempo.IdExp == salidaTb7Laborable.Id &&
|
||||
tiempo.IdTiempoPar == 617 &&
|
||||
tiempo.HoraSalida == new TimeOnly(7, 23)) &&
|
||||
tiemposTaxibus.Any(tiempo =>
|
||||
tiempo.IdExp == salidaTb7Laborable.Id &&
|
||||
tiempo.IdTiempoPar == 599 &&
|
||||
tiempo.HoraSalida == new TimeOnly(7, 30)),
|
||||
"TB7 respeta las horas oficiales de Antiguo, Atotxaerreka y Anorga");
|
||||
var salidaTb7Anorga = expedicionesTaxibus.Single(expedicion =>
|
||||
expedicion.IdPatronExp == "70102" &&
|
||||
expedicion.IdCal.EndsWith("LAB", StringComparison.Ordinal) &&
|
||||
expedicion.HSalida == new TimeOnly(7, 30));
|
||||
var salidaTb7Faro = expedicionesTaxibus.Single(expedicion =>
|
||||
expedicion.IdPatronExp == "70201" &&
|
||||
expedicion.IdCal.EndsWith("LAB", StringComparison.Ordinal) &&
|
||||
expedicion.HSalida == new TimeOnly(7, 45));
|
||||
var vueltaTb7Faro = expedicionesTaxibus.Single(expedicion =>
|
||||
expedicion.IdPatronExp == "70202" &&
|
||||
expedicion.IdCal.EndsWith("LAB", StringComparison.Ordinal) &&
|
||||
expedicion.HSalida == new TimeOnly(8, 5));
|
||||
Check(tiemposTaxibus.Any(tiempo =>
|
||||
tiempo.IdExp == salidaTb7Anorga.Id &&
|
||||
tiempo.IdTiempoPar == 42 &&
|
||||
tiempo.HoraSalida == new TimeOnly(7, 45)) &&
|
||||
tiemposTaxibus.Any(tiempo =>
|
||||
tiempo.IdExp == salidaTb7Faro.Id &&
|
||||
tiempo.IdTiempoPar == 625 &&
|
||||
tiempo.HoraSalida == new TimeOnly(8, 5)) &&
|
||||
tiemposTaxibus.Any(tiempo =>
|
||||
tiempo.IdExp == vueltaTb7Faro.Id &&
|
||||
tiempo.IdTiempoPar == 240 &&
|
||||
tiempo.HoraSalida == new TimeOnly(8, 15)),
|
||||
"TB7 mantiene encadenado el ciclo Anorga, Antiguo y Paseo del Faro");
|
||||
Check(expedicionesTaxibus.Any(expedicion =>
|
||||
expedicion.IdPatronExp == "70101" &&
|
||||
expedicion.IdCal.EndsWith("SAB", StringComparison.Ordinal) &&
|
||||
expedicion.HSalida == new TimeOnly(8, 15)) &&
|
||||
expedicionesTaxibus.Any(expedicion =>
|
||||
expedicion.IdPatronExp == "70101" &&
|
||||
expedicion.IdCal.EndsWith("FEST", StringComparison.Ordinal) &&
|
||||
expedicion.HSalida == new TimeOnly(9, 15)),
|
||||
"TB7 diferencia laborables, sabados y festivos");
|
||||
Check(excepcionesTaxibus.Any(excepcion =>
|
||||
excepcion.Id.EndsWith("LAB", StringComparison.Ordinal) &&
|
||||
excepcion.FechaEx == new DateOnly(2026, 10, 12) &&
|
||||
excepcion.TipoExcepcion == 2) &&
|
||||
excepcionesTaxibus.Any(excepcion =>
|
||||
excepcion.Id.EndsWith("FEST", StringComparison.Ordinal) &&
|
||||
excepcion.FechaEx == new DateOnly(2026, 10, 12) &&
|
||||
excepcion.TipoExcepcion == 1),
|
||||
"Los festivos entre semana usan el horario festivo de TB7");
|
||||
var salidaVeranoAtotxaerreka = expedicionesTaxibus.Single(expedicion =>
|
||||
expedicion.IdPatronExp == "70101" &&
|
||||
expedicion.HSalida == new TimeOnly(22, 15));
|
||||
Check(tiemposTaxibus.Where(tiempo => tiempo.IdExp == salidaVeranoAtotxaerreka.Id)
|
||||
.OrderBy(tiempo => tiempo.SecPar)
|
||||
.Last().IdTiempoPar == 620 &&
|
||||
expedicionesTaxibus.Any(expedicion =>
|
||||
expedicion.IdPatronExp == "70201" &&
|
||||
expedicion.HSalida == new TimeOnly(21, 45)),
|
||||
"TB7 incluye las ampliaciones oficiales de verano sin prolongar el viaje de Atotxaerreka");
|
||||
|
||||
var expedicionesAnalisisTramos = new List<FilaExpedicionCsv>
|
||||
{
|
||||
new("E1", "28", "Amara", 1, "28", "280101", new TimeOnly(8, 0), "LAB"),
|
||||
new("E2", "28", "Amara", 1, "28", "280101", new TimeOnly(9, 0), "LAB"),
|
||||
new("E3", "28", "Amara", 1, "28", "280101", new TimeOnly(10, 0), "LAB"),
|
||||
new("E4", "28", "Amara", 1, "28", "280101", new TimeOnly(11, 0), "LAB"),
|
||||
new("PLANTILLA", "28", "Amara", 1, "28", "280101", null, "LAB"),
|
||||
new("OTRA_VARIANTE", "28", "Boulevard", 2, "28", "280102", new TimeOnly(8, 0), "LAB")
|
||||
};
|
||||
var tiemposAnalisisTramos = new List<FilaTiempoParadaCsv>
|
||||
{
|
||||
new("E1", new TimeOnly(8, 0), new TimeOnly(8, 0), 100, 1, 1, "", "", ""),
|
||||
new("E1", new TimeOnly(8, 2), new TimeOnly(8, 2), 100, 2, 2, "", "", ""),
|
||||
new("E1", new TimeOnly(8, 3), new TimeOnly(8, 3), 0, 3, 3, "", "", ""),
|
||||
new("E2", new TimeOnly(9, 0), new TimeOnly(9, 0), 100, 1, 1, "", "", ""),
|
||||
new("E2", new TimeOnly(9, 2, 30), new TimeOnly(9, 2, 30), 100, 2, 2, "", "", ""),
|
||||
new("E2", new TimeOnly(9, 3, 40), new TimeOnly(9, 3, 40), 0, 3, 3, "", "", ""),
|
||||
new("E3", new TimeOnly(10, 0), new TimeOnly(10, 0), 100, 1, 1, "", "", ""),
|
||||
new("E3", new TimeOnly(10, 2, 59), new TimeOnly(10, 2, 59), 100, 2, 2, "", "", ""),
|
||||
new("E3", new TimeOnly(10, 6, 9), new TimeOnly(10, 6, 9), 0, 3, 3, "", "", ""),
|
||||
new("E4", new TimeOnly(11, 0), new TimeOnly(11, 0), 100, 1, 1, "", "", ""),
|
||||
new("E4", new TimeOnly(11, 4, 10), new TimeOnly(11, 4, 10), 100, 2, 2, "", "", ""),
|
||||
new("E4", new TimeOnly(11, 5, 15), new TimeOnly(11, 5, 15), 0, 3, 3, "", "", "")
|
||||
};
|
||||
var archivoAnalisisTramos = new ArchivoImportacionDbus
|
||||
{
|
||||
Calendarios =
|
||||
[
|
||||
new FilaCalendarioCsv(
|
||||
"LAB", 2, 2, 2, 2, 2, 1, 1,
|
||||
new DateOnly(2026, 1, 1), new DateOnly(2026, 12, 31))
|
||||
],
|
||||
Expediciones = expedicionesAnalisisTramos,
|
||||
Rutas = [new FilaRutaCsv("28", "28", "Amara-Ospitaleak", "", "4", "", "F39E00")],
|
||||
Paradas =
|
||||
[
|
||||
new FilaParadaCsv(1, "101", "Origen", 0, 0, "", "", "", "", "", ""),
|
||||
new FilaParadaCsv(2, "102", "Intermedia", 0, 0, "", "", "", "", "", ""),
|
||||
new FilaParadaCsv(3, "103", "Destino", 0, 0, "", "", "", "", "", "")
|
||||
],
|
||||
TiemposParada = tiemposAnalisisTramos,
|
||||
TiemposParadaPorExpedicion = tiemposAnalisisTramos
|
||||
.GroupBy(x => x.IdExp, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => g.OrderBy(x => x.SecPar).ToList(),
|
||||
StringComparer.OrdinalIgnoreCase)
|
||||
};
|
||||
var servicioAnalisisTramos = new ServicioHorariosDbus(
|
||||
new FakeArchivoImportacionDbus(archivoAnalisisTramos));
|
||||
await servicioAnalisisTramos.AsegurarCargaAsync("");
|
||||
var analisisTramos = servicioAnalisisTramos.AnalizarTramosItinerario(
|
||||
"28", "28", 1, "I", 280101, new DateOnly(2026, 9, 17));
|
||||
Check(analisisTramos.ExpedicionesAnalizadas == 4 && analisisTramos.Tramos.Count == 2,
|
||||
"El analisis de tramos usa solo las expediciones activas de la variante exacta");
|
||||
Check(analisisTramos.Tramos[0].Repeticiones == 3 &&
|
||||
analisisTramos.Tramos[0].ExpedicionesAnalizadas == 4 &&
|
||||
analisisTramos.Tramos[0].DuracionHabitualSegundos == 150,
|
||||
"Los tiempos con hasta un minuto de diferencia forman la frecuencia dominante");
|
||||
|
||||
var panelSinCambios = new PlanificadorRutas();
|
||||
var alternarPanel = typeof(PlanificadorRutas).GetMethod(
|
||||
"ToggleConfigPanel",
|
||||
@@ -261,10 +472,6 @@ typeof(PlanificadorRutas).GetField("_mostrarHistorialRutas", flags)!
|
||||
.SetValue(paginaCierreHistorial, true);
|
||||
typeof(PlanificadorRutas).GetField("_factorEquilibradoAndarBus", flags)!
|
||||
.SetValue(paginaCierreHistorial, 99.0);
|
||||
var configuracionAnteriorHistorial = fabricaHistorial.Clonar();
|
||||
configuracionAnteriorHistorial.FactorEquilibradoAndarBus = 6;
|
||||
typeof(PlanificadorRutas).GetField("_configuracionTemporalPreviaHistorial", flags)!
|
||||
.SetValue(paginaCierreHistorial, configuracionAnteriorHistorial);
|
||||
var alternarHistorial = typeof(PlanificadorRutas).GetMethod("ToggleHistorialPanel", flags)!;
|
||||
await (Task)alternarHistorial.Invoke(paginaCierreHistorial, null)!;
|
||||
Check(
|
||||
@@ -386,6 +593,29 @@ sealed class FakeValhalla : IClienteValhalla
|
||||
}
|
||||
}
|
||||
|
||||
sealed class FakeArchivoImportacionDbus(ArchivoImportacionDbus archivo) : IServicioArchivoImportacionDbus
|
||||
{
|
||||
public Task<ArchivoImportacionDbus> ObtenerAsync(CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(archivo);
|
||||
|
||||
public Task<IReadOnlyList<ModeloHorarioDbusInfo>> ListarModelosAsync(
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult<IReadOnlyList<ModeloHorarioDbusInfo>>(Array.Empty<ModeloHorarioDbusInfo>());
|
||||
|
||||
public Task<ModeloHorarioDbusInfo?> ObtenerModeloActivoAsync(
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult<ModeloHorarioDbusInfo?>(null);
|
||||
|
||||
public Task SeleccionarModeloAsync(string idModelo, CancellationToken cancellationToken = default) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task<ModeloHorarioDbusInfo> GuardarModeloSubidoAsync(
|
||||
string nombreArchivo,
|
||||
Stream contenido,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
sealed class OsmWidthHandler : HttpMessageHandler
|
||||
{
|
||||
public int Calls { get; private set; }
|
||||
|
||||
Reference in New Issue
Block a user