From d13ff13da150797a328f645f80dc19f5261739b4 Mon Sep 17 00:00:00 2001 From: Pedro Date: Mon, 7 Sep 2026 12:02:52 +0200 Subject: [PATCH] =?UTF-8?q?A=C3=B1adido=20filtro=20de=20prohibicion=20de?= =?UTF-8?q?=20caminos=20por=20poligono=20dibujado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pages/PlanificadorRutas.Itinerarios.cs | 2 +- .../Pages/PlanificadorRutas.Planificacion.cs | 8 + ...PlanificadorRutas.RestriccionesCaminata.cs | 115 ++++++++++++++ .../Components/Pages/PlanificadorRutas.razor | 48 ++++++ .../Modelos/Planificacion/ParadaCandidata.cs | 6 + .../Servicios/Caminata/ClienteValhalla.cs | 7 +- .../Servicios/Caminata/IClienteValhalla.cs | 3 +- .../PlanificadorRutasServicio.cs | 150 ++++++++++++++++-- RutasDBUS/wwwroot/app.css | 70 ++++++++ RutasDBUS/wwwroot/app.js | 100 +++++++++++- 10 files changed, 489 insertions(+), 20 deletions(-) create mode 100644 RutasDBUS/Components/Pages/PlanificadorRutas.RestriccionesCaminata.cs diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs index 41bda0d..7b2b574 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-20260903b)"; + private const string _versionApp = "(v-20260907a)"; private double? _paradaElevIdee; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs index cee0068..370a7a5 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs @@ -1659,6 +1659,12 @@ public partial class PlanificadorRutas : ComponentBase { var lat = e.location.latitude; var lon = e.location.longitude; + + if (_modoDibujoExclusionCaminata) + { + await OnExclusionPolygonMapClick(lat, lon); + return; + } // Inspector (Z) sigue siendo click simple inmediato if (_inspectorActivo) @@ -1921,6 +1927,8 @@ public partial class PlanificadorRutas : ComponentBase ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros, PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos, FactorEquilibradoAndarBus = _factorEquilibradoAndarBus, + UsarValhallaParaCaminatas = _walkProvider == ProveedorCaminata.Valhalla, + PoligonosExclusionCaminata = ObtenerPoligonosExclusionCaminata(), VelocidadBusKmH = _velocidadBusKmH, MinutosPorTransbordo = _minutosTransbordo, MargenTransbordoSegundos = Math.Clamp(_margenTransbordoSegundos, 0, 1800), diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.RestriccionesCaminata.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.RestriccionesCaminata.cs new file mode 100644 index 0000000..560ee12 --- /dev/null +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.RestriccionesCaminata.cs @@ -0,0 +1,115 @@ +using Microsoft.JSInterop; + +namespace RutasDBUS.Components.Pages; + +public partial class PlanificadorRutas +{ + private bool _modoDibujoExclusionCaminata; + private bool _poligonoExclusionCaminataCerrado; + private readonly List _verticesExclusionCaminata = new(); + + private bool PuedeProbarExclusionCaminata => + _walkProvider == ProveedorCaminata.Valhalla && + _hayRutaValida && + _opcionesRuta.Count > 0 && + _indiceAlternativaSeleccionada >= 0 && + _indiceAlternativaSeleccionada < _opcionesRuta.Count && + (_opcionesRuta[_indiceAlternativaSeleccionada].EsSoloAPie || + _tramosRuta.Any(tramo => tramo.Tipo.StartsWith("A pie", StringComparison.OrdinalIgnoreCase))); + + private bool PoligonoExclusionCaminataValido => + _verticesExclusionCaminata.Count >= 3 && _poligonoExclusionCaminataCerrado; + + private List>? ObtenerPoligonosExclusionCaminata() + { + if (!PoligonoExclusionCaminataValido) + return null; + + return new List> + { + _verticesExclusionCaminata + .Select(punto => new[] { punto[1], punto[0] }) + .Append(new[] { _verticesExclusionCaminata[0][1], _verticesExclusionCaminata[0][0] }) + .ToList() + }; + } + + private async Task IniciarDibujoExclusionCaminataAsync() + { + if (!PuedeProbarExclusionCaminata) + return; + + _modoDibujoExclusionCaminata = true; + _poligonoExclusionCaminataCerrado = false; + _verticesExclusionCaminata.Clear(); + await JS.InvokeVoidAsync("rt.beginExclusionPolygonEditor"); + StateHasChanged(); + } + + private async Task CancelarDibujoExclusionCaminataAsync() + { + _modoDibujoExclusionCaminata = false; + _poligonoExclusionCaminataCerrado = false; + _verticesExclusionCaminata.Clear(); + await JS.InvokeVoidAsync("rt.clearExclusionPolygonEditor"); + StateHasChanged(); + } + + private async Task DeshacerVerticeExclusionCaminataAsync() + { + if (_verticesExclusionCaminata.Count == 0 || _poligonoExclusionCaminataCerrado) + return; + + _verticesExclusionCaminata.RemoveAt(_verticesExclusionCaminata.Count - 1); + await ActualizarEditorExclusionCaminataAsync(); + } + + private async Task CerrarPoligonoExclusionCaminataAsync() + { + if (_verticesExclusionCaminata.Count < 3) + return; + + _poligonoExclusionCaminataCerrado = true; + await JS.InvokeVoidAsync("rt.closeExclusionPolygonEditor"); + StateHasChanged(); + } + + private async Task CalcularRutaEvitandoPoligonoCaminataAsync() + { + if (!PoligonoExclusionCaminataValido) + return; + + _modoDibujoExclusionCaminata = false; + await JS.InvokeVoidAsync("rt.finishExclusionPolygonEditor"); + _omitirRegistroHistorialEnSiguienteCalculo = true; + await CalcularRutaCompletaAsync(); + } + + [JSInvokable] + public async Task OnExclusionPolygonMapClick(double latitud, double longitud) + { + if (!_modoDibujoExclusionCaminata || _poligonoExclusionCaminataCerrado) + return; + + _verticesExclusionCaminata.Add(new[] { latitud, longitud }); + await ActualizarEditorExclusionCaminataAsync(); + } + + [JSInvokable] + public Task OnExclusionPolygonVertexDragged(int indice, double latitud, double longitud) + { + if (indice >= 0 && indice < _verticesExclusionCaminata.Count) + { + _verticesExclusionCaminata[indice][0] = latitud; + _verticesExclusionCaminata[indice][1] = longitud; + } + + return InvokeAsync(StateHasChanged); + } + + private async Task ActualizarEditorExclusionCaminataAsync() + { + await JS.InvokeVoidAsync("rt.updateExclusionPolygonEditor", _verticesExclusionCaminata); + StateHasChanged(); + } +} diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.razor b/RutasDBUS/Components/Pages/PlanificadorRutas.razor index 9138736..a218463 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.razor +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.razor @@ -847,6 +847,54 @@ } + @if (PuedeProbarExclusionCaminata) + { +
+
+ Probar evitando una zona andando + v2 +
+ + @if (!_modoDibujoExclusionCaminata && !PoligonoExclusionCaminataValido) + { + + } + else + { +
+ @if (_poligonoExclusionCaminataCerrado) + { + Zona cerrada con @_verticesExclusionCaminata.Count vértices. Puedes arrastrarlos. + } + else + { + Pulsa en el mapa para añadir vértices: @_verticesExclusionCaminata.Count. + } +
+
+ @if (!_poligonoExclusionCaminataCerrado) + { + + + } + @if (PoligonoExclusionCaminataValido) + { + + } + +
+ } +
+ } + @* ========= RESUMEN BONITO ARRIBA (único) ========= *@

