Añadido filtro de prohibicion de caminos por poligono dibujado

This commit is contained in:
2026-09-07 12:02:52 +02:00
parent bb9a407c04
commit d13ff13da1
10 changed files with 489 additions and 20 deletions

View File

@@ -1400,7 +1400,7 @@ public partial class PlanificadorRutas : ComponentBase
private bool _elevPopupMinimizado = false; private bool _elevPopupMinimizado = false;
private const string _versionApp = "(v-20260903b)"; private const string _versionApp = "(v-20260907a)";
private double? _paradaElevIdee; private double? _paradaElevIdee;

View File

@@ -1659,6 +1659,12 @@ public partial class PlanificadorRutas : ComponentBase
{ {
var lat = e.location.latitude; var lat = e.location.latitude;
var lon = e.location.longitude; var lon = e.location.longitude;
if (_modoDibujoExclusionCaminata)
{
await OnExclusionPolygonMapClick(lat, lon);
return;
}
// Inspector (Z) sigue siendo click simple inmediato // Inspector (Z) sigue siendo click simple inmediato
if (_inspectorActivo) if (_inspectorActivo)
@@ -1921,6 +1927,8 @@ public partial class PlanificadorRutas : ComponentBase
ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros, ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros,
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos, PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus, FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
UsarValhallaParaCaminatas = _walkProvider == ProveedorCaminata.Valhalla,
PoligonosExclusionCaminata = ObtenerPoligonosExclusionCaminata(),
VelocidadBusKmH = _velocidadBusKmH, VelocidadBusKmH = _velocidadBusKmH,
MinutosPorTransbordo = _minutosTransbordo, MinutosPorTransbordo = _minutosTransbordo,
MargenTransbordoSegundos = Math.Clamp(_margenTransbordoSegundos, 0, 1800), MargenTransbordoSegundos = Math.Clamp(_margenTransbordoSegundos, 0, 1800),

View File

@@ -0,0 +1,115 @@
using Microsoft.JSInterop;
namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas
{
private bool _modoDibujoExclusionCaminata;
private bool _poligonoExclusionCaminataCerrado;
private readonly List<double[]> _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<List<double[]>>? ObtenerPoligonosExclusionCaminata()
{
if (!PoligonoExclusionCaminataValido)
return null;
return new List<List<double[]>>
{
_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();
}
}

View File

@@ -847,6 +847,54 @@
</div> </div>
} }
@if (PuedeProbarExclusionCaminata)
{
<div class="route-exclusion-tool" role="region" aria-label="Evitar zona andando">
<div class="route-exclusion-tool__header">
<strong>Probar evitando una zona andando</strong>
<span class="muted">v2</span>
</div>
@if (!_modoDibujoExclusionCaminata && !PoligonoExclusionCaminataValido)
{
<button type="button" class="btn-ghost route-exclusion-tool__button"
title="Dibujar una zona para evitarla en la siguiente búsqueda"
@onclick="IniciarDibujoExclusionCaminataAsync">
Dibujar zona a evitar
</button>
}
else
{
<div class="route-exclusion-tool__status">
@if (_poligonoExclusionCaminataCerrado)
{
<span>Zona cerrada con @_verticesExclusionCaminata.Count vértices. Puedes arrastrarlos.</span>
}
else
{
<span>Pulsa en el mapa para añadir vértices: @_verticesExclusionCaminata.Count.</span>
}
</div>
<div class="route-exclusion-tool__actions">
@if (!_poligonoExclusionCaminataCerrado)
{
<button type="button" class="btn-ghost" @onclick="DeshacerVerticeExclusionCaminataAsync"
disabled="@(_verticesExclusionCaminata.Count == 0)">Deshacer punto</button>
<button type="button" class="btn-ghost route-exclusion-tool__close" @onclick="CerrarPoligonoExclusionCaminataAsync"
disabled="@(_verticesExclusionCaminata.Count < 3)">Cerrar zona</button>
}
@if (PoligonoExclusionCaminataValido)
{
<button type="button" class="btn-primary route-exclusion-tool__calculate" @onclick="CalcularRutaEvitandoPoligonoCaminataAsync">
Calcular alternativa
</button>
}
<button type="button" class="btn-ghost" @onclick="CancelarDibujoExclusionCaminataAsync">Cancelar</button>
</div>
}
</div>
}
@* ========= RESUMEN BONITO ARRIBA (único) ========= *@ @* ========= RESUMEN BONITO ARRIBA (único) ========= *@
<p class="route-main-text"> <p class="route-main-text">
@if (alt.EsSoloAPie) @if (alt.EsSoloAPie)

View File

@@ -37,6 +37,12 @@ public sealed class ParametrosPlanificadorRuta
// Divisor aplicado a la distancia en bus dentro del score equilibrado. // Divisor aplicado a la distancia en bus dentro del score equilibrado.
public double FactorEquilibradoAndarBus { get; set; } = 6.0; 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<List<double[]>>? PoligonosExclusionCaminata { get; set; }
// selección candidatas // selección candidatas
public int RadioParadasCandidatasMetros { get; set; } = 1200; public int RadioParadasCandidatasMetros { get; set; } = 1200;
public int MaxParadasCandidatasPorLado { get; set; } = 12; public int MaxParadasCandidatasPorLado { get; set; } = 12;

View File

@@ -32,7 +32,8 @@ public class ClienteValhalla : IClienteValhalla
double longitudDesde, double longitudDesde,
double latitudHasta, double latitudHasta,
double longitudHasta, double longitudHasta,
string costing = "pedestrian") string costing = "pedestrian",
List<List<double[]>>? excludePolygons = null)
{ {
var cuerpo = new SolicitudValhalla var cuerpo = new SolicitudValhalla
{ {
@@ -43,6 +44,7 @@ public class ClienteValhalla : IClienteValhalla
}, },
PerfilCoste = string.IsNullOrWhiteSpace(costing) ? "pedestrian" : costing, PerfilCoste = string.IsNullOrWhiteSpace(costing) ? "pedestrian" : costing,
FormatoGeometria = "polyline6", FormatoGeometria = "polyline6",
PoligonosExclusion = excludePolygons,
OpcionesIndicaciones = new OpcionesIndicacionesValhalla OpcionesIndicaciones = new OpcionesIndicacionesValhalla
{ {
Idioma = "es-ES", Idioma = "es-ES",
@@ -125,6 +127,9 @@ public class ClienteValhalla : IClienteValhalla
[JsonPropertyName("shape_format")] [JsonPropertyName("shape_format")]
public string FormatoGeometria { get; set; } = "polyline6"; public string FormatoGeometria { get; set; } = "polyline6";
[JsonPropertyName("exclude_polygons")]
public List<List<double[]>>? PoligonosExclusion { get; set; }
[JsonPropertyName("directions_options")] [JsonPropertyName("directions_options")]
public OpcionesIndicacionesValhalla OpcionesIndicaciones { get; set; } = new(); public OpcionesIndicacionesValhalla OpcionesIndicaciones { get; set; } = new();
} }

View File

@@ -13,5 +13,6 @@ public interface IClienteValhalla
double longitudDesde, double longitudDesde,
double latitudHasta, double latitudHasta,
double longitudHasta, double longitudHasta,
string costing = "pedestrian"); // "pedestrian", "bicycle", etc. string costing = "pedestrian",
List<List<double[]>>? excludePolygons = null); // "pedestrian", "bicycle", etc.
} }

