Optimizacion de codigo, limpieza residual y actualizacionde hover de elevaciones
This commit is contained in:
@@ -29,9 +29,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
private bool _pintandoParadas = false;
|
private bool _pintandoParadas = false;
|
||||||
private Task? _tareaCargaRed;
|
private Task? _tareaCargaRed;
|
||||||
|
|
||||||
private readonly RealTimeMap.PointSymbol _simboloOrigen = new() { color = "green", fillColor = "green", radius = 8 };
|
|
||||||
private readonly RealTimeMap.PointSymbol _simboloDestino = new() { color = "red", fillColor = "red", radius = 8 };
|
|
||||||
|
|
||||||
private int _versionCalculo = 0;
|
private int _versionCalculo = 0;
|
||||||
private int _versionPintadoRuta = 0;
|
private int _versionPintadoRuta = 0;
|
||||||
private CancellationTokenSource? _ctsCalculo;
|
private CancellationTokenSource? _ctsCalculo;
|
||||||
@@ -511,23 +508,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
return stops;
|
return stops;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Construye el payload JS de paradas de un tramo de bus.
|
|
||||||
/// </summary>
|
|
||||||
private List<object> BuildStopsForTramoBus(TramoBus tramo)
|
|
||||||
=> BuildStopsDesdeParadas(BuildParadasForTramoBus(tramo));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registra paradas visibles de un itinerario activo para permitir popup aunque la capa general esté oculta.
|
|
||||||
/// </summary>
|
|
||||||
private void RegistrarParadasItinerarioVisibles(string id, IEnumerable<Parada> paradas)
|
|
||||||
{
|
|
||||||
_paradasItinerarioVisiblesPorId[id] = paradas
|
|
||||||
.GroupBy(p => p.Codigo, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(g => g.First())
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Elimina el registro de paradas visibles de un itinerario activo.
|
/// Elimina el registro de paradas visibles de un itinerario activo.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -638,53 +618,12 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona linea completa variante.
|
|
||||||
/// </summary>
|
|
||||||
private double[,]? ConstruirLineaCompletaVariante(InfoVariante v)
|
|
||||||
{
|
|
||||||
if (v is null || v.CodigosParadas is null || v.CodigosParadas.Count < 2) return null;
|
|
||||||
|
|
||||||
var pts = new List<double[]>(v.CodigosParadas.Count);
|
|
||||||
|
|
||||||
foreach (var cod in v.CodigosParadas)
|
|
||||||
{
|
|
||||||
if (_paradaByCode.TryGetValue(cod, out var p))
|
|
||||||
pts.Add(new[] { p.Latitud, p.Longitud });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pts.Count < 2) return null;
|
|
||||||
|
|
||||||
var arr = new double[pts.Count, 2];
|
|
||||||
for (int i = 0; i < pts.Count; i++)
|
|
||||||
{
|
|
||||||
arr[i, 0] = pts[i][0];
|
|
||||||
arr[i, 1] = pts[i][1];
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ✅ Rango de líneas
|
// ✅ Rango de líneas
|
||||||
private bool _activarRangoLineas = false;
|
private bool _activarRangoLineas = false;
|
||||||
private int _lineaMin = 1;
|
private int _lineaMin = 1;
|
||||||
private int _lineaMax = 200;
|
private int _lineaMax = 200;
|
||||||
private readonly HashSet<int> _lineasPermitidas = new();
|
private readonly HashSet<int> _lineasPermitidas = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona dentro rango.
|
|
||||||
/// </summary>
|
|
||||||
private bool LineaDentroRango(InfoVariante v)
|
|
||||||
{
|
|
||||||
if (!_activarRangoLineas) return true;
|
|
||||||
|
|
||||||
int codLinea = CodigoLineaAsInt(v); // ya lo tienes hecho
|
|
||||||
if (codLinea == int.MaxValue) return false; // si no se puede parsear, fuera
|
|
||||||
|
|
||||||
return _lineasPermitidas.Contains(codLinea);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -859,19 +798,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
linea[0, 1] = punto[1];
|
linea[0, 1] = punto[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sobrescribe el ultimo punto de una linea.
|
|
||||||
/// </summary>
|
|
||||||
private static void EstablecerUltimoPunto(double[,]? linea, double[]? punto)
|
|
||||||
{
|
|
||||||
if (linea is null || punto is null || linea.GetLength(0) == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var ultimo = linea.GetLength(0) - 1;
|
|
||||||
linea[ultimo, 0] = punto[0];
|
|
||||||
linea[ultimo, 1] = punto[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Obtiene la distancia entre dos puntos geograficos.
|
/// Obtiene la distancia entre dos puntos geograficos.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1617,45 +1543,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
? alternativa.Etiqueta
|
? alternativa.Etiqueta
|
||||||
: $"Opcion {indice + 1}";
|
: $"Opcion {indice + 1}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtiene las siglas compactas de categorías adicionales para una alternativa final.
|
|
||||||
/// </summary>
|
|
||||||
private List<string> ObtenerSiglasExtrasAlternativa(AlternativaRuta alternativa)
|
|
||||||
{
|
|
||||||
if (alternativa.EsSoloAPie)
|
|
||||||
return new();
|
|
||||||
|
|
||||||
var alternativasBus = _opcionesRuta
|
|
||||||
.Where(x => !x.EsSoloAPie && x.AlternativaBus is not null)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (alternativasBus.Count == 0)
|
|
||||||
return new();
|
|
||||||
|
|
||||||
var extras = new List<string>(2);
|
|
||||||
var etiqueta = alternativa.Etiqueta ?? string.Empty;
|
|
||||||
|
|
||||||
var minCoste = alternativasBus.Min(x => x.PuntuacionCoste);
|
|
||||||
if (!EtiquetaPrincipalEs(etiqueta, "Más rápida") && alternativa.PuntuacionCoste <= minCoste + 0.001)
|
|
||||||
extras.Add("MR");
|
|
||||||
|
|
||||||
var minTransbordos = alternativasBus.Min(x => x.NumeroTransbordosReales);
|
|
||||||
if (!EtiquetaPrincipalEs(etiqueta, "Menos transbordos") && alternativa.NumeroTransbordosReales == minTransbordos)
|
|
||||||
extras.Add("MT");
|
|
||||||
|
|
||||||
var minDistanciaPie = alternativasBus.Min(x => x.DistanciaPieTotalAprox);
|
|
||||||
if (!EtiquetaPrincipalEs(etiqueta, "Menos andar") && alternativa.DistanciaPieTotalAprox <= minDistanciaPie + 0.001)
|
|
||||||
extras.Add("MA");
|
|
||||||
|
|
||||||
return extras;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Comprueba si la etiqueta principal ya corresponde a una categoría destacada concreta.
|
|
||||||
/// </summary>
|
|
||||||
private static bool EtiquetaPrincipalEs(string? etiquetaActual, string etiquetaEsperada)
|
|
||||||
=> string.Equals(etiquetaActual?.Trim(), etiquetaEsperada, StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -289,22 +289,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indica si match wildcard.
|
|
||||||
/// </summary>
|
|
||||||
private static bool MatchWildcard(string input, string pattern)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pattern)) return true;
|
|
||||||
if (string.IsNullOrEmpty(input)) return false;
|
|
||||||
|
|
||||||
// escapamos regex y convertimos comodines
|
|
||||||
var rx = "^" + Regex.Escape(pattern.Trim())
|
|
||||||
.Replace("\\*", ".*")
|
|
||||||
.Replace("\\?", ".") + "$";
|
|
||||||
|
|
||||||
return Regex.IsMatch(input, rx, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Parada? _paradaPrimerClickPendiente;
|
private Parada? _paradaPrimerClickPendiente;
|
||||||
|
|
||||||
// Detecta si el click cae realmente sobre el círculo de una parada (en píxeles)
|
// Detecta si el click cae realmente sobre el círculo de una parada (en píxeles)
|
||||||
@@ -553,24 +537,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
private int _minTransbordosPermitidos = 0;
|
private int _minTransbordosPermitidos = 0;
|
||||||
private int _maxTransbordosPermitidos = 3;
|
private int _maxTransbordosPermitidos = 3;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indica si cumple filtro transbordos.
|
|
||||||
/// </summary>
|
|
||||||
private bool CumpleFiltroTransbordos(int transbordos)
|
|
||||||
{
|
|
||||||
if (!_activarRangoTransbordos)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
int minT = Math.Max(0, _minTransbordosPermitidos);
|
|
||||||
int maxT = Math.Max(0, _maxTransbordosPermitidos);
|
|
||||||
|
|
||||||
// por si el usuario mete al revés
|
|
||||||
if (minT > maxT) (minT, maxT) = (maxT, minT);
|
|
||||||
|
|
||||||
return transbordos >= minT && transbordos <= maxT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indica si esta popup puntos abierto.
|
/// Indica si esta popup puntos abierto.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -639,15 +605,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
var idx = Math.Abs(StableHash(key)) % _palette.Length;
|
var idx = Math.Abs(StableHash(key)) % _palette.Length;
|
||||||
return _palette[idx];
|
return _palette[idx];
|
||||||
}
|
}
|
||||||
/// <summary>
|
|
||||||
/// Gestiona color clave for variante.
|
|
||||||
/// </summary>
|
|
||||||
private static string ColorKeyForVariante(InfoVariante v)
|
|
||||||
{
|
|
||||||
var linea = v.Linea.Codigo?.ToString() ?? "?";
|
|
||||||
var itin = v.CodigoItinerario.ToString(CultureInfo.InvariantCulture);
|
|
||||||
return $"{linea}.{itin}";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private string _itFiltro = "";
|
private string _itFiltro = "";
|
||||||
@@ -678,15 +635,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
_selectItKey++;
|
_selectItKey++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona it filtro busqueda.
|
|
||||||
/// </summary>
|
|
||||||
private void OnItFiltroSearch(ChangeEventArgs e)
|
|
||||||
{
|
|
||||||
ItFiltro = e?.Value?.ToString() ?? string.Empty;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
private IEnumerable<ElementoItinerario> _itItemsFiltrados =>
|
private IEnumerable<ElementoItinerario> _itItemsFiltrados =>
|
||||||
string.IsNullOrWhiteSpace(_itFiltro)
|
string.IsNullOrWhiteSpace(_itFiltro)
|
||||||
? _itItems
|
? _itItems
|
||||||
@@ -882,23 +830,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indica si match wildcard contains.
|
|
||||||
/// </summary>
|
|
||||||
private static bool MatchWildcardContains(string input, string pattern)
|
|
||||||
{
|
|
||||||
// en modo “contains”, envolvemos con *...*
|
|
||||||
if (!pattern.StartsWith("*")) pattern = "*" + pattern;
|
|
||||||
if (!pattern.EndsWith("*")) pattern = pattern + "*";
|
|
||||||
|
|
||||||
var rx = Regex.Escape(pattern)
|
|
||||||
.Replace("\\*", ".*")
|
|
||||||
.Replace("\\?", ".");
|
|
||||||
|
|
||||||
return Regex.IsMatch(input, rx, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1214,17 +1145,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
return resultado;
|
return resultado;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona itis de alternativa.
|
|
||||||
/// </summary>
|
|
||||||
private string ItisDeAlternativa(AlternativaRuta alt)
|
|
||||||
{
|
|
||||||
if (alt is null || alt.EsSoloAPie || alt.AlternativaBus?.Tramos is null)
|
|
||||||
return "";
|
|
||||||
|
|
||||||
return TextoLineasDeTramos(alt.AlternativaBus.Tramos);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1261,7 +1181,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
private bool _elevPopupMinimizado = false;
|
private bool _elevPopupMinimizado = false;
|
||||||
|
|
||||||
|
|
||||||
private const string _versionApp = "(v-20260713a)";
|
private const string _versionApp = "(v-20260714a)";
|
||||||
|
|
||||||
|
|
||||||
private double? _paradaElevG;
|
private double? _paradaElevG;
|
||||||
|
|||||||
@@ -18,26 +18,6 @@ namespace RutasDBUS.Components.Pages;
|
|||||||
|
|
||||||
public partial class PlanificadorRutas : ComponentBase
|
public partial class PlanificadorRutas : ComponentBase
|
||||||
{
|
{
|
||||||
// =========================================================
|
|
||||||
// Itinerarios: rango (estricto durante BFS)
|
|
||||||
// =========================================================
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona dentro rango.
|
|
||||||
/// </summary>
|
|
||||||
private bool ItinerarioDentroRango(int codigoItinerario)
|
|
||||||
{
|
|
||||||
if (!_activarRangoItinerarios) return true;
|
|
||||||
int min = Math.Min(_itinerarioMin, _itinerarioMax);
|
|
||||||
int max = Math.Max(_itinerarioMin, _itinerarioMax);
|
|
||||||
return codigoItinerario >= min && codigoItinerario <= max;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona permitida.
|
|
||||||
/// </summary>
|
|
||||||
private bool VariantePermitida(InfoVariante v)
|
|
||||||
=> ItinerarioDentroRango(v.CodigoItinerario) && LineaDentroRango(v);
|
|
||||||
|
|
||||||
private IReadOnlyList<int> ObtenerLineasPermitidasSeleccionadas()
|
private IReadOnlyList<int> ObtenerLineasPermitidasSeleccionadas()
|
||||||
=> _lineasPermitidas
|
=> _lineasPermitidas
|
||||||
@@ -121,158 +101,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
_busFilteredCache.Clear();
|
_busFilteredCache.Clear();
|
||||||
await PersistirConfiguracionAsync();
|
await PersistirConfiguracionAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =========================================================
|
|
||||||
// Selección de candidatas: por ubicación, pero SOLO si están
|
|
||||||
// en itinerarios permitidos
|
|
||||||
// =========================================================
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona tiene aparicion permitida.
|
|
||||||
/// </summary>
|
|
||||||
private bool ParadaTieneAparicionPermitida(string codigoParada)
|
|
||||||
{
|
|
||||||
if (!_indiceParadas.TryGetValue(codigoParada, out var apps)) return false;
|
|
||||||
foreach (var a in apps)
|
|
||||||
if (VariantePermitida(a.Variante))
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Selecciona candidatas solo itinerarios.
|
|
||||||
/// </summary>
|
|
||||||
private async Task<List<CandidataParada>> SeleccionarCandidatasSoloItinerariosAsync(double lat, double lon, CancellationToken ct)
|
|
||||||
{
|
|
||||||
EnsureBusIndexBuilt();
|
|
||||||
|
|
||||||
int radio = Math.Clamp(_radioCandidatasMetros, 200, 3500);
|
|
||||||
int k = Math.Clamp(_maxCandidatasPorLado, 3, 20);
|
|
||||||
|
|
||||||
// 1) pool por grid
|
|
||||||
var pool = QueryParadasEnRadio(lat, lon, radio);
|
|
||||||
|
|
||||||
// 2) filtrar: solo paradas que estén en itinerarios permitidos
|
|
||||||
var tmp = new List<CandidataParada>(pool.Count);
|
|
||||||
|
|
||||||
foreach (var p in pool)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if ((_activarRangoItinerarios || _activarRangoLineas) && !ParadaTieneAparicionPermitida(p.Codigo))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
|
|
||||||
var d = CalcularDistanciaMetros(lat, lon, p.Latitud, p.Longitud);
|
|
||||||
if (d <= radio * 1.9)
|
|
||||||
{
|
|
||||||
tmp.Add(new CandidataParada
|
|
||||||
{
|
|
||||||
Parada = p,
|
|
||||||
DistEucl = d,
|
|
||||||
DistWalk = d,
|
|
||||||
TiempoWalk = EstimarDuracionPieSegundos(d) // ✅ SIEMPRE con velocidad configurada
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp.Sort((a, b) => a.DistEucl.CompareTo(b.DistEucl));
|
|
||||||
if (tmp.Count == 0) return tmp;
|
|
||||||
|
|
||||||
// Pre-take (barato)
|
|
||||||
int preTake = Math.Min(32, Math.Max(k + 10, k * 3));
|
|
||||||
if (tmp.Count > preTake) tmp = tmp.Take(preTake).ToList();
|
|
||||||
|
|
||||||
// En estricto: NO usamos OSRM para ranking (por rendimiento)
|
|
||||||
if (!_usarOsrmParaRanking || _activarRangoItinerarios)
|
|
||||||
return tmp.Take(k).ToList();
|
|
||||||
|
|
||||||
// Refinar con OSRM con concurrencia limitada (pero tiempo a pie = distancia / velocidad)
|
|
||||||
var sem = new SemaphoreSlim(4, 4);
|
|
||||||
var tasks = new List<Task>(tmp.Count);
|
|
||||||
|
|
||||||
foreach (var c in tmp)
|
|
||||||
{
|
|
||||||
tasks.Add(Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await sem.WaitAsync(ct);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
var (_, dist, _) = await WalkCached(lat, lon, c.Parada.Latitud, c.Parada.Longitud);
|
|
||||||
if (dist > 0)
|
|
||||||
{
|
|
||||||
c.DistWalk = dist;
|
|
||||||
c.TiempoWalk = EstimarDuracionPieSegundos(dist); // ✅ velocidad configurada
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
finally { sem.Release(); }
|
|
||||||
}, ct));
|
|
||||||
}
|
|
||||||
|
|
||||||
try { await Task.WhenAll(tasks); } catch { }
|
|
||||||
|
|
||||||
return tmp
|
|
||||||
.OrderBy(x => x.TiempoWalk)
|
|
||||||
.ThenBy(x => x.DistEucl)
|
|
||||||
.Take(k)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// =========================================================
|
|
||||||
// ELEVACIÓN (Open-Elevation) — solo tramos andando
|
|
||||||
// =========================================================
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona alt.
|
|
||||||
/// </summary>
|
|
||||||
private static string FmtAlt(double? v) => v is null ? "-" : $"{Math.Round(v.Value):0} m";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Formatea altitudes tramo.
|
|
||||||
/// </summary>
|
|
||||||
private static string FormatearAltitudesTramo(double? ini, double? fin, double? min, double? max)
|
|
||||||
{
|
|
||||||
var sb = new System.Text.StringBuilder();
|
|
||||||
|
|
||||||
sb.Append("<br /><div style='margin-left:1.25rem'>");
|
|
||||||
sb.Append($"<div>Altitud:</div>");
|
|
||||||
sb.Append($"<div style='margin-left:0.75rem'>• (ini→fin) = {FmtAlt(ini)} → {FmtAlt(fin)}</div>");
|
|
||||||
|
|
||||||
if (min is not null && max is not null)
|
|
||||||
sb.Append($"<div style='margin-left:0.75rem'>• (min/max) = {Math.Round(min.Value):0} m – {Math.Round(max.Value):0} m</div>");
|
|
||||||
|
|
||||||
sb.Append("</div>");
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtiene elevacion punto.
|
|
||||||
/// </summary>
|
|
||||||
private async Task<double?> GetElevationPointAsync(double lat, double lon, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var k = ElevKey(lat, lon);
|
|
||||||
|
|
||||||
if (_elevCache.TryGetValue(k, out var cached))
|
|
||||||
return cached;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await ObtenerElevacionesPorLoteAsync(new List<(double lat, double lon)> { (lat, lon) }, ct);
|
|
||||||
return _elevCache.TryGetValue(ElevKey(lat, lon), out var elev) ? elev : (double?)null;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Actualiza las alturas (Z) de origen y destino para el resumen del detalle de ruta.
|
/// Actualiza las alturas (Z) de origen y destino para el resumen del detalle de ruta.
|
||||||
@@ -768,65 +596,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(double? min, double? max)> GetElevationMinMaxForLineAsync(double[,]? line, CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (line is null) return (null, null);
|
|
||||||
if (line.GetLength(0) <= 0) return (null, null);
|
|
||||||
|
|
||||||
var sampled = SampleLine(line, _elevMaxPuntosPorTramo);
|
|
||||||
if (sampled.Count == 0) return (null, null);
|
|
||||||
|
|
||||||
// misses
|
|
||||||
var misses = new List<(double lat, double lon)>(sampled.Count);
|
|
||||||
foreach (var (lat, lon) in sampled)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
var k = ElevKey(lat, lon);
|
|
||||||
if (!_elevCache.ContainsKey(k))
|
|
||||||
{
|
|
||||||
// redondeo para que cachee mejor y mande menos variedad
|
|
||||||
misses.Add((Math.Round(lat, ELEV_DECIMALES_CACHE), Math.Round(lon, ELEV_DECIMALES_CACHE)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pedir en lotes
|
|
||||||
if (misses.Count > 0)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < misses.Count; i += ELEV_BATCH_MAX_PUNTOS)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
var batch = misses.Skip(i).Take(ELEV_BATCH_MAX_PUNTOS).ToList();
|
|
||||||
await ObtenerElevacionesPorLoteAsync(batch, ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// calcular min/max con lo disponible
|
|
||||||
double? min = null;
|
|
||||||
double? max = null;
|
|
||||||
|
|
||||||
foreach (var (lat, lon) in sampled)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
var k = ElevKey(lat, lon);
|
|
||||||
if (_elevCache.TryGetValue(k, out var elev))
|
|
||||||
{
|
|
||||||
min = (min is null) ? elev : Math.Min(min.Value, elev);
|
|
||||||
max = (max is null) ? elev : Math.Max(max.Value, elev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (min, max);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Formatea elevacion minimo maximo.
|
|
||||||
/// </summary>
|
|
||||||
private static string FormatearElevacionMinMax(double? min, double? max)
|
|
||||||
{
|
|
||||||
if (min is null || max is null) return "";
|
|
||||||
return $" · Altitud: {Math.Round(min.Value):0}-{Math.Round(max.Value):0} m";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gestiona geometria completa variante.
|
/// Gestiona geometria completa variante.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1115,167 +884,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
return tramos;
|
return tramos;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Busca rutas bus alternativas filtradas.
|
|
||||||
/// </summary>
|
|
||||||
private IReadOnlyList<RutaBusAlternativa> BuscarRutasBusAlternativasFiltradas(
|
|
||||||
string codigoParadaOrigen,
|
|
||||||
string codigoParadaDestino,
|
|
||||||
int maxAlternativas)
|
|
||||||
{
|
|
||||||
EnsureBusIndexBuilt();
|
|
||||||
|
|
||||||
// -----------------------------------
|
|
||||||
// RANGO transbordos (único):
|
|
||||||
// - maxTrans entra al BFS como poda (si rango activo)
|
|
||||||
// - minTrans se aplica solo como criterio de aceptación
|
|
||||||
// -----------------------------------
|
|
||||||
int minTrans = 0;
|
|
||||||
int? maxTrans = null;
|
|
||||||
|
|
||||||
if (_activarRangoTransbordos)
|
|
||||||
{
|
|
||||||
minTrans = Math.Max(0, _minTransbordosPermitidos);
|
|
||||||
var maxT = Math.Max(0, _maxTransbordosPermitidos);
|
|
||||||
|
|
||||||
if (minTrans > maxT) (minTrans, maxT) = (maxT, minTrans);
|
|
||||||
|
|
||||||
maxTrans = maxT; // ✅ poda BFS
|
|
||||||
}
|
|
||||||
|
|
||||||
int minIt = Math.Min(_itinerarioMin, _itinerarioMax);
|
|
||||||
int maxIt = Math.Max(_itinerarioMin, _itinerarioMax);
|
|
||||||
|
|
||||||
int minLin = Math.Min(_lineaMin, _lineaMax);
|
|
||||||
int maxLin = Math.Max(_lineaMin, _lineaMax);
|
|
||||||
var lineasPermitidas = ObtenerLineasPermitidasSeleccionadas();
|
|
||||||
var lineasPermitidasKey = string.Join(",", lineasPermitidas);
|
|
||||||
|
|
||||||
// ✅ Cache key incluye rango de transbordos (min/max)
|
|
||||||
var key =
|
|
||||||
$"{codigoParadaOrigen}|{codigoParadaDestino}" +
|
|
||||||
$"|itR:{(_activarRangoItinerarios ? 1 : 0)}|{minIt}|{maxIt}" +
|
|
||||||
$"|lnR:{(_activarRangoLineas ? 1 : 0)}|{minLin}|{maxLin}|lnL:{lineasPermitidasKey}" +
|
|
||||||
$"|alts:{maxAlternativas}" +
|
|
||||||
$"|txR:{(_activarRangoTransbordos ? 1 : 0)}|minT:{minTrans}|maxT:{(maxTrans ?? -1)}";
|
|
||||||
|
|
||||||
if (_busFilteredCache.TryGetValue(key, out var cached))
|
|
||||||
return cached;
|
|
||||||
|
|
||||||
var res = new List<RutaBusAlternativa>(Math.Max(1, maxAlternativas));
|
|
||||||
if (maxAlternativas <= 0)
|
|
||||||
{
|
|
||||||
_busFilteredCache[key] = res;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtro por itinerarios/lineas durante BFS
|
|
||||||
Func<InfoVariante, bool> filtro = _ => true;
|
|
||||||
if (_activarRangoItinerarios || _activarRangoLineas)
|
|
||||||
filtro = VariantePermitida;
|
|
||||||
|
|
||||||
string FirmaRuta(List<TramoBus> tramos) =>
|
|
||||||
string.Join(";", tramos.Select(t =>
|
|
||||||
$"{t.Variante.Linea.Codigo}-{t.Variante.CodigoItinerario}-{NormalizarSentidoCodigo(t.Variante.Sentido?.Codigo)}-{t.Variante.Variante.Codigo}-{t.IndiceInicio}-{t.IndiceFin}"));
|
|
||||||
|
|
||||||
// Firmas para evitar duplicados
|
|
||||||
var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
// Cola de "prohibiciones" para forzar alternativas
|
|
||||||
var prohibicionesPendientes = new Queue<HashSet<InfoVariante>>();
|
|
||||||
prohibicionesPendientes.Enqueue(new HashSet<InfoVariante>()); // principal
|
|
||||||
|
|
||||||
// Evitar repetir exactamente la misma combinación de variantes prohibidas
|
|
||||||
var firmasProhibiciones = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
firmasProhibiciones.Add(""); // vacío = principal
|
|
||||||
|
|
||||||
// Límite de seguridad para no explotar tiempo
|
|
||||||
// Si hay mínimo > 0, damos más margen de exploración
|
|
||||||
int limiteIntentos = (_activarRangoTransbordos && minTrans > 0) ? 10 : 12;
|
|
||||||
int intentos = 0;
|
|
||||||
|
|
||||||
while (prohibicionesPendientes.Count > 0 &&
|
|
||||||
res.Count < maxAlternativas &&
|
|
||||||
intentos < limiteIntentos)
|
|
||||||
{
|
|
||||||
intentos++;
|
|
||||||
|
|
||||||
var prohibidas = prohibicionesPendientes.Dequeue();
|
|
||||||
|
|
||||||
bool FiltroConProhibidas(InfoVariante v)
|
|
||||||
=> filtro(v) && !prohibidas.Contains(v);
|
|
||||||
|
|
||||||
var ruta = BuscarRutaBusConFiltro(
|
|
||||||
codigoParadaOrigen,
|
|
||||||
codigoParadaDestino,
|
|
||||||
FiltroConProhibidas,
|
|
||||||
out var trans,
|
|
||||||
maxTrans);
|
|
||||||
|
|
||||||
if (ruta is null || ruta.Count == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var firma = FirmaRuta(ruta);
|
|
||||||
|
|
||||||
// Si la ruta ya la vimos, no la añadimos
|
|
||||||
if (!firmas.Add(firma))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
double dist;
|
|
||||||
_ = RedBusServicio.ConstruirLineasBus(ruta, out dist);
|
|
||||||
|
|
||||||
var alt = new RutaBusAlternativa
|
|
||||||
{
|
|
||||||
Tramos = ruta,
|
|
||||||
CodigosParadasTransbordo = trans,
|
|
||||||
NumeroTransbordos = UtilidadesTransitoDbus.ContarTransbordosReales(ruta),
|
|
||||||
DistanciaTotalBus = dist
|
|
||||||
};
|
|
||||||
|
|
||||||
// ✅ Aceptar solo si cumple rango (min/max)
|
|
||||||
if (CumpleFiltroTransbordos(alt.NumeroTransbordos))
|
|
||||||
{
|
|
||||||
res.Add(alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Seguir explorando alternativas:
|
|
||||||
// generar nuevas búsquedas prohibiendo una variante de esta ruta
|
|
||||||
var variantesRuta = ruta.Select(t => t.Variante).Distinct().ToList();
|
|
||||||
|
|
||||||
foreach (var vProhibir in variantesRuta)
|
|
||||||
{
|
|
||||||
if (prohibicionesPendientes.Count >= 20)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var nuevoSet = new HashSet<InfoVariante>(prohibidas);
|
|
||||||
nuevoSet.Add(vProhibir);
|
|
||||||
|
|
||||||
// firma estable de prohibiciones (por identidad lógica)
|
|
||||||
var firmaProh = string.Join("|",
|
|
||||||
nuevoSet
|
|
||||||
.Select(v =>
|
|
||||||
$"{v.Linea.Codigo}-{v.CodigoItinerario}-{NormalizarSentidoCodigo(v.Sentido?.Codigo)}-{v.Variante.Codigo}")
|
|
||||||
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (firmasProhibiciones.Add(firmaProh))
|
|
||||||
prohibicionesPendientes.Enqueue(nuevoSet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_busFilteredCache[key] = res;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================
|
|
||||||
// Deduplicación fuerte + coste
|
|
||||||
// =========================================================
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona iv.
|
|
||||||
/// </summary>
|
|
||||||
private static string SentidoIV(int indiceInicio, int indiceFin)
|
|
||||||
=> indiceFin >= indiceInicio ? "I" : "V";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Normaliza sentido codigo.
|
/// Normaliza sentido codigo.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1335,29 +943,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
return UtilidadesTransitoDbus.ContarTransbordosReales(alternativa?.Tramos);
|
return UtilidadesTransitoDbus.ContarTransbordosReales(alternativa?.Tramos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona aprox seg.
|
|
||||||
/// </summary>
|
|
||||||
private double CosteAproxSeg(AlternativaRuta alt)
|
|
||||||
{
|
|
||||||
if (alt.EsSoloAPie)
|
|
||||||
return alt.DuracionTotalAproxSegundos;
|
|
||||||
|
|
||||||
double t = 0;
|
|
||||||
t += alt.TiempoPieInicio;
|
|
||||||
t += alt.TiempoPieFin;
|
|
||||||
double extraParadas = 0;
|
|
||||||
foreach (var tb in alt.AlternativaBus!.Tramos)
|
|
||||||
extraParadas += ContarParadasRecorridas(tb) * Math.Max(0, _segundosPorParadaBus);
|
|
||||||
|
|
||||||
t += EstimarDuracionBusSegundos(alt.AlternativaBus!.DistanciaTotalBus) + extraParadas;
|
|
||||||
|
|
||||||
t += alt.NumeroTransbordosReales * (_minutosTransbordo * 60.0);
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// Acciones UI
|
// Acciones UI
|
||||||
// =========================================================
|
// =========================================================
|
||||||
@@ -1629,186 +1214,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================
|
|
||||||
// Duración exacta (tarjetas = detalle) + velocidad a pie configurada
|
|
||||||
// =========================================================
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calcula duracion exacta.
|
|
||||||
/// </summary>
|
|
||||||
private async Task<double> CalcularDuracionExactaAsync(AlternativaRuta alt, CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (alt.EsSoloAPie)
|
|
||||||
return alt.DuracionSoloAPie > 0 ? alt.DuracionSoloAPie : alt.DuracionTotalAproxSegundos;
|
|
||||||
|
|
||||||
var tramosBus = alt.AlternativaBus!.Tramos;
|
|
||||||
|
|
||||||
// 0) Hora base (real/simulada)
|
|
||||||
var tCursor = AhoraLocal();
|
|
||||||
|
|
||||||
// Si horarios ON, asegúrate de tenerlos cargados
|
|
||||||
if (_usarHorariosTeoricos)
|
|
||||||
await EnsureHorariosLoadedAsync();
|
|
||||||
|
|
||||||
// 1) Resolver paradas reales inicio/fin de bus
|
|
||||||
Parada paradaInicioBus = alt.OrigenParada;
|
|
||||||
Parada paradaFinBus = alt.DestinoParada;
|
|
||||||
|
|
||||||
if (tramosBus.Count > 0)
|
|
||||||
{
|
|
||||||
var primerTramo = tramosBus.First();
|
|
||||||
var ultimoTramo = tramosBus.Last();
|
|
||||||
|
|
||||||
string codIniReal = primerTramo.Variante.CodigosParadas[primerTramo.IndiceInicio];
|
|
||||||
string codFinReal = ultimoTramo.Variante.CodigosParadas[ultimoTramo.IndiceFin];
|
|
||||||
|
|
||||||
paradaInicioBus = _paradaByCode.TryGetValue(codIniReal, out var pIni) ? pIni : alt.OrigenParada;
|
|
||||||
paradaFinBus = _paradaByCode.TryGetValue(codFinReal, out var pFin) ? pFin : alt.DestinoParada;
|
|
||||||
}
|
|
||||||
|
|
||||||
double total = 0;
|
|
||||||
|
|
||||||
// ✅ resetea distancias (para “Menos andar”, etc.)
|
|
||||||
alt.DistanciaPieInicioAprox = 0;
|
|
||||||
alt.DistanciaPieTransbordosAprox = 0;
|
|
||||||
alt.DistanciaPieFinAprox = 0;
|
|
||||||
|
|
||||||
// 2) Origen -> inicio bus (walk)
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
var w1 = await WalkCached(_coordenadasOrigen![0], _coordenadasOrigen[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
|
|
||||||
|
|
||||||
double distPie1 = (w1.dist > 0) ? w1.dist
|
|
||||||
: CalcularDistanciaMetros(_coordenadasOrigen[0], _coordenadasOrigen[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
|
|
||||||
|
|
||||||
alt.DistanciaPieInicioAprox = distPie1;
|
|
||||||
var durPie1 = EstimarDuracionPieSegundos(distPie1);
|
|
||||||
|
|
||||||
tCursor = AddSecondsSafe(tCursor, durPie1);
|
|
||||||
total += durPie1;
|
|
||||||
|
|
||||||
// 3) BUS tramo a tramo (con esperas reales si horarios OK)
|
|
||||||
Parada paradaUltimaBus = paradaInicioBus;
|
|
||||||
|
|
||||||
for (int i = 0; i < tramosBus.Count; i++)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var tramo = tramosBus[i];
|
|
||||||
var v = tramo.Variante;
|
|
||||||
|
|
||||||
string codParadaIni = v.CodigosParadas[tramo.IndiceInicio];
|
|
||||||
string codParadaFin = v.CodigosParadas[tramo.IndiceFin];
|
|
||||||
|
|
||||||
var pIniTramo = _paradaByCode.TryGetValue(codParadaIni, out var p0) ? p0 : paradaUltimaBus;
|
|
||||||
var pFinTramo = _paradaByCode.TryGetValue(codParadaFin, out var p1) ? p1 : paradaUltimaBus;
|
|
||||||
|
|
||||||
// ---- Transbordo (si i>0): caminar si cambia parada + penalización fija ----
|
|
||||||
if (i > 0)
|
|
||||||
{
|
|
||||||
if (!paradaUltimaBus.Codigo.Equals(codParadaIni, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var wt = await WalkCached(
|
|
||||||
paradaUltimaBus.Latitud, paradaUltimaBus.Longitud,
|
|
||||||
pIniTramo.Latitud, pIniTramo.Longitud);
|
|
||||||
|
|
||||||
double distT = (wt.dist > 0) ? wt.dist
|
|
||||||
: CalcularDistanciaMetros(
|
|
||||||
paradaUltimaBus.Latitud, paradaUltimaBus.Longitud,
|
|
||||||
pIniTramo.Latitud, pIniTramo.Longitud);
|
|
||||||
|
|
||||||
alt.DistanciaPieTransbordosAprox += distT;
|
|
||||||
|
|
||||||
var durWalkT = EstimarDuracionPieSegundos(distT);
|
|
||||||
tCursor = AddSecondsSafe(tCursor, durWalkT);
|
|
||||||
total += durWalkT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Bus con horarios reales ----
|
|
||||||
bool usoHorarios = _usarHorariosTeoricos && _horariosOk;
|
|
||||||
|
|
||||||
if (usoHorarios)
|
|
||||||
{
|
|
||||||
var fecha = DateOnly.FromDateTime(tCursor);
|
|
||||||
var horaMin = TimeOnly.FromDateTime(tCursor);
|
|
||||||
|
|
||||||
string lineaCod = LineaCodigoStr(v);
|
|
||||||
int itin = v.CodigoItinerario;
|
|
||||||
string sentido = NormalizarSentidoCodigo(v.Sentido?.Codigo);
|
|
||||||
|
|
||||||
if (Horarios.TryGetNextBusSegment(
|
|
||||||
codParadaIni, codParadaFin,
|
|
||||||
lineaCod, itin, sentido,
|
|
||||||
fecha, horaMin,
|
|
||||||
out var dep, out var arr, out var _))
|
|
||||||
{
|
|
||||||
// Espera real si llegas antes de la salida
|
|
||||||
if (dep > tCursor)
|
|
||||||
{
|
|
||||||
var wait = (dep - tCursor).TotalSeconds;
|
|
||||||
if (wait > 0)
|
|
||||||
{
|
|
||||||
tCursor = dep;
|
|
||||||
total += wait;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duración real bus
|
|
||||||
var durBus = (arr - dep).TotalSeconds;
|
|
||||||
if (durBus < 0) durBus = 0;
|
|
||||||
|
|
||||||
tCursor = AddSecondsSafe(tCursor, durBus);
|
|
||||||
total += durBus;
|
|
||||||
|
|
||||||
paradaUltimaBus = pFinTramo;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// si falla el tramo, caemos al fallback geométrico
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Fallback (como antes) ----
|
|
||||||
// (geometría + segundos por parada)
|
|
||||||
double distanciaTramo = 0;
|
|
||||||
{
|
|
||||||
// construir línea geom para este tramo (como haces en PintarRutaDesdeAlternativa)
|
|
||||||
double distBusTotal;
|
|
||||||
var lineasBus = RedBusServicio.ConstruirLineasBus(tramosBus, out distBusTotal);
|
|
||||||
var lineaGeom = lineasBus[i];
|
|
||||||
|
|
||||||
for (int p = 1; p < lineaGeom.GetLength(0); p++)
|
|
||||||
distanciaTramo += CalcularDistanciaMetros(lineaGeom[p - 1, 0], lineaGeom[p - 1, 1], lineaGeom[p, 0], lineaGeom[p, 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
int paradasTramo = ContarParadasRecorridas(tramo);
|
|
||||||
double extraParadasSeg = paradasTramo * Math.Max(0, _segundosPorParadaBus);
|
|
||||||
|
|
||||||
double duracionBusSegundos = EstimarDuracionBusSegundos(distanciaTramo) + extraParadasSeg;
|
|
||||||
|
|
||||||
tCursor = AddSecondsSafe(tCursor, duracionBusSegundos);
|
|
||||||
total += duracionBusSegundos;
|
|
||||||
|
|
||||||
paradaUltimaBus = pFinTramo;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) Fin bus -> destino (walk)
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
var w2 = await WalkCached(paradaFinBus.Latitud, paradaFinBus.Longitud, _coordenadasDestino![0], _coordenadasDestino[1]);
|
|
||||||
|
|
||||||
double distPie2 = (w2.dist > 0) ? w2.dist
|
|
||||||
: CalcularDistanciaMetros(paradaFinBus.Latitud, paradaFinBus.Longitud, _coordenadasDestino[0], _coordenadasDestino[1]);
|
|
||||||
|
|
||||||
alt.DistanciaPieFinAprox = distPie2;
|
|
||||||
|
|
||||||
var durPie2 = EstimarDuracionPieSegundos(distPie2);
|
|
||||||
tCursor = AddSecondsSafe(tCursor, durPie2);
|
|
||||||
total += durPie2;
|
|
||||||
|
|
||||||
// ✅ guardar transbordos reales (para chips)
|
|
||||||
alt.NumeroTransbordosReales = CalcularTransbordosReales(alt.AlternativaBus);
|
|
||||||
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Busca ruta desde buscador.
|
/// Busca ruta desde buscador.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2255,34 +1660,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona alternativa solo a pie.
|
|
||||||
/// </summary>
|
|
||||||
private static AlternativaRuta ClonarAlternativaSoloAPie(AlternativaRuta a)
|
|
||||||
{
|
|
||||||
// Copia “suficiente”: referencia a la línea (geometría) se puede compartir sin problema.
|
|
||||||
// Lo importante es NO compartir la misma instancia porque cambiar Etiqueta afectaría al de abajo.
|
|
||||||
return new AlternativaRuta
|
|
||||||
{
|
|
||||||
EsSoloAPie = true,
|
|
||||||
ProveedorRuta = a.ProveedorRuta,
|
|
||||||
|
|
||||||
LineaSoloAPie = a.LineaSoloAPie,
|
|
||||||
DistanciaSoloAPie = a.DistanciaSoloAPie,
|
|
||||||
DuracionSoloAPie = a.DuracionSoloAPie,
|
|
||||||
|
|
||||||
DuracionTotalAproxSegundos = a.DuracionTotalAproxSegundos,
|
|
||||||
PuntuacionCoste = a.PuntuacionCoste,
|
|
||||||
|
|
||||||
// mantenemos etiqueta original (luego la sobrescribimos a "Más rápida" arriba)
|
|
||||||
Etiqueta = a.Etiqueta
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// Pintado
|
// Pintado
|
||||||
// =========================================================
|
// =========================================================
|
||||||
|
|||||||
@@ -215,6 +215,8 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
color,
|
color,
|
||||||
category = categoria,
|
category = categoria,
|
||||||
slope = pendiente,
|
slope = pendiente,
|
||||||
|
distance = distancia,
|
||||||
|
elevation = elevActual,
|
||||||
label = pendiente is null
|
label = pendiente is null
|
||||||
? "-"
|
? "-"
|
||||||
: pendiente.Value.ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture) + " %"
|
: pendiente.Value.ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture) + " %"
|
||||||
|
|||||||
@@ -23,12 +23,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static string HHmm(DateTime dt) => dt.ToString("HH:mm", CultureInfo.InvariantCulture);
|
private static string HHmm(DateTime dt) => dt.ToString("HH:mm", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
private static DateTime Compose(DateOnly d, TimeOnly t)
|
|
||||||
=> d.ToDateTime(t);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Añade seconds safe.
|
/// Añade seconds safe.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -225,26 +219,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
|
|
||||||
private List<ProximaSalidaVm> _nextDeps = new();
|
private List<ProximaSalidaVm> _nextDeps = new();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gestiona txt desde ruta id.
|
|
||||||
/// </summary>
|
|
||||||
private string LineaTxtDesdeRutaId(string? rutaId)
|
|
||||||
{
|
|
||||||
var rid = CodigoSinDecimal(rutaId);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(rid))
|
|
||||||
return $"L\u00ednea ?";
|
|
||||||
|
|
||||||
// Intenta mapear contra tu JSON de RedBus (Lineas)
|
|
||||||
var linea = RedBusServicio.Lineas
|
|
||||||
.FirstOrDefault(l => CodigoSinDecimal(l.Codigo?.ToString()) == rid);
|
|
||||||
|
|
||||||
if (linea is not null)
|
|
||||||
return $"L\u00ednea {CodigoSinDecimal(linea.Codigo?.ToString())} - {linea.Nombre}";
|
|
||||||
|
|
||||||
// Fallback si no encontramos la línea en el JSON
|
|
||||||
return $"L\u00ednea {rid}";
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona toggle hora simulada.
|
/// Gestiona toggle hora simulada.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task OnToggleHoraSimulada(ChangeEventArgs e)
|
private async Task OnToggleHoraSimulada(ChangeEventArgs e)
|
||||||
@@ -348,19 +322,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Restablece hora pruebas.
|
|
||||||
/// </summary>
|
|
||||||
private async Task ResetHoraPruebas()
|
|
||||||
{
|
|
||||||
_usarHoraSimulada = false;
|
|
||||||
_fechaSimulada = DateOnly.FromDateTime(DateTime.Now);
|
|
||||||
_horaSimulada = TimeOnly.FromDateTime(DateTime.Now);
|
|
||||||
|
|
||||||
StartClock();
|
|
||||||
await RefrescarHoraUIAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private string _horaAhoraTxt = "";
|
private string _horaAhoraTxt = "";
|
||||||
|
|||||||
@@ -1484,14 +1484,6 @@
|
|||||||
@onblur="OnBlurCoeficienteEsfuerzoAltura" />
|
@onblur="OnBlurCoeficienteEsfuerzoAltura" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@* <div class="config-row">
|
|
||||||
<label>Minutos por Transbordo:</label>
|
|
||||||
<div class="num-inline">
|
|
||||||
<input type="number" class="config-input" step="1" @bind="_minutosTransbordo" @bind:event="oninput" />
|
|
||||||
<button type="button" class="step-btn" @onclick="() => _minutosTransbordo = _minutosTransbordo + 1">▲</button>
|
|
||||||
<button type="button" class="step-btn" @onclick="() => _minutosTransbordo = Math.Max(0, _minutosTransbordo - 1)">▼</button>
|
|
||||||
</div>
|
|
||||||
</div> *@
|
|
||||||
<div class="config-row">
|
<div class="config-row">
|
||||||
<label>Método cálculo andando:</label>
|
<label>Método cálculo andando:</label>
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio
|
|||||||
{
|
{
|
||||||
private const string RutaCallejeroZipRelativa = "Modelos/Callejero/GFA_DSET_AD_CSV.zip";
|
private const string RutaCallejeroZipRelativa = "Modelos/Callejero/GFA_DSET_AD_CSV.zip";
|
||||||
private const string NombreCsvB5m = "GFA_DSET_AD.csv";
|
private const string NombreCsvB5m = "GFA_DSET_AD.csv";
|
||||||
private const int MaxResultadosInternos = 60;
|
|
||||||
|
|
||||||
private readonly IWebHostEnvironment _entorno;
|
private readonly IWebHostEnvironment _entorno;
|
||||||
private readonly SemaphoreSlim _semaforoCarga = new(1, 1);
|
private readonly SemaphoreSlim _semaforoCarga = new(1, 1);
|
||||||
private readonly ConcurrentDictionary<string, LugarGeocodificadoCercano> _cacheCercanos = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, LugarGeocodificadoCercano> _cacheCercanos = new(StringComparer.Ordinal);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using RutasDBUS.Modelos.Importacion;
|
using RutasDBUS.Modelos.Importacion;
|
||||||
using RutasDBUS.Servicios.Importacion;
|
using RutasDBUS.Servicios.Importacion;
|
||||||
@@ -34,16 +32,6 @@ namespace RutasDBUS.Servicios.Horarios
|
|||||||
private bool _loaded;
|
private bool _loaded;
|
||||||
private ArchivoImportacionDbus? _archivoCargado;
|
private ArchivoImportacionDbus? _archivoCargado;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Compatibilidad con la API anterior. La resolución real del ZIP la hace la capa de importación compartida.
|
|
||||||
/// </summary>
|
|
||||||
public string CarpetaRelativa { get; set; } = Path.Combine("Modelos", "horarios");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Compatibilidad con la API anterior. La selección real del ZIP la hace la capa de importación compartida.
|
|
||||||
/// </summary>
|
|
||||||
public string? NombreArchivoZip { get; set; } = null;
|
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Modelos mínimos (CSV dBUS)
|
// Modelos mínimos (CSV dBUS)
|
||||||
// =========================
|
// =========================
|
||||||
@@ -334,437 +322,6 @@ namespace RutasDBUS.Servicios.Horarios
|
|||||||
if (id is null) return new();
|
if (id is null) return new();
|
||||||
return GetNextDepartures(id.Value, date, now, take, includePast);
|
return GetNextDepartures(id.Value, date, now, take, includePast);
|
||||||
}
|
}
|
||||||
// =========================
|
|
||||||
// Carga desde ZIP
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Carga from zip.
|
|
||||||
/// </summary>
|
|
||||||
private async Task LoadFromZipAsync(string zipAbsPath, CancellationToken ct)
|
|
||||||
{
|
|
||||||
// limpiar
|
|
||||||
_cal.Clear();
|
|
||||||
_calExByDate.Clear();
|
|
||||||
_expById.Clear();
|
|
||||||
_stopTimesByExp.Clear();
|
|
||||||
_departuresByStop.Clear();
|
|
||||||
_stopById.Clear();
|
|
||||||
_stopIdByCodigo.Clear();
|
|
||||||
_tripMetaByTripId.Clear();
|
|
||||||
|
|
||||||
using var fs = File.OpenRead(zipAbsPath);
|
|
||||||
using var zip = new ZipArchive(fs, ZipArchiveMode.Read, leaveOpen: false);
|
|
||||||
|
|
||||||
// localizar entradas (por si están en subcarpetas)
|
|
||||||
ZipArchiveEntry? eCalendario = FindEntry(zip, "calendario.csv");
|
|
||||||
ZipArchiveEntry? eCalEx = FindEntry(zip, "calendario_excepciones_puntuales.csv");
|
|
||||||
ZipArchiveEntry? eExp = FindEntry(zip, "expedicion.csv");
|
|
||||||
ZipArchiveEntry? eStopTime = FindEntry(zip, "tiempo_parada.csv");
|
|
||||||
ZipArchiveEntry? eStop = FindEntry(zip, "parada.csv");
|
|
||||||
|
|
||||||
// Si no están, intentar en gtfs.zip
|
|
||||||
ZipArchiveEntry? eGtfsZip = FindEntry(zip, "gtfs.zip");
|
|
||||||
|
|
||||||
// 1) Paradas (para mapping)
|
|
||||||
if (eStop is not null)
|
|
||||||
{
|
|
||||||
await LoadParadasFromDbusAsync(eStop, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Calendarios
|
|
||||||
if (eCalendario is not null)
|
|
||||||
await LoadCalendariosDbusAsync(eCalendario, ct);
|
|
||||||
|
|
||||||
// 3) Excepciones
|
|
||||||
if (eCalEx is not null)
|
|
||||||
await LoadCalExDbusAsync(eCalEx, ct);
|
|
||||||
|
|
||||||
// 4) Expediciones
|
|
||||||
if (eExp is not null)
|
|
||||||
await LoadExpedicionesDbusAsync(eExp, ct);
|
|
||||||
|
|
||||||
// 5) Tiempo parada
|
|
||||||
if (eStopTime is not null)
|
|
||||||
await LoadTiempoParadaDbusAsync(eStopTime, ct);
|
|
||||||
|
|
||||||
// Si falta algo esencial, intentamos GTFS
|
|
||||||
bool needGtfs =
|
|
||||||
_cal.Count == 0 ||
|
|
||||||
_expById.Count == 0 ||
|
|
||||||
_stopTimesByExp.Count == 0 ||
|
|
||||||
_stopById.Count == 0;
|
|
||||||
|
|
||||||
if (needGtfs && eGtfsZip is not null)
|
|
||||||
{
|
|
||||||
await LoadFromGtfsZipInsideAsync(eGtfsZip, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
// construir índices de salidas
|
|
||||||
ConstruirIndiceSalidas();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Carga from gtfs zip inside.
|
|
||||||
/// </summary>
|
|
||||||
private async Task LoadFromGtfsZipInsideAsync(ZipArchiveEntry gtfsZipEntry, CancellationToken ct)
|
|
||||||
{
|
|
||||||
using var ms = new MemoryStream();
|
|
||||||
await using (var s = gtfsZipEntry.Open())
|
|
||||||
await s.CopyToAsync(ms, ct);
|
|
||||||
|
|
||||||
ms.Position = 0;
|
|
||||||
using var gtfs = new ZipArchive(ms, ZipArchiveMode.Read, leaveOpen: false);
|
|
||||||
|
|
||||||
// GTFS estándar
|
|
||||||
var stops = FindEntry(gtfs, "stops.txt");
|
|
||||||
var cal = FindEntry(gtfs, "calendar.txt");
|
|
||||||
var calDates = FindEntry(gtfs, "calendar_dates.txt");
|
|
||||||
var trips = FindEntry(gtfs, "trips.txt");
|
|
||||||
var stopTimes = FindEntry(gtfs, "stop_times.txt");
|
|
||||||
|
|
||||||
if (stops is not null && _stopById.Count == 0)
|
|
||||||
await LoadStopsGtfsAsync(stops, ct);
|
|
||||||
|
|
||||||
if (cal is not null && _cal.Count == 0)
|
|
||||||
await LoadCalendarGtfsAsync(cal, ct);
|
|
||||||
|
|
||||||
if (calDates is not null && _calExByDate.Count == 0)
|
|
||||||
await LoadCalendarDatesGtfsAsync(calDates, ct);
|
|
||||||
|
|
||||||
if (trips is not null && _expById.Count == 0)
|
|
||||||
await LoadTripsGtfsAsync(trips, ct);
|
|
||||||
|
|
||||||
if (stopTimes is not null && _stopTimesByExp.Count == 0)
|
|
||||||
await LoadStopTimesGtfsAsync(stopTimes, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Busca entrada.
|
|
||||||
/// </summary>
|
|
||||||
private static ZipArchiveEntry? FindEntry(ZipArchive zip, string fileName)
|
|
||||||
{
|
|
||||||
// busca por "termina en /fileName" o exacto
|
|
||||||
return zip.Entries.FirstOrDefault(e =>
|
|
||||||
e.FullName.EndsWith("/" + fileName, StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
e.FullName.Equals(fileName, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Parse CSV (con comillas)
|
|
||||||
// =========================
|
|
||||||
/// <summary>
|
|
||||||
/// Lee csv all.
|
|
||||||
/// </summary>
|
|
||||||
private static async Task<List<string[]>> ReadCsvAllAsync(ZipArchiveEntry entry, CancellationToken ct)
|
|
||||||
{
|
|
||||||
using var stream = entry.Open();
|
|
||||||
using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
|
||||||
|
|
||||||
var rows = new List<string[]>(8192);
|
|
||||||
string? line;
|
|
||||||
|
|
||||||
// header
|
|
||||||
line = await sr.ReadLineAsync();
|
|
||||||
if (line is null) return rows;
|
|
||||||
|
|
||||||
while ((line = await sr.ReadLineAsync()) is not null)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if (line.Length == 0) continue;
|
|
||||||
|
|
||||||
var fields = SplitCsvLine(line);
|
|
||||||
rows.Add(fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona csv linea.
|
|
||||||
/// </summary>
|
|
||||||
private static string[] SplitCsvLine(string line)
|
|
||||||
{
|
|
||||||
// separador coma, con soporte básico de comillas dobles
|
|
||||||
var res = new List<string>(32);
|
|
||||||
var sb = new StringBuilder(line.Length);
|
|
||||||
|
|
||||||
bool inQuotes = false;
|
|
||||||
for (int i = 0; i < line.Length; i++)
|
|
||||||
{
|
|
||||||
char c = line[i];
|
|
||||||
|
|
||||||
if (c == '"')
|
|
||||||
{
|
|
||||||
// doble comilla escapada
|
|
||||||
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
|
|
||||||
{
|
|
||||||
sb.Append('"');
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
inQuotes = !inQuotes;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c == ',' && !inQuotes)
|
|
||||||
{
|
|
||||||
res.Add(sb.ToString());
|
|
||||||
sb.Clear();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.Append(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Add(sb.ToString());
|
|
||||||
return res.Select(x => x.Trim()).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Analiza date yyyy mm dd.
|
|
||||||
/// </summary>
|
|
||||||
private static DateOnly ParseDate_YYYY_MM_DD(string s)
|
|
||||||
=> DateOnly.ParseExact(s.Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Analiza date yyyymmdd.
|
|
||||||
/// </summary>
|
|
||||||
private static DateOnly ParseDate_YYYYMMDD(string s)
|
|
||||||
=> DateOnly.ParseExact(s.Trim(), "yyyyMMdd", CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Analiza tiempo hh mm ss.
|
|
||||||
/// </summary>
|
|
||||||
private static TimeOnly ParseTime_HH_MM_SS(string s)
|
|
||||||
=> TimeOnly.ParseExact(s.Trim(), "HH:mm:ss", CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Analiza tiempo nullable hh mm ss.
|
|
||||||
/// </summary>
|
|
||||||
private static TimeOnly? ParseTimeNullable_HH_MM_SS(string? s)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(s)) return null;
|
|
||||||
return ParseTime_HH_MM_SS(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Loaders dBUS CSV
|
|
||||||
// =========================
|
|
||||||
/// <summary>
|
|
||||||
/// Carga paradas from dbus.
|
|
||||||
/// </summary>
|
|
||||||
private async Task LoadParadasFromDbusAsync(ZipArchiveEntry paradaCsv, CancellationToken ct)
|
|
||||||
{
|
|
||||||
// parada.csv: id,codigo_sms,nombre_par,latitud_y_par,longitud_x_par,...
|
|
||||||
// Nos quedamos con id, codigo_sms, nombre_par
|
|
||||||
using var stream = paradaCsv.Open();
|
|
||||||
using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
|
||||||
|
|
||||||
var header = await sr.ReadLineAsync();
|
|
||||||
if (header is null) return;
|
|
||||||
|
|
||||||
var cols = SplitCsvLine(header);
|
|
||||||
int iId = IndexOf(cols, "id");
|
|
||||||
int iSms = IndexOf(cols, "codigo_sms");
|
|
||||||
int iNombre = IndexOf(cols, "nombre_par");
|
|
||||||
|
|
||||||
string? line;
|
|
||||||
while ((line = await sr.ReadLineAsync()) is not null)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if (line.Length == 0) continue;
|
|
||||||
var f = SplitCsvLine(line);
|
|
||||||
|
|
||||||
if (iId < 0 || iId >= f.Length) continue;
|
|
||||||
if (!int.TryParse(f[iId], NumberStyles.Integer, CultureInfo.InvariantCulture, out var id)) continue;
|
|
||||||
|
|
||||||
var sms = (iSms >= 0 && iSms < f.Length) ? f[iSms] : null;
|
|
||||||
var nombre = (iNombre >= 0 && iNombre < f.Length) ? f[iNombre] : "";
|
|
||||||
|
|
||||||
var row = new FilaParada(id, string.IsNullOrWhiteSpace(sms) ? null : sms, nombre);
|
|
||||||
_stopById[id] = row;
|
|
||||||
|
|
||||||
// Map por código SMS normalizado
|
|
||||||
if (!string.IsNullOrWhiteSpace(sms))
|
|
||||||
{
|
|
||||||
var cod = NormalizarCodigoParada(sms);
|
|
||||||
_stopIdByCodigo[cod] = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map también por id "como string"
|
|
||||||
_stopIdByCodigo[id.ToString(CultureInfo.InvariantCulture)] = id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Carga calendarios dbus.
|
|
||||||
/// </summary>
|
|
||||||
private async Task LoadCalendariosDbusAsync(ZipArchiveEntry calendarioCsv, CancellationToken ct)
|
|
||||||
{
|
|
||||||
using var stream = calendarioCsv.Open();
|
|
||||||
using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
|
||||||
|
|
||||||
var header = await sr.ReadLineAsync();
|
|
||||||
if (header is null) return;
|
|
||||||
|
|
||||||
var cols = SplitCsvLine(header);
|
|
||||||
int iId = IndexOf(cols, "id");
|
|
||||||
int iL = IndexOf(cols, "lunes");
|
|
||||||
int iMa = IndexOf(cols, "martes");
|
|
||||||
int iMi = IndexOf(cols, "miercoles");
|
|
||||||
int iJ = IndexOf(cols, "jueves");
|
|
||||||
int iV = IndexOf(cols, "viernes");
|
|
||||||
int iS = IndexOf(cols, "sabado");
|
|
||||||
int iD = IndexOf(cols, "domingo");
|
|
||||||
int iIni = IndexOf(cols, "fecha_inicio");
|
|
||||||
int iFin = IndexOf(cols, "fecha_fin");
|
|
||||||
|
|
||||||
string? line;
|
|
||||||
while ((line = await sr.ReadLineAsync()) is not null)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if (line.Length == 0) continue;
|
|
||||||
var f = SplitCsvLine(line);
|
|
||||||
|
|
||||||
if (iId < 0 || iId >= f.Length) continue;
|
|
||||||
var id = f[iId];
|
|
||||||
if (string.IsNullOrWhiteSpace(id)) continue;
|
|
||||||
|
|
||||||
int GetInt(int idx, int def = 1)
|
|
||||||
{
|
|
||||||
if (idx < 0 || idx >= f.Length) return def;
|
|
||||||
return int.TryParse(f[idx], NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) ? n : def;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (iIni < 0 || iIni >= f.Length) continue;
|
|
||||||
if (iFin < 0 || iFin >= f.Length) continue;
|
|
||||||
|
|
||||||
var inicio = ParseDate_YYYY_MM_DD(f[iIni]);
|
|
||||||
var fin = ParseDate_YYYY_MM_DD(f[iFin]);
|
|
||||||
|
|
||||||
var row = new FilaCalendario(
|
|
||||||
id,
|
|
||||||
GetInt(iL), GetInt(iMa), GetInt(iMi), GetInt(iJ), GetInt(iV), GetInt(iS), GetInt(iD),
|
|
||||||
inicio, fin
|
|
||||||
);
|
|
||||||
|
|
||||||
_cal[id] = row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Carga cal ex dbus.
|
|
||||||
/// </summary>
|
|
||||||
private async Task LoadCalExDbusAsync(ZipArchiveEntry calExCsv, CancellationToken ct)
|
|
||||||
{
|
|
||||||
// calendario_excepciones_puntuales.csv: id,fecha_ex,tipo_excepcion
|
|
||||||
using var stream = calExCsv.Open();
|
|
||||||
using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
|
||||||
|
|
||||||
var header = await sr.ReadLineAsync();
|
|
||||||
if (header is null) return;
|
|
||||||
|
|
||||||
var cols = SplitCsvLine(header);
|
|
||||||
int iId = IndexOf(cols, "id");
|
|
||||||
int iFecha = IndexOf(cols, "fecha_ex");
|
|
||||||
int iTipo = IndexOf(cols, "tipo_excepcion");
|
|
||||||
|
|
||||||
string? line;
|
|
||||||
while ((line = await sr.ReadLineAsync()) is not null)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if (line.Length == 0) continue;
|
|
||||||
var f = SplitCsvLine(line);
|
|
||||||
|
|
||||||
if (iId < 0 || iId >= f.Length) continue;
|
|
||||||
if (iFecha < 0 || iFecha >= f.Length) continue;
|
|
||||||
if (iTipo < 0 || iTipo >= f.Length) continue;
|
|
||||||
|
|
||||||
var id = f[iId];
|
|
||||||
if (string.IsNullOrWhiteSpace(id)) continue;
|
|
||||||
|
|
||||||
var fecha = ParseDate_YYYY_MM_DD(f[iFecha]);
|
|
||||||
|
|
||||||
if (!int.TryParse(f[iTipo], NumberStyles.Integer, CultureInfo.InvariantCulture, out var tipo))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var row = new FilaCalendarioExcepcion(id, fecha, tipo);
|
|
||||||
|
|
||||||
if (!_calExByDate.TryGetValue(fecha, out var list))
|
|
||||||
_calExByDate[fecha] = list = new List<FilaCalendarioExcepcion>();
|
|
||||||
list.Add(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Carga expediciones dbus.
|
|
||||||
/// </summary>
|
|
||||||
private async Task LoadExpedicionesDbusAsync(ZipArchiveEntry expCsv, CancellationToken ct)
|
|
||||||
{
|
|
||||||
// expedicion.csv:
|
|
||||||
// id,id_ruta,dir_destino,direccion,cambio_exp,id_patron_exp,h_salida,id_cal
|
|
||||||
using var stream = expCsv.Open();
|
|
||||||
using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
|
||||||
|
|
||||||
var header = await sr.ReadLineAsync();
|
|
||||||
if (header is null) return;
|
|
||||||
|
|
||||||
var cols = SplitCsvLine(header);
|
|
||||||
|
|
||||||
int iId = IndexOf(cols, "id");
|
|
||||||
int iRuta = IndexOf(cols, "id_ruta");
|
|
||||||
int iDir = IndexOf(cols, "Direccion");
|
|
||||||
if (iDir < 0) iDir = IndexOf(cols, "direccion");
|
|
||||||
|
|
||||||
int iDirDestino = IndexOf(cols, "dir_destino");
|
|
||||||
int iPatron = IndexOf(cols, "id_patron_exp");
|
|
||||||
int iH = IndexOf(cols, "h_salida");
|
|
||||||
int iCal = IndexOf(cols, "id_cal");
|
|
||||||
|
|
||||||
string? line;
|
|
||||||
while ((line = await sr.ReadLineAsync()) is not null)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if (line.Length == 0) continue;
|
|
||||||
|
|
||||||
var f = SplitCsvLine(line);
|
|
||||||
|
|
||||||
if (iId < 0 || iId >= f.Length) continue;
|
|
||||||
if (iRuta < 0 || iRuta >= f.Length) continue;
|
|
||||||
if (iCal < 0 || iCal >= f.Length) continue;
|
|
||||||
|
|
||||||
var id = f[iId];
|
|
||||||
var ruta = f[iRuta];
|
|
||||||
var idCal = f[iCal];
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(ruta) || string.IsNullOrWhiteSpace(idCal))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
int direccion = 0;
|
|
||||||
if (iDir >= 0 && iDir < f.Length)
|
|
||||||
int.TryParse(f[iDir], NumberStyles.Integer, CultureInfo.InvariantCulture, out direccion);
|
|
||||||
|
|
||||||
var dirDestino = (iDirDestino >= 0 && iDirDestino < f.Length) ? f[iDirDestino] : "";
|
|
||||||
var idPatronExp = (iPatron >= 0 && iPatron < f.Length) ? f[iPatron] : "";
|
|
||||||
|
|
||||||
TimeOnly? h = null;
|
|
||||||
if (iH >= 0 && iH < f.Length)
|
|
||||||
h = ParseTimeNullable_HH_MM_SS(f[iH]);
|
|
||||||
|
|
||||||
var row = new FilaExpedicion(
|
|
||||||
id,
|
|
||||||
ruta,
|
|
||||||
direccion,
|
|
||||||
dirDestino,
|
|
||||||
idPatronExp,
|
|
||||||
h,
|
|
||||||
idCal
|
|
||||||
);
|
|
||||||
|
|
||||||
_expById[id] = row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Normaliza linea.
|
/// Normaliza linea.
|
||||||
@@ -888,64 +445,6 @@ namespace RutasDBUS.Servicios.Horarios
|
|||||||
return string.IsNullOrWhiteSpace(patron) ? "0" : patron;
|
return string.IsNullOrWhiteSpace(patron) ? "0" : patron;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Carga tiempo parada dbus.
|
|
||||||
/// </summary>
|
|
||||||
private async Task LoadTiempoParadaDbusAsync(ZipArchiveEntry tiempoParadaCsv, CancellationToken ct)
|
|
||||||
{
|
|
||||||
// tiempo_parada.csv: id_exp,hora_llegada,hora_salida,distancia_siguiente_parada,id_tiempo_par,sec_par,...
|
|
||||||
using var stream = tiempoParadaCsv.Open();
|
|
||||||
using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
|
||||||
|
|
||||||
var header = await sr.ReadLineAsync();
|
|
||||||
if (header is null) return;
|
|
||||||
|
|
||||||
var cols = SplitCsvLine(header);
|
|
||||||
|
|
||||||
int iExp = IndexOf(cols, "id_exp");
|
|
||||||
int iLleg = IndexOf(cols, "hora_llegada");
|
|
||||||
int iSal = IndexOf(cols, "hora_salida");
|
|
||||||
int iStop = IndexOf(cols, "id_tiempo_par");
|
|
||||||
int iSec = IndexOf(cols, "sec_par");
|
|
||||||
|
|
||||||
string? line;
|
|
||||||
while ((line = await sr.ReadLineAsync()) is not null)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if (line.Length == 0) continue;
|
|
||||||
var f = SplitCsvLine(line);
|
|
||||||
|
|
||||||
if (iExp < 0 || iExp >= f.Length) continue;
|
|
||||||
if (iStop < 0 || iStop >= f.Length) continue;
|
|
||||||
if (iSec < 0 || iSec >= f.Length) continue;
|
|
||||||
if (iLleg < 0 || iLleg >= f.Length) continue;
|
|
||||||
if (iSal < 0 || iSal >= f.Length) continue;
|
|
||||||
|
|
||||||
var expId = f[iExp];
|
|
||||||
if (string.IsNullOrWhiteSpace(expId)) continue;
|
|
||||||
|
|
||||||
if (!int.TryParse(f[iStop], NumberStyles.Integer, CultureInfo.InvariantCulture, out var stopId))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!int.TryParse(f[iSec], NumberStyles.Integer, CultureInfo.InvariantCulture, out var sec))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// En calendario 0 puede venir 00:00:00
|
|
||||||
var lleg = ParseTime_HH_MM_SS(f[iLleg]);
|
|
||||||
var sal = ParseTime_HH_MM_SS(f[iSal]);
|
|
||||||
|
|
||||||
var row = new FilaTiempoParada(expId, stopId, sec, lleg, sal);
|
|
||||||
|
|
||||||
if (!_stopTimesByExp.TryGetValue(expId, out var list))
|
|
||||||
_stopTimesByExp[expId] = list = new List<FilaTiempoParada>(64);
|
|
||||||
list.Add(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ordenar por secuencia
|
|
||||||
foreach (var kv in _stopTimesByExp)
|
|
||||||
kv.Value.Sort((a, b) => a.Sec.CompareTo(b.Sec));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Obtiene linea itinerario sentido nombre texto.
|
/// Obtiene linea itinerario sentido nombre texto.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1072,266 +571,6 @@ namespace RutasDBUS.Servicios.Horarios
|
|||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Loaders GTFS (fallback)
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Carga paradas gtfs.
|
|
||||||
/// </summary>
|
|
||||||
private async Task LoadStopsGtfsAsync(ZipArchiveEntry stopsTxt, CancellationToken ct)
|
|
||||||
{
|
|
||||||
// stops.txt: stop_id, stop_name, stop_lat, stop_lon, ...
|
|
||||||
using var stream = stopsTxt.Open();
|
|
||||||
using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
|
||||||
|
|
||||||
var header = await sr.ReadLineAsync();
|
|
||||||
if (header is null) return;
|
|
||||||
|
|
||||||
var cols = SplitCsvLine(header);
|
|
||||||
int iId = IndexOf(cols, "stop_id");
|
|
||||||
int iName = IndexOf(cols, "stop_name");
|
|
||||||
|
|
||||||
string? line;
|
|
||||||
while ((line = await sr.ReadLineAsync()) is not null)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if (line.Length == 0) continue;
|
|
||||||
var f = SplitCsvLine(line);
|
|
||||||
|
|
||||||
if (iId < 0 || iId >= f.Length) continue;
|
|
||||||
var idStr = f[iId];
|
|
||||||
if (!int.TryParse(idStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id)) continue;
|
|
||||||
|
|
||||||
var name = (iName >= 0 && iName < f.Length) ? f[iName] : "";
|
|
||||||
var row = new FilaParada(id, idStr, name);
|
|
||||||
_stopById[id] = row;
|
|
||||||
|
|
||||||
_stopIdByCodigo[NormalizarCodigoParada(idStr)] = id;
|
|
||||||
_stopIdByCodigo[id.ToString(CultureInfo.InvariantCulture)] = id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Carga calendario gtfs.
|
|
||||||
/// </summary>
|
|
||||||
private async Task LoadCalendarGtfsAsync(ZipArchiveEntry calendarTxt, CancellationToken ct)
|
|
||||||
{
|
|
||||||
// calendar.txt: service_id,monday,...,sunday,start_date,end_date (0/1)
|
|
||||||
using var stream = calendarTxt.Open();
|
|
||||||
using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
|
||||||
|
|
||||||
var header = await sr.ReadLineAsync();
|
|
||||||
if (header is null) return;
|
|
||||||
|
|
||||||
var cols = SplitCsvLine(header);
|
|
||||||
|
|
||||||
int iId = IndexOf(cols, "service_id");
|
|
||||||
int iMon = IndexOf(cols, "monday");
|
|
||||||
int iTue = IndexOf(cols, "tuesday");
|
|
||||||
int iWed = IndexOf(cols, "wednesday");
|
|
||||||
int iThu = IndexOf(cols, "thursday");
|
|
||||||
int iFri = IndexOf(cols, "friday");
|
|
||||||
int iSat = IndexOf(cols, "saturday");
|
|
||||||
int iSun = IndexOf(cols, "sunday");
|
|
||||||
int iIni = IndexOf(cols, "start_date");
|
|
||||||
int iFin = IndexOf(cols, "end_date");
|
|
||||||
|
|
||||||
string? line;
|
|
||||||
while ((line = await sr.ReadLineAsync()) is not null)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if (line.Length == 0) continue;
|
|
||||||
var f = SplitCsvLine(line);
|
|
||||||
|
|
||||||
if (iId < 0 || iId >= f.Length) continue;
|
|
||||||
var id = f[iId];
|
|
||||||
if (string.IsNullOrWhiteSpace(id)) continue;
|
|
||||||
|
|
||||||
int Flag(int idx)
|
|
||||||
{
|
|
||||||
// GTFS: 0/1 -> convertimos a tu convención: 1=no, 2=sí
|
|
||||||
if (idx < 0 || idx >= f.Length) return 1;
|
|
||||||
return (f[idx].Trim() == "1") ? 2 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ini = ParseDate_YYYYMMDD(f[iIni]);
|
|
||||||
var fin = ParseDate_YYYYMMDD(f[iFin]);
|
|
||||||
|
|
||||||
_cal[id] = new FilaCalendario(id, Flag(iMon), Flag(iTue), Flag(iWed), Flag(iThu), Flag(iFri), Flag(iSat), Flag(iSun), ini, fin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Carga calendario dates gtfs.
|
|
||||||
/// </summary>
|
|
||||||
private async Task LoadCalendarDatesGtfsAsync(ZipArchiveEntry calendarDatesTxt, CancellationToken ct)
|
|
||||||
{
|
|
||||||
// calendar_dates.txt: service_id,date,exception_type (1=add, 2=remove)
|
|
||||||
using var stream = calendarDatesTxt.Open();
|
|
||||||
using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
|
||||||
|
|
||||||
var header = await sr.ReadLineAsync();
|
|
||||||
if (header is null) return;
|
|
||||||
|
|
||||||
var cols = SplitCsvLine(header);
|
|
||||||
|
|
||||||
int iId = IndexOf(cols, "service_id");
|
|
||||||
int iDate = IndexOf(cols, "date");
|
|
||||||
int iType = IndexOf(cols, "exception_type");
|
|
||||||
|
|
||||||
string? line;
|
|
||||||
while ((line = await sr.ReadLineAsync()) is not null)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if (line.Length == 0) continue;
|
|
||||||
var f = SplitCsvLine(line);
|
|
||||||
|
|
||||||
if (iId < 0 || iId >= f.Length) continue;
|
|
||||||
if (iDate < 0 || iDate >= f.Length) continue;
|
|
||||||
if (iType < 0 || iType >= f.Length) continue;
|
|
||||||
|
|
||||||
var id = f[iId];
|
|
||||||
var fecha = ParseDate_YYYYMMDD(f[iDate]);
|
|
||||||
|
|
||||||
if (!int.TryParse(f[iType], NumberStyles.Integer, CultureInfo.InvariantCulture, out var tipo))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var row = new FilaCalendarioExcepcion(id, fecha, tipo);
|
|
||||||
if (!_calExByDate.TryGetValue(fecha, out var list))
|
|
||||||
_calExByDate[fecha] = list = new List<FilaCalendarioExcepcion>();
|
|
||||||
list.Add(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Carga viajes gtfs.
|
|
||||||
/// </summary>
|
|
||||||
private async Task LoadTripsGtfsAsync(ZipArchiveEntry tripsTxt, CancellationToken ct)
|
|
||||||
{
|
|
||||||
using var stream = tripsTxt.Open();
|
|
||||||
using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
|
||||||
|
|
||||||
var header = await sr.ReadLineAsync();
|
|
||||||
if (header is null) return;
|
|
||||||
|
|
||||||
var cols = SplitCsvLine(header);
|
|
||||||
int iTrip = IndexOf(cols, "trip_id");
|
|
||||||
int iRoute = IndexOf(cols, "route_id");
|
|
||||||
int iService = IndexOf(cols, "service_id");
|
|
||||||
int iHead = IndexOf(cols, "trip_headsign");
|
|
||||||
int iDir = IndexOf(cols, "direction_id");
|
|
||||||
int iShape = IndexOf(cols, "shape_id");
|
|
||||||
|
|
||||||
string? line;
|
|
||||||
while ((line = await sr.ReadLineAsync()) is not null)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if (line.Length == 0) continue;
|
|
||||||
var f = SplitCsvLine(line);
|
|
||||||
|
|
||||||
if (iTrip < 0 || iTrip >= f.Length) continue;
|
|
||||||
if (iRoute < 0 || iRoute >= f.Length) continue;
|
|
||||||
if (iService < 0 || iService >= f.Length) continue;
|
|
||||||
|
|
||||||
var tripId = f[iTrip];
|
|
||||||
var routeId = f[iRoute];
|
|
||||||
var serviceId = f[iService];
|
|
||||||
var head = (iHead >= 0 && iHead < f.Length) ? f[iHead] : "";
|
|
||||||
var shapeId = (iShape >= 0 && iShape < f.Length) ? f[iShape] : "";
|
|
||||||
|
|
||||||
int dir = 0;
|
|
||||||
if (iDir >= 0 && iDir < f.Length)
|
|
||||||
int.TryParse(f[iDir], NumberStyles.Integer, CultureInfo.InvariantCulture, out dir);
|
|
||||||
|
|
||||||
_expById[tripId] = new FilaExpedicion(
|
|
||||||
tripId,
|
|
||||||
routeId,
|
|
||||||
dir,
|
|
||||||
head,
|
|
||||||
shapeId,
|
|
||||||
null,
|
|
||||||
serviceId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// Carga parada tiempos gtfs.
|
|
||||||
/// </summary>
|
|
||||||
private async Task LoadStopTimesGtfsAsync(ZipArchiveEntry stopTimesTxt, CancellationToken ct)
|
|
||||||
{
|
|
||||||
// stop_times.txt: trip_id,arrival_time,departure_time,stop_id,stop_sequence
|
|
||||||
using var stream = stopTimesTxt.Open();
|
|
||||||
using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
|
||||||
|
|
||||||
var header = await sr.ReadLineAsync();
|
|
||||||
if (header is null) return;
|
|
||||||
|
|
||||||
var cols = SplitCsvLine(header);
|
|
||||||
|
|
||||||
int iTrip = IndexOf(cols, "trip_id");
|
|
||||||
int iArr = IndexOf(cols, "arrival_time");
|
|
||||||
int iDep = IndexOf(cols, "departure_time");
|
|
||||||
int iStop = IndexOf(cols, "stop_id");
|
|
||||||
int iSeq = IndexOf(cols, "stop_sequence");
|
|
||||||
|
|
||||||
string? line;
|
|
||||||
while ((line = await sr.ReadLineAsync()) is not null)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if (line.Length == 0) continue;
|
|
||||||
var f = SplitCsvLine(line);
|
|
||||||
|
|
||||||
if (iTrip < 0 || iTrip >= f.Length) continue;
|
|
||||||
if (iStop < 0 || iStop >= f.Length) continue;
|
|
||||||
if (iSeq < 0 || iSeq >= f.Length) continue;
|
|
||||||
if (iArr < 0 || iArr >= f.Length) continue;
|
|
||||||
if (iDep < 0 || iDep >= f.Length) continue;
|
|
||||||
|
|
||||||
var tripId = f[iTrip];
|
|
||||||
|
|
||||||
if (!int.TryParse(f[iStop], NumberStyles.Integer, CultureInfo.InvariantCulture, out var stopId))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!int.TryParse(f[iSeq], NumberStyles.Integer, CultureInfo.InvariantCulture, out var sec))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// OJO: GTFS permite >24:00:00. Aquí lo normal es <24:00:00.
|
|
||||||
// Si te llega 25:10:00, lo recortamos mod 24 para "próximas salidas" del mismo día.
|
|
||||||
var arr = ParseGtfsTimeSafe(f[iArr]);
|
|
||||||
var dep = ParseGtfsTimeSafe(f[iDep]);
|
|
||||||
|
|
||||||
var row = new FilaTiempoParada(tripId, stopId, sec, arr, dep);
|
|
||||||
|
|
||||||
if (!_stopTimesByExp.TryGetValue(tripId, out var list))
|
|
||||||
_stopTimesByExp[tripId] = list = new List<FilaTiempoParada>(64);
|
|
||||||
list.Add(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var kv in _stopTimesByExp)
|
|
||||||
kv.Value.Sort((a, b) => a.Sec.CompareTo(b.Sec));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Analiza gtfs tiempo safe.
|
|
||||||
/// </summary>
|
|
||||||
private static TimeOnly ParseGtfsTimeSafe(string s)
|
|
||||||
{
|
|
||||||
// "HH:MM:SS" y puede ser HH>=24
|
|
||||||
var t = s.Trim();
|
|
||||||
var parts = t.Split(':');
|
|
||||||
if (parts.Length != 3) return TimeOnly.MinValue;
|
|
||||||
|
|
||||||
if (!int.TryParse(parts[0], out var hh)) hh = 0;
|
|
||||||
if (!int.TryParse(parts[1], out var mm)) mm = 0;
|
|
||||||
if (!int.TryParse(parts[2], out var ss)) ss = 0;
|
|
||||||
|
|
||||||
hh %= 24;
|
|
||||||
mm = Math.Clamp(mm, 0, 59);
|
|
||||||
ss = Math.Clamp(ss, 0, 59);
|
|
||||||
return new TimeOnly(hh, mm, ss);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Índices derivados
|
// Índices derivados
|
||||||
// =========================
|
// =========================
|
||||||
@@ -1388,17 +627,6 @@ namespace RutasDBUS.Servicios.Horarios
|
|||||||
: $"L\u00ednea ?";
|
: $"L\u00ednea ?";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona of.
|
|
||||||
/// </summary>
|
|
||||||
private static int IndexOf(string[] cols, string name)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < cols.Length; i++)
|
|
||||||
if (cols[i].Equals(name, StringComparison.OrdinalIgnoreCase))
|
|
||||||
return i;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Normaliza codigo parada.
|
/// Normaliza codigo parada.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1592,107 +820,6 @@ namespace RutasDBUS.Servicios.Horarios
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normaliza sentido.
|
|
||||||
/// </summary>
|
|
||||||
private static string NormalizarSentido(string? s)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(s)) return "?";
|
|
||||||
s = s.Trim();
|
|
||||||
|
|
||||||
if (s.Equals("I", StringComparison.OrdinalIgnoreCase)) return "I";
|
|
||||||
if (s.Equals("V", StringComparison.OrdinalIgnoreCase)) return "V";
|
|
||||||
if (s.Equals("Ida", StringComparison.OrdinalIgnoreCase)) return "I";
|
|
||||||
if (s.Equals("Vuelta", StringComparison.OrdinalIgnoreCase)) return "V";
|
|
||||||
|
|
||||||
var c = char.ToUpperInvariant(s[0]);
|
|
||||||
if (c == 'I') return "I";
|
|
||||||
if (c == 'V') return "V";
|
|
||||||
return "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona to direccion.
|
|
||||||
/// </summary>
|
|
||||||
private static int? SentidoToDireccion(string sentidoNorm)
|
|
||||||
{
|
|
||||||
// dBUS/GTFS suele usar 0/1
|
|
||||||
return sentidoNorm switch
|
|
||||||
{
|
|
||||||
"I" => 0,
|
|
||||||
"V" => 1,
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona matches.
|
|
||||||
/// </summary>
|
|
||||||
private static bool RutaMatches(string rutaIdRaw, string lineaCodigo, string itinStr, string sentidoNorm)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(rutaIdRaw)) return false;
|
|
||||||
|
|
||||||
// Normalizamos
|
|
||||||
var r = rutaIdRaw.Trim().ToLowerInvariant();
|
|
||||||
var linea = (lineaCodigo ?? "").Trim().ToLowerInvariant();
|
|
||||||
if (linea.Length == 0) return false;
|
|
||||||
|
|
||||||
// quitar posibles decimales en línea "26.0" -> "26"
|
|
||||||
var iDot = linea.IndexOf('.');
|
|
||||||
if (iDot > 0) linea = linea[..iDot];
|
|
||||||
|
|
||||||
// patrones típicos que suelen aparecer en id_ruta
|
|
||||||
// - "26.12", "26_12", "26-12", "26 12"
|
|
||||||
bool hasLinea = ContainsToken(r, linea);
|
|
||||||
bool hasItin = ContainsToken(r, itinStr);
|
|
||||||
|
|
||||||
if (!hasLinea || !hasItin)
|
|
||||||
{
|
|
||||||
// intentamos combo "26.12"
|
|
||||||
var combo1 = $"{linea}.{itinStr}";
|
|
||||||
var combo2 = $"{linea}_{itinStr}";
|
|
||||||
var combo3 = $"{linea}-{itinStr}";
|
|
||||||
if (!(r.Contains(combo1) || r.Contains(combo2) || r.Contains(combo3)))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// si rutaId incluye algo tipo sentido/dirección, lo aprovechamos (opcional)
|
|
||||||
// (si no, no bloqueamos)
|
|
||||||
if (sentidoNorm == "I")
|
|
||||||
{
|
|
||||||
if (r.Contains("vuelta")) return false; // muy defensivo
|
|
||||||
}
|
|
||||||
else if (sentidoNorm == "V")
|
|
||||||
{
|
|
||||||
if (r.Contains("ida")) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gestiona token.
|
|
||||||
/// </summary>
|
|
||||||
private static bool ContainsToken(string haystack, string token)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(haystack) || string.IsNullOrWhiteSpace(token)) return false;
|
|
||||||
|
|
||||||
// match “token” con bordes razonables (no letra/dígito a los lados)
|
|
||||||
// ej: token "26" no debe casar con "1267"
|
|
||||||
for (int i = 0; i <= haystack.Length - token.Length; i++)
|
|
||||||
{
|
|
||||||
if (!haystack.AsSpan(i, token.Length).Equals(token.AsSpan(), StringComparison.OrdinalIgnoreCase))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
bool leftOk = (i == 0) || !char.IsLetterOrDigit(haystack[i - 1]);
|
|
||||||
bool rightOk = (i + token.Length >= haystack.Length) || !char.IsLetterOrDigit(haystack[i + token.Length]);
|
|
||||||
|
|
||||||
if (leftOk && rightOk) return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Obtiene linea itinerario sentido texto.
|
/// Obtiene linea itinerario sentido texto.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using RutasDBUS.Modelos.Planificacion;
|
using RutasDBUS.Modelos.Planificacion;
|
||||||
@@ -1560,65 +1560,6 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
|||||||
return minimo <= 1;
|
return minimo <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<ResultadoBusquedaRutaBusDbus> SeleccionarPoolRefinamientoExacto(
|
|
||||||
IEnumerable<ResultadoBusquedaRutaBusDbus> candidatas,
|
|
||||||
ParametrosPlanificadorRuta parametros)
|
|
||||||
{
|
|
||||||
var lista = candidatas.ToList();
|
|
||||||
if (lista.Count == 0)
|
|
||||||
return new();
|
|
||||||
|
|
||||||
var maxAlternativas = Math.Max(1, parametros.MaxAlternativasBusPorPar);
|
|
||||||
var limiteTotal = Math.Max(24, maxAlternativas * 8);
|
|
||||||
var seleccionadas = new List<ResultadoBusquedaRutaBusDbus>(limiteTotal);
|
|
||||||
var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
Agregar(lista
|
|
||||||
.OrderBy(x => x.PuntuacionCoste)
|
|
||||||
.ThenBy(x => x.DuracionEstimadaSegundos)
|
|
||||||
.ThenBy(x => x.RutaBus.NumeroTransbordos)
|
|
||||||
.ThenBy(x => x.DistanciaPieTotal)
|
|
||||||
.Take(Math.Max(10, limiteTotal / 2)));
|
|
||||||
|
|
||||||
Agregar(lista
|
|
||||||
.OrderBy(x => x.DistanciaPieTotal)
|
|
||||||
.ThenBy(x => x.DuracionEstimadaSegundos)
|
|
||||||
.ThenBy(x => x.PuntuacionCoste)
|
|
||||||
.Take(Math.Max(8, limiteTotal / 3)));
|
|
||||||
|
|
||||||
Agregar(lista
|
|
||||||
.OrderBy(x => x.DuracionEstimadaSegundos)
|
|
||||||
.ThenBy(x => x.PuntuacionCoste)
|
|
||||||
.ThenBy(x => x.DistanciaPieTotal)
|
|
||||||
.Take(Math.Max(8, limiteTotal / 4)));
|
|
||||||
|
|
||||||
Agregar(lista
|
|
||||||
.OrderBy(x => x.RutaBus.NumeroTransbordos)
|
|
||||||
.ThenBy(x => x.DuracionEstimadaSegundos)
|
|
||||||
.ThenBy(x => x.DistanciaPieTotal)
|
|
||||||
.Take(Math.Max(6, limiteTotal / 4)));
|
|
||||||
|
|
||||||
return seleccionadas
|
|
||||||
.OrderBy(x => x.PuntuacionCoste)
|
|
||||||
.ThenBy(x => x.DuracionEstimadaSegundos)
|
|
||||||
.ThenBy(x => x.RutaBus.NumeroTransbordos)
|
|
||||||
.ThenBy(x => x.DistanciaPieTotal)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
void Agregar(IEnumerable<ResultadoBusquedaRutaBusDbus> origen)
|
|
||||||
{
|
|
||||||
foreach (var candidata in origen)
|
|
||||||
{
|
|
||||||
if (seleccionadas.Count >= limiteTotal)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var firma = UtilidadesTransitoDbus.FirmaRutaBus(candidata.RutaBus);
|
|
||||||
if (firmas.Add(firma))
|
|
||||||
seleccionadas.Add(candidata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ResolverSegmentoHorario(
|
private bool ResolverSegmentoHorario(
|
||||||
PatronTransitoDbus patron,
|
PatronTransitoDbus patron,
|
||||||
string codigoParadaInicio,
|
string codigoParadaInicio,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1544,17 +1544,6 @@ html, body {
|
|||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
.num-field {
|
|
||||||
position: relative;
|
|
||||||
width: 140px; /* igual que la 2ª columna */
|
|
||||||
min-width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.num-field .config-input {
|
|
||||||
width: 100% !important; /* pisa el width:90px anterior */
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding-right: 2.4rem; /* espacio para ?? */
|
|
||||||
}
|
|
||||||
.num-inline {
|
.num-inline {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1.6rem 1.6rem; /* input | ? | ? */
|
grid-template-columns: 1fr 1.6rem 1.6rem; /* input | ? | ? */
|
||||||
@@ -1583,27 +1572,6 @@ html, body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-dual-field {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto minmax(0, 4.5rem);
|
|
||||||
gap: .3rem;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-dual-field span {
|
|
||||||
font-size: .7rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
line-height: 1;
|
|
||||||
text-align: right;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-dual-field .config-input {
|
|
||||||
max-width: 4.5rem;
|
|
||||||
justify-self: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-dual-inline--bare {
|
.config-dual-inline--bare {
|
||||||
grid-template-columns: repeat(2, minmax(0, 4.2rem));
|
grid-template-columns: repeat(2, minmax(0, 4.2rem));
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
@@ -1669,51 +1637,6 @@ html, body {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
.num-stepper {
|
|
||||||
position: absolute;
|
|
||||||
right: .25rem; /* un pelín más metido */
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: .2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.step-btn {
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.05rem;
|
|
||||||
border-radius: .3rem;
|
|
||||||
border: 1px solid rgba(148,163,184,.45);
|
|
||||||
background: rgba(30,41,59,.85);
|
|
||||||
color: #e5e7eb;
|
|
||||||
font-size: .6rem;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-btn:hover {
|
|
||||||
background: rgba(37,99,235, .9);
|
|
||||||
border-color: rgba(59,130,246,.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-btn:disabled {
|
|
||||||
opacity: .42;
|
|
||||||
cursor: not-allowed;
|
|
||||||
color: #94a3b8;
|
|
||||||
background: rgba(30,41,59,.45);
|
|
||||||
border-color: rgba(100,116,139,.25);
|
|
||||||
filter: saturate(.6) blur(.15px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-btn:disabled:hover {
|
|
||||||
background: rgba(30,41,59,.45);
|
|
||||||
border-color: rgba(100,116,139,.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ✅ Solo esta fila: label arriba + control abajo, ancho completo */
|
/* ✅ Solo esta fila: label arriba + control abajo, ancho completo */
|
||||||
.config-row.config-row-full {
|
.config-row.config-row-full {
|
||||||
grid-template-columns: 1fr !important;
|
grid-template-columns: 1fr !important;
|
||||||
@@ -2143,6 +2066,8 @@ html, body {
|
|||||||
color: #f8fafc !important;
|
color: #f8fafc !important;
|
||||||
font-size: .68rem !important;
|
font-size: .68rem !important;
|
||||||
font-weight: 800 !important;
|
font-weight: 800 !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
white-space: nowrap;
|
||||||
padding: 2px 6px !important;
|
padding: 2px 6px !important;
|
||||||
box-shadow: 0 8px 18px rgba(0, 0, 0, .35) !important;
|
box-shadow: 0 8px 18px rgba(0, 0, 0, .35) !important;
|
||||||
}
|
}
|
||||||
@@ -2221,16 +2146,6 @@ html, body {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Burbuja de elevación arriba derecha */
|
|
||||||
.elev-bubble-topright {
|
|
||||||
position: fixed;
|
|
||||||
top: 70px; /* ajusta según altura real del header */
|
|
||||||
right: 14px;
|
|
||||||
z-index: 1200;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Si tu header es más alto/bajo, sube/baja estos top */
|
/* Si tu header es más alto/bajo, sube/baja estos top */
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.popup-elev-topright {
|
.popup-elev-topright {
|
||||||
@@ -2440,43 +2355,12 @@ html, body {
|
|||||||
/* mantén tus estilos actuales */
|
/* mantén tus estilos actuales */
|
||||||
}
|
}
|
||||||
|
|
||||||
.gm-search-panel-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-bottom: .35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gm-search-hide-btn {
|
.gm-search-hide-btn {
|
||||||
padding: .2rem .45rem;
|
padding: .2rem .45rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: .95rem;
|
font-size: .95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* botón flotante cuando el buscador está oculto */
|
|
||||||
.gm-search-fab {
|
|
||||||
position: absolute;
|
|
||||||
top: 14px;
|
|
||||||
right: 14px; /* ✅ derecha */
|
|
||||||
left: auto; /* ✅ anulamos izquierda si estaba */
|
|
||||||
z-index: 1000;
|
|
||||||
border: 1px solid rgba(255,255,255,.18);
|
|
||||||
background: rgba(15, 23, 42, .92);
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 999px;
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 8px 20px rgba(0,0,0,.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gm-search-fab:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* 3 columnas: "Fecha:" | date | time (igual idea que Rango: min/max) */
|
/* 3 columnas: "Fecha:" | date | time (igual idea que Rango: min/max) */
|
||||||
.line-filter__range--fecha {
|
.line-filter__range--fecha {
|
||||||
grid-template-columns: minmax(0, 1.12fr) minmax(0, 0.78fr) auto !important;
|
grid-template-columns: minmax(0, 1.12fr) minmax(0, 0.78fr) auto !important;
|
||||||
|
|||||||
@@ -178,9 +178,22 @@ window.rt.drawRoute = function (id, coords, options) {
|
|||||||
|
|
||||||
const segmentLine = buildLine(segmentCoords, segmentOpts);
|
const segmentLine = buildLine(segmentCoords, segmentOpts);
|
||||||
const slope = segment && typeof segment.slope === "number" ? segment.slope : null;
|
const slope = segment && typeof segment.slope === "number" ? segment.slope : null;
|
||||||
const label = slope !== null && Number.isFinite(slope)
|
const distance = segment && typeof segment.distance === "number" ? segment.distance : null;
|
||||||
? ((slope > 0 ? "+" : "") + slope.toFixed(1) + "%")
|
const elevation = segment && typeof segment.elevation === "number" ? segment.elevation : null;
|
||||||
: (segment && segment.label ? String(segment.label) : "");
|
const slopeText = slope !== null && Number.isFinite(slope)
|
||||||
|
? ((slope > 0 ? "+" : "") + slope.toFixed(1) + " %")
|
||||||
|
: (segment && segment.label ? String(segment.label) : "-");
|
||||||
|
const distanceText = distance !== null && Number.isFinite(distance)
|
||||||
|
? distance.toFixed(2) + " m"
|
||||||
|
: "-";
|
||||||
|
const elevationText = elevation !== null && Number.isFinite(elevation)
|
||||||
|
? elevation.toFixed(2) + " m"
|
||||||
|
: "-";
|
||||||
|
const label = [
|
||||||
|
"Pendiente: " + slopeText,
|
||||||
|
"Dist. anterior: " + distanceText,
|
||||||
|
"Altura: " + elevationText
|
||||||
|
].join("<br>");
|
||||||
|
|
||||||
if (label) {
|
if (label) {
|
||||||
segmentLine.bindTooltip(label, {
|
segmentLine.bindTooltip(label, {
|
||||||
@@ -729,14 +742,6 @@ window.rt.toggleItStops = function (id, stops, options) {
|
|||||||
window.rt._itStopGroups[id] = group;
|
window.rt._itStopGroups[id] = group;
|
||||||
};
|
};
|
||||||
|
|
||||||
// (opcional) limpiar todo, por si quieres usarlo en algún sitio
|
|
||||||
window.rt.clearItStops = function () {
|
|
||||||
if (!window.rt.ensureItStopsLayer()) return;
|
|
||||||
window.rt._itStopsLayer.clearLayers();
|
|
||||||
window.rt._itStopGroups = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Route solution stops layer (paradas destacadas de la solución seleccionada)
|
// Route solution stops layer (paradas destacadas de la solución seleccionada)
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user