@if (alt.EsSoloAPie) diff --git a/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs b/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs index 8d6189f..db78ae2 100644 --- a/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs +++ b/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs @@ -37,6 +37,12 @@ public sealed class ParametrosPlanificadorRuta // Divisor aplicado a la distancia en bus dentro del score equilibrado. public double FactorEquilibradoAndarBus { get; set; } = 6.0; + // Proveedor usado para todas las caminatas que intervienen en la planificacion. + public bool UsarValhallaParaCaminatas { get; set; } = false; + + // Zonas temporales que Valhalla debe excluir al calcular caminatas. + public List>? PoligonosExclusionCaminata { get; set; } + // selección candidatas public int RadioParadasCandidatasMetros { get; set; } = 1200; public int MaxParadasCandidatasPorLado { get; set; } = 12; diff --git a/RutasDBUS/Servicios/Caminata/ClienteValhalla.cs b/RutasDBUS/Servicios/Caminata/ClienteValhalla.cs index 5a2ed53..dce0f80 100644 --- a/RutasDBUS/Servicios/Caminata/ClienteValhalla.cs +++ b/RutasDBUS/Servicios/Caminata/ClienteValhalla.cs @@ -32,7 +32,8 @@ public class ClienteValhalla : IClienteValhalla double longitudDesde, double latitudHasta, double longitudHasta, - string costing = "pedestrian") + string costing = "pedestrian", + List>? excludePolygons = null) { var cuerpo = new SolicitudValhalla { @@ -43,6 +44,7 @@ public class ClienteValhalla : IClienteValhalla }, PerfilCoste = string.IsNullOrWhiteSpace(costing) ? "pedestrian" : costing, FormatoGeometria = "polyline6", + PoligonosExclusion = excludePolygons, OpcionesIndicaciones = new OpcionesIndicacionesValhalla { Idioma = "es-ES", @@ -125,6 +127,9 @@ public class ClienteValhalla : IClienteValhalla [JsonPropertyName("shape_format")] public string FormatoGeometria { get; set; } = "polyline6"; + [JsonPropertyName("exclude_polygons")] + public List>? PoligonosExclusion { 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 e191092..6430b20 100644 --- a/RutasDBUS/Servicios/Caminata/IClienteValhalla.cs +++ b/RutasDBUS/Servicios/Caminata/IClienteValhalla.cs @@ -13,5 +13,6 @@ public interface IClienteValhalla double longitudDesde, double latitudHasta, double longitudHasta, - string costing = "pedestrian"); // "pedestrian", "bicycle", etc. + string costing = "pedestrian", + List>? excludePolygons = null); // "pedestrian", "bicycle", etc. } diff --git a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs index 84b91a7..86607ca 100644 --- a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs +++ b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Globalization; +using System.Text.Json; using Microsoft.AspNetCore.Hosting; using RutasDBUS.Modelos.Planificacion; using RutasDBUS.Modelos.RedBus; @@ -24,6 +25,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas private static readonly ConcurrentDictionary _cacheCaminatas = new(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary _cacheCaminatasConExclusion = + new(StringComparer.OrdinalIgnoreCase); private static readonly ConcurrentDictionary _cacheDuracionBusRefinamiento = new(StringComparer.OrdinalIgnoreCase); private static readonly ConcurrentDictionary _cacheSegmentosHorarios = @@ -2063,7 +2066,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas await semaforo.WaitAsync(ct); try { - var caminata = await WalkOsrmCached(latitud, longitud, candidata.Parada.Latitud, candidata.Parada.Longitud); + var caminata = await WalkCaminataCached( + latitud, + longitud, + candidata.Parada.Latitud, + candidata.Parada.Longitud, + parametros); if (caminata.distancia > 0) { candidata.DistanciaAPie = caminata.distancia; @@ -2200,7 +2208,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas => $"{patron.CodigoLinea}|{patron.CodigoItinerario}|{patron.Sentido}"; ///

