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:
@@ -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)
|
||||||
{
|
{
|
||||||
return !string.IsNullOrWhiteSpace(alternativa.Etiqueta)
|
if (alternativa.EsSoloAPie)
|
||||||
? alternativa.Etiqueta
|
return !string.IsNullOrWhiteSpace(alternativa.Etiqueta)
|
||||||
: $"Opcion {indice + 1}";
|
? alternativa.Etiqueta
|
||||||
|
: "Solo a pie";
|
||||||
|
|
||||||
|
return $"Opción {indice + 1}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 =>
|
||||||
|
|||||||
@@ -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,59 +558,148 @@ 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();
|
||||||
|
if (!EsPopupParadaVigente(paradaDetectada, token, cancellationToken))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_horariosOk)
|
||||||
{
|
{
|
||||||
await EnsureHorariosLoadedAsync();
|
try
|
||||||
|
|
||||||
if (_horariosOk)
|
|
||||||
{
|
{
|
||||||
try
|
var now = AhoraLocal();
|
||||||
{
|
var fecha = DateOnly.FromDateTime(now);
|
||||||
var now = AhoraLocal();
|
var hora = TimeOnly.FromDateTime(now);
|
||||||
var fecha = DateOnly.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,
|
hora,
|
||||||
hora,
|
take: null,
|
||||||
take: null,
|
includePast: true);
|
||||||
includePast: true);
|
|
||||||
|
|
||||||
_nextDeps = ConstruirListaHorariosConActual(deps, hora);
|
if (!EsPopupParadaVigente(paradaDetectada, token, cancellationToken))
|
||||||
_scrollHorariosPendiente = true;
|
return;
|
||||||
}
|
|
||||||
catch (Exception ex)
|
_nextDeps = ConstruirListaHorariosConActual(deps, hora);
|
||||||
{
|
_scrollHorariosPendiente = true;
|
||||||
_horariosOk = false;
|
}
|
||||||
_horariosError = "Error leyendo horarios: " + ex.Message;
|
catch (Exception ex)
|
||||||
}
|
{
|
||||||
|
if (!EsPopupParadaVigente(paradaDetectada, token, cancellationToken))
|
||||||
|
return;
|
||||||
|
_horariosOk = false;
|
||||||
|
_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)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
|||||||
@@ -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,29 +63,326 @@ public class ClienteValhalla : IClienteValhalla
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
HttpResponseMessage respuesta;
|
var respuesta = await EnviarSolicitudRutaAsync(cuerpo);
|
||||||
|
if (respuesta is null)
|
||||||
|
return (null, 0, 0);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage?> EnviarSolicitudRutaAsync(SolicitudValhalla cuerpo)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
respuesta = await _httpClient.PostAsJsonAsync($"{_baseUrl}/route", cuerpo);
|
return await _httpClient.PostAsJsonAsync($"{_baseUrl}/route", cuerpo);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return (null, 0, 0);
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!respuesta.IsSuccessStatusCode)
|
private async Task<HttpResponseMessage?> ReintentarSinRutaConRadiosAsync(
|
||||||
return (null, 0, 0);
|
SolicitudValhalla cuerpo,
|
||||||
|
HttpResponseMessage respuesta,
|
||||||
|
bool permitirReintento)
|
||||||
|
{
|
||||||
|
if (!permitirReintento)
|
||||||
|
return respuesta;
|
||||||
|
|
||||||
RespuestaRutaValhalla? data;
|
var radioActual = cuerpo.Ubicaciones
|
||||||
try
|
.Select(ubicacion => ubicacion.RadioBusquedaMetros ?? 0)
|
||||||
|
.DefaultIfEmpty(0)
|
||||||
|
.Max();
|
||||||
|
foreach (var radio in RadiosReintentoExclusionesMetros.Where(valor => valor > radioActual))
|
||||||
{
|
{
|
||||||
data = await respuesta.Content.ReadFromJsonAsync<RespuestaRutaValhalla>();
|
if (respuesta.IsSuccessStatusCode || !await EsErrorSinRutaAsync(respuesta))
|
||||||
}
|
return respuesta;
|
||||||
catch
|
|
||||||
{
|
respuesta.Dispose();
|
||||||
return (null, 0, 0);
|
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)
|
||||||
|
return (null, 0, 0);
|
||||||
|
|
||||||
|
RespuestaRutaValhalla? data;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
data = await respuesta.Content.ReadFromJsonAsync<RespuestaRutaValhalla>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user