From 7090dfa9be4285baf5876f26efbd5c962b3d74d9 Mon Sep 17 00:00:00 2001 From: Pedro Date: Wed, 16 Sep 2026 13:26:13 +0200 Subject: [PATCH] =?UTF-8?q?v-20260916a=20-=20restaurar=20derivaciones=20-?= =?UTF-8?q?=20a=C3=B1adido=20la=20pedida=20de=20anchura=20de=20calles=20y?= =?UTF-8?q?=20cariiles=20-=20correcion=20paradas=20bus=20-=20desglose=20de?= =?UTF-8?q?l=20score=20-=20historial=20no=20cambia=20la=20hora=20-=20resua?= =?UTF-8?q?rar=20no=20cambia=20modo=20de=20historial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pages/PlanificadorRutas.Estado.cs | 136 ++++++++++++-- .../Pages/PlanificadorRutas.Historial.cs | 97 ++++++++-- .../Pages/PlanificadorRutas.Itinerarios.cs | 32 ++-- .../Pages/PlanificadorRutas.Superficie.cs | 47 ++++- .../Pages/PlanificadorRutas.ValoresFabrica.cs | 1 - .../Components/Pages/PlanificadorRutas.razor | 128 ++++++++++++- .../Planificacion/InformacionSueloTramo.cs | 3 + RutasDBUS/Program.cs | 2 + .../Servicios/Caminata/ClienteAtributosOsm.cs | 136 ++++++++++++++ .../Servicios/Caminata/ClienteValhalla.cs | 79 ++++++-- .../Caminata/IClienteAtributosOsm.cs | 10 ++ .../PlanificadorRutasServicio.cs | 7 - RutasDBUS/appsettings.json | 3 + RutasDBUS/wwwroot/app.css | 168 +++++++++++++++++- tests/RutasDBUS.RegressionTests/Program.cs | 101 +++++++++++ 15 files changed, 867 insertions(+), 83 deletions(-) create mode 100644 RutasDBUS/Servicios/Caminata/ClienteAtributosOsm.cs create mode 100644 RutasDBUS/Servicios/Caminata/IClienteAtributosOsm.cs diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs index f9bac43..b630923 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs @@ -1455,31 +1455,135 @@ public partial class PlanificadorRutas : ComponentBase private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa) { - if (alternativa is null) - return double.PositiveInfinity; + var desglose = CrearDesgloseScoreEquilibrado(alternativa); + return desglose?.ScoreTotal ?? double.PositiveInfinity; + } - var distanciaBus = 0.0; - if (!alternativa.EsSoloAPie) - { - if (alternativa.AlternativaBus is null) - return double.PositiveInfinity; + private DesgloseScoreEquilibrado? CrearDesgloseScoreEquilibrado(AlternativaRuta? alternativa) + { + if (alternativa is null || (!alternativa.EsSoloAPie && alternativa.AlternativaBus is null)) + return null; - distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus); - } + var parametros = CrearParametrosPenalizacionActuales(); + var distanciasPie = alternativa.EsSoloAPie + ? new[] { alternativa.DistanciaSoloAPie } + : alternativa.DistanciasPiePorTramoAprox.Count > 0 + ? alternativa.DistanciasPiePorTramoAprox.ToArray() + : new[] + { + alternativa.DistanciaPieInicioAprox, + alternativa.DistanciaPieTransbordosAprox, + alternativa.DistanciaPieFinAprox + }; - var distanciaAndando = CalcularDistanciaCorregidaAlternativa(alternativa); - var factor = double.IsFinite(_factorEquilibradoAndarBus) && _factorEquilibradoAndarBus > 0 + var tramosAndando = distanciasPie + .Where(distancia => double.IsFinite(distancia) && distancia > 0) + .Select((distancia, indice) => new TramoAndandoScore( + indice + 1, + distancia, + UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(distancia, parametros))) + .ToList(); + var distanciaAndandoCorregidaBase = tramosAndando.Sum(x => x.DistanciaCorregidaMetros); + var esfuerzoExtraPendiente = _mostrarElevacionesRuta && + double.IsFinite(alternativa.EsfuerzoExtraPendienteMetros) + ? Math.Max(0, alternativa.EsfuerzoExtraPendienteMetros) + : 0; + var aportacionAndando = distanciaAndandoCorregidaBase + esfuerzoExtraPendiente; + + var distanciaBus = alternativa.EsSoloAPie + ? 0 + : Math.Max(0, alternativa.AlternativaBus!.DistanciaTotalBus); + var factorEquilibrado = double.IsFinite(_factorEquilibradoAndarBus) && + _factorEquilibradoAndarBus > 0 ? _factorEquilibradoAndarBus : FactorEquilibradoAndarBusPorDefecto; + var aportacionBus = distanciaBus / factorEquilibrado; + + var penalizacionSegundos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingSegundos( + alternativa.NumeroTransbordosReales, + parametros); var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros( alternativa.NumeroTransbordosReales, - CrearParametrosPenalizacionActuales()); - var score = distanciaAndando + (distanciaBus / factor) + penalizacionTransbordos; - return double.IsFinite(score) && score >= 0 - ? score - : double.PositiveInfinity; + parametros); + var pesoTransbordoMinutos = double.IsFinite(_pesoTransbordoRankingMinutos) && + _pesoTransbordoRankingMinutos > 0 + ? _pesoTransbordoRankingMinutos + : 0; + var velocidadBusKmH = double.IsFinite(_velocidadBusKmH) && _velocidadBusKmH > 0 + ? _velocidadBusKmH + : 16.0; + var factorAndando = double.IsFinite(_factorPenalizacionDistanciaPie) + ? _factorPenalizacionDistanciaPie + : 1.8; + var baseAndandoMetros = double.IsFinite(_parametroPenalizacionDistanciaPieMetros) + ? Math.Abs(_parametroPenalizacionDistanciaPieMetros) + : 200.0; + if (baseAndandoMetros <= 0) + baseAndandoMetros = 1.0; + + var score = aportacionAndando + aportacionBus + penalizacionTransbordos; + if (!double.IsFinite(score) || score < 0) + return null; + + return new DesgloseScoreEquilibrado( + tramosAndando, + factorAndando, + baseAndandoMetros, + distanciaAndandoCorregidaBase, + esfuerzoExtraPendiente, + _mostrarElevacionesRuta, + aportacionAndando, + distanciaBus, + factorEquilibrado, + aportacionBus, + alternativa.NumeroTransbordosReales, + pesoTransbordoMinutos, + penalizacionSegundos, + velocidadBusKmH, + penalizacionTransbordos, + score); } + private void AbrirDesgloseScore(AlternativaRuta alternativa) + => _desgloseScoreVisible = CrearDesgloseScoreEquilibrado(alternativa); + + private void CerrarDesgloseScore() + => _desgloseScoreVisible = null; + + private static string FormatearMetrosDesgloseScore(double valor) + => $"{valor.ToString("0.00", CultureInfo.InvariantCulture)} m"; + + private static string FormatearNumeroDesgloseScore(double valor) + => valor.ToString("0.##", CultureInfo.InvariantCulture); + + private static string FormatearScoreFinalDesglose(double valor) + => valor.ToString("0", CultureInfo.InvariantCulture); + + private sealed record TramoAndandoScore( + int Numero, + double DistanciaRealMetros, + double DistanciaCorregidaMetros); + + private sealed record DesgloseScoreEquilibrado( + IReadOnlyList TramosAndando, + double FactorAndando, + double BaseAndandoMetros, + double DistanciaAndandoCorregidaBaseMetros, + double EsfuerzoExtraPendienteMetros, + bool PendientesAplicadas, + double AportacionAndandoMetros, + double DistanciaBusMetros, + double FactorEquilibrado, + double AportacionBusMetros, + int NumeroTransbordos, + double PesoTransbordoMinutos, + double PenalizacionTransbordosSegundos, + double VelocidadBusKmH, + double PenalizacionTransbordosMetros, + double ScoreTotal); + + private DesgloseScoreEquilibrado? _desgloseScoreVisible; + private string FormatearScoreEquilibradoAlternativa(AlternativaRuta? alternativa) { var score = CalcularScoreEquilibradoFactor(alternativa); diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs index 3e5395a..499bb68 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs @@ -40,6 +40,8 @@ public partial class PlanificadorRutas private RegistroHistorialRuta? _escenarioBaseDerivacionHistorial; private bool _mostrarInformacionDerivacionHistorial; private int _indicePasoDerivacionSeleccionado = -1; + private int _indicePasoDerivacionActivo = -1; + private bool _cargandoPasoDerivacionHistorial; private string _mensajeHistorial = string.Empty; private bool _mensajeHistorialEsError = false; private bool _mostrarHistorialRutas = false; @@ -76,7 +78,8 @@ public partial class PlanificadorRutas private sealed record PasoDerivacionHistorial( int Numero, DateTime FechaUtc, - RegistroHistorialRuta Escenario); + RegistroHistorialRuta Escenario, + int? NumeroAnterior); private sealed record CambioDerivacionHistorial( string Campo, @@ -1084,8 +1087,9 @@ public partial class PlanificadorRutas return string.Empty; var numeroDerivacion = _tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido && - _pasosDerivacionHistorial.Count > 0 - ? $" · Derivación n.º {_pasosDerivacionHistorial.Count}" + _indicePasoDerivacionActivo >= 0 && + _indicePasoDerivacionActivo < _pasosDerivacionHistorial.Count + ? $" · Derivación n.º {_pasosDerivacionHistorial[_indicePasoDerivacionActivo].Numero}" : string.Empty; return $"Ruta actual derivada del Historial {nombre} n.º {indice + 1}{numeroDerivacion}."; } @@ -1113,7 +1117,9 @@ public partial class PlanificadorRutas if (!PuedeMostrarInformacionDerivacionHistorial) return; - _indicePasoDerivacionSeleccionado = _pasosDerivacionHistorial.Count - 1; + _indicePasoDerivacionSeleccionado = _indicePasoDerivacionActivo >= 0 + ? _indicePasoDerivacionActivo + : _pasosDerivacionHistorial.Count - 1; _mostrarInformacionDerivacionHistorial = true; } @@ -1130,11 +1136,46 @@ public partial class PlanificadorRutas _indicePasoDerivacionSeleccionado = indice; } + private async Task CargarPasoDerivacionHistorialAsync() + { + if (_cargandoPasoDerivacionHistorial || + _indicePasoDerivacionSeleccionado < 0 || + _indicePasoDerivacionSeleccionado >= _pasosDerivacionHistorial.Count) + { + return; + } + + var indice = _indicePasoDerivacionSeleccionado; + var paso = _pasosDerivacionHistorial[indice]; + _cargandoPasoDerivacionHistorial = true; + try + { + await AplicarRegistroHistorialAsync(ClonarEscenarioHistorial(paso.Escenario)); + _indicePasoDerivacionActivo = indice; + _indicePasoDerivacionSeleccionado = indice; + _rutaActualDerivadaDeHistorial = ExisteReferenciaHistorialActual(); + _mostrarInformacionDerivacionHistorial = false; + MostrarMensajeHistorial($"Derivación n.º {paso.Numero} cargada."); + } + catch + { + MostrarMensajeHistorial( + $"No he podido cargar la Derivación n.º {paso.Numero}.", + esError: true); + } + finally + { + _cargandoPasoDerivacionHistorial = false; + } + } + private void ReiniciarSeguimientoDerivacionHistorial() { _pasosDerivacionHistorial.Clear(); _escenarioBaseDerivacionHistorial = null; _indicePasoDerivacionSeleccionado = -1; + _indicePasoDerivacionActivo = -1; + _cargandoPasoDerivacionHistorial = false; _mostrarInformacionDerivacionHistorial = false; } @@ -1155,9 +1196,11 @@ public partial class PlanificadorRutas if (actual is null) return; - var anterior = _pasosDerivacionHistorial.Count > 0 - ? _pasosDerivacionHistorial[^1].Escenario - : _escenarioBaseDerivacionHistorial; + var pasoAnterior = _indicePasoDerivacionActivo >= 0 && + _indicePasoDerivacionActivo < _pasosDerivacionHistorial.Count + ? _pasosDerivacionHistorial[_indicePasoDerivacionActivo] + : null; + var anterior = pasoAnterior?.Escenario ?? _escenarioBaseDerivacionHistorial; if (string.Equals( CalcularClaveEscenario(anterior), CalcularClaveEscenario(actual), @@ -1169,20 +1212,21 @@ public partial class PlanificadorRutas _pasosDerivacionHistorial.Add(new PasoDerivacionHistorial( _pasosDerivacionHistorial.Count + 1, DateTime.UtcNow, - ClonarEscenarioHistorial(actual))); + ClonarEscenarioHistorial(actual), + pasoAnterior?.Numero)); _indicePasoDerivacionSeleccionado = _pasosDerivacionHistorial.Count - 1; + _indicePasoDerivacionActivo = _indicePasoDerivacionSeleccionado; } private RegistroHistorialRuta? ObtenerEscenarioAnteriorDerivacion( PasoDerivacionHistorial paso) { - var indice = paso.Numero - 1; - if (indice <= 0) + if (paso.NumeroAnterior is null) return _escenarioBaseDerivacionHistorial; - return indice - 1 < _pasosDerivacionHistorial.Count - ? _pasosDerivacionHistorial[indice - 1].Escenario - : null; + return _pasosDerivacionHistorial + .FirstOrDefault(item => item.Numero == paso.NumeroAnterior.Value) + ?.Escenario; } private IReadOnlyList ObtenerCambiosDerivacion( @@ -1527,7 +1571,8 @@ public partial class PlanificadorRutas var configuracionHistorial = _modoConfiguracionHistorial switch { ModoConfiguracionHistorial.Guardada => CrearConfiguracionGuardadaHistorial(item), - ModoConfiguracionHistorial.Predeterminada => CrearConfiguracionPredeterminadaHistorial(), + ModoConfiguracionHistorial.Predeterminada => + CrearConfiguracionPredeterminadaParaHistorial(item), _ => null }; @@ -1609,6 +1654,30 @@ public partial class PlanificadorRutas return configuracion; } + private ConfiguracionHistorialRuta CrearConfiguracionPredeterminadaParaHistorial( + RegistroHistorialRuta item) + { + var predeterminada = CrearConfiguracionPredeterminadaHistorial(); + var guardada = CrearConfiguracionGuardadaHistorial(item); + predeterminada.UsarHoraSimulada = guardada.UsarHoraSimulada; + + if (guardada.UsarHoraSimulada) + { + predeterminada.FechaSimulada = guardada.FechaSimulada; + predeterminada.HoraSimulada = guardada.HoraSimulada; + } + else + { + var ahora = DateTime.Now; + predeterminada.FechaSimulada = DateOnly.FromDateTime(ahora) + .ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + predeterminada.HoraSimulada = TimeOnly.FromDateTime(ahora) + .ToString("HH:mm:ss", CultureInfo.InvariantCulture); + } + + return predeterminada; + } + private void CapturarConfiguracionTemporalAntesDeHistorial() { if (_configuracionTemporalPreviaHistorial is not null) diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs index 913e2ff..2bd81ac 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs @@ -652,17 +652,23 @@ public partial class PlanificadorRutas : ComponentBase var fecha = DateOnly.FromDateTime(now); var hora = TimeOnly.FromDateTime(now); - var deps = Horarios.GetNextDeparturesByCodigo( - paradaDetectada.Codigo, - fecha, - hora, - take: null, - includePast: true); + var horarios = await Task.Run(() => + { + cancellationToken.ThrowIfCancellationRequested(); + var deps = Horarios.GetNextDeparturesByCodigo( + paradaDetectada.Codigo, + fecha, + hora, + take: null, + includePast: true); + cancellationToken.ThrowIfCancellationRequested(); + return ConstruirListaHorariosConActual(deps, hora); + }, cancellationToken); if (!EsPopupParadaVigente(paradaDetectada, token, cancellationToken)) return; - _nextDeps = ConstruirListaHorariosConActual(deps, hora); + _nextDeps = horarios; _scrollHorariosPendiente = true; } catch (Exception ex) @@ -686,7 +692,7 @@ public partial class PlanificadorRutas : ComponentBase token == Volatile.Read(ref _paradaPopupToken) && string.Equals(_paradaClicada?.Codigo, parada.Codigo, StringComparison.OrdinalIgnoreCase); - private async Task CerrarPopupParadaAsync() + private Task CerrarPopupParadaAsync() { Interlocked.Increment(ref _paradaPopupToken); _ctsParadaPopup?.Cancel(); @@ -699,8 +705,14 @@ public partial class PlanificadorRutas : ComponentBase _paradaElevLocal1 = null; _nextDeps.Clear(); _scrollHorariosPendiente = false; - try { await JS.InvokeVoidAsync("rt.clearSelectedStopMarker"); } catch { } StateHasChanged(); + _ = LimpiarMarcadorParadaSeleccionadaAsync(); + return Task.CompletedTask; + } + + private async Task LimpiarMarcadorParadaSeleccionadaAsync() + { + try { await JS.InvokeVoidAsync("rt.clearSelectedStopMarker"); } catch { } } @@ -1494,7 +1506,7 @@ public partial class PlanificadorRutas : ComponentBase private bool _elevPopupMinimizado = false; - private const string _versionApp = "(v-20260914a)"; + private const string _versionApp = "(v-20260916a)"; private double? _paradaElevIdee; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Superficie.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Superficie.cs index a4768e6..23b6e08 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Superficie.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Superficie.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Globalization; +using System.Net; using Microsoft.JSInterop; using RutasDBUS.Modelos.Planificacion; @@ -46,7 +47,7 @@ public partial class PlanificadorRutas return; } - _inspectorSuperficieTexto = "No disponible"; + _inspectorSuperficieTexto = FormatearSuperficieNoDisponible(" · "); _inspectorSuperficieCargando = false; await InvokeAsync(StateHasChanged); } @@ -81,7 +82,7 @@ public partial class PlanificadorRutas private static string FormatearSuperficiePuntoInspector(InformacionSueloTramo? informacion) { if (informacion is null || !informacion.Disponible || informacion.Segmentos.Count == 0) - return "No disponible"; + return FormatearSuperficieNoDisponible(" · "); var superficies = informacion.Segmentos .Select(segmento => @@ -89,7 +90,8 @@ public partial class PlanificadorRutas var uso = string.IsNullOrWhiteSpace(segmento.Uso) ? "" : $" ({segmento.Uso})"; - return $"{segmento.Superficie}{uso}"; + var atributos = FormatearAtributosVia(segmento, " · "); + return $"{segmento.Superficie}{uso}{atributos}"; }) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); @@ -136,7 +138,7 @@ public partial class PlanificadorRutas } catch { - superficie = "No disponible"; + superficie = FormatearSuperficieNoDisponible("
"); } if (version != _versionConsultaSuperficieSegmento) @@ -159,7 +161,7 @@ public partial class PlanificadorRutas private static string FormatearSuperficieSegmento(InformacionSueloTramo? informacion) { if (informacion is null || !informacion.Disponible || informacion.Segmentos.Count == 0) - return "No disponible"; + return FormatearSuperficieNoDisponible("
"); return string.Join( "
", @@ -171,7 +173,40 @@ public partial class PlanificadorRutas var distancia = segmento.DistanciaMetros < 1000 ? segmento.DistanciaMetros.ToString("0.##", CultureInfo.InvariantCulture) + " m" : (segmento.DistanciaMetros / 1000.0).ToString("0.###", CultureInfo.InvariantCulture) + " km"; - return $"{segmento.Superficie}{uso}, {distancia}"; + var atributos = FormatearAtributosVia(segmento, "
"); + return $"{segmento.Superficie}{uso}, {distancia}{atributos}"; })); } + + private static string FormatearAtributosVia(SegmentoSueloTramo segmento, string separador) + { + var atributos = new List(2); + var carriles = segmento.NumeroCarriles is > 0 + ? segmento.NumeroCarriles.Value.ToString(CultureInfo.InvariantCulture) + : "-"; + atributos.Add($"Carriles: {carriles}"); + if (!string.IsNullOrWhiteSpace(segmento.AnchuraVia)) + { + var etiqueta = segmento.AnchuraViaEstimada ? "Anchura estimada" : "Anchura"; + atributos.Add($"{etiqueta}: {FormatearAnchuraVia(segmento.AnchuraVia)}"); + } + else + { + atributos.Add("Anchura: -"); + } + + return separador + string.Join(" · ", atributos); + } + + private static string FormatearSuperficieNoDisponible(string separador) + => "No disponible" + FormatearAtributosVia(new SegmentoSueloTramo(), separador); + + private static string FormatearAnchuraVia(string anchura) + { + var valor = anchura.Trim(); + var texto = double.TryParse(valor, NumberStyles.Float, CultureInfo.InvariantCulture, out _) + ? $"{valor} m" + : valor; + return WebUtility.HtmlEncode(texto); + } } diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.ValoresFabrica.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.ValoresFabrica.cs index ebda092..5339e1e 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.ValoresFabrica.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.ValoresFabrica.cs @@ -33,7 +33,6 @@ public partial class PlanificadorRutas predeterminada.CriterioPrioritarioRuta = actual.CriterioPrioritarioRuta; predeterminada.PreferenciaEtiquetaRuta = actual.PreferenciaEtiquetaRuta; AplicarConfiguracionLocal(predeterminada); - _modoConfiguracionHistorial = ModoConfiguracionHistorial.Actual; } [JSInvokable] diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.razor b/RutasDBUS/Components/Pages/PlanificadorRutas.razor index 87e57b9..b8e17a1 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.razor +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.razor @@ -48,7 +48,6 @@ -