- /// Crea las dos alternativas de solo a pie. + /// Crea las dos alternativas independientes de solo a pie. /// private async Task> CrearAlternativasSoloPieAsync( double[] origenLatLon, @@ -2209,7 +2217,11 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas { var alternativas = new List(2); - var osrm = await WalkOsrmCached(origenLatLon[0], origenLatLon[1], destinoLatLon[0], destinoLatLon[1]); + var osrm = await WalkOsrmCached( + origenLatLon[0], + origenLatLon[1], + destinoLatLon[0], + destinoLatLon[1]); if (osrm.linea is not null && osrm.distancia > 0) { var duracion = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(osrm.distancia, parametros); @@ -2229,7 +2241,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas }); } - var valhalla = await WalkValhallaCached(origenLatLon[0], origenLatLon[1], destinoLatLon[0], destinoLatLon[1]); + var valhalla = await WalkValhallaCached( + origenLatLon[0], + origenLatLon[1], + destinoLatLon[0], + destinoLatLon[1], + parametros.PoligonosExclusionCaminata); if (valhalla.linea is not null && valhalla.distancia > 0) { var duracion = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(valhalla.distancia, parametros); @@ -2365,7 +2382,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas var total = 0.0; var cursor = instanteSalida; - var acceso = await WalkOsrmCached(origenLatLon[0], origenLatLon[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud); + var acceso = await WalkCaminataCached( + origenLatLon[0], + origenLatLon[1], + paradaInicioBus.Latitud, + paradaInicioBus.Longitud, + parametros); alternativa.DistanciaPieInicioAprox = acceso.distancia > 0 ? acceso.distancia : UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud); @@ -2423,11 +2445,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas distanciaUnion > UmbralCosturaVisualMetros && distanciaUnion <= UmbralUnionPeatonalAutomaticaMetros) { - var transferencia = await WalkOsrmCached( + var transferencia = await WalkCaminataCached( puntoUltimaLlegada[0], puntoUltimaLlegada[1], puntoInicioTramo![0], - puntoInicioTramo[1]); + puntoInicioTramo[1], + parametros); var distanciaTransferencia = transferencia.distancia > 0 ? transferencia.distancia @@ -2560,11 +2583,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas paradaFinBus); alternativa.DestinoParada = paradaFinBus; - var salidaFinal = await WalkOsrmCached( + var salidaFinal = await WalkCaminataCached( paradaFinBus.Latitud, paradaFinBus.Longitud, destinoLatLon[0], - destinoLatLon[1]); + destinoLatLon[1], + parametros); alternativa.DistanciaPieFinAprox = salidaFinal.distancia > 0 ? salidaFinal.distancia : UtilidadesTransitoDbus.CalcularDistanciaMetros( @@ -2776,12 +2800,14 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas if (string.IsNullOrWhiteSpace(paradaPrimera.Codigo) || string.IsNullOrWhiteSpace(paradaDirecta.Codigo)) return; - var accesoPrimero = await WalkOsrmCached(origenLatLon[0], origenLatLon[1], paradaPrimera.Latitud, paradaPrimera.Longitud); + var accesoPrimero = await WalkCaminataCached( + origenLatLon[0], origenLatLon[1], paradaPrimera.Latitud, paradaPrimera.Longitud, parametros); var distanciaPrimero = accesoPrimero.distancia > 0 ? accesoPrimero.distancia : UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaPrimera.Latitud, paradaPrimera.Longitud); - var accesoDirecto = await WalkOsrmCached(origenLatLon[0], origenLatLon[1], paradaDirecta.Latitud, paradaDirecta.Longitud); + var accesoDirecto = await WalkCaminataCached( + origenLatLon[0], origenLatLon[1], paradaDirecta.Latitud, paradaDirecta.Longitud, parametros); var distanciaDirecta = accesoDirecto.distancia > 0 ? accesoDirecto.distancia : UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaDirecta.Latitud, paradaDirecta.Longitud); @@ -3026,7 +3052,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas if (parada is null) return null; - var acceso = await WalkOsrmCached(origenLatLon[0], origenLatLon[1], parada.Latitud, parada.Longitud); + var acceso = await WalkCaminataCached( + origenLatLon[0], origenLatLon[1], parada.Latitud, parada.Longitud, parametros); var distanciaWalk = acceso.distancia > 0 ? acceso.distancia : UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], parada.Latitud, parada.Longitud); @@ -3082,7 +3109,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas if (parada is null) return null; - var salidaFinal = await WalkOsrmCached(parada.Latitud, parada.Longitud, destinoLatLon[0], destinoLatLon[1]); + var salidaFinal = await WalkCaminataCached( + parada.Latitud, parada.Longitud, destinoLatLon[0], destinoLatLon[1], parametros); var distanciaWalk = salidaFinal.distancia > 0 ? salidaFinal.distancia : UtilidadesTransitoDbus.CalcularDistanciaMetros(parada.Latitud, parada.Longitud, destinoLatLon[0], destinoLatLon[1]); @@ -3791,6 +3819,26 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas return true; } + /// + /// Devuelve una caminata usando el proveedor elegido para esta planificación. + /// + private Task<(double[,]? linea, double distancia, double duracion)> WalkCaminataCached( + double latitudOrigen, + double longitudOrigen, + double latitudDestino, + double longitudDestino, + ParametrosPlanificadorRuta parametros) + { + return parametros.UsarValhallaParaCaminatas + ? WalkValhallaCached( + latitudOrigen, + longitudOrigen, + latitudDestino, + longitudDestino, + parametros.PoligonosExclusionCaminata) + : WalkOsrmCached(latitudOrigen, longitudOrigen, latitudDestino, longitudDestino); + } + /// /// Devuelve una caminata desde OSRM con cache. /// @@ -3831,8 +3879,47 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas double latitudOrigen, double longitudOrigen, double latitudDestino, - double longitudDestino) + double longitudDestino, + List>? excludePolygons = null) { + if (excludePolygons is { Count: > 0 }) + { + // Una zona claramente alejada no puede cambiar razonablemente este trayecto. + // Evitamos enviar a Valhalla una busqueda restringida innecesaria. + if (!ExclusionPuedeAfectarTrayecto( + latitudOrigen, longitudOrigen, latitudDestino, longitudDestino, excludePolygons)) + { + return await WalkValhallaCached( + latitudOrigen, longitudOrigen, latitudDestino, longitudDestino); + } + + var claveExclusion = BuildWalkKey("valhalla-exclusion", latitudOrigen, longitudOrigen, latitudDestino, longitudDestino) + + "|" + JsonSerializer.Serialize(excludePolygons); + if (_cacheCaminatasConExclusion.TryGetValue(claveExclusion, out var cacheadoConExclusion)) + return cacheadoConExclusion; + + try + { + var (linea, distancia, duracion) = await _clienteValhalla.ObtenerRutaAsync( + latitudOrigen, + longitudOrigen, + latitudDestino, + longitudDestino, + costing: "pedestrian", + excludePolygons: excludePolygons); + + var valor = (linea, distancia, duracion); + _cacheCaminatasConExclusion[claveExclusion] = valor; + return valor; + } + catch + { + var vacio = (linea: (double[,]?)null, distancia: 0.0, duracion: 0.0); + _cacheCaminatasConExclusion[claveExclusion] = vacio; + return vacio; + } + } + var clave = BuildWalkKey("valhalla", latitudOrigen, longitudOrigen, latitudDestino, longitudDestino); if (_cacheCaminatas.TryGetValue(clave, out var cacheado)) return cacheado; @@ -3858,6 +3945,41 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas } } + private static bool ExclusionPuedeAfectarTrayecto( + double latitudOrigen, + double longitudOrigen, + double latitudDestino, + double longitudDestino, + List> poligonos) + { + const double umbralMetros = 2500.0; + var escalaLat = 111_320.0; + var escalaLon = Math.Max(1.0, escalaLat * Math.Cos(latitudOrigen * Math.PI / 180.0)); + var ax = 0.0; + var ay = 0.0; + var bx = (longitudDestino - longitudOrigen) * escalaLon; + var by = (latitudDestino - latitudOrigen) * escalaLat; + var ab2 = bx * bx + by * by; + + foreach (var poligono in poligonos) + { + foreach (var punto in poligono) + { + if (punto is null || punto.Length < 2) continue; + // Valhalla recibe los vertices como [longitud, latitud]. + var px = (punto[0] - longitudOrigen) * escalaLon; + var py = (punto[1] - latitudOrigen) * escalaLat; + var t = ab2 > 0 ? Math.Clamp((px * bx + py * by) / ab2, 0.0, 1.0) : 0.0; + var dx = px - (ax + t * bx); + var dy = py - (ay + t * by); + if (Math.Sqrt(dx * dx + dy * dy) <= umbralMetros) + return true; + } + } + + return false; + } + /// /// Construye una clave estable de cache para caminatas. /// diff --git a/RutasDBUS/wwwroot/app.css b/RutasDBUS/wwwroot/app.css index c2b8bff..e9d4c62 100644 --- a/RutasDBUS/wwwroot/app.css +++ b/RutasDBUS/wwwroot/app.css @@ -1132,6 +1132,76 @@ html, body { line-height: 1.25; } +.route-exclusion-tool { + margin: 0 0 .55rem; + padding: .48rem .6rem; + border: 1px solid rgba(248, 113, 113, .42); + border-left: 3px solid #ef4444; + border-radius: 6px; + background: rgba(127, 29, 29, .16); +} + +.route-exclusion-tool__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: .6rem; + margin-bottom: .28rem; +} + +.route-exclusion-tool__actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: .35rem; + margin-top: .35rem; +} + +.route-exclusion-tool__close { + border-color: rgba(248, 113, 113, .6); + color: #fecaca; +} + +.route-exclusion-tool__close:not(:disabled):hover { + border-color: #f87171; + background: rgba(185, 28, 28, .25); + color: #fff; +} + +.route-exclusion-tool__close:disabled { + opacity: .45; +} + +.route-exclusion-tool__calculate { + border: 1px solid rgba(125, 211, 252, .7); + border-radius: 999px; + background: #1d4ed8; + padding: .3rem .9rem; + font-size: .78rem; + font-weight: 700; + color: #fff; + cursor: pointer; +} + +.route-exclusion-tool__calculate:hover { + background: #2563eb; + border-color: #bae6fd; +} + +.route-exclusion-tool__hint { + color: var(--muted-text, #a1a1aa); + font-size: .82rem; +} + +.rt-exclusion-vertex { + width: 18px !important; + height: 18px !important; + border: 2px solid #fff; + border-radius: 50%; + background: #dc2626; + box-shadow: 0 0 0 2px #7f1d1d, 0 1px 4px rgba(0, 0, 0, .55); +} + .taxibus-notice { margin: 0 0 .45rem; padding: .38rem .5rem; diff --git a/RutasDBUS/wwwroot/app.js b/RutasDBUS/wwwroot/app.js index e9ff6a7..f00af84 100644 --- a/RutasDBUS/wwwroot/app.js +++ b/RutasDBUS/wwwroot/app.js @@ -330,6 +330,7 @@ window.rt.drawRoute = function (id, coords, options) { const attachRouteClick = function (line) { line.on("click", function (e) { try { + if (window.rt._exclusionEditMode) return; if (window.rt.handleInspectorLayerClick(e)) return; if (e && e.originalEvent) { @@ -464,6 +465,7 @@ window.rt.drawRoute = function (id, coords, options) { segmentLine.on("mouseover", updateTooltipAtCursor); segmentLine.on("mousemove", updateTooltipAtCursor); segmentLine.on("click", function (e) { + if (window.rt._exclusionEditMode) return; if (window.rt.handleInspectorLayerClick(e)) return; window.rt.selectElevationSegment( @@ -655,6 +657,93 @@ window.rt.setDotNetHelper = function (dotNetHelper) { window.rt._dotNetHelper = dotNetHelper; }; +// Editor temporal de zonas que se excluiran de las caminatas de Valhalla. +window.rt._exclusionPolygonLayer = null; +window.rt._exclusionPolygon = null; +window.rt._exclusionPolygonMarkers = []; +window.rt._exclusionEditMode = false; + +window.rt.beginExclusionPolygonEditor = function () { + const map = window.__rtLeafletMap; + if (!map || !window.L) return; + + window.rt.clearExclusionPolygonEditor(); + window.rt._exclusionEditMode = true; + window.rt._exclusionPolygonLayer = window.L.layerGroup().addTo(map); + window.rt._exclusionPolygon = window.L.polygon([], { + color: "#dc2626", + weight: 3, + opacity: 0.95, + fillColor: "#ef4444", + fillOpacity: 0.2, + interactive: false + }).addTo(window.rt._exclusionPolygonLayer); +}; + +window.rt.updateExclusionPolygonEditor = function (points) { + const map = window.__rtLeafletMap; + if (!map || !window.L) return; + if (!window.rt._exclusionPolygonLayer) window.rt.beginExclusionPolygonEditor(); + + const polygonPoints = Array.isArray(points) + ? points.filter(p => Array.isArray(p) && Number.isFinite(Number(p[0])) && Number.isFinite(Number(p[1]))) + .map(p => [Number(p[0]), Number(p[1])]) + : []; + + window.rt._exclusionPolygon.setLatLngs(polygonPoints); + window.rt._exclusionPolygonMarkers.forEach(marker => marker.remove()); + window.rt._exclusionPolygonMarkers = polygonPoints.map((point, index) => { + const marker = window.L.marker(point, { + draggable: true, + autoPan: true, + bubblingMouseEvents: false, + icon: window.L.divIcon({ + className: "rt-exclusion-vertex", + html: "", + iconSize: [18, 18], + iconAnchor: [9, 9] + }) + }).addTo(window.rt._exclusionPolygonLayer); + + marker.on("click", event => { + if (event && event.originalEvent) event.originalEvent.stopPropagation(); + }); + marker.on("drag", event => { + const latLng = event.target.getLatLng(); + const current = window.rt._exclusionPolygon.getLatLngs()[0] || []; + if (current[index]) { + current[index] = latLng; + window.rt._exclusionPolygon.setLatLngs([current]); + } + }); + marker.on("dragend", event => { + const latLng = event.target.getLatLng(); + window.rt._dotNetHelper?.invokeMethodAsync( + "OnExclusionPolygonVertexDragged", index, latLng.lat, latLng.lng); + }); + return marker; + }); +}; + +window.rt.closeExclusionPolygonEditor = function () { + if (window.rt._exclusionPolygon) { + window.rt._exclusionPolygon.setStyle({ fillOpacity: 0.28 }); + } +}; + +window.rt.finishExclusionPolygonEditor = function () { + window.rt._exclusionEditMode = false; +}; + +window.rt.clearExclusionPolygonEditor = function () { + window.rt._exclusionPolygonMarkers.forEach(marker => marker.remove()); + window.rt._exclusionPolygonMarkers = []; + window.rt._exclusionPolygonLayer?.remove(); + window.rt._exclusionPolygonLayer = null; + window.rt._exclusionPolygon = null; + window.rt._exclusionEditMode = false; +}; + @@ -1025,6 +1114,7 @@ window.rt.drawPreviewLine = function (id, coords, options) { // ✅ Hacer la línea clicable y avisar a Blazor line.on("click", function (e) { try { + if (window.rt._exclusionEditMode) return; if (window.rt.handleInspectorLayerClick(e)) return; if (e && e.originalEvent) { @@ -1213,9 +1303,10 @@ window.rt.createHighlightedStopMarker = function (stop, pane, options) { }); if (stop.code && window.rt._dotNetHelper) { - marker.on("click", function (event) { - try { - if (window.rt.handleInspectorLayerClick(event)) return; + marker.on("click", function (event) { + try { + if (window.rt._exclusionEditMode) return; + if (window.rt.handleInspectorLayerClick(event)) return; if (event && event.originalEvent) { if (event.originalEvent.preventDefault) event.originalEvent.preventDefault(); @@ -1407,6 +1498,7 @@ window.rt.setEndpointMarker = function (kind, lat, lon, options) { }; marker.on("click", function (event) { + if (window.rt._exclusionEditMode) return; if (window.rt.handleInspectorLayerClick(event)) return; stopEvent(event); }); @@ -1493,6 +1585,7 @@ window.rt.setInspectorMarkers = function (lat, lon, places) { className: "rt-inspector-tooltip" }); puntoZ.on("click", function (event) { + if (window.rt._exclusionEditMode) return; window.rt.handleInspectorLayerClick(event); }); puntoZ.addTo(window.rt._inspectorLayer); @@ -1568,6 +1661,7 @@ window.rt.setInspectorMarkers = function (lat, lon, places) { className: "rt-inspector-tooltip" }); marker.on("click", function (event) { + if (window.rt._exclusionEditMode) return; window.rt.handleInspectorLayerClick(event); }); marker.addTo(window.rt._inspectorLayer);