v-20260914a

- punto z mas pequeño
- parada destacada
- restaurar valores por itinerairos y lineas
- intercambio en el buscar
- etiqyetas de soluciones elimiandas
- aumentado el nuemro de puntos de exclusion
- arreglado zoans de exlusion sin solucion
This commit is contained in:
2026-09-14 14:25:28 +02:00
parent 6ff14045e4
commit 9f312d2ca9
13 changed files with 924 additions and 76 deletions

View File

@@ -1799,9 +1799,12 @@ public partial class PlanificadorRutas : ComponentBase
/// </summary> /// </summary>
private string FormatearEtiquetaAlternativaTarjeta(AlternativaRuta alternativa, int indice) private string FormatearEtiquetaAlternativaTarjeta(AlternativaRuta alternativa, int indice)
{ {
if (alternativa.EsSoloAPie)
return !string.IsNullOrWhiteSpace(alternativa.Etiqueta) return !string.IsNullOrWhiteSpace(alternativa.Etiqueta)
? alternativa.Etiqueta ? alternativa.Etiqueta
: $"Opcion {indice + 1}"; : "Solo a pie";
return $"Opción {indice + 1}";
} }

View File

@@ -33,6 +33,7 @@ public partial class PlanificadorRutas
private SugerenciaBusquedaMapa? _ubicacionOrigenSeleccionada; private SugerenciaBusquedaMapa? _ubicacionOrigenSeleccionada;
private SugerenciaBusquedaMapa? _ubicacionDestinoSeleccionada; private SugerenciaBusquedaMapa? _ubicacionDestinoSeleccionada;
private int _versionSugerenciasBusqueda; private int _versionSugerenciasBusqueda;
private int _versionIntercambioBuscador;
private CancellationTokenSource? _ctsSugerenciasBusqueda; private CancellationTokenSource? _ctsSugerenciasBusqueda;
private IReadOnlyList<SugerenciaBusquedaMapa> SugerenciasOrigenVisibles => private IReadOnlyList<SugerenciaBusquedaMapa> SugerenciasOrigenVisibles =>

View File

