diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs index 9a15384..6b83e65 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs @@ -56,6 +56,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable try { await JS.InvokeVoidAsync("rt.initMobileMapControls"); } catch (Exception ex) { Console.WriteLine("rt.initMobileMapControls: " + ex); } try { await JS.InvokeVoidAsync("rt.initDraggablePopups"); } catch (Exception ex) { Console.WriteLine("rt.initDraggablePopups: " + ex); } await CargarConfiguracionLocalAsync(); + await CargarZonasAsync(); await CargarModoConfiguracionHistorialAsync(); await AsegurarHistorialLocalCargadoAsync(); await CargarHistorialCompartidoAsync(); @@ -509,15 +510,17 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable private async Task<(double[,]? line, double dist, double dur)> WalkValhallaCached(double lat1, double lon1, double lat2, double lon2) { var poligonos = ObtenerPoligonosExclusionCaminata(); + var puntos = ObtenerPuntosExclusionCaminata(); var key = WalkKey("valhalla", lat1, lon1, lat2, lon2); if (poligonos is not null) key += "|" + JsonSerializer.Serialize(poligonos); + if (puntos is not null) key += "|puntos|" + JsonSerializer.Serialize(puntos); if (_walkCache.TryGetValue(key, out var v)) return v; try { var (line, dist, dur) = await ClienteValhalla.ObtenerRutaAsync(lat1, lon1, lat2, lon2, - costing: "pedestrian", excludePolygons: poligonos); + costing: "pedestrian", excludePolygons: poligonos, excludeLocations: puntos); // ✅ Solo cachear si es un resultado "bueno" if (line is not null && dist > 0) diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.InspectorZ.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.InspectorZ.cs index 7db1695..63bb395 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.InspectorZ.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.InspectorZ.cs @@ -1,10 +1,29 @@ using System.Globalization; +using Microsoft.JSInterop; namespace RutasDBUS.Components.Pages; public partial class PlanificadorRutas { private PuntoInspectorAnterior? _inspectorPuntoAnterior; + private double[,]? _caminoInspectorAnterior; + private bool _caminoInspectorVisible; + + private async Task OcultarCaminoInspectorAsync() + { + _caminoInspectorAnterior = null; + _caminoInspectorVisible = false; + try { await JS.InvokeVoidAsync("rt.setInspectorWalk", Array.Empty(), false); } catch { } + } + + private async Task AlternarCaminoInspectorAsync() + { + if (_caminoInspectorAnterior is null) return; + _caminoInspectorVisible = !_caminoInspectorVisible; + var puntos = Enumerable.Range(0, _caminoInspectorAnterior.GetLength(0)) + .Select(i => new[] { _caminoInspectorAnterior[i, 0], _caminoInspectorAnterior[i, 1] }).ToArray(); + await JS.InvokeVoidAsync("rt.setInspectorWalk", puntos, _caminoInspectorVisible); + } private sealed record PuntoInspectorAnterior( double Latitud, @@ -81,7 +100,7 @@ public partial class PlanificadorRutas try { var tareaDireccion = ResolverDireccionPuntoInspectorAnteriorAsync(punto); - var tareaCaminata = WalkCached( + var tareaCaminata = ClienteValhalla.ObtenerRutaAsync( punto.Latitud, punto.Longitud, _inspectorLat, @@ -96,11 +115,12 @@ public partial class PlanificadorRutas } var caminata = await tareaCaminata; + _caminoInspectorAnterior = caminata.Linea; _inspectorPuntoAnterior = puntoVigente with { Direccion = await tareaDireccion, - DistanciaAndandoMetros = caminata.line is not null && caminata.dist > 0 - ? caminata.dist + DistanciaAndandoMetros = caminata.Linea is not null && double.IsFinite(caminata.DistanciaMetros) && caminata.DistanciaMetros >= 0 + ? caminata.DistanciaMetros : null, CalculandoDatosAdicionales = false }; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs index d9d6669..14d279a 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs @@ -1400,7 +1400,7 @@ public partial class PlanificadorRutas : ComponentBase private bool _elevPopupMinimizado = false; - private const string _versionApp = "(v-20260908b)"; + private const string _versionApp = "(v-20260909a)"; private double? _paradaElevIdee; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs index 01016f1..1aee165 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs @@ -1534,6 +1534,8 @@ public partial class PlanificadorRutas : ComponentBase : null; var versionInspector = ++_versionInspector; + await OcultarCaminoInspectorAsync(); + if (versionInspector != _versionInspector) return; _inspectorLat = lat; _inspectorLon = lon; _inspectorElevIdee = null; @@ -1649,6 +1651,7 @@ public partial class PlanificadorRutas : ComponentBase private async Task CerrarInspectorAsync() { _versionInspector++; + await OcultarCaminoInspectorAsync(); _popupInspectorVisible = false; _inspectorPuntoAnterior = null; _inspectorLugaresCercanos = Array.Empty(); @@ -1949,6 +1952,7 @@ public partial class PlanificadorRutas : ComponentBase FactorEquilibradoAndarBus = _factorEquilibradoAndarBus, UsarValhallaParaCaminatas = _walkProvider == ProveedorCaminata.Valhalla, PoligonosExclusionCaminata = ObtenerPoligonosExclusionCaminata(), + PuntosExclusionCaminata = ObtenerPuntosExclusionCaminata(), VelocidadBusKmH = _velocidadBusKmH, MinutosPorTransbordo = _minutosTransbordo, MargenTransbordoSegundos = Math.Clamp(_margenTransbordoSegundos, 0, 1800), diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs index aa5b2de..c64d5a5 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs @@ -1322,7 +1322,7 @@ public partial class PlanificadorRutas : ComponentBase private async Task ReiniciarSeleccion() { _proveedorSoloAPieRecalculo = null; - await LimpiarZonasExclusionAsync(); + await CerrarHerramientaZonasAsync(); await CerrarHerramientaZonasAsync(); await CerrarTodasLasPreviewsAsync(); diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.RestriccionesCaminata.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.RestriccionesCaminata.cs index 3f8c216..9a87c28 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.RestriccionesCaminata.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.RestriccionesCaminata.cs @@ -1,4 +1,6 @@ using Microsoft.JSInterop; +using Microsoft.AspNetCore.Components; +using System.Text.Json; namespace RutasDBUS.Components.Pages; @@ -6,9 +8,11 @@ public partial class PlanificadorRutas { private sealed class ZonaExclusionCaminata { - public string Id { get; } = Guid.NewGuid().ToString("N"); - public List Vertices { get; } = new(); + public string Id { get; set; } = Guid.NewGuid().ToString("N"); + public List Vertices { get; set; } = new(); public bool Cerrada { get; set; } + public bool Activa { get; set; } = true; + public bool EsPunto { get; set; } } private readonly List _zonasExclusionCaminata = new(); @@ -16,6 +20,57 @@ public partial class PlanificadorRutas private bool _mostrarHerramientaZonas; private bool _recalculandoZonas; private string? _errorZonas; + private const string ClaveZonasLocal = "rutasdbus.zonas.v1"; + private bool _zonasCargadas; + private bool _nuevaExclusionEsPunto; + + private async Task CargarZonasAsync() + { + try + { + var resultado = await JS.InvokeAsync("rt.loadLocalValueResult", ClaveZonasLocal); + if (!resultado.GetProperty("ok").GetBoolean()) + throw new InvalidOperationException("Almacenamiento no disponible"); + var json = resultado.GetProperty("value").GetString(); + if (!string.IsNullOrWhiteSpace(json)) + { + var zonas = JsonSerializer.Deserialize>(json) ?? new(); + foreach (var zona in zonas.Take(10)) + { + if (zona is null || zona.Vertices is null || zona.Vertices.Count > 50 || + zona.Vertices.Any(p => p is null || p.Length != 2 || !CoordenadaExclusionValida(p[0], p[1]))) + continue; + if (zona.EsPunto && zona.Vertices.Count > 1) continue; + if (zona.Cerrada && (zona.EsPunto ? zona.Vertices.Count != 1 : + !await JS.InvokeAsync("rt.isExclusionPolygonValid", zona.Vertices))) + continue; + zona.Id = Guid.NewGuid().ToString("N"); + _zonasExclusionCaminata.Add(zona); + } + } + _zonasCargadas = true; + } + catch { _errorZonas = "No se pudieron cargar las zonas guardadas. Recarga antes de editarlas."; } + } + + private async Task GuardarZonasAsync() + { + if (!_zonasCargadas) return; + try + { + var resultado = await JS.InvokeAsync("rt.saveLocalValueResult", ClaveZonasLocal, + JsonSerializer.Serialize(_zonasExclusionCaminata)); + if (!resultado.GetProperty("ok").GetBoolean()) + _errorZonas = "No se pudieron guardar las zonas en este navegador."; + } + catch { _errorZonas = "No se pudieron guardar las zonas en este navegador."; } + } + + private async Task CambiarZonaActivaAsync(ZonaExclusionCaminata zona, ChangeEventArgs args) + { + zona.Activa = args.Value is true; + await ActualizarEditorExclusionCaminataAsync(); + } private ZonaExclusionCaminata? ZonaExclusionSeleccionada => _zonasExclusionCaminata.Find(zona => zona.Id == _zonaExclusionSeleccionada); private bool _modoDibujoExclusionCaminata => @@ -23,12 +78,19 @@ public partial class PlanificadorRutas private List>? ObtenerPoligonosExclusionCaminata() { - var poligonos = _zonasExclusionCaminata.Where(zona => zona.Cerrada) + var poligonos = _zonasExclusionCaminata.Where(zona => zona.Cerrada && zona.Activa && !zona.EsPunto) .Select(zona => zona.Vertices.Select(p => new[] { p[1], p[0] }) .Append(new[] { zona.Vertices[0][1], zona.Vertices[0][0] }).ToList()).ToList(); return poligonos.Count == 0 ? null : poligonos; } + private List? ObtenerPuntosExclusionCaminata() + { + var puntos = _zonasExclusionCaminata.Where(z => z.Cerrada && z.Activa && z.EsPunto) + .Select(z => z.Vertices[0].ToArray()).ToList(); + return puntos.Count == 0 ? null : puntos; + } + private async Task AbrirHerramientaZonasAsync() { _mostrarConfigAvanzada = false; @@ -49,7 +111,6 @@ public partial class PlanificadorRutas private async Task CerrarHerramientaZonasAsync() { - _zonasExclusionCaminata.RemoveAll(zona => !zona.Cerrada); _zonaExclusionSeleccionada = null; _mostrarHerramientaZonas = false; _errorZonas = null; @@ -58,8 +119,8 @@ public partial class PlanificadorRutas private async Task IniciarDibujoExclusionCaminataAsync() { - if (_recalculandoZonas || _modoDibujoExclusionCaminata || _zonasExclusionCaminata.Count >= 10) return; - var zona = new ZonaExclusionCaminata(); + if (!_zonasCargadas || _recalculandoZonas || _modoDibujoExclusionCaminata || _zonasExclusionCaminata.Count >= 10) return; + var zona = new ZonaExclusionCaminata { EsPunto = _nuevaExclusionEsPunto }; _zonasExclusionCaminata.Add(zona); _zonaExclusionSeleccionada = zona.Id; _errorZonas = null; @@ -69,7 +130,6 @@ public partial class PlanificadorRutas private async Task SeleccionarZonaExclusionAsync(string id) { if (_recalculandoZonas) return; - _zonasExclusionCaminata.RemoveAll(zona => !zona.Cerrada); _zonaExclusionSeleccionada = id; _errorZonas = null; await ActualizarEditorExclusionCaminataAsync(); @@ -89,6 +149,7 @@ public partial class PlanificadorRutas _zonasExclusionCaminata.Clear(); _zonaExclusionSeleccionada = null; _errorZonas = null; + await GuardarZonasAsync(); await JS.InvokeVoidAsync("rt.clearExclusionPolygonEditor"); } @@ -115,7 +176,7 @@ public partial class PlanificadorRutas private async Task CalcularRutaEvitandoPoligonoCaminataAsync() { if (_recalculandoZonas || _modoDibujoExclusionCaminata || _coordenadasOrigen is null || _coordenadasDestino is null) return; - foreach (var zona in _zonasExclusionCaminata) + foreach (var zona in _zonasExclusionCaminata.Where(z => z.Cerrada && z.Activa && !z.EsPunto)) { if (!await JS.InvokeAsync("rt.isExclusionPolygonValid", zona.Vertices)) { @@ -145,18 +206,20 @@ public partial class PlanificadorRutas var zona = ZonaExclusionSeleccionada!; if (zona.Vertices.Count >= 50) return; zona.Vertices.Add(new[] { latitud, longitud }); + if (zona.EsPunto) zona.Cerrada = true; await ActualizarEditorExclusionCaminataAsync(); StateHasChanged(); } [JSInvokable] - public Task OnExclusionPolygonVertexDragged(string id, int indice, double latitud, double longitud) + public async Task OnExclusionPolygonVertexDragged(string id, int indice, double latitud, double longitud) { var zona = _zonasExclusionCaminata.Find(zona => zona.Id == id); if (_mostrarHerramientaZonas && !_recalculandoZonas && zona is not null && indice >= 0 && indice < zona.Vertices.Count && CoordenadaExclusionValida(latitud, longitud)) zona.Vertices[indice] = new[] { latitud, longitud }; - return InvokeAsync(StateHasChanged); + await GuardarZonasAsync(); + await InvokeAsync(StateHasChanged); } private static bool CoordenadaExclusionValida(double latitud, double longitud) => @@ -164,9 +227,10 @@ public partial class PlanificadorRutas private async Task ActualizarEditorExclusionCaminataAsync() { + await GuardarZonasAsync(); if (!_mostrarHerramientaZonas) return; await JS.InvokeVoidAsync("rt.updateExclusionPolygonEditor", - _zonasExclusionCaminata.Select(zona => new { id = zona.Id, points = zona.Vertices, closed = zona.Cerrada }), + _zonasExclusionCaminata.Select(zona => new { id = zona.Id, points = zona.Vertices, closed = zona.Cerrada, active = zona.Activa, point = zona.EsPunto }), _zonaExclusionSeleccionada, _recalculandoZonas); } } diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.ValoresFabrica.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.ValoresFabrica.cs index 891c044..3d3fdbf 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.ValoresFabrica.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.ValoresFabrica.cs @@ -9,6 +9,10 @@ public partial class PlanificadorRutas { [Inject] private ServicioValoresFabrica ValoresFabrica { get; set; } = default!; private ConfiguracionHistorialRuta? _configuracionFabrica; + private ConfiguracionHistorialRuta? _fabricaPendiente; + private readonly List<(string Campo, string Antes, string Despues)> _cambiosFabrica = new(); + private string? _mensajeFabrica; + private bool _guardandoFabrica; private ConfiguracionHistorialRuta CrearConfiguracionPredeterminadaHistorial() { @@ -39,14 +43,48 @@ public partial class PlanificadorRutas [JSInvokable] public async Task GuardarConfiguracionFabricaDesdeAtajoAsync() { - if (!_configuracionLocalCargada) + if (!_configuracionLocalCargada || _guardandoFabrica || _fabricaPendiente is not null) return; var actual = CrearConfiguracionLocal(); - if (!await ValoresFabrica.GuardarAsync(actual)) - return; - - _configuracionFabrica = actual.Clonar(); + var anterior = await ValoresFabrica.CargarAsync() ?? CrearConfiguracionBaseHistorial(); + _cambiosFabrica.Clear(); + foreach (var propiedad in typeof(ConfiguracionHistorialRuta).GetProperties()) + { + if (propiedad.Name.StartsWith("Version", StringComparison.Ordinal)) continue; + var antes = System.Text.Json.JsonSerializer.Serialize(propiedad.GetValue(anterior)); + var despues = System.Text.Json.JsonSerializer.Serialize(propiedad.GetValue(actual)); + if (antes == despues) continue; + var nombre = System.Text.RegularExpressions.Regex.Replace(propiedad.Name, "([a-z])([A-Z])", "$1 $2"); + _cambiosFabrica.Add((nombre, antes.Trim('"'), despues.Trim('"'))); + } + _mensajeFabrica = null; + _fabricaPendiente = actual.Clonar(); await InvokeAsync(StateHasChanged); } + + private void CancelarGuardadoFabrica() + { + if (_guardandoFabrica) return; + _fabricaPendiente = null; + _mensajeFabrica = null; + } + + private async Task ConfirmarGuardadoFabricaAsync() + { + if (_fabricaPendiente is null || _guardandoFabrica) return; + _guardandoFabrica = true; + try + { + if (await ValoresFabrica.GuardarAsync(_fabricaPendiente)) + { + _configuracionFabrica = _fabricaPendiente.Clonar(); + _fabricaPendiente = null; + _mensajeFabrica = "Valores predeterminados guardados."; + } + else + _mensajeFabrica = "No se pudo guardar. Revisa los valores y los permisos de escritura del servidor."; + } + finally { _guardandoFabrica = false; } + } } diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.razor b/RutasDBUS/Components/Pages/PlanificadorRutas.razor index 43cc1fa..57a3f32 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.razor +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.razor @@ -447,7 +447,7 @@ }
- Superficie: + Superficie (V2): @if (_inspectorSuperficieCargando) { Consultando... @@ -457,6 +457,10 @@ @(_inspectorSuperficieTexto ?? "No disponible") }
+
+ Clasificación LiDAR: + No disponible: falta la nube LiDAR clasificada local. +
@if (_inspectorLugaresCercanos.Count > 0) { @@ -507,6 +511,15 @@ else { @(puntoAnterior.DistanciaAndandoMetros is { } distanciaAndando ? FormatearDistanciaPuntoInspector(distanciaAndando) : "-") + @if (_caminoInspectorAnterior is not null) + { + + } } @@ -1480,9 +1493,12 @@ { var zona = _zonasExclusionCaminata[i];
+ @@ -1494,13 +1510,17 @@ { - } else { + @onclick="IniciarDibujoExclusionCaminataAsync">@(_nuevaExclusionEsPunto ? "Añadir punto" : "Añadir zona") + } @@ -2244,3 +2264,37 @@
} +@if (_fabricaPendiente is not null || _mensajeFabrica is not null) +{ +
+ +
+} + diff --git a/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs b/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs index cd72061..ba6ad99 100644 --- a/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs +++ b/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs @@ -42,6 +42,7 @@ public sealed class ParametrosPlanificadorRuta // Zonas temporales que Valhalla debe excluir al calcular caminatas. public List>? PoligonosExclusionCaminata { get; set; } + public List? PuntosExclusionCaminata { get; set; } // selección candidatas public int RadioParadasCandidatasMetros { get; set; } = 1200; diff --git a/RutasDBUS/Servicios/Caminata/ClienteValhalla.cs b/RutasDBUS/Servicios/Caminata/ClienteValhalla.cs index 5c9cdb4..5985064 100644 --- a/RutasDBUS/Servicios/Caminata/ClienteValhalla.cs +++ b/RutasDBUS/Servicios/Caminata/ClienteValhalla.cs @@ -34,7 +34,8 @@ public class ClienteValhalla : IClienteValhalla double latitudHasta, double longitudHasta, string costing = "pedestrian", - List>? excludePolygons = null) + List>? excludePolygons = null, + List? excludeLocations = null) { var cuerpo = new SolicitudValhalla { @@ -46,6 +47,8 @@ public class ClienteValhalla : IClienteValhalla PerfilCoste = string.IsNullOrWhiteSpace(costing) ? "pedestrian" : costing, FormatoGeometria = "polyline6", PoligonosExclusion = excludePolygons, + PuntosExclusion = excludeLocations?.Select(p => new LocalizacionValhalla + { Latitud = p[0], Longitud = p[1] }).ToList(), OpcionesIndicaciones = new OpcionesIndicacionesValhalla { Idioma = "es-ES", @@ -208,8 +211,8 @@ public class ClienteValhalla : IClienteValhalla if (distanciaMetros <= 0) continue; - var superficie = EtiquetaSuperficie(arista.Superficie, arista.SinPavimentar); - var uso = EtiquetaUso(arista.Uso); + var superficie = arista.Superficie ?? "No disponible"; + var uso = arista.Uso; if (segmentos.Count > 0 && EsMismaSuperficie(segmentos[^1], superficie, uso, arista.SinPavimentar)) { @@ -324,6 +327,10 @@ public class ClienteValhalla : IClienteValhalla [JsonPropertyName("exclude_polygons")] public List>? PoligonosExclusion { get; set; } + [JsonPropertyName("exclude_locations")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PuntosExclusion { get; set; } + [JsonPropertyName("directions_options")] public OpcionesIndicacionesValhalla OpcionesIndicaciones { get; set; } = new(); } diff --git a/RutasDBUS/Servicios/Caminata/IClienteValhalla.cs b/RutasDBUS/Servicios/Caminata/IClienteValhalla.cs index f6283d8..4a0f415 100644 --- a/RutasDBUS/Servicios/Caminata/IClienteValhalla.cs +++ b/RutasDBUS/Servicios/Caminata/IClienteValhalla.cs @@ -15,7 +15,8 @@ public interface IClienteValhalla double latitudHasta, double longitudHasta, string costing = "pedestrian", - List>? excludePolygons = null); // "pedestrian", "bicycle", etc. + List>? excludePolygons = null, + List? excludeLocations = null); // Coordenadas [lat, lon]. /// /// Obtiene la superficie y el tipo de uso de los segmentos de una geometría peatonal. diff --git a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs index 65a6c75..a32e274 100644 --- a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs +++ b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs @@ -2246,7 +2246,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas origenLatLon[1], destinoLatLon[0], destinoLatLon[1], - parametros.PoligonosExclusionCaminata); + parametros.PoligonosExclusionCaminata, parametros.PuntosExclusionCaminata); if (valhalla.linea is not null && valhalla.distancia > 0) { var duracion = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(valhalla.distancia, parametros); @@ -3835,7 +3835,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas longitudOrigen, latitudDestino, longitudDestino, - parametros.PoligonosExclusionCaminata) + parametros.PoligonosExclusionCaminata, parametros.PuntosExclusionCaminata) : WalkOsrmCached(latitudOrigen, longitudOrigen, latitudDestino, longitudDestino); } @@ -3880,20 +3880,23 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas double longitudOrigen, double latitudDestino, double longitudDestino, - List>? excludePolygons = null) + List>? excludePolygons = null, + List? excludeLocations = null) { - if (excludePolygons is { Count: > 0 }) + if (excludePolygons is { Count: > 0 } || excludeLocations is { Count: > 0 }) { // Solo reutilizar una geometria conocida y alejada de todas las zonas. var claveNormal = BuildWalkKey("valhalla", latitudOrigen, longitudOrigen, latitudDestino, longitudDestino); - if (_cacheCaminatas.TryGetValue(claveNormal, out var normal) && normal.linea is not null && + // Un punto se asocia a una arista, que puede extenderse mas alla del punto. + if (excludeLocations is not { Count: > 0 } && excludePolygons is not null && + _cacheCaminatas.TryGetValue(claveNormal, out var normal) && normal.linea is not null && !ExclusionPuedeAfectarTrayecto(normal.linea, excludePolygons)) { return normal; } var claveExclusion = BuildWalkKey("valhalla-exclusion", latitudOrigen, longitudOrigen, latitudDestino, longitudDestino) - + "|" + JsonSerializer.Serialize(excludePolygons); + + "|" + JsonSerializer.Serialize(excludePolygons) + "|" + JsonSerializer.Serialize(excludeLocations); if (_cacheCaminatasConExclusion.TryGetValue(claveExclusion, out var cacheadoConExclusion)) return cacheadoConExclusion; @@ -3905,7 +3908,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas latitudDestino, longitudDestino, costing: "pedestrian", - excludePolygons: excludePolygons); + excludePolygons: excludePolygons, excludeLocations: excludeLocations); var valor = (linea, distancia, duracion); if (linea is not null && distancia > 0) diff --git a/RutasDBUS/wwwroot/app.css b/RutasDBUS/wwwroot/app.css index d698d7d..45996c7 100644 --- a/RutasDBUS/wwwroot/app.css +++ b/RutasDBUS/wwwroot/app.css @@ -971,7 +971,8 @@ html, body { .popup-elev-topright.draggable-popup--free, .route-panel-overlay.draggable-popup--free, .history-panel.draggable-popup--free, -.config-panel.draggable-popup--free { +.config-panel.draggable-popup--free, +.zones-panel.draggable-popup--free { position: fixed !important; left: var(--drag-left) !important; top: var(--drag-top) !important; @@ -1137,6 +1138,7 @@ html, body { top: 90px; left: 16px; width: min(340px, calc(100vw - 32px)); + max-width: calc(100vw - 32px); max-height: calc(100dvh - 110px); overflow: auto; z-index: 1600; @@ -1151,12 +1153,21 @@ html, body { } .zones-panel .config-panel-header h5 { font-size: .94rem; } +@media (max-width: 700px) { + .zones-panel.draggable-popup--free { + left: 16px !important; + top: 72px !important; + max-height: calc(100dvh - 150px) !important; + } +} .zones-panel h5 small { font-size: .74rem; color: #94a3b8; } .zones-panel__list { display: grid; gap: .35rem; margin-block: .5rem; } .zones-panel__item { display: flex; gap: .35rem; min-width: 0; } -.zones-panel__item > :first-child { flex: 1; text-align: left; } +.zones-panel__item > button:first-of-type { flex: 1; text-align: left; } +.zones-panel__item > input { flex: 0 0 auto; align-self: center; } .zones-panel__actions { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin-top: .5rem; } .zones-panel__actions .btn-ghost { white-space: nowrap; } +.zones-panel__type { background: #161d2a; color: #e5e7eb; border: 1px solid #64748b; border-radius: 6px; min-height: 30px; max-width: 110px; font: inherit; } .zones-panel__icon { padding-inline: .6rem; } .btn-ghost.zones-panel__primary { background: #1d4ed8; color: #fff; } .btn-ghost.zones-panel__primary:disabled { background: #111827; color: #94a3b8; } @@ -1198,8 +1209,9 @@ html, body { } .rt-exclusion-vertex { - width: 18px !important; - height: 18px !important; + width: 10px !important; + height: 10px !important; + box-sizing: border-box; border: 2px solid #fff; border-radius: 50%; background: #a21caf; @@ -1367,6 +1379,13 @@ html, body { overflow-wrap: anywhere; } +.btn-ghost.inspector-walk-eye { display: inline-flex; padding: 2px 5px; min-height: 0; vertical-align: middle; margin-left: 5px; } +.inspector-walk-eye[aria-pressed="true"] { color: #22d3ee; border-color: #22d3ee; } + +.fabrica-confirmacion { width: min(560px, calc(100vw - 24px)); max-height: calc(100dvh - 32px); overflow-y: auto; } +.fabrica-cambios { max-height: 45dvh; overflow-y: auto; font-size: .8rem; overflow-wrap: anywhere; } +.fabrica-cambios > div { padding: 5px 0; border-bottom: 1px solid #374151; } + /* LISTA DE TRAMOS TIPO “DIRECCIONES” CON NÚMEROS */ .lista-tramos-ruta { diff --git a/RutasDBUS/wwwroot/app.js b/RutasDBUS/wwwroot/app.js index 99a3835..7645d63 100644 --- a/RutasDBUS/wwwroot/app.js +++ b/RutasDBUS/wwwroot/app.js @@ -36,7 +36,7 @@ window.initZonasShortcut = function (dotNetHelper) { window.rt._zonasShortcutInicializado = true; document.addEventListener("keydown", function (e) { - if (e.repeat || e.code !== "KeyZ" || !e.ctrlKey || !e.altKey || e.shiftKey) return; + if (e.repeat || e.code !== "KeyZ" || !e.ctrlKey || !e.shiftKey || e.altKey || e.metaKey) return; e.preventDefault(); e.stopImmediatePropagation(); @@ -51,10 +51,11 @@ window.initFabricaShortcut = function (dotNetHelper) { window.rt._fabricaShortcutInicializado = true; document.addEventListener("keydown", function (e) { - if (e.repeat || e.code !== "KeyF" || !e.ctrlKey || !e.altKey || e.shiftKey || e.metaKey) return; + if (e.repeat || e.code !== "KeyF" || !e.ctrlKey || !e.shiftKey || e.altKey || e.metaKey) return; e.preventDefault(); e.stopImmediatePropagation(); + document.activeElement?.blur(); dotNetHelper.invokeMethodAsync("GuardarConfiguracionFabricaDesdeAtajoAsync").catch(function () { }); }, true); }; @@ -466,7 +467,8 @@ window.rt.drawRoute = function (id, coords, options) { "Altura: " + elevationText ]; if (segment && segment.surfaceText) { - tooltipLines.push("Superficie: " + String(segment.surfaceText)); + tooltipLines.push("Superficie (V2): " + String(segment.surfaceText)); + tooltipLines.push("LiDAR: No disponible (falta la nube clasificada local)"); } return tooltipLines.join("
"); }; @@ -766,21 +768,25 @@ window.rt.updateExclusionPolygonEditor = function (zones, selectedId, busy) { const selected = zone.id === selectedId; const points = zone.points.map(p => [p[0], p[1]]); const options = { - color: selected ? "#e879f9" : "#f97316", weight: selected ? 3 : 2, - fillOpacity: zone.closed ? 0.18 : 0.08, interactive: false + color: zone.active === false ? "#9ca3af" : selected ? "#e879f9" : "#f97316", weight: selected ? 3 : 2, + dashArray: zone.active === false ? "4 5" : null, + fillOpacity: zone.active === false ? 0.03 : zone.closed ? 0.18 : 0.08, interactive: false }; - const polygon = (zone.closed ? L.polygon(points, options) : L.polyline(points, options)).addTo(layer); + const polygon = zone.point + ? (points.length ? L.circleMarker(points[0], { ...options, radius: 6, fillOpacity: .5 }).addTo(layer) : null) + : (zone.closed ? L.polygon(points, options) : L.polyline(points, options)).addTo(layer); if (!selected || busy) continue; points.forEach((point, index) => { const marker = L.marker(point, { draggable: true, autoPan: true, bubblingMouseEvents: false, zIndexOffset: 3000, icon: L.divIcon({ className: "rt-exclusion-vertex", html: "", - iconSize: [18, 18], iconAnchor: [9, 9] }) + iconSize: [10, 10], iconAnchor: [5, 5] }) }).addTo(layer); const update = () => { const pos = marker.getLatLng(); points[index] = [pos.lat, pos.lng]; - polygon.setLatLngs(points); + if (zone.point) polygon?.setLatLng(points[0]); + else polygon.setLatLngs(points); return pos; }; marker.on("drag", update); @@ -1761,6 +1767,17 @@ window.rt.clearInspectorMarkers = function () { } }; +window.rt.setInspectorWalk = function (points, visible) { + window.rt._inspectorWalkLayer?.remove(); + window.rt._inspectorWalkLayer = null; + const map = window.__rtLeafletMap; + if (!visible || !map || !window.L || points.length < 2) return; + window.rt._inspectorWalkLayer = window.L.polyline(points, { + color: "#0891b2", weight: 5, dashArray: "2 7", lineCap: "round", interactive: false + }).addTo(map); + if (!points.some(p => map.getBounds().contains(p))) map.panTo(points[0]); +}; + window.rt = window.rt || {}; diff --git a/tests/RutasDBUS.RegressionTests/Program.cs b/tests/RutasDBUS.RegressionTests/Program.cs index 16bb67e..e9e99af 100644 --- a/tests/RutasDBUS.RegressionTests/Program.cs +++ b/tests/RutasDBUS.RegressionTests/Program.cs @@ -50,7 +50,7 @@ var fake = new FakeValhalla(); var planner = new PlanificadorRutasServicio(null!, null!, fake, null!, null!, null!, null!); var walk = typeof(PlanificadorRutasServicio).GetMethod("WalkValhallaCached", BindingFlags.Instance | BindingFlags.NonPublic)!; async Task<(double[,]? linea, double distancia, double duracion)> Walk(List>? zones = null) => - await (Task<(double[,]?, double, double)>)walk.Invoke(planner, new object?[] { 43.31, -1.99, 43.32, -1.98, zones })!; + await (Task<(double[,]?, double, double)>)walk.Invoke(planner, new object?[] { 43.31, -1.99, 43.32, -1.98, zones, null })!; List Ring(double lon, double lat, double size) => new() { new[] { lon, lat }, new[] { lon + size, lat }, new[] { lon + size, lat + size }, new[] { lon, lat + size }, new[] { lon, lat } }; @@ -96,12 +96,17 @@ Console.WriteLine("Pruebas completadas."); sealed class FakeValhalla : IClienteValhalla { + public Task ObtenerInformacionSueloAsync( + double[,] linea, CancellationToken cancellationToken = default) + => Task.FromResult(null); + public int Calls { get; private set; } public int Zones { get; private set; } public bool Fail { get; set; } public Task<(double[,]? Linea, double DistanciaMetros, double DuracionSegundos)> ObtenerRutaAsync( double latitudDesde, double longitudDesde, double latitudHasta, double longitudHasta, - string costing = "pedestrian", List>? excludePolygons = null) + string costing = "pedestrian", List>? excludePolygons = null, + List? excludeLocations = null) { Calls++; Zones = excludePolygons?.Count ?? 0; diff --git a/tests/verify-zones.cjs b/tests/verify-zones.cjs index 9e2cb37..4f59c40 100644 --- a/tests/verify-zones.cjs +++ b/tests/verify-zones.cjs @@ -14,7 +14,9 @@ const path = require("node:path"); await page.goto("http://127.0.0.1:5199", { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => !!window.rt?._dotNetHelper && !!window.__rtLeafletMap); const initialErrors = errors.slice(); - await page.getByRole("button", { name: "Abrir zonas a evitar", exact: true }).click(); + await page.keyboard.press("Control+Shift+KeyZ"); + await page.locator(".zones-panel").waitFor(); + await page.waitForTimeout(1000); await page.getByRole("button", { name: "Añadir zona", exact: true }).click(); for (const [x, y] of [[600, 420], [850, 430], [760, 640]]) { await page.mouse.click(x, y); @@ -24,6 +26,7 @@ const path = require("node:path"); await page.getByRole("button", { name: "Añadir zona", exact: true }).waitFor(); assert.equal(await page.locator(".zones-panel__item").count(), 1); assert.equal(await page.locator(".rt-exclusion-vertex").count(), 3); + assert.equal(Math.round((await page.locator(".rt-exclusion-vertex").first().boundingBox()).width), 10); const polygon = page.locator('path[stroke="#e879f9"]'); const previousShape = await polygon.getAttribute("d"); const marker = await page.locator(".rt-exclusion-vertex").first().boundingBox(); @@ -49,9 +52,32 @@ const path = require("node:path"); await page.locator(".zones-panel").waitFor({ state: "detached" }); assert.equal(await page.locator(".rt-exclusion-vertex").count(), 0); assert.equal(await page.locator('path[stroke="#e879f9"], path[stroke="#f97316"]').count(), 0); - await page.getByRole("button", { name: "Abrir zonas a evitar", exact: true }).click(); + await page.keyboard.press("Control+Shift+KeyZ"); await page.locator(".zones-panel").waitFor(); assert.equal(await page.locator(".zones-panel__item").count(), 2, "Reabrir conserva zonas editables"); + await page.getByRole("checkbox", { name: "Activar zona", exact: true }).first().uncheck(); + await page.waitForFunction(() => JSON.parse(localStorage.getItem("rutasdbus.zonas.v1"))[0].Activa === false); + await page.reload(); + await page.waitForFunction(() => !!window.rt?._dotNetHelper && !!window.__rtLeafletMap); + await page.waitForTimeout(1000); + await page.keyboard.press("Control+Shift+KeyZ"); + await page.locator(".zones-panel__item").first().waitFor(); + assert.equal(await page.locator(".zones-panel__item").count(), 2, "F5 conserva zonas"); + assert.equal(await page.getByRole("checkbox", { name: "Activar zona", exact: true }).first().isChecked(), false); + const panelBefore = await page.locator(".zones-panel").boundingBox(); + const header = await page.locator(".zones-panel .draggable-popup-handle").boundingBox(); + await page.mouse.move(header.x + 40, header.y + 15); + await page.mouse.down(); + await page.mouse.move(header.x + 190, header.y + 65, { steps: 10 }); + await page.mouse.up(); + const panelAfter = await page.locator(".zones-panel").boundingBox(); + assert.ok(Math.abs(panelAfter.x - panelBefore.x) > 50, "El modal se arrastra"); + await page.screenshot({ path: path.join(artifacts, "persistencia-arrastre.png") }); + await page.keyboard.press("Control+Shift+KeyF"); + await page.getByRole("dialog", { name: "Guardar valores predeterminados" }).waitFor(); + assert.ok(await page.getByRole("button", { name: "Guardar y sustituir", exact: true }).isVisible()); + await page.screenshot({ path: path.join(artifacts, "confirmacion-fabrica.png") }); + await page.getByRole("button", { name: "Cancelar", exact: true }).click(); await page.getByRole("button", { name: "Eliminar zona", exact: true }).first().click(); await page.waitForFunction(() => document.querySelectorAll(".zones-panel__item").length === 1); assert.equal(await page.locator(".zones-panel__item").count(), 1);