diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs index 14d279a..0c2ccea 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-20260909a)"; + private const string _versionApp = "(v-20260910a)"; private double? _paradaElevIdee; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.RestriccionesCaminata.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.RestriccionesCaminata.cs index 9a87c28..a2dece7 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.RestriccionesCaminata.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.RestriccionesCaminata.cs @@ -69,6 +69,7 @@ public partial class PlanificadorRutas private async Task CambiarZonaActivaAsync(ZonaExclusionCaminata zona, ChangeEventArgs args) { zona.Activa = args.Value is true; + await GuardarZonasAsync(); await ActualizarEditorExclusionCaminataAsync(); } private ZonaExclusionCaminata? ZonaExclusionSeleccionada => @@ -127,6 +128,12 @@ public partial class PlanificadorRutas await ActualizarEditorExclusionCaminataAsync(); } + private void SeleccionarTipoExclusionCaminata(bool esPunto) + { + if (_recalculandoZonas || _modoDibujoExclusionCaminata) return; + _nuevaExclusionEsPunto = esPunto; + } + private async Task SeleccionarZonaExclusionAsync(string id) { if (_recalculandoZonas) return; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.razor b/RutasDBUS/Components/Pages/PlanificadorRutas.razor index 57a3f32..8c20882 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.razor +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.razor @@ -1515,12 +1515,16 @@ } else { +
+ + +
- } diff --git a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs index a32e274..3b8315c 100644 --- a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs +++ b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs @@ -3885,16 +3885,6 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas { 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); - // 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(excludeLocations); if (_cacheCaminatasConExclusion.TryGetValue(claveExclusion, out var cacheadoConExclusion)) @@ -3950,38 +3940,6 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas } } - private static bool ExclusionPuedeAfectarTrayecto(double[,] linea, List> poligonos) - { - if (linea.GetLength(0) < 2) return true; - var minLat = double.PositiveInfinity; - var maxLat = double.NegativeInfinity; - var minLon = double.PositiveInfinity; - var maxLon = double.NegativeInfinity; - for (var i = 0; i < linea.GetLength(0); i++) - { - if (!double.IsFinite(linea[i, 0]) || !double.IsFinite(linea[i, 1])) return true; - minLat = Math.Min(minLat, linea[i, 0]); - maxLat = Math.Max(maxLat, linea[i, 0]); - minLon = Math.Min(minLon, linea[i, 1]); - maxLon = Math.Max(maxLon, linea[i, 1]); - } - // Margen conservador para las vias y sus extremos, no solo los vertices dibujados. - const double margenLat = 2500.0 / 111320.0; - var margenLon = margenLat / Math.Max(0.01, Math.Cos((minLat + maxLat) * Math.PI / 360.0)); - foreach (var poligono in poligonos) - { - if (poligono.Count < 3 || poligono.Any(p => p.Length < 2 || !double.IsFinite(p[0]) || !double.IsFinite(p[1]))) - return true; - // Los poligonos usan [lon, lat]; sus limites incluyen lados y area interior. - if (poligono.Max(p => p[1]) >= minLat - margenLat && - poligono.Min(p => p[1]) <= maxLat + margenLat && - poligono.Max(p => p[0]) >= minLon - margenLon && - poligono.Min(p => p[0]) <= maxLon + margenLon) - 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 45996c7..e175fae 100644 --- a/RutasDBUS/wwwroot/app.css +++ b/RutasDBUS/wwwroot/app.css @@ -1167,7 +1167,31 @@ html, body { .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__type { + display: inline-grid; + grid-template-columns: 1fr 1fr; + padding: 2px; + border: 1px solid rgba(148, 163, 184, .6); + border-radius: 999px; + background: #0b1220; +} +.zones-panel__type button { + min-height: 28px; + padding: .2rem .7rem; + border: 0; + border-radius: 999px; + background: transparent; + color: #94a3b8; + font: inherit; + font-size: .78rem; + cursor: pointer; +} +.zones-panel__type button.is-active { + background: #334155; + color: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, .35); +} +.zones-panel__type button:disabled { cursor: not-allowed; opacity: .55; } .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; } diff --git a/RutasDBUS/wwwroot/app.js b/RutasDBUS/wwwroot/app.js index 7645d63..14613a5 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.shiftKey || e.altKey || e.metaKey) return; + if (e.repeat || e.code !== "KeyE" || !e.ctrlKey || !e.shiftKey || e.altKey || e.metaKey) return; e.preventDefault(); e.stopImmediatePropagation(); @@ -798,19 +798,8 @@ window.rt.updateExclusionPolygonEditor = function (zones, selectedId, busy) { }); } - if (drawing) { - const container = map.getContainer(); - const handler = event => { - if (event.target.closest(".rt-exclusion-vertex, .leaflet-control, .leaflet-popup")) return; - event.preventDefault(); - event.stopImmediatePropagation(); - const pos = map.mouseEventToLatLng(event); - window.rt._dotNetHelper?.invokeMethodAsync("OnExclusionPolygonMapClick", pos.lat, pos.lng); - }; - container.addEventListener("click", handler, true); - window.rt._exclusionMapContainer = container; - window.rt._exclusionClickHandler = handler; - } + // El clic llega por el evento normal del mapa. No se registra otro manejador + // aquí para evitar duplicar vértices, especialmente en las exclusiones puntuales. }; window.rt.isExclusionPolygonValid = function (points) { diff --git a/tests/RutasDBUS.RegressionTests/Program.cs b/tests/RutasDBUS.RegressionTests/Program.cs index e9e99af..1f47a5e 100644 --- a/tests/RutasDBUS.RegressionTests/Program.cs +++ b/tests/RutasDBUS.RegressionTests/Program.cs @@ -58,23 +58,27 @@ List Ring(double lon, double lat, double size) => await Walk(); var far = new List> { Ring(-2.3, 43.0, .01) }; await Walk(far); -Check(fake.Calls == 1, "Zona lejana reutiliza la geometria conocida"); +Check(fake.Calls == 2 && fake.Zones == 1, "Toda zona activa se envia a V2 aunque este lejos de la ruta previa"); var near = new List> { Ring(-1.985, 43.315, .001), Ring(-1.982, 43.317, .001) }; await Walk(near); Check(fake.Zones == 2, "Se envian todos los poligonos"); await Walk(near); -Check(fake.Calls == 2, "Mismas zonas reutilizan cache restringida"); +Check(fake.Calls == 3, "Mismas zonas reutilizan cache restringida"); near[0][0][0] += .00001; await Walk(near); -Check(fake.Calls == 3, "Mover un vertice invalida esa entrada de cache"); +Check(fake.Calls == 4, "Mover un vertice invalida esa entrada de cache"); await Walk(new() { Ring(-2.2, 43.1, .5) }); -Check(fake.Calls == 4, "Una zona que rodea la ruta no se ignora aunque sus vertices esten lejos"); +Check(fake.Calls == 5, "Una zona que rodea la ruta no se ignora aunque sus vertices esten lejos"); fake.Fail = true; var retry = new List> { Ring(-1.984, 43.312, .0002) }; await Walk(retry); fake.Fail = false; await Walk(retry); -Check(fake.Calls == 6, "Un fallo temporal no queda guardado en cache de zonas"); +Check(fake.Calls == 7, "Un fallo temporal no queda guardado en cache de zonas"); +var puntos = new List { new[] { 43.314, -1.984 }, new[] { 43.315, -1.983 } }; +await (Task<(double[,]?, double, double)>)walk.Invoke(planner, + new object?[] { 43.31, -1.99, 43.32, -1.98, near, puntos })!; +Check(fake.Zones == 2 && fake.Points == 2, "Se envian juntos todos los poligonos y puntos activos"); var page = new PlanificadorRutas(); var flags = BindingFlags.Instance | BindingFlags.NonPublic; var options = (List)typeof(PlanificadorRutas).GetField("_opcionesRuta", flags)!.GetValue(page)!; @@ -102,6 +106,7 @@ sealed class FakeValhalla : IClienteValhalla public int Calls { get; private set; } public int Zones { get; private set; } + public int Points { 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, @@ -110,6 +115,7 @@ sealed class FakeValhalla : IClienteValhalla { Calls++; Zones = excludePolygons?.Count ?? 0; + Points = excludeLocations?.Count ?? 0; return Task.FromResult<(double[,]?, double, double)>(Fail ? (null, 0, 0) : (new[,] { { latitudDesde, longitudDesde }, { latitudHasta, longitudHasta } }, 100, 100)); } diff --git a/tests/verify-zones.cjs b/tests/verify-zones.cjs index 4f59c40..21090ca 100644 --- a/tests/verify-zones.cjs +++ b/tests/verify-zones.cjs @@ -14,9 +14,10 @@ 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.keyboard.press("Control+Shift+KeyZ"); + await page.keyboard.press("Control+Shift+KeyE"); await page.locator(".zones-panel").waitFor(); await page.waitForTimeout(1000); + await page.getByRole("button", { name: "Polígono", exact: true }).click(); 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); @@ -52,7 +53,7 @@ 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.keyboard.press("Control+Shift+KeyZ"); + await page.keyboard.press("Control+Shift+KeyE"); 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(); @@ -60,7 +61,7 @@ const path = require("node:path"); await page.reload(); await page.waitForFunction(() => !!window.rt?._dotNetHelper && !!window.__rtLeafletMap); await page.waitForTimeout(1000); - await page.keyboard.press("Control+Shift+KeyZ"); + await page.keyboard.press("Control+Shift+KeyE"); 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); @@ -81,6 +82,16 @@ const path = require("node:path"); 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); + await page.getByRole("button", { name: "Punto", exact: true }).click(); + await page.getByRole("button", { name: "Añadir punto", exact: true }).click(); + await page.mouse.click(1050, 720); + await page.waitForFunction(() => document.querySelectorAll(".zones-panel__item").length === 2); + assert.match(await page.locator(".zones-panel__item").last().innerText(), /Punto/); + assert.equal(await page.locator(".zones-panel__item").last().innerText().then(text => text.includes("abierta")), false, + "Un clic cierra la exclusion puntual"); + const puntoGuardado = await page.evaluate(() => JSON.parse(localStorage.getItem("rutasdbus.zonas.v1")).at(-1)); + assert.equal(puntoGuardado.EsPunto, true); + assert.equal(puntoGuardado.Vertices.length, 1, "El punto se guarda con un solo vertice"); await page.getByRole("button", { name: "Borrar todas", exact: true }).click(); await page.waitForFunction(() => document.querySelectorAll(".zones-panel__item").length === 0); assert.equal(await page.locator(".zones-panel__item").count(), 0); @@ -88,6 +99,7 @@ const path = require("node:path"); await page.screenshot({ path: path.join(artifacts, "mobile.png") }); const box = await page.locator(".zones-panel").boundingBox(); assert.ok(box.x >= 0 && box.x + box.width <= 390 && box.y + box.height <= 844, "Panel dentro del movil"); + await page.getByRole("button", { name: "Polígono", exact: true }).click(); await page.getByRole("button", { name: "Añadir zona", exact: true }).click(); await page.getByRole("button", { name: "Cerrar herramienta", exact: true }).click(); await page.setViewportSize({ width: 1440, height: 1000 }); @@ -99,7 +111,7 @@ const path = require("node:path"); const newErrors = errors.filter(error => !initialErrors.includes(error) && !externalErrors.includes(error)); assert.equal(newErrors.length, 0, newErrors.join("\n")); if (externalErrors.length) console.log("Aviso del complemento externo de geocodificacion: " + externalErrors[0]); - console.log("OK multiples zonas, arrastre en vivo, cierre, reapertura, borrado, alturas de botones y movil"); + console.log("OK poligonos y puntos, arrastre en vivo, cierre, reapertura, borrado, alturas de botones y movil"); } finally { await browser.close(); }