Files
RutasDBUS/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs

459 lines
15 KiB
C#

using System.Collections.Concurrent;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using LeafletForBlazor;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus;
using RutasDBUS.Servicios;
using RutasDBUS.Servicios.Caminata;
using RutasDBUS.Servicios.Horarios;
using RutasDBUS.Servicios.Planificacion;
using RutasDBUS.Servicios.RedBus;
namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas : ComponentBase
{
/// <summary>
/// Gestiona hmm.
/// </summary>
private static string HHmm(DateTime dt) => dt.ToString("HH:mm", CultureInfo.InvariantCulture);
/// <summary>
/// Añade seconds safe.
/// </summary>
private static DateTime AddSecondsSafe(DateTime dt, double seconds)
{
if (seconds <= 0) return dt;
return dt.AddSeconds(seconds);
}
// Para sacar de InfoVariante lo que necesitamos para horarios
/// <summary>
/// Gestiona codigo str.
/// </summary>
private static string LineaCodigoStr(InfoVariante v) => v?.Linea?.Codigo?.ToString() ?? "?";
// =======================
// HORARIOS (ZIP)
// =======================
private bool _usarHorariosTeoricos = true;
private bool _horariosOk = false;
private string? _horariosError = null;
/// <summary>
/// Sincroniza la penalizacion manual por parada con el modo horario activo.
/// </summary>
private void SincronizarSegundosPorParadaConHorarios()
{
if (_usarHorariosTeoricos)
{
_segundosPorParadaBus = 0;
return;
}
if (_segundosPorParadaBus == 0 && _segundosPorParadaBusManual >= 0)
_segundosPorParadaBus = _segundosPorParadaBusManual;
}
/// <summary>
/// Gestiona lista horarios con actual.
/// </summary>
private List<ProximaSalidaVm> ConstruirListaHorariosConActual(
List<(TimeOnly dep, string expId, string rutaId)> deps,
TimeOnly now)
{
var ordenados = deps
.OrderBy(x => x.dep)
.ToList();
int idxProximo = ordenados.FindIndex(x => x.dep >= now);
var lista = new List<ProximaSalidaVm>(ordenados.Count);
for (int i = 0; i < ordenados.Count; i++)
{
var d = ordenados[i];
var itiClave = Horarios.GetLineaItinerarioSentidoText(d.expId, d.rutaId);
lista.Add(new ProximaSalidaVm(
Hora: d.dep.ToString("HH:mm"),
LineaTxt: Horarios.GetLineaItinerarioSentidoNombreText(d.expId, d.rutaId),
RutaId: d.rutaId,
ExpId: d.expId,
EsProximo: i == idxProximo,
AnchorId: i == idxProximo ? "horario-proximo-anchor" : "",
ItinerarioClave: itiClave));
}
return lista;
}
/// <summary>
/// Calcula el orden lógico de una línea visible del popup, respetando TB y enviando las líneas B al bloque 100.
/// </summary>
private static int ResolverOrdenLineaHorario(string? lineaVisible, string? rutaId)
{
var texto = (lineaVisible ?? "").Trim().ToUpperInvariant();
var matchTb = Regex.Match(texto, @"^TB(\d+)$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (matchTb.Success &&
int.TryParse(matchTb.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var tb))
{
return tb;
}
var matchBuho = Regex.Match(texto, @"^B(\d+)$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (matchBuho.Success &&
int.TryParse(matchBuho.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var buho))
{
return 100 + buho;
}
if (int.TryParse(texto, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeroDirecto))
return numeroDirecto;
if (int.TryParse(rutaId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var idRuta))
return idRuta;
var numeroExtraido = UtilidadesTransitoDbus.ExtraerNumeroLinea(texto);
return numeroExtraido ?? int.MaxValue;
}
/// <summary>
/// Extrae el orden lógico visible de línea, itinerario y sentido para los horarios del popup de parada.
/// </summary>
private static (int lineaOrden, string lineaVisible, int itinerarioOrden, string sentidoOrden) ParseClaveOrdenHorario(string? rutaId, string? lineaTxt)
{
if (string.IsNullOrWhiteSpace(lineaTxt))
return (ResolverOrdenLineaHorario(null, rutaId), "", int.MaxValue, "?");
var txt = lineaTxt.Trim();
var separadorDestino = txt.IndexOf(" - ", StringComparison.Ordinal);
if (separadorDestino > 0)
txt = txt[..separadorDestino].Trim();
var match = Regex.Match(
txt,
@"^(?<linea>[^.\s]+)\.(?<itin>\d+)-(?<sentido>[A-Za-z?]+)$",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (!match.Success)
return (ResolverOrdenLineaHorario(null, rutaId), "", int.MaxValue, "?");
var lineaVisible = match.Groups["linea"].Value.Trim().ToUpperInvariant();
var sentido = match.Groups["sentido"].Value.Trim().ToUpperInvariant();
int itinerario = int.MaxValue;
int.TryParse(match.Groups["itin"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out itinerario);
return (ResolverOrdenLineaHorario(lineaVisible, rutaId), lineaVisible, itinerario, sentido);
}
private IEnumerable<ProximaSalidaVm> NextDepsOrdenados
{
get
{
var ordenadosPorClave = _nextDeps
.Select(x => new
{
Item = x,
Orden = ParseClaveOrdenHorario(x.RutaId, x.LineaTxt)
});
if (_horarioOrden == HorarioOrden.PorHora)
{
return ordenadosPorClave
.OrderBy(x => x.Item.Hora, StringComparer.Ordinal)
.ThenBy(x => x.Orden.lineaOrden)
.ThenBy(x => x.Orden.lineaVisible, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Orden.itinerarioOrden)
.ThenBy(x => x.Orden.sentidoOrden, StringComparer.OrdinalIgnoreCase)
.Select(x => x.Item);
}
return ordenadosPorClave
.OrderBy(x => x.Orden.lineaOrden)
.ThenBy(x => x.Orden.lineaVisible, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Orden.itinerarioOrden)
.ThenBy(x => x.Orden.sentidoOrden, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Item.Hora, StringComparer.Ordinal)
.Select(x => x.Item);
}
}
/// <summary>
/// Alterna orden horarios.
/// </summary>
private void ToggleOrdenHorarios()
{
_horarioOrden = _horarioOrden == HorarioOrden.PorHora
? HorarioOrden.PorItinerario
: HorarioOrden.PorHora;
if (_horarioOrden == HorarioOrden.PorHora)
_scrollHorariosPendiente = true;
}
// Próximas salidas para el popup de parada
/// <summary>
/// Inicializa una nueva instancia de la clase ProximaSalidaVm.
/// </summary>
private sealed record ProximaSalidaVm(
string Hora,
string LineaTxt,
string RutaId,
string ExpId,
bool EsProximo,
string AnchorId,
string ItinerarioClave);
private List<ProximaSalidaVm> _nextDeps = new();
private bool HorariosTaxibusVisibles
=> _nextDeps.Any(salida =>
TaxibusDbus.EsLinea(salida.RutaId) ||
TaxibusDbus.EsLinea(salida.LineaTxt));
private string MensajeHorarioTaxibusVisible
=> TaxibusDbus.MensajeReserva;
private static string ObtenerAvisoTaxibus(AlternativaRuta alternativa)
{
var lineasTaxibus = alternativa.AlternativaBus?.Tramos
.Select(tramo => tramo.Variante.Linea)
.Where(linea => TaxibusDbus.EsLinea(linea.Codigo))
.Distinct()
.ToList();
if (lineasTaxibus is not { Count: > 0 })
return string.Empty;
return TaxibusDbus.MensajeReserva;
}
/// <summary>
/// Gestiona toggle hora simulada.
/// </summary>
private async Task OnToggleHoraSimulada(ChangeEventArgs e)
{
_usarHoraSimulada = e?.Value is bool b && b;
if (_usarHoraSimulada)
{
var ahora = DateTime.Now;
_fechaSimulada = DateOnly.FromDateTime(ahora);
_horaSimulada = TimeOnly.FromDateTime(ahora);
StopClock();
}
else
{
StartClock();
}
await RefrescarHoraUIAsync();
await PersistirConfiguracionAsync();
}
// ====== HORA (real / simulada) ======
private bool _usarHoraSimulada = true;
private TimeOnly _horaSimulada = TimeOnly.FromDateTime(DateTime.Now);
private DateOnly _fechaSimulada = DateOnly.FromDateTime(DateTime.Now);
/// <summary>
/// Gestiona local.
/// </summary>
private DateTime AhoraLocal()
{
if (_usarHoraSimulada)
return _fechaSimulada.ToDateTime(_horaSimulada);
return DateTime.Now;
}
/// <summary>
/// Inicializa la hora simulada con la hora real actual y la deja parada.
/// </summary>
protected override void OnInitialized()
{
SincronizarSegundosPorParadaConHorarios();
if (!_usarHoraSimulada)
return;
var ahora = DateTime.Now;
_fechaSimulada = DateOnly.FromDateTime(ahora);
_horaSimulada = TimeOnly.FromDateTime(ahora);
var es = new CultureInfo("es-ES");
_horaAhoraTxt = es.TextInfo.ToTitleCase(ahora.ToString("ddd dd/MM HH:mm:ss", es));
}
/// <summary>
/// Gestiona change fecha simulada.
/// </summary>
private async Task OnChangeFechaSimulada(ChangeEventArgs e)
{
var s = e?.Value?.ToString();
if (!string.IsNullOrWhiteSpace(s) && DateOnly.TryParse(s, out var d))
_fechaSimulada = d;
if (_usarHoraSimulada)
await RefrescarHoraUIAsync();
await PersistirConfiguracionAsync();
}
/// <summary>
/// Gestiona change hora simulada.
/// </summary>
private async Task OnChangeHoraSimulada(ChangeEventArgs e)
{
var s = e?.Value?.ToString();
if (!string.IsNullOrWhiteSpace(s) && TimeOnly.TryParse(s, out var t))
_horaSimulada = t;
if (_usarHoraSimulada)
await RefrescarHoraUIAsync();
await PersistirConfiguracionAsync();
}
/// <summary>
/// Ajusta la fecha y la hora simuladas al momento actual.
/// </summary>
private async Task IrAAhoraSimuladoAsync()
{
var ahora = DateTime.Now;
_fechaSimulada = DateOnly.FromDateTime(ahora);
_horaSimulada = TimeOnly.FromDateTime(ahora);
if (_usarHoraSimulada)
await RefrescarHoraUIAsync();
await PersistirConfiguracionAsync();
StateHasChanged();
}
private string _horaAhoraTxt = "";
private PeriodicTimer? _clockTimer;
private CancellationTokenSource? _clockCts;
/// <summary>
/// Gestiona clock.
/// </summary>
private void StartClock()
{
if (_clockTimer is not null) return;
_clockCts = new CancellationTokenSource();
_clockTimer = new PeriodicTimer(TimeSpan.FromSeconds(1));
_ = Task.Run(async () =>
{
try
{
while (await _clockTimer.WaitForNextTickAsync(_clockCts.Token))
{
// ✅ Solo actualizamos si NO estamos en simulada
if (_usarHoraSimulada) continue;
var now = DateTime.Now;
var es = new CultureInfo("es-ES");
_horaAhoraTxt = es.TextInfo.ToTitleCase(now.ToString("ddd dd/MM HH:mm:ss", es));
await InvokeAsync(StateHasChanged); // ✅ como antes
}
}
catch { /* cancel */ }
});
}
/// <summary>
/// Gestiona clock.
/// </summary>
private void StopClock()
{
_clockCts?.Cancel();
_clockTimer?.Dispose();
_clockCts?.Dispose();
_clockTimer = null;
_clockCts = null;
}
/// <summary>
/// Actualiza hora ui.
/// </summary>
private async Task RefrescarHoraUIAsync()
{
// pinta la hora (real o simulada) y refresca UI
var es = new CultureInfo("es-ES");
DateTime now;
if (_usarHoraSimulada)
now = _fechaSimulada.ToDateTime(_horaSimulada);
else
now = DateTime.Now;
_horaAhoraTxt = es.TextInfo.ToTitleCase(now.ToString("ddd dd/MM HH:mm:ss", es));
await InvokeAsync(StateHasChanged);
}
/// <summary>
///
/// </summary>
public void Dispose()
{
_ctsParadaPopup?.Cancel();
_ctsParadaPopup?.Dispose();
_ctsSugerenciasBusqueda?.Cancel();
_ctsSugerenciasBusqueda?.Dispose();
_clockCts?.Cancel();
_clockTimer?.Dispose();
_clockCts?.Dispose();
StopRefrescoHistorialCompartido();
_dotRef?.Dispose();
}
/// <summary>
///
/// </summary>
private static IEnumerable<int> Range(int from, int to, int step = 1)
{
if (step <= 0) step = 1;
if (from <= to)
{
for (int i = from; i <= to; i += step) yield return i;
}
else
{
for (int i = from; i >= to; i -= step) yield return i;
}
}
private Task<(double[,]? line, double dist, double dur)> WalkCached(double lat1, double lon1, double lat2, double lon2)
{
return _walkProvider == ProveedorCaminata.Valhalla
? WalkValhallaCached(lat1, lon1, lat2, lon2)
: WalkOsrmCached(lat1, lon1, lat2, lon2);
}
}