añadidos atajos ctrl + alt + z para abrir zonas de exclusion añadido superficio de terreno en el punto z
2077 lines
78 KiB
C#
2077 lines
78 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using LeafletForBlazor;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.JSInterop;
|
|
using RutasDBUS.Modelos.Geocodificacion;
|
|
using RutasDBUS.Modelos.Planificacion;
|
|
using RutasDBUS.Modelos.RedBus;
|
|
using RutasDBUS.Servicios;
|
|
using RutasDBUS.Servicios.Caminata;
|
|
using RutasDBUS.Servicios.Elevacion;
|
|
using RutasDBUS.Servicios.Horarios;
|
|
using RutasDBUS.Servicios.Planificacion;
|
|
using RutasDBUS.Servicios.RedBus;
|
|
|
|
namespace RutasDBUS.Components.Pages;
|
|
|
|
public partial class PlanificadorRutas : ComponentBase
|
|
{
|
|
|
|
private IReadOnlyList<int> ObtenerLineasPermitidasSeleccionadas()
|
|
=> _lineasPermitidas
|
|
.Where(linea => linea > 0)
|
|
.Distinct()
|
|
.OrderBy(linea => linea)
|
|
.ToList();
|
|
|
|
private IReadOnlyList<int> LineasFiltroDisponibles()
|
|
{
|
|
var lineas = _infoVariantes
|
|
.Select(CodigoLineaAsInt)
|
|
.Concat(RedBusServicio.Lineas.Select(CodigoLineaAsInt))
|
|
.Where(linea => linea != int.MaxValue && linea > 0)
|
|
.Distinct()
|
|
.OrderBy(linea => linea)
|
|
.ToList();
|
|
|
|
if (lineas.Count > 0)
|
|
return lineas;
|
|
|
|
return _lineasPermitidas
|
|
.Where(linea => linea > 0)
|
|
.Distinct()
|
|
.OrderBy(linea => linea)
|
|
.ToList();
|
|
}
|
|
|
|
private bool LineaFiltroSeleccionada(int linea)
|
|
=> _lineasPermitidas.Contains(linea);
|
|
|
|
private bool TodasLineasFiltroSeleccionadas()
|
|
{
|
|
var disponibles = LineasFiltroDisponibles();
|
|
return disponibles.Count > 0 && disponibles.All(_lineasPermitidas.Contains);
|
|
}
|
|
|
|
private string TextoBotonTodasLineasPermitidas()
|
|
=> TodasLineasFiltroSeleccionadas() ? "Ninguna" : "Todas";
|
|
|
|
private string TextoResumenLineasPermitidas()
|
|
{
|
|
var seleccionadas = ObtenerLineasPermitidasSeleccionadas();
|
|
var disponibles = LineasFiltroDisponibles();
|
|
if (seleccionadas.Count == 0)
|
|
return "Ninguna linea seleccionada";
|
|
|
|
if (disponibles.Count > 0 && seleccionadas.Count >= disponibles.Count)
|
|
return $"Todas las lineas ({disponibles.Count})";
|
|
|
|
return seleccionadas.Count <= 12
|
|
? string.Join(", ", seleccionadas)
|
|
: $"{seleccionadas.Count} lineas seleccionadas";
|
|
}
|
|
|
|
private async Task ToggleLineaFiltroAsync(int linea, ChangeEventArgs args)
|
|
{
|
|
var seleccionada = args.Value is bool valor && valor;
|
|
if (seleccionada)
|
|
_lineasPermitidas.Add(linea);
|
|
else
|
|
_lineasPermitidas.Remove(linea);
|
|
|
|
_busFilteredCache.Clear();
|
|
await PersistirConfiguracionAsync();
|
|
}
|
|
|
|
private async Task ToggleTodasLineasPermitidasAsync()
|
|
{
|
|
var disponibles = LineasFiltroDisponibles();
|
|
if (disponibles.Count > 0 && disponibles.All(_lineasPermitidas.Contains))
|
|
{
|
|
_lineasPermitidas.Clear();
|
|
}
|
|
else
|
|
{
|
|
foreach (var linea in disponibles)
|
|
_lineasPermitidas.Add(linea);
|
|
}
|
|
|
|
_busFilteredCache.Clear();
|
|
await PersistirConfiguracionAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Actualiza las alturas de origen y destino con la fuente seleccionada para el calculo.
|
|
/// </summary>
|
|
private async Task ActualizarAlturasResumenRutaAsync(CancellationToken ct)
|
|
{
|
|
if (_coordenadasOrigen is null || _coordenadasDestino is null)
|
|
{
|
|
_altitudOrigenResumenRuta = null;
|
|
_altitudDestinoResumenRuta = null;
|
|
return;
|
|
}
|
|
|
|
var origen = (_coordenadasOrigen[0], _coordenadasOrigen[1]);
|
|
var destino = (_coordenadasDestino[0], _coordenadasDestino[1]);
|
|
|
|
try
|
|
{
|
|
var puntos = new List<(double lat, double lon)> { origen, destino };
|
|
await AsegurarElevacionesFuenteCalculoAsync(puntos, ct);
|
|
|
|
_altitudOrigenResumenRuta = ObtenerElevacionFuenteCalculo(origen.Item1, origen.Item2);
|
|
_altitudDestinoResumenRuta = ObtenerElevacionFuenteCalculo(destino.Item1, destino.Item2);
|
|
}
|
|
catch
|
|
{
|
|
_altitudOrigenResumenRuta = null;
|
|
_altitudDestinoResumenRuta = null;
|
|
}
|
|
}
|
|
|
|
private async Task ActualizarCallejeroResumenRutaAsync(CancellationToken ct)
|
|
{
|
|
if (_coordenadasOrigen is null || _coordenadasDestino is null)
|
|
{
|
|
_callejeroOrigenResumenRuta = null;
|
|
_callejeroDestinoResumenRuta = null;
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var origen = await GeocodificadorLocal.BuscarMasCercanoAsync(
|
|
_coordenadasOrigen[0],
|
|
_coordenadasOrigen[1],
|
|
ct);
|
|
|
|
var destino = await GeocodificadorLocal.BuscarMasCercanoAsync(
|
|
_coordenadasDestino[0],
|
|
_coordenadasDestino[1],
|
|
ct);
|
|
|
|
_callejeroOrigenResumenRuta = FormatearLugarCallejeroResumen(origen);
|
|
_callejeroDestinoResumenRuta = FormatearLugarCallejeroResumen(destino);
|
|
}
|
|
catch
|
|
{
|
|
_callejeroOrigenResumenRuta = null;
|
|
_callejeroDestinoResumenRuta = null;
|
|
}
|
|
}
|
|
|
|
private async Task ActualizarResumenTecnicoRutaAsync(CancellationToken ct)
|
|
{
|
|
await ActualizarAlturasResumenRutaAsync(ct);
|
|
await ActualizarCallejeroResumenRutaAsync(ct);
|
|
}
|
|
|
|
|
|
private static readonly ConcurrentDictionary<string, double> _elevIdeeCache =
|
|
new(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly ConcurrentDictionary<string, double> _elevLocal5Cache =
|
|
new(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly ConcurrentDictionary<string, double> _elevLocal2Cache =
|
|
new(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly ConcurrentDictionary<string, double> _elevLocal1Cache =
|
|
new(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly SemaphoreSlim _semaforoElevacionIdee = new(1, 1);
|
|
private static long _ideeReintentarDespuesTicksUtc;
|
|
|
|
private const string FuenteElevacionLocal5 = "local5";
|
|
private const string FuenteElevacionLocal2 = "local2";
|
|
private const string FuenteElevacionLocal1 = "local1";
|
|
private const string FuenteElevacionIdee = "idee";
|
|
private const int TiempoMaximoIdeeSegundos = 10;
|
|
private string _fuenteElevacionCalculo = FuenteElevacionLocal5;
|
|
|
|
// IDEE: en el correo indican que 6 decimales es suficiente
|
|
private const int ELEV_IDEE_DECIMALES_CACHE = 6;
|
|
|
|
/// <summary>
|
|
/// Gestiona idee clave.
|
|
/// </summary>
|
|
private static string ClaveElevacionIdee(double lat, double lon)
|
|
{
|
|
double rLat = Math.Round(lat, ELEV_IDEE_DECIMALES_CACHE);
|
|
double rLon = Math.Round(lon, ELEV_IDEE_DECIMALES_CACHE);
|
|
return $"{rLat.ToString("0.000000", CultureInfo.InvariantCulture)},{rLon.ToString("0.000000", CultureInfo.InvariantCulture)}";
|
|
}
|
|
|
|
private static string NormalizarFuenteElevacion(string? fuente)
|
|
{
|
|
if (string.Equals(fuente, FuenteElevacionIdee, StringComparison.OrdinalIgnoreCase))
|
|
return FuenteElevacionIdee;
|
|
if (string.Equals(fuente, FuenteElevacionLocal1, StringComparison.OrdinalIgnoreCase))
|
|
return FuenteElevacionLocal1;
|
|
if (string.Equals(fuente, FuenteElevacionLocal2, StringComparison.OrdinalIgnoreCase))
|
|
return FuenteElevacionLocal2;
|
|
if (string.Equals(fuente, FuenteElevacionLocal5, StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(fuente, "local", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return FuenteElevacionLocal5;
|
|
}
|
|
|
|
return FuenteElevacionLocal5;
|
|
}
|
|
|
|
private bool ElevacionLocal5Disponible
|
|
=> ElevacionLocal.EstaDisponible(ModeloElevacionLocal.Local5);
|
|
|
|
private bool ElevacionLocal2Disponible
|
|
=> ElevacionLocal.EstaDisponible(ModeloElevacionLocal.Local2);
|
|
|
|
private bool ElevacionLocal1Disponible
|
|
=> ElevacionLocal.EstaDisponible(ModeloElevacionLocal.Local1);
|
|
|
|
private bool UsarElevacionLocal5ParaCalculo
|
|
=> string.Equals(_fuenteElevacionCalculo, FuenteElevacionLocal5, StringComparison.OrdinalIgnoreCase)
|
|
&& ElevacionLocal5Disponible;
|
|
|
|
private bool UsarElevacionLocal2ParaCalculo
|
|
=> string.Equals(_fuenteElevacionCalculo, FuenteElevacionLocal2, StringComparison.OrdinalIgnoreCase)
|
|
&& ElevacionLocal2Disponible;
|
|
|
|
private bool UsarElevacionLocal1ParaCalculo
|
|
=> string.Equals(_fuenteElevacionCalculo, FuenteElevacionLocal1, StringComparison.OrdinalIgnoreCase)
|
|
&& ElevacionLocal1Disponible;
|
|
|
|
private string EtiquetaFuenteElevacionCalculo
|
|
=> UsarElevacionLocal1ParaCalculo
|
|
? "Altura (L1)"
|
|
: UsarElevacionLocal2ParaCalculo
|
|
? "Altura (L2)"
|
|
: UsarElevacionLocal5ParaCalculo
|
|
? "Altura (L5)"
|
|
: "Altura (G)";
|
|
|
|
private double? ObtenerElevacionFuenteCalculo(double lat, double lon)
|
|
{
|
|
var clave = ClaveElevacionIdee(lat, lon);
|
|
if (UsarElevacionLocal1ParaCalculo)
|
|
return _elevLocal1Cache.TryGetValue(clave, out var local1) ? local1 : null;
|
|
if (UsarElevacionLocal2ParaCalculo)
|
|
return _elevLocal2Cache.TryGetValue(clave, out var local2) ? local2 : null;
|
|
if (UsarElevacionLocal5ParaCalculo)
|
|
return _elevLocal5Cache.TryGetValue(clave, out var local5) ? local5 : null;
|
|
|
|
return _elevIdeeCache.TryGetValue(clave, out var idee) ? idee : null;
|
|
}
|
|
|
|
private double? ObtenerElevacionFuenteCalculo(MuestraElevacion muestra)
|
|
=> UsarElevacionLocal1ParaCalculo
|
|
? muestra.ElevacionLocal1
|
|
: UsarElevacionLocal2ParaCalculo
|
|
? muestra.ElevacionLocal2
|
|
: UsarElevacionLocal5ParaCalculo
|
|
? muestra.ElevacionLocal5
|
|
: muestra.ElevacionIdee;
|
|
|
|
private void ObtenerElevacionesLocalesPorLote(
|
|
ModeloElevacionLocal modelo,
|
|
ConcurrentDictionary<string, double> cache,
|
|
IReadOnlyList<(double lat, double lon)> puntos)
|
|
{
|
|
if (!ElevacionLocal.EstaDisponible(modelo) || puntos.Count == 0)
|
|
return;
|
|
|
|
var pendientes = puntos
|
|
.Where(punto => !cache.ContainsKey(ClaveElevacionIdee(punto.lat, punto.lon)))
|
|
.Distinct()
|
|
.ToList();
|
|
if (pendientes.Count == 0)
|
|
return;
|
|
|
|
var elevaciones = ElevacionLocal.ObtenerElevaciones(modelo, pendientes);
|
|
for (var i = 0; i < pendientes.Count && i < elevaciones.Count; i++)
|
|
{
|
|
if (elevaciones[i] is { } elevacion && double.IsFinite(elevacion))
|
|
cache[ClaveElevacionIdee(pendientes[i].lat, pendientes[i].lon)] = elevacion;
|
|
}
|
|
}
|
|
|
|
private void ObtenerElevacionesLocal5PorLote(IReadOnlyList<(double lat, double lon)> puntos)
|
|
=> ObtenerElevacionesLocalesPorLote(
|
|
ModeloElevacionLocal.Local5,
|
|
_elevLocal5Cache,
|
|
puntos);
|
|
|
|
private void ObtenerElevacionesLocal2PorLote(IReadOnlyList<(double lat, double lon)> puntos)
|
|
=> ObtenerElevacionesLocalesPorLote(
|
|
ModeloElevacionLocal.Local2,
|
|
_elevLocal2Cache,
|
|
puntos);
|
|
|
|
private void ObtenerElevacionesLocal1PorLote(IReadOnlyList<(double lat, double lon)> puntos)
|
|
=> ObtenerElevacionesLocalesPorLote(
|
|
ModeloElevacionLocal.Local1,
|
|
_elevLocal1Cache,
|
|
puntos);
|
|
|
|
private void ObtenerElevacionesLocalesComparativasPorLote(
|
|
IReadOnlyList<(double lat, double lon)> puntos)
|
|
{
|
|
ObtenerElevacionesLocal5PorLote(puntos);
|
|
ObtenerElevacionesLocal2PorLote(puntos);
|
|
ObtenerElevacionesLocal1PorLote(puntos);
|
|
}
|
|
|
|
private async Task AsegurarElevacionesFuenteCalculoAsync(
|
|
IReadOnlyList<(double lat, double lon)> puntos,
|
|
CancellationToken ct)
|
|
{
|
|
if (UsarElevacionLocal1ParaCalculo)
|
|
{
|
|
ObtenerElevacionesLocal1PorLote(puntos);
|
|
return;
|
|
}
|
|
if (UsarElevacionLocal2ParaCalculo)
|
|
{
|
|
ObtenerElevacionesLocal2PorLote(puntos);
|
|
return;
|
|
}
|
|
if (UsarElevacionLocal5ParaCalculo)
|
|
{
|
|
ObtenerElevacionesLocal5PorLote(puntos);
|
|
return;
|
|
}
|
|
|
|
var faltan = puntos
|
|
.Where(punto => !_elevIdeeCache.ContainsKey(ClaveElevacionIdee(punto.lat, punto.lon)))
|
|
.Distinct()
|
|
.ToList();
|
|
for (var i = 0; i < faltan.Count; i += ELEV_BATCH_MAX_PUNTOS)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
await ObtenerElevacionesIdeePorLoteAsync(
|
|
faltan.Skip(i).Take(ELEV_BATCH_MAX_PUNTOS).ToList(),
|
|
ct);
|
|
}
|
|
}
|
|
|
|
private sealed class RespuestaElevacionIdee
|
|
{
|
|
public string? id { get; set; }
|
|
public List<double>? values { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recupera elevations idee el lote.
|
|
/// </summary>
|
|
private async Task ObtenerElevacionesIdeePorLoteAsync(List<(double lat, double lon)> points, CancellationToken ct)
|
|
{
|
|
if (points.Count == 0 || DateTime.UtcNow.Ticks < Interlocked.Read(ref _ideeReintentarDespuesTicksUtc))
|
|
return;
|
|
|
|
await _semaforoElevacionIdee.WaitAsync(ct);
|
|
try
|
|
{
|
|
if (DateTime.UtcNow.Ticks < Interlocked.Read(ref _ideeReintentarDespuesTicksUtc))
|
|
return;
|
|
|
|
points = points
|
|
.Where(punto => !_elevIdeeCache.ContainsKey(ClaveElevacionIdee(punto.lat, punto.lon)))
|
|
.Distinct()
|
|
.ToList();
|
|
if (points.Count == 0)
|
|
return;
|
|
|
|
// Construir FeatureCollection GeoJSON, OJO: coordinates = [lon, lat]
|
|
var features = new List<object>(points.Count);
|
|
foreach (var (lat, lon) in points)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
double rLat = Math.Round(lat, ELEV_IDEE_DECIMALES_CACHE);
|
|
double rLon = Math.Round(lon, ELEV_IDEE_DECIMALES_CACHE);
|
|
|
|
features.Add(new
|
|
{
|
|
type = "Feature",
|
|
geometry = new
|
|
{
|
|
type = "Point",
|
|
coordinates = new[] { rLon, rLat }
|
|
}
|
|
});
|
|
}
|
|
|
|
var geomObj = new
|
|
{
|
|
type = "FeatureCollection",
|
|
features
|
|
};
|
|
|
|
// En la API, "geom" va como STRING con el JSON dentro
|
|
string geomJson = JsonSerializer.Serialize(geomObj);
|
|
|
|
var body = new
|
|
{
|
|
inputs = new
|
|
{
|
|
crs = 4326,
|
|
formato = "geojson",
|
|
geom = geomJson,
|
|
outputFormat = "array",
|
|
withCoord = false
|
|
}
|
|
};
|
|
|
|
const string url = "https://api-processes.idee.es/processes/getElevation/execution";
|
|
|
|
try
|
|
{
|
|
using var limite = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
|
limite.CancelAfter(TimeSpan.FromSeconds(TiempoMaximoIdeeSegundos));
|
|
using var req = new HttpRequestMessage(HttpMethod.Post, url);
|
|
req.Headers.Accept.ParseAdd("application/json");
|
|
req.Content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json");
|
|
|
|
using var resp = await Http.SendAsync(req, limite.Token);
|
|
if (!resp.IsSuccessStatusCode)
|
|
{
|
|
PosponerConsultasIdee(TimeSpan.FromSeconds(15));
|
|
return;
|
|
}
|
|
|
|
var json = await resp.Content.ReadAsStringAsync(limite.Token);
|
|
|
|
var data = JsonSerializer.Deserialize<RespuestaElevacionIdee>(json, new JsonSerializerOptions
|
|
{
|
|
PropertyNameCaseInsensitive = true
|
|
});
|
|
|
|
if (data?.values is null || data.values.Count == 0) return;
|
|
|
|
// values viene en el mismo orden que mandas los puntos
|
|
int n = Math.Min(points.Count, data.values.Count);
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
var (lat, lon) = points[i];
|
|
var k = ClaveElevacionIdee(lat, lon);
|
|
_elevIdeeCache[k] = data.values[i];
|
|
}
|
|
|
|
Interlocked.Exchange(ref _ideeReintentarDespuesTicksUtc, 0);
|
|
}
|
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
|
{
|
|
throw;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
PosponerConsultasIdee(TimeSpan.FromSeconds(45));
|
|
}
|
|
catch
|
|
{
|
|
PosponerConsultasIdee(TimeSpan.FromSeconds(20));
|
|
// silencioso: elevación es informativa
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_semaforoElevacionIdee.Release();
|
|
}
|
|
}
|
|
|
|
private static void PosponerConsultasIdee(TimeSpan demora)
|
|
=> Interlocked.Exchange(
|
|
ref _ideeReintentarDespuesTicksUtc,
|
|
DateTime.UtcNow.Add(demora).Ticks);
|
|
|
|
// Ajustes (rendimiento)
|
|
/* private const int ELEV_MAX_PUNTOS_POR_TRAMO = 24; */
|
|
private int _elevMaxPuntosPorTramo = 999; // configurable
|
|
// muestreo por tramo
|
|
private const int ELEV_BATCH_MAX_PUNTOS = 90; // por request (prudente)
|
|
|
|
private static List<(double lat, double lon)> SampleLine(double[,] line, int maxPoints)
|
|
{
|
|
int n = line.GetLength(0);
|
|
if (n <= 0) return new();
|
|
|
|
if (maxPoints < 2) maxPoints = 2;
|
|
|
|
if (n <= maxPoints)
|
|
{
|
|
var all = new List<(double, double)>(n);
|
|
for (int i = 0; i < n; i++)
|
|
all.Add((line[i, 0], line[i, 1]));
|
|
return all;
|
|
}
|
|
|
|
// muestreo equiespaciado incluyendo extremos
|
|
var res = new List<(double, double)>(maxPoints);
|
|
for (int i = 0; i < maxPoints; i++)
|
|
{
|
|
double t = (maxPoints == 1) ? 0 : (double)i / (maxPoints - 1);
|
|
int idx = (int)Math.Round(t * (n - 1));
|
|
if (idx < 0) idx = 0;
|
|
if (idx >= n) idx = n - 1;
|
|
|
|
var p = (line[idx, 0], line[idx, 1]);
|
|
if (res.Count == 0 || res[^1].Item1 != p.Item1 || res[^1].Item2 != p.Item2)
|
|
res.Add(p);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Construye elev samples.
|
|
/// </summary>
|
|
private async Task<List<MuestraElevacion>> ConstruirMuestrasElevacionAsync(double[,] line, CancellationToken ct)
|
|
{
|
|
var sampled = SampleLine(line, _elevMaxPuntosPorTramo);
|
|
await AsegurarElevacionesFuenteCalculoAsync(sampled, ct);
|
|
|
|
return ConstruirMuestrasElevacionDesdeCache(line);
|
|
}
|
|
|
|
private List<MuestraElevacion> ConstruirMuestrasElevacionDesdeCache(double[,] line)
|
|
{
|
|
var sampled = SampleLine(line, _elevMaxPuntosPorTramo);
|
|
var res = new List<MuestraElevacion>(sampled.Count);
|
|
for (int i = 0; i < sampled.Count; i++)
|
|
{
|
|
var (lat, lon) = sampled[i];
|
|
var elevIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var e2) ? e2 : (double?)null;
|
|
var elevLocal5 = _elevLocal5Cache.TryGetValue(ClaveElevacionIdee(lat, lon), out var local5)
|
|
? local5
|
|
: (double?)null;
|
|
var elevLocal2 = _elevLocal2Cache.TryGetValue(ClaveElevacionIdee(lat, lon), out var local2)
|
|
? local2
|
|
: (double?)null;
|
|
var elevLocal1 = _elevLocal1Cache.TryGetValue(ClaveElevacionIdee(lat, lon), out var local1)
|
|
? local1
|
|
: (double?)null;
|
|
|
|
res.Add(new MuestraElevacion
|
|
{
|
|
Indice = i,
|
|
Latitud = lat,
|
|
Longitud = lon,
|
|
ElevacionIdee = elevIdee,
|
|
ElevacionLocal5 = elevLocal5,
|
|
ElevacionLocal2 = elevLocal2,
|
|
ElevacionLocal1 = elevLocal1
|
|
});
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
private async Task CalcularEsfuerzoExtraPendienteAlternativasAsync(
|
|
IReadOnlyList<AlternativaRuta> alternativas,
|
|
CancellationToken ct)
|
|
{
|
|
foreach (var alternativa in alternativas)
|
|
alternativa.EsfuerzoExtraPendienteMetros = 0;
|
|
|
|
if (!_mostrarElevacionesRuta || alternativas.Count == 0)
|
|
return;
|
|
|
|
var lineasPorAlternativa = alternativas
|
|
.Select(alternativa => new
|
|
{
|
|
Alternativa = alternativa,
|
|
Lineas = (alternativa.EsSoloAPie
|
|
? alternativa.LineaSoloAPie is { } lineaSolo
|
|
? new[] { lineaSolo }
|
|
: Enumerable.Empty<double[,]>()
|
|
: alternativa.LineasPieCalculoElevacion)
|
|
.Where(linea => linea.GetLength(0) >= 2)
|
|
.ToList()
|
|
})
|
|
.ToList();
|
|
|
|
var puntosPendientes = new Dictionary<string, (double lat, double lon)>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var item in lineasPorAlternativa)
|
|
{
|
|
foreach (var linea in item.Lineas)
|
|
{
|
|
foreach (var (lat, lon) in SampleLine(linea, _elevMaxPuntosPorTramo))
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
var clave = ClaveElevacionIdee(lat, lon);
|
|
puntosPendientes.TryAdd(
|
|
clave,
|
|
(Math.Round(lat, ELEV_IDEE_DECIMALES_CACHE), Math.Round(lon, ELEV_IDEE_DECIMALES_CACHE)));
|
|
}
|
|
}
|
|
}
|
|
|
|
var puntos = puntosPendientes.Values.ToList();
|
|
await AsegurarElevacionesFuenteCalculoAsync(puntos, ct);
|
|
|
|
foreach (var item in lineasPorAlternativa)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
var esfuerzoExtra = 0.0;
|
|
foreach (var linea in item.Lineas)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
var muestras = ConstruirMuestrasElevacionDesdeCache(linea);
|
|
esfuerzoExtra += CalcularResumenEsfuerzoElevacion(muestras).EsfuerzoExtraMetros;
|
|
}
|
|
|
|
item.Alternativa.EsfuerzoExtraPendienteMetros = double.IsFinite(esfuerzoExtra)
|
|
? Math.Max(0, esfuerzoExtra)
|
|
: 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gestiona tramo bus es basura.
|
|
/// </summary>
|
|
private bool PrimerTramoBusEsBasura(
|
|
RutaBusAlternativa alternativa,
|
|
double[] coordsOrigen,
|
|
double[] coordsDestino)
|
|
{
|
|
if (alternativa?.Tramos is null || alternativa.Tramos.Count < 2)
|
|
return false; // si solo hay un bus, no aplicamos esta regla
|
|
|
|
var tramos = alternativa.Tramos;
|
|
|
|
var primer = tramos[0];
|
|
var segundo = tramos[1];
|
|
|
|
// Parada real donde subes al primer bus
|
|
string codIniPrimer = primer.Variante.CodigosParadas[primer.IndiceInicio];
|
|
|
|
// Parada real donde bajas del primer bus
|
|
string codFinPrimer = primer.Variante.CodigosParadas[primer.IndiceFin];
|
|
|
|
// Parada real donde subes al segundo bus
|
|
string codIniSegundo = segundo.Variante.CodigosParadas[segundo.IndiceInicio];
|
|
|
|
if (!_paradaByCode.TryGetValue(codIniPrimer, out var pIniPrimer))
|
|
return false;
|
|
|
|
if (!_paradaByCode.TryGetValue(codFinPrimer, out var pFinPrimer))
|
|
return false;
|
|
|
|
if (!_paradaByCode.TryGetValue(codIniSegundo, out var pIniSegundo))
|
|
return false;
|
|
|
|
// Distancia del primer tramo bus
|
|
double distPrimerBus = 0;
|
|
{
|
|
double distTmp;
|
|
var lineas = RedBusServicio.ConstruirLineasBus(new List<TramoBus> { primer }, out distTmp);
|
|
distPrimerBus = Math.Max(0, distTmp);
|
|
}
|
|
|
|
// Nº de paradas del primer tramo
|
|
int paradasPrimerBus = ContarParadasRecorridas(primer);
|
|
|
|
// Caminata del transbordo tras ese primer tramo
|
|
double distWalkTransfer = CalcularDistanciaMetros(
|
|
pFinPrimer.Latitud, pFinPrimer.Longitud,
|
|
pIniSegundo.Latitud, pIniSegundo.Longitud);
|
|
|
|
// Distancia desde origen hasta la parada de subida
|
|
double distPieInicio = CalcularDistanciaMetros(
|
|
coordsOrigen[0], coordsOrigen[1],
|
|
pIniPrimer.Latitud, pIniPrimer.Longitud);
|
|
|
|
// Distancia desde donde te deja ese primer bus hasta destino
|
|
double distDesdeBajada1ADestino = CalcularDistanciaMetros(
|
|
pFinPrimer.Latitud, pFinPrimer.Longitud,
|
|
coordsDestino[0], coordsDestino[1]);
|
|
|
|
// Distancia desde donde subes al primer bus hasta destino
|
|
double distDesdeSubida1ADestino = CalcularDistanciaMetros(
|
|
pIniPrimer.Latitud, pIniPrimer.Longitud,
|
|
coordsDestino[0], coordsDestino[1]);
|
|
|
|
double avanceUtil = distDesdeSubida1ADestino - distDesdeBajada1ADestino;
|
|
|
|
// =====================================================
|
|
// Reglas SOLO para detectar "primer bus absurdo"
|
|
// =====================================================
|
|
|
|
// 1) Bus muy corto + transbordo andando largo
|
|
if (distPrimerBus <= 350 && distWalkTransfer >= 250)
|
|
return true;
|
|
|
|
// 2) Apenas 1 parada + luego caminata importante
|
|
if (paradasPrimerBus <= 1 && distWalkTransfer >= 180)
|
|
return true;
|
|
|
|
// 3) El primer bus aporta poco avance real hacia destino
|
|
if (avanceUtil <= 180 && distWalkTransfer >= 180)
|
|
return true;
|
|
|
|
// 4) Subes andando poco, haces mini-bus, y luego una caminata grande
|
|
if (distPieInicio <= 180 && distPrimerBus <= 500 && distWalkTransfer >= 300)
|
|
return true;
|
|
|
|
// 5) El transbordo andando es casi tan grande como el propio tramo bus
|
|
if (distPrimerBus > 0 && distWalkTransfer >= distPrimerBus * 0.9 && distWalkTransfer >= 200)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
string FmtDist(double? d)
|
|
{
|
|
if (d is null) return "-";
|
|
if (d.Value < 50) return $"{d.Value:0.0} m";
|
|
return $"{Math.Round(d.Value):0} m";
|
|
}
|
|
|
|
string FmtPend(double? pend, double? distPrev)
|
|
{
|
|
if (pend is null || distPrev is null) return "-";
|
|
if (distPrev.Value < 15) return "-";
|
|
return $"{pend.Value:+0.0;-0.0;0.0} %";
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Gestiona geometria completa variante.
|
|
/// </summary>
|
|
private double[,]? ConstruirGeometriaCompletaVariante(InfoVariante v)
|
|
{
|
|
if (v?.CodigosParadas is null || v.CodigosParadas.Count < 2) return null;
|
|
|
|
// tramo completo: desde la primera parada a la última
|
|
var tramo = new TramoBus
|
|
{
|
|
Variante = v,
|
|
IndiceInicio = 0,
|
|
IndiceFin = v.CodigosParadas.Count - 1
|
|
};
|
|
|
|
double dist;
|
|
var lineas = RedBusServicio.ConstruirLineasBus(new List<TramoBus> { tramo }, out dist);
|
|
|
|
// ConstruirLineasBus devuelve lista de lineas por tramo (aquí solo 1)
|
|
if (lineas is null || lineas.Count == 0) return null;
|
|
|
|
return lineas[0];
|
|
}
|
|
|
|
|
|
// =========================================================
|
|
// Rutado BUS filtrado (BFS + filtroVariante)
|
|
// =========================================================
|
|
|
|
/// <summary>
|
|
/// Busca ruta bus con filtro.
|
|
/// </summary>
|
|
private List<TramoBus>? BuscarRutaBusConFiltro(
|
|
string codigoParadaOrigen,
|
|
string codigoParadaDestino,
|
|
Func<InfoVariante, bool> filtroVariante,
|
|
out List<string> codigosParadasTransbordo,
|
|
int? maxTransbordos = null)
|
|
{
|
|
codigosParadasTransbordo = new();
|
|
|
|
if (!_indiceParadas.TryGetValue(codigoParadaOrigen, out var aparicionesOrigen))
|
|
return null;
|
|
if (!_indiceParadas.TryGetValue(codigoParadaDestino, out var aparicionesDestino))
|
|
return null;
|
|
|
|
// Estado = (variante, índice)
|
|
// Coste = nº de transbordos (cambios de variante) desde que subes al primer bus
|
|
var dq = new ColaDoble<(InfoVariante Variante, int IndiceParada)>();
|
|
|
|
var dist = new Dictionary<(InfoVariante Variante, int IndiceParada), int>();
|
|
var padre = new Dictionary<(InfoVariante Variante, int IndiceParada),
|
|
(InfoVariante Variante, int IndiceParada)?>();
|
|
|
|
// Inicial: subir al primer bus NO cuenta como transbordo => 0
|
|
foreach (var ap in aparicionesOrigen)
|
|
{
|
|
if (!filtroVariante(ap.Variante))
|
|
continue;
|
|
|
|
var nodo = (ap.Variante, ap.IndiceParada);
|
|
if (dist.ContainsKey(nodo)) continue;
|
|
|
|
dist[nodo] = 0;
|
|
padre[nodo] = null;
|
|
dq.PushFront(nodo);
|
|
}
|
|
|
|
(InfoVariante Variante, int IndiceParada)? objetivo = null;
|
|
|
|
while (dq.Count > 0)
|
|
{
|
|
var actual = dq.PopFront();
|
|
var (variante, indice) = actual;
|
|
|
|
int transUsados = dist[actual];
|
|
if (maxTransbordos is not null && transUsados > maxTransbordos.Value)
|
|
continue;
|
|
|
|
string codigoParadaActual = variante.CodigosParadas[indice];
|
|
|
|
if (codigoParadaActual == codigoParadaDestino)
|
|
{
|
|
objetivo = actual;
|
|
break; // ✅ 0-1 BFS => primer objetivo es mínimo en transbordos
|
|
}
|
|
|
|
// -----------------------
|
|
// 1) AVANZAR (coste 0)
|
|
// -----------------------
|
|
if (indice + 1 < variante.CodigosParadas.Count)
|
|
{
|
|
var sig = (variante, indice + 1);
|
|
int nuevo = transUsados; // coste 0
|
|
|
|
if (!dist.TryGetValue(sig, out var best) || nuevo < best)
|
|
{
|
|
dist[sig] = nuevo;
|
|
padre[sig] = actual;
|
|
dq.PushFront(sig); // coste 0 => delante
|
|
}
|
|
}
|
|
|
|
// -----------------------
|
|
// 2) TRANSBORDO MISMA PARADA (coste 1 si cambia variante)
|
|
// -----------------------
|
|
if (_indiceParadas.TryGetValue(codigoParadaActual, out var aparicionesParada))
|
|
{
|
|
foreach (var ap in aparicionesParada)
|
|
{
|
|
if (!filtroVariante(ap.Variante))
|
|
continue;
|
|
|
|
// si es exactamente el mismo estado, nada
|
|
if (ReferenceEquals(ap.Variante, variante) && ap.IndiceParada == indice)
|
|
continue;
|
|
|
|
int nuevo = transUsados + 1; // cambio de bus = +1
|
|
if (maxTransbordos is not null && nuevo > maxTransbordos.Value)
|
|
continue;
|
|
|
|
var sig = (ap.Variante, ap.IndiceParada);
|
|
|
|
if (!dist.TryGetValue(sig, out var best) || nuevo < best)
|
|
{
|
|
dist[sig] = nuevo;
|
|
padre[sig] = actual;
|
|
dq.PushBack(sig); // coste 1 => detrás
|
|
}
|
|
}
|
|
}
|
|
|
|
// -----------------------
|
|
// 3) TRANSBORDO ANDANDO A PARADAS CERCANAS (coste 1 si cambia variante)
|
|
// -----------------------
|
|
if (_paradasCercanas.TryGetValue(codigoParadaActual, out var vecinas))
|
|
{
|
|
foreach (var codVecino in vecinas)
|
|
{
|
|
if (!_indiceParadas.TryGetValue(codVecino, out var aparicionesVecino))
|
|
continue;
|
|
|
|
foreach (var ap in aparicionesVecino)
|
|
{
|
|
if (!filtroVariante(ap.Variante))
|
|
continue;
|
|
|
|
int nuevo = transUsados + 1;
|
|
if (maxTransbordos is not null && nuevo > maxTransbordos.Value)
|
|
continue;
|
|
|
|
var sig = (ap.Variante, ap.IndiceParada);
|
|
|
|
if (!dist.TryGetValue(sig, out var best) || nuevo < best)
|
|
{
|
|
dist[sig] = nuevo;
|
|
padre[sig] = actual;
|
|
dq.PushBack(sig);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (objetivo is null)
|
|
return null;
|
|
|
|
// reconstrucción camino
|
|
var nodosCamino = new List<(InfoVariante Variante, int IndiceParada)>();
|
|
var nodoActual = objetivo.Value;
|
|
|
|
while (true)
|
|
{
|
|
nodosCamino.Add(nodoActual);
|
|
if (!padre.TryGetValue(nodoActual, out var anterior) || anterior is null)
|
|
break;
|
|
nodoActual = anterior.Value;
|
|
}
|
|
|
|
nodosCamino.Reverse();
|
|
|
|
int numeroTransbordos;
|
|
var tramos = ConstruirTramosYTransbordosDesdeCamino(
|
|
nodosCamino,
|
|
codigoParadaOrigen,
|
|
codigoParadaDestino,
|
|
out codigosParadasTransbordo,
|
|
out numeroTransbordos);
|
|
|
|
// ✅ númeroTransbordos aquí será coherente con el 0-1 BFS (mínimo transbordos)
|
|
return tramos;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Gestiona tramos y transbordos desde camino.
|
|
/// </summary>
|
|
private List<TramoBus> ConstruirTramosYTransbordosDesdeCamino(
|
|
List<(InfoVariante Variante, int IndiceParada)> nodosCamino,
|
|
string codigoParadaOrigen,
|
|
string codigoParadaDestino,
|
|
out List<string> codigosParadasTransbordo,
|
|
out int numeroTransbordos)
|
|
{
|
|
codigosParadasTransbordo = new List<string>();
|
|
|
|
for (int i = 1; i < nodosCamino.Count; i++)
|
|
{
|
|
var previo = nodosCamino[i - 1];
|
|
var ahora = nodosCamino[i];
|
|
|
|
if (!ReferenceEquals(previo.Variante, ahora.Variante))
|
|
{
|
|
string codigoParada = ahora.Variante.CodigosParadas[ahora.IndiceParada];
|
|
if (!codigosParadasTransbordo.Contains(codigoParada) &&
|
|
codigoParada != codigoParadaOrigen &&
|
|
codigoParada != codigoParadaDestino)
|
|
{
|
|
codigosParadasTransbordo.Add(codigoParada);
|
|
}
|
|
}
|
|
}
|
|
|
|
var tramos = new List<TramoBus>();
|
|
if (nodosCamino.Count <= 1)
|
|
{
|
|
numeroTransbordos = 0;
|
|
return tramos;
|
|
}
|
|
|
|
var varianteActual = nodosCamino[0].Variante;
|
|
int indiceInicio = nodosCamino[0].IndiceParada;
|
|
int ultimoIndice = nodosCamino[0].IndiceParada;
|
|
|
|
for (int i = 1; i < nodosCamino.Count; i++)
|
|
{
|
|
var (variante, indice) = nodosCamino[i];
|
|
|
|
if (ReferenceEquals(variante, varianteActual) && indice == ultimoIndice + 1)
|
|
{
|
|
ultimoIndice = indice;
|
|
continue;
|
|
}
|
|
|
|
if (ultimoIndice > indiceInicio)
|
|
{
|
|
tramos.Add(new TramoBus
|
|
{
|
|
Variante = varianteActual,
|
|
IndiceInicio = indiceInicio,
|
|
IndiceFin = ultimoIndice
|
|
});
|
|
}
|
|
|
|
varianteActual = variante;
|
|
indiceInicio = indice;
|
|
ultimoIndice = indice;
|
|
}
|
|
|
|
if (ultimoIndice > indiceInicio)
|
|
{
|
|
tramos.Add(new TramoBus
|
|
{
|
|
Variante = varianteActual,
|
|
IndiceInicio = indiceInicio,
|
|
IndiceFin = ultimoIndice
|
|
});
|
|
}
|
|
|
|
codigosParadasTransbordo = new List<string>();
|
|
for (var i = 1; i < tramos.Count; i++)
|
|
{
|
|
if (!UtilidadesTransitoDbus.EsTransbordoReal(tramos[i - 1], tramos[i]))
|
|
continue;
|
|
|
|
var codigoParada = tramos[i].CodigoParadaInicio;
|
|
if (!codigosParadasTransbordo.Contains(codigoParada, StringComparer.OrdinalIgnoreCase) &&
|
|
codigoParada != codigoParadaOrigen &&
|
|
codigoParada != codigoParadaDestino)
|
|
{
|
|
codigosParadasTransbordo.Add(codigoParada);
|
|
}
|
|
}
|
|
|
|
numeroTransbordos = UtilidadesTransitoDbus.ContarTransbordosReales(tramos);
|
|
return tramos;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Normaliza sentido codigo.
|
|
/// </summary>
|
|
private static string NormalizarSentidoCodigo(string? codigo)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(codigo)) return "?";
|
|
|
|
var s = codigo.Trim();
|
|
|
|
// aceptar códigos ya normalizados
|
|
if (s.Equals("V", StringComparison.OrdinalIgnoreCase)) return "V";
|
|
if (s.Equals("I", StringComparison.OrdinalIgnoreCase)) return "I";
|
|
|
|
// aceptar textos
|
|
if (s.Equals("Vuelta", StringComparison.OrdinalIgnoreCase)) return "V";
|
|
if (s.Equals("Ida", StringComparison.OrdinalIgnoreCase)) return "I";
|
|
|
|
// fallback razonable
|
|
var c = char.ToUpperInvariant(s[0]);
|
|
if (c == 'V') return "V";
|
|
if (c == 'I') return "I";
|
|
return "?";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gestiona linea itinerario sentido.
|
|
/// </summary>
|
|
private static string CodigoLineaItinerarioSentido(TramoBus tramo)
|
|
{
|
|
var v = tramo.Variante;
|
|
|
|
var linea = v.Linea.Codigo?.ToString() ?? "?";
|
|
var itin = v.CodigoItinerario;
|
|
|
|
// ✅ sentido real del JSON
|
|
var sentido = NormalizarSentidoCodigo(v.Sentido?.Codigo);
|
|
|
|
return $"{linea}.{itin}-{sentido}";
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Gestiona ruta bus.
|
|
/// </summary>
|
|
private static string FirmaRutaBus(RutaBusAlternativa alt)
|
|
{
|
|
return string.Join(">",
|
|
alt.Tramos.Select(t => $"{CodigoLineaItinerarioSentido(t)}:{t.IndiceInicio}->{t.IndiceFin}")
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calcula transbordos reales.
|
|
/// </summary>
|
|
private static int CalcularTransbordosReales(RutaBusAlternativa alternativa)
|
|
{
|
|
return UtilidadesTransitoDbus.ContarTransbordosReales(alternativa?.Tramos);
|
|
}
|
|
|
|
// =========================================================
|
|
// Acciones UI
|
|
// =========================================================
|
|
|
|
/// <summary>
|
|
/// Resuelve parada desde texto.
|
|
/// </summary>
|
|
private Parada? ResolverParadaDesdeTexto(string texto)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(texto))
|
|
return null;
|
|
|
|
texto = texto.Trim();
|
|
|
|
int idx = texto.LastIndexOf('(');
|
|
if (idx >= 0 && texto.EndsWith(")"))
|
|
{
|
|
var codigoRaw = texto.Substring(idx + 1, texto.Length - idx - 2).Trim();
|
|
var codNorm = NormalizarCodigoParada(codigoRaw);
|
|
|
|
var paradaPorCodigo = RedBusServicio.Paradas.FirstOrDefault(p =>
|
|
NormalizarCodigoParada(p.Codigo).Equals(codNorm, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (paradaPorCodigo is not null)
|
|
return paradaPorCodigo;
|
|
}
|
|
|
|
var codNorm2 = NormalizarCodigoParada(texto);
|
|
|
|
var porCodigo = RedBusServicio.Paradas.FirstOrDefault(p =>
|
|
NormalizarCodigoParada(p.Codigo).Equals(codNorm2, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (porCodigo is not null)
|
|
return porCodigo;
|
|
|
|
var porNombre = RedBusServicio.Paradas
|
|
.FirstOrDefault(p =>
|
|
p.Nombre.Contains(texto, StringComparison.OrdinalIgnoreCase) ||
|
|
p.NombreCorto.Contains(texto, StringComparison.OrdinalIgnoreCase) ||
|
|
p.Abreviatura.Contains(texto, StringComparison.OrdinalIgnoreCase));
|
|
|
|
return porNombre;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cambia alternativa absoluta.
|
|
/// </summary>
|
|
private async Task CambiarAlternativaAbsoluta(int indice)
|
|
{
|
|
await CerrarTodasLasPreviewsAsync();
|
|
if (indice < 0 || indice >= _opcionesRuta.Count) return;
|
|
|
|
_indiceAlternativaSeleccionada = indice;
|
|
_proveedorSoloAPieRecalculo = _opcionesRuta[indice].EsSoloAPie ? _opcionesRuta[indice].ProveedorRuta : null;
|
|
await CerrarPopupsAsync();
|
|
|
|
ResetEstadoRuta(limpiarOpciones: false, limpiarMensaje: false);
|
|
_mensajeRuta = "Representando ruta seleccionada...";
|
|
StateHasChanged();
|
|
await Task.Yield();
|
|
await PintarOpcionRutaCompleta(_opcionesRuta[indice]);
|
|
await RegistrarEscenarioActualEnHistorialLocalAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cambia el criterio prioritario de rutas sugeridas.
|
|
/// </summary>
|
|
private async Task CambiarCriterioPrioritarioRutasAsync(string criterio)
|
|
{
|
|
var criterioNormalizado = NormalizarCriterioRuta(criterio);
|
|
if (string.Equals(_criterioPrioritarioRuta, criterioNormalizado, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_criterioPrioritarioRuta = criterioNormalizado;
|
|
SincronizarPreferenciaDesdeCriterioRuta();
|
|
await PersistirConfiguracionAsync();
|
|
|
|
if (_mapa is not null && _coordenadasOrigen is not null && _coordenadasDestino is not null)
|
|
{
|
|
_omitirRegistroHistorialEnSiguienteCalculo = true;
|
|
await CalcularRutaCompletaAsync();
|
|
return;
|
|
}
|
|
|
|
if (_opcionesRuta.Count == 0)
|
|
{
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
OrdenarOpcionesRutaSegunCriterio();
|
|
var indicePreferido = BuscarIndicePreferidoPorCriterio(criterioNormalizado);
|
|
if (indicePreferido < 0 || indicePreferido >= _opcionesRuta.Count)
|
|
{
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
await CerrarTodasLasPreviewsAsync();
|
|
_indiceAlternativaSeleccionada = indicePreferido;
|
|
await CerrarPopupsAsync();
|
|
ResetEstadoRuta(limpiarOpciones: false, limpiarMensaje: false);
|
|
_mensajeRuta = "Representando ruta seleccionada...";
|
|
StateHasChanged();
|
|
await Task.Yield();
|
|
await PintarOpcionRutaCompleta(_opcionesRuta[indicePreferido]);
|
|
StateHasChanged();
|
|
}
|
|
|
|
private int BuscarIndicePreferidoPorCriterio(string criterio)
|
|
{
|
|
if (string.Equals(criterio, CriterioRutaEquilibrado, StringComparison.OrdinalIgnoreCase))
|
|
return BuscarIndiceEquilibrado();
|
|
|
|
if (string.Equals(criterio, CriterioRutaAndar, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return BuscarPrimerIndiceOrdenadoPor(
|
|
opcion => CalcularCosteAndarEquilibradoSegundos(opcion),
|
|
opcion => DuracionOrdenacion(opcion));
|
|
}
|
|
|
|
return BuscarPrimerIndiceOrdenadoPor(
|
|
opcion => DuracionOrdenacion(opcion),
|
|
opcion => TiempoPieOrdenacion(opcion));
|
|
}
|
|
|
|
private int BuscarIndiceEquilibrado()
|
|
{
|
|
var candidatas = _opcionesRuta
|
|
.Select((opcion, indice) => new { opcion, indice })
|
|
.ToList();
|
|
|
|
if (candidatas.Count == 0)
|
|
return _opcionesRuta.Count > 0 ? 0 : -1;
|
|
|
|
return candidatas
|
|
.OrderBy(x => CalcularScoreEquilibradoFactor(x.opcion))
|
|
.ThenBy(x => x.opcion.NumeroTransbordosReales)
|
|
.ThenBy(x => x.opcion.TiempoEsperaTotalSegundos)
|
|
.ThenBy(x => x.opcion.PuntuacionCoste)
|
|
.Select(x => x.indice)
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
private void OrdenarOpcionesRutaSegunCriterio()
|
|
{
|
|
if (_opcionesRuta.Count <= 1)
|
|
return;
|
|
|
|
IEnumerable<AlternativaRuta> ordenadas = _opcionesRuta;
|
|
if (CriterioPrioritarioAndarActivo)
|
|
{
|
|
ordenadas = ordenadas
|
|
.OrderBy(CalcularCosteAndarEquilibradoSegundos)
|
|
.ThenBy(DuracionOrdenacion)
|
|
.ThenBy(opcion => opcion.PuntuacionCoste);
|
|
}
|
|
else if (CriterioPrioritarioTiempoActivo)
|
|
{
|
|
ordenadas = ordenadas
|
|
.OrderBy(DuracionOrdenacion)
|
|
.ThenBy(TiempoPieOrdenacion)
|
|
.ThenBy(opcion => opcion.PuntuacionCoste);
|
|
}
|
|
else if (CriterioPrioritarioEquilibradoActivo)
|
|
{
|
|
ordenadas = ordenadas
|
|
.OrderBy(CalcularScoreEquilibradoFactor)
|
|
.ThenBy(opcion => opcion.NumeroTransbordosReales)
|
|
.ThenBy(opcion => opcion.TiempoEsperaTotalSegundos)
|
|
.ThenBy(opcion => opcion.PuntuacionCoste);
|
|
}
|
|
|
|
var listaOrdenada = ordenadas.ToList();
|
|
_opcionesRuta.Clear();
|
|
_opcionesRuta.AddRange(listaOrdenada);
|
|
FiltrarAlternativasBusRedundantes();
|
|
RenumerarEtiquetasGenericasOpcionesRuta();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Conserva la mejor alternativa de cada direccion de acceso a una misma linea
|
|
/// y descarta rutas en bus que no ahorran ninguna distancia andando.
|
|
/// </summary>
|
|
private void FiltrarAlternativasBusRedundantes()
|
|
{
|
|
if (_opcionesRuta.Count <= 1)
|
|
return;
|
|
|
|
var distanciaSoloPie = _opcionesRuta
|
|
.Where(opcion => opcion.EsSoloAPie &&
|
|
double.IsFinite(opcion.DistanciaSoloAPie) &&
|
|
opcion.DistanciaSoloAPie > 0)
|
|
.Select(opcion => opcion.DistanciaSoloAPie)
|
|
.DefaultIfEmpty(double.PositiveInfinity)
|
|
.Min();
|
|
|
|
var conservadas = new List<AlternativaRuta>(_opcionesRuta.Count);
|
|
foreach (var candidata in _opcionesRuta)
|
|
{
|
|
if (candidata.EsSoloAPie)
|
|
{
|
|
conservadas.Add(candidata);
|
|
continue;
|
|
}
|
|
|
|
if (double.IsFinite(distanciaSoloPie) &&
|
|
double.IsFinite(candidata.DistanciaPieTotalAprox) &&
|
|
candidata.DistanciaPieTotalAprox >= distanciaSoloPie)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var repiteDireccionAcceso = conservadas.Any(referencia =>
|
|
!referencia.EsSoloAPie &&
|
|
EsMismaLineaInicial(referencia, candidata) &&
|
|
CaminanEnLaMismaDireccionDesdeOrigen(referencia, candidata));
|
|
|
|
if (!repiteDireccionAcceso)
|
|
conservadas.Add(candidata);
|
|
}
|
|
|
|
if (conservadas.Count == _opcionesRuta.Count)
|
|
return;
|
|
|
|
_opcionesRuta.Clear();
|
|
_opcionesRuta.AddRange(conservadas);
|
|
}
|
|
|
|
private static bool EsMismaLineaInicial(
|
|
AlternativaRuta referencia,
|
|
AlternativaRuta candidata)
|
|
{
|
|
var tramoReferencia = referencia.AlternativaBus?.Tramos.FirstOrDefault();
|
|
var tramoCandidata = candidata.AlternativaBus?.Tramos.FirstOrDefault();
|
|
if (tramoReferencia is null || tramoCandidata is null)
|
|
return false;
|
|
|
|
return string.Equals(
|
|
UtilidadesTransitoDbus.LineaCodigoStr(tramoReferencia.Variante),
|
|
UtilidadesTransitoDbus.LineaCodigoStr(tramoCandidata.Variante),
|
|
StringComparison.OrdinalIgnoreCase) &&
|
|
tramoReferencia.Variante.CodigoItinerario == tramoCandidata.Variante.CodigoItinerario;
|
|
}
|
|
|
|
private bool CaminanEnLaMismaDireccionDesdeOrigen(
|
|
AlternativaRuta referencia,
|
|
AlternativaRuta candidata)
|
|
{
|
|
if (_coordenadasOrigen is null ||
|
|
_coordenadasOrigen.Length < 2 ||
|
|
referencia.OrigenParada is null ||
|
|
candidata.OrigenParada is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var latitudOrigen = _coordenadasOrigen[0];
|
|
var longitudOrigen = _coordenadasOrigen[1];
|
|
if (!double.IsFinite(latitudOrigen) || !double.IsFinite(longitudOrigen))
|
|
return false;
|
|
|
|
var metrosPorGradoLatitud = 111_320d;
|
|
var metrosPorGradoLongitud =
|
|
metrosPorGradoLatitud * Math.Cos(latitudOrigen * Math.PI / 180d);
|
|
|
|
var referenciaX =
|
|
(referencia.OrigenParada.Longitud - longitudOrigen) * metrosPorGradoLongitud;
|
|
var referenciaY =
|
|
(referencia.OrigenParada.Latitud - latitudOrigen) * metrosPorGradoLatitud;
|
|
var candidataX =
|
|
(candidata.OrigenParada.Longitud - longitudOrigen) * metrosPorGradoLongitud;
|
|
var candidataY =
|
|
(candidata.OrigenParada.Latitud - latitudOrigen) * metrosPorGradoLatitud;
|
|
|
|
var moduloReferencia = Math.Sqrt(
|
|
referenciaX * referenciaX + referenciaY * referenciaY);
|
|
var moduloCandidata = Math.Sqrt(
|
|
candidataX * candidataX + candidataY * candidataY);
|
|
if (!double.IsFinite(moduloReferencia) ||
|
|
!double.IsFinite(moduloCandidata) ||
|
|
moduloReferencia < 1d ||
|
|
moduloCandidata < 1d)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var cosenoDireccion =
|
|
(referenciaX * candidataX + referenciaY * candidataY) /
|
|
(moduloReferencia * moduloCandidata);
|
|
return double.IsFinite(cosenoDireccion) && cosenoDireccion >= 0d;
|
|
}
|
|
|
|
private void RenumerarEtiquetasGenericasOpcionesRuta()
|
|
{
|
|
for (var i = 0; i < _opcionesRuta.Count; i++)
|
|
{
|
|
var etiqueta = _opcionesRuta[i].Etiqueta;
|
|
if (!EsEtiquetaOpcionGenerica(etiqueta))
|
|
continue;
|
|
|
|
_opcionesRuta[i].Etiqueta = $"Opcion {i + 1}";
|
|
}
|
|
}
|
|
|
|
private static bool EsEtiquetaOpcionGenerica(string? etiqueta)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(etiqueta))
|
|
return false;
|
|
|
|
return Regex.IsMatch(
|
|
etiqueta.Trim(),
|
|
@"^Opci(?:o|ó|ó)n\s+\d+$",
|
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
|
}
|
|
|
|
private int BuscarPrimerIndiceOrdenadoPor(
|
|
Func<AlternativaRuta, double> criterioPrincipal,
|
|
Func<AlternativaRuta, double> criterioSecundario)
|
|
{
|
|
return _opcionesRuta
|
|
.Select((opcion, indice) => new { opcion, indice })
|
|
.OrderBy(x => criterioPrincipal(x.opcion))
|
|
.ThenBy(x => criterioSecundario(x.opcion))
|
|
.ThenBy(x => x.opcion.PuntuacionCoste)
|
|
.Select(x => x.indice)
|
|
.FirstOrDefault(-1);
|
|
}
|
|
|
|
private double DuracionOrdenacion(AlternativaRuta alternativa)
|
|
=> double.IsFinite(alternativa.DuracionTotalAproxSegundos)
|
|
? alternativa.DuracionTotalAproxSegundos
|
|
: double.PositiveInfinity;
|
|
|
|
private double TiempoPieOrdenacion(AlternativaRuta alternativa)
|
|
{
|
|
if (alternativa.EsSoloAPie)
|
|
return DuracionOrdenacion(alternativa);
|
|
|
|
var tiempoTransbordosPie = EstimarDuracionPieSegundos(alternativa.DistanciaPieTransbordosAprox);
|
|
return Math.Max(0, alternativa.TiempoPieInicio) +
|
|
Math.Max(0, tiempoTransbordosPie) +
|
|
Math.Max(0, alternativa.TiempoPieFin);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Intercambia origen destino.
|
|
/// </summary>
|
|
private async Task IntercambiarOrigenDestino()
|
|
{
|
|
(_coordenadasOrigen, _coordenadasDestino) = (_coordenadasDestino, _coordenadasOrigen);
|
|
(_textoBusquedaOrigen, _textoBusquedaDestino) = (_textoBusquedaDestino, _textoBusquedaOrigen);
|
|
(_ubicacionOrigenSeleccionada, _ubicacionDestinoSeleccionada) = (_ubicacionDestinoSeleccionada, _ubicacionOrigenSeleccionada);
|
|
_sugerenciasBusquedaOrigen.Clear();
|
|
_sugerenciasBusquedaDestino.Clear();
|
|
_campoBusquedaActivo = null;
|
|
|
|
await ClearRoutesJsAsync();
|
|
await CerrarTodasLasPreviewsAsync();
|
|
await SincronizarMarcadoresOrigenDestinoJsAsync();
|
|
|
|
if (_mapa is not null)
|
|
{
|
|
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
|
|
}
|
|
|
|
if (_coordenadasOrigen is not null && _coordenadasDestino is not null)
|
|
await CalcularRutaCompletaAsync(conservarSeleccionSoloAPie: true);
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Busca ruta desde buscador.
|
|
/// </summary>
|
|
private async Task BuscarRutaDesdeBuscador()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_textoBusquedaOrigen) ||
|
|
string.IsNullOrWhiteSpace(_textoBusquedaDestino))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var po = await ResolverUbicacionBusquedaAsync(CampoBusquedaMapa.Origen);
|
|
var pd = await ResolverUbicacionBusquedaAsync(CampoBusquedaMapa.Destino);
|
|
|
|
if (po is null || pd is null)
|
|
{
|
|
_mensajeRuta = "No he podido interpretar el origen o el destino. Prueba a seleccionar una sugerencia de la lista.";
|
|
_hayRutaValida = false;
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
_coordenadasOrigen = new[] { po.Latitud, po.Longitud };
|
|
_coordenadasDestino = new[] { pd.Latitud, pd.Longitud };
|
|
_textoBusquedaOrigen = po.TextoEntrada;
|
|
_textoBusquedaDestino = pd.TextoEntrada;
|
|
_ubicacionOrigenSeleccionada = po;
|
|
_ubicacionDestinoSeleccionada = pd;
|
|
_sugerenciasBusquedaOrigen.Clear();
|
|
_sugerenciasBusquedaDestino.Clear();
|
|
_campoBusquedaActivo = null;
|
|
|
|
if (_mapa is not null)
|
|
{
|
|
await ClearRoutesJsAsync();
|
|
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
|
|
|
|
await _mapa.Geometric.DisplayPointsFromArray.deleteAllPoints();
|
|
await SincronizarMarcadoresOrigenDestinoJsAsync();
|
|
|
|
if (_mostrarParadas)
|
|
await DibujarParadasAsync();
|
|
}
|
|
|
|
await CerrarTodasLasPreviewsAsync();
|
|
await CerrarPopupsAsync();
|
|
await CalcularRutaCompletaAsync(conservarSeleccionSoloAPie: true);
|
|
}
|
|
/// <summary>
|
|
/// Muestra inspector.
|
|
/// </summary>
|
|
private async Task MostrarInspectorAsync(double lat, double lon)
|
|
{
|
|
_inspectorPuntoAnterior = _popupInspectorVisible
|
|
? new PuntoInspectorAnterior(
|
|
_inspectorLat,
|
|
_inspectorLon,
|
|
_inspectorElevIdee,
|
|
_inspectorElevLocal5,
|
|
_inspectorElevLocal2,
|
|
_inspectorElevLocal1,
|
|
ObtenerDireccionPuntoInspectorActual(),
|
|
DistanciaAndandoMetros: null,
|
|
CalculandoDatosAdicionales: true)
|
|
: null;
|
|
|
|
var versionInspector = ++_versionInspector;
|
|
_inspectorLat = lat;
|
|
_inspectorLon = lon;
|
|
_inspectorElevIdee = null;
|
|
_inspectorElevLocal5 = null;
|
|
_inspectorElevLocal2 = null;
|
|
_inspectorElevLocal1 = null;
|
|
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
|
|
_inspectorSuperficieTexto = null;
|
|
_inspectorSuperficieCargando = true;
|
|
_popupInspectorVisible = true;
|
|
|
|
ObtenerElevacionesLocalesComparativasPorLote(new[] { (lat, lon) });
|
|
var claveLocal = ClaveElevacionIdee(lat, lon);
|
|
_inspectorElevLocal5 = _elevLocal5Cache.TryGetValue(claveLocal, out var elevLocal5)
|
|
? elevLocal5
|
|
: null;
|
|
_inspectorElevLocal2 = _elevLocal2Cache.TryGetValue(claveLocal, out var elevLocal2)
|
|
? elevLocal2
|
|
: null;
|
|
_inspectorElevLocal1 = _elevLocal1Cache.TryGetValue(claveLocal, out var elevLocal1)
|
|
? elevLocal1
|
|
: null;
|
|
|
|
await ActualizarMarcadoresInspectorJsAsync();
|
|
StateHasChanged();
|
|
|
|
_ = CompletarSuperficiePuntoInspectorAsync(lat, lon, versionInspector);
|
|
|
|
if (_inspectorPuntoAnterior is { } puntoAnterior)
|
|
_ = CompletarDatosPuntoInspectorAnteriorAsync(puntoAnterior, versionInspector);
|
|
|
|
var tareaLugares = GeocodificadorLocal.BuscarCercanosAsync(
|
|
lat,
|
|
lon,
|
|
radioMetros: 20,
|
|
maxResultados: 3,
|
|
cancellationToken: CancellationToken.None);
|
|
|
|
double? elevacionIdee = null;
|
|
try
|
|
{
|
|
// -------------------------
|
|
// IDEE (cache primero)
|
|
// -------------------------
|
|
var kIdee = ClaveElevacionIdee(lat, lon);
|
|
if (_elevIdeeCache.TryGetValue(kIdee, out var cachedIdee))
|
|
{
|
|
elevacionIdee = cachedIdee;
|
|
}
|
|
else
|
|
{
|
|
await ObtenerElevacionesIdeePorLoteAsync(
|
|
new List<(double lat, double lon)> { (lat, lon) },
|
|
CancellationToken.None);
|
|
|
|
elevacionIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var elevIdee)
|
|
? elevIdee
|
|
: (double?)null;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
try
|
|
{
|
|
var lugares = await tareaLugares;
|
|
if (versionInspector != _versionInspector)
|
|
return;
|
|
|
|
_inspectorLugaresCercanos = lugares;
|
|
}
|
|
catch
|
|
{
|
|
if (versionInspector != _versionInspector)
|
|
return;
|
|
|
|
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
|
|
}
|
|
|
|
if (versionInspector != _versionInspector)
|
|
return;
|
|
|
|
_inspectorElevIdee = elevacionIdee;
|
|
await ActualizarMarcadoresInspectorJsAsync();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task ActualizarMarcadoresInspectorJsAsync()
|
|
{
|
|
var lugares = _inspectorLugaresCercanos
|
|
.Select(cercano => new
|
|
{
|
|
latitud = cercano.Lugar.Latitud,
|
|
longitud = cercano.Lugar.Longitud,
|
|
texto = FormatearLugarCallejeroResumen(cercano)
|
|
})
|
|
.ToList();
|
|
|
|
try
|
|
{
|
|
await JS.InvokeVoidAsync(
|
|
"rt.setInspectorMarkers",
|
|
_inspectorLat,
|
|
_inspectorLon,
|
|
lugares);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
private async Task CerrarInspectorAsync()
|
|
{
|
|
_versionInspector++;
|
|
_popupInspectorVisible = false;
|
|
_inspectorPuntoAnterior = null;
|
|
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
|
|
_inspectorSuperficieCargando = false;
|
|
_inspectorSuperficieTexto = null;
|
|
|
|
try { await JS.InvokeVoidAsync("rt.clearInspectorMarkers"); } catch { }
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Gestiona hacer clic en mapa.
|
|
/// </summary>
|
|
private async Task AlHacerClickEnMapa(RealTimeMap.ClicksMapArgs e)
|
|
{
|
|
var lat = e.location.latitude;
|
|
var lon = e.location.longitude;
|
|
|
|
if (_modoDibujoExclusionCaminata)
|
|
{
|
|
await OnExclusionPolygonMapClick(lat, lon);
|
|
return;
|
|
}
|
|
|
|
// Inspector (Z) sigue siendo click simple inmediato
|
|
if (_inspectorActivo)
|
|
{
|
|
await MostrarInspectorAsync(lat, lon);
|
|
return;
|
|
}
|
|
|
|
// ¿Este click cae SOBRE una parada?
|
|
var paradaHit = await DetectarParadaHitAsync(lat, lon);
|
|
if (paradaHit is null && !_mostrarParadas)
|
|
paradaHit = await DetectarParadaDestacadaHitAsync(lat, lon);
|
|
|
|
// =====================================================
|
|
// 2º click (si había uno pendiente)
|
|
// =====================================================
|
|
if (_primerClickPendiente is (double lat1, double lon1))
|
|
{
|
|
_ctsDobleClick?.Cancel();
|
|
_ctsDobleClick?.Dispose();
|
|
_ctsDobleClick = null;
|
|
|
|
_primerClickPendiente = null;
|
|
_paradaPrimerClickPendiente = null; // ✅ si era parada, anulamos popup
|
|
|
|
// si los dos clicks son cerca => doble click => PONER PUNTO SIEMPRE
|
|
if (Cerca(lat1, lon1, lat, lon, DBLCLICK_MAX_METROS))
|
|
{
|
|
// Si el segundo click cae en parada, ponemos el punto EXACTO en la parada
|
|
if (paradaHit is not null)
|
|
await EjecutarSeleccionOrigenDestinoAsync(paradaHit.Latitud, paradaHit.Longitud);
|
|
else
|
|
await EjecutarSeleccionOrigenDestinoAsync(lat, lon);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// =====================================================
|
|
// 1º click (armamos la ventana de doble click)
|
|
// =====================================================
|
|
_primerClickPendiente = (lat, lon);
|
|
_paradaPrimerClickPendiente = paradaHit;
|
|
|
|
_ctsDobleClick = new CancellationTokenSource();
|
|
var ct = _ctsDobleClick.Token;
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await Task.Delay(DBLCLICK_MS, ct);
|
|
|
|
if (ct.IsCancellationRequested) return;
|
|
|
|
// Se acabó la ventana: NO hubo doble click
|
|
var parada = _paradaPrimerClickPendiente;
|
|
|
|
_primerClickPendiente = null;
|
|
_paradaPrimerClickPendiente = null;
|
|
|
|
// Si era parada => click simple => mostrar popup
|
|
if (parada is not null)
|
|
{
|
|
await InvokeAsync(async () =>
|
|
{
|
|
await MostrarPopupParadaAsync(parada);
|
|
});
|
|
}
|
|
// Si NO era parada: click simple no hace nada (como querías)
|
|
}
|
|
catch { }
|
|
finally
|
|
{
|
|
if (!ct.IsCancellationRequested)
|
|
{
|
|
// por si acaso
|
|
_primerClickPendiente = null;
|
|
_paradaPrimerClickPendiente = null;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Selecciona origen o destino mediante pulsacion larga en dispositivos tactiles.
|
|
/// </summary>
|
|
[JSInvokable]
|
|
public async Task OnMobileMapLongPress(double lat, double lon)
|
|
{
|
|
if (_modoDibujoExclusionCaminata) return;
|
|
await InvokeAsync(async () =>
|
|
{
|
|
if (_inspectorActivo)
|
|
return;
|
|
|
|
_ctsDobleClick?.Cancel();
|
|
_ctsDobleClick?.Dispose();
|
|
_ctsDobleClick = null;
|
|
_primerClickPendiente = null;
|
|
_paradaPrimerClickPendiente = null;
|
|
|
|
var paradaHit = await DetectarParadaHitAsync(lat, lon);
|
|
if (paradaHit is null && !_mostrarParadas)
|
|
paradaHit = await DetectarParadaDestacadaHitAsync(lat, lon);
|
|
|
|
if (paradaHit is not null)
|
|
await EjecutarSeleccionOrigenDestinoAsync(paradaHit.Latitud, paradaHit.Longitud);
|
|
else
|
|
await EjecutarSeleccionOrigenDestinoAsync(lat, lon);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gestiona seleccion origen destino.
|
|
/// </summary>
|
|
private async Task EjecutarSeleccionOrigenDestinoAsync(double lat, double lon)
|
|
{
|
|
if (_coordenadasOrigen is null)
|
|
{
|
|
_coordenadasOrigen = new[] { lat, lon };
|
|
|
|
await SincronizarMarcadoresOrigenDestinoJsAsync();
|
|
|
|
_mensajeRuta = string.Empty;
|
|
_hayRutaValida = false;
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
if (_coordenadasDestino is null)
|
|
{
|
|
_coordenadasDestino = new[] { lat, lon };
|
|
|
|
await SincronizarMarcadoresOrigenDestinoJsAsync();
|
|
|
|
await CalcularRutaCompletaAsync(conservarSeleccionSoloAPie: true);
|
|
return;
|
|
}
|
|
|
|
_coordenadasDestino = new[] { lat, lon };
|
|
|
|
if (_mapa is not null)
|
|
{
|
|
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
|
|
await ClearRoutesJsAsync();
|
|
await SincronizarMarcadoresOrigenDestinoJsAsync();
|
|
}
|
|
|
|
await CalcularRutaCompletaAsync(conservarSeleccionSoloAPie: true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reacciona al arrastre de origen o destino desde el mapa.
|
|
/// </summary>
|
|
[JSInvokable]
|
|
public async Task OnEndpointDragged(string kind, double lat, double lon)
|
|
{
|
|
await InvokeAsync(async () =>
|
|
{
|
|
if (string.Equals(kind, "origin", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_coordenadasOrigen = new[] { lat, lon };
|
|
_ubicacionOrigenSeleccionada = null;
|
|
_sugerenciasBusquedaOrigen.Clear();
|
|
}
|
|
else if (string.Equals(kind, "destination", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_coordenadasDestino = new[] { lat, lon };
|
|
_ubicacionDestinoSeleccionada = null;
|
|
_sugerenciasBusquedaDestino.Clear();
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
await SincronizarMarcadoresOrigenDestinoJsAsync();
|
|
await CerrarTodasLasPreviewsAsync();
|
|
await CerrarPopupsAsync();
|
|
|
|
if (_mapa is not null)
|
|
{
|
|
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
|
|
await ClearRoutesJsAsync();
|
|
}
|
|
|
|
if (_coordenadasOrigen is not null && _coordenadasDestino is not null)
|
|
{
|
|
await CalcularRutaCompletaAsync(conservarSeleccionSoloAPie: true);
|
|
}
|
|
else
|
|
{
|
|
ResetEstadoRuta(limpiarOpciones: true);
|
|
_mensajeRuta = string.Empty;
|
|
StateHasChanged();
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
// Cálculo completo (optimizado + estricto real)
|
|
// =========================================================
|
|
|
|
/// <summary>
|
|
/// Calcula ruta completa.
|
|
/// </summary>
|
|
private string? _proveedorSoloAPieRecalculo;
|
|
|
|
private string? ObtenerProveedorSoloAPieParaRecalculo(bool conservarSeleccion)
|
|
{
|
|
if (!conservarSeleccion)
|
|
_proveedorSoloAPieRecalculo = null;
|
|
else if (_indiceAlternativaSeleccionada >= 0 && _indiceAlternativaSeleccionada < _opcionesRuta.Count)
|
|
{
|
|
var seleccionada = _opcionesRuta[_indiceAlternativaSeleccionada];
|
|
_proveedorSoloAPieRecalculo = seleccionada.EsSoloAPie ? seleccionada.ProveedorRuta : null;
|
|
}
|
|
// Durante un calculo las opciones se vacian; otro cambio de punto conserva la preferencia.
|
|
return _proveedorSoloAPieRecalculo;
|
|
}
|
|
|
|
private async Task CalcularRutaCompletaAsync(bool conservarSeleccionSoloAPie = false)
|
|
{
|
|
if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null)
|
|
return;
|
|
|
|
var proveedorSoloAPiePreferido = ObtenerProveedorSoloAPieParaRecalculo(conservarSeleccionSoloAPie);
|
|
|
|
_mostrarElevacionesRuta = _mostrarElevacionesRutaConfiguracion;
|
|
|
|
var confirmarDerivacionHistorial = !_aplicandoRegistroHistorial &&
|
|
PrepararDerivacionHistorialParaRecalculo();
|
|
|
|
var cronometroCalculo = System.Diagnostics.Stopwatch.StartNew();
|
|
_panelRutasMovilContraido = _mostrarBuscadorMapa;
|
|
|
|
await CerrarTodasLasPreviewsAsync();
|
|
|
|
_ctsCalculo?.Cancel();
|
|
_ctsCalculo = new CancellationTokenSource();
|
|
var ct = _ctsCalculo.Token;
|
|
var omitirRegistroHistorialEsteCalculo = _omitirRegistroHistorialEnSiguienteCalculo;
|
|
_omitirRegistroHistorialEnSiguienteCalculo = false;
|
|
|
|
int versionActual = Interlocked.Increment(ref _versionCalculo);
|
|
IniciarNuevaVersionPintadoRuta();
|
|
|
|
ResetEstadoRuta(limpiarOpciones: true);
|
|
|
|
_mensajeRuta = "Calculando ruta…";
|
|
StateHasChanged();
|
|
|
|
await ClearRoutesJsAsync();
|
|
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
|
|
|
|
try
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
if (versionActual != _versionCalculo)
|
|
throw new OperationCanceledException();
|
|
|
|
var salidaBase = AhoraLocal();
|
|
_ultimaHoraReferenciaRuta = salidaBase;
|
|
|
|
var parametros = new ParametrosPlanificadorRuta
|
|
{
|
|
VelocidadPieKmH = _velocidadPieKmH,
|
|
FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie,
|
|
ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros,
|
|
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
|
|
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
|
|
UsarValhallaParaCaminatas = _walkProvider == ProveedorCaminata.Valhalla,
|
|
PoligonosExclusionCaminata = ObtenerPoligonosExclusionCaminata(),
|
|
VelocidadBusKmH = _velocidadBusKmH,
|
|
MinutosPorTransbordo = _minutosTransbordo,
|
|
MargenTransbordoSegundos = Math.Clamp(_margenTransbordoSegundos, 0, 1800),
|
|
SegundosPorParadaBus = _segundosPorParadaBus,
|
|
RadioParadasCandidatasMetros = _radioCandidatasMetros,
|
|
MaxParadasCandidatasPorLado = _maxCandidatasPorLado,
|
|
UsarOsrmParaRanking = _usarOsrmParaRanking,
|
|
ActivarRangoLineas = _activarRangoLineas,
|
|
LineaMin = _lineaMin,
|
|
LineaMax = _lineaMax,
|
|
LineasPermitidas = ObtenerLineasPermitidasSeleccionadas().ToList(),
|
|
ActivarRangoItinerarios = _activarRangoItinerarios,
|
|
ItinerarioMin = _itinerarioMin,
|
|
ItinerarioMax = _itinerarioMax,
|
|
FiltroSentidoItinerario = _filtroSentidoItinerario,
|
|
ActivarRangoTransbordos = _activarRangoTransbordos,
|
|
MinTransbordosPermitidos = _minTransbordosPermitidos,
|
|
MaxTransbordosPermitidos = _maxTransbordosPermitidos,
|
|
MaxParesEvaluados = _activarRangoItinerarios ? 280 : 140,
|
|
MaxAlternativasBusPorPar = (_activarRangoTransbordos && _minTransbordosPermitidos > 0)
|
|
? (_activarRangoItinerarios ? 8 : 6)
|
|
: (_activarRangoItinerarios ? 4 : 3),
|
|
UsarHorariosReales = _usarHorariosTeoricos,
|
|
FechaSalida = DateOnly.FromDateTime(salidaBase),
|
|
HoraSalida = TimeOnly.FromDateTime(salidaBase),
|
|
DescartarAlternativasSinHorario = false,
|
|
MaxEsperaInicialMinutos = 90,
|
|
MaxEsperaTransbordoMinutos = 60,
|
|
MaxEsperaTotalMinutos = 120,
|
|
PreferenciaEtiquetaRuta = _preferenciaEtiquetaRuta
|
|
};
|
|
|
|
var resultado = await PlanificadorRutasServicio.CalcularAsync(
|
|
_coordenadasOrigen,
|
|
_coordenadasDestino,
|
|
parametros,
|
|
ct);
|
|
|
|
ct.ThrowIfCancellationRequested();
|
|
if (versionActual != _versionCalculo)
|
|
throw new OperationCanceledException();
|
|
|
|
var alternativas = resultado.Alternativas;
|
|
var filtradas = alternativas
|
|
.Where(a => a.EsSoloAPie || a.AlternativaBus is null || !PrimerTramoBusEsBasura(a.AlternativaBus, _coordenadasOrigen, _coordenadasDestino))
|
|
.ToList();
|
|
|
|
if (filtradas.Count == 0 && alternativas.Count > 0)
|
|
filtradas = alternativas;
|
|
|
|
_opcionesRuta.Clear();
|
|
_opcionesRuta.AddRange(filtradas);
|
|
await CalcularEsfuerzoExtraPendienteAlternativasAsync(_opcionesRuta, ct);
|
|
OrdenarOpcionesRutaSegunCriterio();
|
|
|
|
if (!resultado.Ok || _opcionesRuta.Count == 0)
|
|
{
|
|
_mensajeRuta = string.IsNullOrWhiteSpace(resultado.Mensaje)
|
|
? "No hay rutas disponibles para esta combinacion."
|
|
: resultado.Mensaje;
|
|
_hayRutaValida = false;
|
|
try
|
|
{
|
|
await ActualizarResumenTecnicoRutaAsync(CancellationToken.None);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
_tiempoUltimoCalculoRuta = cronometroCalculo.Elapsed;
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
var idxPreferida = proveedorSoloAPiePreferido is not null
|
|
? _opcionesRuta.FindIndex(opcion => opcion.EsSoloAPie &&
|
|
string.Equals(opcion.ProveedorRuta, proveedorSoloAPiePreferido, StringComparison.OrdinalIgnoreCase))
|
|
: BuscarIndicePreferidoPorCriterio(_criterioPrioritarioRuta);
|
|
if (idxPreferida < 0 || idxPreferida >= _opcionesRuta.Count)
|
|
{
|
|
idxPreferida = BuscarIndicePreferidoPorCriterio(_criterioPrioritarioRuta);
|
|
if (idxPreferida < 0 || idxPreferida >= _opcionesRuta.Count)
|
|
idxPreferida = 0;
|
|
}
|
|
|
|
_indiceAlternativaSeleccionada = idxPreferida;
|
|
ResetEstadoRuta(limpiarOpciones: false, limpiarMensaje: false);
|
|
_mensajeRuta = "Representando ruta seleccionada...";
|
|
StateHasChanged();
|
|
await Task.Yield();
|
|
await PintarOpcionRutaCompleta(_opcionesRuta[idxPreferida]);
|
|
if (!omitirRegistroHistorialEsteCalculo)
|
|
{
|
|
await RegistrarEscenarioActualEnHistorialLocalAsync();
|
|
}
|
|
if (confirmarDerivacionHistorial)
|
|
ConfirmarDerivacionHistorial();
|
|
await ActualizarResumenTecnicoRutaAsync(CancellationToken.None);
|
|
_tiempoUltimoCalculoRuta = cronometroCalculo.Elapsed;
|
|
StateHasChanged();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_mensajeRuta = $"Error calculando ruta: {ex.Message}";
|
|
_hayRutaValida = false;
|
|
try
|
|
{
|
|
await ActualizarResumenTecnicoRutaAsync(CancellationToken.None);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
_tiempoUltimoCalculoRuta = cronometroCalculo.Elapsed;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
|
|
// =========================================================
|
|
// Pintado
|
|
// =========================================================
|
|
|
|
}
|