459 lines
15 KiB
C#
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);
|
|
}
|
|
|
|
}
|