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) &&
|
if (!string.IsNullOrWhiteSpace(_itinerarioClicadoSubId) &&
|
||||||
string.Equals(_itinerarioClicadoSubId, id, StringComparison.OrdinalIgnoreCase))
|
string.Equals(_itinerarioClicadoSubId, id, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_itinerarioClicado = null;
|
await CerrarPopupItinerarioAsync();
|
||||||
_itinerarioClicadoSubId = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +252,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CerrarTodasLasPreviewsAsync()
|
private async Task CerrarTodasLasPreviewsAsync()
|
||||||
{
|
{
|
||||||
|
await LimpiarResaltadoTramoHorarioAsync();
|
||||||
try { await JS.InvokeVoidAsync("rt.clearPreview"); } catch { }
|
try { await JS.InvokeVoidAsync("rt.clearPreview"); } catch { }
|
||||||
|
|
||||||
_itPreviewActivos.Clear();
|
_itPreviewActivos.Clear();
|
||||||
@@ -356,6 +356,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
_itinerarioClicado = encontrado;
|
_itinerarioClicado = encontrado;
|
||||||
|
await CargarTramosHorariosItinerarioAsync(GetVarianteRealClicada());
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
{
|
{
|
||||||
new RealTimeMap.BasemapLayer
|
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",
|
attribution = "© OpenStreetMap",
|
||||||
title = "",
|
title = "",
|
||||||
detectRetina = true
|
detectRetina = true
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ public partial class PlanificadorRutas
|
|||||||
private ModoConfiguracionHistorial _modoConfiguracionHistorial = ModoConfiguracionHistorial.Actual;
|
private ModoConfiguracionHistorial _modoConfiguracionHistorial = ModoConfiguracionHistorial.Actual;
|
||||||
private bool _cambiandoParametrosHistorial = false;
|
private bool _cambiandoParametrosHistorial = false;
|
||||||
private string? _historialCompartidoPendienteEliminarId;
|
private string? _historialCompartidoPendienteEliminarId;
|
||||||
private ConfiguracionHistorialRuta? _configuracionTemporalPreviaHistorial;
|
|
||||||
private ConfiguracionHistorialRuta? _configuracionParametrosActualesHistorial;
|
|
||||||
private Task<bool>? _tareaCargaHistorialLocal;
|
private Task<bool>? _tareaCargaHistorialLocal;
|
||||||
private bool _historialLocalCargado = false;
|
private bool _historialLocalCargado = false;
|
||||||
private bool _historialLocalCargando = false;
|
private bool _historialLocalCargando = false;
|
||||||
@@ -150,8 +148,6 @@ public partial class PlanificadorRutas
|
|||||||
_mostrarInformacionDerivacionHistorial = false;
|
_mostrarInformacionDerivacionHistorial = false;
|
||||||
_historialLocalCargando = false;
|
_historialLocalCargando = false;
|
||||||
StopRefrescoHistorialCompartido();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,22 +914,9 @@ public partial class PlanificadorRutas
|
|||||||
{
|
{
|
||||||
var itemSeleccionado = ObtenerRegistroHistorialSeleccionado();
|
var itemSeleccionado = ObtenerRegistroHistorialSeleccionado();
|
||||||
|
|
||||||
if (_modoConfiguracionHistorial == ModoConfiguracionHistorial.Actual &&
|
|
||||||
modo != ModoConfiguracionHistorial.Actual)
|
|
||||||
{
|
|
||||||
CapturarConfiguracionTemporalAntesDeHistorial();
|
|
||||||
_configuracionParametrosActualesHistorial = CrearConfiguracionLocal();
|
|
||||||
}
|
|
||||||
|
|
||||||
_modoConfiguracionHistorial = modo;
|
_modoConfiguracionHistorial = modo;
|
||||||
await PersistirModoConfiguracionHistorialAsync();
|
await PersistirModoConfiguracionHistorialAsync();
|
||||||
|
|
||||||
if (modo == ModoConfiguracionHistorial.Actual &&
|
|
||||||
_configuracionParametrosActualesHistorial is not null)
|
|
||||||
{
|
|
||||||
AplicarConfiguracionLocal(_configuracionParametrosActualesHistorial);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemSeleccionado is null)
|
if (itemSeleccionado is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1072,7 +1055,8 @@ public partial class PlanificadorRutas
|
|||||||
|
|
||||||
private string TextoReferenciaHistorialDerivado()
|
private string TextoReferenciaHistorialDerivado()
|
||||||
{
|
{
|
||||||
if (!_rutaActualDerivadaDeHistorial)
|
var originalDerivacionActivo = EsOriginalDerivacionActivo;
|
||||||
|
if (!_rutaActualDerivadaDeHistorial && !originalDerivacionActivo)
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
var (nombre, indice) = _tipoReferenciaHistorial switch
|
var (nombre, indice) = _tipoReferenciaHistorial switch
|
||||||
@@ -1087,6 +1071,9 @@ public partial class PlanificadorRutas
|
|||||||
if (indice < 0)
|
if (indice < 0)
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
|
if (originalDerivacionActivo)
|
||||||
|
return $"Ruta original del Historial {nombre} n.º {indice + 1} cargada.";
|
||||||
|
|
||||||
var numeroDerivacion = _tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido &&
|
var numeroDerivacion = _tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido &&
|
||||||
_indicePasoDerivacionActivo >= 0 &&
|
_indicePasoDerivacionActivo >= 0 &&
|
||||||
_indicePasoDerivacionActivo < _pasosDerivacionHistorial.Count
|
_indicePasoDerivacionActivo < _pasosDerivacionHistorial.Count
|
||||||
@@ -1096,11 +1083,18 @@ public partial class PlanificadorRutas
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool PuedeMostrarInformacionDerivacionHistorial =>
|
private bool PuedeMostrarInformacionDerivacionHistorial =>
|
||||||
_rutaActualDerivadaDeHistorial &&
|
(_rutaActualDerivadaDeHistorial || EsOriginalDerivacionActivo) &&
|
||||||
_tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido &&
|
_tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido &&
|
||||||
_escenarioBaseDerivacionHistorial is not null &&
|
_escenarioBaseDerivacionHistorial is not null &&
|
||||||
_pasosDerivacionHistorial.Count > 0;
|
_pasosDerivacionHistorial.Count > 0;
|
||||||
|
|
||||||
|
private bool EsOriginalDerivacionActivo =>
|
||||||
|
!_rutaActualDerivadaDeHistorial &&
|
||||||
|
_indicePasoDerivacionActivo == -1 &&
|
||||||
|
_pasosDerivacionHistorial.Count > 0 &&
|
||||||
|
_escenarioBaseDerivacionHistorial is not null &&
|
||||||
|
ExisteReferenciaHistorialActual();
|
||||||
|
|
||||||
private int IndiceHistorialCompartidoReferencia =>
|
private int IndiceHistorialCompartidoReferencia =>
|
||||||
_historialCompartido.FindIndex(item => string.Equals(
|
_historialCompartido.FindIndex(item => string.Equals(
|
||||||
item.Id,
|
item.Id,
|
||||||
@@ -1113,12 +1107,19 @@ public partial class PlanificadorRutas
|
|||||||
? _pasosDerivacionHistorial[_indicePasoDerivacionSeleccionado]
|
? _pasosDerivacionHistorial[_indicePasoDerivacionSeleccionado]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
private RegistroHistorialRuta? EscenarioDerivacionSeleccionado =>
|
||||||
|
_indicePasoDerivacionSeleccionado == -1
|
||||||
|
? _escenarioBaseDerivacionHistorial
|
||||||
|
: PasoDerivacionSeleccionado?.Escenario;
|
||||||
|
|
||||||
private void AbrirInformacionDerivacionHistorial()
|
private void AbrirInformacionDerivacionHistorial()
|
||||||
{
|
{
|
||||||
if (!PuedeMostrarInformacionDerivacionHistorial)
|
if (!PuedeMostrarInformacionDerivacionHistorial)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_indicePasoDerivacionSeleccionado = _indicePasoDerivacionActivo >= 0
|
_indicePasoDerivacionSeleccionado = EsOriginalDerivacionActivo
|
||||||
|
? -1
|
||||||
|
: _indicePasoDerivacionActivo >= 0
|
||||||
? _indicePasoDerivacionActivo
|
? _indicePasoDerivacionActivo
|
||||||
: _pasosDerivacionHistorial.Count - 1;
|
: _pasosDerivacionHistorial.Count - 1;
|
||||||
_mostrarInformacionDerivacionHistorial = true;
|
_mostrarInformacionDerivacionHistorial = true;
|
||||||
@@ -1131,7 +1132,7 @@ public partial class PlanificadorRutas
|
|||||||
|
|
||||||
private void SeleccionarPasoDerivacionHistorial(int indice)
|
private void SeleccionarPasoDerivacionHistorial(int indice)
|
||||||
{
|
{
|
||||||
if (indice < 0 || indice >= _pasosDerivacionHistorial.Count)
|
if (indice < -1 || indice >= _pasosDerivacionHistorial.Count)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_indicePasoDerivacionSeleccionado = indice;
|
_indicePasoDerivacionSeleccionado = indice;
|
||||||
@@ -1140,28 +1141,46 @@ public partial class PlanificadorRutas
|
|||||||
private async Task CargarPasoDerivacionHistorialAsync()
|
private async Task CargarPasoDerivacionHistorialAsync()
|
||||||
{
|
{
|
||||||
if (_cargandoPasoDerivacionHistorial ||
|
if (_cargandoPasoDerivacionHistorial ||
|
||||||
_indicePasoDerivacionSeleccionado < 0 ||
|
_indicePasoDerivacionSeleccionado < -1 ||
|
||||||
_indicePasoDerivacionSeleccionado >= _pasosDerivacionHistorial.Count)
|
_indicePasoDerivacionSeleccionado >= _pasosDerivacionHistorial.Count)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var indice = _indicePasoDerivacionSeleccionado;
|
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;
|
_cargandoPasoDerivacionHistorial = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await AplicarRegistroHistorialAsync(ClonarEscenarioHistorial(paso.Escenario));
|
await AplicarRegistroHistorialAsync(ClonarEscenarioHistorial(escenario));
|
||||||
_indicePasoDerivacionActivo = indice;
|
_indicePasoDerivacionActivo = indice;
|
||||||
_indicePasoDerivacionSeleccionado = indice;
|
_indicePasoDerivacionSeleccionado = indice;
|
||||||
_rutaActualDerivadaDeHistorial = ExisteReferenciaHistorialActual();
|
_rutaActualDerivadaDeHistorial = !esOriginal && ExisteReferenciaHistorialActual();
|
||||||
|
if (esOriginal)
|
||||||
|
{
|
||||||
|
_historialLocalSeleccionadoId = string.Empty;
|
||||||
|
_historialCompartidoSeleccionadoId = _historialReferenciaId;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LimpiarSeleccionHistorial();
|
||||||
|
}
|
||||||
_mostrarInformacionDerivacionHistorial = false;
|
_mostrarInformacionDerivacionHistorial = false;
|
||||||
MostrarMensajeHistorial($"Derivación n.º {paso.Numero} cargada.");
|
MostrarMensajeHistorial(esOriginal
|
||||||
|
? "Ruta original cargada."
|
||||||
|
: $"Derivación n.º {paso!.Numero} cargada.");
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
MostrarMensajeHistorial(
|
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);
|
esError: true);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -1558,7 +1577,6 @@ public partial class PlanificadorRutas
|
|||||||
_aplicandoRegistroHistorial = true;
|
_aplicandoRegistroHistorial = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CapturarConfiguracionTemporalAntesDeHistorial();
|
|
||||||
var mostrarParadasAntes = _mostrarParadas;
|
var mostrarParadasAntes = _mostrarParadas;
|
||||||
|
|
||||||
_omitirRegistroHistorialEnSiguienteCalculo = true;
|
_omitirRegistroHistorialEnSiguienteCalculo = true;
|
||||||
@@ -1626,9 +1644,7 @@ public partial class PlanificadorRutas
|
|||||||
|
|
||||||
// Los JSON anteriores no incluían una instantánea completa. Conservamos los
|
// 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.
|
// parámetros actuales para lo desconocido y superponemos todos los que sí viajaban.
|
||||||
var configuracion = (_configuracionParametrosActualesHistorial ??
|
var configuracion = CrearConfiguracionLocal();
|
||||||
_configuracionTemporalPreviaHistorial ??
|
|
||||||
CrearConfiguracionLocal()).Clonar();
|
|
||||||
|
|
||||||
configuracion.UsarHorariosTeoricos = item.UsarHorariosTeoricos;
|
configuracion.UsarHorariosTeoricos = item.UsarHorariosTeoricos;
|
||||||
configuracion.UsarHoraSimulada = item.UsarHoraSimulada;
|
configuracion.UsarHoraSimulada = item.UsarHoraSimulada;
|
||||||
@@ -1646,15 +1662,6 @@ public partial class PlanificadorRutas
|
|||||||
private ConfiguracionHistorialRuta CrearConfiguracionPredeterminadaParaHistorial()
|
private ConfiguracionHistorialRuta CrearConfiguracionPredeterminadaParaHistorial()
|
||||||
=> CrearConfiguracionPredeterminadaHistorial();
|
=> CrearConfiguracionPredeterminadaHistorial();
|
||||||
|
|
||||||
private void CapturarConfiguracionTemporalAntesDeHistorial()
|
|
||||||
{
|
|
||||||
if (_configuracionTemporalPreviaHistorial is not null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_configuracionTemporalPreviaHistorial = CrearConfiguracionLocal();
|
|
||||||
_configuracionParametrosActualesHistorial = CrearConfiguracionLocal();
|
|
||||||
}
|
|
||||||
|
|
||||||
private RegistroHistorialRuta? ConstruirRegistroHistorialActual()
|
private RegistroHistorialRuta? ConstruirRegistroHistorialActual()
|
||||||
{
|
{
|
||||||
if (_coordenadasOrigen is null || _coordenadasDestino is null)
|
if (_coordenadasOrigen is null || _coordenadasDestino is null)
|
||||||
|
|||||||
@@ -43,10 +43,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
private async Task QuitarTodosItinerarios()
|
private async Task QuitarTodosItinerarios()
|
||||||
{
|
{
|
||||||
await CerrarTodasLasPreviewsAsync();
|
await CerrarTodasLasPreviewsAsync();
|
||||||
|
await CerrarPopupItinerarioAsync();
|
||||||
// cerrar popup de itinerario al ocultar todo
|
|
||||||
_itinerarioClicado = null;
|
|
||||||
_itinerarioClicadoSubId = null;
|
|
||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
@@ -1506,7 +1503,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
private bool _elevPopupMinimizado = false;
|
private bool _elevPopupMinimizado = false;
|
||||||
|
|
||||||
|
|
||||||
private const string _versionApp = "(v-20260917a)";
|
private const string _versionApp = "(v-20260918a)";
|
||||||
|
|
||||||
|
|
||||||
private double? _paradaElevIdee;
|
private double? _paradaElevIdee;
|
||||||
@@ -1524,7 +1521,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CerrarPopupsAsync()
|
private async Task CerrarPopupsAsync()
|
||||||
{
|
{
|
||||||
_itinerarioClicado = null;
|
await CerrarPopupItinerarioAsync();
|
||||||
|
|
||||||
await CerrarPopupParadaAsync();
|
await CerrarPopupParadaAsync();
|
||||||
await CerrarInspectorAsync();
|
await CerrarInspectorAsync();
|
||||||
|
|||||||
@@ -219,14 +219,13 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
|
|
||||||
private List<ProximaSalidaVm> _nextDeps = new();
|
private List<ProximaSalidaVm> _nextDeps = new();
|
||||||
|
|
||||||
private bool HorariosTaxibusSimuladosVisibles
|
private bool HorariosTaxibusVisibles
|
||||||
=> _nextDeps.Any(salida => TaxibusDbus.EsExpedicionSimulada(salida.ExpId));
|
=> _nextDeps.Any(salida =>
|
||||||
|
TaxibusDbus.EsLinea(salida.RutaId) ||
|
||||||
|
TaxibusDbus.EsLinea(salida.LineaTxt));
|
||||||
|
|
||||||
private string MensajeHorarioTaxibusSimuladoVisible
|
private string MensajeHorarioTaxibusVisible
|
||||||
=> TaxibusDbus.ObtenerMensajeHorarioSimulado(
|
=> TaxibusDbus.MensajeReserva;
|
||||||
_nextDeps
|
|
||||||
.Where(salida => TaxibusDbus.EsExpedicionSimulada(salida.ExpId))
|
|
||||||
.SelectMany(salida => new[] { salida.RutaId, salida.LineaTxt }));
|
|
||||||
|
|
||||||
private static string ObtenerAvisoTaxibus(AlternativaRuta alternativa)
|
private static string ObtenerAvisoTaxibus(AlternativaRuta alternativa)
|
||||||
{
|
{
|
||||||
@@ -239,14 +238,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
if (lineasTaxibus is not { Count: > 0 })
|
if (lineasTaxibus is not { Count: > 0 })
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
var lineasSimuladas = lineasTaxibus
|
return TaxibusDbus.MensajeReserva;
|
||||||
.Where(linea => linea.HorarioSimulado)
|
|
||||||
.Select(linea => linea.Codigo)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return lineasSimuladas.Count > 0
|
|
||||||
? TaxibusDbus.ObtenerMensajeHorarioSimulado(lineasSimuladas)
|
|
||||||
: TaxibusDbus.MensajeReserva;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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"
|
aria-pressed="@todosItinerariosParadaPintados"
|
||||||
disabled="@_procesandoTodosItinerariosParada"
|
disabled="@_procesandoTodosItinerariosParada"
|
||||||
@onclick="ToggleTodosItinerariosParadaAsync">
|
@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>
|
<span>@TextoBotonTodosItinerariosParada</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -317,10 +317,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (HorariosTaxibusSimuladosVisibles)
|
@if (HorariosTaxibusVisibles)
|
||||||
{
|
{
|
||||||
<div class="taxibus-notice taxibus-notice--schedule" role="status">
|
<div class="taxibus-notice taxibus-notice--schedule" role="status">
|
||||||
<strong>Taxibús:</strong> @MensajeHorarioTaxibusSimuladoVisible
|
<strong>Taxibús:</strong> @MensajeHorarioTaxibusVisible
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@
|
|||||||
aria-label="@(itinerarioPintado ? "Ocultar itinerario" : "Ver itinerario")"
|
aria-label="@(itinerarioPintado ? "Ocultar itinerario" : "Ver itinerario")"
|
||||||
aria-pressed="@itinerarioPintado"
|
aria-pressed="@itinerarioPintado"
|
||||||
@onclick="() => ToggleItinerarioSalidaParadaAsync(x)">
|
@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>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@@ -385,10 +385,66 @@
|
|||||||
<div><b>Recorrido:</b> @ConstruirTextoRecorridoVariante(vReal, incluirCodigosParada: true)</div>
|
<div><b>Recorrido:</b> @ConstruirTextoRecorridoVariante(vReal, incluirCodigosParada: true)</div>
|
||||||
<div><b>Longitud:</b> @FormatearDistanciaVariante(vReal)</div>
|
<div><b>Longitud:</b> @FormatearDistanciaVariante(vReal)</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<button class="popup-parada-cerrar" @onclick="() => { _itinerarioClicado = null; _itinerarioClicadoSubId = null; }">
|
<button class="popup-parada-cerrar" @onclick="CerrarPopupItinerarioAsync">
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1378,16 +1434,20 @@
|
|||||||
|
|
||||||
@if (_mostrarInformacionDerivacionHistorial &&
|
@if (_mostrarInformacionDerivacionHistorial &&
|
||||||
_escenarioBaseDerivacionHistorial is not null &&
|
_escenarioBaseDerivacionHistorial is not null &&
|
||||||
PasoDerivacionSeleccionado is { } pasoDerivacion)
|
EscenarioDerivacionSeleccionado is { } escenarioDerivacionSeleccionado)
|
||||||
{
|
{
|
||||||
|
var esOriginalDerivacion = _indicePasoDerivacionSeleccionado == -1;
|
||||||
|
var pasoDerivacion = PasoDerivacionSeleccionado;
|
||||||
var cambiosRespectoOriginal = ObtenerCambiosDerivacion(
|
var cambiosRespectoOriginal = ObtenerCambiosDerivacion(
|
||||||
_escenarioBaseDerivacionHistorial,
|
_escenarioBaseDerivacionHistorial,
|
||||||
pasoDerivacion.Escenario);
|
escenarioDerivacionSeleccionado);
|
||||||
var escenarioAnterior = ObtenerEscenarioAnteriorDerivacion(pasoDerivacion);
|
var escenarioAnterior = pasoDerivacion is null
|
||||||
var cambiosRespectoAnterior = ObtenerCambiosDerivacion(
|
? null
|
||||||
escenarioAnterior,
|
: ObtenerEscenarioAnteriorDerivacion(pasoDerivacion);
|
||||||
pasoDerivacion.Escenario);
|
var cambiosRespectoAnterior = pasoDerivacion is null
|
||||||
var textoComparacionAnterior = pasoDerivacion.NumeroAnterior is null
|
? Array.Empty<CambioDerivacionHistorial>()
|
||||||
|
: ObtenerCambiosDerivacion(escenarioAnterior, pasoDerivacion.Escenario);
|
||||||
|
var textoComparacionAnterior = pasoDerivacion?.NumeroAnterior is null
|
||||||
? "Respecto al Historial Compartido original"
|
? "Respecto al Historial Compartido original"
|
||||||
: $"Respecto a la Derivación n.º {pasoDerivacion.NumeroAnterior}";
|
: $"Respecto a la Derivación n.º {pasoDerivacion.NumeroAnterior}";
|
||||||
|
|
||||||
@@ -1412,6 +1472,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="history-derivation-steps" aria-label="Derivaciones registradas">
|
<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)
|
@foreach (var paso in _pasosDerivacionHistorial)
|
||||||
{
|
{
|
||||||
var indicePaso = paso.Numero - 1;
|
var indicePaso = paso.Numero - 1;
|
||||||
@@ -1426,8 +1492,16 @@
|
|||||||
|
|
||||||
<div class="history-derivation-selected">
|
<div class="history-derivation-selected">
|
||||||
<div class="history-derivation-selected__summary">
|
<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>
|
<strong>Derivación n.º @pasoDerivacion.Numero</strong>
|
||||||
<span>@pasoDerivacion.FechaUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss")</span>
|
<span>@pasoDerivacion.FechaUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss")</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn-ghost history-derivation-load"
|
class="btn-ghost history-derivation-load"
|
||||||
@@ -1437,10 +1511,23 @@
|
|||||||
? "Cargando..."
|
? "Cargando..."
|
||||||
: _indicePasoDerivacionSeleccionado == _indicePasoDerivacionActivo
|
: _indicePasoDerivacionSeleccionado == _indicePasoDerivacionActivo
|
||||||
? "Cargada"
|
? "Cargada"
|
||||||
|
: esOriginalDerivacion
|
||||||
|
? "Cargar original"
|
||||||
: "Cargar derivación")
|
: "Cargar derivación")
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<section class="history-derivation-comparison">
|
||||||
<h6>Respecto al Historial Compartido original</h6>
|
<h6>Respecto al Historial Compartido original</h6>
|
||||||
@if (cambiosRespectoOriginal.Count == 0)
|
@if (cambiosRespectoOriginal.Count == 0)
|
||||||
@@ -1471,8 +1558,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
@if (pasoDerivacion.NumeroAnterior is not null)
|
@if (pasoDerivacion?.NumeroAnterior is not null)
|
||||||
{
|
{
|
||||||
<section class="history-derivation-comparison">
|
<section class="history-derivation-comparison">
|
||||||
<h6>@textoComparacionAnterior</h6>
|
<h6>@textoComparacionAnterior</h6>
|
||||||
|
|||||||
@@ -5,6 +5,28 @@ using RutasDBUS.Servicios.Importacion;
|
|||||||
|
|
||||||
namespace RutasDBUS.Servicios.Horarios
|
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>
|
/// <summary>
|
||||||
/// Lee el ZIP de importación de dBUS (CSV + opcional gtfs.zip) y expone:
|
/// Lee el ZIP de importación de dBUS (CSV + opcional gtfs.zip) y expone:
|
||||||
/// - Calendarios activos para una fecha
|
/// - Calendarios activos para una fecha
|
||||||
@@ -280,6 +302,159 @@ namespace RutasDBUS.Servicios.Horarios
|
|||||||
return active;
|
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>
|
/// <summary>
|
||||||
/// Próximas salidas desde una parada (por stopId interno).
|
/// Próximas salidas desde una parada (por stopId interno).
|
||||||
/// Devuelve: (horaSalidaEnParada, expId, rutaId)
|
/// 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 tiemposReales = await ReadTiemposRealesAsync(FindEntry(zip, "tiempo_real.csv"), ct);
|
||||||
var transbordos = await ReadTransbordosAsync(FindEntry(zip, "transbordos.csv"), ct);
|
var transbordos = await ReadTransbordosAsync(FindEntry(zip, "transbordos.csv"), ct);
|
||||||
var vehiculos = await ReadVehiculosAsync(FindEntry(zip, "vehiculos.csv"), ct);
|
var vehiculos = await ReadVehiculosAsync(FindEntry(zip, "vehiculos.csv"), ct);
|
||||||
var rutasConHorarioSimulado = SimuladorHorariosTaxibus.AplicarSiFaltanHorarios(
|
HorariosOficialesTaxibus.AplicarSiFaltanHorarios(
|
||||||
calendarios,
|
calendarios,
|
||||||
|
calendarioEx,
|
||||||
expediciones,
|
expediciones,
|
||||||
rutas,
|
rutas,
|
||||||
tiemposParada);
|
tiemposParada);
|
||||||
@@ -131,7 +132,6 @@ public sealed class ServicioArchivoImportacionDbus
|
|||||||
TiemposReales = tiemposReales,
|
TiemposReales = tiemposReales,
|
||||||
Transbordos = transbordos,
|
Transbordos = transbordos,
|
||||||
Vehiculos = vehiculos,
|
Vehiculos = vehiculos,
|
||||||
RutasConHorarioSimulado = rutasConHorarioSimulado,
|
|
||||||
ParadasPorId = paradas.ToDictionary(x => x.Id),
|
ParadasPorId = paradas.ToDictionary(x => x.Id),
|
||||||
RutasPorId = rutas.ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase),
|
RutasPorId = rutas.ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase),
|
||||||
ExpedicionesPorId = expediciones.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>
|
/// </summary>
|
||||||
public static class TaxibusDbus
|
public static class TaxibusDbus
|
||||||
{
|
{
|
||||||
public const string PrefijoExpedicionSimulada = "TAXIBUS_SIM_";
|
public const string PrefijoExpedicionHorarioOficial = "TAXIBUS_OFICIAL_";
|
||||||
public const string MensajeReserva = "Esta l\u00ednea requiere solicitud de RESERVA.";
|
public const string MensajeReserva =
|
||||||
public const string MensajeHorarioSimuladoTb6 =
|
"Horario oficial. Esta l\u00ednea requiere solicitud de RESERVA.";
|
||||||
"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 static bool EsLinea(string? codigo)
|
public static bool EsLinea(string? codigo)
|
||||||
=> EsLineaTb6(codigo) || EsLineaTb7(codigo);
|
=> EsLineaTb6(codigo) || EsLineaTb7(codigo);
|
||||||
@@ -22,23 +19,7 @@ public static class TaxibusDbus
|
|||||||
normalizado.Equals("6", StringComparison.OrdinalIgnoreCase);
|
normalizado.Equals("6", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ObtenerMensajeHorarioSimulado(IEnumerable<string?> codigosLinea)
|
public static bool EsLineaTb7(string? codigo)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
var normalizado = NormalizarCodigoLinea(codigo);
|
var normalizado = NormalizarCodigoLinea(codigo);
|
||||||
return normalizado.Equals("TB7", StringComparison.OrdinalIgnoreCase) ||
|
return normalizado.Equals("TB7", StringComparison.OrdinalIgnoreCase) ||
|
||||||
@@ -58,7 +39,7 @@ public static class TaxibusDbus
|
|||||||
return normalizado;
|
return normalizado;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool EsExpedicionSimulada(string? idExpedicion)
|
public static bool EsExpedicionHorarioOficial(string? idExpedicion)
|
||||||
=> !string.IsNullOrWhiteSpace(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 */
|
/* Itinerario: bajar texto para que no choque con la X */
|
||||||
.popup-stack-left .popup-itinerario-body {
|
.popup-stack-left .popup-itinerario-body {
|
||||||
padding-right: 2.2rem;
|
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 {
|
.popup-parada-header {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -1149,6 +1149,7 @@ window.rt.clearPreview = function () {
|
|||||||
if (!window.rt.ensurePreviewLayer()) return;
|
if (!window.rt.ensurePreviewLayer()) return;
|
||||||
window.rt._previewLayer.clearLayers();
|
window.rt._previewLayer.clearLayers();
|
||||||
window.rt._previewLines = {};
|
window.rt._previewLines = {};
|
||||||
|
window.rt._itinerarySegmentHighlight = null;
|
||||||
|
|
||||||
// ✅ también limpiar paradas destacadas
|
// ✅ también limpiar paradas destacadas
|
||||||
if (window.rt._itStopsLayer) {
|
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) {
|
window.rt.drawPreviewLine = function (id, coords, options) {
|
||||||
if (!window.rt.ensurePreviewLayer()) return;
|
if (!window.rt.ensurePreviewLayer()) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const TILE_CACHE = 'tile-cache-v1';
|
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.addEventListener('install', event => {
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ using Microsoft.Extensions.Caching.Memory;
|
|||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using RutasDBUS.Components.Pages;
|
using RutasDBUS.Components.Pages;
|
||||||
using RutasDBUS.Modelos.Historial;
|
using RutasDBUS.Modelos.Historial;
|
||||||
|
using RutasDBUS.Modelos.Importacion;
|
||||||
using RutasDBUS.Modelos.Planificacion;
|
using RutasDBUS.Modelos.Planificacion;
|
||||||
|
using RutasDBUS.Servicios;
|
||||||
using RutasDBUS.Servicios.Caminata;
|
using RutasDBUS.Servicios.Caminata;
|
||||||
using RutasDBUS.Servicios.Configuracion;
|
using RutasDBUS.Servicios.Configuracion;
|
||||||
|
using RutasDBUS.Servicios.Horarios;
|
||||||
|
using RutasDBUS.Servicios.Importacion;
|
||||||
using RutasDBUS.Servicios.Planificacion;
|
using RutasDBUS.Servicios.Planificacion;
|
||||||
|
|
||||||
static void Check(bool result, string message)
|
static void Check(bool result, string message)
|
||||||
@@ -20,6 +24,213 @@ var baseMethod = typeof(PlanificadorRutas).GetMethod("CrearConfiguracionBaseHist
|
|||||||
var defaults = (ConfiguracionHistorialRuta)baseMethod.Invoke(null, null)!;
|
var defaults = (ConfiguracionHistorialRuta)baseMethod.Invoke(null, null)!;
|
||||||
Check(defaults.PesoTransbordoRankingMinutos == 6, "Penalizacion inicial de 6 minutos");
|
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 panelSinCambios = new PlanificadorRutas();
|
||||||
var alternarPanel = typeof(PlanificadorRutas).GetMethod(
|
var alternarPanel = typeof(PlanificadorRutas).GetMethod(
|
||||||
"ToggleConfigPanel",
|
"ToggleConfigPanel",
|
||||||
@@ -261,10 +472,6 @@ typeof(PlanificadorRutas).GetField("_mostrarHistorialRutas", flags)!
|
|||||||
.SetValue(paginaCierreHistorial, true);
|
.SetValue(paginaCierreHistorial, true);
|
||||||
typeof(PlanificadorRutas).GetField("_factorEquilibradoAndarBus", flags)!
|
typeof(PlanificadorRutas).GetField("_factorEquilibradoAndarBus", flags)!
|
||||||
.SetValue(paginaCierreHistorial, 99.0);
|
.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)!;
|
var alternarHistorial = typeof(PlanificadorRutas).GetMethod("ToggleHistorialPanel", flags)!;
|
||||||
await (Task)alternarHistorial.Invoke(paginaCierreHistorial, null)!;
|
await (Task)alternarHistorial.Invoke(paginaCierreHistorial, null)!;
|
||||||
Check(
|
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
|
sealed class OsmWidthHandler : HttpMessageHandler
|
||||||
{
|
{
|
||||||
public int Calls { get; private set; }
|
public int Calls { get; private set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user