View File

@@ -1,5 +1,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Globalization; using System.Globalization;
using System.Text.Json;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using RutasDBUS.Modelos.Planificacion; using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus; using RutasDBUS.Modelos.RedBus;
@@ -24,6 +25,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
private static readonly ConcurrentDictionary<string, (double[,]? linea, double distancia, double duracion)> _cacheCaminatas = private static readonly ConcurrentDictionary<string, (double[,]? linea, double distancia, double duracion)> _cacheCaminatas =
new(StringComparer.OrdinalIgnoreCase); new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, (double[,]? linea, double distancia, double duracion)> _cacheCaminatasConExclusion =
new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, double> _cacheDuracionBusRefinamiento = private static readonly ConcurrentDictionary<string, double> _cacheDuracionBusRefinamiento =
new(StringComparer.OrdinalIgnoreCase); new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, (bool ok, DateTime salida, DateTime llegada)> _cacheSegmentosHorarios = private static readonly ConcurrentDictionary<string, (bool ok, DateTime salida, DateTime llegada)> _cacheSegmentosHorarios =
@@ -2063,7 +2066,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
await semaforo.WaitAsync(ct); await semaforo.WaitAsync(ct);
try 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) if (caminata.distancia > 0)
{ {
candidata.DistanciaAPie = caminata.distancia; candidata.DistanciaAPie = caminata.distancia;
@@ -2200,7 +2208,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
=> $"{patron.CodigoLinea}|{patron.CodigoItinerario}|{patron.Sentido}"; => $"{patron.CodigoLinea}|{patron.CodigoItinerario}|{patron.Sentido}";
/// <summary> /// <summary>
/// Crea las dos alternativas de solo a pie. /// Crea las dos alternativas independientes de solo a pie.
/// </summary> /// </summary>
private async Task<List<AlternativaRuta>> CrearAlternativasSoloPieAsync( private async Task<List<AlternativaRuta>> CrearAlternativasSoloPieAsync(
double[] origenLatLon, double[] origenLatLon,
@@ -2209,7 +2217,11 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
{ {
var alternativas = new List<AlternativaRuta>(2); var alternativas = new List<AlternativaRuta>(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) if (osrm.linea is not null && osrm.distancia > 0)
{ {
var duracion = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(osrm.distancia, parametros); 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) if (valhalla.linea is not null && valhalla.distancia > 0)
{ {
var duracion = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(valhalla.distancia, parametros); var duracion = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(valhalla.distancia, parametros);
@@ -2365,7 +2382,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
var total = 0.0; var total = 0.0;
var cursor = instanteSalida; 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 alternativa.DistanciaPieInicioAprox = acceso.distancia > 0
? acceso.distancia ? acceso.distancia
: UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud); : UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
@@ -2423,11 +2445,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
distanciaUnion > UmbralCosturaVisualMetros && distanciaUnion > UmbralCosturaVisualMetros &&
distanciaUnion <= UmbralUnionPeatonalAutomaticaMetros) distanciaUnion <= UmbralUnionPeatonalAutomaticaMetros)
{ {
var transferencia = await WalkOsrmCached( var transferencia = await WalkCaminataCached(
puntoUltimaLlegada[0], puntoUltimaLlegada[0],
puntoUltimaLlegada[1], puntoUltimaLlegada[1],
puntoInicioTramo![0], puntoInicioTramo![0],
puntoInicioTramo[1]); puntoInicioTramo[1],
parametros);
var distanciaTransferencia = transferencia.distancia > 0 var distanciaTransferencia = transferencia.distancia > 0
? transferencia.distancia ? transferencia.distancia
@@ -2560,11 +2583,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
paradaFinBus); paradaFinBus);
alternativa.DestinoParada = paradaFinBus; alternativa.DestinoParada = paradaFinBus;
var salidaFinal = await WalkOsrmCached( var salidaFinal = await WalkCaminataCached(
paradaFinBus.Latitud, paradaFinBus.Latitud,
paradaFinBus.Longitud, paradaFinBus.Longitud,
destinoLatLon[0], destinoLatLon[0],
destinoLatLon[1]); destinoLatLon[1],
parametros);
alternativa.DistanciaPieFinAprox = salidaFinal.distancia > 0 alternativa.DistanciaPieFinAprox = salidaFinal.distancia > 0
? salidaFinal.distancia ? salidaFinal.distancia
: UtilidadesTransitoDbus.CalcularDistanciaMetros( : UtilidadesTransitoDbus.CalcularDistanciaMetros(
@@ -2776,12 +2800,14 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
if (string.IsNullOrWhiteSpace(paradaPrimera.Codigo) || string.IsNullOrWhiteSpace(paradaDirecta.Codigo)) if (string.IsNullOrWhiteSpace(paradaPrimera.Codigo) || string.IsNullOrWhiteSpace(paradaDirecta.Codigo))
return; 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 var distanciaPrimero = accesoPrimero.distancia > 0
? accesoPrimero.distancia ? accesoPrimero.distancia
: UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaPrimera.Latitud, paradaPrimera.Longitud); : 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 var distanciaDirecta = accesoDirecto.distancia > 0
? accesoDirecto.distancia ? accesoDirecto.distancia
: UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaDirecta.Latitud, paradaDirecta.Longitud); : UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaDirecta.Latitud, paradaDirecta.Longitud);
@@ -3026,7 +3052,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
if (parada is null) if (parada is null)
return 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 var distanciaWalk = acceso.distancia > 0
? acceso.distancia ? acceso.distancia
: UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], parada.Latitud, parada.Longitud); : UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], parada.Latitud, parada.Longitud);
@@ -3082,7 +3109,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
if (parada is null) if (parada is null)
return 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 var distanciaWalk = salidaFinal.distancia > 0
? salidaFinal.distancia ? salidaFinal.distancia
: UtilidadesTransitoDbus.CalcularDistanciaMetros(parada.Latitud, parada.Longitud, destinoLatLon[0], destinoLatLon[1]); : UtilidadesTransitoDbus.CalcularDistanciaMetros(parada.Latitud, parada.Longitud, destinoLatLon[0], destinoLatLon[1]);
@@ -3791,6 +3819,26 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
return true; return true;
} }
/// <summary>
/// Devuelve una caminata usando el proveedor elegido para esta planificación.
/// </summary>
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);
}
/// <summary> /// <summary>
/// Devuelve una caminata desde OSRM con cache. /// Devuelve una caminata desde OSRM con cache.
/// </summary> /// </summary>
@@ -3831,8 +3879,47 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
double latitudOrigen, double latitudOrigen,
double longitudOrigen, double longitudOrigen,
double latitudDestino, double latitudDestino,
double longitudDestino) double longitudDestino,
List<List<double[]>>? 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); var clave = BuildWalkKey("valhalla", latitudOrigen, longitudOrigen, latitudDestino, longitudDestino);
if (_cacheCaminatas.TryGetValue(clave, out var cacheado)) if (_cacheCaminatas.TryGetValue(clave, out var cacheado))
return 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<List<double[]>> 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;
}
/// <summary> /// <summary>
/// Construye una clave estable de cache para caminatas. /// Construye una clave estable de cache para caminatas.
/// </summary> /// </summary>

