Files
RutasDBUS/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs
Pedro acbdd1501d añadidos atajos ctrl + alt + f para guardar datos de fabrica
añadidos atajos ctrl + alt + z para abrir zonas de exclusion
añadido superficio de terreno en el punto z
2026-09-08 14:29:35 +02:00

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
// =========================================================
}