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