cambaido ctrl por los shift

mover ventana de zonas de exclusion
puntos en zonas de exclusion
vertices mas pequeños en las zonas de exclusion
check y persistencia de las zonas d eexclusion*
distancia andando visual de un punto z al anterior
primer lector del lidar para simualciones de terreno
quitado el traductor de el selector de superficie de valhalla
This commit is contained in:
2026-09-09 14:59:44 +02:00
parent acbdd1501d
commit d50333f82a
16 changed files with 316 additions and 54 deletions

View File

@@ -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)

View File

@@ -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<double[]>(), 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
};

View File

@@ -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;

View File

@@ -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<LugarGeocodificadoCercano>();
@@ -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),

View File

@@ -1322,7 +1322,7 @@ public partial class PlanificadorRutas : ComponentBase
private async Task ReiniciarSeleccion()
{
_proveedorSoloAPieRecalculo = null;
await LimpiarZonasExclusionAsync();
await CerrarHerramientaZonasAsync();
await CerrarHerramientaZonasAsync();
await CerrarTodasLasPreviewsAsync();

View File

@@ -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<double[]> Vertices { get; } = new();
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public List<double[]> Vertices { get; set; } = new();
public bool Cerrada { get; set; }
public bool Activa { get; set; } = true;
public bool EsPunto { get; set; }
}
private readonly List<ZonaExclusionCaminata> _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<JsonElement>("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<List<ZonaExclusionCaminata>>(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<bool>("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<JsonElement>("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<List<double[]>>? 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<double[]>? 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<bool>("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);
}
}

View File

@@ -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; }
}
}

View File

@@ -447,7 +447,7 @@
}
</div>
<div class="inspector-surface-row">
<b>Superficie:</b>
<b>Superficie (V2):</b>
@if (_inspectorSuperficieCargando)
{
<span class="muted">Consultando...</span>
@@ -457,6 +457,10 @@
<span>@(_inspectorSuperficieTexto ?? "No disponible")</span>
}
</div>
<div class="inspector-surface-row">
<b>Clasificación LiDAR:</b>
<span class="muted">No disponible: falta la nube LiDAR clasificada local.</span>
</div>
@if (_inspectorLugaresCercanos.Count > 0)
{
@@ -507,6 +511,15 @@
else
{
<span>@(puntoAnterior.DistanciaAndandoMetros is { } distanciaAndando ? FormatearDistanciaPuntoInspector(distanciaAndando) : "-")</span>
@if (_caminoInspectorAnterior is not null)
{
<button type="button" class="btn-ghost inspector-walk-eye"
title="@(_caminoInspectorVisible ? "Ocultar camino" : "Ver camino")"
aria-label="@(_caminoInspectorVisible ? "Ocultar camino" : "Ver camino")"
aria-pressed="@_caminoInspectorVisible" @onclick="AlternarCaminoInspectorAsync">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7S2 12 2 12Z" /><circle cx="12" cy="12" r="3" /></svg>
</button>
}
}
</span>
</div>
@@ -1480,9 +1493,12 @@
{
var zona = _zonasExclusionCaminata[i];
<div class="zones-panel__item" @key="zona.Id">
<input type="checkbox" checked="@zona.Activa" title="Activar zona"
aria-label="Activar zona" disabled="@_recalculandoZonas"
@onchange="e => CambiarZonaActivaAsync(zona, e)" />
<button type="button" class="btn-ghost @(zona.Id == _zonaExclusionSeleccionada ? "zones-panel__selected" : "")"
disabled="@(_recalculandoZonas || !zona.Cerrada)" @onclick="() => SeleccionarZonaExclusionAsync(zona.Id)">
Zona @(i + 1) · @zona.Vertices.Count puntos @(!zona.Cerrada ? "(abierta)" : "")
disabled="@_recalculandoZonas" @onclick="() => SeleccionarZonaExclusionAsync(zona.Id)">
@(zona.EsPunto ? "Punto" : "Zona") @(i + 1) @(!zona.EsPunto ? $"· {zona.Vertices.Count} puntos" : "") @(!zona.Cerrada ? "(abierta)" : "")
</button>
<button type="button" class="btn-ghost zones-panel__icon" title="Eliminar zona" aria-label="Eliminar zona"
disabled="@_recalculandoZonas" @onclick="() => EliminarZonaExclusionAsync(zona.Id)">×</button>
@@ -1494,13 +1510,17 @@
{
<button type="button" class="btn-ghost" title="Deshacer último punto" aria-label="Deshacer último punto"
disabled="@(ZonaExclusionSeleccionada!.Vertices.Count == 0)" @onclick="DeshacerVerticeExclusionCaminataAsync">↶</button>
<button type="button" class="btn-ghost" disabled="@(ZonaExclusionSeleccionada!.Vertices.Count < 3)"
<button type="button" class="btn-ghost" disabled="@(ZonaExclusionSeleccionada!.Vertices.Count < 3 || ZonaExclusionSeleccionada.EsPunto)"
@onclick="CerrarPoligonoExclusionCaminataAsync">Cerrar zona</button>
}
else
{
<button type="button" class="btn-ghost" disabled="@(_recalculandoZonas || _zonasExclusionCaminata.Count >= 10)"
@onclick="IniciarDibujoExclusionCaminataAsync">Añadir zona</button>
@onclick="IniciarDibujoExclusionCaminataAsync">@(_nuevaExclusionEsPunto ? "Añadir punto" : "Añadir zona")</button>
<select class="zones-panel__type" aria-label="Tipo de exclusión" @bind="_nuevaExclusionEsPunto" disabled="@_recalculandoZonas">
<option value="false">Polígono</option>
<option value="true">Punto</option>
</select>
}
<button type="button" class="btn-ghost" disabled="@(_recalculandoZonas || _zonasExclusionCaminata.Count == 0)"
@onclick="LimpiarZonasExclusionAsync">Borrar todas</button>
@@ -2244,3 +2264,37 @@
</div>
}
@if (_fabricaPendiente is not null || _mensajeFabrica is not null)
{
<div class="custom-modal-backdrop" @onclick="CancelarGuardadoFabrica">
<div class="custom-modal-card fabrica-confirmacion" @onclick:stopPropagation="true" role="dialog" aria-modal="true" aria-label="Guardar valores predeterminados">
<div class="custom-modal-title">Valores predeterminados</div>
@if (_fabricaPendiente is not null)
{
<p>¿Seguro que quieres guardar esta configuración y sustituir los valores predeterminados para todos los usuarios?</p>
<div class="fabrica-cambios">
@foreach (var cambio in _cambiosFabrica)
{
<div><b>@cambio.Campo:</b> @cambio.Antes → @cambio.Despues</div>
}
@if (_cambiosFabrica.Count == 0)
{
<div>Sin cambios respecto a los valores guardados.</div>
}
</div>
}
@if (_mensajeFabrica is not null)
{
<p role="status">@_mensajeFabrica</p>
}
<div class="custom-modal-actions">
<button class="btn-ghost" disabled="@_guardandoFabrica" @onclick="CancelarGuardadoFabrica">@(_fabricaPendiente is null ? "Cerrar" : "Cancelar")</button>
@if (_fabricaPendiente is not null)
{
<button class="btn-ghost" disabled="@_guardandoFabrica" @onclick="ConfirmarGuardadoFabricaAsync">@(_guardandoFabrica ? "Guardando..." : "Guardar y sustituir")</button>
}
</div>
</div>
</div>
}