From 7b52dd2c73a12227010f530ff1eab4bd89664144 Mon Sep 17 00:00:00 2001 From: Pedro Date: Mon, 20 Jul 2026 15:29:25 +0200 Subject: [PATCH] =?UTF-8?q?-=20Redise=C3=B1ada=20la=20configuraci=C3=B3n?= =?UTF-8?q?=20de=20pendientes=20con=20un=20panel=20compacto=20accesible=20?= =?UTF-8?q?desde=20la=20rueda.=20-=20Corregida=20la=20posici=C3=B3n=20del?= =?UTF-8?q?=20panel=20para=20que=20no=20salga=20fuera=20de=20la=20pantalla?= =?UTF-8?q?.=20-=20Renombrado=20el=20interruptor=20a=20Visualizar=20Pendie?= =?UTF-8?q?ntes.=20-=20Aplicado=20el=20porcentaje=20de=20subida=20como=20u?= =?UTF-8?q?mbral=20para=20calcular=20el=20esfuerzo.=20-=20Mostrado=20el=20?= =?UTF-8?q?umbral=20de=20subida=20utilizado=20dentro=20de=20la=20tabla=20d?= =?UTF-8?q?e=20elevaci=C3=B3n.=20-=20A=C3=B1adido=20al=20aviso=20el=20tota?= =?UTF-8?q?l=20de=20distancia=20con=20pendiente=20y=20la=20altura=20subida?= =?UTF-8?q?=20en=20metros=20y=20pisos.=20-=20Evitada=20la=20duplicaci?= =?UTF-8?q?=C3=B3n=20de=20acumulados=20al=20repintar=20las=20pendientes.?= =?UTF-8?q?=20-=20Cambiados=20los=20valores=20por=20defecto=20a=20subida?= =?UTF-8?q?=202=20%,=20bajada=205=20%=20y=20coeficiente=208,2.=20-=20A?= =?UTF-8?q?=C3=B1adida=20la=20distancia=20equilibrada=20andando=20al=20det?= =?UTF-8?q?alle=20t=C3=A9cnico.=20-=20Mostradas=20las=20distancias=20en=20?= =?UTF-8?q?kil=C3=B3metros=20con=20tres=20decimales.=20-=20Mostrada=20la?= =?UTF-8?q?=20fecha=20del=20ZIP=20activo=20en=20el=20acceso=20de=20gesti?= =?UTF-8?q?=C3=B3n=20del=20modelo=20DBUS.=20-=20A=C3=B1adidas=20migracione?= =?UTF-8?q?s=20para=20actualizar=20los=20antiguos=20valores=20predetermina?= =?UTF-8?q?dos.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- RutasDBUS/Components/App.razor | 4 +- .../Pages/PlanificadorRutas.CicloVidaYMapa.cs | 6 +- .../Pages/PlanificadorRutas.Estado.cs | 79 ++++-- .../PlanificadorRutas.Geocodificacion.cs | 6 +- .../Pages/PlanificadorRutas.Itinerarios.cs | 28 ++- .../PlanificadorRutas.ModelosHorarios.cs | 32 +++ .../Pages/PlanificadorRutas.Persistencia.cs | 38 ++- .../Pages/PlanificadorRutas.Planificacion.cs | 88 +++++-- .../Pages/PlanificadorRutas.Renderizado.cs | 122 ++++++++- .../Components/Pages/PlanificadorRutas.razor | 222 +++++++++++------ .../Modelos/Planificacion/ParadaCandidata.cs | 2 +- .../GeocodificadorLocalServicio.cs | 71 ++++++ .../IGeocodificadorLocalServicio.cs | 7 + .../Planificacion/UtilidadesTransitoDbus.cs | 2 +- RutasDBUS/wwwroot/app.css | 231 +++++++++++++++--- RutasDBUS/wwwroot/app.js | 189 +++++++++++++- 16 files changed, 941 insertions(+), 186 deletions(-) diff --git a/RutasDBUS/Components/App.razor b/RutasDBUS/Components/App.razor index 38ecdf7..1e17520 100644 --- a/RutasDBUS/Components/App.razor +++ b/RutasDBUS/Components/App.razor @@ -7,14 +7,14 @@ - + - + diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs index 54f5261..07f37c0 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using LeafletForBlazor; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; +using RutasDBUS.Modelos.Geocodificacion; using RutasDBUS.Modelos.Planificacion; using RutasDBUS.Modelos.RedBus; using RutasDBUS.Servicios; @@ -174,6 +175,8 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable if (it is null) return; await PintarItItemAsync(it); + var coordenadas = ConstruirCoordenadasItinerario(it); + await FitRoutesJsAsync(coordenadas); } /// /// Quita it seleccionado. @@ -254,7 +257,8 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable private double _inspectorLat, _inspectorLon; private double? _inspectorElev; private double? _inspectorElevIdee; - private string? _inspectorCallejero; + 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 986aa72..47abedd 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs @@ -47,16 +47,22 @@ public partial class PlanificadorRutas : ComponentBase private int _nivelBrilloMapa = 0; // CONFIG + private const double FactorPenalizacionDistanciaPiePorDefecto = 1.35; + private const double CoeficienteEsfuerzoAlturaPorDefecto = 8.2; + private const double UmbralPendienteSubidaRutaPorDefecto = 2.0; + private const double UmbralPendienteBajadaRutaPorDefecto = 5.0; + private const double UmbralPisosPendienteRutaPorDefecto = 2.0; + private const double MetrosPorPiso = 3.0; + private double _velocidadPieKmH = 3.6; - private double _factorPenalizacionDistanciaPie = 1.6; + private double _factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto; private double _parametroPenalizacionDistanciaPieMetros = 100.0; - private const double CoeficienteEsfuerzoAlturaPorDefecto = 5.2; private double _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; private double _pesoTransbordoRankingMinutos = 4.0; private double _factorEquilibradoAndarBus = 12.0; - private string _factorPenalizacionDistanciaPieTextoEdicion = "1.60"; + private string _factorPenalizacionDistanciaPieTextoEdicion = "1.35"; private string _parametroPenalizacionDistanciaPieTextoEdicion = "100"; - private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20"; + private string _coeficienteEsfuerzoAlturaTextoEdicion = "8.20"; private string _pesoTransbordoRankingTextoEdicion = "4,0"; private string _factorEquilibradoAndarBusTextoEdicion = "12"; private double _velocidadBusKmH = 16.0; @@ -222,10 +228,11 @@ public partial class PlanificadorRutas : ComponentBase ? _coeficienteEsfuerzoAltura : CoeficienteEsfuerzoAlturaPorDefecto; - var pendiente = pendientePorcentaje.Value / 100.0; - if (pendiente <= 0) + var umbralSubida = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta); + if (pendientePorcentaje.Value < umbralSubida) return distanciaMetros.Value; + var pendiente = pendientePorcentaje.Value / 100.0; var factorConPotencia = Math.Pow(1.0 + pendiente, coeficiente); var esfuerzo = distanciaMetros.Value * factorConPotencia; return double.IsFinite(esfuerzo) @@ -351,9 +358,18 @@ public partial class PlanificadorRutas : ComponentBase private TimeSpan? _tiempoUltimoCalculoRuta = null; private bool _mostrarElevacionesRuta = false; private bool _pintandoElevacionesRuta = false; + private bool _mostrarConfiguracionPendientesRuta = false; private bool _panelRutasMovilContraido = false; - private double _umbralPendienteSubidaRuta = 3.0; - private double _umbralPendienteBajadaRuta = 3.0; + private double _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto; + private double _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto; + private double _umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto; + private double _alturaPendientePositivaSignificativaRutaMetros = 0.0; + private double _distanciaPendientePositivaSignificativaRutaMetros = 0.0; + + private bool MostrarAvisoPendientesPositivasSignificativas => + _mostrarElevacionesRuta && + _alturaPendientePositivaSignificativaRutaMetros > + (NormalizarUmbralPisosPendienteRuta(_umbralPisosPendienteRuta) * MetrosPorPiso); private readonly Dictionary> _paradasItinerarioVisiblesPorId = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary> _paradasRutaVisiblesPorId = new(StringComparer.OrdinalIgnoreCase); @@ -987,9 +1003,16 @@ public partial class PlanificadorRutas : ComponentBase /// /// Ajusta rutas js. /// - private async Task FitRoutesJsAsync() + private async Task FitRoutesJsAsync(IReadOnlyList? coordenadas = null) { - try { await JS.InvokeVoidAsync("rt.fitRoutes"); } catch { } + try + { + if (coordenadas is null) + await JS.InvokeVoidAsync("rt.fitRoutes"); + else + await JS.InvokeVoidAsync("rt.fitRoutes", coordenadas); + } + catch { } } /// @@ -1043,7 +1066,7 @@ public partial class PlanificadorRutas : ComponentBase { if (d <= 0) return "-"; if (d < 1000) return $"{Math.Truncate(d):0} m"; - return $"{(d / 1000.0):0.00} km"; + return $"{(d / 1000.0):0.000} km"; } /// @@ -1206,18 +1229,35 @@ public partial class PlanificadorRutas : ComponentBase return EstimarDuracionPieSegundos(distanciaCorregida); } + /// + /// Calcula la aportacion de todos los tramos andando a la distancia equilibrada. + /// + private double CalcularDistanciaEquilibradaAndandoAlternativa(AlternativaRuta? alternativa) + { + if (alternativa is null) + return 0; + + var distanciaAndando = alternativa.EsSoloAPie + ? alternativa.DistanciaSoloAPie + : alternativa.DistanciaPieTotalAprox; + var factor = double.IsFinite(_factorEquilibradoAndarBus) + ? Math.Max(0, _factorEquilibradoAndarBus) + : 12.0; + var distanciaEquilibrada = Math.Max(0, distanciaAndando) * factor; + + return double.IsFinite(distanciaEquilibrada) + ? distanciaEquilibrada + : 0; + } + private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa) { if (alternativa is null || alternativa.EsSoloAPie || alternativa.AlternativaBus is null) return double.PositiveInfinity; var distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus); - var distanciaAndando = Math.Max(0, alternativa.DistanciaPieTotalAprox); - var factor = double.IsFinite(_factorEquilibradoAndarBus) - ? Math.Max(0, _factorEquilibradoAndarBus) - : 12.0; - - var score = distanciaBus + (distanciaAndando * factor); + var distanciaEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(alternativa); + var score = distanciaBus + distanciaEquilibradaAndando; return double.IsFinite(score) && score >= 0 ? score : double.PositiveInfinity; @@ -1232,9 +1272,6 @@ public partial class PlanificadorRutas : ComponentBase return $"Score {FormatearDistanciaTarjeta(score)} (x{_factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture)})"; } - private string FactorEquilibradoAndarBusResumen - => _factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture); - private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync() { if (!CriterioPrioritarioEquilibradoActivo) @@ -1572,6 +1609,8 @@ public partial class PlanificadorRutas : ComponentBase _hayRutaValida = false; _tramosRuta.Clear(); _duracionTotalSegundos = 0; + _alturaPendientePositivaSignificativaRutaMetros = 0.0; + _distanciaPendientePositivaSignificativaRutaMetros = 0.0; _altitudOrigenResumenRuta = null; _altitudDestinoResumenRuta = null; _callejeroOrigenResumenRuta = null; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs index f10a4ea..b8d0a5b 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs @@ -76,7 +76,7 @@ public partial class PlanificadorRutas try { await Task.Delay(120, ct); - var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 10, ct); + var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 20, ct); if (ct.IsCancellationRequested || version != _versionSugerenciasBusqueda) return; @@ -118,6 +118,10 @@ public partial class PlanificadorRutas _hayRutaValida = false; await SincronizarMarcadoresOrigenDestinoJsAsync(); + await FitRoutesJsAsync(new List + { + new[] { sugerencia.Latitud, sugerencia.Longitud } + }); StateHasChanged(); } diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs index 2db5773..1a32ea2 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs @@ -232,6 +232,9 @@ public partial class PlanificadorRutas : ComponentBase /// private async Task PintarItFiltro() { + if (!PuedePintarItinerariosFiltrados) + return; + foreach (var it in _itItemsFiltrados) { if (it is null) continue; @@ -289,6 +292,23 @@ public partial class PlanificadorRutas : ComponentBase RegistrarPreview(clave, subId); } } + + /// + /// Reúne la geometría de todos los sentidos incluidos en un elemento de itinerario. + /// + private List ConstruirCoordenadasItinerario(ElementoItinerario it) + { + var coordenadas = new List(); + + foreach (var variante in it.Variantes) + { + var geometria = ConstruirGeometriaCompletaVariante(variante); + if (geometria is not null) + coordenadas.AddRange(LineaToLatLng(geometria)); + } + + return coordenadas; + } private Parada? _paradaPrimerClickPendiente; @@ -609,6 +629,8 @@ public partial class PlanificadorRutas : ComponentBase private string _itFiltro = ""; + private bool PuedePintarItinerariosFiltrados => !string.IsNullOrWhiteSpace(_itFiltro); + private string ItFiltro { get => _itFiltro; @@ -1181,8 +1203,8 @@ public partial class PlanificadorRutas : ComponentBase private bool _elevPopupMinimizado = false; - - private const string _versionApp = "(v-20260716a)"; + + private const string _versionApp = "(v-20260720c)"; private double? _paradaElevG; @@ -1200,7 +1222,7 @@ public partial class PlanificadorRutas : ComponentBase _itinerarioClicado = null; _paradaClicada = null; - _popupInspectorVisible = false; + await CerrarInspectorAsync(); if (_tramoElevDetalle is not null) { diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.ModelosHorarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.ModelosHorarios.cs index 837bdb6..6636af0 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.ModelosHorarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.ModelosHorarios.cs @@ -18,6 +18,38 @@ public partial class PlanificadorRutas private ModeloHorarioDbusInfo? ModeloHorarioActivo => _modelosHorarios.FirstOrDefault(modelo => modelo.Activo); + private string FechaModeloHorarioActivo => + FormatearFechaModeloHorario(ModeloHorarioActivo?.NombreArchivo); + + private static string FormatearFechaModeloHorario(string? nombreArchivo) + { + if (string.IsNullOrWhiteSpace(nombreArchivo)) + return "Sin modelo cargado"; + + for (var indice = nombreArchivo.Length - 8; indice >= 0; indice--) + { + if (indice > 0 && char.IsDigit(nombreArchivo[indice - 1])) + continue; + + var fin = indice + 8; + if (fin < nombreArchivo.Length && char.IsDigit(nombreArchivo[fin])) + continue; + + var candidata = nombreArchivo.Substring(indice, 8); + if (DateTime.TryParseExact( + candidata, + "yyyyMMdd", + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out _)) + { + return candidata; + } + } + + return "Fecha no disponible"; + } + private void AbrirModelosHorariosPanel() { _mostrarModelosHorariosPanel = true; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs index b74d01c..d44cb6a 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs @@ -38,6 +38,7 @@ public partial class PlanificadorRutas public double? FactorEquilibradoAndarBus { get; set; } public double? UmbralPendienteSubidaRuta { get; set; } public double? UmbralPendienteBajadaRuta { get; set; } + public double? UmbralPisosPendienteRuta { get; set; } public double VelocidadPieKmH { get; set; } public double VelocidadBusKmH { get; set; } public int ElevMaxPuntosPorTramo { get; set; } @@ -140,6 +141,7 @@ public partial class PlanificadorRutas FactorEquilibradoAndarBus = _factorEquilibradoAndarBus, UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta, UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta, + UmbralPisosPendienteRuta = _umbralPisosPendienteRuta, VelocidadPieKmH = _velocidadPieKmH, VelocidadBusKmH = _velocidadBusKmH, ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo, @@ -185,11 +187,17 @@ public partial class PlanificadorRutas _horaSimulada = horaSimulada; if (config.FactorPenalizacionDistanciaPie is { } factor && double.IsFinite(factor)) - _factorPenalizacionDistanciaPie = Math.Abs(factor - 1.3) < 0.0001 ? 1.6 : factor; + _factorPenalizacionDistanciaPie = + Math.Abs(factor - 1.3) < 0.0001 || Math.Abs(factor - 1.6) < 0.0001 + ? FactorPenalizacionDistanciaPiePorDefecto + : factor; if (config.ParametroPenalizacionDistanciaPieMetros is { } parametro && double.IsFinite(parametro)) _parametroPenalizacionDistanciaPieMetros = parametro; if (config.CoeficienteEsfuerzoAltura is { } coeficienteAltura && double.IsFinite(coeficienteAltura)) - _coeficienteEsfuerzoAltura = Math.Abs(coeficienteAltura - 1.6) < 0.0001 + _coeficienteEsfuerzoAltura = + Math.Abs(coeficienteAltura - 1.6) < 0.0001 || + Math.Abs(coeficienteAltura - 5.2) < 0.0001 || + Math.Abs(coeficienteAltura - 9.2) < 0.0001 ? CoeficienteEsfuerzoAlturaPorDefecto : coeficienteAltura; if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo)) @@ -197,9 +205,17 @@ public partial class PlanificadorRutas if (config.FactorEquilibradoAndarBus is { } factorEquilibrado && double.IsFinite(factorEquilibrado)) _factorEquilibradoAndarBus = Math.Max(0, factorEquilibrado); if (config.UmbralPendienteSubidaRuta is { } umbralSubida && double.IsFinite(umbralSubida)) - _umbralPendienteSubidaRuta = NormalizarUmbralPendienteRuta(umbralSubida); + _umbralPendienteSubidaRuta = Math.Abs(umbralSubida - 3.0) < 0.0001 + ? UmbralPendienteSubidaRutaPorDefecto + : NormalizarUmbralPendienteRuta(umbralSubida); if (config.UmbralPendienteBajadaRuta is { } umbralBajada && double.IsFinite(umbralBajada)) - _umbralPendienteBajadaRuta = NormalizarUmbralPendienteRuta(umbralBajada); + _umbralPendienteBajadaRuta = + Math.Abs(umbralBajada - 3.0) < 0.0001 || + Math.Abs(umbralBajada - 6.0) < 0.0001 + ? UmbralPendienteBajadaRutaPorDefecto + : NormalizarUmbralPendienteRuta(umbralBajada); + if (config.UmbralPisosPendienteRuta is { } umbralPisos && double.IsFinite(umbralPisos)) + _umbralPisosPendienteRuta = NormalizarUmbralPisosPendienteRuta(umbralPisos); _velocidadPieKmH = Math.Clamp(config.VelocidadPieKmH, 0.5, 25.0); _velocidadBusKmH = Math.Clamp( Math.Abs(config.VelocidadBusKmH - 15.0) < 0.0001 || @@ -298,13 +314,14 @@ public partial class PlanificadorRutas _minTransbordosPermitidos = 0; _maxTransbordosPermitidos = 3; - _factorPenalizacionDistanciaPie = 1.6; + _factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto; _parametroPenalizacionDistanciaPieMetros = 100.0; _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; _pesoTransbordoRankingMinutos = 4.0; _factorEquilibradoAndarBus = 12.0; - _umbralPendienteSubidaRuta = 3.0; - _umbralPendienteBajadaRuta = 3.0; + _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto; + _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto; + _umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto; _velocidadPieKmH = 3.6; _velocidadBusKmH = 16.0; _minutosTransbordo = 5; @@ -347,13 +364,14 @@ public partial class PlanificadorRutas _minTransbordosPermitidos = 0; _maxTransbordosPermitidos = 3; - _factorPenalizacionDistanciaPie = 1.6; + _factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto; _parametroPenalizacionDistanciaPieMetros = 100.0; _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; _pesoTransbordoRankingMinutos = 4.0; _factorEquilibradoAndarBus = 12.0; - _umbralPendienteSubidaRuta = 3.0; - _umbralPendienteBajadaRuta = 3.0; + _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto; + _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto; + _umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto; _velocidadPieKmH = 3.6; _velocidadBusKmH = 16.0; _minutosTransbordo = 5; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs index 03618fd..0435fd3 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using LeafletForBlazor; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; +using RutasDBUS.Modelos.Geocodificacion; using RutasDBUS.Modelos.Planificacion; using RutasDBUS.Modelos.RedBus; using RutasDBUS.Servicios; @@ -1267,10 +1268,26 @@ public partial class PlanificadorRutas : ComponentBase /// private async Task MostrarInspectorAsync(double lat, double lon) { + var versionInspector = ++_versionInspector; _inspectorLat = lat; _inspectorLon = lon; - _inspectorCallejero = null; - + _inspectorElev = null; + _inspectorElevIdee = null; + _inspectorLugaresCercanos = Array.Empty(); + _popupInspectorVisible = true; + + await ActualizarMarcadoresInspectorJsAsync(); + StateHasChanged(); + + var tareaLugares = GeocodificadorLocal.BuscarCercanosAsync( + lat, + lon, + radioMetros: 20, + maxResultados: 3, + cancellationToken: CancellationToken.None); + + double? elevacionOpen = null; + double? elevacionIdee = null; try { // ------------------------- @@ -1279,7 +1296,7 @@ public partial class PlanificadorRutas : ComponentBase var kOpen = ElevKey(lat, lon); if (_elevCache.TryGetValue(kOpen, out var cachedOpen)) { - _inspectorElev = cachedOpen; + elevacionOpen = cachedOpen; } else { @@ -1287,7 +1304,7 @@ public partial class PlanificadorRutas : ComponentBase new List<(double lat, double lon)> { (lat, lon) }, CancellationToken.None); - _inspectorElev = _elevCache.TryGetValue(ElevKey(lat, lon), out var elevOpen) + elevacionOpen = _elevCache.TryGetValue(ElevKey(lat, lon), out var elevOpen) ? elevOpen : (double?)null; } @@ -1298,7 +1315,7 @@ public partial class PlanificadorRutas : ComponentBase var kIdee = ClaveElevacionIdee(lat, lon); if (_elevIdeeCache.TryGetValue(kIdee, out var cachedIdee)) { - _inspectorElevIdee = cachedIdee; + elevacionIdee = cachedIdee; } else { @@ -1306,33 +1323,72 @@ public partial class PlanificadorRutas : ComponentBase new List<(double lat, double lon)> { (lat, lon) }, CancellationToken.None); - _inspectorElevIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var elevIdee) + elevacionIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var elevIdee) ? elevIdee : (double?)null; } } catch { - _inspectorElev = null; - _inspectorElevIdee = null; } - + try { - var lugar = await GeocodificadorLocal.BuscarMasCercanoAsync( - lat, - lon, - CancellationToken.None); - _inspectorCallejero = FormatearLugarCallejeroResumen(lugar); + var lugares = await tareaLugares; + if (versionInspector != _versionInspector) + return; + + _inspectorLugaresCercanos = lugares; } catch { - _inspectorCallejero = null; + if (versionInspector != _versionInspector) + return; + + _inspectorLugaresCercanos = Array.Empty(); } - _popupInspectorVisible = true; + if (versionInspector != _versionInspector) + return; + + _inspectorElev = elevacionOpen; + _inspectorElevIdee = elevacionIdee; + await ActualizarMarcadoresInspectorJsAsync(); StateHasChanged(); } + + private async Task ActualizarMarcadoresInspectorJsAsync() + { + var lugares = _inspectorLugaresCercanos + .Select(cercano => new + { + latitud = cercano.Lugar.Latitud, + longitud = cercano.Lugar.Longitud, + texto = FormatearLugarCallejeroResumen(cercano) + }) + .ToList(); + + try + { + await JS.InvokeVoidAsync( + "rt.setInspectorMarkers", + _inspectorLat, + _inspectorLon, + lugares); + } + catch + { + } + } + + private async Task CerrarInspectorAsync() + { + _versionInspector++; + _popupInspectorVisible = false; + _inspectorLugaresCercanos = Array.Empty(); + + try { await JS.InvokeVoidAsync("rt.clearInspectorMarkers"); } catch { } + } /// diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs index 194ac0d..e44a8de 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs @@ -77,7 +77,7 @@ public partial class PlanificadorRutas : ComponentBase }; await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta - ? "Aplicando elevaciones..." + ? "Aplicando pendientes..." : "Representando ruta seleccionada..."); } @@ -96,11 +96,21 @@ public partial class PlanificadorRutas : ComponentBase await PersistirConfiguracionAsync(); if (_mostrarElevacionesRuta) - await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando elevaciones..."); + await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando pendientes..."); else StateHasChanged(); } + private async Task CambiarUmbralPisosPendienteRutaAsync(ChangeEventArgs e) + { + _umbralPisosPendienteRuta = ParseUmbralPisosPendienteRuta( + e.Value, + _umbralPisosPendienteRuta); + + await PersistirConfiguracionAsync(); + StateHasChanged(); + } + private async Task RepintarRutaSeleccionadaPorElevacionesAsync(string mensaje) { if (_opcionesRuta.Count == 0 || @@ -111,6 +121,7 @@ public partial class PlanificadorRutas : ComponentBase return; } + ReiniciarResumenPendientesRuta(); _pintandoElevacionesRuta = true; _mensajeRuta = mensaje; StateHasChanged(); @@ -143,7 +154,24 @@ public partial class PlanificadorRutas : ComponentBase private static double NormalizarUmbralPendienteRuta(double valor) => double.IsFinite(valor) ? Math.Clamp(Math.Abs(valor), 0.0, 50.0) - : 3.0; + : UmbralPendienteSubidaRutaPorDefecto; + + private static double ParseUmbralPisosPendienteRuta(object? valor, double actual) + { + var texto = Convert.ToString(valor, CultureInfo.InvariantCulture)?.Replace(',', '.'); + if (!double.TryParse(texto, NumberStyles.Float, CultureInfo.InvariantCulture, out var nuevo) || + !double.IsFinite(nuevo)) + { + return NormalizarUmbralPisosPendienteRuta(actual); + } + + return NormalizarUmbralPisosPendienteRuta(nuevo); + } + + private static double NormalizarUmbralPisosPendienteRuta(double valor) + => double.IsFinite(valor) + ? Math.Clamp(Math.Abs(valor), 0.0, 100.0) + : UmbralPisosPendienteRutaPorDefecto; private async Task DrawWalkingRouteJsAsync( string id, @@ -152,9 +180,17 @@ public partial class PlanificadorRutas : ComponentBase int versionPintado, bool arrowEnd = true) { - var segmentosElevacion = await ConstruirSegmentosElevacionMapaAsync(linea, CancellationToken.None); + var resultadoElevacion = await ConstruirSegmentosElevacionMapaAsync(linea, CancellationToken.None); ThrowIfPintadoObsoleto(versionPintado); + if (_mostrarElevacionesRuta) + { + _alturaPendientePositivaSignificativaRutaMetros += + resultadoElevacion.AlturaPositivaSignificativaMetros; + _distanciaPendientePositivaSignificativaRutaMetros += + resultadoElevacion.DistanciaPositivaSignificativaMetros; + } + await DrawRouteJsAsync( id, linea, @@ -164,20 +200,25 @@ public partial class PlanificadorRutas : ComponentBase dashArray: DashArrayPiePunteado, arrowEnd: arrowEnd, lineCap: "round", - elevationSegments: segmentosElevacion); + elevationSegments: resultadoElevacion.Segmentos); } - private async Task?> ConstruirSegmentosElevacionMapaAsync(double[,] linea, CancellationToken ct) + private async Task<( + List? Segmentos, + double AlturaPositivaSignificativaMetros, + double DistanciaPositivaSignificativaMetros)> + ConstruirSegmentosElevacionMapaAsync(double[,] linea, CancellationToken ct) { if (!_mostrarElevacionesRuta || linea.GetLength(0) < 2) - return null; + return (null, 0.0, 0.0); var muestras = await ConstruirMuestrasElevacionAsync(linea, ct); if (muestras.Count < 2) - return null; + return (null, 0.0, 0.0); var segmentos = new List(); var hayPendienteCalculada = false; + var totalesPendientePositiva = CalcularTotalesPendientePositivaSignificativa(muestras); for (var i = 1; i < muestras.Count; i++) { @@ -200,7 +241,8 @@ public partial class PlanificadorRutas : ComponentBase if (elevAnterior is not null && elevActual is not null) { - pendiente = (elevActual.Value - elevAnterior.Value) / distancia * 100.0; + var diferenciaAltura = elevActual.Value - elevAnterior.Value; + pendiente = diferenciaAltura / distancia * 100.0; hayPendienteCalculada = true; } @@ -224,9 +266,64 @@ public partial class PlanificadorRutas : ComponentBase }); } - return hayPendienteCalculada && segmentos.Count > 0 - ? segmentos - : null; + return ( + hayPendienteCalculada && segmentos.Count > 0 ? segmentos : null, + totalesPendientePositiva.AlturaMetros, + totalesPendientePositiva.DistanciaMetros); + } + + private (double DistanciaMetros, double AlturaMetros) + CalcularTotalesPendientePositivaSignificativa(IReadOnlyList? muestras) + { + if (muestras is null || muestras.Count < 2) + return (0.0, 0.0); + + var distanciaTotal = 0.0; + var alturaTotal = 0.0; + var umbralSubida = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta); + + for (var i = 1; i < muestras.Count; i++) + { + var anterior = muestras[i - 1]; + var actual = muestras[i]; + if (anterior.ElevacionIdee is null || actual.ElevacionIdee is null) + continue; + + var distancia = CalcularDistanciaMetros( + anterior.Latitud, + anterior.Longitud, + actual.Latitud, + actual.Longitud); + if (distancia <= 0) + continue; + + var diferenciaAltura = actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value; + var pendiente = diferenciaAltura / distancia * 100.0; + if (pendiente <= umbralSubida) + continue; + + distanciaTotal += distancia; + alturaTotal += Math.Max(0, diferenciaAltura); + } + + return (distanciaTotal, alturaTotal); + } + + private static string FormatearDistanciaResumenElevacion(double distanciaMetros) + { + if (!double.IsFinite(distanciaMetros) || distanciaMetros <= 0) + return "-"; + + if (distanciaMetros < 950) + return $"{distanciaMetros:0.00} m"; + + return $"{(distanciaMetros / 1000.0):0.000} km"; + } + + private void ReiniciarResumenPendientesRuta() + { + _alturaPendientePositivaSignificativaRutaMetros = 0.0; + _distanciaPendientePositivaSignificativaRutaMetros = 0.0; } private static double? ElevacionPreferidaMapa(MuestraElevacion muestra) @@ -259,6 +356,7 @@ public partial class PlanificadorRutas : ComponentBase { var versionPintado = IniciarNuevaVersionPintadoRuta(); var mensajeAnterior = _mensajeRuta; + ReiniciarResumenPendientesRuta(); try { diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.razor b/RutasDBUS/Components/Pages/PlanificadorRutas.razor index a3480f7..6f92d82 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.razor +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.razor @@ -60,7 +60,9 @@ - + @@ -344,29 +346,38 @@
Altitud (G): @(_inspectorElevIdee is null ? "-" : _inspectorElevIdee.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m") -
- + - + @if (_inspectorLugaresCercanos.Count > 0) + { +
+ @for (var indiceLugar = 0; indiceLugar < _inspectorLugaresCercanos.Count; indiceLugar++) + { + var lugarCercano = _inspectorLugaresCercanos[indiceLugar]; +
+ @(indiceLugar + 1) + @FormatearLugarCallejeroResumen(lugarCercano) +
+ } +
+ } + + + - } + } } @@ -427,6 +438,11 @@ } else { +
+ Umbral de subida (Esf.): + @NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta).ToString("0.#", CultureInfo.InvariantCulture) % +
+ -
O: (ini→fin) = @F(OIni) → @F(OFin) · (min/max) = @F(OMin) / @F(OMax)
-
G: (ini→fin) = @F(GIni) → @F(GFin) · (min/max) = @F(GMin) / @F(GMax)
+
O: (inicio → fin) = @F(OIni) → @F(OFin) · (mínimo/máximo) = @F(OMin) / @F(OMax)
+
G: (inicio → fin) = @F(GIni) → @F(GFin) · (mínimo/máximo) = @F(GMin) / @F(GMax)
- Suma distancia de puntos: @FDist(totalDist) + Suma de distancias entre puntos: @FormatearDistanciaResumenElevacion(totalDist)
- Suma dist pend positiva: @FDist(totalDistPendPositiva) + Suma de distancias con pendiente positiva: @FormatearDistanciaResumenElevacion(totalDistPendPositiva)
- Suma alturas pend positiva: @(totalAlturaPendPositiva > 0 - ? totalAlturaPendPositiva.ToString("0.00", CultureInfo.InvariantCulture) + " m (" + (totalAlturaPendPositiva / 3.0).ToString("0.0", CultureInfo.InvariantCulture) + " pisos)" + Suma de alturas con pendiente positiva: @(totalAlturaPendPositiva > 0 + ? totalAlturaPendPositiva.ToString("0.00", CultureInfo.InvariantCulture) + " m (" + (totalAlturaPendPositiva / MetrosPorPiso).ToString("0.0", CultureInfo.InvariantCulture) + " pisos)" : "-")
Suma de esfuerzos positivos: @(hayEsfuerzoPositivo ? totalEsfuerzoPositivo.ToString("0.00", CultureInfo.InvariantCulture) : "-") - total: @(hayEsfuerzo ? totalEsfuerzo.ToString("0.00", CultureInfo.InvariantCulture) : "-") + Total: @(hayEsfuerzo ? totalEsfuerzo.ToString("0.00", CultureInfo.InvariantCulture) : "-")
} @@ -670,33 +673,74 @@ - @(_pintandoElevacionesRuta ? "Elevaciones..." : "Elevaciones") + Visualizar Pendientes -
- - +
+ + + @if (_mostrarConfiguracionPendientesRuta) + { + + }
@@ -768,6 +812,20 @@ { var alt = _opcionesRuta[_indiceAlternativaSeleccionada]; + @if (MostrarAvisoPendientesPositivasSignificativas) + { +
+
Solución con pendientes positivas significativas.
+
+
- Distancia con pendiente: @FormatearDistanciaResumenElevacion(_distanciaPendientePositivaSignificativaRutaMetros)
+
+ - Altura subida: @_alturaPendientePositivaSignificativaRutaMetros.ToString("0.00", CultureInfo.InvariantCulture) m + (@((_alturaPendientePositivaSignificativaRutaMetros / MetrosPorPiso).ToString("0.0", CultureInfo.InvariantCulture)) pisos) +
+
+
+ } + @* ========= RESUMEN BONITO ARRIBA (único) ========= *@

@if (alt.EsSoloAPie) @@ -928,6 +986,7 @@ : _duracionTotalSegundos; var dRecta = DistanciaLineaRectaOrigenDestino(); var dCorregida = CalcularDistanciaCorregidaAlternativa(altSeleccionada); + var dEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(altSeleccionada); var xyzOrigen = FormatearResumenXYZOrigen(); var xyzDestino = FormatearResumenXYZDestino(); var callejeroOrigen = FormatearResumenCallejeroOrigen(); @@ -948,7 +1007,7 @@

} - @if (dRecta > 0 || dCorregida > 0) + @if (dRecta > 0 || dCorregida > 0 || dEquilibradaAndando > 0) {
@if (dRecta > 0) @@ -959,6 +1018,10 @@ {
d.Corregida a pie: @FormatearDistanciaDetalleRuta(dCorregida)
} + @if (dEquilibradaAndando > 0) + { +
d.Equilib. A: @FormatearDistanciaDetalleRuta(dEquilibradaAndando)
+ } @if (!string.IsNullOrWhiteSpace(scoreEquilibradoSeleccionado)) {
Score equilibrado: @scoreEquilibradoSeleccionado
@@ -1177,6 +1240,22 @@
+
+
+ +
+
+ @*
@@ -1254,22 +1333,6 @@
-
-
- -
-
- @* ========================= RANGO DE LÍNEAS ========================= *@ @@ -1470,13 +1533,10 @@ Factor - - 1 m andando = @FactorEquilibradoAndarBusResumen m bus - diff --git a/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs b/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs index 9361648..bf90651 100644 --- a/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs +++ b/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs @@ -23,7 +23,7 @@ public sealed class ParametrosPlanificadorRuta public int MinutosPorTransbordo { get; set; } = 5; public int MargenTransbordoSegundos { get; set; } = 120; public int SegundosPorParadaBus { get; set; } = 30; - public double FactorPenalizacionDistanciaPie { get; set; } = 1.6; + public double FactorPenalizacionDistanciaPie { get; set; } = 1.35; public double ParametroPenalizacionDistanciaPieMetros { get; set; } = 100.0; public double PesoTransbordoRankingMinutos { get; set; } = 4.0; public double FactorEquilibradoAndarBus { get; set; } = 12.0; diff --git a/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs b/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs index dbc5ca6..78a8a22 100644 --- a/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs +++ b/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs @@ -13,6 +13,7 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio private readonly IWebHostEnvironment _entorno; private readonly SemaphoreSlim _semaforoCarga = new(1, 1); private readonly ConcurrentDictionary _cacheCercanos = new(StringComparer.Ordinal); + private readonly ConcurrentDictionary> _cacheCercanosPorRadio = new(StringComparer.Ordinal); private IReadOnlyList? _indice; public GeocodificadorLocalServicio(IWebHostEnvironment entorno) @@ -109,6 +110,73 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio return cercano; } + public async Task> BuscarCercanosAsync( + double latitud, + double longitud, + double radioMetros = 20, + int maxResultados = 3, + CancellationToken cancellationToken = default) + { + if (!double.IsFinite(latitud) || !double.IsFinite(longitud)) + return Array.Empty(); + + radioMetros = double.IsFinite(radioMetros) + ? Math.Clamp(radioMetros, 0, 1000) + : 20; + maxResultados = Math.Clamp(maxResultados, 1, 10); + + var clave = ClaveCercanos(latitud, longitud, radioMetros, maxResultados); + if (_cacheCercanosPorRadio.TryGetValue(clave, out var cacheados)) + return cacheados; + + var indice = await ObtenerIndiceAsync(cancellationToken); + if (indice.Count == 0) + return Array.Empty(); + + LugarGeocodificadoCercano? masCercano = null; + var dentroDelRadio = new List(); + + foreach (var entrada in indice) + { + cancellationToken.ThrowIfCancellationRequested(); + + var lugar = entrada.Lugar; + var distancia = DistanciaHaversineMetros(latitud, longitud, lugar.Latitud, lugar.Longitud); + if (masCercano is null || distancia < masCercano.DistanciaMetros) + { + masCercano = new LugarGeocodificadoCercano + { + Lugar = lugar, + DistanciaMetros = distancia + }; + } + + if (distancia <= radioMetros) + { + dentroDelRadio.Add(new LugarGeocodificadoCercano + { + Lugar = lugar, + DistanciaMetros = distancia + }); + } + } + + IReadOnlyList resultado = dentroDelRadio.Count > 0 + ? dentroDelRadio + .OrderBy(x => x.DistanciaMetros) + .Take(maxResultados) + .ToArray() + : masCercano is null + ? Array.Empty() + : new[] { masCercano! }; + + if (resultado.Count > 0) + _cacheCercanos.TryAdd(ClaveCercano(latitud, longitud), resultado[0]); + + _cacheCercanosPorRadio.TryAdd(clave, resultado); + return resultado; + } + private async Task> ObtenerIndiceAsync(CancellationToken cancellationToken) { if (_indice is not null) @@ -411,6 +479,9 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio private static string ClaveCercano(double latitud, double longitud) => $"{latitud.ToString("0.000000", CultureInfo.InvariantCulture)}|{longitud.ToString("0.000000", CultureInfo.InvariantCulture)}"; + private static string ClaveCercanos(double latitud, double longitud, double radioMetros, int maxResultados) + => $"{ClaveCercano(latitud, longitud)}|{radioMetros.ToString("0.##", CultureInfo.InvariantCulture)}|{maxResultados}"; + private static double DistanciaHaversineMetros(double lat1, double lon1, double lat2, double lon2) { const double radioTierraMetros = 6371000.0; diff --git a/RutasDBUS/Servicios/Geocodificacion/IGeocodificadorLocalServicio.cs b/RutasDBUS/Servicios/Geocodificacion/IGeocodificadorLocalServicio.cs index 0898486..684d165 100644 --- a/RutasDBUS/Servicios/Geocodificacion/IGeocodificadorLocalServicio.cs +++ b/RutasDBUS/Servicios/Geocodificacion/IGeocodificadorLocalServicio.cs @@ -14,5 +14,12 @@ public interface IGeocodificadorLocalServicio double longitud, CancellationToken cancellationToken = default); + Task> BuscarCercanosAsync( + double latitud, + double longitud, + double radioMetros = 20, + int maxResultados = 3, + CancellationToken cancellationToken = default); + Task ContarLugaresAsync(CancellationToken cancellationToken = default); } diff --git a/RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs b/RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs index f48729c..d0a3f82 100644 --- a/RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs +++ b/RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs @@ -313,7 +313,7 @@ internal static class UtilidadesTransitoDbus var factor = double.IsFinite(parametros.FactorPenalizacionDistanciaPie) ? parametros.FactorPenalizacionDistanciaPie - : 1.6; + : 1.35; var parametroBaseMetros = double.IsFinite(parametros.ParametroPenalizacionDistanciaPieMetros) ? Math.Abs(parametros.ParametroPenalizacionDistanciaPieMetros) : 100.0; diff --git a/RutasDBUS/wwwroot/app.css b/RutasDBUS/wwwroot/app.css index 9d5f1d7..73efd3f 100644 --- a/RutasDBUS/wwwroot/app.css +++ b/RutasDBUS/wwwroot/app.css @@ -13,6 +13,63 @@ font-size: 1rem; } +.inspector-address-list { + display: grid; + gap: 4px; + margin: 5px 0 7px; +} + +.popup-parada-body .inspector-address-item { + display: grid; + grid-template-columns: 22px minmax(0, 1fr); + align-items: center; + gap: 6px; + margin: 0; + line-height: 1.25; +} + +.inspector-address-number, +.rt-inspector-place-marker { + display: grid; + place-items: center; + box-sizing: border-box; + border: 2px solid #ecfeff; + border-radius: 50%; + color: #ffffff; + background: #0f766e; + font-weight: 800; + line-height: 1; + box-shadow: 0 0 0 2px rgba(15, 23, 42, .78), 0 4px 10px rgba(15, 23, 42, .3); +} + +.inspector-address-number { + width: 20px; + height: 20px; + font-size: .68rem; +} + +.inspector-address-item > span:last-child { + min-width: 0; + overflow-wrap: anywhere; +} + +.rt-inspector-place-icon { + border: 0 !important; + background: transparent !important; +} + +.rt-inspector-place-marker { + width: 24px; + height: 24px; + font-size: .72rem; +} + +.rt-inspector-tooltip { + max-width: min(280px, calc(100vw - 28px)); + white-space: normal; + font-size: .75rem; +} + .history-open-icon { display: inline-block; font-size: 0.9rem; @@ -247,6 +304,14 @@ h1:focus { background: #1d4ed8; color: #fff; } + + .btn-ghost:disabled, + .btn-ghost:disabled:hover { + background: #111827; + color: #94a3b8; + cursor: not-allowed; + opacity: .48; + } .page, main, .full-page, .content, article.content { overflow: hidden; } @@ -810,9 +875,9 @@ html, body { .route-visual-options { display: flex; align-items: center; - justify-content: flex-end; - flex-wrap: wrap; - gap: .35rem .5rem; + justify-content: flex-start; + flex-wrap: nowrap; + gap: .35rem; margin: -.15rem 0 .4rem; } @@ -877,27 +942,109 @@ html, body { opacity: .55; } -.route-elevation-thresholds { +.route-elevation-settings { + position: relative; display: inline-flex; align-items: center; - gap: .25rem; + margin-left: auto; pointer-events: auto; } -.route-elevation-threshold { - display: inline-flex; - align-items: center; - gap: .18rem; - min-height: 24px; +.route-elevation-settings-trigger { + display: grid; + place-items: center; + width: 26px; + height: 24px; + padding: 0; + border: 1px solid rgba(148, 163, 184, .55); + border-radius: 6px; + background: rgba(2, 6, 23, .78); color: #cbd5e1; - font-size: .64rem; - font-weight: 700; - white-space: nowrap; + cursor: pointer; + font-family: "Segoe UI Symbol", sans-serif; + font-size: 1rem; + line-height: 1; } -.route-elevation-threshold input { - width: 40px; +.route-elevation-settings-trigger:hover, +.route-elevation-settings-trigger--active { + border-color: rgba(56, 189, 248, .9); + background: rgba(30, 64, 175, .55); + color: #f8fafc; +} + +.route-elevation-settings-trigger:disabled { + cursor: default; + opacity: .55; +} + +.route-elevation-settings-popover { + position: absolute; + z-index: 20; + top: calc(100% + 6px); + right: 0; + left: auto; + width: 238px; + max-width: calc(100vw - 24px); + box-sizing: border-box; + padding: .55rem; + border: 1px solid rgba(148, 163, 184, .55); + border-radius: 7px; + background: rgba(15, 23, 42, .99); + box-shadow: 0 14px 32px rgba(0, 0, 0, .5); + color: #e5e7eb; +} + +.route-elevation-settings-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: .5rem; + margin-bottom: .45rem; + font-size: .72rem; + font-weight: 800; +} + +.route-elevation-settings-close { + display: grid; + place-items: center; + width: 22px; height: 22px; + padding: 0; + border: 0; + border-radius: 4px; + background: transparent; + color: #cbd5e1; + cursor: pointer; + font-size: 1rem; + line-height: 1; +} + +.route-elevation-settings-close:hover { + background: rgba(71, 85, 105, .55); + color: #fff; +} + +.route-elevation-settings-fields { + display: grid; + gap: .35rem; +} + +.route-elevation-setting-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 44px 32px; + align-items: center; + gap: .35rem; + min-height: 26px; + color: #cbd5e1; + font-size: .68rem; + font-weight: 700; +} + +.route-elevation-setting-row input { + width: 44px; + height: 22px; + box-sizing: border-box; border: 1px solid rgba(148, 163, 184, .55); border-radius: 6px; background: rgba(2, 6, 23, .78); @@ -909,15 +1056,27 @@ html, body { outline: none; } -.route-elevation-threshold input:focus { +.route-elevation-setting-row input:focus { border-color: rgba(56, 189, 248, .95); box-shadow: 0 0 0 2px rgba(56, 189, 248, .18); } -.route-elevation-threshold input:disabled { +.route-elevation-setting-row input:disabled { opacity: .55; } +.route-slope-warning { + margin: 0 0 .45rem; + padding: .36rem .5rem; + border-left: 3px solid #f59e0b; + border-radius: 4px; + background: rgba(245, 158, 11, .13); + color: #fde68a; + font-size: .72rem; + font-weight: 800; + line-height: 1.25; +} + /* Lista de alternativas de ruta */ .route-alt-list { @@ -1623,16 +1782,9 @@ html, body { } .config-dual-labeled--equilibrado { - grid-template-columns: minmax(0, 4rem) minmax(0, 4.4rem); + grid-template-columns: minmax(0, 4rem); align-items: end; -} - -.config-equilibrado-help { - align-self: end; - padding-bottom: .12rem; - font-size: .58rem; - line-height: 1.08; - text-align: left; + justify-content: start; } @@ -2837,6 +2989,7 @@ html, body { .route-visual-options { justify-content: flex-start; + flex-wrap: wrap; gap: 6px 10px; margin: 0 0 8px; } @@ -2846,14 +2999,27 @@ html, body { font-size: .72rem; } - .route-elevation-threshold { - min-height: 44px; - font-size: .68rem; + .route-elevation-settings-trigger { + width: 40px; + height: 40px; } - .route-elevation-threshold input { + .route-elevation-settings-popover { + right: 0; + left: auto; + width: 250px; + max-width: calc(100vw - 20px); + } + + .route-elevation-setting-row { + grid-template-columns: minmax(0, 1fr) 48px 36px; + min-height: 40px; + font-size: .72rem; + } + + .route-elevation-setting-row input { width: 48px; - height: 40px; + height: 36px; font-size: .78rem; } @@ -3056,8 +3222,7 @@ html, body { width: 100% !important; } - .config-dual-labeled__field span, - .config-equilibrado-help { + .config-dual-labeled__field span { font-size: .64rem; } diff --git a/RutasDBUS/wwwroot/app.js b/RutasDBUS/wwwroot/app.js index ab132b2..82f14d0 100644 --- a/RutasDBUS/wwwroot/app.js +++ b/RutasDBUS/wwwroot/app.js @@ -96,6 +96,7 @@ window.rt.ensureRouteLayer = function () { window.rt._routeLayer = window.L.layerGroup().addTo(map); } if (!window.rt._routeLines) window.rt._routeLines = {}; + if (!window.rt._routeCoords) window.rt._routeCoords = {}; return true; }; @@ -103,6 +104,7 @@ window.rt.clearRoutes = function () { if (!window.rt.ensureRouteLayer()) return; window.rt._routeLayer.clearLayers(); window.rt._routeLines = {}; + window.rt._routeCoords = {}; if (window.rt._routeStopsLayer) { window.rt._routeStopsLayer.clearLayers(); @@ -123,6 +125,19 @@ window.rt.drawRoute = function (id, coords, options) { delete window.rt._routeLines[arrowId]; } + const routeCoords = Array.isArray(coords) + ? coords + .map(function (point) { + return [Number(point && point[0]), Number(point && point[1])]; + }) + .filter(function (point) { + return Number.isFinite(point[0]) && Number.isFinite(point[1]); + }) + : []; + + if (routeCoords.length > 0) window.rt._routeCoords[id] = routeCoords; + else delete window.rt._routeCoords[id]; + const color = (options && options.color) || "#000"; const weight = (options && options.weight) || 4; const opacity = (options && options.opacity) || 0.9; @@ -307,14 +322,53 @@ window.rt.drawRoute = function (id, coords, options) { } }; -window.rt.fitRoutes = function () { +window.rt.fitRoutes = function (coords) { const map = window.__rtLeafletMap; - if (!map || !window.rt._routeLayer) return; + if (!map || !window.L) return false; - const bounds = window.rt._routeLayer.getBounds(); - if (bounds && bounds.isValid()) { - map.fitBounds(bounds, { padding: [30, 30] }); + const routePoints = []; + if (Array.isArray(coords)) { + coords.forEach(function (point) { + routePoints.push(point); + }); + } else { + if (!window.rt._routeCoords) return false; + Object.values(window.rt._routeCoords).forEach(function (routeCoords) { + if (!Array.isArray(routeCoords)) return; + routeCoords.forEach(function (point) { + routePoints.push(point); + }); + }); } + + const points = routePoints + .filter(function (point) { + return Array.isArray(point) + && point.length >= 2 + && Number.isFinite(Number(point[0])) + && Number.isFinite(Number(point[1])); + }) + .map(function (point) { + return [Number(point[0]), Number(point[1])]; + }); + + if (points.length === 0) return false; + + const visibleBounds = map.getBounds(); + const algunPuntoVisible = visibleBounds && points.some(function (point) { + return visibleBounds.contains(window.L.latLng(point[0], point[1])); + }); + + if (algunPuntoVisible) return false; + + const geometryBounds = window.L.latLngBounds(points); + if (!geometryBounds.isValid()) return false; + + map.panTo(geometryBounds.getCenter(), { + animate: true, + duration: 0.35 + }); + return true; }; window.rt = window.rt || {}; @@ -1084,6 +1138,131 @@ window.rt.clearEndpointMarkers = function (kind) { }; +// ----------------------------------------------------------------------------- +// Punto Z y lugares cercanos del callejero +// ----------------------------------------------------------------------------- +window.rt.ensureInspectorLayer = function () { + const map = window.__rtLeafletMap; + if (!map || !window.L) return false; + + if (!map.getPane("rtInspectorPane")) { + const pane = map.createPane("rtInspectorPane"); + pane.style.zIndex = 790; + } + + if (!window.rt._inspectorLayer) { + window.rt._inspectorLayer = window.L.layerGroup().addTo(map); + } + + return true; +}; + +window.rt.setInspectorMarkers = function (lat, lon, places) { + if (!window.rt.ensureInspectorLayer()) return; + + window.rt._inspectorLayer.clearLayers(); + + const zLat = Number(lat); + const zLon = Number(lon); + if (!Number.isFinite(zLat) || !Number.isFinite(zLon)) return; + + const puntoZ = window.L.circleMarker([zLat, zLon], { + pane: "rtInspectorPane", + radius: 12, + color: "#713f12", + weight: 3, + opacity: 1, + fillColor: "#fde047", + fillOpacity: 0.42, + bubblingMouseEvents: false + }); + puntoZ.bindTooltip("Punto Z", { + direction: "top", + opacity: 0.95, + className: "rt-inspector-tooltip" + }); + puntoZ.addTo(window.rt._inspectorLayer); + + const lugares = Array.isArray(places) ? places.slice(0, 3) : []; + const coincidencias = {}; + lugares.forEach(function (place) { + const placeLat = Number(place && place.latitud); + const placeLon = Number(place && place.longitud); + if (!Number.isFinite(placeLat) || !Number.isFinite(placeLon)) return; + + const clave = `${placeLat.toFixed(6)}|${placeLon.toFixed(6)}`; + coincidencias[clave] = (coincidencias[clave] || 0) + 1; + }); + + const posicionCoincidente = {}; + lugares.forEach(function (place, index) { + const placeLat = Number(place && place.latitud); + const placeLon = Number(place && place.longitud); + if (!Number.isFinite(placeLat) || !Number.isFinite(placeLon)) return; + + const clave = `${placeLat.toFixed(6)}|${placeLon.toFixed(6)}`; + const totalCoincidentes = coincidencias[clave] || 1; + const posicion = posicionCoincidente[clave] || 0; + posicionCoincidente[clave] = posicion + 1; + + let offsetX = 0; + if (totalCoincidentes === 2) + offsetX = posicion === 0 ? -14 : 14; + else if (totalCoincidentes >= 3) + offsetX = [-26, 0, 26][Math.min(posicion, 2)]; + + window.L.polyline( + [[zLat, zLon], [placeLat, placeLon]], + { + pane: "rtInspectorPane", + color: "#0f766e", + weight: 1.5, + opacity: 0.75, + dashArray: "4 5", + interactive: false + }) + .addTo(window.rt._inspectorLayer); + + const numero = index + 1; + const size = 24; + const icon = window.L.divIcon({ + className: "rt-inspector-place-icon", + html: `${numero}`, + iconSize: [size, size], + iconAnchor: [(size / 2) - offsetX, size / 2] + }); + + const texto = place && typeof place.texto === "string" + ? place.texto + : `Lugar ${numero}`; + const marker = window.L.marker([placeLat, placeLon], { + pane: "rtInspectorPane", + icon, + title: texto, + keyboard: false, + bubblingMouseEvents: false, + zIndexOffset: 20 + numero + }); + + const tooltip = document.createElement("span"); + tooltip.textContent = texto; + marker.bindTooltip(tooltip, { + direction: "top", + offset: [offsetX, -12], + opacity: 0.95, + className: "rt-inspector-tooltip" + }); + marker.addTo(window.rt._inspectorLayer); + }); +}; + +window.rt.clearInspectorMarkers = function () { + if (window.rt._inspectorLayer) { + window.rt._inspectorLayer.clearLayers(); + } +}; + + window.rt = window.rt || {}; window.rt.scrollToHorarioProximo = function () {