From 4c71402e24edf3c1fc3627664c922d285730eeac Mon Sep 17 00:00:00 2001 From: Pedro Date: Mon, 13 Jul 2026 15:00:47 +0200 Subject: [PATCH] =?UTF-8?q?-=20A=C3=B1adidas=20alternativas=20que=20permit?= =?UTF-8?q?en=20incorporarse=20directamente=20al=20segundo=20sentido=20de?= =?UTF-8?q?=20una=20l=C3=ADnea=20circular=20cuando=20ofrecen=20mejor=20pun?= =?UTF-8?q?tuaci=C3=B3n.=20-=20Corregida=20la=20p=C3=A9rdida=20del=20histo?= =?UTF-8?q?rial=20personal=20al=20pulsar=20Historial=20inmediatamente=20de?= =?UTF-8?q?spu=C3=A9s=20de=20recargar=20la=20aplicaci=C3=B3n.=20-=20Evitad?= =?UTF-8?q?as=20escrituras=20y=20eliminaciones=20antes=20de=20completar=20?= =?UTF-8?q?la=20carga=20de=20localStorage.=20-=20Oscurecido=20el=20amarill?= =?UTF-8?q?o=20de=20la=20l=C3=ADnea=2040=20para=20mejorar=20su=20visibilid?= =?UTF-8?q?ad=20sobre=20el=20mapa.=20-=20Actualizada=20la=20versi=C3=B3n?= =?UTF-8?q?=20de=20la=20aplicaci=C3=B3n=20a=20v-20260713a.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pages/PlanificadorRutas.CicloVidaYMapa.cs | 2 +- .../Pages/PlanificadorRutas.Historial.cs | 60 ++++-- .../Pages/PlanificadorRutas.Itinerarios.cs | 2 +- .../PlanificadorRutasServicio.cs | 177 ++++++++++++++++++ RutasDBUS/Servicios/RedBus/RedBusServicio.cs | 14 +- 5 files changed, 240 insertions(+), 15 deletions(-) diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs index 78d8d60..913c010 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs @@ -52,7 +52,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable try { await JS.InvokeVoidAsync("rt.blockLeafletDoubleClick"); } catch (Exception ex) { Console.WriteLine("rt.blockLeafletDoubleClick: " + ex); } try { await JS.InvokeVoidAsync("rt.initDraggablePopups"); } catch (Exception ex) { Console.WriteLine("rt.initDraggablePopups: " + ex); } await CargarConfiguracionLocalAsync(); - await CargarHistorialLocalAsync(); + await AsegurarHistorialLocalCargadoAsync(); await CargarHistorialCompartidoAsync(); await CargarModelosHorariosAsync(); diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs index e1b527b..bac82fe 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs @@ -24,6 +24,8 @@ public partial class PlanificadorRutas private bool _omitirRegistroHistorialEnSiguienteCalculo = false; private string? _historialCompartidoPendienteEliminarId; private ConfiguracionLocalPlanificador? _configuracionTemporalPreviaHistorial; + private Task? _tareaCargaHistorialLocal; + private bool _historialLocalCargado = false; private async Task ToggleHistorialPanel() { @@ -35,41 +37,69 @@ public partial class PlanificadorRutas return; } + if (!await AsegurarHistorialLocalCargadoAsync()) + { + MostrarMensajeHistorial("No he podido cargar el historial personal. Intentalo de nuevo.", esError: true); + return; + } + _mostrarHistorialRutas = true; await CargarHistorialCompartidoAsync(); StartRefrescoHistorialCompartido(); } - private async Task CargarHistorialLocalAsync() + private async Task AsegurarHistorialLocalCargadoAsync() + { + if (_historialLocalCargado) + return true; + + _tareaCargaHistorialLocal ??= CargarHistorialLocalDesdeNavegadorAsync(); + var tareaCarga = _tareaCargaHistorialLocal; + var cargado = await tareaCarga; + + if (!cargado && ReferenceEquals(_tareaCargaHistorialLocal, tareaCarga)) + _tareaCargaHistorialLocal = null; + + return cargado; + } + + private async Task CargarHistorialLocalDesdeNavegadorAsync() { try { var json = await JS.InvokeAsync("rt.loadLocalValue", ClaveHistorialLocal); - if (string.IsNullOrWhiteSpace(json)) - return; + if (!string.IsNullOrWhiteSpace(json)) + { + var items = JsonSerializer.Deserialize>(json); + if (items is null) + return false; - var items = JsonSerializer.Deserialize>(json); - if (items is null) - return; - - _historialLocal.Clear(); - _historialLocal.AddRange(items - .Where(x => !string.IsNullOrWhiteSpace(x.Id)) - .OrderByDescending(x => x.FechaActualizacionUtc) - .Take(MaxItemsHistorialLocal)); + _historialLocal.Clear(); + _historialLocal.AddRange(items + .Where(x => !string.IsNullOrWhiteSpace(x.Id)) + .OrderByDescending(x => x.FechaActualizacionUtc) + .Take(MaxItemsHistorialLocal)); + } _historialLocalSeleccionadoId = _historialLocal.FirstOrDefault()?.Id ?? string.Empty; + _historialLocalCargado = true; + return true; } catch (JSException) { + return false; } catch (JsonException) { + return false; } } private async Task PersistirHistorialLocalAsync() { + if (!_historialLocalCargado) + return; + try { var json = JsonSerializer.Serialize(_historialLocal); @@ -157,6 +187,9 @@ public partial class PlanificadorRutas private async Task RegistrarEscenarioActualEnHistorialLocalAsync() { + if (!await AsegurarHistorialLocalCargadoAsync()) + return; + var actual = ConstruirRegistroHistorialActual(); if (actual is null) return; @@ -287,6 +320,9 @@ public partial class PlanificadorRutas private async Task EliminarHistorialLocalAsync(string id) { + if (!await AsegurarHistorialLocalCargadoAsync()) + return; + var item = _historialLocal.FirstOrDefault(x => x.Id == id); if (item is null) return; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs index 787ef24..e04ffb3 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs @@ -1261,7 +1261,7 @@ public partial class PlanificadorRutas : ComponentBase private bool _elevPopupMinimizado = false; - private const string _versionApp = "(v-20260710a)"; + private const string _versionApp = "(v-20260713a)"; private double? _paradaElevG; diff --git a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs index 8e6794d..6571b08 100644 --- a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs +++ b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs @@ -244,6 +244,33 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas Math.Max(16, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 10), parametros); } + + var iniciosCircularesDirectos = BuscarIniciosDirectosTrasCircularConHorario( + candidatasBus, + candidatasOrigen, + instanteSalida, + parametros); + + if (iniciosCircularesDirectos.Count > 0) + { + var porFirma = candidatasBus.ToDictionary( + candidata => UtilidadesTransitoDbus.FirmaRutaBus(candidata.RutaBus), + StringComparer.OrdinalIgnoreCase); + + foreach (var inicioDirecto in iniciosCircularesDirectos) + { + var firma = UtilidadesTransitoDbus.FirmaRutaBus(inicioDirecto.RutaBus); + if (!porFirma.TryGetValue(firma, out var actual) || + ResultadoBusquedaEsMejor(inicioDirecto, actual)) + { + porFirma[firma] = inicioDirecto; + } + } + + // Estos recortes ya proceden del pool seleccionado. No se vuelve a podar para que + // una familia con muchos itinerarios equivalentes no los elimine otra vez. + candidatasBus = porFirma.Values.ToList(); + } } candidatasBus = candidatasBus @@ -2326,6 +2353,156 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas return total; } + /// + /// Conserva una variante que entra directamente en el segundo sentido de una circular cuando + /// una parada inmediata permite coger el mismo servicio con mejor score equilibrado. + /// + private List BuscarIniciosDirectosTrasCircularConHorario( + IReadOnlyList candidatasBase, + IReadOnlyList candidatasOrigen, + DateTime instanteSalida, + ParametrosPlanificadorRuta parametros) + { + if (candidatasBase.Count == 0 || candidatasOrigen.Count == 0) + return new(); + + var origenesPorCodigo = candidatasOrigen + .GroupBy(x => x.Parada.Codigo, StringComparer.OrdinalIgnoreCase) + .ToDictionary( + grupo => grupo.Key, + grupo => grupo.OrderBy(x => x.TiempoAPie).ThenBy(x => x.DistanciaAPie).First(), + StringComparer.OrdinalIgnoreCase); + var mejores = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var candidataBase in candidatasBase) + { + var tramosBase = candidataBase.RutaBus.Tramos; + if (tramosBase.Count < 2) + continue; + + var primero = tramosBase[0]; + var segundo = tramosBase[1]; + if (!primero.EsValido || + !segundo.EsValido || + !EsContinuidadCircular(primero.Variante, segundo.Variante) || + !string.Equals(primero.CodigoParadaFin, segundo.CodigoParadaInicio, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var llegadaPrimeraParada = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, candidataBase.TiempoPieInicio); + if (!ResolverSegmentoHorario( + primero.CodigoParadaInicio, + primero.CodigoParadaFin, + UtilidadesTransitoDbus.LineaCodigoStr(primero.Variante), + primero.Variante.CodigoItinerario, + UtilidadesTransitoDbus.NormalizarSentidoCodigo(primero.Variante.Sentido?.Codigo), + llegadaPrimeraParada, + out _, + out var llegadaIntermedia) || + !ResolverSegmentoHorario( + segundo.CodigoParadaInicio, + segundo.CodigoParadaFin, + UtilidadesTransitoDbus.LineaCodigoStr(segundo.Variante), + segundo.Variante.CodigoItinerario, + UtilidadesTransitoDbus.NormalizarSentidoCodigo(segundo.Variante.Sentido?.Codigo), + llegadaIntermedia, + out _, + out var llegadaOriginalSegundo)) + { + continue; + } + + ResultadoBusquedaRutaBusDbus? mejorRecorte = null; + var indiceMaximo = Math.Min( + segundo.IndiceFin - 1, + segundo.IndiceInicio + VentanaRefinamientoParadaInicio); + + for (var indiceSubida = segundo.IndiceInicio + 1; indiceSubida <= indiceMaximo; indiceSubida++) + { + var codigoSubida = segundo.Variante.CodigosParadas[indiceSubida]; + if (!origenesPorCodigo.TryGetValue(codigoSubida, out var origenDirecto) || + origenDirecto.DistanciaAPie > candidataBase.DistanciaPieInicio + MaxExcesoAccesoDirectoCircularMetros) + { + continue; + } + + var llegadaDirectaParada = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, origenDirecto.TiempoAPie); + if (!ResolverSegmentoHorario( + codigoSubida, + segundo.CodigoParadaFin, + UtilidadesTransitoDbus.LineaCodigoStr(segundo.Variante), + segundo.Variante.CodigoItinerario, + UtilidadesTransitoDbus.NormalizarSentidoCodigo(segundo.Variante.Sentido?.Codigo), + llegadaDirectaParada, + out var salidaDirecta, + out var llegadaDirectaSegundo) || + llegadaDirectaSegundo > llegadaOriginalSegundo.AddSeconds(ToleranciaMismoBusCircularSegundos)) + { + continue; + } + + var rutaDirecta = ClonarRutaBus(candidataBase.RutaBus); + rutaDirecta.Tramos.RemoveAt(0); + rutaDirecta.Tramos[0].IndiceInicio = indiceSubida; + rutaDirecta.NumeroTransbordos = UtilidadesTransitoDbus.ContarTransbordosReales(rutaDirecta.Tramos); + rutaDirecta.CodigosParadasTransbordo = ConstruirCodigosTransbordo(rutaDirecta.Tramos); + rutaDirecta.DistanciaTotalBus = 0; + + var diferenciaLlegada = (llegadaDirectaSegundo - llegadaOriginalSegundo).TotalSeconds; + var duracionEstimada = Math.Max(0, candidataBase.DuracionEstimadaSegundos + diferenciaLlegada); + var esperaInicial = Math.Max(0, (salidaDirecta - llegadaDirectaParada).TotalSeconds); + var recorte = new ResultadoBusquedaRutaBusDbus + { + ParadaOrigen = origenDirecto.Parada, + ParadaDestino = candidataBase.ParadaDestino, + DistanciaPieInicio = origenDirecto.DistanciaAPie, + TiempoPieInicio = origenDirecto.TiempoAPie, + DistanciaPieFin = candidataBase.DistanciaPieFin, + TiempoPieFin = candidataBase.TiempoPieFin, + DistanciaPieTransbordos = candidataBase.DistanciaPieTransbordos, + TiempoEsperaInicialSegundos = esperaInicial, + TiempoEsperaTransbordosSegundos = candidataBase.TiempoEsperaTransbordosSegundos, + DuracionEstimadaSegundos = duracionEstimada, + PuntuacionCoste = UtilidadesTransitoDbus.CalcularCosteConPenalizacionesRankingSegundos( + duracionEstimada, + parametros, + rutaDirecta.NumeroTransbordos, + origenDirecto.DistanciaAPie, + candidataBase.DistanciaPieTransbordos, + candidataBase.DistanciaPieFin), + RutaBus = rutaDirecta + }; + + if (CalcularScoreBusquedaEquilibrado(recorte, parametros) + 1e-6 >= + CalcularScoreBusquedaEquilibrado(candidataBase, parametros)) + { + continue; + } + + if (mejorRecorte is null || + CalcularScoreBusquedaEquilibrado(recorte, parametros) + 1e-6 < + CalcularScoreBusquedaEquilibrado(mejorRecorte, parametros)) + { + mejorRecorte = recorte; + } + } + + if (mejorRecorte is null) + continue; + + var firma = UtilidadesTransitoDbus.FirmaRutaBus(mejorRecorte.RutaBus); + if (!mejores.TryGetValue(firma, out var actual) || + CalcularScoreBusquedaEquilibrado(mejorRecorte, parametros) + 1e-6 < + CalcularScoreBusquedaEquilibrado(actual, parametros)) + { + mejores[firma] = mejorRecorte; + } + } + + return mejores.Values.ToList(); + } + /// /// Evita empezar una circular yendo hacia atras si se puede coger directamente el tramo siguiente. /// diff --git a/RutasDBUS/Servicios/RedBus/RedBusServicio.cs b/RutasDBUS/Servicios/RedBus/RedBusServicio.cs index 14ceccf..c9b1904 100644 --- a/RutasDBUS/Servicios/RedBus/RedBusServicio.cs +++ b/RutasDBUS/Servicios/RedBus/RedBusServicio.cs @@ -13,6 +13,8 @@ namespace RutasDBUS.Servicios.RedBus; public class RedBusServicio : IRedBusServicio { + private const string ColorVisualLinea40Argb = "FF9A7B00"; + private readonly IWebHostEnvironment _entorno; private readonly OpcionesRedBus _opciones; private readonly ILogger _logger; @@ -98,7 +100,11 @@ public class RedBusServicio : IRedBusServicio _paradas.AddRange(network.Paradas); foreach (var kvp in network.Trayectos) _trayectos[kvp.Key] = kvp.Value; - _lineas.AddRange(network.Lineas); + foreach (var linea in network.Lineas) + { + AjustarColorVisualLinea(linea); + _lineas.Add(linea); + } if (_paradas.Count == 0 || _lineas.Count == 0) { @@ -174,6 +180,12 @@ public class RedBusServicio : IRedBusServicio _gridParadas.Clear(); } + private static void AjustarColorVisualLinea(LineaBus linea) + { + if (string.Equals(linea.Codigo, "40", StringComparison.OrdinalIgnoreCase)) + linea.ColorArgb = ColorVisualLinea40Argb; + } + // ========================================================= // ZIP // =========================================================