From 3a629de88768cc8a837c837ca32fba6c2790ae81 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 27 Aug 2026 14:55:37 +0200 Subject: [PATCH] =?UTF-8?q?A=C3=B1adido=20horario=20simualdo=20con=20adver?= =?UTF-8?q?tencia=20a=20lostaxibus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit añadido desseleccion de segmentos y iluminado del mismo añadido limites nuevos en lugares y ordenes alfabeticos quitado acalarion b5m defectos de L5 y maximos al 80% formula cambiada a 5.2 y division por distancia de bus dsitancia con z anteriores --- .../Pages/PlanificadorRutas.Estado.cs | 10 +- .../PlanificadorRutas.Geocodificacion.cs | 10 +- .../Pages/PlanificadorRutas.Historial.cs | 289 +++++++++++++++++- .../Pages/PlanificadorRutas.InspectorZ.cs | 35 +++ .../Pages/PlanificadorRutas.Itinerarios.cs | 2 +- .../Pages/PlanificadorRutas.Persistencia.cs | 33 +- .../Pages/PlanificadorRutas.Planificacion.cs | 15 +- .../PlanificadorRutas.TiempoYHorarios.cs | 20 ++ .../Components/Pages/PlanificadorRutas.razor | 178 ++++++++++- .../Importacion/ArchivoImportacionDbus.cs | 2 + RutasDBUS/Modelos/RedBus/LineaBus.cs | 3 + .../GeocodificadorLocalServicio.cs | 3 +- .../Horarios/ServicioHorariosDbus.cs | 26 +- .../ServicioArchivoImportacionDbus.cs | 6 + .../Importacion/SimuladorHorariosTaxibus.cs | 200 ++++++++++++ .../PlanificadorRutasServicio.cs | 3 +- .../RedBus/CreadorRedTransporteDbus.cs | 1 + RutasDBUS/Servicios/TaxibusDbus.cs | 32 ++ RutasDBUS/wwwroot/app.css | 237 +++++++++++++- RutasDBUS/wwwroot/app.js | 157 ++++++++++ 20 files changed, 1219 insertions(+), 43 deletions(-) create mode 100644 RutasDBUS/Components/Pages/PlanificadorRutas.InspectorZ.cs create mode 100644 RutasDBUS/Servicios/Importacion/SimuladorHorariosTaxibus.cs create mode 100644 RutasDBUS/Servicios/TaxibusDbus.cs diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs index c98ce10..c488c98 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs @@ -49,7 +49,8 @@ public partial class PlanificadorRutas : ComponentBase // CONFIG private const double FactorPenalizacionDistanciaPiePorDefecto = 1.35; private const double ParametroPenalizacionDistanciaPieMetrosPorDefecto = 200.0; - private const double CoeficienteEsfuerzoAlturaPorDefecto = 8.2; + private const double CoeficienteEsfuerzoAlturaPorDefecto = 5.2; + private const double PendienteMaximaEsfuerzoPorcentaje = 80.0; private const double FactorEquilibradoAndarBusPorDefecto = 6.0; private const double UmbralPendienteSubidaRutaPorDefecto = 2.0; private const double UmbralPendienteBajadaRutaPorDefecto = 5.0; @@ -64,7 +65,7 @@ public partial class PlanificadorRutas : ComponentBase private double _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto; private string _factorPenalizacionDistanciaPieTextoEdicion = "1.35"; private string _parametroPenalizacionDistanciaPieTextoEdicion = "200"; - private string _coeficienteEsfuerzoAlturaTextoEdicion = "8.20"; + private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20"; private string _pesoTransbordoRankingTextoEdicion = "4,0"; private string _factorEquilibradoAndarBusTextoEdicion = "6"; private double _velocidadBusKmH = 16.0; @@ -234,7 +235,10 @@ public partial class PlanificadorRutas : ComponentBase if (pendientePorcentaje.Value < umbralSubida) return distanciaMetros.Value; - var pendiente = pendientePorcentaje.Value / 100.0; + var pendienteLimitadaPorcentaje = Math.Min( + pendientePorcentaje.Value, + PendienteMaximaEsfuerzoPorcentaje); + var pendiente = pendienteLimitadaPorcentaje / 100.0; var factorConPotencia = Math.Pow(1.0 + pendiente, coeficiente); var esfuerzo = distanciaMetros.Value * factorConPotencia; return double.IsFinite(esfuerzo) diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs index b8d0a5b..7c47b8e 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs @@ -76,7 +76,7 @@ public partial class PlanificadorRutas try { await Task.Delay(120, ct); - var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 20, ct); + var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 50, ct); if (ct.IsCancellationRequested || version != _versionSugerenciasBusqueda) return; @@ -160,7 +160,9 @@ public partial class PlanificadorRutas .Concat(lugares.Select(CrearSugerenciaLugar)) .GroupBy(sugerencia => NormalizarClaveSugerencia(sugerencia), StringComparer.Ordinal) .Select(grupo => grupo.First()) - .Take(Math.Clamp(maxResultados, 1, 20)) + .OrderBy(sugerencia => sugerencia.Titulo, StringComparer.CurrentCultureIgnoreCase) + .ThenBy(sugerencia => sugerencia.Subtitulo, StringComparer.CurrentCultureIgnoreCase) + .Take(Math.Clamp(maxResultados, 1, 50)) .ToList(); } @@ -247,8 +249,8 @@ public partial class PlanificadorRutas TextoEntrada = lugar.Nombre, Titulo = lugar.Nombre, Subtitulo = string.IsNullOrWhiteSpace(lugar.Subtitulo) - ? $"{lugar.Tipo} · {lugar.Fuente}" - : $"{lugar.Subtitulo} · {lugar.Fuente}", + ? lugar.Tipo + : lugar.Subtitulo, Tipo = lugar.Tipo, Fuente = lugar.Fuente, Latitud = lugar.Latitud, diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs index fc18127..1b4ad4d 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs @@ -30,6 +30,10 @@ public partial class PlanificadorRutas private TipoReferenciaHistorial _tipoReferenciaHistorial; private string _historialReferenciaId = string.Empty; private bool _rutaActualDerivadaDeHistorial; + private readonly List _pasosDerivacionHistorial = new(); + private RegistroHistorialRuta? _escenarioBaseDerivacionHistorial; + private bool _mostrarInformacionDerivacionHistorial; + private int _indicePasoDerivacionSeleccionado = -1; private string _mensajeHistorial = string.Empty; private bool _mensajeHistorialEsError = false; private bool _mostrarHistorialRutas = false; @@ -53,6 +57,17 @@ public partial class PlanificadorRutas Compartido } + private sealed record PasoDerivacionHistorial( + int Numero, + DateTime FechaUtc, + RegistroHistorialRuta Escenario); + + private sealed record CambioDerivacionHistorial( + string Campo, + string ValorAnterior, + string ValorActual, + string Detalle); + private sealed class ResultadoStorageNavegador { [JsonPropertyName("ok")] @@ -113,6 +128,7 @@ public partial class PlanificadorRutas { _versionAperturaHistorial++; _mostrarHistorialRutas = false; + _mostrarInformacionDerivacionHistorial = false; _historialLocalCargando = false; StopRefrescoHistorialCompartido(); await RestaurarConfiguracionTemporalHistorialAsync(); @@ -834,13 +850,27 @@ public partial class PlanificadorRutas private void ConfirmarDerivacionHistorial() { _rutaActualDerivadaDeHistorial = ExisteReferenciaHistorialActual(); + if (_rutaActualDerivadaDeHistorial && + _tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido) + { + RegistrarPasoDerivacionHistorialActual(); + } } private void EstablecerReferenciaHistorial(TipoReferenciaHistorial tipo, string id) { + ReiniciarSeguimientoDerivacionHistorial(); _tipoReferenciaHistorial = tipo; _historialReferenciaId = id; _rutaActualDerivadaDeHistorial = false; + + if (tipo == TipoReferenciaHistorial.Compartido) + { + var compartida = _historialCompartido.FirstOrDefault(item => + string.Equals(item.Id, id, StringComparison.OrdinalIgnoreCase)); + if (compartida is not null) + _escenarioBaseDerivacionHistorial = ClonarEscenarioHistorial(compartida); + } } private void LimpiarReferenciaHistorial() @@ -848,6 +878,7 @@ public partial class PlanificadorRutas _tipoReferenciaHistorial = TipoReferenciaHistorial.Ninguna; _historialReferenciaId = string.Empty; _rutaActualDerivadaDeHistorial = false; + ReiniciarSeguimientoDerivacionHistorial(); } private bool ExisteReferenciaHistorialActual() @@ -884,11 +915,265 @@ public partial class PlanificadorRutas _ => (string.Empty, -1) }; - return indice >= 0 - ? $"Ruta actual derivada del Historial {nombre} n.º {indice + 1}." + if (indice < 0) + return string.Empty; + + var numeroDerivacion = _tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido && + _pasosDerivacionHistorial.Count > 0 + ? $" · Derivación n.º {_pasosDerivacionHistorial.Count}" : string.Empty; + return $"Ruta actual derivada del Historial {nombre} n.º {indice + 1}{numeroDerivacion}."; } + private bool PuedeMostrarInformacionDerivacionHistorial => + _rutaActualDerivadaDeHistorial && + _tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido && + _escenarioBaseDerivacionHistorial is not null && + _pasosDerivacionHistorial.Count > 0; + + private int IndiceHistorialCompartidoReferencia => + _historialCompartido.FindIndex(item => string.Equals( + item.Id, + _historialReferenciaId, + StringComparison.OrdinalIgnoreCase)); + + private PasoDerivacionHistorial? PasoDerivacionSeleccionado => + _indicePasoDerivacionSeleccionado >= 0 && + _indicePasoDerivacionSeleccionado < _pasosDerivacionHistorial.Count + ? _pasosDerivacionHistorial[_indicePasoDerivacionSeleccionado] + : null; + + private void AbrirInformacionDerivacionHistorial() + { + if (!PuedeMostrarInformacionDerivacionHistorial) + return; + + _indicePasoDerivacionSeleccionado = _pasosDerivacionHistorial.Count - 1; + _mostrarInformacionDerivacionHistorial = true; + } + + private void CerrarInformacionDerivacionHistorial() + { + _mostrarInformacionDerivacionHistorial = false; + } + + private void SeleccionarPasoDerivacionHistorial(int indice) + { + if (indice < 0 || indice >= _pasosDerivacionHistorial.Count) + return; + + _indicePasoDerivacionSeleccionado = indice; + } + + private void ReiniciarSeguimientoDerivacionHistorial() + { + _pasosDerivacionHistorial.Clear(); + _escenarioBaseDerivacionHistorial = null; + _indicePasoDerivacionSeleccionado = -1; + _mostrarInformacionDerivacionHistorial = false; + } + + private void RegistrarPasoDerivacionHistorialActual() + { + _escenarioBaseDerivacionHistorial ??= _historialCompartido + .FirstOrDefault(item => string.Equals( + item.Id, + _historialReferenciaId, + StringComparison.OrdinalIgnoreCase)) is { } compartida + ? ClonarEscenarioHistorial(compartida) + : null; + + if (_escenarioBaseDerivacionHistorial is null) + return; + + var actual = ConstruirRegistroHistorialActual(); + if (actual is null) + return; + + var anterior = _pasosDerivacionHistorial.Count > 0 + ? _pasosDerivacionHistorial[^1].Escenario + : _escenarioBaseDerivacionHistorial; + if (string.Equals( + CalcularClaveEscenario(anterior), + CalcularClaveEscenario(actual), + StringComparison.OrdinalIgnoreCase)) + { + return; + } + + _pasosDerivacionHistorial.Add(new PasoDerivacionHistorial( + _pasosDerivacionHistorial.Count + 1, + DateTime.UtcNow, + ClonarEscenarioHistorial(actual))); + _indicePasoDerivacionSeleccionado = _pasosDerivacionHistorial.Count - 1; + } + + private RegistroHistorialRuta? ObtenerEscenarioAnteriorDerivacion( + PasoDerivacionHistorial paso) + { + var indice = paso.Numero - 1; + if (indice <= 0) + return _escenarioBaseDerivacionHistorial; + + return indice - 1 < _pasosDerivacionHistorial.Count + ? _pasosDerivacionHistorial[indice - 1].Escenario + : null; + } + + private IReadOnlyList ObtenerCambiosDerivacion( + RegistroHistorialRuta? anterior, + RegistroHistorialRuta actual) + { + if (anterior is null) + return Array.Empty(); + + var cambios = new List(); + AgregarCambioPuntoDerivacion( + cambios, + "Origen", + anterior.OrigenTexto, + anterior.OrigenLatitud, + anterior.OrigenLongitud, + actual.OrigenTexto, + actual.OrigenLatitud, + actual.OrigenLongitud); + AgregarCambioPuntoDerivacion( + cambios, + "Destino", + anterior.DestinoTexto, + anterior.DestinoLatitud, + anterior.DestinoLongitud, + actual.DestinoTexto, + actual.DestinoLatitud, + actual.DestinoLongitud); + + AgregarCambioTextoDerivacion( + cambios, + "Fecha y hora", + FormatearFechaHoraComparacionDerivacion(anterior), + FormatearFechaHoraComparacionDerivacion(actual)); + AgregarCambioTextoDerivacion( + cambios, + "Modo", + FormatearModoHistorial(anterior), + FormatearModoHistorial(actual)); + AgregarCambioTextoDerivacion( + cambios, + "Factor Equilib. A", + FormatearFactorDerivacion(anterior), + FormatearFactorDerivacion(actual)); + AgregarCambioTextoDerivacion( + cambios, + "Horarios", + anterior.UsarHorariosTeoricos ? "Activados" : "Desactivados", + actual.UsarHorariosTeoricos ? "Activados" : "Desactivados"); + AgregarCambioTextoDerivacion( + cambios, + "Líneas utilizables", + FormatearLineasDerivacion(anterior), + FormatearLineasDerivacion(actual)); + + return cambios; + } + + private static void AgregarCambioPuntoDerivacion( + ICollection cambios, + string campo, + string textoAnterior, + double latitudAnterior, + double longitudAnterior, + string textoActual, + double latitudActual, + double longitudActual) + { + var distancia = CalcularDistanciaMetros( + latitudAnterior, + longitudAnterior, + latitudActual, + longitudActual); + if (!double.IsFinite(distancia) || distancia < 0.5) + return; + + cambios.Add(new CambioDerivacionHistorial( + campo, + FormatearPuntoDerivacion(textoAnterior, latitudAnterior, longitudAnterior), + FormatearPuntoDerivacion(textoActual, latitudActual, longitudActual), + $"Desplazado {FormatearDistanciaDerivacion(distancia)}")); + } + + private static void AgregarCambioTextoDerivacion( + ICollection cambios, + string campo, + string? valorAnterior, + string? valorActual) + { + var anterior = string.IsNullOrWhiteSpace(valorAnterior) ? "-" : valorAnterior.Trim(); + var actual = string.IsNullOrWhiteSpace(valorActual) ? "-" : valorActual.Trim(); + if (string.Equals(anterior, actual, StringComparison.OrdinalIgnoreCase)) + return; + + cambios.Add(new CambioDerivacionHistorial(campo, anterior, actual, string.Empty)); + } + + private static string FormatearPuntoDerivacion( + string texto, + double latitud, + double longitud) + { + var nombre = string.IsNullOrWhiteSpace(texto) ? "Punto del mapa" : texto.Trim(); + return $"{nombre} · {latitud.ToString("0.000000", CultureInfo.InvariantCulture)}, " + + longitud.ToString("0.000000", CultureInfo.InvariantCulture); + } + + private static string FormatearDistanciaDerivacion(double distanciaMetros) + => distanciaMetros >= 1000 + ? $"{(distanciaMetros / 1000.0).ToString("0.000", CultureInfo.InvariantCulture)} km" + : $"{Math.Round(distanciaMetros).ToString("0", CultureInfo.InvariantCulture)} m"; + + private static string FormatearFechaHoraComparacionDerivacion(RegistroHistorialRuta item) + => item.UsarHoraSimulada + ? FormatearHoraReferenciaHistorial(item) + : "Fecha y hora actuales"; + + private static string FormatearFactorDerivacion(RegistroHistorialRuta item) + => item.FactorEquilibradoAndarBus is { } factor && double.IsFinite(factor) && factor > 0 + ? factor.ToString("0.##", CultureInfo.InvariantCulture) + : "-"; + + private static string FormatearLineasDerivacion(RegistroHistorialRuta item) + { + if (!item.ActivarFiltroLineas) + return "Todas"; + + var lineas = NormalizarLineasHistorial(item.LineasPermitidas); + return lineas.Count == 0 + ? "Ninguna" + : string.Join(", ", lineas); + } + + private static RegistroHistorialRuta ClonarEscenarioHistorial(RegistroHistorialRuta item) + => new() + { + Id = item.Id, + OrigenTexto = item.OrigenTexto, + DestinoTexto = item.DestinoTexto, + OrigenLatitud = item.OrigenLatitud, + OrigenLongitud = item.OrigenLongitud, + DestinoLatitud = item.DestinoLatitud, + DestinoLongitud = item.DestinoLongitud, + UsarHorariosTeoricos = item.UsarHorariosTeoricos, + UsarHoraSimulada = item.UsarHoraSimulada, + FechaSimulada = item.FechaSimulada, + HoraSimulada = item.HoraSimulada, + CriterioPrioritarioRuta = item.CriterioPrioritarioRuta, + PreferenciaEtiquetaRuta = item.PreferenciaEtiquetaRuta, + FactorEquilibradoAndarBus = item.FactorEquilibradoAndarBus, + ActivarFiltroLineas = item.ActivarFiltroLineas, + LineasPermitidas = item.LineasPermitidas.ToList(), + FechaCreacionUtc = item.FechaCreacionUtc, + FechaActualizacionUtc = item.FechaActualizacionUtc + }; + private async Task CompartirEscenarioActualAsync() { var actual = ConstruirRegistroHistorialActual() diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.InspectorZ.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.InspectorZ.cs new file mode 100644 index 0000000..45d9ea1 --- /dev/null +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.InspectorZ.cs @@ -0,0 +1,35 @@ +using System.Globalization; + +namespace RutasDBUS.Components.Pages; + +public partial class PlanificadorRutas +{ + private PuntoInspectorAnterior? _inspectorPuntoAnterior; + + private sealed record PuntoInspectorAnterior( + double Latitud, + double Longitud, + double? ElevacionIdee, + double? ElevacionLocal5, + double? ElevacionLocal2, + double? ElevacionLocal1); + + private double? ObtenerElevacionPuntoInspectorAnterior(PuntoInspectorAnterior punto) + => UsarElevacionLocal1ParaCalculo + ? punto.ElevacionLocal1 + : UsarElevacionLocal2ParaCalculo + ? punto.ElevacionLocal2 + : UsarElevacionLocal5ParaCalculo + ? punto.ElevacionLocal5 + : punto.ElevacionIdee; + + private static string FormatearDistanciaPuntoInspector(double distanciaMetros) + => distanciaMetros < 1000 + ? distanciaMetros.ToString("0.0", CultureInfo.InvariantCulture) + " m" + : (distanciaMetros / 1000.0).ToString("0.000", CultureInfo.InvariantCulture) + " km"; + + private static string FormatearAlturaPuntoInspector(double? altura) + => altura is { } valor + ? valor.ToString("0.00", CultureInfo.InvariantCulture) + " m" + : "-"; +} diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs index d726c85..5793427 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs @@ -1202,7 +1202,7 @@ public partial class PlanificadorRutas : ComponentBase private bool _elevPopupMinimizado = false; - private const string _versionApp = "(v-20260825b)"; + private const string _versionApp = "(v-20260827b)"; private double? _paradaElevIdee; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs index b65f8fc..e94e55b 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs @@ -10,6 +10,8 @@ public partial class PlanificadorRutas private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1"; private const int VersionPersistenciaFactorEquilibrado = 2; private const int VersionPersistenciaPendientes = 2; + private const int VersionPersistenciaCoeficienteEsfuerzo = 1; + private const int VersionPersistenciaFuenteElevacion = 1; private bool _configuracionLocalCargada = false; private bool _mostrarConfirmacionRestablecer = false; @@ -38,11 +40,13 @@ public partial class PlanificadorRutas public double? FactorPenalizacionDistanciaPie { get; set; } public double? ParametroPenalizacionDistanciaPieMetros { get; set; } public double? CoeficienteEsfuerzoAltura { get; set; } + public int? VersionCoeficienteEsfuerzo { get; set; } public double? PesoTransbordoRankingMinutos { get; set; } public double? FactorEquilibradoAndarBus { get; set; } public int? VersionFactorEquilibrado { get; set; } public bool? MostrarElevacionesRuta { get; set; } public string? FuenteElevacionCalculo { get; set; } + public int? VersionFuenteElevacion { get; set; } public int? VersionPendientes { get; set; } public double? UmbralPendienteSubidaRuta { get; set; } public double? UmbralPendienteBajadaRuta { get; set; } @@ -154,11 +158,13 @@ public partial class PlanificadorRutas FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie, ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros, CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura, + VersionCoeficienteEsfuerzo = VersionPersistenciaCoeficienteEsfuerzo, PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos, FactorEquilibradoAndarBus = _factorEquilibradoAndarBus, VersionFactorEquilibrado = VersionPersistenciaFactorEquilibrado, MostrarElevacionesRuta = _mostrarElevacionesRutaConfiguracion, FuenteElevacionCalculo = _fuenteElevacionCalculo, + VersionFuenteElevacion = VersionPersistenciaFuenteElevacion, VersionPendientes = VersionPersistenciaPendientes, UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta, UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta, @@ -218,11 +224,15 @@ public partial class PlanificadorRutas Math.Abs(parametro - 100.0) < 0.0001 ? ParametroPenalizacionDistanciaPieMetrosPorDefecto : parametro; + var migrarCoeficienteEsfuerzo = + (config.VersionCoeficienteEsfuerzo ?? 0) < VersionPersistenciaCoeficienteEsfuerzo; if (config.CoeficienteEsfuerzoAltura is { } coeficienteAltura && double.IsFinite(coeficienteAltura)) _coeficienteEsfuerzoAltura = - Math.Abs(coeficienteAltura - 1.6) < 0.0001 || - Math.Abs(coeficienteAltura - 5.2) < 0.0001 || - Math.Abs(coeficienteAltura - 9.2) < 0.0001 + migrarCoeficienteEsfuerzo && + (Math.Abs(coeficienteAltura - 1.6) < 0.0001 || + Math.Abs(coeficienteAltura - 5.2) < 0.0001 || + Math.Abs(coeficienteAltura - 8.2) < 0.0001 || + Math.Abs(coeficienteAltura - 9.2) < 0.0001) ? CoeficienteEsfuerzoAlturaPorDefecto : coeficienteAltura; if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo)) @@ -239,11 +249,14 @@ public partial class PlanificadorRutas ? true : config.MostrarElevacionesRuta ?? true; _mostrarElevacionesRutaConfiguracion = _mostrarElevacionesRuta; - _fuenteElevacionCalculo = migrarValoresPendientes && - (string.IsNullOrWhiteSpace(config.FuenteElevacionCalculo) || - string.Equals(config.FuenteElevacionCalculo, FuenteElevacionLocal5, StringComparison.OrdinalIgnoreCase) || - string.Equals(config.FuenteElevacionCalculo, "local", StringComparison.OrdinalIgnoreCase)) - ? FuenteElevacionLocal1 + var migrarFuenteElevacion = + (config.VersionFuenteElevacion ?? 0) < VersionPersistenciaFuenteElevacion; + _fuenteElevacionCalculo = migrarFuenteElevacion && + string.Equals( + config.FuenteElevacionCalculo, + FuenteElevacionLocal1, + StringComparison.OrdinalIgnoreCase) + ? FuenteElevacionLocal5 : NormalizarFuenteElevacion(config.FuenteElevacionCalculo); if (config.UmbralPendienteSubidaRuta is { } umbralSubida && double.IsFinite(umbralSubida)) _umbralPendienteSubidaRuta = Math.Abs(umbralSubida - 3.0) < 0.0001 @@ -376,7 +389,7 @@ public partial class PlanificadorRutas _pesoTransbordoRankingMinutos = 4.0; _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto; _mostrarElevacionesRutaConfiguracion = true; - _fuenteElevacionCalculo = FuenteElevacionLocal1; + _fuenteElevacionCalculo = FuenteElevacionLocal5; _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto; _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto; _umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto; @@ -429,7 +442,7 @@ public partial class PlanificadorRutas _pesoTransbordoRankingMinutos = 4.0; _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto; _mostrarElevacionesRutaConfiguracion = true; - _fuenteElevacionCalculo = FuenteElevacionLocal1; + _fuenteElevacionCalculo = FuenteElevacionLocal5; _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto; _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto; _umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs index f14e24d..cd74b8a 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs @@ -188,7 +188,7 @@ public partial class PlanificadorRutas : ComponentBase private const string FuenteElevacionLocal1 = "local1"; private const string FuenteElevacionIdee = "idee"; private const int TiempoMaximoIdeeSegundos = 10; - private string _fuenteElevacionCalculo = FuenteElevacionLocal1; + private string _fuenteElevacionCalculo = FuenteElevacionLocal5; // IDEE: en el correo indican que 6 decimales es suficiente private const int ELEV_IDEE_DECIMALES_CACHE = 6; @@ -217,7 +217,7 @@ public partial class PlanificadorRutas : ComponentBase return FuenteElevacionLocal5; } - return FuenteElevacionLocal1; + return FuenteElevacionLocal5; } private bool ElevacionLocal5Disponible @@ -1519,6 +1519,16 @@ public partial class PlanificadorRutas : ComponentBase /// private async Task MostrarInspectorAsync(double lat, double lon) { + _inspectorPuntoAnterior = _popupInspectorVisible + ? new PuntoInspectorAnterior( + _inspectorLat, + _inspectorLon, + _inspectorElevIdee, + _inspectorElevLocal5, + _inspectorElevLocal2, + _inspectorElevLocal1) + : null; + var versionInspector = ++_versionInspector; _inspectorLat = lat; _inspectorLon = lon; @@ -1629,6 +1639,7 @@ public partial class PlanificadorRutas : ComponentBase { _versionInspector++; _popupInspectorVisible = false; + _inspectorPuntoAnterior = null; _inspectorLugaresCercanos = Array.Empty(); try { await JS.InvokeVoidAsync("rt.clearInspectorMarkers"); } catch { } diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs index 85805c2..4ef9498 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs @@ -218,6 +218,26 @@ public partial class PlanificadorRutas : ComponentBase string ItinerarioClave); private List _nextDeps = new(); + + private bool HorariosTaxibusSimuladosVisibles + => _nextDeps.Any(salida => TaxibusDbus.EsExpedicionSimulada(salida.ExpId)); + + private static string ObtenerAvisoTaxibus(AlternativaRuta alternativa) + { + var lineasTaxibus = alternativa.AlternativaBus?.Tramos + .Select(tramo => tramo.Variante.Linea) + .Where(linea => TaxibusDbus.EsLinea(linea.Codigo)) + .Distinct() + .ToList(); + + if (lineasTaxibus is not { Count: > 0 }) + return string.Empty; + + return lineasTaxibus.Any(linea => linea.HorarioSimulado) + ? TaxibusDbus.MensajeHorarioSimulado + : TaxibusDbus.MensajeReserva; + } + /// /// Gestiona toggle hora simulada. /// diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.razor b/RutasDBUS/Components/Pages/PlanificadorRutas.razor index 112042d..e4c1275 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.razor +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.razor @@ -299,6 +299,13 @@ + @if (HorariosTaxibusSimuladosVisibles) + { +
+ Taxibús: @TaxibusDbus.MensajeHorarioSimulado +
+ } +
    @foreach (var x in NextDepsOrdenados) @@ -401,6 +408,29 @@ }