@@ -535,6 +535,10 @@ public partial class PlanificadorRutas : ComponentBase
return; return;
var myToken = Interlocked.Increment(ref _paradaPopupToken); var myToken = Interlocked.Increment(ref _paradaPopupToken);
_ctsParadaPopup?.Cancel();
_ctsParadaPopup?.Dispose();
_ctsParadaPopup = new CancellationTokenSource();
var cancellationToken = _ctsParadaPopup.Token;
_paradaClicada = null; _paradaClicada = null;
_paradaElevIdee = null; _paradaElevIdee = null;
@@ -554,31 +558,91 @@ public partial class PlanificadorRutas : ComponentBase
_paradaClicada = paradaDetectada; _paradaClicada = paradaDetectada;
StateHasChanged(); StateHasChanged();
try
{
await JS.InvokeVoidAsync(
"rt.setSelectedStopMarker",
paradaDetectada.Latitud,
paradaDetectada.Longitud);
}
catch { }
_ = CompletarPopupParadaAsync(paradaDetectada, punto, clave, myToken, cancellationToken);
}
private async Task CompletarPopupParadaAsync(
Parada paradaDetectada,
(double Latitud, double Longitud) punto,
string clave,
int token,
CancellationToken cancellationToken)
{
var tareaElevacion = CompletarElevacionPopupParadaAsync(
paradaDetectada, punto, clave, token, cancellationToken);
var tareaHorarios = CompletarHorariosPopupParadaAsync(
paradaDetectada, token, cancellationToken);
try
{
await Task.WhenAll(tareaElevacion, tareaHorarios);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
catch
{
// Una carga auxiliar no debe bloquear ni cerrar el popup de la parada.
}
}
private async Task CompletarElevacionPopupParadaAsync(
Parada paradaDetectada,
(double Latitud, double Longitud) punto,
string clave,
int token,
CancellationToken cancellationToken)
{
try try
{ {
if (!_elevIdeeCache.ContainsKey(clave)) if (!_elevIdeeCache.ContainsKey(clave))
{ {
await ObtenerElevacionesIdeePorLoteAsync( await ObtenerElevacionesIdeePorLoteAsync(
new List<(double lat, double lon)> { punto }, new List<(double lat, double lon)> { punto },
CancellationToken.None); cancellationToken);
} }
if (!EsPopupParadaVigente(paradaDetectada, token, cancellationToken))
return;
_paradaElevIdee = _elevIdeeCache.TryGetValue(clave, out var idee) ? idee : null; _paradaElevIdee = _elevIdeeCache.TryGetValue(clave, out var idee) ? idee : null;
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return;
}
catch catch
{ {
if (!EsPopupParadaVigente(paradaDetectada, token, cancellationToken))
return;
_paradaElevIdee = null; _paradaElevIdee = null;
} }
if (myToken != _paradaPopupToken) return; if (EsPopupParadaVigente(paradaDetectada, token, cancellationToken))
StateHasChanged(); await InvokeAsync(StateHasChanged);
}
// ========================== private async Task CompletarHorariosPopupParadaAsync(
// HORARIOS (ZIP) -> usando tu AhoraLocal() (real o simulada) Parada paradaDetectada,
// ========================== int token,
if (_usarHorariosTeoricos) CancellationToken cancellationToken)
{ {
if (!_usarHorariosTeoricos)
return;
await EnsureHorariosLoadedAsync(); await EnsureHorariosLoadedAsync();
if (!EsPopupParadaVigente(paradaDetectada, token, cancellationToken))
return;
if (_horariosOk) if (_horariosOk)
{ {
@@ -588,7 +652,6 @@ public partial class PlanificadorRutas : ComponentBase
var fecha = DateOnly.FromDateTime(now); var fecha = DateOnly.FromDateTime(now);
var hora = TimeOnly.FromDateTime(now); var hora = TimeOnly.FromDateTime(now);
// tu parada usa Codigo (ej "34" o "P-34"). El servicio normaliza.
var deps = Horarios.GetNextDeparturesByCodigo( var deps = Horarios.GetNextDeparturesByCodigo(
paradaDetectada.Codigo, paradaDetectada.Codigo,
fecha, fecha,
@@ -596,17 +659,47 @@ public partial class PlanificadorRutas : ComponentBase
take: null, take: null,
includePast: true); includePast: true);
if (!EsPopupParadaVigente(paradaDetectada, token, cancellationToken))
return;
_nextDeps = ConstruirListaHorariosConActual(deps, hora); _nextDeps = ConstruirListaHorariosConActual(deps, hora);
_scrollHorariosPendiente = true; _scrollHorariosPendiente = true;
} }
catch (Exception ex) catch (Exception ex)
{ {
if (!EsPopupParadaVigente(paradaDetectada, token, cancellationToken))
return;
_horariosOk = false; _horariosOk = false;
_horariosError = "Error leyendo horarios: " + ex.Message; _horariosError = "Error leyendo horarios: " + ex.Message;
} }
} }
if (EsPopupParadaVigente(paradaDetectada, token, cancellationToken))
await InvokeAsync(StateHasChanged);
} }
private bool EsPopupParadaVigente(
Parada parada,
int token,
CancellationToken cancellationToken)
=> !cancellationToken.IsCancellationRequested &&
token == Volatile.Read(ref _paradaPopupToken) &&
string.Equals(_paradaClicada?.Codigo, parada.Codigo, StringComparison.OrdinalIgnoreCase);
private async Task CerrarPopupParadaAsync()
{
Interlocked.Increment(ref _paradaPopupToken);
_ctsParadaPopup?.Cancel();
_ctsParadaPopup?.Dispose();
_ctsParadaPopup = null;
_paradaClicada = null;
_paradaElevIdee = null;
_paradaElevLocal5 = null;
_paradaElevLocal2 = null;
_paradaElevLocal1 = null;
_nextDeps.Clear();
_scrollHorariosPendiente = false;
try { await JS.InvokeVoidAsync("rt.clearSelectedStopMarker"); } catch { }
StateHasChanged(); StateHasChanged();
} }
@@ -1294,6 +1387,7 @@ public partial class PlanificadorRutas : ComponentBase
private int _paradaPopupToken = 0; private int _paradaPopupToken = 0;
private CancellationTokenSource? _ctsParadaPopup;
/// <summary> /// <summary>
@@ -1400,7 +1494,7 @@ public partial class PlanificadorRutas : ComponentBase
private bool _elevPopupMinimizado = false; private bool _elevPopupMinimizado = false;
private const string _versionApp = "(v-20260910b)"; private const string _versionApp = "(v-20260914a)";
private double? _paradaElevIdee; private double? _paradaElevIdee;
@@ -1420,7 +1514,7 @@ public partial class PlanificadorRutas : ComponentBase
{ {
_itinerarioClicado = null; _itinerarioClicado = null;
_paradaClicada = null; await CerrarPopupParadaAsync();
await CerrarInspectorAsync(); await CerrarInspectorAsync();
if (_tramoElevDetalle is not null) if (_tramoElevDetalle is not null)

View File

@@ -1445,12 +1445,20 @@ public partial class PlanificadorRutas : ComponentBase
/// </summary> /// </summary>
private async Task IntercambiarOrigenDestino() private async Task IntercambiarOrigenDestino()
{ {
_ctsSugerenciasBusqueda?.Cancel();
_ctsSugerenciasBusqueda?.Dispose();
_ctsSugerenciasBusqueda = null;
_versionSugerenciasBusqueda++;
(_coordenadasOrigen, _coordenadasDestino) = (_coordenadasDestino, _coordenadasOrigen); (_coordenadasOrigen, _coordenadasDestino) = (_coordenadasDestino, _coordenadasOrigen);
(_textoBusquedaOrigen, _textoBusquedaDestino) = (_textoBusquedaDestino, _textoBusquedaOrigen); (_textoBusquedaOrigen, _textoBusquedaDestino) = (_textoBusquedaDestino, _textoBusquedaOrigen);
(_ubicacionOrigenSeleccionada, _ubicacionDestinoSeleccionada) = (_ubicacionDestinoSeleccionada, _ubicacionOrigenSeleccionada); (_ubicacionOrigenSeleccionada, _ubicacionDestinoSeleccionada) = (_ubicacionDestinoSeleccionada, _ubicacionOrigenSeleccionada);
_sugerenciasBusquedaOrigen.Clear(); _sugerenciasBusquedaOrigen.Clear();
_sugerenciasBusquedaDestino.Clear(); _sugerenciasBusquedaDestino.Clear();
_campoBusquedaActivo = null; _campoBusquedaActivo = null;
_versionIntercambioBuscador++;
StateHasChanged();
await Task.Yield();
await ClearRoutesJsAsync(); await ClearRoutesJsAsync();
await CerrarTodasLasPreviewsAsync(); await CerrarTodasLasPreviewsAsync();
@@ -1916,6 +1924,7 @@ public partial class PlanificadorRutas : ComponentBase
_panelRutasMovilContraido = _mostrarBuscadorMapa; _panelRutasMovilContraido = _mostrarBuscadorMapa;
await CerrarTodasLasPreviewsAsync(); await CerrarTodasLasPreviewsAsync();
await CerrarPopupParadaAsync();
_ctsCalculo?.Cancel(); _ctsCalculo?.Cancel();
_ctsCalculo = new CancellationTokenSource(); _ctsCalculo = new CancellationTokenSource();

View File

@@ -21,6 +21,8 @@ public partial class PlanificadorRutas
private bool _recalculandoZonas; private bool _recalculandoZonas;
private string? _errorZonas; private string? _errorZonas;
private const string ClaveZonasLocal = "rutasdbus.zonas.v1"; private const string ClaveZonasLocal = "rutasdbus.zonas.v1";
private const int MaximoExclusionesCaminata = 25;
private const int MaximoVerticesPorExclusion = 50;
private bool _zonasCargadas; private bool _zonasCargadas;
private bool _nuevaExclusionEsPunto; private bool _nuevaExclusionEsPunto;
@@ -35,9 +37,9 @@ public partial class PlanificadorRutas
if (!string.IsNullOrWhiteSpace(json)) if (!string.IsNullOrWhiteSpace(json))
{ {
var zonas = JsonSerializer.Deserialize<List<ZonaExclusionCaminata>>(json) ?? new(); var zonas = JsonSerializer.Deserialize<List<ZonaExclusionCaminata>>(json) ?? new();
foreach (var zona in zonas.Take(10)) foreach (var zona in zonas.Take(MaximoExclusionesCaminata))
{ {
if (zona is null || zona.Vertices is null || zona.Vertices.Count > 50 || if (zona is null || zona.Vertices is null || zona.Vertices.Count > MaximoVerticesPorExclusion ||
zona.Vertices.Any(p => p is null || p.Length != 2 || !CoordenadaExclusionValida(p[0], p[1]))) zona.Vertices.Any(p => p is null || p.Length != 2 || !CoordenadaExclusionValida(p[0], p[1])))
continue; continue;
if (zona.EsPunto && zona.Vertices.Count > 1) continue; if (zona.EsPunto && zona.Vertices.Count > 1) continue;
@@ -59,7 +61,7 @@ public partial class PlanificadorRutas
try try
{ {
var resultado = await JS.InvokeAsync<JsonElement>("rt.saveLocalValueResult", ClaveZonasLocal, var resultado = await JS.InvokeAsync<JsonElement>("rt.saveLocalValueResult", ClaveZonasLocal,
JsonSerializer.Serialize(_zonasExclusionCaminata)); JsonSerializer.Serialize(_zonasExclusionCaminata.Take(MaximoExclusionesCaminata)));
if (!resultado.GetProperty("ok").GetBoolean()) if (!resultado.GetProperty("ok").GetBoolean())
_errorZonas = "No se pudieron guardar las zonas en este navegador."; _errorZonas = "No se pudieron guardar las zonas en este navegador.";
} }
@@ -120,7 +122,8 @@ public partial class PlanificadorRutas
private async Task IniciarDibujoExclusionCaminataAsync() private async Task IniciarDibujoExclusionCaminataAsync()
{ {
if (!_zonasCargadas || _recalculandoZonas || _modoDibujoExclusionCaminata || _zonasExclusionCaminata.Count >= 10) return; if (!_zonasCargadas || _recalculandoZonas || _modoDibujoExclusionCaminata ||
_zonasExclusionCaminata.Count >= MaximoExclusionesCaminata) return;
var zona = new ZonaExclusionCaminata { EsPunto = _nuevaExclusionEsPunto }; var zona = new ZonaExclusionCaminata { EsPunto = _nuevaExclusionEsPunto };
_zonasExclusionCaminata.Add(zona); _zonasExclusionCaminata.Add(zona);
_zonaExclusionSeleccionada = zona.Id; _zonaExclusionSeleccionada = zona.Id;
@@ -211,7 +214,7 @@ public partial class PlanificadorRutas
{ {
if (!_modoDibujoExclusionCaminata || _recalculandoZonas || !CoordenadaExclusionValida(latitud, longitud)) return; if (!_modoDibujoExclusionCaminata || _recalculandoZonas || !CoordenadaExclusionValida(latitud, longitud)) return;
var zona = ZonaExclusionSeleccionada!; var zona = ZonaExclusionSeleccionada!;
if (zona.Vertices.Count >= 50) return; if (zona.Vertices.Count >= MaximoVerticesPorExclusion) return;
zona.Vertices.Add(new[] { latitud, longitud }); zona.Vertices.Add(new[] { latitud, longitud });
if (zona.EsPunto) zona.Cerrada = true; if (zona.EsPunto) zona.Cerrada = true;
await ActualizarEditorExclusionCaminataAsync(); await ActualizarEditorExclusionCaminataAsync();

View File

@@ -427,6 +427,10 @@ public partial class PlanificadorRutas : ComponentBase
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
_ctsParadaPopup?.Cancel();
_ctsParadaPopup?.Dispose();
_ctsSugerenciasBusqueda?.Cancel();
_ctsSugerenciasBusqueda?.Dispose();
_clockCts?.Cancel(); _clockCts?.Cancel();
_clockTimer?.Dispose(); _clockTimer?.Dispose();
_clockCts?.Dispose(); _clockCts?.Dispose();

View File

@@ -27,10 +27,6 @@ public partial class PlanificadorRutas
{ {
var actual = CrearConfiguracionLocal(); var actual = CrearConfiguracionLocal();
var predeterminada = CrearConfiguracionPredeterminadaHistorial(); var predeterminada = CrearConfiguracionPredeterminadaHistorial();
predeterminada.ActivarRangoLineas = actual.ActivarRangoLineas;
predeterminada.LineaMin = actual.LineaMin;
predeterminada.LineaMax = actual.LineaMax;
predeterminada.LineasPermitidas = actual.LineasPermitidas;
predeterminada.UsarHoraSimulada = actual.UsarHoraSimulada; predeterminada.UsarHoraSimulada = actual.UsarHoraSimulada;
predeterminada.FechaSimulada = actual.FechaSimulada; predeterminada.FechaSimulada = actual.FechaSimulada;
predeterminada.HoraSimulada = actual.HoraSimulada; predeterminada.HoraSimulada = actual.HoraSimulada;

View File

@@ -148,6 +148,7 @@
<div class="gm-input-group"> <div class="gm-input-group">
<label>Origen</label> <label>Origen</label>
<input type="text" <input type="text"
@key="@($"origen-{_versionIntercambioBuscador}")"
placeholder="Calle, direccion, lugar o parada" placeholder="Calle, direccion, lugar o parada"
autocomplete="off" autocomplete="off"
@bind="_textoBusquedaOrigen" @bind="_textoBusquedaOrigen"
@@ -184,6 +185,7 @@
<div class="gm-input-group"> <div class="gm-input-group">
<label>Destino</label> <label>Destino</label>
<input type="text" <input type="text"
@key="@($"destino-{_versionIntercambioBuscador}")"
placeholder="Calle, direccion, lugar o parada" placeholder="Calle, direccion, lugar o parada"
autocomplete="off" autocomplete="off"
@bind="_textoBusquedaDestino" @bind="_textoBusquedaDestino"
@@ -358,7 +360,7 @@
</div> </div>
<button class="popup-parada-cerrar" @onclick="() => { _paradaClicada = null; _paradaElevIdee = null; _paradaElevLocal5 = null; _paradaElevLocal2 = null; _paradaElevLocal1 = null; }"> <button class="popup-parada-cerrar" @onclick="CerrarPopupParadaAsync">
× ×
</button> </button>
</div> </div>
@@ -1530,7 +1532,7 @@
aria-pressed="@_nuevaExclusionEsPunto" disabled="@_recalculandoZonas" aria-pressed="@_nuevaExclusionEsPunto" disabled="@_recalculandoZonas"
@onclick="() => SeleccionarTipoExclusionCaminata(true)">Punto</button> @onclick="() => SeleccionarTipoExclusionCaminata(true)">Punto</button>
</div> </div>
<button type="button" class="btn-ghost" disabled="@(_recalculandoZonas || _zonasExclusionCaminata.Count >= 10)" <button type="button" class="btn-ghost" disabled="@(_recalculandoZonas || _zonasExclusionCaminata.Count >= MaximoExclusionesCaminata)"
@onclick="IniciarDibujoExclusionCaminataAsync">@(_nuevaExclusionEsPunto ? "Añadir punto" : "Añadir zona")</button> @onclick="IniciarDibujoExclusionCaminataAsync">@(_nuevaExclusionEsPunto ? "Añadir punto" : "Añadir zona")</button>
} }
<button type="button" class="btn-ghost" disabled="@(_recalculandoZonas || _zonasExclusionCaminata.Count == 0)" <button type="button" class="btn-ghost" disabled="@(_recalculandoZonas || _zonasExclusionCaminata.Count == 0)"

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@@ -11,6 +12,12 @@ namespace RutasDBUS.Servicios.Caminata;
public class ClienteValhalla : IClienteValhalla public class ClienteValhalla : IClienteValhalla
{ {
private static readonly int[] RadiosReintentoExclusionesMetros = [50, 100, 200];
private const int MaximoReintentosRefuerzoExclusion = 3;
private const int MaximoPuntosRefuerzoExclusion = 12;
private const int MaximoPuntosExclusionValhalla = 50;
private const double PasoComprobacionExclusionMetros = 2.0;
private const double ToleranciaBordeExclusionMetros = 1.5;
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly string _baseUrl; private readonly string _baseUrl;
@@ -56,16 +63,158 @@ public class ClienteValhalla : IClienteValhalla
} }
}; };
HttpResponseMessage respuesta; var respuesta = await EnviarSolicitudRutaAsync(cuerpo);
try if (respuesta is null)
{ return (null, 0, 0);
respuesta = await _httpClient.PostAsJsonAsync($"{_baseUrl}/route", cuerpo);
var tieneExclusiones = excludePolygons is { Count: > 0 } || excludeLocations is { Count: > 0 };
var extremosFueraDePoligonos = !PuntoDentroDePoligonosExclusion(
latitudDesde, longitudDesde, excludePolygons) &&
!PuntoDentroDePoligonosExclusion(latitudHasta, longitudHasta, excludePolygons);
respuesta = await ReintentarSinRutaConRadiosAsync(
cuerpo, respuesta, tieneExclusiones && extremosFueraDePoligonos);
if (respuesta is null)
return (null, 0, 0);
var resultado = await LeerResultadoRutaAsync(
respuesta, latitudDesde, longitudDesde, latitudHasta, longitudHasta);
if (excludePolygons is not { Count: > 0 })
return resultado;
if (resultado.Linea is not null &&
!RutaInvadePoligonosExclusion(resultado.Linea, excludePolygons))
return resultado;
return await ObtenerRutaSustituyendoPoligonosPorPuntosAsync(
cuerpo,
resultado,
excludePolygons,
latitudDesde,
longitudDesde,
latitudHasta,
longitudHasta,
extremosFueraDePoligonos);
} }
catch
private async Task<(double[,]? Linea, double DistanciaMetros, double DuracionSegundos)>
ObtenerRutaSustituyendoPoligonosPorPuntosAsync(
SolicitudValhalla cuerpo,
(double[,]? Linea, double DistanciaMetros, double DuracionSegundos) resultado,
List<List<double[]>> poligonos,
double latitudDesde,
double longitudDesde,
double latitudHasta,
double longitudHasta,
bool extremosFueraDePoligonos)
{ {
// Algunas versiones de v2 devuelven 442 con exclude_polygons aunque exista
// alternativa. En ese caso usamos el poligono para validar, pero vetamos
// directamente las aristas afectadas mediante exclude_locations.
cuerpo.PoligonosExclusion = null;
foreach (var ubicacion in cuerpo.Ubicaciones)
ubicacion.RadioBusquedaMetros = null;
if (resultado.Linea is null)
{
var respuestaBase = await EnviarSolicitudRutaAsync(cuerpo);
if (respuestaBase is null)
return (null, 0, 0);
respuestaBase = await ReintentarSinRutaConRadiosAsync(
cuerpo, respuestaBase, extremosFueraDePoligonos);
if (respuestaBase is null)
return (null, 0, 0);
resultado = await LeerResultadoRutaAsync(
respuestaBase, latitudDesde, longitudDesde, latitudHasta, longitudHasta);
if (resultado.Linea is null)
return (null, 0, 0);
if (!RutaInvadePoligonosExclusion(resultado.Linea, poligonos))
return resultado;
}
for (var intento = 0; intento < MaximoReintentosRefuerzoExclusion; intento++)
{
var puntosRefuerzo = ObtenerPuntosRefuerzoExclusion(resultado.Linea, poligonos);
if (puntosRefuerzo.Count == 0)
return resultado;
var cantidadAnterior = cuerpo.PuntosExclusion?.Count ?? 0;
cuerpo.PuntosExclusion = CombinarPuntosExclusion(cuerpo.PuntosExclusion, puntosRefuerzo);
if (cuerpo.PuntosExclusion.Count <= cantidadAnterior)
return (null, 0, 0);
var respuestaRefuerzo = await EnviarSolicitudRutaAsync(cuerpo);
if (respuestaRefuerzo is null)
return (null, 0, 0);
respuestaRefuerzo = await ReintentarSinRutaConRadiosAsync(
cuerpo, respuestaRefuerzo, extremosFueraDePoligonos);
if (respuestaRefuerzo is null)
return (null, 0, 0);
resultado = await LeerResultadoRutaAsync(
respuestaRefuerzo, latitudDesde, longitudDesde, latitudHasta, longitudHasta);
if (resultado.Linea is null)
return (null, 0, 0);
if (!RutaInvadePoligonosExclusion(resultado.Linea, poligonos))
return resultado;
}
return (null, 0, 0); return (null, 0, 0);
} }
private async Task<HttpResponseMessage?> EnviarSolicitudRutaAsync(SolicitudValhalla cuerpo)
{
try
{
return await _httpClient.PostAsJsonAsync($"{_baseUrl}/route", cuerpo);
}
catch
{
return null;
}
}
private async Task<HttpResponseMessage?> ReintentarSinRutaConRadiosAsync(
SolicitudValhalla cuerpo,
HttpResponseMessage respuesta,
bool permitirReintento)
{
if (!permitirReintento)
return respuesta;
var radioActual = cuerpo.Ubicaciones
.Select(ubicacion => ubicacion.RadioBusquedaMetros ?? 0)
.DefaultIfEmpty(0)
.Max();
foreach (var radio in RadiosReintentoExclusionesMetros.Where(valor => valor > radioActual))
{
if (respuesta.IsSuccessStatusCode || !await EsErrorSinRutaAsync(respuesta))
return respuesta;
respuesta.Dispose();
foreach (var ubicacion in cuerpo.Ubicaciones)
ubicacion.RadioBusquedaMetros = radio;
var respuestaReintento = await EnviarSolicitudRutaAsync(cuerpo);
if (respuestaReintento is null)
return null;
respuesta = respuestaReintento;
}
return respuesta;
}
private static async Task<(double[,]? Linea, double DistanciaMetros, double DuracionSegundos)> LeerResultadoRutaAsync(
HttpResponseMessage respuesta,
double latitudDesde,
double longitudDesde,
double latitudHasta,
double longitudHasta)
{
using (respuesta)
{
if (!respuesta.IsSuccessStatusCode) if (!respuesta.IsSuccessStatusCode)
return (null, 0, 0); return (null, 0, 0);
@@ -79,6 +228,161 @@ public class ClienteValhalla : IClienteValhalla
return (null, 0, 0); return (null, 0, 0);
} }
return ConstruirResultadoRuta(
data, latitudDesde, longitudDesde, latitudHasta, longitudHasta);
}
}
private static List<LocalizacionValhalla> CombinarPuntosExclusion(
List<LocalizacionValhalla>? existentes,
List<LocalizacionValhalla> refuerzos)
{
var resultado = existentes?.Take(MaximoPuntosExclusionValhalla).ToList() ?? new();
foreach (var punto in refuerzos)
{
if (resultado.Count >= MaximoPuntosExclusionValhalla)
break;
if (resultado.Any(actual => DistanciaMetros(
actual.Latitud, actual.Longitud, punto.Latitud, punto.Longitud) < 3.0))
continue;
resultado.Add(punto);
}
return resultado;
}
private static bool RutaInvadePoligonosExclusion(
double[,]? linea,
List<List<double[]>>? poligonos)
=> linea is not null && ObtenerPuntosRefuerzoExclusion(linea, poligonos).Count > 0;
private static List<LocalizacionValhalla> ObtenerPuntosRefuerzoExclusion(
double[,] linea,
List<List<double[]>>? poligonos)
{
if (linea.GetLength(0) < 2 || poligonos is not { Count: > 0 })
return new();
var candidatos = new List<LocalizacionValhalla>();
for (var i = 0; i < linea.GetLength(0) - 1; i++)
{
var latitudDesde = linea[i, 0];
var longitudDesde = linea[i, 1];
var latitudHasta = linea[i + 1, 0];
var longitudHasta = linea[i + 1, 1];
var distancia = DistanciaMetros(latitudDesde, longitudDesde, latitudHasta, longitudHasta);
var pasos = Math.Clamp((int)Math.Ceiling(distancia / PasoComprobacionExclusionMetros), 1, 500);
for (var paso = 0; paso <= pasos; paso++)
{
var proporcion = paso / (double)pasos;
var latitud = latitudDesde + (latitudHasta - latitudDesde) * proporcion;
var longitud = longitudDesde + (longitudHasta - longitudDesde) * proporcion;
if (!PuntoDentroOCercaDePoligonos(
latitud, longitud, poligonos, ToleranciaBordeExclusionMetros))
continue;
if (candidatos.Count == 0 || DistanciaMetros(
candidatos[^1].Latitud, candidatos[^1].Longitud, latitud, longitud) >= 5.0)
{
candidatos.Add(new LocalizacionValhalla { Latitud = latitud, Longitud = longitud });
}
}
}
if (candidatos.Count <= MaximoPuntosRefuerzoExclusion)
return candidatos;
var seleccionados = new List<LocalizacionValhalla>(MaximoPuntosRefuerzoExclusion);
for (var i = 0; i < MaximoPuntosRefuerzoExclusion; i++)
{
var indice = (int)Math.Round(i * (candidatos.Count - 1d) /
(MaximoPuntosRefuerzoExclusion - 1d));
seleccionados.Add(candidatos[indice]);
}
return seleccionados;
}
private static bool PuntoDentroOCercaDePoligonos(
double latitud,
double longitud,
List<List<double[]>> poligonos,
double toleranciaMetros)
{
if (PuntoDentroDePoligonosExclusion(latitud, longitud, poligonos))
return true;
foreach (var poligono in poligonos)
{
for (var i = 0; i < poligono.Count; i++)
{
var actual = poligono[i];
var siguiente = poligono[(i + 1) % poligono.Count];
if (actual.Length < 2 || siguiente.Length < 2)
continue;
if (DistanciaPuntoASegmentoMetros(
latitud, longitud,
actual[1], actual[0], siguiente[1], siguiente[0]) <= toleranciaMetros)
return true;
}
}
return false;
}
private static double DistanciaPuntoASegmentoMetros(
double latitud,
double longitud,
double latitudA,
double longitudA,
double latitudB,
double longitudB)
{
const double metrosPorGradoLatitud = 111_320.0;
var metrosPorGradoLongitud = metrosPorGradoLatitud *
Math.Cos(latitud * Math.PI / 180.0);
var ax = (longitudA - longitud) * metrosPorGradoLongitud;
var ay = (latitudA - latitud) * metrosPorGradoLatitud;
var bx = (longitudB - longitud) * metrosPorGradoLongitud;
var by = (latitudB - latitud) * metrosPorGradoLatitud;
var dx = bx - ax;
var dy = by - ay;
var longitudCuadrada = dx * dx + dy * dy;
if (longitudCuadrada <= double.Epsilon)
return Math.Sqrt(ax * ax + ay * ay);
var proporcion = Math.Clamp(-(ax * dx + ay * dy) / longitudCuadrada, 0.0, 1.0);
var x = ax + proporcion * dx;
var y = ay + proporcion * dy;
return Math.Sqrt(x * x + y * y);
}
private static double DistanciaMetros(
double latitudA,
double longitudA,
double latitudB,
double longitudB)
{
const double radioTierraMetros = 6_371_000.0;
var latitudARadianes = latitudA * Math.PI / 180.0;
var latitudBRadianes = latitudB * Math.PI / 180.0;
var diferenciaLatitud = (latitudB - latitudA) * Math.PI / 180.0;
var diferenciaLongitud = (longitudB - longitudA) * Math.PI / 180.0;
var senoLatitud = Math.Sin(diferenciaLatitud / 2.0);
var senoLongitud = Math.Sin(diferenciaLongitud / 2.0);
var a = senoLatitud * senoLatitud + Math.Cos(latitudARadianes) *
Math.Cos(latitudBRadianes) * senoLongitud * senoLongitud;
return radioTierraMetros * 2.0 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1.0 - a));
}
private static (double[,]? Linea, double DistanciaMetros, double DuracionSegundos) ConstruirResultadoRuta(
RespuestaRutaValhalla? data,
double latitudDesde,
double longitudDesde,
double latitudHasta,
double longitudHasta)
{
var viaje = data?.Viaje; var viaje = data?.Viaje;
if (viaje is null || viaje.Tramos is null || viaje.Tramos.Count == 0) if (viaje is null || viaje.Tramos is null || viaje.Tramos.Count == 0)
return (null, 0, 0); return (null, 0, 0);
@@ -116,6 +420,62 @@ public class ClienteValhalla : IClienteValhalla
duracionSegundos); duracionSegundos);
} }
private static async Task<bool> EsErrorSinRutaAsync(HttpResponseMessage respuesta)
{
try
{
var contenido = await respuesta.Content.ReadAsStringAsync();
using var documento = JsonDocument.Parse(contenido);
return documento.RootElement.TryGetProperty("error_code", out var codigo) &&
codigo.TryGetInt32(out var valor) &&
valor == 442;
}
catch
{
return false;
}
}
private static bool PuntoDentroDePoligonosExclusion(
double latitud,
double longitud,
List<List<double[]>>? poligonos)
{
if (poligonos is null)
return false;
foreach (var poligono in poligonos)
{
if (poligono.Count < 3)
continue;
var dentro = false;
for (int i = 0, j = poligono.Count - 1; i < poligono.Count; j = i++)
{
var actual = poligono[i];
var anterior = poligono[j];
if (actual.Length < 2 || anterior.Length < 2)
continue;
var latActual = actual[1];
var lonActual = actual[0];
var latAnterior = anterior[1];
var lonAnterior = anterior[0];
if ((latActual > latitud) != (latAnterior > latitud) &&
longitud < (lonAnterior - lonActual) * (latitud - latActual) /
(latAnterior - latActual) + lonActual)
{
dentro = !dentro;
}
}
if (dentro)
return true;
}
return false;
}
public async Task<InformacionSueloTramo?> ObtenerInformacionSueloAsync( public async Task<InformacionSueloTramo?> ObtenerInformacionSueloAsync(
double[,] linea, double[,] linea,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
@@ -411,6 +771,10 @@ public class ClienteValhalla : IClienteValhalla
[JsonPropertyName("lon")] [JsonPropertyName("lon")]
public double Longitud { get; set; } public double Longitud { get; set; }
[JsonPropertyName("radius")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? RadioBusquedaMetros { get; set; }
} }
private sealed class OpcionesIndicacionesValhalla private sealed class OpcionesIndicacionesValhalla

View File

@@ -2026,6 +2026,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
{ {
var radio = Math.Clamp(parametros.RadioParadasCandidatasMetros, 200, 3500); var radio = Math.Clamp(parametros.RadioParadasCandidatasMetros, 200, 3500);
var maximo = Math.Clamp(parametros.MaxParadasCandidatasPorLado, 3, 20); var maximo = Math.Clamp(parametros.MaxParadasCandidatasPorLado, 3, 20);
var validarExclusionesV2 = TieneExclusionesCaminataV2(parametros);
var pool = indiceTransito.QueryParadasEnRadio(latitud, longitud, radio); var pool = indiceTransito.QueryParadasEnRadio(latitud, longitud, radio);
var candidatas = new List<ParadaCandidata>(pool.Count); var candidatas = new List<ParadaCandidata>(pool.Count);
@@ -2042,6 +2043,10 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
var distancia = UtilidadesTransitoDbus.CalcularDistanciaMetros(latitud, longitud, parada.Latitud, parada.Longitud); var distancia = UtilidadesTransitoDbus.CalcularDistanciaMetros(latitud, longitud, parada.Latitud, parada.Longitud);
if (distancia > radio * 1.9) if (distancia > radio * 1.9)
continue; continue;
if (validarExclusionesV2 &&
EsDistanciaPieVisible(distancia) &&
PuntoDentroDePoligonoExclusion(parada.Latitud, parada.Longitud, parametros.PoligonosExclusionCaminata))
continue;
candidatas.Add(new ParadaCandidata candidatas.Add(new ParadaCandidata
{ {
@@ -2057,7 +2062,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
.Take(Math.Min(64, Math.Max(maximo + 20, maximo * 4))) .Take(Math.Min(64, Math.Max(maximo + 20, maximo * 4)))
.ToList(); .ToList();
if (!parametros.UsarOsrmParaRanking || parametros.ActivarRangoLineas || parametros.ActivarRangoItinerarios) if (!validarExclusionesV2 &&
(!parametros.UsarOsrmParaRanking || parametros.ActivarRangoLineas || parametros.ActivarRangoItinerarios))
return SeleccionarCandidatasConCoberturaPatrones(candidatas, maximo, indiceTransito, parametros); return SeleccionarCandidatasConCoberturaPatrones(candidatas, maximo, indiceTransito, parametros);
var semaforo = new SemaphoreSlim(4, 4); var semaforo = new SemaphoreSlim(4, 4);
@@ -2077,9 +2083,19 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
candidata.DistanciaAPie = caminata.distancia; candidata.DistanciaAPie = caminata.distancia;
candidata.TiempoAPie = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(caminata.distancia, parametros); candidata.TiempoAPie = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(caminata.distancia, parametros);
} }
else if (validarExclusionesV2 && EsDistanciaPieVisible(candidata.DistanciaEuclidea))
{
candidata.DistanciaAPie = double.PositiveInfinity;
candidata.TiempoAPie = double.PositiveInfinity;
}
} }
catch catch
{ {
if (validarExclusionesV2 && EsDistanciaPieVisible(candidata.DistanciaEuclidea))
{
candidata.DistanciaAPie = double.PositiveInfinity;
candidata.TiempoAPie = double.PositiveInfinity;
}
} }
finally finally
{ {
@@ -2089,6 +2105,9 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
await Task.WhenAll(tareas); await Task.WhenAll(tareas);
if (validarExclusionesV2)
candidatas.RemoveAll(candidata => !double.IsFinite(candidata.DistanciaAPie));
var candidatasOrdenadas = candidatas var candidatasOrdenadas = candidatas
.OrderBy(x => x.TiempoAPie) .OrderBy(x => x.TiempoAPie)
.ThenBy(x => x.DistanciaEuclidea) .ThenBy(x => x.DistanciaEuclidea)
@@ -2388,9 +2407,13 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
paradaInicioBus.Latitud, paradaInicioBus.Latitud,
paradaInicioBus.Longitud, paradaInicioBus.Longitud,
parametros); parametros);
var distanciaAccesoDirecta = UtilidadesTransitoDbus.CalcularDistanciaMetros(
origenLatLon[0], origenLatLon[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
if (CaminataNoResueltaConExclusiones(parametros, distanciaAccesoDirecta, acceso.linea, acceso.distancia))
return double.PositiveInfinity;
alternativa.DistanciaPieInicioAprox = acceso.distancia > 0 alternativa.DistanciaPieInicioAprox = acceso.distancia > 0
? acceso.distancia ? acceso.distancia
: UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud); : distanciaAccesoDirecta;
AgregarLineaPieCalculoElevacion( AgregarLineaPieCalculoElevacion(
alternativa, alternativa,
alternativa.DistanciaPieInicioAprox, alternativa.DistanciaPieInicioAprox,
@@ -2452,6 +2475,10 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
puntoInicioTramo[1], puntoInicioTramo[1],
parametros); parametros);
if (CaminataNoResueltaConExclusiones(
parametros, distanciaUnion, transferencia.linea, transferencia.distancia))
return double.PositiveInfinity;
var distanciaTransferencia = transferencia.distancia > 0 var distanciaTransferencia = transferencia.distancia > 0
? transferencia.distancia ? transferencia.distancia
: distanciaUnion; : distanciaUnion;
@@ -2589,13 +2616,13 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
destinoLatLon[0], destinoLatLon[0],
destinoLatLon[1], destinoLatLon[1],
parametros); parametros);
var distanciaSalidaDirecta = UtilidadesTransitoDbus.CalcularDistanciaMetros(
paradaFinBus.Latitud, paradaFinBus.Longitud, destinoLatLon[0], destinoLatLon[1]);
if (CaminataNoResueltaConExclusiones(parametros, distanciaSalidaDirecta, salidaFinal.linea, salidaFinal.distancia))
return double.PositiveInfinity;
alternativa.DistanciaPieFinAprox = salidaFinal.distancia > 0 alternativa.DistanciaPieFinAprox = salidaFinal.distancia > 0
? salidaFinal.distancia ? salidaFinal.distancia
: UtilidadesTransitoDbus.CalcularDistanciaMetros( : distanciaSalidaDirecta;
paradaFinBus.Latitud,
paradaFinBus.Longitud,
destinoLatLon[0],
destinoLatLon[1]);
AgregarLineaPieCalculoElevacion( AgregarLineaPieCalculoElevacion(
alternativa, alternativa,
alternativa.DistanciaPieFinAprox, alternativa.DistanciaPieFinAprox,
@@ -3054,9 +3081,13 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
var acceso = await WalkCaminataCached( var acceso = await WalkCaminataCached(
origenLatLon[0], origenLatLon[1], parada.Latitud, parada.Longitud, parametros); origenLatLon[0], origenLatLon[1], parada.Latitud, parada.Longitud, parametros);
var distanciaDirecta = UtilidadesTransitoDbus.CalcularDistanciaMetros(
origenLatLon[0], origenLatLon[1], parada.Latitud, parada.Longitud);
if (CaminataNoResueltaConExclusiones(parametros, distanciaDirecta, acceso.linea, acceso.distancia))
return null;
var distanciaWalk = acceso.distancia > 0 var distanciaWalk = acceso.distancia > 0
? acceso.distancia ? acceso.distancia
: UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], parada.Latitud, parada.Longitud); : distanciaDirecta;
var duracionWalk = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaWalk, parametros); var duracionWalk = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaWalk, parametros);
double costeTotal; double costeTotal;
@@ -3111,9 +3142,13 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
var salidaFinal = await WalkCaminataCached( var salidaFinal = await WalkCaminataCached(
parada.Latitud, parada.Longitud, destinoLatLon[0], destinoLatLon[1], parametros); parada.Latitud, parada.Longitud, destinoLatLon[0], destinoLatLon[1], parametros);
var distanciaDirecta = UtilidadesTransitoDbus.CalcularDistanciaMetros(
parada.Latitud, parada.Longitud, destinoLatLon[0], destinoLatLon[1]);
if (CaminataNoResueltaConExclusiones(parametros, distanciaDirecta, salidaFinal.linea, salidaFinal.distancia))
return null;
var distanciaWalk = salidaFinal.distancia > 0 var distanciaWalk = salidaFinal.distancia > 0
? salidaFinal.distancia ? salidaFinal.distancia
: UtilidadesTransitoDbus.CalcularDistanciaMetros(parada.Latitud, parada.Longitud, destinoLatLon[0], destinoLatLon[1]); : distanciaDirecta;
var duracionWalk = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaWalk, parametros); var duracionWalk = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaWalk, parametros);
double costeTotal; double costeTotal;
@@ -3822,6 +3857,60 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
/// <summary> /// <summary>
/// Devuelve una caminata usando el proveedor elegido para esta planificación. /// Devuelve una caminata usando el proveedor elegido para esta planificación.
/// </summary> /// </summary>
private static bool TieneExclusionesCaminataV2(ParametrosPlanificadorRuta parametros)
=> parametros.UsarValhallaParaCaminatas &&
(parametros.PoligonosExclusionCaminata is { Count: > 0 } ||
parametros.PuntosExclusionCaminata is { Count: > 0 });
private static bool CaminataNoResueltaConExclusiones(
ParametrosPlanificadorRuta parametros,
double distanciaDirecta,
double[,]? linea,
double distanciaRuta)
=> TieneExclusionesCaminataV2(parametros) &&
EsDistanciaPieVisible(distanciaDirecta) &&
(linea is null || linea.GetLength(0) < 2 || !double.IsFinite(distanciaRuta) || distanciaRuta <= 0);
private static bool PuntoDentroDePoligonoExclusion(
double latitud,
double longitud,
List<List<double[]>>? poligonos)
{
if (poligonos is null)
return false;
foreach (var poligono in poligonos)
{
if (poligono.Count < 3)
continue;
var dentro = false;
for (int i = 0, j = poligono.Count - 1; i < poligono.Count; j = i++)
{
var actual = poligono[i];
var anterior = poligono[j];
if (actual.Length < 2 || anterior.Length < 2)
continue;
var latActual = actual[1];
var lonActual = actual[0];
var latAnterior = anterior[1];
var lonAnterior = anterior[0];
if ((latActual > latitud) != (latAnterior > latitud) &&
longitud < (lonAnterior - lonActual) * (latitud - latActual) /
(latAnterior - latActual) + lonActual)
{
dentro = !dentro;
}
}
if (dentro)
return true;
}
return false;
}
private Task<(double[,]? linea, double distancia, double duracion)> WalkCaminataCached( private Task<(double[,]? linea, double distancia, double duracion)> WalkCaminataCached(
double latitudOrigen, double latitudOrigen,
double longitudOrigen, double longitudOrigen,

View File

@@ -2268,6 +2268,34 @@ html, body {
pointer-events: auto; pointer-events: auto;
} }
.rt-selected-stop-icon {
border: 0;
background: transparent;
pointer-events: none;
}
.rt-selected-stop-marker {
position: absolute;
inset: 4px;
box-sizing: border-box;
border: 3px solid #f8fafc;
border-radius: 50%;
background: rgba(250, 204, 21, .28);
box-shadow: 0 0 0 2px #f59e0b, 0 0 10px rgba(245, 158, 11, .9);
animation: rt-selected-stop-pulse 1.15s ease-in-out infinite;
}
@keyframes rt-selected-stop-pulse {
0%, 100% {
transform: scale(.82);
opacity: .72;
}
50% {
transform: scale(1.18);
opacity: 1;
}
}
.rt-highlighted-stop-marker { .rt-highlighted-stop-marker {
position: relative; position: relative;
display: block; display: block;

View File

@@ -1651,9 +1651,9 @@ window.rt.setInspectorMarkers = function (lat, lon, places) {
const puntoZ = window.L.circleMarker([zLat, zLon], { const puntoZ = window.L.circleMarker([zLat, zLon], {
pane: "rtInspectorPane", pane: "rtInspectorPane",
radius: 12, radius: 8,
color: "#713f12", color: "#713f12",
weight: 3, weight: 2,
opacity: 1, opacity: 1,
fillColor: "#fde047", fillColor: "#fde047",
fillOpacity: 0.42, fillOpacity: 0.42,
@@ -1662,7 +1662,7 @@ window.rt.setInspectorMarkers = function (lat, lon, places) {
puntoZ.bindTooltip("Punto Z", { puntoZ.bindTooltip("Punto Z", {
pane: "rtInspectorTooltipPane", pane: "rtInspectorTooltipPane",
direction: "top", direction: "top",
offset: [0, -16], offset: [0, -12],
opacity: 0.95, opacity: 0.95,
className: "rt-inspector-tooltip" className: "rt-inspector-tooltip"
}); });
@@ -1756,6 +1756,45 @@ window.rt.clearInspectorMarkers = function () {
} }
}; };
window.rt.setSelectedStopMarker = function (lat, lon) {
const map = window.__rtLeafletMap;
if (!map || !window.L) return;
if (!map.getPane("rtSelectedStopPane")) {
const pane = map.createPane("rtSelectedStopPane");
pane.style.zIndex = 785;
pane.style.pointerEvents = "none";
}
window.rt.clearSelectedStopMarker();
const stopLat = Number(lat);
const stopLon = Number(lon);
if (!Number.isFinite(stopLat) || !Number.isFinite(stopLon)) return;
const icon = window.L.divIcon({
className: "rt-selected-stop-icon",
html: '<span class="rt-selected-stop-marker"></span>',
iconSize: [28, 28],
iconAnchor: [14, 14]
});
window.rt._selectedStopMarker = window.L.marker([stopLat, stopLon], {
pane: "rtSelectedStopPane",
icon,
interactive: false,
keyboard: false
}).addTo(map);
};
window.rt.clearSelectedStopMarker = function () {
const map = window.__rtLeafletMap;
if (map && window.rt._selectedStopMarker) {
map.removeLayer(window.rt._selectedStopMarker);
}
window.rt._selectedStopMarker = null;
};
window.rt.setInspectorWalk = function (points, visible) { window.rt.setInspectorWalk = function (points, visible) {
window.rt._inspectorWalkLayer?.remove(); window.rt._inspectorWalkLayer?.remove();
window.rt._inspectorWalkLayer = null; window.rt._inspectorWalkLayer = null;

View File

@@ -54,6 +54,31 @@ async Task<(double[,]? linea, double distancia, double duracion)> Walk(List<List
List<double[]> Ring(double lon, double lat, double size) => List<double[]> Ring(double lon, double lat, double size) =>
new() { new[] { lon, lat }, new[] { lon + size, lat }, new[] { lon + size, lat + size }, new() { new[] { lon, lat }, new[] { lon + size, lat }, new[] { lon + size, lat + size },
new[] { lon, lat + size }, new[] { lon, lat } }; new[] { lon, lat + size }, new[] { lon, lat } };
string Polyline6(params (double lat, double lon)[] puntos)
{
var texto = new System.Text.StringBuilder();
var latitudAnterior = 0;
var longitudAnterior = 0;
foreach (var punto in puntos)
{
Codificar((int)Math.Round(punto.lat * 1_000_000) - latitudAnterior, texto);
Codificar((int)Math.Round(punto.lon * 1_000_000) - longitudAnterior, texto);
latitudAnterior = (int)Math.Round(punto.lat * 1_000_000);
longitudAnterior = (int)Math.Round(punto.lon * 1_000_000);
}
return texto.ToString();
static void Codificar(int diferencia, System.Text.StringBuilder texto)
{
var valor = diferencia < 0 ? ~(diferencia << 1) : diferencia << 1;
while (valor >= 0x20)
{
texto.Append((char)((0x20 | (valor & 0x1f)) + 63));
valor >>= 5;
}
texto.Append((char)(valor + 63));
}
}
await Walk(); await Walk();
var far = new List<List<double[]>> { Ring(-2.3, 43.0, .01) }; var far = new List<List<double[]>> { Ring(-2.3, 43.0, .01) };
@@ -79,8 +104,108 @@ var puntos = new List<double[]> { new[] { 43.314, -1.984 }, new[] { 43.315, -1.9
await (Task<(double[,]?, double, double)>)walk.Invoke(planner, await (Task<(double[,]?, double, double)>)walk.Invoke(planner,
new object?[] { 43.31, -1.99, 43.32, -1.98, near, puntos })!; 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"); Check(fake.Zones == 2 && fake.Points == 2, "Se envian juntos todos los poligonos y puntos activos");
var retryHandler = new RetryValhallaHandler();
var retryConfig = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["Caminata:Valhalla:BaseUrl"] = "http://v2.test"
}).Build();
var retryClient = new ClienteValhalla(new HttpClient(retryHandler), retryConfig);
await retryClient.ObtenerRutaAsync(
43.316344, -1.983193, 43.315282, -1.985457,
excludePolygons: new() { Ring(-1.9848, 43.3158, .0001) });
Check(retryHandler.Bodies.Count == 8,
"V2 amplia progresivamente la busqueda y prueba la alternativa puntual cuando recibe 442");
using (var primerIntento = JsonDocument.Parse(retryHandler.Bodies[0]))
using (var segundoIntento = JsonDocument.Parse(retryHandler.Bodies[1]))
using (var tercerIntento = JsonDocument.Parse(retryHandler.Bodies[2]))
using (var cuartoIntento = JsonDocument.Parse(retryHandler.Bodies[3]))
{
var primeraUbicacionInicial = primerIntento.RootElement.GetProperty("locations")[0];
var primeraUbicacionReintento = segundoIntento.RootElement.GetProperty("locations")[0];
var primeraUbicacionSegundoReintento = tercerIntento.RootElement.GetProperty("locations")[0];
var primeraUbicacionTercerReintento = cuartoIntento.RootElement.GetProperty("locations")[0];
Check(!primeraUbicacionInicial.TryGetProperty("radius", out _) &&
primeraUbicacionReintento.GetProperty("radius").GetInt32() == 50 &&
primeraUbicacionSegundoReintento.GetProperty("radius").GetInt32() == 100 &&
primeraUbicacionTercerReintento.GetProperty("radius").GetInt32() == 200,
"Los reintentos buscan una arista alternativa hasta un radio controlado de 200 metros");
}
var polygonFallbackHandler = new PolygonNoPathThenPointSuccessHandler(
Polyline6((43.31, -1.99), (43.32, -1.98)),
Polyline6((43.31, -1.99), (43.31, -1.98), (43.32, -1.98)));
var polygonFallbackClient = new ClienteValhalla(new HttpClient(polygonFallbackHandler), retryConfig);
var rutaConPoligonoConvertido = await polygonFallbackClient.ObtenerRutaAsync(
43.31, -1.99, 43.32, -1.98,
excludePolygons: new() { Ring(-1.9855, 43.3145, .001) });
Check(rutaConPoligonoConvertido.Linea is not null && polygonFallbackHandler.Bodies.Count == 6,
"Un poligono que devuelve 442 se sustituye por puntos y encuentra una alternativa");
using (var peticionConPuntos = JsonDocument.Parse(polygonFallbackHandler.Bodies[^1]))
{
Check(peticionConPuntos.RootElement.GetProperty("exclude_polygons").ValueKind == JsonValueKind.Null &&
peticionConPuntos.RootElement.GetProperty("exclude_locations").GetArrayLength() > 0,
"El fallback conserva la zona como validacion y envia a v2 las aristas afectadas");
}
var poligonoValidacion = Ring(-1.9855, 43.3145, .001);
var validationHandler = new PolygonValidationValhallaHandler(
Polyline6((43.31, -1.99), (43.32, -1.98)),
Polyline6((43.31, -1.99), (43.31, -1.98), (43.32, -1.98)));
var validationClient = new ClienteValhalla(new HttpClient(validationHandler), retryConfig);
var rutaValidada = await validationClient.ObtenerRutaAsync(
43.31, -1.99, 43.32, -1.98, excludePolygons: new() { poligonoValidacion });
Check(validationHandler.Bodies.Count == 2 && rutaValidada.Linea is not null,
"V2 recalcula una ruta que atraviesa una zona activa");
using (var peticionReforzada = JsonDocument.Parse(validationHandler.Bodies[1]))
{
Check(peticionReforzada.RootElement.GetProperty("exclude_locations").GetArrayLength() > 0,
"La ruta conflictiva refuerza la exclusion sobre las aristas atravesadas");
}
var parametrosConExclusion = new ParametrosPlanificadorRuta
{
UsarValhallaParaCaminatas = true,
PoligonosExclusionCaminata = new() { Ring(-1.985, 43.315, .001) }
};
var caminataNoResuelta = typeof(PlanificadorRutasServicio).GetMethod(
"CaminataNoResueltaConExclusiones", BindingFlags.Static | BindingFlags.NonPublic)!;
bool RechazarCaminata(ParametrosPlanificadorRuta parametros, double directa, double[,]? linea, double distancia) =>
(bool)caminataNoResuelta.Invoke(null, new object?[] { parametros, directa, linea, distancia })!;
Check(RechazarCaminata(parametrosConExclusion, 100, null, 0),
"Una caminata V2 sin ruta se descarta cuando hay exclusiones");
Check(!RechazarCaminata(new ParametrosPlanificadorRuta { UsarValhallaParaCaminatas = true }, 100, null, 0),
"Sin exclusiones se conserva el fallback historico");
Check(!RechazarCaminata(parametrosConExclusion, 100, new[,] { { 43.31, -1.99 }, { 43.32, -1.98 } }, 120),
"Una caminata V2 valida se conserva con exclusiones");
var puntoDentro = typeof(PlanificadorRutasServicio).GetMethod(
"PuntoDentroDePoligonoExclusion", BindingFlags.Static | BindingFlags.NonPublic)!;
Check((bool)puntoDentro.Invoke(null, new object?[] { 43.3155, -1.9845, parametrosConExclusion.PoligonosExclusionCaminata })!,
"Las paradas interiores a una zona se detectan antes de calcular");
var page = new PlanificadorRutas(); var page = new PlanificadorRutas();
var flags = BindingFlags.Instance | BindingFlags.NonPublic; var flags = BindingFlags.Instance | BindingFlags.NonPublic;
var formatearEtiqueta = typeof(PlanificadorRutas).GetMethod("FormatearEtiquetaAlternativaTarjeta", flags)!;
Check((string)formatearEtiqueta.Invoke(page, new object[]
{ new AlternativaRuta { Etiqueta = "Equilibrada" }, 0 })! == "Opción 1",
"Las alternativas de bus se muestran numeradas sin etiquetas de criterio");
Check((string)formatearEtiqueta.Invoke(page, new object[]
{ new AlternativaRuta { EsSoloAPie = true, Etiqueta = "Solo a pie (v2)" }, 3 })! == "Solo a pie (v2)",
"Las dos alternativas solo a pie conservan su identificacion");
var maximoExclusiones = typeof(PlanificadorRutas).GetField(
"MaximoExclusionesCaminata", BindingFlags.Static | BindingFlags.NonPublic)!;
Check((int)maximoExclusiones.GetRawConstantValue()! == 25,
"Las exclusiones quedan limitadas a 25 elementos");
typeof(PlanificadorRutas).GetField("_configuracionFabrica", flags)!.SetValue(page, defaults.Clonar());
typeof(PlanificadorRutas).GetField("_activarRangoLineas", flags)!.SetValue(page, true);
typeof(PlanificadorRutas).GetField("_lineaMin", flags)!.SetValue(page, 40);
typeof(PlanificadorRutas).GetField("_lineaMax", flags)!.SetValue(page, 40);
typeof(PlanificadorRutas).GetField("_activarRangoItinerarios", flags)!.SetValue(page, false);
typeof(PlanificadorRutas).GetField("_itinerarioMin", flags)!.SetValue(page, 7);
typeof(PlanificadorRutas).GetField("_itinerarioMax", flags)!.SetValue(page, 7);
typeof(PlanificadorRutas).GetMethod("AplicarValoresFabricaConservandoSeleccion", flags)!.Invoke(page, null);
Check(!(bool)typeof(PlanificadorRutas).GetField("_activarRangoLineas", flags)!.GetValue(page)! &&
(int)typeof(PlanificadorRutas).GetField("_lineaMin", flags)!.GetValue(page)! == defaults.LineaMin &&
(int)typeof(PlanificadorRutas).GetField("_lineaMax", flags)!.GetValue(page)! == defaults.LineaMax &&
(bool)typeof(PlanificadorRutas).GetField("_activarRangoItinerarios", flags)!.GetValue(page)! &&
(int)typeof(PlanificadorRutas).GetField("_itinerarioMin", flags)!.GetValue(page)! == defaults.ItinerarioMin &&
(int)typeof(PlanificadorRutas).GetField("_itinerarioMax", flags)!.GetValue(page)! == defaults.ItinerarioMax,
"Restaurar aplica tambien los valores de lineas e itinerarios");
var options = (List<AlternativaRuta>)typeof(PlanificadorRutas).GetField("_opcionesRuta", flags)!.GetValue(page)!; var options = (List<AlternativaRuta>)typeof(PlanificadorRutas).GetField("_opcionesRuta", flags)!.GetValue(page)!;
var selected = typeof(PlanificadorRutas).GetField("_indiceAlternativaSeleccionada", flags)!; var selected = typeof(PlanificadorRutas).GetField("_indiceAlternativaSeleccionada", flags)!;
var remember = typeof(PlanificadorRutas).GetMethod("ObtenerProveedorSoloAPieParaRecalculo", flags)!; var remember = typeof(PlanificadorRutas).GetMethod("ObtenerProveedorSoloAPieParaRecalculo", flags)!;
@@ -120,3 +245,94 @@ sealed class FakeValhalla : IClienteValhalla
(new[,] { { latitudDesde, longitudDesde }, { latitudHasta, longitudHasta } }, 100, 100)); (new[,] { { latitudDesde, longitudDesde }, { latitudHasta, longitudHasta } }, 100, 100));
} }
} }
sealed class RetryValhallaHandler : HttpMessageHandler
{
public List<string> Bodies { get; } = new();
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
Bodies.Add(await request.Content!.ReadAsStringAsync(cancellationToken));
return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
{
Content = new StringContent(
"{\"error_code\":442,\"error\":\"No path could be found for input\"}",
System.Text.Encoding.UTF8,
"application/json")
};
}
}
sealed class PolygonValidationValhallaHandler(string primeraForma, string segundaForma) : HttpMessageHandler
{
public List<string> Bodies { get; } = new();
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
Bodies.Add(await request.Content!.ReadAsStringAsync(cancellationToken));
var forma = Bodies.Count == 1 ? primeraForma : segundaForma;
var contenido = JsonSerializer.Serialize(new
{
trip = new
{
units = "kilometers",
summary = new { length = 1.0, time = 600.0 },
legs = new[] { new { shape = forma } }
}
});
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(contenido, System.Text.Encoding.UTF8, "application/json")
};
}
}
sealed class PolygonNoPathThenPointSuccessHandler(string formaConflictiva, string formaAlternativa)
: HttpMessageHandler
{
public List<string> Bodies { get; } = new();
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var cuerpo = await request.Content!.ReadAsStringAsync(cancellationToken);
Bodies.Add(cuerpo);
using var documento = JsonDocument.Parse(cuerpo);
var raiz = documento.RootElement;
var tienePoligonos = raiz.TryGetProperty("exclude_polygons", out var poligonos) &&
poligonos.ValueKind == JsonValueKind.Array &&
poligonos.GetArrayLength() > 0;
if (tienePoligonos)
{
return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
{
Content = new StringContent(
"{\"error_code\":442,\"error\":\"No path could be found for input\"}",
System.Text.Encoding.UTF8,
"application/json")
};
}
var tienePuntos = raiz.TryGetProperty("exclude_locations", out var puntos) &&
puntos.ValueKind == JsonValueKind.Array &&
puntos.GetArrayLength() > 0;
var contenido = JsonSerializer.Serialize(new
{
trip = new
{
units = "kilometers",
summary = new { length = 1.0, time = 600.0 },
legs = new[] { new { shape = tienePuntos ? formaAlternativa : formaConflictiva } }
}
});
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(contenido, System.Text.Encoding.UTF8, "application/json")
};
}
}