diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs index ad5ef94..f2f32fe 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs @@ -7,6 +7,7 @@ using LeafletForBlazor; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; using RutasDBUS.Modelos.Planificacion; +using RutasDBUS.Modelos.Geocodificacion; using RutasDBUS.Modelos.RedBus; using RutasDBUS.Servicios; using RutasDBUS.Servicios.Caminata; @@ -344,6 +345,8 @@ public partial class PlanificadorRutas : ComponentBase private double _duracionTotalSegundos = 0; private double? _altitudOrigenResumenRuta = null; private double? _altitudDestinoResumenRuta = null; + private string? _callejeroOrigenResumenRuta = null; + private string? _callejeroDestinoResumenRuta = null; private readonly List _opcionesRuta = new(); private int _indiceAlternativaSeleccionada = 0; @@ -351,6 +354,8 @@ public partial class PlanificadorRutas : ComponentBase private TimeSpan? _tiempoUltimoCalculoRuta = null; private bool _mostrarElevacionesRuta = false; private bool _pintandoElevacionesRuta = false; + private double _umbralPendienteSubidaRuta = 3.0; + private double _umbralPendienteBajadaRuta = 3.0; private readonly Dictionary> _paradasItinerarioVisiblesPorId = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary> _paradasRutaVisiblesPorId = new(StringComparer.OrdinalIgnoreCase); @@ -1037,7 +1042,8 @@ public partial class PlanificadorRutas : ComponentBase double opacity, string? dashArray = null, bool arrowEnd = false, - string? lineCap = null) + string? lineCap = null, + IReadOnlyList? elevationSegments = null) { try { @@ -1045,28 +1051,11 @@ public partial class PlanificadorRutas : ComponentBase "rt.drawRoute", id, LineaToLatLng(linea), - new { color, weight, opacity, dashArray, arrowEnd, lineCap } + new { color, weight, opacity, dashArray, arrowEnd, lineCap, elevationSegments } ); } catch { } } - - private async Task DrawRouteElevationPointsJsAsync(string id, IReadOnlyList points) - { - try - { - await JS.InvokeVoidAsync("rt.drawElevPoints", $"route_elev_{id}", points, new - { - radius = 4, - opacity = 0.98, - fillOpacity = 0.95, - showLabels = false, - showSlopeTooltips = true, - className = "rt-route-elev-point" - }); - } - catch { } - } /// /// Ajusta rutas js. @@ -1163,6 +1152,67 @@ public partial class PlanificadorRutas : ComponentBase return $"D: Y.Lat = {lat} X.Lon = {lon} Z = {alt}"; } + + private string FormatearResumenCallejeroOrigen() + { + if (string.IsNullOrWhiteSpace(_callejeroOrigenResumenRuta)) + return string.Empty; + + return $"O callejero: {_callejeroOrigenResumenRuta}"; + } + + private string FormatearResumenCallejeroDestino() + { + if (string.IsNullOrWhiteSpace(_callejeroDestinoResumenRuta)) + return string.Empty; + + return $"D callejero: {_callejeroDestinoResumenRuta}"; + } + + private static string FormatearLugarCallejeroResumen(LugarGeocodificadoCercano? cercano) + { + if (cercano is null) + return string.Empty; + + var nombre = LimpiarTextoCallejeroResumen(cercano.Lugar.Nombre); + if (string.IsNullOrWhiteSpace(nombre)) + nombre = LimpiarTextoCallejeroResumen(cercano.Lugar.Subtitulo); + + if (string.IsNullOrWhiteSpace(nombre)) + return string.Empty; + + var distancia = FormatearDistanciaCallejeroResumen(cercano.DistanciaMetros); + if (string.IsNullOrWhiteSpace(distancia)) + return nombre; + + return cercano.DistanciaMetros <= 20 + ? $"{nombre} ({distancia})" + : $"mas cercano: {nombre} ({distancia})"; + } + + private static string LimpiarTextoCallejeroResumen(string texto) + { + if (string.IsNullOrWhiteSpace(texto)) + return string.Empty; + + texto = texto + .Replace(" · ", " - ", StringComparison.Ordinal) + .Replace("·", "-", StringComparison.Ordinal) + .Replace("·", "-", StringComparison.Ordinal); + + return Regex.Replace(texto, @"\s+", " ").Trim(); + } + + private static string FormatearDistanciaCallejeroResumen(double distanciaMetros) + { + if (!double.IsFinite(distanciaMetros) || distanciaMetros < 0) + return string.Empty; + + if (distanciaMetros < 1000) + return $"{Math.Round(distanciaMetros):0} m"; + + return $"{(distanciaMetros / 1000.0).ToString("0.000", CultureInfo.InvariantCulture)} km"; + } /// /// Formatea duracion. @@ -1638,6 +1688,8 @@ public partial class PlanificadorRutas : ComponentBase _duracionTotalSegundos = 0; _altitudOrigenResumenRuta = null; _altitudDestinoResumenRuta = null; + _callejeroOrigenResumenRuta = null; + _callejeroDestinoResumenRuta = null; _paradaOrigenSeleccionada = null; _paradaDestinoSeleccionada = null; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs index 5c211ab..787ef24 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs @@ -1261,7 +1261,7 @@ public partial class PlanificadorRutas : ComponentBase private bool _elevPopupMinimizado = false; - private const string _versionApp = "(v-20260709a)"; + private const string _versionApp = "(v-20260710a)"; private double? _paradaElevG; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs index ed02c62..b74d01c 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs @@ -36,6 +36,8 @@ public partial class PlanificadorRutas public double? CoeficienteEsfuerzoAltura { get; set; } public double? PesoTransbordoRankingMinutos { get; set; } public double? FactorEquilibradoAndarBus { get; set; } + public double? UmbralPendienteSubidaRuta { get; set; } + public double? UmbralPendienteBajadaRuta { get; set; } public double VelocidadPieKmH { get; set; } public double VelocidadBusKmH { get; set; } public int ElevMaxPuntosPorTramo { get; set; } @@ -136,6 +138,8 @@ public partial class PlanificadorRutas CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura, PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos, FactorEquilibradoAndarBus = _factorEquilibradoAndarBus, + UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta, + UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta, VelocidadPieKmH = _velocidadPieKmH, VelocidadBusKmH = _velocidadBusKmH, ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo, @@ -192,6 +196,10 @@ public partial class PlanificadorRutas _pesoTransbordoRankingMinutos = Math.Abs(pesoTransbordo - 8.0) < 0.0001 ? 4.0 : Math.Max(0, pesoTransbordo); if (config.FactorEquilibradoAndarBus is { } factorEquilibrado && double.IsFinite(factorEquilibrado)) _factorEquilibradoAndarBus = Math.Max(0, factorEquilibrado); + if (config.UmbralPendienteSubidaRuta is { } umbralSubida && double.IsFinite(umbralSubida)) + _umbralPendienteSubidaRuta = NormalizarUmbralPendienteRuta(umbralSubida); + if (config.UmbralPendienteBajadaRuta is { } umbralBajada && double.IsFinite(umbralBajada)) + _umbralPendienteBajadaRuta = NormalizarUmbralPendienteRuta(umbralBajada); _velocidadPieKmH = Math.Clamp(config.VelocidadPieKmH, 0.5, 25.0); _velocidadBusKmH = Math.Clamp( Math.Abs(config.VelocidadBusKmH - 15.0) < 0.0001 || @@ -295,6 +303,8 @@ public partial class PlanificadorRutas _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; _pesoTransbordoRankingMinutos = 4.0; _factorEquilibradoAndarBus = 12.0; + _umbralPendienteSubidaRuta = 3.0; + _umbralPendienteBajadaRuta = 3.0; _velocidadPieKmH = 3.6; _velocidadBusKmH = 16.0; _minutosTransbordo = 5; @@ -342,6 +352,8 @@ public partial class PlanificadorRutas _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; _pesoTransbordoRankingMinutos = 4.0; _factorEquilibradoAndarBus = 12.0; + _umbralPendienteSubidaRuta = 3.0; + _umbralPendienteBajadaRuta = 3.0; _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 3c00323..25f61fd 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs @@ -331,6 +331,43 @@ public partial class PlanificadorRutas : ComponentBase _altitudDestinoResumenRuta = null; } } + + private async Task ActualizarCallejeroResumenRutaAsync(CancellationToken ct) + { + if (_coordenadasOrigen is null || _coordenadasDestino is null) + { + _callejeroOrigenResumenRuta = null; + _callejeroDestinoResumenRuta = null; + return; + } + + try + { + var origen = await GeocodificadorLocal.BuscarMasCercanoAsync( + _coordenadasOrigen[0], + _coordenadasOrigen[1], + ct); + + var destino = await GeocodificadorLocal.BuscarMasCercanoAsync( + _coordenadasDestino[0], + _coordenadasDestino[1], + ct); + + _callejeroOrigenResumenRuta = FormatearLugarCallejeroResumen(origen); + _callejeroDestinoResumenRuta = FormatearLugarCallejeroResumen(destino); + } + catch + { + _callejeroOrigenResumenRuta = null; + _callejeroDestinoResumenRuta = null; + } + } + + private async Task ActualizarResumenTecnicoRutaAsync(CancellationToken ct) + { + await ActualizarAlturasResumenRutaAsync(ct); + await ActualizarCallejeroResumenRutaAsync(ct); + } private static readonly ConcurrentDictionary _elevIdeeCache = @@ -2170,7 +2207,7 @@ public partial class PlanificadorRutas : ComponentBase _hayRutaValida = false; try { - await ActualizarAlturasResumenRutaAsync(CancellationToken.None); + await ActualizarResumenTecnicoRutaAsync(CancellationToken.None); } catch { @@ -2194,7 +2231,7 @@ public partial class PlanificadorRutas : ComponentBase { await RegistrarEscenarioActualEnHistorialLocalAsync(); } - await ActualizarAlturasResumenRutaAsync(CancellationToken.None); + await ActualizarResumenTecnicoRutaAsync(CancellationToken.None); _tiempoUltimoCalculoRuta = cronometroCalculo.Elapsed; StateHasChanged(); } @@ -2207,7 +2244,7 @@ public partial class PlanificadorRutas : ComponentBase _hayRutaValida = false; try { - await ActualizarAlturasResumenRutaAsync(CancellationToken.None); + await ActualizarResumenTecnicoRutaAsync(CancellationToken.None); } catch { diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs index 4f964b2..69e8c8e 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs @@ -76,6 +76,33 @@ public partial class PlanificadorRutas : ComponentBase _ => !_mostrarElevacionesRuta }; + await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta + ? "Aplicando elevaciones..." + : "Representando ruta seleccionada..."); + } + + private async Task CambiarUmbralElevacionRutaAsync(ChangeEventArgs e, bool esSubida) + { + var actual = esSubida + ? _umbralPendienteSubidaRuta + : _umbralPendienteBajadaRuta; + var nuevo = ParseUmbralPendienteRuta(e.Value, actual); + + if (esSubida) + _umbralPendienteSubidaRuta = nuevo; + else + _umbralPendienteBajadaRuta = nuevo; + + await PersistirConfiguracionAsync(); + + if (_mostrarElevacionesRuta) + await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando elevaciones..."); + else + StateHasChanged(); + } + + private async Task RepintarRutaSeleccionadaPorElevacionesAsync(string mensaje) + { if (_opcionesRuta.Count == 0 || _indiceAlternativaSeleccionada < 0 || _indiceAlternativaSeleccionada >= _opcionesRuta.Count) @@ -85,9 +112,7 @@ public partial class PlanificadorRutas : ComponentBase } _pintandoElevacionesRuta = true; - _mensajeRuta = _mostrarElevacionesRuta - ? "Aplicando elevaciones..." - : "Representando ruta seleccionada..."; + _mensajeRuta = mensaje; StateHasChanged(); await Task.Yield(); @@ -103,6 +128,23 @@ public partial class PlanificadorRutas : ComponentBase } } + private static double ParseUmbralPendienteRuta(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 NormalizarUmbralPendienteRuta(actual); + } + + return NormalizarUmbralPendienteRuta(nuevo); + } + + private static double NormalizarUmbralPendienteRuta(double valor) + => double.IsFinite(valor) + ? Math.Clamp(Math.Abs(valor), 0.0, 50.0) + : 3.0; + private async Task DrawWalkingRouteJsAsync( string id, double[,] linea, @@ -110,7 +152,7 @@ public partial class PlanificadorRutas : ComponentBase int versionPintado, bool arrowEnd = true) { - var puntosElevacion = await ConstruirPuntosElevacionMapaAsync(linea, CancellationToken.None); + var segmentosElevacion = await ConstruirSegmentosElevacionMapaAsync(linea, CancellationToken.None); ThrowIfPintadoObsoleto(versionPintado); await DrawRouteJsAsync( @@ -121,16 +163,11 @@ public partial class PlanificadorRutas : ComponentBase 0.9, dashArray: DashArrayPiePunteado, arrowEnd: arrowEnd, - lineCap: "round"); - - if (puntosElevacion is { Count: > 0 }) - { - ThrowIfPintadoObsoleto(versionPintado); - await DrawRouteElevationPointsJsAsync(id, puntosElevacion); - } + lineCap: "round", + elevationSegments: segmentosElevacion); } - private async Task?> ConstruirPuntosElevacionMapaAsync(double[,] linea, CancellationToken ct) + private async Task?> ConstruirSegmentosElevacionMapaAsync(double[,] linea, CancellationToken ct) { if (!_mostrarElevacionesRuta || linea.GetLength(0) < 2) return null; @@ -139,7 +176,7 @@ public partial class PlanificadorRutas : ComponentBase if (muestras.Count < 2) return null; - var puntos = new List(); + var segmentos = new List(); var hayPendienteCalculada = false; for (var i = 1; i < muestras.Count; i++) @@ -168,10 +205,13 @@ public partial class PlanificadorRutas : ComponentBase } var (categoria, color) = ClasificarPendienteMapa(pendiente); - puntos.Add(new + segmentos.Add(new { - lat = actual.Latitud, - lon = actual.Longitud, + coords = new[] + { + new[] { anterior.Latitud, anterior.Longitud }, + new[] { actual.Latitud, actual.Longitud } + }, color, category = categoria, slope = pendiente, @@ -181,32 +221,28 @@ public partial class PlanificadorRutas : ComponentBase }); } - return hayPendienteCalculada && puntos.Count > 0 - ? puntos + return hayPendienteCalculada && segmentos.Count > 0 + ? segmentos : null; } private static double? ElevacionPreferidaMapa(MuestraElevacion muestra) => muestra.ElevacionIdee ?? muestra.Elevacion; - private static (string Categoria, string Color) ClasificarPendienteMapa(double? pendientePorcentaje) + private (string Categoria, string Color) ClasificarPendienteMapa(double? pendientePorcentaje) { if (pendientePorcentaje is null || !double.IsFinite(pendientePorcentaje.Value)) return ("sin-datos", "#94a3b8"); - if (pendientePorcentaje.Value >= 8.0) - return ("subida-fuerte", "#ef4444"); + var umbralSubida = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta); + var umbralBajada = NormalizarUmbralPendienteRuta(_umbralPendienteBajadaRuta); + if (pendientePorcentaje.Value >= umbralSubida) + return ("subida", "#000000"); - if (pendientePorcentaje.Value >= 3.0) - return ("subida", "#f97316"); + if (pendientePorcentaje.Value <= -umbralBajada) + return ("bajada", "#2563eb"); - if (pendientePorcentaje.Value <= -8.0) - return ("bajada-fuerte", "#1d4ed8"); - - if (pendientePorcentaje.Value <= -3.0) - return ("bajada", "#38bdf8"); - - return ("llano", "#facc15"); + return ("llano", "#dc2626"); } @@ -238,7 +274,7 @@ public partial class PlanificadorRutas : ComponentBase } ThrowIfPintadoObsoleto(versionPintado); - await ActualizarAlturasResumenRutaAsync(CancellationToken.None); + await ActualizarResumenTecnicoRutaAsync(CancellationToken.None); ThrowIfPintadoObsoleto(versionPintado); await CerrarPopupsAsync(); diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.razor b/RutasDBUS/Components/Pages/PlanificadorRutas.razor index d88ad32..2ce076f 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.razor +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.razor @@ -653,6 +653,31 @@ @(_pintandoElevacionesRuta ? "Elevaciones..." : "Elevaciones") + +
+ + +
@if (_opcionesRuta.Count > 0) @@ -885,6 +910,8 @@ var dCorregida = CalcularDistanciaCorregidaAlternativa(altSeleccionada); var xyzOrigen = FormatearResumenXYZOrigen(); var xyzDestino = FormatearResumenXYZDestino(); + var callejeroOrigen = FormatearResumenCallejeroOrigen(); + var callejeroDestino = FormatearResumenCallejeroDestino(); var tiempoCalculo = FormatearTiempoCalculoRuta(); var scoreEquilibradoSeleccionado = CriterioPrioritarioEquilibradoActivo ? FormatearScoreEquilibradoAlternativa(altSeleccionada) @@ -920,10 +947,18 @@ {
@xyzOrigen
} + @if (!string.IsNullOrWhiteSpace(callejeroOrigen)) + { +
@callejeroOrigen
+ } @if (!string.IsNullOrWhiteSpace(xyzDestino)) {
@xyzDestino
} + @if (!string.IsNullOrWhiteSpace(callejeroDestino)) + { +
@callejeroDestino
+ } @if (!string.IsNullOrWhiteSpace(tiempoCalculo)) {
Tiempo de calculo: @tiempoCalculo
@@ -951,13 +986,17 @@ var dRectaSinRuta = DistanciaLineaRectaOrigenDestino(); var xyzOrigenSinRuta = FormatearResumenXYZOrigen(); var xyzDestinoSinRuta = FormatearResumenXYZDestino(); + var callejeroOrigenSinRuta = FormatearResumenCallejeroOrigen(); + var callejeroDestinoSinRuta = FormatearResumenCallejeroDestino(); var tiempoCalculoSinRuta = FormatearTiempoCalculoRuta();

@mensajeSinRuta

@if (dRectaSinRuta > 0 || !string.IsNullOrWhiteSpace(xyzOrigenSinRuta) - || !string.IsNullOrWhiteSpace(xyzDestinoSinRuta)) + || !string.IsNullOrWhiteSpace(xyzDestinoSinRuta) + || !string.IsNullOrWhiteSpace(callejeroOrigenSinRuta) + || !string.IsNullOrWhiteSpace(callejeroDestinoSinRuta)) {
@if (dRectaSinRuta > 0) @@ -968,10 +1007,18 @@ {
@xyzOrigenSinRuta
} + @if (!string.IsNullOrWhiteSpace(callejeroOrigenSinRuta)) + { +
@callejeroOrigenSinRuta
+ } @if (!string.IsNullOrWhiteSpace(xyzDestinoSinRuta)) {
@xyzDestinoSinRuta
} + @if (!string.IsNullOrWhiteSpace(callejeroDestinoSinRuta)) + { +
@callejeroDestinoSinRuta
+ } @if (!string.IsNullOrWhiteSpace(tiempoCalculoSinRuta)) {
Tiempo de calculo: @tiempoCalculoSinRuta
diff --git a/RutasDBUS/Modelos/Geocodificacion/LugarGeocodificado.cs b/RutasDBUS/Modelos/Geocodificacion/LugarGeocodificado.cs index b6c6c46..6688734 100644 --- a/RutasDBUS/Modelos/Geocodificacion/LugarGeocodificado.cs +++ b/RutasDBUS/Modelos/Geocodificacion/LugarGeocodificado.cs @@ -11,3 +11,9 @@ public sealed class LugarGeocodificado public double Latitud { get; init; } public double Longitud { get; init; } } + +public sealed class LugarGeocodificadoCercano +{ + public LugarGeocodificado Lugar { get; init; } = new(); + public double DistanciaMetros { get; init; } +} diff --git a/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs b/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs index 4f41b39..6b9a7dc 100644 --- a/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs +++ b/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Globalization; using System.IO.Compression; using System.Text; @@ -13,6 +14,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 IReadOnlyList? _indice; public GeocodificadorLocalServicio(IWebHostEnvironment entorno) @@ -64,6 +66,51 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio return indice.Count; } + public async Task BuscarMasCercanoAsync( + double latitud, + double longitud, + CancellationToken cancellationToken = default) + { + if (!double.IsFinite(latitud) || !double.IsFinite(longitud)) + return null; + + var clave = ClaveCercano(latitud, longitud); + if (_cacheCercanos.TryGetValue(clave, out var cercanoCacheado)) + return cercanoCacheado; + + var indice = await ObtenerIndiceAsync(cancellationToken); + if (indice.Count == 0) + return null; + + EntradaGeocodificada? mejorEntrada = null; + var mejorDistancia = double.MaxValue; + + foreach (var entrada in indice) + { + cancellationToken.ThrowIfCancellationRequested(); + + var lugar = entrada.Lugar; + var distancia = DistanciaHaversineMetros(latitud, longitud, lugar.Latitud, lugar.Longitud); + if (distancia >= mejorDistancia) + continue; + + mejorDistancia = distancia; + mejorEntrada = entrada; + } + + if (mejorEntrada is null) + return null; + + var cercano = new LugarGeocodificadoCercano + { + Lugar = mejorEntrada.Lugar, + DistanciaMetros = mejorDistancia + }; + + _cacheCercanos.TryAdd(clave, cercano); + return cercano; + } + private async Task> ObtenerIndiceAsync(CancellationToken cancellationToken) { if (_indice is not null) @@ -363,6 +410,29 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio private static string PrimerValor(params string[] valores) => valores.FirstOrDefault(valor => !string.IsNullOrWhiteSpace(valor))?.Trim() ?? ""; + private static string ClaveCercano(double latitud, double longitud) + => $"{latitud.ToString("0.000000", CultureInfo.InvariantCulture)}|{longitud.ToString("0.000000", CultureInfo.InvariantCulture)}"; + + private static double DistanciaHaversineMetros(double lat1, double lon1, double lat2, double lon2) + { + const double radioTierraMetros = 6371000.0; + var dLat = GradosARadianes(lat2 - lat1); + var dLon = GradosARadianes(lon2 - lon1); + var rLat1 = GradosARadianes(lat1); + var rLat2 = GradosARadianes(lat2); + + var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + + Math.Cos(rLat1) * Math.Cos(rLat2) * + Math.Sin(dLon / 2) * Math.Sin(dLon / 2); + + a = Math.Clamp(a, 0.0, 1.0); + var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); + return radioTierraMetros * c; + } + + private static double GradosARadianes(double grados) + => grados * Math.PI / 180.0; + private static List ParsearCsv(string linea) { var campos = new List(); diff --git a/RutasDBUS/Servicios/Geocodificacion/IGeocodificadorLocalServicio.cs b/RutasDBUS/Servicios/Geocodificacion/IGeocodificadorLocalServicio.cs index 6e6e09d..0898486 100644 --- a/RutasDBUS/Servicios/Geocodificacion/IGeocodificadorLocalServicio.cs +++ b/RutasDBUS/Servicios/Geocodificacion/IGeocodificadorLocalServicio.cs @@ -9,5 +9,10 @@ public interface IGeocodificadorLocalServicio int maxResultados = 8, CancellationToken cancellationToken = default); + Task BuscarMasCercanoAsync( + double latitud, + double longitud, + CancellationToken cancellationToken = default); + Task ContarLugaresAsync(CancellationToken cancellationToken = default); } diff --git a/RutasDBUS/wwwroot/app.css b/RutasDBUS/wwwroot/app.css index 76efeda..b28bda8 100644 --- a/RutasDBUS/wwwroot/app.css +++ b/RutasDBUS/wwwroot/app.css @@ -811,6 +811,8 @@ html, body { display: flex; align-items: center; justify-content: flex-end; + flex-wrap: wrap; + gap: .35rem .5rem; margin: -.15rem 0 .4rem; } @@ -875,6 +877,47 @@ html, body { opacity: .55; } +.route-elevation-thresholds { + display: inline-flex; + align-items: center; + gap: .25rem; + pointer-events: auto; +} + +.route-elevation-threshold { + display: inline-flex; + align-items: center; + gap: .18rem; + min-height: 24px; + color: #cbd5e1; + font-size: .64rem; + font-weight: 700; + white-space: nowrap; +} + +.route-elevation-threshold input { + width: 40px; + height: 22px; + border: 1px solid rgba(148, 163, 184, .55); + border-radius: 6px; + background: rgba(2, 6, 23, .78); + color: #f8fafc; + font-size: .68rem; + font-weight: 800; + text-align: center; + padding: 0 .18rem; + outline: none; +} + +.route-elevation-threshold 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 { + opacity: .55; +} + /* Lista de alternativas de ruta */ .route-alt-list { @@ -2108,53 +2151,6 @@ html, body { display: none !important; } -.rt-route-elev-point { - stroke: #020617 !important; - stroke-width: 1.8px !important; - filter: drop-shadow(0 1px 2px rgba(2, 6, 23, .75)); -} - -.rt-route-elev-point--subida, -.rt-route-elev-point--subida-fuerte { - animation: rt-route-elev-pulse-up 1.45s ease-in-out infinite; -} - -.rt-route-elev-point--bajada, -.rt-route-elev-point--bajada-fuerte { - animation: rt-route-elev-pulse-down 1.45s ease-in-out infinite; -} - -.rt-route-elev-point--llano { - opacity: .88; -} - -@keyframes rt-route-elev-pulse-up { - 0%, 100% { - stroke-width: 1.8px; - opacity: .95; - } - - 50% { - stroke-width: 4px; - opacity: .72; - } -} - -@keyframes rt-route-elev-pulse-down { - 0%, 100% { - stroke-width: 1.8px; - opacity: .95; - } - - 50% { - stroke-width: 3.5px; - opacity: .68; - } -} - - - - .leaflet-control-zoom-indicator { background: rgba(15,23,42,.96); color: #e5e7eb; diff --git a/RutasDBUS/wwwroot/app.js b/RutasDBUS/wwwroot/app.js index 0547fd3..9bb71c1 100644 --- a/RutasDBUS/wwwroot/app.js +++ b/RutasDBUS/wwwroot/app.js @@ -103,7 +103,6 @@ window.rt.clearRoutes = function () { if (!window.rt.ensureRouteLayer()) return; window.rt._routeLayer.clearLayers(); window.rt._routeLines = {}; - if (window.rt.clearRouteElevPoints) window.rt.clearRouteElevPoints(); if (window.rt._routeStopsLayer) { window.rt._routeStopsLayer.clearLayers(); @@ -130,6 +129,9 @@ window.rt.drawRoute = function (id, coords, options) { const dashArray = options && options.dashArray; const lineCap = options && options.lineCap; const arrowEnd = !!(options && options.arrowEnd); + const elevationSegments = Array.isArray(options && options.elevationSegments) + ? options.elevationSegments + : []; const attachRouteClick = function (line) { line.on("click", function (e) { @@ -153,13 +155,61 @@ window.rt.drawRoute = function (id, coords, options) { return line; }; - const lineOpts = { color, weight, opacity }; - if (dashArray) lineOpts.dashArray = dashArray; - if (lineCap) lineOpts.lineCap = lineCap; + let arrowColor = color; + let hasElevationPaint = false; - const line = buildLine(coords, lineOpts); - line.addTo(window.rt._routeLayer); - window.rt._routeLines[id] = line; + if (elevationSegments.length > 0 && coords && coords.length >= 2) { + const group = window.L.layerGroup(); + + elevationSegments.forEach(function (segment) { + const segmentCoords = segment && segment.coords; + if (!Array.isArray(segmentCoords) || segmentCoords.length < 2) return; + + const segmentColor = (segment && segment.color) || color; + arrowColor = segmentColor; + + const segmentOpts = { + color: segmentColor, + weight, + opacity, + lineCap: lineCap || "round" + }; + if (dashArray) segmentOpts.dashArray = dashArray; + + const segmentLine = buildLine(segmentCoords, segmentOpts); + const slope = segment && typeof segment.slope === "number" ? segment.slope : null; + const label = slope !== null && Number.isFinite(slope) + ? ((slope > 0 ? "+" : "") + slope.toFixed(1) + "%") + : (segment && segment.label ? String(segment.label) : ""); + + if (label) { + segmentLine.bindTooltip(label, { + sticky: true, + direction: "top", + opacity: 0.92, + className: "rt-elev-tooltip" + }); + } + + segmentLine.addTo(group); + hasElevationPaint = true; + }); + + if (hasElevationPaint) { + group.addTo(window.rt._routeLayer); + window.rt._routeLines[id] = group; + } + } + + if (!hasElevationPaint) { + const lineOpts = { color, weight, opacity }; + if (dashArray) lineOpts.dashArray = dashArray; + if (lineCap) lineOpts.lineCap = lineCap; + + const line = buildLine(coords, lineOpts); + line.addTo(window.rt._routeLayer); + window.rt._routeLines[id] = line; + } if (arrowEnd && coords && coords.length >= 2) { const a = coords[coords.length - 2]; @@ -183,7 +233,7 @@ window.rt.drawRoute = function (id, coords, options) { html: `
${arrowInnerHtml}
`, iconSize: [size, size], iconAnchor: [size / 2, size / 2] @@ -281,18 +331,6 @@ window.rt.clearElevPoints = function (id) { window.rt._elevGroups = {}; }; -window.rt.clearRouteElevPoints = function () { - if (!window.rt._elevLayer || !window.rt._elevGroups) return; - - Object.keys(window.rt._elevGroups) - .filter(id => id.indexOf("route_elev_") === 0) - .forEach(id => { - const group = window.rt._elevGroups[id]; - if (group) window.rt._elevLayer.removeLayer(group); - delete window.rt._elevGroups[id]; - }); -}; - // points: [{ lat, lon, label }] // options: { radius, opacity, fillOpacity, showLabels, labelEvery } window.rt.drawElevPoints = function (id, points, options) {