Optimizacion de codigo, limpieza residual y actualizacionde hover de elevaciones

This commit is contained in:
2026-07-14 12:13:53 +02:00
parent 4c71402e24
commit ba502e8ebf
12 changed files with 22 additions and 2931 deletions

View File

@@ -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>

View File

@@ -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;

View File

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

View File

@@ -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) + " %"

View File

@@ -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 = "";

View File

@@ -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>

View File

@@ -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);

View File

@@ -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.

View File

@@ -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

View File

@@ -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;

View File

@@ -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)
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------