+ @if (_inspectorPuntoAnterior is { } puntoAnterior) + { + var distanciaPuntoAnterior = CalcularDistanciaMetros( + puntoAnterior.Latitud, + puntoAnterior.Longitud, + _inspectorLat, + _inspectorLon); + var elevacionPuntoAnterior = ObtenerElevacionPuntoInspectorAnterior(puntoAnterior); + +
+
+ Distancia al punto Z anterior: + @FormatearDistanciaPuntoInspector(distanciaPuntoAnterior) +
+
+ Punto anterior + Lat: @puntoAnterior.Latitud.ToString("0.000000", CultureInfo.InvariantCulture) + Lon: @puntoAnterior.Longitud.ToString("0.000000", CultureInfo.InvariantCulture) + @EtiquetaFuenteElevacionCalculo: @FormatearAlturaPuntoInspector(elevacionPuntoAnterior) +
+
+ } + @if (_inspectorLugaresCercanos.Count > 0) {
@@ -741,6 +771,7 @@ @if (_opcionesRuta.Count > 0 && _indiceAlternativaSeleccionada >= 0 && _indiceAlternativaSeleccionada < _opcionesRuta.Count) { var alt = _opcionesRuta[_indiceAlternativaSeleccionada]; + var avisoTaxibus = ObtenerAvisoTaxibus(alt); @if (MostrarAlertaPendientesPositivasSignificativas) { @@ -753,6 +784,13 @@
} + @if (!string.IsNullOrWhiteSpace(avisoTaxibus)) + { +
+ Taxibús: @avisoTaxibus +
+ } + @* ========= RESUMEN BONITO ARRIBA (único) ========= *@

@if (alt.EsSoloAPie) @@ -1162,7 +1200,15 @@ {

- @textoReferenciaDerivada + @textoReferenciaDerivada + @if (PuedeMostrarInformacionDerivacionHistorial) + { + + }
} @@ -1198,6 +1244,126 @@ } + @if (_mostrarInformacionDerivacionHistorial && + _escenarioBaseDerivacionHistorial is not null && + PasoDerivacionSeleccionado is { } pasoDerivacion) + { + var cambiosRespectoOriginal = ObtenerCambiosDerivacion( + _escenarioBaseDerivacionHistorial, + pasoDerivacion.Escenario); + var escenarioAnterior = ObtenerEscenarioAnteriorDerivacion(pasoDerivacion); + var cambiosRespectoAnterior = ObtenerCambiosDerivacion( + escenarioAnterior, + pasoDerivacion.Escenario); + var textoComparacionAnterior = pasoDerivacion.Numero <= 1 + ? "Respecto al Historial Compartido original" + : $"Respecto a la Derivación n.º {pasoDerivacion.Numero - 1}"; + +
+
+
+
+
Cambios de la derivación
+
+ Historial Compartido n.º @(IndiceHistorialCompartidoReferencia + 1) +
+
+ +
+ +
+ @foreach (var paso in _pasosDerivacionHistorial) + { + var indicePaso = paso.Numero - 1; + + } +
+ +
+ Derivación n.º @pasoDerivacion.Numero + @pasoDerivacion.FechaUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss") +
+ +
+
Respecto al Historial Compartido original
+ @if (cambiosRespectoOriginal.Count == 0) + { +
Sin cambios registrados.
+ } + else + { + @foreach (var cambio in cambiosRespectoOriginal) + { +
+
+ @cambio.Campo + @if (!string.IsNullOrWhiteSpace(cambio.Detalle)) + { + @cambio.Detalle + } +
+
+ Antes +
@cambio.ValorAnterior
+
+
+ Ahora +
@cambio.ValorActual
+
+
+ } + } +
+ + @if (pasoDerivacion.Numero > 1) + { +
+
@textoComparacionAnterior
+ @if (cambiosRespectoAnterior.Count == 0) + { +
Sin cambios respecto a la anterior.
+ } + else + { + @foreach (var cambio in cambiosRespectoAnterior) + { +
+
+ @cambio.Campo + @if (!string.IsNullOrWhiteSpace(cambio.Detalle)) + { + @cambio.Detalle + } +
+
+ Antes +
@cambio.ValorAnterior
+
+
+ Ahora +
@cambio.ValorActual
+
+
+ } + } +
+ } +
+
+ } + @* *@ @@ -1309,7 +1475,7 @@
@if (_activarRangoLineas) @@ -1651,15 +1817,15 @@ disabled="@_pintandoElevacionesRuta" @bind="_fuenteElevacionCalculo" @bind:after="OnFuenteElevacionChangedAsync"> - + diff --git a/RutasDBUS/Modelos/Importacion/ArchivoImportacionDbus.cs b/RutasDBUS/Modelos/Importacion/ArchivoImportacionDbus.cs index 848ac36..ddc17c7 100644 --- a/RutasDBUS/Modelos/Importacion/ArchivoImportacionDbus.cs +++ b/RutasDBUS/Modelos/Importacion/ArchivoImportacionDbus.cs @@ -16,6 +16,8 @@ public sealed class ArchivoImportacionDbus public IReadOnlyList TiemposReales { get; init; } = Array.Empty(); public IReadOnlyList Transbordos { get; init; } = Array.Empty(); public IReadOnlyList Vehiculos { get; init; } = Array.Empty(); + public IReadOnlySet RutasConHorarioSimulado { get; init; } = + new HashSet(StringComparer.OrdinalIgnoreCase); public IReadOnlyDictionary ParadasPorId { get; init; } = new Dictionary(); public IReadOnlyDictionary RutasPorId { get; init; } = diff --git a/RutasDBUS/Modelos/RedBus/LineaBus.cs b/RutasDBUS/Modelos/RedBus/LineaBus.cs index 4ba63ee..5617e4d 100644 --- a/RutasDBUS/Modelos/RedBus/LineaBus.cs +++ b/RutasDBUS/Modelos/RedBus/LineaBus.cs @@ -17,6 +17,9 @@ public class LineaBus /// Color de fondo en formato ARGB (ej. FF0000FF). public string ColorArgb { get; set; } = "FF0000FF"; + /// Indica si el horario activo se ha completado con una frecuencia provisional. + public bool HorarioSimulado { get; set; } + /// Código del itinerario por defecto. public int CodigoItinerarioPorDefecto { get; set; } diff --git a/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs b/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs index 78a8a22..8c050de 100644 --- a/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs +++ b/RutasDBUS/Servicios/Geocodificacion/GeocodificadorLocalServicio.cs @@ -43,9 +43,10 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio if (tokens.Length == 0) return Array.Empty(); - maxResultados = Math.Clamp(maxResultados, 1, 30); + maxResultados = Math.Clamp(maxResultados, 1, 50); return indice + .Where(entrada => EsDonostia(entrada.Municipio)) .Select(entrada => new { Entrada = entrada, diff --git a/RutasDBUS/Servicios/Horarios/ServicioHorariosDbus.cs b/RutasDBUS/Servicios/Horarios/ServicioHorariosDbus.cs index ef4c6cc..8d082b3 100644 --- a/RutasDBUS/Servicios/Horarios/ServicioHorariosDbus.cs +++ b/RutasDBUS/Servicios/Horarios/ServicioHorariosDbus.cs @@ -534,17 +534,24 @@ namespace RutasDBUS.Servicios.Horarios /// private bool CoincideLineaBusqueda(FilaExpedicion exp, string idRutaPedido, string lineaPedida) { + return CoincideLineaExpedicion(exp, idRutaPedido) || + CoincideLineaExpedicion(exp, lineaPedida); + } + + private bool CoincideLineaExpedicion(FilaExpedicion exp, string? lineaPedida) + { + if (string.IsNullOrWhiteSpace(lineaPedida) || + string.Equals(lineaPedida, "?", StringComparison.Ordinal)) + { + return false; + } + var idRutaExp = NormalizarLinea(exp.IdRuta); var codigoVisibleExp = NormalizarLinea(ResolverCodigoLineaVisible(exp.IdRuta)); + var lineaNormalizada = NormalizarLinea(lineaPedida); - return (!string.IsNullOrWhiteSpace(idRutaPedido) && - !string.Equals(idRutaPedido, "?", StringComparison.Ordinal) && - (string.Equals(idRutaExp, idRutaPedido, StringComparison.OrdinalIgnoreCase) || - string.Equals(codigoVisibleExp, idRutaPedido, StringComparison.OrdinalIgnoreCase))) || - (!string.IsNullOrWhiteSpace(lineaPedida) && - !string.Equals(lineaPedida, "?", StringComparison.Ordinal) && - (string.Equals(idRutaExp, lineaPedida, StringComparison.OrdinalIgnoreCase) || - string.Equals(codigoVisibleExp, lineaPedida, StringComparison.OrdinalIgnoreCase))); + return string.Equals(idRutaExp, lineaNormalizada, StringComparison.OrdinalIgnoreCase) || + string.Equals(codigoVisibleExp, lineaNormalizada, StringComparison.OrdinalIgnoreCase); } /// @@ -763,8 +770,7 @@ namespace RutasDBUS.Servicios.Horarios if (!activeCals.Contains(exp.IdCal)) continue; - var lineaExp = NormalizarLinea(exp.IdRuta); - if (!string.Equals(lineaExp, lineaPedida, StringComparison.OrdinalIgnoreCase)) + if (!CoincideLineaExpedicion(exp, lineaPedida)) continue; var sentidoExp = DireccionToSentido(exp.Direccion); diff --git a/RutasDBUS/Servicios/Importacion/ServicioArchivoImportacionDbus.cs b/RutasDBUS/Servicios/Importacion/ServicioArchivoImportacionDbus.cs index 6e2de4e..102742c 100644 --- a/RutasDBUS/Servicios/Importacion/ServicioArchivoImportacionDbus.cs +++ b/RutasDBUS/Servicios/Importacion/ServicioArchivoImportacionDbus.cs @@ -109,6 +109,11 @@ public sealed class ServicioArchivoImportacionDbus var tiemposReales = await ReadTiemposRealesAsync(FindEntry(zip, "tiempo_real.csv"), ct); var transbordos = await ReadTransbordosAsync(FindEntry(zip, "transbordos.csv"), ct); var vehiculos = await ReadVehiculosAsync(FindEntry(zip, "vehiculos.csv"), ct); + var rutasConHorarioSimulado = SimuladorHorariosTaxibus.AplicarSiFaltanHorarios( + calendarios, + expediciones, + rutas, + tiemposParada); return new ArchivoImportacionDbus { @@ -126,6 +131,7 @@ public sealed class ServicioArchivoImportacionDbus TiemposReales = tiemposReales, Transbordos = transbordos, Vehiculos = vehiculos, + RutasConHorarioSimulado = rutasConHorarioSimulado, ParadasPorId = paradas.ToDictionary(x => x.Id), RutasPorId = rutas.ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase), ExpedicionesPorId = expediciones.ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase), diff --git a/RutasDBUS/Servicios/Importacion/SimuladorHorariosTaxibus.cs b/RutasDBUS/Servicios/Importacion/SimuladorHorariosTaxibus.cs new file mode 100644 index 0000000..9599174 --- /dev/null +++ b/RutasDBUS/Servicios/Importacion/SimuladorHorariosTaxibus.cs @@ -0,0 +1,200 @@ +using RutasDBUS.Modelos.Importacion; + +namespace RutasDBUS.Servicios.Importacion; + +/// +/// Completa las plantillas sin horario de TB6 y TB7 con una frecuencia provisional. +/// +internal static class SimuladorHorariosTaxibus +{ + private const string IdCalendario = "TAXIBUS_SIM_TODOS_LOS_DIAS"; + private const int FrecuenciaMinutos = 30; + private const double VelocidadMetrosSegundo = 16_000.0 / 3_600.0; + private const int SegundosMinimosEntreParadas = 30; + + public static IReadOnlySet AplicarSiFaltanHorarios( + List calendarios, + List expediciones, + IReadOnlyList rutas, + List tiemposParada) + { + var rutasSimuladas = new HashSet(StringComparer.OrdinalIgnoreCase); + var tiemposOriginalesPorExpedicion = tiemposParada + .GroupBy(tiempo => tiempo.IdExp, StringComparer.OrdinalIgnoreCase) + .ToDictionary( + grupo => grupo.Key, + grupo => grupo.OrderBy(tiempo => tiempo.SecPar).ToList(), + StringComparer.OrdinalIgnoreCase); + + foreach (var ruta in rutas.Where(EsRutaTaxibus)) + { + var expedicionesRuta = expediciones + .Where(expedicion => string.Equals( + expedicion.IdRuta, + ruta.Id, + StringComparison.OrdinalIgnoreCase)) + .OrderBy(expedicion => expedicion.Id, StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (expedicionesRuta.Count == 0 || + TieneHorarioOficial(expedicionesRuta, tiemposOriginalesPorExpedicion)) + { + continue; + } + + var plantillas = expedicionesRuta + .Select(expedicion => new PlantillaTaxibus( + expedicion, + tiemposOriginalesPorExpedicion.TryGetValue(expedicion.Id, out var tiempos) + ? tiempos + : new List())) + .Where(plantilla => plantilla.Tiempos.Count >= 2) + .ToList(); + + if (plantillas.Count == 0) + continue; + + var idsPlantilla = expedicionesRuta + .Select(expedicion => expedicion.Id) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + expediciones.RemoveAll(expedicion => idsPlantilla.Contains(expedicion.Id)); + tiemposParada.RemoveAll(tiempo => idsPlantilla.Contains(tiempo.IdExp)); + + for (var indicePlantilla = 0; indicePlantilla < plantillas.Count; indicePlantilla++) + { + CrearExpedicionesSimuladas( + ruta, + plantillas[indicePlantilla], + indicePlantilla, + expediciones, + tiemposParada); + } + + rutasSimuladas.Add(ruta.Id); + } + + if (rutasSimuladas.Count > 0) + { + calendarios.RemoveAll(calendario => + calendario.Id.Equals(IdCalendario, StringComparison.OrdinalIgnoreCase)); + calendarios.Add(new FilaCalendarioCsv( + IdCalendario, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + new DateOnly(2000, 1, 1), + new DateOnly(2099, 12, 31))); + } + + return rutasSimuladas; + } + + private static bool EsRutaTaxibus(FilaRutaCsv ruta) + => TaxibusDbus.EsLinea(ruta.Abreviatura) || TaxibusDbus.EsLinea(ruta.Id); + + private static bool TieneHorarioOficial( + IReadOnlyList expediciones, + IReadOnlyDictionary> tiemposPorExpedicion) + { + foreach (var expedicion in expediciones) + { + if (!string.IsNullOrWhiteSpace(expedicion.IdCal) && + !expedicion.IdCal.Equals("0000", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (expedicion.HSalida is { } salida && salida != TimeOnly.MinValue) + return true; + + if (!tiemposPorExpedicion.TryGetValue(expedicion.Id, out var tiempos)) + continue; + + if (tiempos.Any(tiempo => + EsHoraReal(tiempo.HoraLlegada) || EsHoraReal(tiempo.HoraSalida))) + { + return true; + } + } + + return false; + } + + private static bool EsHoraReal(TimeOnly? hora) + => hora is { } valor && valor != TimeOnly.MinValue; + + private static void CrearExpedicionesSimuladas( + FilaRutaCsv ruta, + PlantillaTaxibus plantilla, + int indicePlantilla, + ICollection expediciones, + ICollection tiemposParada) + { + var cantidadSalidas = 24 * 60 / FrecuenciaMinutos; + for (var indiceSalida = 0; indiceSalida < cantidadSalidas; indiceSalida++) + { + var segundosInicio = indiceSalida * FrecuenciaMinutos * 60; + var horaInicio = HoraDesdeSegundos(segundosInicio); + var idExpedicion = + $"{TaxibusDbus.PrefijoExpedicionSimulada}{ruta.Id}_{indicePlantilla:00}_{indiceSalida:00}"; + + expediciones.Add(new FilaExpedicionCsv( + idExpedicion, + plantilla.Expedicion.IdRuta, + plantilla.Expedicion.DirDestino, + plantilla.Expedicion.Direccion, + plantilla.Expedicion.CambioExp, + plantilla.Expedicion.IdPatronExp, + horaInicio, + IdCalendario)); + + var segundosPaso = segundosInicio; + for (var indiceParada = 0; indiceParada < plantilla.Tiempos.Count; indiceParada++) + { + var tiempoPlantilla = plantilla.Tiempos[indiceParada]; + var horaPaso = HoraDesdeSegundos(segundosPaso); + + tiemposParada.Add(new FilaTiempoParadaCsv( + idExpedicion, + horaPaso, + horaPaso, + tiempoPlantilla.DistanciaSiguienteParada, + tiempoPlantilla.IdTiempoPar, + tiempoPlantilla.SecPar, + tiempoPlantilla.TipoSubidaPar, + tiempoPlantilla.TipoBajadaPar, + tiempoPlantilla.NuevaDirDestinoPar)); + + if (indiceParada < plantilla.Tiempos.Count - 1) + segundosPaso += CalcularSegundosEntreParadas(tiempoPlantilla.DistanciaSiguienteParada); + } + } + } + + private static int CalcularSegundosEntreParadas(double distanciaMetros) + { + if (!double.IsFinite(distanciaMetros) || distanciaMetros <= 0) + return SegundosMinimosEntreParadas; + + return Math.Max( + SegundosMinimosEntreParadas, + (int)Math.Round( + distanciaMetros / VelocidadMetrosSegundo, + MidpointRounding.AwayFromZero)); + } + + private static TimeOnly HoraDesdeSegundos(int segundos) + { + var segundosDia = ((segundos % (24 * 3600)) + (24 * 3600)) % (24 * 3600); + return TimeOnly.FromTimeSpan(TimeSpan.FromSeconds(segundosDia)); + } + + private sealed record PlantillaTaxibus( + FilaExpedicionCsv Expedicion, + IReadOnlyList Tiempos); +} diff --git a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs index fbe6257..c8af80e 100644 --- a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs +++ b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs @@ -3363,8 +3363,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas .Where(x => x.DistanciaPieTotalAprox <= limiteAhorroClaro) .OrderBy(x => x.DistanciaPieTotalAprox) .ThenBy(x => DuracionVisibleOrdenacion(x)) - .ThenBy(x => x.PuntuacionCoste) - .Take(8)); + .ThenBy(x => x.PuntuacionCoste)); } } diff --git a/RutasDBUS/Servicios/RedBus/CreadorRedTransporteDbus.cs b/RutasDBUS/Servicios/RedBus/CreadorRedTransporteDbus.cs index 76c61bf..62805a9 100644 --- a/RutasDBUS/Servicios/RedBus/CreadorRedTransporteDbus.cs +++ b/RutasDBUS/Servicios/RedBus/CreadorRedTransporteDbus.cs @@ -57,6 +57,7 @@ public sealed class CreadorRedTransporteDbus : CodigoSinDecimal(row.Id), Nombre = !string.IsNullOrWhiteSpace(row.Nombre) ? row.Nombre : (row.Descripcion ?? ""), ColorArgb = NormalizarColorArgb(row.Color), + HorarioSimulado = archive.RutasConHorarioSimulado.Contains(row.Id ?? ""), CodigoItinerarioPorDefecto = 0 }; diff --git a/RutasDBUS/Servicios/TaxibusDbus.cs b/RutasDBUS/Servicios/TaxibusDbus.cs new file mode 100644 index 0000000..add669b --- /dev/null +++ b/RutasDBUS/Servicios/TaxibusDbus.cs @@ -0,0 +1,32 @@ +namespace RutasDBUS.Servicios; + +/// +/// Identificacion y textos comunes de las lineas Taxibus. +/// +public static class TaxibusDbus +{ + public const string PrefijoExpedicionSimulada = "TAXIBUS_SIM_"; + public const string MensajeReserva = "Esta l\u00ednea requiere solicitud de reserva."; + public const string MensajeHorarioSimulado = + "Horario provisional simulado cada 30 min. Esta l\u00ednea requiere solicitud de reserva."; + + public static bool EsLinea(string? codigo) + { + if (string.IsNullOrWhiteSpace(codigo)) + return false; + + var normalizado = codigo.Trim(); + var separador = normalizado.IndexOf('.'); + if (separador > 0) + normalizado = normalizado[..separador]; + + return normalizado.Equals("TB6", StringComparison.OrdinalIgnoreCase) || + normalizado.Equals("TB7", StringComparison.OrdinalIgnoreCase) || + normalizado.Equals("6", StringComparison.OrdinalIgnoreCase) || + normalizado.Equals("7", StringComparison.OrdinalIgnoreCase); + } + + public static bool EsExpedicionSimulada(string? idExpedicion) + => !string.IsNullOrWhiteSpace(idExpedicion) && + idExpedicion.StartsWith(PrefijoExpedicionSimulada, StringComparison.OrdinalIgnoreCase); +} diff --git a/RutasDBUS/wwwroot/app.css b/RutasDBUS/wwwroot/app.css index 1f8ac76..002e4e6 100644 --- a/RutasDBUS/wwwroot/app.css +++ b/RutasDBUS/wwwroot/app.css @@ -53,6 +53,33 @@ overflow-wrap: anywhere; } +.inspector-previous-point { + display: grid; + gap: 4px; + margin: 7px 0 6px; + padding: 7px 0 2px; + border-top: 1px solid rgba(148, 163, 184, .25); +} + +.inspector-previous-point__distance { + line-height: 1.3; +} + +.inspector-previous-point__details { + display: flex; + flex-wrap: wrap; + gap: 2px 8px; + color: #94a3b8; + font-size: .68rem; + line-height: 1.25; +} + +.inspector-previous-point__details > span:first-child { + flex-basis: 100%; + color: #cbd5e1; + font-weight: 700; +} + .rt-inspector-place-icon { border: 0 !important; background: transparent !important; @@ -373,6 +400,19 @@ html, body { cursor: default !important; } +/* Leaflet enfoca cada tramo SVG con una caja rectangular al pulsarlo. */ +.leaflet-interactive.rt-elev-segment:focus { + outline: none !important; +} + +.rt-elev-segment-halo { + pointer-events: none; +} + +.rt-elev-segment-halo--outer { + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .75)); +} + /* Evitar cursor de mano en puntos, polilíneas, polígonos de Leaflet */ .leaflet-marker-icon, .leaflet-marker-shadow, @@ -946,6 +986,27 @@ html, body { line-height: 1.25; } +.taxibus-notice { + margin: 0 0 .45rem; + padding: .38rem .5rem; + border-left: 3px solid #38bdf8; + border-radius: 4px; + background: rgba(14, 116, 144, .2); + color: #e0f2fe; + font-size: .72rem; + font-weight: 600; + line-height: 1.3; +} + +.taxibus-notice strong { + color: #7dd3fc; + font-weight: 800; +} + +.taxibus-notice--schedule { + margin: .3rem 0 .4rem; +} + /* Lista de alternativas de ruta */ .route-alt-list { @@ -1243,13 +1304,185 @@ html, body { } .history-derived-note { - display: flex; - align-items: baseline; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; gap: .35rem; + width: 100%; font-size: .76rem; color: #fcd34d; } +.history-derived-note__text { + min-width: 0; + overflow-wrap: anywhere; +} + +.history-derived-info-btn { + flex: 0 0 auto; + min-height: 26px; + padding: .2rem .5rem; + border: 1px solid rgba(251, 191, 36, .5); + border-radius: 5px; + background: rgba(120, 53, 15, .24); + color: #fde68a; + font-size: .7rem; + font-weight: 700; + line-height: 1; +} + +.history-derived-info-btn:hover, +.history-derived-info-btn:focus-visible { + border-color: rgba(251, 191, 36, .86); + background: rgba(146, 64, 14, .38); + color: #fef3c7; + outline: none; +} + +.history-derivation-backdrop { + z-index: 5700; +} + +.custom-modal-card.history-derivation-modal { + width: min(620px, calc(100vw - 2rem)); + max-height: min(760px, calc(100dvh - 2rem)); + overflow-y: auto; +} + +.history-derivation-modal__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: .75rem; + margin-bottom: .75rem; +} + +.history-derivation-modal__header .custom-modal-title { + margin-bottom: .15rem; +} + +.history-derivation-modal__source { + color: #94a3b8; + font-size: .75rem; +} + +.history-derivation-steps { + display: flex; + gap: .35rem; + padding: .15rem 0 .55rem; + overflow-x: auto; +} + +.history-derivation-step { + flex: 0 0 auto; + min-height: 30px; + padding: .25rem .55rem; + border: 1px solid rgba(148, 163, 184, .32); + border-radius: 5px; + background: rgba(30, 41, 59, .52); + color: #cbd5e1; + font-size: .72rem; + font-weight: 650; +} + +.history-derivation-step:hover, +.history-derivation-step:focus-visible { + border-color: rgba(56, 189, 248, .7); + color: #e0f2fe; + outline: none; +} + +.history-derivation-step--selected { + border-color: rgba(251, 191, 36, .8); + background: rgba(120, 53, 15, .3); + color: #fef3c7; +} + +.history-derivation-selected { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: .75rem; + padding: .5rem 0; + border-top: 1px solid rgba(148, 163, 184, .2); + border-bottom: 1px solid rgba(148, 163, 184, .2); + color: #f8fafc; +} + +.history-derivation-selected span { + color: #94a3b8; + font-size: .72rem; + white-space: nowrap; +} + +.history-derivation-comparison { + padding-top: .7rem; +} + +.history-derivation-comparison + .history-derivation-comparison { + margin-top: .25rem; + border-top: 1px solid rgba(148, 163, 184, .2); +} + +.history-derivation-comparison h6 { + margin: 0 0 .35rem; + color: #cbd5e1; + font-size: .78rem; + font-weight: 700; +} + +.history-derivation-change { + padding: .48rem 0; + border-bottom: 1px solid rgba(148, 163, 184, .14); +} + +.history-derivation-change:last-child { + border-bottom: 0; +} + +.history-derivation-change__heading { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: .75rem; + margin-bottom: .28rem; + color: #e2e8f0; + font-size: .76rem; +} + +.history-derivation-change__heading span { + color: #fbbf24; + font-size: .7rem; + text-align: right; +} + +.history-derivation-change__value { + display: grid; + grid-template-columns: 54px minmax(0, 1fr); + gap: .4rem; + padding: .08rem 0; + color: #cbd5e1; + font-size: .72rem; + line-height: 1.35; +} + +.history-derivation-change__value > span { + color: #64748b; + font-weight: 700; + text-transform: uppercase; +} + +.history-derivation-change__value > div { + min-width: 0; + overflow-wrap: anywhere; +} + +.history-derivation-no-changes { + padding: .35rem 0; + color: #94a3b8; + font-size: .74rem; +} + .history-item-action, .history-item-delete { flex: 0 0 32px; diff --git a/RutasDBUS/wwwroot/app.js b/RutasDBUS/wwwroot/app.js index 1bd05e4..5ca8bfd 100644 --- a/RutasDBUS/wwwroot/app.js +++ b/RutasDBUS/wwwroot/app.js @@ -100,8 +100,137 @@ window.rt.ensureRouteLayer = function () { return true; }; +window.rt.clearElevationSegmentSelection = function () { + const selected = window.rt._selectedElevationSegment; + if (!selected) return; + + try { + if (selected.line && selected.originalStyle) { + selected.line.setStyle(selected.originalStyle); + } + + if (selected.group && Array.isArray(selected.endpoints)) { + selected.endpoints.forEach(function (endpoint) { + selected.group.removeLayer(endpoint); + }); + } + + if (selected.group && Array.isArray(selected.halos)) { + selected.halos.forEach(function (halo) { + selected.group.removeLayer(halo); + }); + } + } catch { } + + window.rt._selectedElevationSegment = null; +}; + +window.rt.ensureElevationSegmentSelectionDismiss = function () { + if (window.rt._elevationSelectionDismissHandler) return; + + window.rt._elevationSelectionDismissHandler = function (event) { + const selected = window.rt._selectedElevationSegment; + if (!selected) return; + + const selectedPath = selected.line && selected.line._path; + const target = event && event.target; + if (selectedPath && target && + (target === selectedPath || + (typeof selectedPath.contains === "function" && selectedPath.contains(target)))) { + return; + } + + window.rt.clearElevationSegmentSelection(); + }; + + document.addEventListener( + "click", + window.rt._elevationSelectionDismissHandler, + true); +}; + +window.rt.ensureElevationSegmentSelectionDismiss(); + +window.rt.selectElevationSegment = function ( + routeId, + line, + coords, + group, + originalStyle, + baseWeight) { + window.rt.clearElevationSegmentSelection(); + + if (!line || !Array.isArray(coords) || coords.length < 2 || !group) return; + + const selectionColor = originalStyle && originalStyle.color + ? originalStyle.color + : "#dc2626"; + const selectedWeight = Math.max(6, Number(baseWeight || 4) + 2); + const outerHalo = window.L.polyline(coords, { + color: "#111827", + weight: selectedWeight + 7, + opacity: 0.94, + dashArray: null, + lineCap: "round", + interactive: false, + className: "rt-elev-segment-halo rt-elev-segment-halo--outer" + }); + const innerHalo = window.L.polyline(coords, { + color: "#f8fafc", + weight: selectedWeight + 4, + opacity: 0.96, + dashArray: null, + lineCap: "round", + interactive: false, + className: "rt-elev-segment-halo rt-elev-segment-halo--inner" + }); + outerHalo.addTo(group); + innerHalo.addTo(group); + + line.setStyle({ + color: selectionColor, + weight: selectedWeight, + opacity: 1, + dashArray: null, + lineCap: "round" + }); + + const endpointOptions = { + radius: 4, + color: "#ffffff", + weight: 2, + opacity: 1, + fillColor: selectionColor, + fillOpacity: 1, + interactive: false, + className: "rt-elev-segment-endpoint" + }; + const endpoints = [ + window.L.circleMarker(coords[0], endpointOptions), + window.L.circleMarker(coords[coords.length - 1], endpointOptions) + ]; + + endpoints.forEach(function (endpoint) { + endpoint.addTo(group); + }); + try { line.bringToFront(); } catch { } + endpoints.forEach(function (endpoint) { + try { endpoint.bringToFront(); } catch { } + }); + + window.rt._selectedElevationSegment = { + routeId, + line, + group, + halos: [outerHalo, innerHalo], + endpoints, + originalStyle + }; +}; + window.rt.clearRoutes = function () { if (!window.rt.ensureRouteLayer()) return; + window.rt.clearElevationSegmentSelection(); window.rt._routeLayer.clearLayers(); window.rt._routeLines = {}; window.rt._routeCoords = {}; @@ -114,6 +243,11 @@ window.rt.clearRoutes = function () { window.rt.drawRoute = function (id, coords, options) { if (!window.rt.ensureRouteLayer()) return; + if (window.rt._selectedElevationSegment && + window.rt._selectedElevationSegment.routeId === id) { + window.rt.clearElevationSegmentSelection(); + } + if (window.rt._routeLines[id]) { window.rt._routeLayer.removeLayer(window.rt._routeLines[id]); delete window.rt._routeLines[id]; @@ -241,11 +375,19 @@ window.rt.drawRoute = function (id, coords, options) { weight, opacity, lineCap: lineCap || "round", + className: "rt-elev-segment", bubblingMouseEvents: false }; if (dashArray) segmentOpts.dashArray = dashArray; const segmentLine = buildLine(segmentCoords, segmentOpts); + const originalSegmentStyle = { + color: segmentColor, + weight, + opacity, + lineCap: lineCap || "round", + dashArray: dashArray || null + }; const label = elevationTooltip(segment, 0.5); if (label) { @@ -262,6 +404,21 @@ window.rt.drawRoute = function (id, coords, options) { segmentLine.on("mouseover", updateTooltipAtCursor); segmentLine.on("mousemove", updateTooltipAtCursor); segmentLine.on("click", function (e) { + window.rt.selectElevationSegment( + id, + segmentLine, + segmentCoords, + group, + originalSegmentStyle, + weight); + window.requestAnimationFrame(function () { + try { + if (segmentLine._path && typeof segmentLine._path.blur === "function") { + segmentLine._path.blur(); + } + } catch { } + }); + if (!window.rt.isMobileLayout || !window.rt.isMobileLayout()) return; updateTooltipAtCursor(e); try { segmentLine.openTooltip(e && e.latlng); } catch { }