diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs index f2f32fe..4676425 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs @@ -29,9 +29,6 @@ public partial class PlanificadorRutas : ComponentBase private bool _pintandoParadas = false; 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 _versionPintadoRuta = 0; private CancellationTokenSource? _ctsCalculo; @@ -511,23 +508,6 @@ public partial class PlanificadorRutas : ComponentBase return stops; } - /// - /// Construye el payload JS de paradas de un tramo de bus. - /// - private List BuildStopsForTramoBus(TramoBus tramo) - => BuildStopsDesdeParadas(BuildParadasForTramoBus(tramo)); - - /// - /// Registra paradas visibles de un itinerario activo para permitir popup aunque la capa general esté oculta. - /// - private void RegistrarParadasItinerarioVisibles(string id, IEnumerable paradas) - { - _paradasItinerarioVisiblesPorId[id] = paradas - .GroupBy(p => p.Codigo, StringComparer.OrdinalIgnoreCase) - .Select(g => g.First()) - .ToList(); - } - /// /// Elimina el registro de paradas visibles de un itinerario activo. /// @@ -638,53 +618,12 @@ public partial class PlanificadorRutas : ComponentBase } - - /// - /// Gestiona linea completa variante. - /// - private double[,]? ConstruirLineaCompletaVariante(InfoVariante v) - { - if (v is null || v.CodigosParadas is null || v.CodigosParadas.Count < 2) return null; - - var pts = new List(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 private bool _activarRangoLineas = false; private int _lineaMin = 1; private int _lineaMax = 200; private readonly HashSet _lineasPermitidas = new(); - /// - /// Gestiona dentro rango. - /// - 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); - } - /// @@ -859,19 +798,6 @@ public partial class PlanificadorRutas : ComponentBase linea[0, 1] = punto[1]; } - /// - /// Sobrescribe el ultimo punto de una linea. - /// - 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]; - } - /// /// Obtiene la distancia entre dos puntos geograficos. /// @@ -1617,45 +1543,6 @@ public partial class PlanificadorRutas : ComponentBase ? alternativa.Etiqueta : $"Opcion {indice + 1}"; } - - /// - /// Obtiene las siglas compactas de categorías adicionales para una alternativa final. - /// - private List 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(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; - } - - /// - /// Comprueba si la etiqueta principal ya corresponde a una categoría destacada concreta. - /// - private static bool EtiquetaPrincipalEs(string? etiquetaActual, string etiquetaEsperada) - => string.Equals(etiquetaActual?.Trim(), etiquetaEsperada, StringComparison.OrdinalIgnoreCase); /// diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs index e04ffb3..75a9562 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs @@ -289,22 +289,6 @@ public partial class PlanificadorRutas : ComponentBase } } - /// - /// Indica si match wildcard. - /// - 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; // 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 _maxTransbordosPermitidos = 3; - - /// - /// Indica si cumple filtro transbordos. - /// - 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; - } - /// /// Indica si esta popup puntos abierto. /// @@ -639,15 +605,6 @@ public partial class PlanificadorRutas : ComponentBase var idx = Math.Abs(StableHash(key)) % _palette.Length; return _palette[idx]; } - /// - /// Gestiona color clave for variante. - /// - 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 = ""; @@ -678,15 +635,6 @@ public partial class PlanificadorRutas : ComponentBase _selectItKey++; } } - - /// - /// Gestiona it filtro busqueda. - /// - private void OnItFiltroSearch(ChangeEventArgs e) - { - ItFiltro = e?.Value?.ToString() ?? string.Empty; - StateHasChanged(); - } private IEnumerable _itItemsFiltrados => string.IsNullOrWhiteSpace(_itFiltro) ? _itItems @@ -882,23 +830,6 @@ public partial class PlanificadorRutas : ComponentBase } - /// - /// Indica si match wildcard contains. - /// - 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; } - /// - /// Gestiona itis de alternativa. - /// - 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 const string _versionApp = "(v-20260713a)"; + private const string _versionApp = "(v-20260714a)"; private double? _paradaElevG; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs index 25f61fd..4acc6ef 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs @@ -18,26 +18,6 @@ namespace RutasDBUS.Components.Pages; public partial class PlanificadorRutas : ComponentBase { - // ========================================================= - // Itinerarios: rango (estricto durante BFS) - // ========================================================= - - /// - /// Gestiona dentro rango. - /// - 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; - } - - /// - /// Gestiona permitida. - /// - private bool VariantePermitida(InfoVariante v) - => ItinerarioDentroRango(v.CodigoItinerario) && LineaDentroRango(v); private IReadOnlyList ObtenerLineasPermitidasSeleccionadas() => _lineasPermitidas @@ -121,158 +101,6 @@ public partial class PlanificadorRutas : ComponentBase _busFilteredCache.Clear(); await PersistirConfiguracionAsync(); } - - - // ========================================================= - // Selección de candidatas: por ubicación, pero SOLO si están - // en itinerarios permitidos - // ========================================================= - - /// - /// Gestiona tiene aparicion permitida. - /// - 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; - } - - /// - /// Selecciona candidatas solo itinerarios. - /// - private async Task> 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(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(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 - // ========================================================= - - /// - /// Gestiona alt. - /// - private static string FmtAlt(double? v) => v is null ? "-" : $"{Math.Round(v.Value):0} m"; - - /// - /// Formatea altitudes tramo. - /// - private static string FormatearAltitudesTramo(double? ini, double? fin, double? min, double? max) - { - var sb = new System.Text.StringBuilder(); - - sb.Append("
"); - sb.Append($"
Altitud:
"); - sb.Append($"
• (ini→fin) = {FmtAlt(ini)} → {FmtAlt(fin)}
"); - - if (min is not null && max is not null) - sb.Append($"
• (min/max) = {Math.Round(min.Value):0} m – {Math.Round(max.Value):0} m
"); - - sb.Append("
"); - - return sb.ToString(); - } - - - - - - /// - /// Obtiene elevacion punto. - /// - private async Task 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; - } - } /// /// 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); - } - - /// - /// Formatea elevacion minimo maximo. - /// - 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"; - } - /// /// Gestiona geometria completa variante. /// @@ -1115,167 +884,6 @@ public partial class PlanificadorRutas : ComponentBase return tramos; } - /// - /// Busca rutas bus alternativas filtradas. - /// - private IReadOnlyList 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(Math.Max(1, maxAlternativas)); - if (maxAlternativas <= 0) - { - _busFilteredCache[key] = res; - return res; - } - - // Filtro por itinerarios/lineas durante BFS - Func filtro = _ => true; - if (_activarRangoItinerarios || _activarRangoLineas) - filtro = VariantePermitida; - - string FirmaRuta(List 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(StringComparer.OrdinalIgnoreCase); - - // Cola de "prohibiciones" para forzar alternativas - var prohibicionesPendientes = new Queue>(); - prohibicionesPendientes.Enqueue(new HashSet()); // principal - - // Evitar repetir exactamente la misma combinación de variantes prohibidas - var firmasProhibiciones = new HashSet(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(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 - // ========================================================= - - /// - /// Gestiona iv. - /// - private static string SentidoIV(int indiceInicio, int indiceFin) - => indiceFin >= indiceInicio ? "I" : "V"; - /// /// Normaliza sentido codigo. /// @@ -1335,29 +943,6 @@ public partial class PlanificadorRutas : ComponentBase return UtilidadesTransitoDbus.ContarTransbordosReales(alternativa?.Tramos); } - - - /// - /// Gestiona aprox seg. - /// - 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 // ========================================================= @@ -1629,186 +1214,6 @@ public partial class PlanificadorRutas : ComponentBase StateHasChanged(); } - // ========================================================= - // Duración exacta (tarjetas = detalle) + velocidad a pie configurada - // ========================================================= - - /// - /// Calcula duracion exacta. - /// - private async Task 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; - } - /// /// Busca ruta desde buscador. /// @@ -2255,34 +1660,6 @@ public partial class PlanificadorRutas : ComponentBase } - - - - /// - /// Gestiona alternativa solo a pie. - /// - 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 // ========================================================= diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs index 69e8c8e..37b3f29 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs @@ -215,6 +215,8 @@ public partial class PlanificadorRutas : ComponentBase color, category = categoria, slope = pendiente, + distance = distancia, + elevation = elevActual, label = pendiente is null ? "-" : pendiente.Value.ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture) + " %" diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs index 62092f7..85805c2 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs @@ -23,12 +23,6 @@ public partial class PlanificadorRutas : ComponentBase /// private static string HHmm(DateTime dt) => dt.ToString("HH:mm", CultureInfo.InvariantCulture); - /// - /// - /// - private static DateTime Compose(DateOnly d, TimeOnly t) - => d.ToDateTime(t); - /// /// Añade seconds safe. /// @@ -225,26 +219,6 @@ public partial class PlanificadorRutas : ComponentBase private List _nextDeps = new(); /// - /// Gestiona txt desde ruta id. - /// - 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}"; - } - /// /// Gestiona toggle hora simulada. /// private async Task OnToggleHoraSimulada(ChangeEventArgs e) @@ -348,19 +322,6 @@ public partial class PlanificadorRutas : ComponentBase StateHasChanged(); } - /// - /// Restablece hora pruebas. - /// - private async Task ResetHoraPruebas() - { - _usarHoraSimulada = false; - _fechaSimulada = DateOnly.FromDateTime(DateTime.Now); - _horaSimulada = TimeOnly.FromDateTime(DateTime.Now); - - StartClock(); - await RefrescarHoraUIAsync(); - } - private string _horaAhoraTxt = ""; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.razor b/RutasDBUS/Components/Pages/PlanificadorRutas.razor index 2ce076f..6a6c4a2 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.razor +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.razor @@ -1484,14 +1484,6 @@ @onblur="OnBlurCoeficienteEsfuerzoAltura" /> -@*
- -
- - - -
-
*@
diff --git a/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs b/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs index 6b9a7dc..dbc5ca6 100644 --- a/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs +++ b/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs @@ -10,8 +10,6 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio { private const string RutaCallejeroZipRelativa = "Modelos/Callejero/GFA_DSET_AD_CSV.zip"; private const string NombreCsvB5m = "GFA_DSET_AD.csv"; - private const int MaxResultadosInternos = 60; - private readonly IWebHostEnvironment _entorno; private readonly SemaphoreSlim _semaforoCarga = new(1, 1); private readonly ConcurrentDictionary _cacheCercanos = new(StringComparer.Ordinal); diff --git a/RutasDBUS/Servicios/Horarios/ServicioHorariosDbus.cs b/RutasDBUS/Servicios/Horarios/ServicioHorariosDbus.cs index 17da525..ef4c6cc 100644 --- a/RutasDBUS/Servicios/Horarios/ServicioHorariosDbus.cs +++ b/RutasDBUS/Servicios/Horarios/ServicioHorariosDbus.cs @@ -1,6 +1,4 @@ using System.Globalization; -using System.IO.Compression; -using System.Text; using System.Text.RegularExpressions; using RutasDBUS.Modelos.Importacion; using RutasDBUS.Servicios.Importacion; @@ -34,16 +32,6 @@ namespace RutasDBUS.Servicios.Horarios private bool _loaded; private ArchivoImportacionDbus? _archivoCargado; - /// - /// Compatibilidad con la API anterior. La resolución real del ZIP la hace la capa de importación compartida. - /// - public string CarpetaRelativa { get; set; } = Path.Combine("Modelos", "horarios"); - - /// - /// Compatibilidad con la API anterior. La selección real del ZIP la hace la capa de importación compartida. - /// - public string? NombreArchivoZip { get; set; } = null; - // ========================= // Modelos mínimos (CSV dBUS) // ========================= @@ -334,437 +322,6 @@ namespace RutasDBUS.Servicios.Horarios if (id is null) return new(); return GetNextDepartures(id.Value, date, now, take, includePast); } - // ========================= - // Carga desde ZIP - // ========================= - - /// - /// Carga from zip. - /// - 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(); - } - - /// - /// Carga from gtfs zip inside. - /// - 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); - } - - /// - /// Busca entrada. - /// - 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) - // ========================= - /// - /// Lee csv all. - /// - private static async Task> ReadCsvAllAsync(ZipArchiveEntry entry, CancellationToken ct) - { - using var stream = entry.Open(); - using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); - - var rows = new List(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; - } - - /// - /// Gestiona csv linea. - /// - private static string[] SplitCsvLine(string line) - { - // separador coma, con soporte básico de comillas dobles - var res = new List(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(); - } - - /// - /// Analiza date yyyy mm dd. - /// - private static DateOnly ParseDate_YYYY_MM_DD(string s) - => DateOnly.ParseExact(s.Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture); - - /// - /// Analiza date yyyymmdd. - /// - private static DateOnly ParseDate_YYYYMMDD(string s) - => DateOnly.ParseExact(s.Trim(), "yyyyMMdd", CultureInfo.InvariantCulture); - - /// - /// Analiza tiempo hh mm ss. - /// - private static TimeOnly ParseTime_HH_MM_SS(string s) - => TimeOnly.ParseExact(s.Trim(), "HH:mm:ss", CultureInfo.InvariantCulture); - - /// - /// Analiza tiempo nullable hh mm ss. - /// - private static TimeOnly? ParseTimeNullable_HH_MM_SS(string? s) - { - if (string.IsNullOrWhiteSpace(s)) return null; - return ParseTime_HH_MM_SS(s); - } - - // ========================= - // Loaders dBUS CSV - // ========================= - /// - /// Carga paradas from dbus. - /// - 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; - } - } - - /// - /// Carga calendarios dbus. - /// - 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; - } - } - - /// - /// Carga cal ex dbus. - /// - 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(); - list.Add(row); - } - } - - /// - /// Carga expediciones dbus. - /// - 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; - } - } /// /// Normaliza linea. @@ -888,64 +445,6 @@ namespace RutasDBUS.Servicios.Horarios return string.IsNullOrWhiteSpace(patron) ? "0" : patron; } - /// - /// Carga tiempo parada dbus. - /// - 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(64); - list.Add(row); - } - - // ordenar por secuencia - foreach (var kv in _stopTimesByExp) - kv.Value.Sort((a, b) => a.Sec.CompareTo(b.Sec)); - } - /// /// Obtiene linea itinerario sentido nombre texto. /// @@ -1072,266 +571,6 @@ namespace RutasDBUS.Servicios.Horarios return "?"; } - // ========================= - // Loaders GTFS (fallback) - // ========================= - - /// - /// Carga paradas gtfs. - /// - 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; - } - } - - /// - /// Carga calendario gtfs. - /// - 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); - } - } - - /// - /// Carga calendario dates gtfs. - /// - 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(); - list.Add(row); - } - } - - /// - /// Carga viajes gtfs. - /// - 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 - ); - } - } - /// - /// Carga parada tiempos gtfs. - /// - 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(64); - list.Add(row); - } - - foreach (var kv in _stopTimesByExp) - kv.Value.Sort((a, b) => a.Sec.CompareTo(b.Sec)); - } - - /// - /// Analiza gtfs tiempo safe. - /// - 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 // ========================= @@ -1388,17 +627,6 @@ namespace RutasDBUS.Servicios.Horarios : $"L\u00ednea ?"; } - /// - /// Gestiona of. - /// - 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; - } - /// /// Normaliza codigo parada. /// @@ -1592,107 +820,6 @@ namespace RutasDBUS.Servicios.Horarios return false; } - /// - /// Normaliza sentido. - /// - 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 "?"; - } - - /// - /// Gestiona to direccion. - /// - private static int? SentidoToDireccion(string sentidoNorm) - { - // dBUS/GTFS suele usar 0/1 - return sentidoNorm switch - { - "I" => 0, - "V" => 1, - _ => null - }; - } - - /// - /// Gestiona matches. - /// - 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; - } - - /// - /// Gestiona token. - /// - 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; - } - /// /// Obtiene linea itinerario sentido texto. diff --git a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs index 6571b08..ddd40db 100644 --- a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs +++ b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Globalization; using Microsoft.AspNetCore.Hosting; using RutasDBUS.Modelos.Planificacion; @@ -1560,65 +1560,6 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas return minimo <= 1; } - private static List SeleccionarPoolRefinamientoExacto( - IEnumerable 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(limiteTotal); - var firmas = new HashSet(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 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( PatronTransitoDbus patron, string codigoParadaInicio, diff --git a/RutasDBUS/Servicios/RedBus/RedBusServicio.cs b/RutasDBUS/Servicios/RedBus/RedBusServicio.cs index c9b1904..2ab3a43 100644 --- a/RutasDBUS/Servicios/RedBus/RedBusServicio.cs +++ b/RutasDBUS/Servicios/RedBus/RedBusServicio.cs @@ -1,8 +1,3 @@ -using System.Globalization; -using System.IO.Compression; -using System.Text; -using System.Text.RegularExpressions; -using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using RutasDBUS.Modelos.Importacion; @@ -14,8 +9,6 @@ namespace RutasDBUS.Servicios.RedBus; public class RedBusServicio : IRedBusServicio { private const string ColorVisualLinea40Argb = "FF9A7B00"; - - private readonly IWebHostEnvironment _entorno; private readonly OpcionesRedBus _opciones; private readonly ILogger _logger; private readonly IServicioArchivoImportacionDbus _archiveService; @@ -47,13 +40,11 @@ public class RedBusServicio : IRedBusServicio /// Inicializa una nueva instancia de la clase RedBusServicio. /// public RedBusServicio( - IWebHostEnvironment entorno, IOptions opciones, ILogger logger, IServicioArchivoImportacionDbus archiveService, CreadorRedTransporteDbus networkBuilder) { - _entorno = entorno; _opciones = opciones.Value ?? new OpcionesRedBus(); _logger = logger; _archiveService = archiveService; @@ -136,36 +127,6 @@ public class RedBusServicio : IRedBusServicio } } - /// - /// Resuelve zip. - /// - private string? ResolverZip() - { - if (!string.IsNullOrWhiteSpace(_opciones.ArchivoDatos)) - { - var ruta = Path.Combine( - _entorno.ContentRootPath, - _opciones.ArchivoDatos.Replace('/', Path.DirectorySeparatorChar)); - - if (File.Exists(ruta)) - return ruta; - } - - var carpeta = Path.Combine(_entorno.ContentRootPath, "Modelos", "horarios"); - if (!Directory.Exists(carpeta)) - return null; - - var zips = Directory.GetFiles(carpeta, "*.zip", SearchOption.TopDirectoryOnly); - if (zips.Length == 0) - return null; - - return zips - .Select(x => new FileInfo(x)) - .OrderByDescending(x => x.LastWriteTimeUtc) - .First() - .FullName; - } - /// /// Gestiona todo. /// @@ -186,761 +147,6 @@ public class RedBusServicio : IRedBusServicio linea.ColorArgb = ColorVisualLinea40Argb; } - // ========================================================= - // ZIP - // ========================================================= - - /// - /// Inicializa una nueva instancia de la clase ZipParadaRow. - /// - private sealed record ZipParadaRow( - int Id, - string CodigoSms, - string Nombre, - double X, - double Y, - string Descripcion); - - /// - /// Inicializa una nueva instancia de la clase ZipRutaRow. - /// - private sealed record ZipRutaRow( - string Id, - string Abreviatura, - string Nombre, - string Descripcion, - string Color); - - /// - /// Inicializa una nueva instancia de la clase ZipExpedicionRow. - /// - private sealed record ZipExpedicionRow( - string Id, - string IdRuta, - string DirDestino, - int Direccion, - string IdPatronExp, - string IdCal); - - /// - /// Inicializa una nueva instancia de la clase ZipTiempoParadaRow. - /// - private sealed record ZipTiempoParadaRow( - string IdExp, - int StopId, - int Secuencia); - - /// - /// Inicializa una nueva instancia de la clase ZipPatronPointRow. - /// - private sealed record ZipPatronPointRow( - string IdPatron, - int Secuencia, - double X, - double Y); - - /// - /// Gestiona desde zip. - /// - private async Task CargarDesdeZipAsync(string zipAbsPath, CancellationToken ct) - { - using var fs = File.OpenRead(zipAbsPath); - using var zip = new ZipArchive(fs, ZipArchiveMode.Read, leaveOpen: false); - - var eParada = FindEntry(zip, "parada.csv"); - var eRuta = FindEntry(zip, "ruta.csv"); - var eExp = FindEntry(zip, "expedicion.csv"); - var eTiempo = FindEntry(zip, "tiempo_parada.csv"); - var ePatron = FindEntry(zip, "patron.csv"); - - if (eParada is null || eRuta is null || eExp is null || eTiempo is null || ePatron is null) - throw new InvalidOperationException("El ZIP no contiene los CSV mínimos esperados."); - - var paradas = await LeerParadasZipAsync(eParada, ct); - var rutas = await LeerRutasZipAsync(eRuta, ct); - var expediciones = await LeerExpedicionesZipAsync(eExp, ct); - var tiempos = await LeerTiemposParadaZipAsync(eTiempo, ct); - var patrones = await LeerPatronesZipAsync(ePatron, ct); - - ConstruirModeloDesdeZip(paradas, rutas, expediciones, tiempos, patrones); - } - - /// - /// Gestiona modelo desde zip. - /// - private void ConstruirModeloDesdeZip( - List paradasRows, - List rutasRows, - List expedicionesRows, - List tiempoRows, - Dictionary> patrones) - { - var paradaById = new Dictionary(); - var paradaByCodigo = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // ===================================================== - // 1) PARADAS - // ===================================================== - foreach (var p in paradasRows) - { - var (lat, lon) = ConvertirCoordenadaZipALatLon(p.X, p.Y); - - if (!EsLatLonValida(lat, lon)) - { - _logger.LogWarning( - "Parada descartada por coordenada inválida: id={Id} codigo={Codigo} nombre={Nombre} X={X} Y={Y}", - p.Id, p.CodigoSms, p.Nombre, p.X, p.Y); - continue; - } - - var codigo = !string.IsNullOrWhiteSpace(p.CodigoSms) - ? NormalizarCodigoParada(p.CodigoSms) - : NormalizarCodigoParada(p.Id.ToString(CultureInfo.InvariantCulture)); - - var parada = new Parada - { - Codigo = codigo, - Nombre = p.Nombre ?? "", - NombreCorto = !string.IsNullOrWhiteSpace(p.Descripcion) ? p.Descripcion : (p.Nombre ?? ""), - Abreviatura = p.Descripcion ?? "", - Altura = 0, - PreZona = "", - PostZona = "", - ZonaSalida = "", - Latitud = lat, - Longitud = lon - }; - - _paradas.Add(parada); - paradaById[p.Id] = parada; - paradaByCodigo[parada.Codigo] = parada; - } - - // ===================================================== - // 2) TIEMPOS POR EXPEDICIÓN - // ===================================================== - var tiemposPorExp = tiempoRows - .GroupBy(x => x.IdExp, StringComparer.OrdinalIgnoreCase) - .ToDictionary( - g => g.Key, - g => g.OrderBy(x => x.Secuencia).ToList(), - StringComparer.OrdinalIgnoreCase); - - // patrón -> secuencia de stop ids - var secuenciaParadasPorPatron = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - foreach (var exp in expedicionesRows) - { - if (string.IsNullOrWhiteSpace(exp.IdPatronExp)) - continue; - - if (!tiemposPorExp.TryGetValue(exp.Id, out var seq) || seq.Count < 2) - continue; - - if (!secuenciaParadasPorPatron.ContainsKey(exp.IdPatronExp)) - secuenciaParadasPorPatron[exp.IdPatronExp] = seq.Select(x => x.StopId).ToList(); - } - - // ===================================================== - // 3) RUTAS -> LÍNEAS - // ===================================================== - var lineaByRutaId = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var r in rutasRows) - { - var codigoLinea = !string.IsNullOrWhiteSpace(r.Abreviatura) - ? CodigoSinDecimal(r.Abreviatura) - : CodigoSinDecimal(r.Id); - - var lineaBus = new LineaBus - { - IdRuta = r.Id ?? "", - Codigo = codigoLinea, - Nombre = !string.IsNullOrWhiteSpace(r.Nombre) ? r.Nombre : (r.Descripcion ?? ""), - CodigoItinerarioPorDefecto = 0, - ColorArgb = NormalizarColorArgb(r.Color) - }; - - _lineas.Add(lineaBus); - lineaByRutaId[r.Id ?? ""] = lineaBus; - } - - // ===================================================== - // 4) EXPEDICIONES -> VARIANTES TEMPORALES - // ===================================================== - var variantesTemp = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var exp in expedicionesRows) - { - if (!lineaByRutaId.TryGetValue(exp.IdRuta, out var linea)) - continue; - - if (!secuenciaParadasPorPatron.TryGetValue(exp.IdPatronExp, out var stopIds) || stopIds.Count < 2) - continue; - - var sentido = DireccionZipASentido(exp.Direccion); - var codigoItinerario = ExtraerCodigoItinerario(exp.IdRuta, exp.IdPatronExp); - var codigoVariante = ExtraerCodigoVariante(exp.IdPatronExp); - - var key = $"{exp.IdRuta}|{codigoItinerario}|{sentido}|{exp.IdPatronExp}"; - - if (!variantesTemp.TryGetValue(key, out var temp)) - { - temp = new VarianteTemporal - { - RutaId = exp.IdRuta, - CodigoItinerario = codigoItinerario, - Sentido = sentido, - CodigoVariante = codigoVariante, - IdPatron = exp.IdPatronExp, - Destino = exp.DirDestino ?? "", - StopIds = stopIds - }; - variantesTemp[key] = temp; - } - } - - // ===================================================== - // 5) CREAR TRAYECTOS SINTÉTICOS - // ===================================================== - int nextTrayectoId = 1; - - foreach (var temp in variantesTemp.Values) - { - if (!lineaByRutaId.TryGetValue(temp.RutaId, out var linea)) - continue; - - var itinerario = linea.Itinerarios.FirstOrDefault(x => x.Codigo == temp.CodigoItinerario); - if (itinerario is null) - { - itinerario = new Itinerario { Codigo = temp.CodigoItinerario }; - linea.Itinerarios.Add(itinerario); - } - - var sentido = itinerario.Sentidos.FirstOrDefault(x => - string.Equals(NormalizarSentidoCodigo(x.Codigo), temp.Sentido, StringComparison.OrdinalIgnoreCase)); - - if (sentido is null) - { - sentido = new Sentido { Codigo = temp.Sentido }; - itinerario.Sentidos.Add(sentido); - } - - var variante = new Variante - { - Codigo = temp.CodigoVariante - }; - - var stopIdsValidos = temp.StopIds - .Where(paradaById.ContainsKey) - .ToList(); - - var codigosParadas = stopIdsValidos - .Select(id => paradaById[id].Codigo) - .Where(x => !string.IsNullOrWhiteSpace(x)) - .ToList(); - - if (codigosParadas.Count < 2) - continue; - - var patronPts = patrones.TryGetValue(temp.IdPatron, out var pts) - ? pts.OrderBy(x => x.Secuencia).ToList() - : new List(); - - var patronLatLon = patronPts - .Select(p => - { - var (lat, lon) = ConvertirCoordenadaZipALatLon(p.X, p.Y); - return new[] { lat, lon }; - }) - .Where(x => EsLatLonValida(x[0], x[1])) - .ToList(); - - var stopLatLon = stopIdsValidos - .Select(id => new[] { paradaById[id].Latitud, paradaById[id].Longitud }) - .ToList(); - - var segmentos = ConstruirSegmentosDelPatron(stopLatLon, patronLatLon); - - for (int i = 0; i < codigosParadas.Count - 1; i++) - { - var codOrigen = codigosParadas[i]; - var codDestino = codigosParadas[i + 1]; - - var puntosSegmento = (i < segmentos.Count && segmentos[i].Count >= 2) - ? segmentos[i] - : new List - { - new[] { paradaByCodigo[codOrigen].Latitud, paradaByCodigo[codOrigen].Longitud }, - new[] { paradaByCodigo[codDestino].Latitud, paradaByCodigo[codDestino].Longitud } - }; - - var trayecto = new Trayecto - { - Id = nextTrayectoId, - CodigoParadaOrigen = codOrigen, - CodigoParadaDestino = codDestino, - Posiciones = puntosSegmento - }; - - _trayectos[nextTrayectoId] = trayecto; - variante.IdsTrayectos.Add(nextTrayectoId); - nextTrayectoId++; - } - - if (variante.IdsTrayectos.Count > 0) - sentido.Variantes.Add(variante); - } - - foreach (var linea in _lineas) - { - linea.Itinerarios = linea.Itinerarios - .Where(it => it.Sentidos.Any(s => s.Variantes.Any())) - .OrderBy(it => it.Codigo) - .ToList(); - - foreach (var it in linea.Itinerarios) - { - it.Sentidos = it.Sentidos - .Where(s => s.Variantes.Any()) - .OrderBy(s => NormalizarSentidoCodigo(s.Codigo)) - .ToList(); - } - } - - _lineas.RemoveAll(l => l.Itinerarios.Count == 0); - } - - private sealed class VarianteTemporal - { - public string RutaId { get; set; } = ""; - public int CodigoItinerario { get; set; } - public string Sentido { get; set; } = ""; - public int CodigoVariante { get; set; } - public string IdPatron { get; set; } = ""; - public string Destino { get; set; } = ""; - public List StopIds { get; set; } = new(); - } - - // ========================================================= - // CSV - // ========================================================= - - /// - /// Busca entrada. - /// - private static ZipArchiveEntry? FindEntry(ZipArchive zip, string fileName) - { - return zip.Entries.FirstOrDefault(e => - e.FullName.EndsWith("/" + fileName, StringComparison.OrdinalIgnoreCase) || - e.FullName.Equals(fileName, StringComparison.OrdinalIgnoreCase)); - } - - /// - /// Lee csv. - /// - private static async Task> ReadCsvAsync(ZipArchiveEntry entry, CancellationToken ct) - { - using var stream = entry.Open(); - using var sr = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); - - var rows = new List(); - string? line; - while ((line = await sr.ReadLineAsync()) is not null) - { - ct.ThrowIfCancellationRequested(); - if (string.IsNullOrWhiteSpace(line)) continue; - rows.Add(SplitCsvLine(line)); - } - - return rows; - } - - /// - /// Gestiona csv linea. - /// - private static string[] SplitCsvLine(string line) - { - var res = new List(32); - var sb = new StringBuilder(line.Length); - bool inQuotes = false; - - for (int i = 0; i < line.Length; i++) - { - char c = line[i]; - - if (c == '"') - { - if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') - { - sb.Append('"'); - i++; - continue; - } - - inQuotes = !inQuotes; - continue; - } - - if (c == ',' && !inQuotes) - { - res.Add(sb.ToString().Trim()); - sb.Clear(); - continue; - } - - sb.Append(c); - } - - res.Add(sb.ToString().Trim()); - return res.ToArray(); - } - - /// - /// - /// - private static int Col(string[] header, string name) - { - for (int i = 0; i < header.Length; i++) - { - if (header[i].Equals(name, StringComparison.OrdinalIgnoreCase)) - return i; - } - return -1; - } - - /// - /// - /// - private static string Get(string[] row, int idx) - => idx >= 0 && idx < row.Length ? row[idx] : ""; - - /// - /// Lee paradas zip. - /// - private async Task> LeerParadasZipAsync(ZipArchiveEntry entry, CancellationToken ct) - { - var rows = await ReadCsvAsync(entry, ct); - if (rows.Count == 0) return new(); - - var h = rows[0]; - - int iId = Col(h, "ID"); - int iSms = Col(h, "CODIGO_SMS"); - int iNombre = Col(h, "NOMBRE_PAR"); - int iY = Col(h, "LATITUD_Y_PAR"); - int iX = Col(h, "LONGITUD_X_PAR"); - int iDesc = Col(h, "DESCRIPCION_PAR"); - - var res = new List(); - - foreach (var r in rows.Skip(1)) - { - if (!int.TryParse(Get(r, iId), NumberStyles.Integer, CultureInfo.InvariantCulture, out var id)) - continue; - - double.TryParse(Get(r, iX), NumberStyles.Any, CultureInfo.InvariantCulture, out var x); - double.TryParse(Get(r, iY), NumberStyles.Any, CultureInfo.InvariantCulture, out var y); - - res.Add(new ZipParadaRow( - id, - Get(r, iSms), - Get(r, iNombre), - x, - y, - Get(r, iDesc))); - } - - return res; - } - - /// - /// Lee rutas zip. - /// - private async Task> LeerRutasZipAsync(ZipArchiveEntry entry, CancellationToken ct) - { - var rows = await ReadCsvAsync(entry, ct); - if (rows.Count == 0) return new(); - - var h = rows[0]; - - int iId = Col(h, "ID"); - int iAbr = Col(h, "ABREVIATURA"); - int iNombre = Col(h, "NOMBRE"); - int iDesc = Col(h, "DESCRIPCION"); - int iColor = Col(h, "COLOR"); - - var res = new List(); - - foreach (var r in rows.Skip(1)) - { - var id = Get(r, iId); - if (string.IsNullOrWhiteSpace(id)) continue; - - res.Add(new ZipRutaRow( - id, - Get(r, iAbr), - Get(r, iNombre), - Get(r, iDesc), - Get(r, iColor))); - } - - return res; - } - - /// - /// Lee expediciones zip. - /// - private async Task> LeerExpedicionesZipAsync(ZipArchiveEntry entry, CancellationToken ct) - { - var rows = await ReadCsvAsync(entry, ct); - if (rows.Count == 0) return new(); - - var h = rows[0]; - - int iId = Col(h, "ID"); - int iRuta = Col(h, "ID_RUTA"); - int iDestino = Col(h, "DIR_DESTINO"); - int iDir = Col(h, "DIRECCION"); - int iPatron = Col(h, "ID_PATRON_EXP"); - int iCal = Col(h, "ID_CAL"); - - var res = new List(); - - foreach (var r in rows.Skip(1)) - { - var id = Get(r, iId); - if (string.IsNullOrWhiteSpace(id)) continue; - - int.TryParse(Get(r, iDir), NumberStyles.Integer, CultureInfo.InvariantCulture, out var dir); - - res.Add(new ZipExpedicionRow( - id, - Get(r, iRuta), - Get(r, iDestino), - dir, - Get(r, iPatron), - Get(r, iCal))); - } - - return res; - } - - /// - /// Lee tiempos parada zip. - /// - private async Task> LeerTiemposParadaZipAsync(ZipArchiveEntry entry, CancellationToken ct) - { - var rows = await ReadCsvAsync(entry, ct); - if (rows.Count == 0) return new(); - - var h = rows[0]; - - int iExp = Col(h, "ID_EXP"); - int iStop = Col(h, "ID_TIEMPO_PAR"); - int iSec = Col(h, "SEC_PAR"); - - var res = new List(); - - foreach (var r in rows.Skip(1)) - { - var expId = Get(r, iExp); - if (string.IsNullOrWhiteSpace(expId)) continue; - - if (!int.TryParse(Get(r, iStop), NumberStyles.Integer, CultureInfo.InvariantCulture, out var stopId)) - continue; - - if (!int.TryParse(Get(r, iSec), NumberStyles.Integer, CultureInfo.InvariantCulture, out var sec)) - continue; - - res.Add(new ZipTiempoParadaRow(expId, stopId, sec)); - } - - return res; - } - - /// - /// Lee patrones zip. - /// - private async Task>> LeerPatronesZipAsync(ZipArchiveEntry entry, CancellationToken ct) - { - var rows = await ReadCsvAsync(entry, ct); - if (rows.Count == 0) return new(StringComparer.OrdinalIgnoreCase); - - var h = rows[0]; - - int iId = Col(h, "ID"); - int iY = Col(h, "PTO_LATITUD_Y"); - int iX = Col(h, "PTO_LONGITUD_X"); - int iSec = Col(h, "PTO_SECUENCIA"); - - var res = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - foreach (var r in rows.Skip(1)) - { - var id = Get(r, iId); - if (string.IsNullOrWhiteSpace(id)) continue; - - int.TryParse(Get(r, iSec), NumberStyles.Integer, CultureInfo.InvariantCulture, out var sec); - double.TryParse(Get(r, iX), NumberStyles.Any, CultureInfo.InvariantCulture, out var x); - double.TryParse(Get(r, iY), NumberStyles.Any, CultureInfo.InvariantCulture, out var y); - - if (!res.TryGetValue(id, out var list)) - { - list = new List(); - res[id] = list; - } - - list.Add(new ZipPatronPointRow(id, sec, x, y)); - } - - foreach (var kv in res) - kv.Value.Sort((a, b) => a.Secuencia.CompareTo(b.Secuencia)); - - return res; - } - - // ========================================================= - // RECONSTRUCCIÓN PATRÓN - // ========================================================= - - /// - /// Gestiona segmentos del patron. - /// - private static List> ConstruirSegmentosDelPatron( - List stopLatLon, - List patronLatLon) - { - var res = new List>(); - - if (stopLatLon.Count < 2) - return res; - - if (patronLatLon.Count < 2) - { - for (int i = 0; i < stopLatLon.Count - 1; i++) - { - res.Add(new List - { - new[] { stopLatLon[i][0], stopLatLon[i][1] }, - new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] } - }); - } - return res; - } - - var indices = CalcularIndicesParadasEnPatron(stopLatLon, patronLatLon); - - for (int i = 0; i < indices.Count - 1; i++) - { - int a = indices[i]; - int b = indices[i + 1]; - if (b < a) b = a; - - var seg = new List(); - - for (int j = a; j <= b && j < patronLatLon.Count; j++) - seg.Add(new[] { patronLatLon[j][0], patronLatLon[j][1] }); - - if (seg.Count == 0) - { - seg.Add(new[] { stopLatLon[i][0], stopLatLon[i][1] }); - seg.Add(new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] }); - } - else if (seg.Count == 1) - { - seg.Insert(0, new[] { stopLatLon[i][0], stopLatLon[i][1] }); - seg.Add(new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] }); - } - else - { - seg[0] = new[] { stopLatLon[i][0], stopLatLon[i][1] }; - seg[^1] = new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] }; - } - - res.Add(QuitarDuplicadosConsecutivos(seg)); - } - - return res; - } - - /// - /// Calcula para cada parada el indice del punto del patron que mejor la representa - /// manteniendo un avance monotono a lo largo de la geometria. - /// - private static List CalcularIndicesParadasEnPatron( - List stopLatLon, - List patronLatLon) - { - var indices = new List(stopLatLon.Count); - var ultimoIndicePatron = patronLatLon.Count - 1; - - for (int i = 0; i < stopLatLon.Count; i++) - { - var minIdx = indices.Count == 0 - ? 0 - : Math.Min(ultimoIndicePatron, indices[^1] + 1); - - var paradasRestantes = stopLatLon.Count - i - 1; - var maxIdx = Math.Max(minIdx, ultimoIndicePatron - paradasRestantes); - - indices.Add(BuscarIndicePatronMasCercano(stopLatLon[i], patronLatLon, minIdx, maxIdx)); - } - - return indices; - } - - /// - /// Busca el punto del patron mas cercano a una parada dentro del rango permitido. - /// - private static int BuscarIndicePatronMasCercano( - double[] paradaLatLon, - List patronLatLon, - int minIdx, - int maxIdx) - { - var inicio = Math.Clamp(minIdx, 0, patronLatLon.Count - 1); - var fin = Math.Clamp(maxIdx, inicio, patronLatLon.Count - 1); - - var mejorIdx = inicio; - var mejorDist = double.MaxValue; - - for (int j = inicio; j <= fin; j++) - { - var dist = Dist2(paradaLatLon[0], paradaLatLon[1], patronLatLon[j][0], patronLatLon[j][1]); - if (dist < mejorDist) - { - mejorDist = dist; - mejorIdx = j; - } - } - - return mejorIdx; - } - - /// - /// Quita duplicados consecutivos. - /// - private static List QuitarDuplicadosConsecutivos(List pts) - { - var res = new List(pts.Count); - foreach (var p in pts) - { - if (res.Count == 0) - { - res.Add(p); - continue; - } - - var u = res[^1]; - if (Math.Abs(u[0] - p[0]) < 1e-9 && Math.Abs(u[1] - p[1]) < 1e-9) - continue; - - res.Add(p); - } - return res; - } - /// /// /// @@ -1102,96 +308,6 @@ public class RedBusServicio : IRedBusServicio lista.Add(codigoCercana); } - // ========================================================= - // GEO - // ========================================================= - - /// - /// Gestiona lat lon valida. - /// - private static bool EsLatLonValida(double lat, double lon) - => lat >= 42.0 && lat <= 44.5 && lon >= -3.5 && lon <= 0.5; - - private const double OFFSET_LAT = -0.0018141507864726236; - private const double OFFSET_LON = -0.0013070642408454791; - - /// - /// - /// - private static (double lat, double lon) ConvertirCoordenadaZipALatLon(double x, double y) - { - if (x == 0 || y == 0) - return (0, 0); - - // Conversión base UTM 30N -> geográficas - var (lat, lon) = Utm30ToLatLonWgs84(x, y); - - // Corrección sistemática observada en los CSV reales - lat += OFFSET_LAT; - lon += OFFSET_LON; - - return (lat, lon); - } - /// - /// - /// - private static (double lat, double lon) Utm30ToLatLonWgs84(double easting, double northing) - { - const int zone = 30; - const double a = 6378137.0; - const double eccSquared = 0.0066943799901413165; - const double k0 = 0.9996; - - double x = easting - 500000.0; - double y = northing; - - double longOrigin = (zone - 1) * 6 - 180 + 3; - double eccPrimeSquared = eccSquared / (1 - eccSquared); - - double M = y / k0; - double mu = M / (a * ( - 1 - - eccSquared / 4.0 - - 3.0 * eccSquared * eccSquared / 64.0 - - 5.0 * Math.Pow(eccSquared, 3) / 256.0)); - - double e1 = (1.0 - Math.Sqrt(1.0 - eccSquared)) / (1.0 + Math.Sqrt(1.0 - eccSquared)); - - double phi1Rad = - mu - + (3.0 * e1 / 2.0 - 27.0 * Math.Pow(e1, 3) / 32.0) * Math.Sin(2.0 * mu) - + (21.0 * e1 * e1 / 16.0 - 55.0 * Math.Pow(e1, 4) / 32.0) * Math.Sin(4.0 * mu) - + (151.0 * Math.Pow(e1, 3) / 96.0) * Math.Sin(6.0 * mu) - + (1097.0 * Math.Pow(e1, 4) / 512.0) * Math.Sin(8.0 * mu); - - double N1 = a / Math.Sqrt(1.0 - eccSquared * Math.Sin(phi1Rad) * Math.Sin(phi1Rad)); - double T1 = Math.Tan(phi1Rad) * Math.Tan(phi1Rad); - double C1 = eccPrimeSquared * Math.Cos(phi1Rad) * Math.Cos(phi1Rad); - double R1 = a * (1.0 - eccSquared) / Math.Pow(1.0 - eccSquared * Math.Sin(phi1Rad) * Math.Sin(phi1Rad), 1.5); - double D = x / (N1 * k0); - - double latRad = - phi1Rad - - (N1 * Math.Tan(phi1Rad) / R1) * - ( - D * D / 2.0 - - (5.0 + 3.0 * T1 + 10.0 * C1 - 4.0 * C1 * C1 - 9.0 * eccPrimeSquared) * Math.Pow(D, 4) / 24.0 - + (61.0 + 90.0 * T1 + 298.0 * C1 + 45.0 * T1 * T1 - 252.0 * eccPrimeSquared - 3.0 * C1 * C1) * Math.Pow(D, 6) / 720.0 - ); - - double lonRad = - ( - D - - (1.0 + 2.0 * T1 + C1) * Math.Pow(D, 3) / 6.0 - + (5.0 - 2.0 * C1 + 28.0 * T1 - 3.0 * C1 * C1 + 8.0 * eccPrimeSquared + 24.0 * T1 * T1) * Math.Pow(D, 5) / 120.0 - ) / Math.Cos(phi1Rad); - - double lat = latRad * 180.0 / Math.PI; - double lon = longOrigin + lonRad * 180.0 / Math.PI; - - return (lat, lon); - } - /// /// Calcula distancia metros. /// @@ -1676,74 +792,6 @@ public class RedBusServicio : IRedBusServicio .First(); } - /// - /// Gestiona sin decimal. - /// - private static string CodigoSinDecimal(string? codigo) - { - if (string.IsNullOrWhiteSpace(codigo)) return ""; - codigo = codigo.Trim(); - - if (codigo.StartsWith("P-", StringComparison.OrdinalIgnoreCase)) - codigo = codigo[2..]; - - var i = codigo.IndexOf('.'); - if (i > 0) - codigo = codigo[..i]; - - return codigo.Trim(); - } - - /// - /// Normaliza codigo parada. - /// - private static string NormalizarCodigoParada(string? s) - { - if (string.IsNullOrWhiteSpace(s)) return ""; - - s = s.Trim(); - s = s.Replace("P-", "", StringComparison.OrdinalIgnoreCase); - - var i = s.IndexOf('.'); - if (i > 0) - s = s[..i]; - - return s.Trim(); - } - - /// - /// Normaliza color argb. - /// - private static string NormalizarColorArgb(string? color) - { - if (string.IsNullOrWhiteSpace(color)) - return "FF2563EB"; - - color = color.Trim().TrimStart('#'); - - if (color.Length == 6) - return "FF" + color.ToUpperInvariant(); - - if (color.Length == 8) - return color.ToUpperInvariant(); - - return "FF2563EB"; - } - - /// - /// Gestiona zip a sentido. - /// - private static string DireccionZipASentido(int direccion) - { - return direccion switch - { - 1 => "I", - 2 => "V", - 0 => "I", - _ => "?" - }; - } - /// /// Normaliza sentido codigo. /// @@ -1763,55 +811,4 @@ public class RedBusServicio : IRedBusServicio if (c == 'I') return "I"; return "?"; } - - /// - /// Gestiona codigo variante. - /// - private static int ExtraerCodigoVariante(string? idPatronExp) - { - if (string.IsNullOrWhiteSpace(idPatronExp)) - return 0; - - var s = idPatronExp.Trim(); - - if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n)) - return n; - - var m = Regex.Match(s, @"(\d+)$"); - return m.Success && int.TryParse(m.Groups[1].Value, out n) ? n : 0; - } - - /// - /// Gestiona codigo itinerario. - /// - private static int ExtraerCodigoItinerario(string? idRuta, string? idPatronExp) - { - if (string.IsNullOrWhiteSpace(idPatronExp)) - return 0; - - var patron = idPatronExp.Trim(); - var ruta = CodigoSinDecimal(idRuta); - - if (!string.IsNullOrWhiteSpace(ruta) && - patron.StartsWith(ruta, StringComparison.OrdinalIgnoreCase)) - { - patron = patron[ruta.Length..]; - } - - patron = patron.Trim(); - - if (patron.Length >= 4) - { - var sinDir = patron[..^2]; - sinDir = sinDir.TrimStart('0'); - if (int.TryParse(sinDir, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n1)) - return n1; - } - - var m = Regex.Match(patron, @"(\d{1,3})"); - if (m.Success && int.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n2)) - return n2; - - return 0; - } } diff --git a/RutasDBUS/wwwroot/app.css b/RutasDBUS/wwwroot/app.css index b28bda8..7e19545 100644 --- a/RutasDBUS/wwwroot/app.css +++ b/RutasDBUS/wwwroot/app.css @@ -1544,17 +1544,6 @@ html, body { -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 { display: grid; grid-template-columns: 1fr 1.6rem 1.6rem; /* input | ? | ? */ @@ -1583,27 +1572,6 @@ html, body { 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 { grid-template-columns: repeat(2, minmax(0, 4.2rem)); 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 */ .config-row.config-row-full { grid-template-columns: 1fr !important; @@ -2143,6 +2066,8 @@ html, body { color: #f8fafc !important; font-size: .68rem !important; font-weight: 800 !important; + line-height: 1.3 !important; + white-space: nowrap; padding: 2px 6px !important; box-shadow: 0 8px 18px rgba(0, 0, 0, .35) !important; } @@ -2221,16 +2146,6 @@ html, body { 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 */ @media (max-width: 720px) { .popup-elev-topright { @@ -2440,43 +2355,12 @@ html, body { /* mantén tus estilos actuales */ } -.gm-search-panel-actions { - display: flex; - justify-content: flex-end; - margin-bottom: .35rem; -} - .gm-search-hide-btn { padding: .2rem .45rem; line-height: 1; 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) */ .line-filter__range--fecha { grid-template-columns: minmax(0, 1.12fr) minmax(0, 0.78fr) auto !important; diff --git a/RutasDBUS/wwwroot/app.js b/RutasDBUS/wwwroot/app.js index 9bb71c1..44c87f4 100644 --- a/RutasDBUS/wwwroot/app.js +++ b/RutasDBUS/wwwroot/app.js @@ -178,9 +178,22 @@ window.rt.drawRoute = function (id, coords, options) { const segmentLine = buildLine(segmentCoords, segmentOpts); const slope = segment && typeof segment.slope === "number" ? segment.slope : null; - const label = slope !== null && Number.isFinite(slope) - ? ((slope > 0 ? "+" : "") + slope.toFixed(1) + "%") - : (segment && segment.label ? String(segment.label) : ""); + const distance = segment && typeof segment.distance === "number" ? segment.distance : null; + const elevation = segment && typeof segment.elevation === "number" ? segment.elevation : null; + 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("
"); if (label) { segmentLine.bindTooltip(label, { @@ -729,14 +742,6 @@ window.rt.toggleItStops = function (id, stops, options) { 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) // -----------------------------------------------------------------------------