View File

@@ -1132,6 +1132,76 @@ html, body {
line-height: 1.25; 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 { .taxibus-notice {
margin: 0 0 .45rem; margin: 0 0 .45rem;
padding: .38rem .5rem; padding: .38rem .5rem;

View File

@@ -330,6 +330,7 @@ window.rt.drawRoute = function (id, coords, options) {
const attachRouteClick = function (line) { const attachRouteClick = function (line) {
line.on("click", function (e) { line.on("click", function (e) {
try { try {
if (window.rt._exclusionEditMode) return;
if (window.rt.handleInspectorLayerClick(e)) return; if (window.rt.handleInspectorLayerClick(e)) return;
if (e && e.originalEvent) { if (e && e.originalEvent) {
@@ -464,6 +465,7 @@ window.rt.drawRoute = function (id, coords, options) {
segmentLine.on("mouseover", updateTooltipAtCursor); segmentLine.on("mouseover", updateTooltipAtCursor);
segmentLine.on("mousemove", updateTooltipAtCursor); segmentLine.on("mousemove", updateTooltipAtCursor);
segmentLine.on("click", function (e) { segmentLine.on("click", function (e) {
if (window.rt._exclusionEditMode) return;
if (window.rt.handleInspectorLayerClick(e)) return; if (window.rt.handleInspectorLayerClick(e)) return;
window.rt.selectElevationSegment( window.rt.selectElevationSegment(
@@ -655,6 +657,93 @@ window.rt.setDotNetHelper = function (dotNetHelper) {
window.rt._dotNetHelper = 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 // ✅ Hacer la línea clicable y avisar a Blazor
line.on("click", function (e) { line.on("click", function (e) {
try { try {
if (window.rt._exclusionEditMode) return;
if (window.rt.handleInspectorLayerClick(e)) return; if (window.rt.handleInspectorLayerClick(e)) return;
if (e && e.originalEvent) { if (e && e.originalEvent) {
@@ -1213,9 +1303,10 @@ window.rt.createHighlightedStopMarker = function (stop, pane, options) {
}); });
if (stop.code && window.rt._dotNetHelper) { if (stop.code && window.rt._dotNetHelper) {
marker.on("click", function (event) { marker.on("click", function (event) {
try { try {
if (window.rt.handleInspectorLayerClick(event)) return; if (window.rt._exclusionEditMode) return;
if (window.rt.handleInspectorLayerClick(event)) return;
if (event && event.originalEvent) { if (event && event.originalEvent) {
if (event.originalEvent.preventDefault) event.originalEvent.preventDefault(); if (event.originalEvent.preventDefault) event.originalEvent.preventDefault();
@@ -1407,6 +1498,7 @@ window.rt.setEndpointMarker = function (kind, lat, lon, options) {
}; };
marker.on("click", function (event) { marker.on("click", function (event) {
if (window.rt._exclusionEditMode) return;
if (window.rt.handleInspectorLayerClick(event)) return; if (window.rt.handleInspectorLayerClick(event)) return;
stopEvent(event); stopEvent(event);
}); });
@@ -1493,6 +1585,7 @@ window.rt.setInspectorMarkers = function (lat, lon, places) {
className: "rt-inspector-tooltip" className: "rt-inspector-tooltip"
}); });
puntoZ.on("click", function (event) { puntoZ.on("click", function (event) {
if (window.rt._exclusionEditMode) return;
window.rt.handleInspectorLayerClick(event); window.rt.handleInspectorLayerClick(event);
}); });
puntoZ.addTo(window.rt._inspectorLayer); puntoZ.addTo(window.rt._inspectorLayer);
@@ -1568,6 +1661,7 @@ window.rt.setInspectorMarkers = function (lat, lon, places) {
className: "rt-inspector-tooltip" className: "rt-inspector-tooltip"
}); });
marker.on("click", function (event) { marker.on("click", function (event) {
if (window.rt._exclusionEditMode) return;
window.rt.handleInspectorLayerClick(event); window.rt.handleInspectorLayerClick(event);
}); });
marker.addTo(window.rt._inspectorLayer); marker.addTo(window.rt._inspectorLayer);