From e51f2b70e2282b09be8c70b2342b9a0db53a3826 Mon Sep 17 00:00:00 2001 From: Pedro Date: Fri, 7 Aug 2026 13:56:19 +0200 Subject: [PATCH] =?UTF-8?q?-=20Integrados=20los=20modelos=20locales=20de?= =?UTF-8?q?=20elevaci=C3=B3n=20L5,=20L2=20y=20L1,=20manteniendo=20la=20fue?= =?UTF-8?q?nte=20G=20para=20comparaci=C3=B3n.=20-=20A=C3=B1adida=20la=20se?= =?UTF-8?q?lecci=C3=B3n=20del=20m=C3=A9todo=20de=20c=C3=A1lculo=20de=20alt?= =?UTF-8?q?uras,=20con=20L1=20como=20opci=C3=B3n=20predeterminada.=20-=20M?= =?UTF-8?q?ostradas=20las=20alturas=20G,=20L5,=20L2=20y=20L1=20en=20puntos?= =?UTF-8?q?=20Z=20y=20paradas,=20dejando=20en=20+info=20=C3=BAnicamente=20?= =?UTF-8?q?la=20fuente=20seleccionada.=20-=20A=C3=B1adidos=20l=C3=ADmites?= =?UTF-8?q?=20de=20tiempo,=20cach=C3=A9=20y=20control=20de=20reintentos=20?= =?UTF-8?q?para=20evitar=20bloqueos=20de=20la=20API=20de=20elevaciones.=20?= =?UTF-8?q?-=20Incorporado=20el=20filtrado=20de=20itinerarios=20por=20sent?= =?UTF-8?q?ido:=20ambos,=20solo=20ida,=20solo=20vuelta=20o=20ninguno.=20-?= =?UTF-8?q?=20Conservadas=20las=20alternativas=20exclusivamente=20a=20pie?= =?UTF-8?q?=20cuando=20los=20filtros=20impiden=20utilizar=20autobuses.=20-?= =?UTF-8?q?=20Incluidas=20las=20soluciones=20solo=20a=20pie=20dentro=20del?= =?UTF-8?q?=20ranking=20cuando=20resultan=20mejores=20que=20las=20alternat?= =?UTF-8?q?ivas=20en=20bus.=20-=20Mejorada=20la=20selecci=C3=B3n=20de=20pa?= =?UTF-8?q?radas=20y=20la=20eliminaci=C3=B3n=20de=20recorridos=20circulare?= =?UTF-8?q?s=20redundantes.=20-=20Corregidos=20los=20tramos=20peatonales?= =?UTF-8?q?=20inexistentes:=20los=20recorridos=20mostrados=20como=200=20m?= =?UTF-8?q?=20ya=20no=20generan=20pendientes=20ni=20penalizaciones=20en=20?= =?UTF-8?q?el=20score.=20-=20Alineado=20el=20c=C3=A1lculo=20final=20a=20pi?= =?UTF-8?q?e=20con=20la=20parada=20real=20utilizada=20por=20la=20soluci?= =?UTF-8?q?=C3=B3n.=20-=20Eliminada=20la=20unidad=20km=20del=20score=20par?= =?UTF-8?q?a=20mostrarlo=20correctamente=20como=20puntuaci=C3=B3n.=20-=20M?= =?UTF-8?q?ejorados=20los=20textos,=20umbrales,=20avisos=20y=20presentaci?= =?UTF-8?q?=C3=B3n=20de=20la=20configuraci=C3=B3n=20de=20pendientes.=20-?= =?UTF-8?q?=20La=20activaci=C3=B3n=20de=20Usar=20pendientes=20ya=20no=20mo?= =?UTF-8?q?difica=20el=20score=20ni=20la=20ruta=20hasta=20pulsar=20Recalcu?= =?UTF-8?q?lar.=20-=20Limitado=20el=20historial=20personal=20a=2025=20regi?= =?UTF-8?q?stros=20y=20a=C3=B1adido=20control=20por=20tama=C3=B1o.=20-=20A?= =?UTF-8?q?=C3=B1adida=20recuperaci=C3=B3n=20autom=C3=A1tica,=20copia=20de?= =?UTF-8?q?=20respaldo=20y=20diagn=C3=B3stico=20para=20historiales=20local?= =?UTF-8?q?es=20da=C3=B1ados=20o=20demasiado=20grandes.=20-=20Numerado=20t?= =?UTF-8?q?ambi=C3=A9n=20el=20historial=20personal=20e=20indicado=20el=20n?= =?UTF-8?q?=C3=BAmero=20del=20=C3=BAltimo=20registro=20cargado.=20-=20Mejo?= =?UTF-8?q?rados=20los=20mensajes=20de=20estado=20del=20historial=20person?= =?UTF-8?q?al=20y=20compartido.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pages/PlanificadorRutas.CicloVidaYMapa.cs | 3 + .../Pages/PlanificadorRutas.Estado.cs | 35 +- .../Pages/PlanificadorRutas.Historial.cs | 170 ++++++-- .../Pages/PlanificadorRutas.Itinerarios.cs | 45 +-- .../Pages/PlanificadorRutas.Persistencia.cs | 43 ++- .../Pages/PlanificadorRutas.Planificacion.cs | 345 +++++++++++++---- .../Pages/PlanificadorRutas.Renderizado.cs | 79 ++-- .../Components/Pages/PlanificadorRutas.razor | 191 ++++++--- .../Pages/PlanificadorRutas.razor.cs | 2 + .../Modelos/Planificacion/MuestraElevacion.cs | 3 + .../Modelos/Planificacion/ParadaCandidata.cs | 10 + RutasDBUS/Program.cs | 2 + RutasDBUS/RutasDBUS.csproj | 2 + .../Elevacion/IServicioElevacionLocal.cs | 24 ++ .../Elevacion/ServicioElevacionMdtLocal.cs | 362 ++++++++++++++++++ .../BuscadorRutasTemporalesDbus.cs | 7 + .../PlanificadorRutasServicio.cs | 79 +++- .../Planificacion/UtilidadesTransitoDbus.cs | 18 + RutasDBUS/appsettings.json | 5 + RutasDBUS/wwwroot/app.css | 54 ++- RutasDBUS/wwwroot/app.js | 79 +++- 21 files changed, 1283 insertions(+), 275 deletions(-) create mode 100644 RutasDBUS/Servicios/Elevacion/IServicioElevacionLocal.cs create mode 100644 RutasDBUS/Servicios/Elevacion/ServicioElevacionMdtLocal.cs diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs index f4130f8..95fdae4 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs @@ -256,6 +256,9 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable private bool _popupInspectorVisible = false; private double _inspectorLat, _inspectorLon; private double? _inspectorElevIdee; + private double? _inspectorElevLocal5; + private double? _inspectorElevLocal2; + private double? _inspectorElevLocal1; private IReadOnlyList _inspectorLugaresCercanos = Array.Empty(); private int _versionInspector; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs index 5f2a960..9bf6988 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs @@ -276,11 +276,6 @@ public partial class PlanificadorRutas : ComponentBase { SincronizarTextosPenalizacionPie(); await PersistirConfiguracionAsync(); - - if (_mostrarElevacionesRuta) - await RecalcularRankingTrasCambioEsfuerzoPendienteAsync(); - else - StateHasChanged(); } /// @@ -290,7 +285,6 @@ public partial class PlanificadorRutas : ComponentBase { SincronizarTextosPenalizacionPie(); await PersistirConfiguracionAsync(); - await RecalcularRutaEquilibradaTrasCambioFactorAsync(); } /// @@ -300,7 +294,6 @@ public partial class PlanificadorRutas : ComponentBase { SincronizarTextosPenalizacionPie(); await PersistirConfiguracionAsync(); - await RecalcularRutaEquilibradaTrasCambioFactorAsync(); } private int _radioCandidatasMetros = 1200; @@ -310,6 +303,7 @@ public partial class PlanificadorRutas : ComponentBase private bool _activarRangoItinerarios = true; private int _itinerarioMin = 1; private int _itinerarioMax = 99; + private FiltroSentidoItinerario _filtroSentidoItinerario = FiltroSentidoItinerario.Ambos; // Estado private string _textoBusquedaOrigen = string.Empty; @@ -364,7 +358,8 @@ public partial class PlanificadorRutas : ComponentBase private int _indiceAlternativaSeleccionada = 0; private DateTime? _ultimaHoraReferenciaRuta = null; private TimeSpan? _tiempoUltimoCalculoRuta = null; - private bool _mostrarElevacionesRuta = false; + private bool _mostrarElevacionesRuta = true; + private bool _mostrarElevacionesRutaConfiguracion = true; private bool _pintandoElevacionesRuta = false; private bool _mostrarConfiguracionPendientesRuta = false; private bool _panelRutasMovilContraido = false; @@ -1356,6 +1351,16 @@ public partial class PlanificadorRutas : ComponentBase return $"{Math.Max(0, tiempo.TotalSeconds).ToString("0.00", CultureInfo.InvariantCulture)} s"; } + private string FormatearAlturaSubidaAlerta() + { + var alturaMetros = Math.Max(0, _alturaPendientePositivaSignificativaRutaMetros); + var alturaRedondeada = Math.Round(alturaMetros, MidpointRounding.AwayFromZero); + var pisos = alturaMetros / MetrosPorPiso; + + return $"{alturaRedondeada.ToString("0", CultureInfo.InvariantCulture)} m " + + $"({pisos.ToString("0.0", CultureInfo.InvariantCulture)} pisos)"; + } + /// /// Construye los parametros actuales que afectan al ranking de rutas. /// @@ -1436,10 +1441,18 @@ public partial class PlanificadorRutas : ComponentBase private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa) { - if (alternativa is null || alternativa.EsSoloAPie || alternativa.AlternativaBus is null) + if (alternativa is null) return double.PositiveInfinity; - var distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus); + var distanciaBus = 0.0; + if (!alternativa.EsSoloAPie) + { + if (alternativa.AlternativaBus is null) + return double.PositiveInfinity; + + distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus); + } + var distanciaEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(alternativa); var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros( alternativa.NumeroTransbordosReales, @@ -1456,7 +1469,7 @@ public partial class PlanificadorRutas : ComponentBase if (!double.IsFinite(score)) return string.Empty; - return FormatearDistanciaTarjeta(score); + return (score / 1000.0).ToString("0.000", CultureInfo.InvariantCulture); } private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync() diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs index e833030..e368a96 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.JSInterop; @@ -10,7 +11,8 @@ public partial class PlanificadorRutas { private const string ClaveHistorialLocal = "rutasdbus.planificador.routeHistory.v1"; private const string ClaveRespaldoHistorialLocal = "rutasdbus.planificador.routeHistory.recovery.v1"; - private const int MaxItemsHistorialLocal = 100; + private const int MaxItemsHistorialLocal = 25; + private const int MaxBytesHistorialLocal = 24 * 1024; private const int IntervaloRefrescoHistorialCompartidoSegundos = 3; private const int TiempoMaxOperacionHistorialLocalSegundos = 8; private static readonly JsonSerializerOptions OpcionesLecturaHistorialLocal = new() @@ -57,6 +59,42 @@ public partial class PlanificadorRutas [JsonPropertyName("errorMessage")] public string? ErrorMessage { get; set; } + + [JsonPropertyName("trimmed")] + public bool Recortado { get; set; } + + [JsonPropertyName("backupSaved")] + public bool RespaldoGuardado { get; set; } + + [JsonPropertyName("originalItemCount")] + public int? RegistrosOriginales { get; set; } + + [JsonPropertyName("itemCount")] + public int? RegistrosFinales { get; set; } + + [JsonPropertyName("originalByteLength")] + public int? BytesOriginales { get; set; } + + [JsonPropertyName("byteLength")] + public int? BytesFinales { get; set; } + } + + private static string FormatearEstadisticasStorage(ResultadoStorageNavegador? resultado) + { + var registrosOriginales = resultado?.RegistrosOriginales?.ToString(CultureInfo.InvariantCulture) + ?? "no disponible"; + var registrosFinales = resultado?.RegistrosFinales?.ToString(CultureInfo.InvariantCulture) + ?? "no disponible"; + var bytesOriginales = resultado?.BytesOriginales?.ToString(CultureInfo.InvariantCulture) + ?? "no disponible"; + var bytesFinales = resultado?.BytesFinales?.ToString(CultureInfo.InvariantCulture) + ?? "no disponible"; + + return $"Registros: {registrosFinales}/{registrosOriginales}. " + + $"Tamano UTF-8: {bytesFinales}/{bytesOriginales} bytes. " + + $"Recortado: {resultado?.Recortado == true}. " + + $"Respaldo guardado: {resultado?.RespaldoGuardado == true}. " + + $"Limites: {MaxItemsHistorialLocal} registros y {MaxBytesHistorialLocal} bytes."; } private async Task ToggleHistorialPanel() @@ -125,7 +163,10 @@ public partial class PlanificadorRutas resultadoLectura = await JS.InvokeAsync( "rt.loadLocalValueResult", cts.Token, - ClaveHistorialLocal); + ClaveHistorialLocal, + MaxItemsHistorialLocal, + MaxBytesHistorialLocal, + ClaveRespaldoHistorialLocal); } catch (OperationCanceledException ex) { @@ -134,7 +175,8 @@ public partial class PlanificadorRutas new TimeoutException( $"La lectura no respondio en {TiempoMaxOperacionHistorialLocalSegundos} segundos.", ex), - $"Intento de carga: {numeroIntento}."); + $"Intento de carga: {numeroIntento}. " + + FormatearEstadisticasStorage(null)); return false; } catch (Exception ex) @@ -142,7 +184,8 @@ public partial class PlanificadorRutas RegistrarErrorHistorialLocal( "lectura de localStorage", ex, - $"Intento de carga: {numeroIntento}."); + $"Intento de carga: {numeroIntento}. " + + FormatearEstadisticasStorage(null)); return false; } @@ -152,7 +195,8 @@ public partial class PlanificadorRutas "lectura de localStorage", "RespuestaVacia", "JavaScript no devolvio ningun resultado de lectura.", - $"Intento de carga: {numeroIntento}."); + $"Intento de carga: {numeroIntento}. " + + FormatearEstadisticasStorage(null)); return false; } @@ -162,7 +206,8 @@ public partial class PlanificadorRutas "lectura de localStorage", resultadoLectura.ErrorName ?? "StorageError", resultadoLectura.ErrorMessage ?? "El navegador no ha indicado el motivo del error.", - $"Intento de carga: {numeroIntento}."); + $"Intento de carga: {numeroIntento}. " + + FormatearEstadisticasStorage(resultadoLectura)); return false; } @@ -184,7 +229,9 @@ public partial class PlanificadorRutas if (registrosOmitidos > 0) { - var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(json); + var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync( + json, + resultadoLectura.RespaldoGuardado); var detalleFalloRecuperacion = recuperacionGuardada ? null : _detalleErrorHistorialLocal; @@ -197,7 +244,8 @@ public partial class PlanificadorRutas $"Se omitieron {registrosOmitidos} registros que no se podian utilizar.", $"Primer fallo: {primerErrorRegistro ?? "sin detalle"}. " + $"Registros recuperados: {_historialLocal.Count}. " + - $"Tamano leido: {json.Length} caracteres. " + + $"Tamano leido: {Encoding.UTF8.GetByteCount(json)} bytes y {json.Length} caracteres. " + + FormatearEstadisticasStorage(resultadoLectura) + " " + $"Copia de recuperacion: {(recuperacionGuardada ? "guardada" : "no guardada")}." + (string.IsNullOrWhiteSpace(detalleFalloRecuperacion) ? string.Empty @@ -209,6 +257,14 @@ public partial class PlanificadorRutas else { LimpiarErrorHistorialLocal(); + if (resultadoLectura.Recortado) + { + MostrarMensajeHistorial( + $"Historial personal ajustado a {resultadoLectura.RegistrosFinales ?? _historialLocal.Count} registro(s). " + + "Se ha guardado una copia de recuperacion."); + Console.WriteLine( + $"[Historial personal] Ajuste automatico. {FormatearEstadisticasStorage(resultadoLectura)}"); + } } return _historialLocalCargado; @@ -220,7 +276,9 @@ public partial class PlanificadorRutas ArgumentException) { FinalizarCargaHistorialLocal(Array.Empty()); - var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(json); + var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync( + json, + resultadoLectura.RespaldoGuardado); var detalleFalloRecuperacion = recuperacionGuardada ? null : _detalleErrorHistorialLocal; @@ -230,7 +288,8 @@ public partial class PlanificadorRutas RegistrarErrorHistorialLocal( "deserializacion del historial", ex, - $"Tamano leido: {json.Length} caracteres. " + + $"Tamano leido: {Encoding.UTF8.GetByteCount(json)} bytes y {json.Length} caracteres. " + + FormatearEstadisticasStorage(resultadoLectura) + " " + $"Copia de recuperacion: {(recuperacionGuardada ? "guardada" : "no guardada")}." + (string.IsNullOrWhiteSpace(detalleFalloRecuperacion) ? string.Empty @@ -248,7 +307,8 @@ public partial class PlanificadorRutas RegistrarErrorHistorialLocal( "fallo inesperado al procesar el historial", ex, - $"Tamano leido: {json.Length} caracteres. El contenido original no se ha modificado."); + $"Tamano leido: {Encoding.UTF8.GetByteCount(json)} bytes y {json.Length} caracteres. " + + $"{FormatearEstadisticasStorage(resultadoLectura)} El contenido original no se ha modificado."); MostrarMensajeHistorial( "El codigo ha producido un error inesperado al cargar el historial. Consulta el diagnostico; los datos originales no se han modificado.", esError: true); @@ -343,19 +403,43 @@ public partial class PlanificadorRutas _historialLocalCargado = true; } - private async Task GuardarHistorialLocalRecuperadoAsync(string jsonOriginal) + private static (List Items, string Json, int Bytes) PrepararHistorialLocalParaGuardar( + IEnumerable origen) { - if (!await GuardarValorHistorialLocalAsync( + var items = origen.Take(MaxItemsHistorialLocal).ToList(); + var json = JsonSerializer.Serialize(items); + var bytes = Encoding.UTF8.GetByteCount(json); + + while (items.Count > 0 && bytes > MaxBytesHistorialLocal) + { + items.RemoveAt(items.Count - 1); + json = JsonSerializer.Serialize(items); + bytes = Encoding.UTF8.GetByteCount(json); + } + + return (items, json, bytes); + } + + private async Task GuardarHistorialLocalRecuperadoAsync( + string jsonOriginal, + bool respaldoOriginalYaGuardado) + { + if (!respaldoOriginalYaGuardado && + !await GuardarValorHistorialLocalAsync( ClaveRespaldoHistorialLocal, jsonOriginal, "guardado de la copia de recuperacion")) return false; - var jsonRecuperado = JsonSerializer.Serialize(_historialLocal); + var preparado = PrepararHistorialLocalParaGuardar(_historialLocal); + if (preparado.Items.Count != _historialLocal.Count) + FinalizarCargaHistorialLocal(preparado.Items); + return await GuardarValorHistorialLocalAsync( ClaveHistorialLocal, - jsonRecuperado, - "guardado del historial recuperado"); + preparado.Json, + "guardado del historial recuperado", + preparado.Items.Count); } private async Task PersistirHistorialLocalAsync() @@ -363,11 +447,34 @@ public partial class PlanificadorRutas if (!_historialLocalCargado) return; - var json = JsonSerializer.Serialize(_historialLocal); + var itemsOriginales = _historialLocal.ToList(); + var jsonOriginal = JsonSerializer.Serialize(itemsOriginales); + var preparado = PrepararHistorialLocalParaGuardar(itemsOriginales); + var fueRecortado = preparado.Items.Count != itemsOriginales.Count; + + if (fueRecortado) + { + var respaldoGuardado = await GuardarValorHistorialLocalAsync( + ClaveRespaldoHistorialLocal, + jsonOriginal, + "respaldo previo al limite del historial", + itemsOriginales.Count); + if (!respaldoGuardado) + { + MostrarMensajeHistorial( + "No se ha podido limitar el historial porque fallo su copia de recuperacion. Consulta el diagnostico.", + esError: true); + return; + } + + FinalizarCargaHistorialLocal(preparado.Items); + } + if (!await GuardarValorHistorialLocalAsync( ClaveHistorialLocal, - json, - "persistencia del historial")) + preparado.Json, + "persistencia del historial", + preparado.Items.Count)) { MostrarMensajeHistorial( "No se ha podido guardar el historial personal. Consulta el diagnostico.", @@ -375,8 +482,16 @@ public partial class PlanificadorRutas } } - private async Task GuardarValorHistorialLocalAsync(string clave, string valor, string fase) + private async Task GuardarValorHistorialLocalAsync( + string clave, + string valor, + string fase, + int? registros = null) { + var tamanoBytes = Encoding.UTF8.GetByteCount(valor); + var contexto = $"Registros: {registros?.ToString(CultureInfo.InvariantCulture) ?? "no disponible"}. " + + $"Tamano UTF-8: {tamanoBytes} bytes. Caracteres: {valor.Length}."; + try { using var cts = new CancellationTokenSource( @@ -394,7 +509,7 @@ public partial class PlanificadorRutas fase, resultado?.ErrorName ?? "RespuestaVacia", resultado?.ErrorMessage ?? "JavaScript no devolvio un resultado valido al guardar.", - $"Tamano que se intentaba guardar: {valor.Length} caracteres."); + contexto); return false; } catch (OperationCanceledException ex) @@ -404,7 +519,7 @@ public partial class PlanificadorRutas new TimeoutException( $"La escritura no respondio en {TiempoMaxOperacionHistorialLocalSegundos} segundos.", ex), - $"Tamano que se intentaba guardar: {valor.Length} caracteres."); + contexto); return false; } catch (Exception ex) @@ -412,7 +527,7 @@ public partial class PlanificadorRutas RegistrarErrorHistorialLocal( fase, ex, - $"Tamano que se intentaba guardar: {valor.Length} caracteres."); + contexto); return false; } } @@ -618,9 +733,6 @@ public partial class PlanificadorRutas _historialLocal.Insert(0, existente); } - while (_historialLocal.Count > MaxItemsHistorialLocal) - _historialLocal.RemoveAt(_historialLocal.Count - 1); - await PersistirHistorialLocalAsync(); } @@ -635,9 +747,10 @@ public partial class PlanificadorRutas return; } + var numeroPersonal = _historialLocal.FindIndex(x => x.Id == id) + 1; await AplicarRegistroHistorialAsync(item); SeleccionarHistorialLocal(id); - MostrarMensajeHistorial("Combinacion personal cargada."); + MostrarMensajeHistorial($"Historial Personal Cargado: n.º {numeroPersonal}."); } catch (Exception ex) { @@ -660,9 +773,10 @@ public partial class PlanificadorRutas return; } + var numeroCompartido = _historialCompartido.FindIndex(x => x.Id == id) + 1; await AplicarRegistroHistorialAsync(item); SeleccionarHistorialCompartido(id); - MostrarMensajeHistorial("Combinación compartida cargada."); + MostrarMensajeHistorial($"Historial Compartido Cargado: n.º {numeroCompartido}."); } private void SeleccionarHistorialLocal(string id) diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs index 3bf88e9..b160338 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs @@ -347,43 +347,41 @@ public partial class PlanificadorRutas : ComponentBase var myToken = Interlocked.Increment(ref _paradaPopupToken); _paradaClicada = null; - _paradaElevG = null; + _paradaElevIdee = null; + _paradaElevLocal5 = null; + _paradaElevLocal2 = null; + _paradaElevLocal1 = null; // limpiar horarios del popup _nextDeps.Clear(); + var punto = (paradaDetectada.Latitud, paradaDetectada.Longitud); + ObtenerElevacionesLocalesComparativasPorLote(new[] { punto }); + var clave = ClaveElevacionIdee(punto.Latitud, punto.Longitud); + _paradaElevLocal5 = _elevLocal5Cache.TryGetValue(clave, out var local5) ? local5 : null; + _paradaElevLocal2 = _elevLocal2Cache.TryGetValue(clave, out var local2) ? local2 : null; + _paradaElevLocal1 = _elevLocal1Cache.TryGetValue(clave, out var local1) ? local1 : null; + _paradaClicada = paradaDetectada; StateHasChanged(); - - double? elevG = null; - + try { - var kIdee = ClaveElevacionIdee(paradaDetectada.Latitud, paradaDetectada.Longitud); - - if (_elevIdeeCache.TryGetValue(kIdee, out var g)) - { - elevG = g; - } - else + if (!_elevIdeeCache.ContainsKey(clave)) { await ObtenerElevacionesIdeePorLoteAsync( - new List<(double lat, double lon)> { (paradaDetectada.Latitud, paradaDetectada.Longitud) }, + new List<(double lat, double lon)> { punto }, CancellationToken.None); - - elevG = _elevIdeeCache.TryGetValue( - ClaveElevacionIdee(paradaDetectada.Latitud, paradaDetectada.Longitud), - out g) ? g : (double?)null; } + + _paradaElevIdee = _elevIdeeCache.TryGetValue(clave, out var idee) ? idee : null; } catch { - elevG = null; + _paradaElevIdee = null; } if (myToken != _paradaPopupToken) return; - - _paradaElevG = elevG; - _paradaClicada = paradaDetectada; + StateHasChanged(); // ========================== // HORARIOS (ZIP) -> usando tu AhoraLocal() (real o simulada) @@ -1204,10 +1202,13 @@ public partial class PlanificadorRutas : ComponentBase private bool _elevPopupMinimizado = false; - private const string _versionApp = "(v-20260731a)"; + private const string _versionApp = "(v-20260807b)"; - private double? _paradaElevG; + private double? _paradaElevIdee; + private double? _paradaElevLocal5; + private double? _paradaElevLocal2; + private double? _paradaElevLocal1; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs index 4a64d48..1bffe95 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Text.Json; using Microsoft.JSInterop; +using RutasDBUS.Modelos.Planificacion; namespace RutasDBUS.Components.Pages; @@ -8,6 +9,7 @@ public partial class PlanificadorRutas { private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1"; private const int VersionPersistenciaFactorEquilibrado = 2; + private const int VersionPersistenciaPendientes = 2; private bool _configuracionLocalCargada = false; private bool _mostrarConfirmacionRestablecer = false; @@ -26,6 +28,7 @@ public partial class PlanificadorRutas public bool ActivarRangoItinerarios { get; set; } public int ItinerarioMin { get; set; } public int ItinerarioMax { get; set; } + public string? FiltroSentidoItinerario { get; set; } public bool ActivarRangoTransbordos { get; set; } public int MinTransbordosPermitidos { get; set; } public int MaxTransbordosPermitidos { get; set; } @@ -39,6 +42,8 @@ public partial class PlanificadorRutas public double? FactorEquilibradoAndarBus { get; set; } public int? VersionFactorEquilibrado { get; set; } public bool? MostrarElevacionesRuta { get; set; } + public string? FuenteElevacionCalculo { get; set; } + public int? VersionPendientes { get; set; } public double? UmbralPendienteSubidaRuta { get; set; } public double? UmbralPendienteBajadaRuta { get; set; } public double? UmbralAlturaAlertaPendienteRutaMetros { get; set; } @@ -117,6 +122,12 @@ public partial class PlanificadorRutas await PersistirConfiguracionAsync(); } + private async Task OnFuenteElevacionChangedAsync() + { + _fuenteElevacionCalculo = NormalizarFuenteElevacion(_fuenteElevacionCalculo); + await PersistirConfiguracionAsync(); + } + /// /// Construye la instantánea persistible a partir del estado actual. /// @@ -133,6 +144,7 @@ public partial class PlanificadorRutas ActivarRangoItinerarios = _activarRangoItinerarios, ItinerarioMin = _itinerarioMin, ItinerarioMax = _itinerarioMax, + FiltroSentidoItinerario = _filtroSentidoItinerario.ToString(), ActivarRangoTransbordos = _activarRangoTransbordos, MinTransbordosPermitidos = _minTransbordosPermitidos, MaxTransbordosPermitidos = _maxTransbordosPermitidos, @@ -145,7 +157,9 @@ public partial class PlanificadorRutas PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos, FactorEquilibradoAndarBus = _factorEquilibradoAndarBus, VersionFactorEquilibrado = VersionPersistenciaFactorEquilibrado, - MostrarElevacionesRuta = _mostrarElevacionesRuta, + MostrarElevacionesRuta = _mostrarElevacionesRutaConfiguracion, + FuenteElevacionCalculo = _fuenteElevacionCalculo, + VersionPendientes = VersionPersistenciaPendientes, UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta, UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta, UmbralAlturaAlertaPendienteRutaMetros = _umbralAlturaAlertaPendienteRutaMetros, @@ -182,6 +196,7 @@ public partial class PlanificadorRutas _activarRangoItinerarios = config.ActivarRangoItinerarios; _itinerarioMin = Math.Clamp(config.ItinerarioMin, 1, 99); _itinerarioMax = Math.Clamp(config.ItinerarioMax, 1, 99); + _filtroSentidoItinerario = NormalizarFiltroSentidoItinerario(config.FiltroSentidoItinerario); _activarRangoTransbordos = config.ActivarRangoTransbordos; _minTransbordosPermitidos = Math.Clamp(config.MinTransbordosPermitidos, 0, 10); @@ -217,7 +232,17 @@ public partial class PlanificadorRutas Math.Abs(factorEquilibrado - 12.0) < 0.0001 ? FactorEquilibradoAndarBusPorDefecto : Math.Max(0, factorEquilibrado); - _mostrarElevacionesRuta = config.MostrarElevacionesRuta ?? false; + var migrarValoresPendientes = (config.VersionPendientes ?? 0) < VersionPersistenciaPendientes; + _mostrarElevacionesRuta = migrarValoresPendientes + ? true + : config.MostrarElevacionesRuta ?? true; + _mostrarElevacionesRutaConfiguracion = _mostrarElevacionesRuta; + _fuenteElevacionCalculo = migrarValoresPendientes && + (string.IsNullOrWhiteSpace(config.FuenteElevacionCalculo) || + string.Equals(config.FuenteElevacionCalculo, FuenteElevacionLocal5, StringComparison.OrdinalIgnoreCase) || + string.Equals(config.FuenteElevacionCalculo, "local", StringComparison.OrdinalIgnoreCase)) + ? FuenteElevacionLocal1 + : NormalizarFuenteElevacion(config.FuenteElevacionCalculo); if (config.UmbralPendienteSubidaRuta is { } umbralSubida && double.IsFinite(umbralSubida)) _umbralPendienteSubidaRuta = Math.Abs(umbralSubida - 3.0) < 0.0001 ? UmbralPendienteSubidaRutaPorDefecto @@ -262,6 +287,12 @@ public partial class PlanificadorRutas SincronizarSegundosPorParadaConHorarios(); } + private static FiltroSentidoItinerario NormalizarFiltroSentidoItinerario(string? valor) + => Enum.TryParse(valor, ignoreCase: true, out var filtro) && + Enum.IsDefined(typeof(FiltroSentidoItinerario), filtro) + ? filtro + : FiltroSentidoItinerario.Ambos; + private static IReadOnlyList NormalizarLineasPermitidas( IReadOnlyList? lineas, int lineaMinLegacy, @@ -331,6 +362,7 @@ public partial class PlanificadorRutas _activarRangoItinerarios = true; _itinerarioMin = 1; _itinerarioMax = 99; + _filtroSentidoItinerario = FiltroSentidoItinerario.Ambos; _activarRangoTransbordos = true; _minTransbordosPermitidos = 0; @@ -341,7 +373,8 @@ public partial class PlanificadorRutas _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; _pesoTransbordoRankingMinutos = 4.0; _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto; - _mostrarElevacionesRuta = false; + _mostrarElevacionesRutaConfiguracion = true; + _fuenteElevacionCalculo = FuenteElevacionLocal1; _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto; _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto; _umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto; @@ -382,6 +415,7 @@ public partial class PlanificadorRutas _activarRangoItinerarios = true; _itinerarioMin = 1; _itinerarioMax = 99; + _filtroSentidoItinerario = FiltroSentidoItinerario.Ambos; _activarRangoTransbordos = true; _minTransbordosPermitidos = 0; @@ -392,7 +426,8 @@ public partial class PlanificadorRutas _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; _pesoTransbordoRankingMinutos = 4.0; _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto; - _mostrarElevacionesRuta = false; + _mostrarElevacionesRutaConfiguracion = true; + _fuenteElevacionCalculo = FuenteElevacionLocal1; _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto; _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto; _umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs index 603cb13..93c03fd 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs @@ -11,6 +11,7 @@ using RutasDBUS.Modelos.Planificacion; using RutasDBUS.Modelos.RedBus; using RutasDBUS.Servicios; using RutasDBUS.Servicios.Caminata; +using RutasDBUS.Servicios.Elevacion; using RutasDBUS.Servicios.Horarios; using RutasDBUS.Servicios.Planificacion; using RutasDBUS.Servicios.RedBus; @@ -104,8 +105,7 @@ public partial class PlanificadorRutas : ComponentBase } /// - /// Actualiza las alturas (Z) de origen y destino para el resumen del detalle de ruta. - /// Usa la fuente G (IDEE). + /// Actualiza las alturas de origen y destino con la fuente seleccionada para el calculo. /// private async Task ActualizarAlturasResumenRutaAsync(CancellationToken ct) { @@ -121,24 +121,11 @@ public partial class PlanificadorRutas : ComponentBase try { - var faltanIdee = new List<(double lat, double lon)>(2); - if (!_elevIdeeCache.ContainsKey(ClaveElevacionIdee(origen.Item1, origen.Item2))) - faltanIdee.Add(origen); - if (!_elevIdeeCache.ContainsKey(ClaveElevacionIdee(destino.Item1, destino.Item2))) - faltanIdee.Add(destino); + var puntos = new List<(double lat, double lon)> { origen, destino }; + await AsegurarElevacionesFuenteCalculoAsync(puntos, ct); - if (faltanIdee.Count > 0) - await ObtenerElevacionesIdeePorLoteAsync(faltanIdee, CancellationToken.None); - - double? zOrigen = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(origen.Item1, origen.Item2), out var gOri) - ? gOri - : (double?)null; - double? zDestino = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(destino.Item1, destino.Item2), out var gDst) - ? gDst - : (double?)null; - - _altitudOrigenResumenRuta = zOrigen; - _altitudDestinoResumenRuta = zDestino; + _altitudOrigenResumenRuta = ObtenerElevacionFuenteCalculo(origen.Item1, origen.Item2); + _altitudDestinoResumenRuta = ObtenerElevacionFuenteCalculo(destino.Item1, destino.Item2); } catch { @@ -186,7 +173,22 @@ public partial class PlanificadorRutas : ComponentBase private static readonly ConcurrentDictionary _elevIdeeCache = - new(StringComparer.OrdinalIgnoreCase); + new(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary _elevLocal5Cache = + new(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary _elevLocal2Cache = + new(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary _elevLocal1Cache = + new(StringComparer.OrdinalIgnoreCase); + private static readonly SemaphoreSlim _semaforoElevacionIdee = new(1, 1); + private static long _ideeReintentarDespuesTicksUtc; + + private const string FuenteElevacionLocal5 = "local5"; + private const string FuenteElevacionLocal2 = "local2"; + private const string FuenteElevacionLocal1 = "local1"; + private const string FuenteElevacionIdee = "idee"; + private const int TiempoMaximoIdeeSegundos = 10; + private string _fuenteElevacionCalculo = FuenteElevacionLocal1; // IDEE: en el correo indican que 6 decimales es suficiente private const int ELEV_IDEE_DECIMALES_CACHE = 6; @@ -200,7 +202,158 @@ public partial class PlanificadorRutas : ComponentBase double rLon = Math.Round(lon, ELEV_IDEE_DECIMALES_CACHE); return $"{rLat.ToString("0.000000", CultureInfo.InvariantCulture)},{rLon.ToString("0.000000", CultureInfo.InvariantCulture)}"; } - + + private static string NormalizarFuenteElevacion(string? fuente) + { + if (string.Equals(fuente, FuenteElevacionIdee, StringComparison.OrdinalIgnoreCase)) + return FuenteElevacionIdee; + if (string.Equals(fuente, FuenteElevacionLocal1, StringComparison.OrdinalIgnoreCase)) + return FuenteElevacionLocal1; + if (string.Equals(fuente, FuenteElevacionLocal2, StringComparison.OrdinalIgnoreCase)) + return FuenteElevacionLocal2; + if (string.Equals(fuente, FuenteElevacionLocal5, StringComparison.OrdinalIgnoreCase) || + string.Equals(fuente, "local", StringComparison.OrdinalIgnoreCase)) + { + return FuenteElevacionLocal5; + } + + return FuenteElevacionLocal1; + } + + private bool ElevacionLocal5Disponible + => ElevacionLocal.EstaDisponible(ModeloElevacionLocal.Local5); + + private bool ElevacionLocal2Disponible + => ElevacionLocal.EstaDisponible(ModeloElevacionLocal.Local2); + + private bool ElevacionLocal1Disponible + => ElevacionLocal.EstaDisponible(ModeloElevacionLocal.Local1); + + private bool UsarElevacionLocal5ParaCalculo + => string.Equals(_fuenteElevacionCalculo, FuenteElevacionLocal5, StringComparison.OrdinalIgnoreCase) + && ElevacionLocal5Disponible; + + private bool UsarElevacionLocal2ParaCalculo + => string.Equals(_fuenteElevacionCalculo, FuenteElevacionLocal2, StringComparison.OrdinalIgnoreCase) + && ElevacionLocal2Disponible; + + private bool UsarElevacionLocal1ParaCalculo + => string.Equals(_fuenteElevacionCalculo, FuenteElevacionLocal1, StringComparison.OrdinalIgnoreCase) + && ElevacionLocal1Disponible; + + private string EtiquetaFuenteElevacionCalculo + => UsarElevacionLocal1ParaCalculo + ? "Altura (L1)" + : UsarElevacionLocal2ParaCalculo + ? "Altura (L2)" + : UsarElevacionLocal5ParaCalculo + ? "Altura (L5)" + : "Altura (G)"; + + private double? ObtenerElevacionFuenteCalculo(double lat, double lon) + { + var clave = ClaveElevacionIdee(lat, lon); + if (UsarElevacionLocal1ParaCalculo) + return _elevLocal1Cache.TryGetValue(clave, out var local1) ? local1 : null; + if (UsarElevacionLocal2ParaCalculo) + return _elevLocal2Cache.TryGetValue(clave, out var local2) ? local2 : null; + if (UsarElevacionLocal5ParaCalculo) + return _elevLocal5Cache.TryGetValue(clave, out var local5) ? local5 : null; + + return _elevIdeeCache.TryGetValue(clave, out var idee) ? idee : null; + } + + private double? ObtenerElevacionFuenteCalculo(MuestraElevacion muestra) + => UsarElevacionLocal1ParaCalculo + ? muestra.ElevacionLocal1 + : UsarElevacionLocal2ParaCalculo + ? muestra.ElevacionLocal2 + : UsarElevacionLocal5ParaCalculo + ? muestra.ElevacionLocal5 + : muestra.ElevacionIdee; + + private void ObtenerElevacionesLocalesPorLote( + ModeloElevacionLocal modelo, + ConcurrentDictionary cache, + IReadOnlyList<(double lat, double lon)> puntos) + { + if (!ElevacionLocal.EstaDisponible(modelo) || puntos.Count == 0) + return; + + var pendientes = puntos + .Where(punto => !cache.ContainsKey(ClaveElevacionIdee(punto.lat, punto.lon))) + .Distinct() + .ToList(); + if (pendientes.Count == 0) + return; + + var elevaciones = ElevacionLocal.ObtenerElevaciones(modelo, pendientes); + for (var i = 0; i < pendientes.Count && i < elevaciones.Count; i++) + { + if (elevaciones[i] is { } elevacion && double.IsFinite(elevacion)) + cache[ClaveElevacionIdee(pendientes[i].lat, pendientes[i].lon)] = elevacion; + } + } + + private void ObtenerElevacionesLocal5PorLote(IReadOnlyList<(double lat, double lon)> puntos) + => ObtenerElevacionesLocalesPorLote( + ModeloElevacionLocal.Local5, + _elevLocal5Cache, + puntos); + + private void ObtenerElevacionesLocal2PorLote(IReadOnlyList<(double lat, double lon)> puntos) + => ObtenerElevacionesLocalesPorLote( + ModeloElevacionLocal.Local2, + _elevLocal2Cache, + puntos); + + private void ObtenerElevacionesLocal1PorLote(IReadOnlyList<(double lat, double lon)> puntos) + => ObtenerElevacionesLocalesPorLote( + ModeloElevacionLocal.Local1, + _elevLocal1Cache, + puntos); + + private void ObtenerElevacionesLocalesComparativasPorLote( + IReadOnlyList<(double lat, double lon)> puntos) + { + ObtenerElevacionesLocal5PorLote(puntos); + ObtenerElevacionesLocal2PorLote(puntos); + ObtenerElevacionesLocal1PorLote(puntos); + } + + private async Task AsegurarElevacionesFuenteCalculoAsync( + IReadOnlyList<(double lat, double lon)> puntos, + CancellationToken ct) + { + if (UsarElevacionLocal1ParaCalculo) + { + ObtenerElevacionesLocal1PorLote(puntos); + return; + } + if (UsarElevacionLocal2ParaCalculo) + { + ObtenerElevacionesLocal2PorLote(puntos); + return; + } + if (UsarElevacionLocal5ParaCalculo) + { + ObtenerElevacionesLocal5PorLote(puntos); + return; + } + + var faltan = puntos + .Where(punto => !_elevIdeeCache.ContainsKey(ClaveElevacionIdee(punto.lat, punto.lon))) + .Distinct() + .ToList(); + for (var i = 0; i < faltan.Count; i += ELEV_BATCH_MAX_PUNTOS) + { + ct.ThrowIfCancellationRequested(); + await ObtenerElevacionesIdeePorLoteAsync( + faltan.Skip(i).Take(ELEV_BATCH_MAX_PUNTOS).ToList(), + ct); + } + } + private sealed class RespuestaElevacionIdee { public string? id { get; set; } @@ -212,7 +365,21 @@ public partial class PlanificadorRutas : ComponentBase /// private async Task ObtenerElevacionesIdeePorLoteAsync(List<(double lat, double lon)> points, CancellationToken ct) { - if (points.Count == 0) return; + if (points.Count == 0 || DateTime.UtcNow.Ticks < Interlocked.Read(ref _ideeReintentarDespuesTicksUtc)) + return; + + await _semaforoElevacionIdee.WaitAsync(ct); + try + { + if (DateTime.UtcNow.Ticks < Interlocked.Read(ref _ideeReintentarDespuesTicksUtc)) + return; + + points = points + .Where(punto => !_elevIdeeCache.ContainsKey(ClaveElevacionIdee(punto.lat, punto.lon))) + .Distinct() + .ToList(); + if (points.Count == 0) + return; // Construir FeatureCollection GeoJSON, OJO: coordinates = [lon, lat] var features = new List(points.Count); @@ -259,14 +426,20 @@ public partial class PlanificadorRutas : ComponentBase try { + using var limite = CancellationTokenSource.CreateLinkedTokenSource(ct); + limite.CancelAfter(TimeSpan.FromSeconds(TiempoMaximoIdeeSegundos)); using var req = new HttpRequestMessage(HttpMethod.Post, url); req.Headers.Accept.ParseAdd("application/json"); req.Content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json"); - using var resp = await Http.SendAsync(req, ct); - if (!resp.IsSuccessStatusCode) return; + using var resp = await Http.SendAsync(req, limite.Token); + if (!resp.IsSuccessStatusCode) + { + PosponerConsultasIdee(TimeSpan.FromSeconds(15)); + return; + } - var json = await resp.Content.ReadAsStringAsync(ct); + var json = await resp.Content.ReadAsStringAsync(limite.Token); var data = JsonSerializer.Deserialize(json, new JsonSerializerOptions { @@ -283,15 +456,34 @@ public partial class PlanificadorRutas : ComponentBase var k = ClaveElevacionIdee(lat, lon); _elevIdeeCache[k] = data.values[i]; } + + Interlocked.Exchange(ref _ideeReintentarDespuesTicksUtc, 0); + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + throw; + } + catch (OperationCanceledException) + { + PosponerConsultasIdee(TimeSpan.FromSeconds(45)); } catch { + PosponerConsultasIdee(TimeSpan.FromSeconds(20)); // silencioso: elevación es informativa } + } + finally + { + _semaforoElevacionIdee.Release(); + } } - - - + + private static void PosponerConsultasIdee(TimeSpan demora) + => Interlocked.Exchange( + ref _ideeReintentarDespuesTicksUtc, + DateTime.UtcNow.Add(demora).Ticks); + // Ajustes (rendimiento) /* private const int ELEV_MAX_PUNTOS_POR_TRAMO = 24; */ private int _elevMaxPuntosPorTramo = 999; // configurable @@ -340,21 +532,7 @@ public partial class PlanificadorRutas : ComponentBase private async Task> ConstruirMuestrasElevacionAsync(double[,] line, CancellationToken ct) { var sampled = SampleLine(line, _elevMaxPuntosPorTramo); - - // --- IDEE: pedir misses primero, porque es la fuente importante --- - var missesIdee = new List<(double lat, double lon)>(); - foreach (var (lat, lon) in sampled) - { - var k = ClaveElevacionIdee(lat, lon); - if (!_elevIdeeCache.ContainsKey(k)) - missesIdee.Add((Math.Round(lat, ELEV_IDEE_DECIMALES_CACHE), Math.Round(lon, ELEV_IDEE_DECIMALES_CACHE))); - } - - for (int i = 0; i < missesIdee.Count; i += ELEV_BATCH_MAX_PUNTOS) - { - ct.ThrowIfCancellationRequested(); - await ObtenerElevacionesIdeePorLoteAsync(missesIdee.Skip(i).Take(ELEV_BATCH_MAX_PUNTOS).ToList(), ct); - } + await AsegurarElevacionesFuenteCalculoAsync(sampled, ct); return ConstruirMuestrasElevacionDesdeCache(line); } @@ -367,13 +545,25 @@ public partial class PlanificadorRutas : ComponentBase { var (lat, lon) = sampled[i]; var elevIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var e2) ? e2 : (double?)null; + var elevLocal5 = _elevLocal5Cache.TryGetValue(ClaveElevacionIdee(lat, lon), out var local5) + ? local5 + : (double?)null; + var elevLocal2 = _elevLocal2Cache.TryGetValue(ClaveElevacionIdee(lat, lon), out var local2) + ? local2 + : (double?)null; + var elevLocal1 = _elevLocal1Cache.TryGetValue(ClaveElevacionIdee(lat, lon), out var local1) + ? local1 + : (double?)null; res.Add(new MuestraElevacion { Indice = i, Latitud = lat, Longitud = lon, - ElevacionIdee = elevIdee + ElevacionIdee = elevIdee, + ElevacionLocal5 = elevLocal5, + ElevacionLocal2 = elevLocal2, + ElevacionLocal1 = elevLocal1 }); } @@ -413,24 +603,15 @@ public partial class PlanificadorRutas : ComponentBase { ct.ThrowIfCancellationRequested(); var clave = ClaveElevacionIdee(lat, lon); - if (!_elevIdeeCache.ContainsKey(clave)) - { - puntosPendientes.TryAdd( - clave, - (Math.Round(lat, ELEV_IDEE_DECIMALES_CACHE), Math.Round(lon, ELEV_IDEE_DECIMALES_CACHE))); - } + puntosPendientes.TryAdd( + clave, + (Math.Round(lat, ELEV_IDEE_DECIMALES_CACHE), Math.Round(lon, ELEV_IDEE_DECIMALES_CACHE))); } } } var puntos = puntosPendientes.Values.ToList(); - for (var i = 0; i < puntos.Count; i += ELEV_BATCH_MAX_PUNTOS) - { - ct.ThrowIfCancellationRequested(); - await ObtenerElevacionesIdeePorLoteAsync( - puntos.Skip(i).Take(ELEV_BATCH_MAX_PUNTOS).ToList(), - ct); - } + await AsegurarElevacionesFuenteCalculoAsync(puntos, ct); foreach (var item in lineasPorAlternativa) { @@ -1029,12 +1210,12 @@ public partial class PlanificadorRutas : ComponentBase if (string.Equals(criterio, CriterioRutaAndar, StringComparison.OrdinalIgnoreCase)) { - return BuscarPrimerIndiceBusOrdenadoPor( + return BuscarPrimerIndiceOrdenadoPor( opcion => CalcularCosteAndarEquilibradoSegundos(opcion), opcion => DuracionOrdenacion(opcion)); } - return BuscarPrimerIndiceBusOrdenadoPor( + return BuscarPrimerIndiceOrdenadoPor( opcion => DuracionOrdenacion(opcion), opcion => TiempoPieOrdenacion(opcion)); } @@ -1043,7 +1224,6 @@ public partial class PlanificadorRutas : ComponentBase { var candidatas = _opcionesRuta .Select((opcion, indice) => new { opcion, indice }) - .Where(x => !x.opcion.EsSoloAPie) .ToList(); if (candidatas.Count == 0) @@ -1063,38 +1243,33 @@ public partial class PlanificadorRutas : ComponentBase if (_opcionesRuta.Count <= 1) return; - IEnumerable rutasBus = _opcionesRuta.Where(opcion => !opcion.EsSoloAPie); + IEnumerable ordenadas = _opcionesRuta; if (CriterioPrioritarioAndarActivo) { - rutasBus = rutasBus + ordenadas = ordenadas .OrderBy(CalcularCosteAndarEquilibradoSegundos) .ThenBy(DuracionOrdenacion) .ThenBy(opcion => opcion.PuntuacionCoste); } else if (CriterioPrioritarioTiempoActivo) { - rutasBus = rutasBus + ordenadas = ordenadas .OrderBy(DuracionOrdenacion) .ThenBy(TiempoPieOrdenacion) .ThenBy(opcion => opcion.PuntuacionCoste); } else if (CriterioPrioritarioEquilibradoActivo) { - rutasBus = rutasBus + ordenadas = ordenadas .OrderBy(CalcularScoreEquilibradoFactor) .ThenBy(opcion => opcion.NumeroTransbordosReales) .ThenBy(opcion => opcion.TiempoEsperaTotalSegundos) .ThenBy(opcion => opcion.PuntuacionCoste); } - var soloPie = _opcionesRuta - .Where(opcion => opcion.EsSoloAPie) - .OrderBy(DuracionOrdenacion) - .ThenBy(opcion => opcion.DistanciaSoloAPie); - - var ordenadas = rutasBus.Concat(soloPie).ToList(); + var listaOrdenada = ordenadas.ToList(); _opcionesRuta.Clear(); - _opcionesRuta.AddRange(ordenadas); + _opcionesRuta.AddRange(listaOrdenada); RenumerarEtiquetasGenericasOpcionesRuta(); } @@ -1121,23 +1296,17 @@ public partial class PlanificadorRutas : ComponentBase RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } - private int BuscarPrimerIndiceBusOrdenadoPor( + private int BuscarPrimerIndiceOrdenadoPor( Func criterioPrincipal, Func criterioSecundario) { - var indiceBus = _opcionesRuta + return _opcionesRuta .Select((opcion, indice) => new { opcion, indice }) - .Where(x => !x.opcion.EsSoloAPie) .OrderBy(x => criterioPrincipal(x.opcion)) .ThenBy(x => criterioSecundario(x.opcion)) .ThenBy(x => x.opcion.PuntuacionCoste) .Select(x => x.indice) .FirstOrDefault(-1); - - if (indiceBus >= 0) - return indiceBus; - - return _opcionesRuta.Count > 0 ? 0 : -1; } private double DuracionOrdenacion(AlternativaRuta alternativa) @@ -1240,9 +1409,24 @@ public partial class PlanificadorRutas : ComponentBase _inspectorLat = lat; _inspectorLon = lon; _inspectorElevIdee = null; + _inspectorElevLocal5 = null; + _inspectorElevLocal2 = null; + _inspectorElevLocal1 = null; _inspectorLugaresCercanos = Array.Empty(); _popupInspectorVisible = true; + ObtenerElevacionesLocalesComparativasPorLote(new[] { (lat, lon) }); + var claveLocal = ClaveElevacionIdee(lat, lon); + _inspectorElevLocal5 = _elevLocal5Cache.TryGetValue(claveLocal, out var elevLocal5) + ? elevLocal5 + : null; + _inspectorElevLocal2 = _elevLocal2Cache.TryGetValue(claveLocal, out var elevLocal2) + ? elevLocal2 + : null; + _inspectorElevLocal1 = _elevLocal1Cache.TryGetValue(claveLocal, out var elevLocal1) + ? elevLocal1 + : null; + await ActualizarMarcadoresInspectorJsAsync(); StateHasChanged(); @@ -1558,6 +1742,8 @@ public partial class PlanificadorRutas : ComponentBase if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null) return; + _mostrarElevacionesRuta = _mostrarElevacionesRutaConfiguracion; + if (!_aplicandoRegistroHistorial) LimpiarSeleccionHistorial(); @@ -1613,6 +1799,7 @@ public partial class PlanificadorRutas : ComponentBase ActivarRangoItinerarios = _activarRangoItinerarios, ItinerarioMin = _itinerarioMin, ItinerarioMax = _itinerarioMax, + FiltroSentidoItinerario = _filtroSentidoItinerario, ActivarRangoTransbordos = _activarRangoTransbordos, MinTransbordosPermitidos = _minTransbordosPermitidos, MaxTransbordosPermitidos = _maxTransbordosPermitidos, diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs index 3d5d51a..1bbbaba 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs @@ -38,8 +38,10 @@ public partial class PlanificadorRutas : ComponentBase { if (tramo is null) return; - // Si ya está calculado, nada - if (tramo.MuestrasElevacion is not null && tramo.MuestrasElevacion.Count > 0) return; + // Si la fuente seleccionada ya está calculada, no hace falta cargar las demás. + if (tramo.MuestrasElevacion is { Count: > 0 } + && tramo.MuestrasElevacion.All(muestra => ObtenerElevacionFuenteCalculo(muestra) is not null)) + return; // Necesitamos la geometría if (tramo.Linea is null || tramo.Linea.GetLength(0) <= 0) return; @@ -52,20 +54,20 @@ public partial class PlanificadorRutas : ComponentBase try { var ctNone = CancellationToken.None; - - // Samples bajo demanda usando la fuente G. + var samples = await ConstruirMuestrasElevacionAsync(tramo.Linea, ctNone); tramo.MuestrasElevacion = samples; - var muestrasIdee = samples - .Where(x => x.ElevacionIdee is not null) - .Select(x => x.ElevacionIdee!.Value) + var muestrasFuenteCalculo = samples + .Select(ObtenerElevacionFuenteCalculo) + .Where(elevacion => elevacion is not null) + .Select(elevacion => elevacion!.Value) .ToList(); - tramo.ElevInicio = samples.FirstOrDefault()?.ElevacionIdee; - tramo.ElevFin = samples.LastOrDefault()?.ElevacionIdee; - tramo.ElevMin = muestrasIdee.Count > 0 ? muestrasIdee.Min() : null; - tramo.ElevMax = muestrasIdee.Count > 0 ? muestrasIdee.Max() : null; + tramo.ElevInicio = samples.Count > 0 ? ObtenerElevacionFuenteCalculo(samples[0]) : null; + tramo.ElevFin = samples.Count > 0 ? ObtenerElevacionFuenteCalculo(samples[^1]) : null; + tramo.ElevMin = muestrasFuenteCalculo.Count > 0 ? muestrasFuenteCalculo.Min() : null; + tramo.ElevMax = muestrasFuenteCalculo.Count > 0 ? muestrasFuenteCalculo.Max() : null; } catch { @@ -85,36 +87,15 @@ public partial class PlanificadorRutas : ComponentBase { bool valor => valor, string texto when bool.TryParse(texto, out var valor) => valor, - _ => !_mostrarElevacionesRuta + _ => !_mostrarElevacionesRutaConfiguracion }; - if (_mostrarElevacionesRuta == mostrarElevaciones) + if (_mostrarElevacionesRutaConfiguracion == mostrarElevaciones) return; - _mostrarElevacionesRuta = mostrarElevaciones; + _mostrarElevacionesRutaConfiguracion = mostrarElevaciones; await PersistirConfiguracionAsync(); - if (_opcionesRuta.Count == 0) - { - StateHasChanged(); - return; - } - - _pintandoElevacionesRuta = true; - _mensajeRuta = _mostrarElevacionesRuta - ? "Aplicando pendientes..." - : "Recalculando sin pendientes..."; StateHasChanged(); - await Task.Yield(); - - try - { - await RecalcularRankingTrasCambioEsfuerzoPendienteAsync(forzarRepintado: true); - } - finally - { - _pintandoElevacionesRuta = false; - StateHasChanged(); - } } private async Task CambiarUmbralElevacionRutaAsync(ChangeEventArgs e, bool esSubida) @@ -130,15 +111,7 @@ public partial class PlanificadorRutas : ComponentBase _umbralPendienteBajadaRuta = nuevo; await PersistirConfiguracionAsync(); - - if (esSubida && _mostrarElevacionesRuta) - { - await RecalcularRankingTrasCambioEsfuerzoPendienteAsync(); - } - else if (_mostrarElevacionesRuta) - await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando pendientes..."); - else - StateHasChanged(); + StateHasChanged(); } private async Task RecalcularRankingTrasCambioEsfuerzoPendienteAsync(bool forzarRepintado = false) @@ -385,10 +358,12 @@ public partial class PlanificadorRutas : ComponentBase continue; distanciaTotal += distancia; - if (anterior.ElevacionIdee is null || actual.ElevacionIdee is null) + var elevacionAnterior = ObtenerElevacionFuenteCalculo(anterior); + var elevacionActual = ObtenerElevacionFuenteCalculo(actual); + if (elevacionAnterior is null || elevacionActual is null) continue; - var diferenciaAltura = actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value; + var diferenciaAltura = elevacionActual.Value - elevacionAnterior.Value; var pendiente = diferenciaAltura / distancia * 100.0; var esfuerzo = CalcularEsfuerzoElevacion(distancia, pendiente); if (esfuerzo is null) @@ -427,14 +402,22 @@ public partial class PlanificadorRutas : ComponentBase return $"{(distanciaMetros / 1000.0):0.000} km"; } + private static string FormatearDistanciaMetrosElevacion(double distanciaMetros) + { + if (!double.IsFinite(distanciaMetros) || distanciaMetros <= 0) + return "-"; + + return $"{distanciaMetros:0.00} m"; + } + private void ReiniciarResumenPendientesRuta() { _alturaPendientePositivaSignificativaRutaMetros = 0.0; _distanciaPendientePositivaSignificativaRutaMetros = 0.0; } - private static double? ElevacionPreferidaMapa(MuestraElevacion muestra) - => muestra.ElevacionIdee; + private double? ElevacionPreferidaMapa(MuestraElevacion muestra) + => ObtenerElevacionFuenteCalculo(muestra); private (string Categoria, string Color) ClasificarPendienteMapa(double? pendientePorcentaje) { diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.razor b/RutasDBUS/Components/Pages/PlanificadorRutas.razor index d9c0353..5329467 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.razor +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.razor @@ -248,11 +248,23 @@ } @if (_paradaClicada.Altura > 0) { -
Altitud (J): @_paradaClicada.Altura.ToString("0") m
+
Altura (J): @_paradaClicada.Altura.ToString("0") m
} - @if (_paradaElevG is not null) + @if (_paradaElevIdee is not null) { -
Altitud (G): @_paradaElevG.Value.ToString("0.00", CultureInfo.InvariantCulture) m
+
Altura (G): @_paradaElevIdee.Value.ToString("0.00", CultureInfo.InvariantCulture) m
+ } + @if (_paradaElevLocal5 is not null) + { +
Altura (L5): @_paradaElevLocal5.Value.ToString("0.00", CultureInfo.InvariantCulture) m
+ } + @if (_paradaElevLocal2 is not null) + { +
Altura (L2): @_paradaElevLocal2.Value.ToString("0.00", CultureInfo.InvariantCulture) m
+ } + @if (_paradaElevLocal1 is not null) + { +
Altura (L1): @_paradaElevLocal1.Value.ToString("0.00", CultureInfo.InvariantCulture) m
} @if (_usarHorariosTeoricos) @@ -303,7 +315,7 @@ - @@ -347,10 +359,46 @@
Latitud: @_inspectorLat.ToString("0.000000")
Longitud: @_inspectorLon.ToString("0.000000")
- Altitud (G): + Altura (G): @(_inspectorElevIdee is null - ? "-" - : _inspectorElevIdee.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m") + ? "-" + : _inspectorElevIdee.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m") +
+
+ Altura (L5): + @(_inspectorElevLocal5 is null + ? "-" + : _inspectorElevLocal5.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m") + @if (_inspectorElevIdee is not null && _inspectorElevLocal5 is not null) + { + + - Dif: @((_inspectorElevLocal5.Value - _inspectorElevIdee.Value).ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture)) m + + } +
+
+ Altura (L2): + @(_inspectorElevLocal2 is null + ? "-" + : _inspectorElevLocal2.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m") + @if (_inspectorElevIdee is not null && _inspectorElevLocal2 is not null) + { + + - Dif: @((_inspectorElevLocal2.Value - _inspectorElevIdee.Value).ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture)) m + + } +
+
+ Altura (L1): + @(_inspectorElevLocal1 is null + ? "-" + : _inspectorElevLocal1.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m") + @if (_inspectorElevIdee is not null && _inspectorElevLocal1 is not null) + { + + - Dif: @((_inspectorElevLocal1.Value - _inspectorElevIdee.Value).ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture)) m + + }
@if (_inspectorLugaresCercanos.Count > 0) @@ -401,6 +449,7 @@ Umbral de subida (Esf.): @NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta).ToString("0.#", CultureInfo.InvariantCulture) % + · Cálculo: @EtiquetaFuenteElevacionCalculo @@ -425,7 +474,9 @@