diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs index b31ecd1..5f2a960 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs @@ -50,9 +50,10 @@ public partial class PlanificadorRutas : ComponentBase private const double FactorPenalizacionDistanciaPiePorDefecto = 1.35; private const double ParametroPenalizacionDistanciaPieMetrosPorDefecto = 200.0; private const double CoeficienteEsfuerzoAlturaPorDefecto = 8.2; + private const double FactorEquilibradoAndarBusPorDefecto = 6.0; private const double UmbralPendienteSubidaRutaPorDefecto = 2.0; private const double UmbralPendienteBajadaRutaPorDefecto = 5.0; - private const double UmbralPisosPendienteRutaPorDefecto = 2.0; + private const double UmbralAlturaAlertaPendienteRutaMetrosPorDefecto = 6.0; private const double MetrosPorPiso = 3.0; private double _velocidadPieKmH = 3.6; @@ -60,12 +61,12 @@ public partial class PlanificadorRutas : ComponentBase private double _parametroPenalizacionDistanciaPieMetros = ParametroPenalizacionDistanciaPieMetrosPorDefecto; private double _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; private double _pesoTransbordoRankingMinutos = 4.0; - private double _factorEquilibradoAndarBus = 12.0; + private double _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto; private string _factorPenalizacionDistanciaPieTextoEdicion = "1.35"; private string _parametroPenalizacionDistanciaPieTextoEdicion = "200"; private string _coeficienteEsfuerzoAlturaTextoEdicion = "8.20"; private string _pesoTransbordoRankingTextoEdicion = "4,0"; - private string _factorEquilibradoAndarBusTextoEdicion = "12"; + private string _factorEquilibradoAndarBusTextoEdicion = "6"; private double _velocidadBusKmH = 16.0; private int _minutosTransbordo = 5; private int _margenTransbordoSegundos = 120; @@ -275,7 +276,11 @@ public partial class PlanificadorRutas : ComponentBase { SincronizarTextosPenalizacionPie(); await PersistirConfiguracionAsync(); - await RecalcularRankingTrasCambioEsfuerzoPendienteAsync(); + + if (_mostrarElevacionesRuta) + await RecalcularRankingTrasCambioEsfuerzoPendienteAsync(); + else + StateHasChanged(); } /// @@ -285,6 +290,7 @@ public partial class PlanificadorRutas : ComponentBase { SincronizarTextosPenalizacionPie(); await PersistirConfiguracionAsync(); + await RecalcularRutaEquilibradaTrasCambioFactorAsync(); } /// @@ -364,14 +370,14 @@ public partial class PlanificadorRutas : ComponentBase private bool _panelRutasMovilContraido = false; private double _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto; private double _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto; - private double _umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto; + private double _umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto; private double _alturaPendientePositivaSignificativaRutaMetros = 0.0; private double _distanciaPendientePositivaSignificativaRutaMetros = 0.0; - private bool MostrarAvisoPendientesPositivasSignificativas => + private bool MostrarAlertaPendientesPositivasSignificativas => _mostrarElevacionesRuta && _alturaPendientePositivaSignificativaRutaMetros > - (NormalizarUmbralPisosPendienteRuta(_umbralPisosPendienteRuta) * MetrosPorPiso); + NormalizarUmbralAlturaAlertaPendienteRutaMetros(_umbralAlturaAlertaPendienteRutaMetros); private readonly Dictionary> _paradasItinerarioVisiblesPorId = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary> _paradasRutaVisiblesPorId = new(StringComparer.OrdinalIgnoreCase); @@ -1351,15 +1357,18 @@ public partial class PlanificadorRutas : ComponentBase } /// - /// Construye los parametros actuales que afectan a la penalizacion peatonal. + /// Construye los parametros actuales que afectan al ranking de rutas. /// private ParametrosPlanificadorRuta CrearParametrosPenalizacionActuales() { return new ParametrosPlanificadorRuta { VelocidadPieKmH = _velocidadPieKmH, + VelocidadBusKmH = _velocidadBusKmH, FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie, - ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros + ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros, + PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos, + FactorEquilibradoAndarBus = _factorEquilibradoAndarBus }; } @@ -1386,7 +1395,8 @@ public partial class PlanificadorRutas : ComponentBase var distanciaCorregidaBase = UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaTotalMetros( distanciasPiePorTramo, parametros); - var esfuerzoExtra = double.IsFinite(alternativa.EsfuerzoExtraPendienteMetros) + var esfuerzoExtra = _mostrarElevacionesRuta && + double.IsFinite(alternativa.EsfuerzoExtraPendienteMetros) ? Math.Max(0, alternativa.EsfuerzoExtraPendienteMetros) : 0; @@ -1416,7 +1426,7 @@ public partial class PlanificadorRutas : ComponentBase var distanciaAndando = CalcularDistanciaCorregidaAlternativa(alternativa); var factor = double.IsFinite(_factorEquilibradoAndarBus) ? Math.Max(0, _factorEquilibradoAndarBus) - : 12.0; + : FactorEquilibradoAndarBusPorDefecto; var distanciaEquilibrada = Math.Max(0, distanciaAndando) * factor; return double.IsFinite(distanciaEquilibrada) @@ -1431,7 +1441,10 @@ public partial class PlanificadorRutas : ComponentBase var distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus); var distanciaEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(alternativa); - var score = distanciaBus + distanciaEquilibradaAndando; + var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros( + alternativa.NumeroTransbordosReales, + CrearParametrosPenalizacionActuales()); + var score = distanciaBus + distanciaEquilibradaAndando + penalizacionTransbordos; return double.IsFinite(score) && score >= 0 ? score : double.PositiveInfinity; @@ -1443,7 +1456,7 @@ public partial class PlanificadorRutas : ComponentBase if (!double.IsFinite(score)) return string.Empty; - return $"Score {FormatearDistanciaTarjeta(score)}"; + return FormatearDistanciaTarjeta(score); } private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync() diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs index d0123a5..e833030 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs @@ -12,6 +12,7 @@ public partial class PlanificadorRutas private const string ClaveRespaldoHistorialLocal = "rutasdbus.planificador.routeHistory.recovery.v1"; private const int MaxItemsHistorialLocal = 100; private const int IntervaloRefrescoHistorialCompartidoSegundos = 3; + private const int TiempoMaxOperacionHistorialLocalSegundos = 8; private static readonly JsonSerializerOptions OpcionesLecturaHistorialLocal = new() { PropertyNameCaseInsensitive = true, @@ -36,6 +37,27 @@ public partial class PlanificadorRutas private bool _historialLocalCargado = false; private bool _historialLocalCargando = false; private int _versionAperturaHistorial; + private int _numeroIntentoCargaHistorialLocal; + private string _resumenErrorHistorialLocal = string.Empty; + private string _detalleErrorHistorialLocal = string.Empty; + + private sealed class ResultadoStorageNavegador + { + [JsonPropertyName("ok")] + public bool Ok { get; set; } + + [JsonPropertyName("found")] + public bool Found { get; set; } + + [JsonPropertyName("value")] + public string? Value { get; set; } + + [JsonPropertyName("errorName")] + public string? ErrorName { get; set; } + + [JsonPropertyName("errorMessage")] + public string? ErrorMessage { get; set; } + } private async Task ToggleHistorialPanel() { @@ -80,8 +102,9 @@ public partial class PlanificadorRutas { return await tareaCarga; } - catch + catch (Exception ex) { + RegistrarErrorHistorialLocal("coordinacion de la carga", ex); return false; } finally @@ -93,62 +116,159 @@ public partial class PlanificadorRutas private async Task CargarHistorialLocalDesdeNavegadorAsync() { - string? json; + var numeroIntento = ++_numeroIntentoCargaHistorialLocal; + ResultadoStorageNavegador? resultadoLectura; try { - json = await JS.InvokeAsync("rt.loadLocalValue", ClaveHistorialLocal); + using var cts = new CancellationTokenSource( + TimeSpan.FromSeconds(TiempoMaxOperacionHistorialLocalSegundos)); + resultadoLectura = await JS.InvokeAsync( + "rt.loadLocalValueResult", + cts.Token, + ClaveHistorialLocal); } - catch (JSException) + catch (OperationCanceledException ex) { + RegistrarErrorHistorialLocal( + "lectura de localStorage", + new TimeoutException( + $"La lectura no respondio en {TiempoMaxOperacionHistorialLocalSegundos} segundos.", + ex), + $"Intento de carga: {numeroIntento}."); return false; } - catch (Exception) + catch (Exception ex) { + RegistrarErrorHistorialLocal( + "lectura de localStorage", + ex, + $"Intento de carga: {numeroIntento}."); return false; } - if (string.IsNullOrWhiteSpace(json)) + if (resultadoLectura is null) + { + RegistrarErrorHistorialLocal( + "lectura de localStorage", + "RespuestaVacia", + "JavaScript no devolvio ningun resultado de lectura.", + $"Intento de carga: {numeroIntento}."); + return false; + } + + if (!resultadoLectura.Ok) + { + RegistrarErrorHistorialLocal( + "lectura de localStorage", + resultadoLectura.ErrorName ?? "StorageError", + resultadoLectura.ErrorMessage ?? "El navegador no ha indicado el motivo del error.", + $"Intento de carga: {numeroIntento}."); + return false; + } + + var json = resultadoLectura.Value; + if (!resultadoLectura.Found || string.IsNullOrWhiteSpace(json)) { FinalizarCargaHistorialLocal(Array.Empty()); + LimpiarErrorHistorialLocal(); return true; } try { - var items = DeserializarHistorialLocal(json, out var registrosOmitidos); + var items = DeserializarHistorialLocal( + json, + out var registrosOmitidos, + out var primerErrorRegistro); FinalizarCargaHistorialLocal(items); if (registrosOmitidos > 0) { - await GuardarHistorialLocalRecuperadoAsync(json); + var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(json); + var detalleFalloRecuperacion = recuperacionGuardada + ? null + : _detalleErrorHistorialLocal; + if (!recuperacionGuardada) + _historialLocalCargado = false; + + RegistrarErrorHistorialLocal( + "validacion de registros", + "RegistrosDaniados", + $"Se omitieron {registrosOmitidos} registros que no se podian utilizar.", + $"Primer fallo: {primerErrorRegistro ?? "sin detalle"}. " + + $"Registros recuperados: {_historialLocal.Count}. " + + $"Tamano leido: {json.Length} caracteres. " + + $"Copia de recuperacion: {(recuperacionGuardada ? "guardada" : "no guardada")}." + + (string.IsNullOrWhiteSpace(detalleFalloRecuperacion) + ? string.Empty + : $"{Environment.NewLine}Fallo al recuperar: {detalleFalloRecuperacion}")); MostrarMensajeHistorial( $"Se han recuperado {_historialLocal.Count} registros del historial; {registrosOmitidos} estaban dañados.", esError: true); } + else + { + LimpiarErrorHistorialLocal(); + } - return true; + return _historialLocalCargado; } - catch (Exception ex) when (ex is JsonException or NotSupportedException or InvalidOperationException) + catch (Exception ex) when ( + ex is JsonException or + NotSupportedException or + InvalidOperationException or + ArgumentException) { FinalizarCargaHistorialLocal(Array.Empty()); - await GuardarHistorialLocalRecuperadoAsync(json); + var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(json); + var detalleFalloRecuperacion = recuperacionGuardada + ? null + : _detalleErrorHistorialLocal; + if (!recuperacionGuardada) + _historialLocalCargado = false; + + RegistrarErrorHistorialLocal( + "deserializacion del historial", + ex, + $"Tamano leido: {json.Length} caracteres. " + + $"Copia de recuperacion: {(recuperacionGuardada ? "guardada" : "no guardada")}." + + (string.IsNullOrWhiteSpace(detalleFalloRecuperacion) + ? string.Empty + : $"{Environment.NewLine}Fallo al recuperar: {detalleFalloRecuperacion}")); MostrarMensajeHistorial( - "El historial personal guardado estaba dañado. Se ha conservado una copia y se ha iniciado uno nuevo.", + recuperacionGuardada + ? "El historial personal guardado estaba dañado. Se ha conservado una copia y se ha iniciado uno nuevo." + : "El historial personal esta dañado, pero no se ha modificado porque no se pudo guardar la copia. Reintenta la carga.", esError: true); - return true; + return _historialLocalCargado; + } + catch (Exception ex) + { + _historialLocalCargado = false; + RegistrarErrorHistorialLocal( + "fallo inesperado al procesar el historial", + ex, + $"Tamano leido: {json.Length} caracteres. El contenido original no se ha modificado."); + MostrarMensajeHistorial( + "El codigo ha producido un error inesperado al cargar el historial. Consulta el diagnostico; los datos originales no se han modificado.", + esError: true); + return false; } } private static IReadOnlyList DeserializarHistorialLocal( string json, - out int registrosOmitidos) + out int registrosOmitidos, + out string? primerErrorRegistro) { using var documento = JsonDocument.Parse(json); if (documento.RootElement.ValueKind != JsonValueKind.Array) throw new JsonException("El historial local no contiene una lista."); registrosOmitidos = 0; + primerErrorRegistro = null; var recuperados = new List(); + var idsVistos = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var elemento in documento.RootElement.EnumerateArray()) { try @@ -157,18 +277,27 @@ public partial class PlanificadorRutas if (item is null) { registrosOmitidos++; + primerErrorRegistro ??= "El registro deserializado era nulo."; continue; } - if (string.IsNullOrWhiteSpace(item.Id)) + if (string.IsNullOrWhiteSpace(item.Id) || !idsVistos.Add(item.Id)) + { item.Id = Guid.NewGuid().ToString("N"); + idsVistos.Add(item.Id); + } - item.LineasPermitidas ??= new List(); + NormalizarRegistroHistorialLocal(item); recuperados.Add(item); } - catch (Exception ex) when (ex is JsonException or NotSupportedException or InvalidOperationException) + catch (Exception ex) when ( + ex is JsonException or + NotSupportedException or + InvalidOperationException or + ArgumentException) { registrosOmitidos++; + primerErrorRegistro ??= $"{ex.GetType().Name}: {ex.Message}"; } } @@ -178,6 +307,33 @@ public partial class PlanificadorRutas .ToList(); } + private static void NormalizarRegistroHistorialLocal(RegistroHistorialRuta item) + { + if (!CoordenadaHistorialValida(item.OrigenLatitud, item.OrigenLongitud)) + throw new JsonException("Las coordenadas de origen no son validas."); + if (!CoordenadaHistorialValida(item.DestinoLatitud, item.DestinoLongitud)) + throw new JsonException("Las coordenadas de destino no son validas."); + + item.OrigenTexto = string.IsNullOrWhiteSpace(item.OrigenTexto) + ? $"{item.OrigenLatitud.ToString("0.000000", CultureInfo.InvariantCulture)}, {item.OrigenLongitud.ToString("0.000000", CultureInfo.InvariantCulture)}" + : item.OrigenTexto.Trim(); + item.DestinoTexto = string.IsNullOrWhiteSpace(item.DestinoTexto) + ? $"{item.DestinoLatitud.ToString("0.000000", CultureInfo.InvariantCulture)}, {item.DestinoLongitud.ToString("0.000000", CultureInfo.InvariantCulture)}" + : item.DestinoTexto.Trim(); + item.LineasPermitidas = NormalizarLineasHistorial(item.LineasPermitidas).ToList(); + + if (item.FechaCreacionUtc == default) + item.FechaCreacionUtc = item.FechaActualizacionUtc == default ? DateTime.UtcNow : item.FechaActualizacionUtc; + if (item.FechaActualizacionUtc == default) + item.FechaActualizacionUtc = item.FechaCreacionUtc; + } + + private static bool CoordenadaHistorialValida(double latitud, double longitud) + => double.IsFinite(latitud) && + double.IsFinite(longitud) && + latitud is >= -90 and <= 90 && + longitud is >= -180 and <= 180; + private void FinalizarCargaHistorialLocal(IEnumerable items) { _historialLocal.Clear(); @@ -187,17 +343,19 @@ public partial class PlanificadorRutas _historialLocalCargado = true; } - private async Task GuardarHistorialLocalRecuperadoAsync(string jsonOriginal) + private async Task GuardarHistorialLocalRecuperadoAsync(string jsonOriginal) { - try - { - await JS.InvokeVoidAsync("rt.saveLocalValue", ClaveRespaldoHistorialLocal, jsonOriginal); - var jsonRecuperado = JsonSerializer.Serialize(_historialLocal); - await JS.InvokeVoidAsync("rt.saveLocalValue", ClaveHistorialLocal, jsonRecuperado); - } - catch (JSException) - { - } + if (!await GuardarValorHistorialLocalAsync( + ClaveRespaldoHistorialLocal, + jsonOriginal, + "guardado de la copia de recuperacion")) + return false; + + var jsonRecuperado = JsonSerializer.Serialize(_historialLocal); + return await GuardarValorHistorialLocalAsync( + ClaveHistorialLocal, + jsonRecuperado, + "guardado del historial recuperado"); } private async Task PersistirHistorialLocalAsync() @@ -205,14 +363,143 @@ public partial class PlanificadorRutas if (!_historialLocalCargado) return; + var json = JsonSerializer.Serialize(_historialLocal); + if (!await GuardarValorHistorialLocalAsync( + ClaveHistorialLocal, + json, + "persistencia del historial")) + { + MostrarMensajeHistorial( + "No se ha podido guardar el historial personal. Consulta el diagnostico.", + esError: true); + } + } + + private async Task GuardarValorHistorialLocalAsync(string clave, string valor, string fase) + { try { - var json = JsonSerializer.Serialize(_historialLocal); - await JS.InvokeVoidAsync("rt.saveLocalValue", ClaveHistorialLocal, json); + using var cts = new CancellationTokenSource( + TimeSpan.FromSeconds(TiempoMaxOperacionHistorialLocalSegundos)); + var resultado = await JS.InvokeAsync( + "rt.saveLocalValueResult", + cts.Token, + clave, + valor); + + if (resultado?.Ok == true) + return true; + + RegistrarErrorHistorialLocal( + fase, + resultado?.ErrorName ?? "RespuestaVacia", + resultado?.ErrorMessage ?? "JavaScript no devolvio un resultado valido al guardar.", + $"Tamano que se intentaba guardar: {valor.Length} caracteres."); + return false; } - catch (JSException) + catch (OperationCanceledException ex) { + RegistrarErrorHistorialLocal( + fase, + new TimeoutException( + $"La escritura no respondio en {TiempoMaxOperacionHistorialLocalSegundos} segundos.", + ex), + $"Tamano que se intentaba guardar: {valor.Length} caracteres."); + return false; } + catch (Exception ex) + { + RegistrarErrorHistorialLocal( + fase, + ex, + $"Tamano que se intentaba guardar: {valor.Length} caracteres."); + return false; + } + } + + private async Task ReintentarCargaHistorialLocalAsync() + { + if (_historialLocalCargando) + return; + + _historialLocalCargado = false; + _tareaCargaHistorialLocal = null; + _historialLocalCargando = true; + StateHasChanged(); + + try + { + var cargado = await AsegurarHistorialLocalCargadoAsync(); + MostrarMensajeHistorial( + cargado + ? $"Historial personal cargado: {_historialLocal.Count} registro(s)." + : "El historial personal sigue sin poder cargarse. Consulta el diagnostico.", + esError: !cargado); + } + finally + { + _historialLocalCargando = false; + StateHasChanged(); + } + } + + private void RegistrarErrorHistorialLocal( + string fase, + Exception excepcion, + string? contexto = null) + { + var causa = excepcion.GetBaseException(); + RegistrarErrorHistorialLocal( + fase, + causa.GetType().Name, + causa.Message, + CombinarDetalle(contexto, excepcion.ToString())); + } + + private void RegistrarErrorHistorialLocal( + string fase, + string tipo, + string mensaje, + string? contexto = null) + { + var marcaTiempo = DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss", CultureInfo.CurrentCulture); + _resumenErrorHistorialLocal = $"{tipo}: {LimitarTextoDiagnostico(mensaje, 240)}"; + _detalleErrorHistorialLocal = string.Join( + Environment.NewLine, + new[] + { + $"Fecha: {marcaTiempo}", + $"Fase: {fase}", + $"Tipo: {tipo}", + $"Mensaje: {mensaje}", + $"Estado: cargado={_historialLocalCargado}, cargando={_historialLocalCargando}, registrosEnMemoria={_historialLocal.Count}", + string.IsNullOrWhiteSpace(contexto) ? null : $"Contexto: {contexto}" + }.Where(linea => linea is not null)); + + Console.Error.WriteLine($"[Historial personal] {_detalleErrorHistorialLocal}"); + } + + private void LimpiarErrorHistorialLocal() + { + _resumenErrorHistorialLocal = string.Empty; + _detalleErrorHistorialLocal = string.Empty; + } + + private static string? CombinarDetalle(string? contexto, string? detalle) + { + if (string.IsNullOrWhiteSpace(contexto)) + return detalle; + if (string.IsNullOrWhiteSpace(detalle)) + return contexto; + return $"{contexto}{Environment.NewLine}{detalle}"; + } + + private static string LimitarTextoDiagnostico(string texto, int maximo) + { + if (string.IsNullOrWhiteSpace(texto) || texto.Length <= maximo) + return texto; + + return texto[..maximo] + "..."; } private async Task CargarHistorialCompartidoAsync(bool mostrarError = true) @@ -339,16 +626,29 @@ public partial class PlanificadorRutas private async Task CargarHistorialLocalAsync(string id) { - var item = _historialLocal.FirstOrDefault(x => x.Id == id); - if (item is null) + try { - MostrarMensajeHistorial("No encuentro esa combinacion en tu historial personal.", esError: true); - return; - } + var item = _historialLocal.FirstOrDefault(x => x.Id == id); + if (item is null) + { + MostrarMensajeHistorial("No encuentro esa combinacion en tu historial personal.", esError: true); + return; + } - await AplicarRegistroHistorialAsync(item); - SeleccionarHistorialLocal(id); - MostrarMensajeHistorial("Combinacion personal cargada."); + await AplicarRegistroHistorialAsync(item); + SeleccionarHistorialLocal(id); + MostrarMensajeHistorial("Combinacion personal cargada."); + } + catch (Exception ex) + { + RegistrarErrorHistorialLocal( + "aplicacion de un registro personal", + ex, + $"Id del registro: {id}."); + MostrarMensajeHistorial( + "La combinacion personal ha producido un error. Consulta el diagnostico.", + esError: true); + } } private async Task CargarHistorialCompartidoAsync(string id) @@ -429,33 +729,46 @@ public partial class PlanificadorRutas private async Task EliminarHistorialLocalAsync(string id) { - if (!await AsegurarHistorialLocalCargadoAsync()) - return; - - var indiceEliminado = _historialLocal.FindIndex(x => x.Id == id); - if (indiceEliminado < 0) - return; - - var estabaSeleccionado = _historialLocalSeleccionadoId == id; - _historialLocal.RemoveAt(indiceEliminado); - - await PersistirHistorialLocalAsync(); - - if (estabaSeleccionado) + try { - if (_historialLocal.Count == 0) - { - _historialLocalSeleccionadoId = string.Empty; - } - else - { - var siguiente = _historialLocal[Math.Min(indiceEliminado, _historialLocal.Count - 1)]; - await AplicarRegistroHistorialAsync(siguiente); - SeleccionarHistorialLocal(siguiente.Id); - } - } + if (!await AsegurarHistorialLocalCargadoAsync()) + return; - MostrarMensajeHistorial("Combinacion eliminada del historial personal."); + var indiceEliminado = _historialLocal.FindIndex(x => x.Id == id); + if (indiceEliminado < 0) + return; + + var estabaSeleccionado = _historialLocalSeleccionadoId == id; + _historialLocal.RemoveAt(indiceEliminado); + + await PersistirHistorialLocalAsync(); + + if (estabaSeleccionado) + { + if (_historialLocal.Count == 0) + { + _historialLocalSeleccionadoId = string.Empty; + } + else + { + var siguiente = _historialLocal[Math.Min(indiceEliminado, _historialLocal.Count - 1)]; + await AplicarRegistroHistorialAsync(siguiente); + SeleccionarHistorialLocal(siguiente.Id); + } + } + + MostrarMensajeHistorial("Combinacion eliminada del historial personal."); + } + catch (Exception ex) + { + RegistrarErrorHistorialLocal( + "eliminacion de un registro personal", + ex, + $"Id del registro: {id}."); + MostrarMensajeHistorial( + "No se ha podido completar la eliminacion. Consulta el diagnostico.", + esError: true); + } } private void PedirConfirmacionEliminarHistorialCompartido(string id) @@ -638,7 +951,7 @@ public partial class PlanificadorRutas PreferenciaEtiquetaRuta = _preferenciaEtiquetaRuta, FactorEquilibradoAndarBus = double.IsFinite(_factorEquilibradoAndarBus) ? Math.Max(0, _factorEquilibradoAndarBus) - : 12.0, + : FactorEquilibradoAndarBusPorDefecto, ActivarFiltroLineas = _activarRangoLineas, LineasPermitidas = ObtenerLineasPermitidasSeleccionadas().ToList(), FechaCreacionUtc = DateTime.UtcNow, diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs index c4adad0..3bf88e9 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs @@ -1204,7 +1204,7 @@ public partial class PlanificadorRutas : ComponentBase private bool _elevPopupMinimizado = false; - private const string _versionApp = "(v-20260728a)"; + private const string _versionApp = "(v-20260731a)"; private double? _paradaElevG; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs index 13f79b4..4a64d48 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs @@ -7,6 +7,7 @@ namespace RutasDBUS.Components.Pages; public partial class PlanificadorRutas { private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1"; + private const int VersionPersistenciaFactorEquilibrado = 2; private bool _configuracionLocalCargada = false; private bool _mostrarConfirmacionRestablecer = false; @@ -36,8 +37,12 @@ public partial class PlanificadorRutas public double? CoeficienteEsfuerzoAltura { get; set; } public double? PesoTransbordoRankingMinutos { get; set; } public double? FactorEquilibradoAndarBus { get; set; } + public int? VersionFactorEquilibrado { get; set; } + public bool? MostrarElevacionesRuta { get; set; } public double? UmbralPendienteSubidaRuta { get; set; } public double? UmbralPendienteBajadaRuta { get; set; } + public double? UmbralAlturaAlertaPendienteRutaMetros { get; set; } + // Compatibilidad con configuraciones guardadas cuando el umbral se expresaba en pisos. public double? UmbralPisosPendienteRuta { get; set; } public double VelocidadPieKmH { get; set; } public double VelocidadBusKmH { get; set; } @@ -139,9 +144,11 @@ public partial class PlanificadorRutas CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura, PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos, FactorEquilibradoAndarBus = _factorEquilibradoAndarBus, + VersionFactorEquilibrado = VersionPersistenciaFactorEquilibrado, + MostrarElevacionesRuta = _mostrarElevacionesRuta, UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta, UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta, - UmbralPisosPendienteRuta = _umbralPisosPendienteRuta, + UmbralAlturaAlertaPendienteRutaMetros = _umbralAlturaAlertaPendienteRutaMetros, VelocidadPieKmH = _velocidadPieKmH, VelocidadBusKmH = _velocidadBusKmH, ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo, @@ -206,7 +213,11 @@ public partial class PlanificadorRutas if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo)) _pesoTransbordoRankingMinutos = Math.Abs(pesoTransbordo - 8.0) < 0.0001 ? 4.0 : Math.Max(0, pesoTransbordo); if (config.FactorEquilibradoAndarBus is { } factorEquilibrado && double.IsFinite(factorEquilibrado)) - _factorEquilibradoAndarBus = Math.Max(0, factorEquilibrado); + _factorEquilibradoAndarBus = (config.VersionFactorEquilibrado ?? 0) < VersionPersistenciaFactorEquilibrado && + Math.Abs(factorEquilibrado - 12.0) < 0.0001 + ? FactorEquilibradoAndarBusPorDefecto + : Math.Max(0, factorEquilibrado); + _mostrarElevacionesRuta = config.MostrarElevacionesRuta ?? false; if (config.UmbralPendienteSubidaRuta is { } umbralSubida && double.IsFinite(umbralSubida)) _umbralPendienteSubidaRuta = Math.Abs(umbralSubida - 3.0) < 0.0001 ? UmbralPendienteSubidaRutaPorDefecto @@ -217,8 +228,16 @@ public partial class PlanificadorRutas Math.Abs(umbralBajada - 6.0) < 0.0001 ? UmbralPendienteBajadaRutaPorDefecto : NormalizarUmbralPendienteRuta(umbralBajada); - if (config.UmbralPisosPendienteRuta is { } umbralPisos && double.IsFinite(umbralPisos)) - _umbralPisosPendienteRuta = NormalizarUmbralPisosPendienteRuta(umbralPisos); + if (config.UmbralAlturaAlertaPendienteRutaMetros is { } umbralAltura && double.IsFinite(umbralAltura)) + { + _umbralAlturaAlertaPendienteRutaMetros = + NormalizarUmbralAlturaAlertaPendienteRutaMetros(umbralAltura); + } + else if (config.UmbralPisosPendienteRuta is { } umbralPisos && double.IsFinite(umbralPisos)) + { + _umbralAlturaAlertaPendienteRutaMetros = + NormalizarUmbralAlturaAlertaPendienteRutaMetros(umbralPisos * MetrosPorPiso); + } _velocidadPieKmH = Math.Clamp(config.VelocidadPieKmH, 0.5, 25.0); _velocidadBusKmH = Math.Clamp( Math.Abs(config.VelocidadBusKmH - 15.0) < 0.0001 || @@ -321,10 +340,11 @@ public partial class PlanificadorRutas _parametroPenalizacionDistanciaPieMetros = ParametroPenalizacionDistanciaPieMetrosPorDefecto; _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; _pesoTransbordoRankingMinutos = 4.0; - _factorEquilibradoAndarBus = 12.0; + _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto; + _mostrarElevacionesRuta = false; _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto; _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto; - _umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto; + _umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto; _velocidadPieKmH = 3.6; _velocidadBusKmH = 16.0; _minutosTransbordo = 5; @@ -371,10 +391,11 @@ public partial class PlanificadorRutas _parametroPenalizacionDistanciaPieMetros = ParametroPenalizacionDistanciaPieMetrosPorDefecto; _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; _pesoTransbordoRankingMinutos = 4.0; - _factorEquilibradoAndarBus = 12.0; + _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto; + _mostrarElevacionesRuta = false; _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto; _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto; - _umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto; + _umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto; _velocidadPieKmH = 3.6; _velocidadBusKmH = 16.0; _minutosTransbordo = 5; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs index 764fc2b..603cb13 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs @@ -384,6 +384,12 @@ public partial class PlanificadorRutas : ComponentBase IReadOnlyList alternativas, CancellationToken ct) { + foreach (var alternativa in alternativas) + alternativa.EsfuerzoExtraPendienteMetros = 0; + + if (!_mostrarElevacionesRuta || alternativas.Count == 0) + return; + var lineasPorAlternativa = alternativas .Select(alternativa => new { diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs index a5949df..3d5d51a 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs @@ -81,16 +81,40 @@ public partial class PlanificadorRutas : ComponentBase private async Task ToggleElevacionesRutaAsync(ChangeEventArgs e) { - _mostrarElevacionesRuta = e.Value switch + var mostrarElevaciones = e.Value switch { bool valor => valor, string texto when bool.TryParse(texto, out var valor) => valor, _ => !_mostrarElevacionesRuta }; - await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta + if (_mostrarElevacionesRuta == mostrarElevaciones) + return; + + _mostrarElevacionesRuta = mostrarElevaciones; + await PersistirConfiguracionAsync(); + if (_opcionesRuta.Count == 0) + { + StateHasChanged(); + return; + } + + _pintandoElevacionesRuta = true; + _mensajeRuta = _mostrarElevacionesRuta ? "Aplicando pendientes..." - : "Representando ruta seleccionada..."); + : "Recalculando sin pendientes..."; + StateHasChanged(); + await Task.Yield(); + + try + { + await RecalcularRankingTrasCambioEsfuerzoPendienteAsync(forzarRepintado: true); + } + finally + { + _pintandoElevacionesRuta = false; + StateHasChanged(); + } } private async Task CambiarUmbralElevacionRutaAsync(ChangeEventArgs e, bool esSubida) @@ -107,7 +131,7 @@ public partial class PlanificadorRutas : ComponentBase await PersistirConfiguracionAsync(); - if (esSubida) + if (esSubida && _mostrarElevacionesRuta) { await RecalcularRankingTrasCambioEsfuerzoPendienteAsync(); } @@ -117,7 +141,7 @@ public partial class PlanificadorRutas : ComponentBase StateHasChanged(); } - private async Task RecalcularRankingTrasCambioEsfuerzoPendienteAsync() + private async Task RecalcularRankingTrasCambioEsfuerzoPendienteAsync(bool forzarRepintado = false) { if (_opcionesRuta.Count == 0) { @@ -143,7 +167,7 @@ public partial class PlanificadorRutas : ComponentBase !ReferenceEquals(opcionSeleccionadaAnterior, _opcionesRuta[nuevoIndice]); _indiceAlternativaSeleccionada = nuevoIndice; - if (_mostrarElevacionesRuta || cambioSeleccion) + if (forzarRepintado || _mostrarElevacionesRuta || cambioSeleccion) { await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta ? "Aplicando pendientes..." @@ -155,11 +179,11 @@ public partial class PlanificadorRutas : ComponentBase } } - private async Task CambiarUmbralPisosPendienteRutaAsync(ChangeEventArgs e) + private async Task CambiarUmbralAlturaAlertaPendienteRutaAsync(ChangeEventArgs e) { - _umbralPisosPendienteRuta = ParseUmbralPisosPendienteRuta( + _umbralAlturaAlertaPendienteRutaMetros = ParseUmbralAlturaAlertaPendienteRutaMetros( e.Value, - _umbralPisosPendienteRuta); + _umbralAlturaAlertaPendienteRutaMetros); await PersistirConfiguracionAsync(); StateHasChanged(); @@ -210,22 +234,22 @@ public partial class PlanificadorRutas : ComponentBase ? Math.Clamp(Math.Abs(valor), 0.0, 100.0) : UmbralPendienteSubidaRutaPorDefecto; - private static double ParseUmbralPisosPendienteRuta(object? valor, double actual) + private static double ParseUmbralAlturaAlertaPendienteRutaMetros(object? valor, double actual) { var texto = Convert.ToString(valor, CultureInfo.InvariantCulture)?.Replace(',', '.'); if (!double.TryParse(texto, NumberStyles.Float, CultureInfo.InvariantCulture, out var nuevo) || !double.IsFinite(nuevo)) { - return NormalizarUmbralPisosPendienteRuta(actual); + return NormalizarUmbralAlturaAlertaPendienteRutaMetros(actual); } - return NormalizarUmbralPisosPendienteRuta(nuevo); + return NormalizarUmbralAlturaAlertaPendienteRutaMetros(nuevo); } - private static double NormalizarUmbralPisosPendienteRuta(double valor) + private static double NormalizarUmbralAlturaAlertaPendienteRutaMetros(double valor) => double.IsFinite(valor) - ? Math.Clamp(Math.Abs(valor), 0.0, 100.0) - : UmbralPisosPendienteRutaPorDefecto; + ? Math.Clamp(Math.Abs(valor), 0.0, 1000.0) + : UmbralAlturaAlertaPendienteRutaMetrosPorDefecto; private async Task DrawWalkingRouteJsAsync( string id, diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.razor b/RutasDBUS/Components/Pages/PlanificadorRutas.razor index 7816e4a..d9c0353 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.razor +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.razor @@ -613,88 +613,6 @@ -
- - -
- - - @if (_mostrarConfiguracionPendientesRuta) - { - - } -
-
- @if (_opcionesRuta.Count > 0) {
@@ -763,16 +681,13 @@ { var alt = _opcionesRuta[_indiceAlternativaSeleccionada]; - @if (MostrarAvisoPendientesPositivasSignificativas) + @if (MostrarAlertaPendientesPositivasSignificativas) {
Pendientes positivas significativas.
- Distancia con subidas: @FormatearDistanciaResumenElevacion(_distanciaPendientePositivaSignificativaRutaMetros)
-
- - Subidas acumuladas: @_alturaPendientePositivaSignificativaRutaMetros.ToString("0.00", CultureInfo.InvariantCulture) m - (@((_alturaPendientePositivaSignificativaRutaMetros / MetrosPorPiso).ToString("0.0", CultureInfo.InvariantCulture)) pisos) -
+
- Subidas acumuladas: @_alturaPendientePositivaSignificativaRutaMetros.ToString("0.00", CultureInfo.InvariantCulture) m
} @@ -975,7 +890,7 @@ } @if (!string.IsNullOrWhiteSpace(scoreEquilibradoSeleccionado)) { -
Score equilibrado: @scoreEquilibradoSeleccionado
+
Score equilibrado A: @scoreEquilibradoSeleccionado
} @if (!string.IsNullOrWhiteSpace(xyzOrigen)) { @@ -1113,6 +1028,27 @@ }
} + + @if (!string.IsNullOrWhiteSpace(_resumenErrorHistorialLocal)) + { + + }
@@ -1262,10 +1198,10 @@
- +
-
- -
- - +
+ +
+ +
@@ -1464,7 +1404,7 @@ Peso rank. @@ -1478,59 +1418,90 @@
- -
+ +
+
- -
- - + +
+ +
- -
- - + +
+ +
-
- -
- -
-
+
+ +
+ + + +
+
+@if (_mostrarConfiguracionPendientesRuta) +{ +
+
+
+
Configurar Pendientes
+ +
+ +
+ + + + + +
+
+
+} + @if (_mostrarModelosHorariosPanel) {
diff --git a/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs b/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs index 9c77554..34c3d0f 100644 --- a/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs +++ b/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs @@ -26,7 +26,7 @@ public sealed class ParametrosPlanificadorRuta public double FactorPenalizacionDistanciaPie { get; set; } = 1.35; public double ParametroPenalizacionDistanciaPieMetros { get; set; } = 200.0; public double PesoTransbordoRankingMinutos { get; set; } = 4.0; - public double FactorEquilibradoAndarBus { get; set; } = 12.0; + public double FactorEquilibradoAndarBus { get; set; } = 6.0; // selección candidatas public int RadioParadasCandidatasMetros { get; set; } = 1200; diff --git a/RutasDBUS/Servicios/Planificacion/BuscadorRutasTemporalesDbus.cs b/RutasDBUS/Servicios/Planificacion/BuscadorRutasTemporalesDbus.cs index 568de3d..bca2aeb 100644 --- a/RutasDBUS/Servicios/Planificacion/BuscadorRutasTemporalesDbus.cs +++ b/RutasDBUS/Servicios/Planificacion/BuscadorRutasTemporalesDbus.cs @@ -828,9 +828,12 @@ public sealed class BuscadorRutasTemporalesDbus parametros); var factor = double.IsFinite(parametros.FactorEquilibradoAndarBus) ? Math.Max(0, parametros.FactorEquilibradoAndarBus) - : 12.0; + : 6.0; + var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros( + UtilidadesTransitoDbus.ContarTransbordosReales(resultado.RutaBus.Tramos), + parametros); - var score = distanciaBus + (distanciaAndando * factor); + var score = distanciaBus + (distanciaAndando * factor) + penalizacionTransbordos; return double.IsFinite(score) && score >= 0 ? score : double.PositiveInfinity; diff --git a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs index 2544c46..c36abda 100644 --- a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs +++ b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs @@ -1449,6 +1449,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas var limiteCoste = Math.Max(1, maxPorCoste); var limiteAndar = Math.Max(8, limiteCoste / 2); var limiteEquilibrado = Math.Max(8, limiteCoste / 2); + var limiteDirectas = Math.Max(8, limiteCoste / 4); var duracionMinima = lista .Select(x => x.DuracionEstimadaSegundos) .Where(double.IsFinite) @@ -1479,6 +1480,15 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas .ThenBy(x => x.PuntuacionCoste) .Take(limiteEquilibrado)); + // Las directas no deben depender de que una reconstruccion circular las vuelva a inyectar. + Agregar(lista + .Where(x => x.RutaBus.Tramos.Count == 1) + .OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros)) + .ThenBy(x => x.PuntuacionCoste) + .ThenBy(x => x.DuracionEstimadaSegundos) + .ThenBy(x => x.DistanciaPieTotal) + .Take(limiteDirectas)); + Agregar(lista .Where(x => x.DuracionEstimadaSegundos <= limiteDuracionAndarRazonable) .OrderBy(x => x.DistanciaPieTotal) @@ -1527,9 +1537,14 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas parametros); var factor = parametros is not null && double.IsFinite(parametros.FactorEquilibradoAndarBus) ? Math.Max(0, parametros.FactorEquilibradoAndarBus) - : 12.0; + : 6.0; + var penalizacionTransbordos = parametros is null + ? 0 + : UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros( + UtilidadesTransitoDbus.ContarTransbordosReales(resultado.RutaBus.Tramos), + parametros); - var score = distanciaBus + (distanciaAndando * factor); + var score = distanciaBus + (distanciaAndando * factor) + penalizacionTransbordos; return double.IsFinite(score) && score >= 0 ? score : double.PositiveInfinity; @@ -3199,9 +3214,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas var distanciaAndando = CalcularDistanciaAndandoCorregidaMetros(alternativa, parametros); var factor = double.IsFinite(parametros.FactorEquilibradoAndarBus) ? Math.Max(0, parametros.FactorEquilibradoAndarBus) - : 12.0; + : 6.0; + var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros( + alternativa.NumeroTransbordosReales, + parametros); - var score = distanciaBus + (distanciaAndando * factor); + var score = distanciaBus + (distanciaAndando * factor) + penalizacionTransbordos; return double.IsFinite(score) && score >= 0 ? score : double.PositiveInfinity; @@ -3458,7 +3476,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas } /// - /// Firma orientada a UI: agrupa alternativas que usan la misma familia de lineas aunque cambie la parada exacta. + /// Firma orientada a UI: agrupa alternativas con la misma secuencia de lineas y sentidos + /// aunque cambie la parada exacta. /// private static string CrearFirmaFamiliaRutaBus(RutaBusAlternativa alternativa) { @@ -3466,7 +3485,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas foreach (var tramo in alternativa.Tramos) { var linea = UtilidadesTransitoDbus.LineaCodigoStr(tramo.Variante); - var parte = $"{linea}.{tramo.Variante.CodigoItinerario}"; + var sentido = UtilidadesTransitoDbus.NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo); + var parte = $"{linea}.{tramo.Variante.CodigoItinerario}-{sentido}"; if (partes.Count == 0 || !string.Equals(partes[^1], parte, StringComparison.OrdinalIgnoreCase)) partes.Add(parte); } diff --git a/RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs b/RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs index f0065fe..ece9e08 100644 --- a/RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs +++ b/RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs @@ -384,6 +384,28 @@ internal static class UtilidadesTransitoDbus return numeroTransbordosReales * pesoMinutos * 60.0; } + /// + /// Convierte la penalizacion temporal de los transbordos en distancia equivalente de bus. + /// + public static double CalcularPenalizacionTransbordosRankingMetros( + int numeroTransbordosReales, + ParametrosPlanificadorRuta parametros) + { + var penalizacionSegundos = CalcularPenalizacionTransbordosRankingSegundos( + numeroTransbordosReales, + parametros); + if (penalizacionSegundos <= 0) + return 0; + + var velocidadBusKmH = double.IsFinite(parametros.VelocidadBusKmH) && parametros.VelocidadBusKmH > 0 + ? parametros.VelocidadBusKmH + : 16.0; + var penalizacionMetros = penalizacionSegundos * (velocidadBusKmH * 1000.0 / 3600.0); + return double.IsFinite(penalizacionMetros) && penalizacionMetros > 0 + ? penalizacionMetros + : 0; + } + /// /// Calcula el coste interno aplicando preferencias de caminata y transbordos reales. /// diff --git a/RutasDBUS/wwwroot/app.css b/RutasDBUS/wwwroot/app.css index a78c5e7..cdb5b1b 100644 --- a/RutasDBUS/wwwroot/app.css +++ b/RutasDBUS/wwwroot/app.css @@ -872,24 +872,6 @@ html, body { background: #2563eb; } -.route-visual-options { - position: relative; - display: inline-flex; - align-self: flex-start; - align-items: center; - justify-content: flex-start; - flex-wrap: nowrap; - width: fit-content; - max-width: 100%; - box-sizing: border-box; - gap: .25rem; - margin: -.15rem 0 .4rem; - padding: 2px 3px 2px 5px; - border: 1px solid rgba(148, 163, 184, .28); - border-radius: 7px; - background: rgba(2, 6, 23, .3); -} - .route-elevation-switch { display: inline-flex; align-items: center; @@ -952,130 +934,6 @@ html, body { opacity: .55; } -.route-elevation-settings { - display: inline-flex; - align-items: center; - margin-left: .05rem; - padding-left: .3rem; - border-left: 1px solid rgba(148, 163, 184, .35); - pointer-events: auto; -} - -.route-elevation-settings-trigger { - display: grid; - place-items: center; - width: 26px; - height: 24px; - padding: 0; - border: 1px solid rgba(148, 163, 184, .55); - border-radius: 6px; - background: rgba(2, 6, 23, .78); - color: #cbd5e1; - cursor: pointer; - font-family: "Segoe UI Symbol", sans-serif; - font-size: 1rem; - line-height: 1; -} - -.route-elevation-settings-trigger:hover, -.route-elevation-settings-trigger--active { - border-color: rgba(56, 189, 248, .9); - background: rgba(30, 64, 175, .55); - color: #f8fafc; -} - -.route-elevation-settings-trigger:disabled { - cursor: default; - opacity: .55; -} - -.route-elevation-settings-popover { - position: absolute; - z-index: 20; - top: calc(100% + 6px); - right: auto; - left: 0; - width: 238px; - max-width: calc(100vw - 24px); - box-sizing: border-box; - padding: .55rem; - border: 1px solid rgba(148, 163, 184, .55); - border-radius: 7px; - background: rgba(15, 23, 42, .99); - box-shadow: 0 14px 32px rgba(0, 0, 0, .5); - color: #e5e7eb; -} - -.route-elevation-settings-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: .5rem; - margin-bottom: .45rem; - font-size: .72rem; - font-weight: 800; -} - -.route-elevation-settings-close { - display: grid; - place-items: center; - width: 22px; - height: 22px; - padding: 0; - border: 0; - border-radius: 4px; - background: transparent; - color: #cbd5e1; - cursor: pointer; - font-size: 1rem; - line-height: 1; -} - -.route-elevation-settings-close:hover { - background: rgba(71, 85, 105, .55); - color: #fff; -} - -.route-elevation-settings-fields { - display: grid; - gap: .35rem; -} - -.route-elevation-setting-row { - display: grid; - grid-template-columns: minmax(0, 1fr) 44px 32px; - align-items: center; - gap: .35rem; - min-height: 26px; - color: #cbd5e1; - font-size: .68rem; - font-weight: 700; -} - -.route-elevation-setting-row input { - width: 44px; - height: 22px; - box-sizing: border-box; - border: 1px solid rgba(148, 163, 184, .55); - border-radius: 6px; - background: rgba(2, 6, 23, .78); - color: #f8fafc; - font-size: .68rem; - font-weight: 800; - text-align: center; - padding: 0 .18rem; - outline: none; -} - -.route-elevation-setting-row input:focus { - border-color: rgba(56, 189, 248, .95); - box-shadow: 0 0 0 2px rgba(56, 189, 248, .18); -} - -.route-elevation-setting-row input:disabled { - opacity: .55; -} - .route-slope-warning { margin: 0 0 .45rem; padding: .36rem .5rem; @@ -1414,6 +1272,70 @@ html, body { color: #fca5a5; } +.history-local-diagnostic { + margin-top: .35rem; + padding: .5rem .58rem; + border-left: 3px solid #ef4444; + background: rgba(127, 29, 29, .18); + color: #fecaca; + font-size: .72rem; + line-height: 1.35; +} + +.history-local-diagnostic__title { + color: #fee2e2; + font-weight: 700; +} + +.history-local-diagnostic__summary { + margin-top: .14rem; + overflow-wrap: anywhere; +} + +.history-local-diagnostic__details { + margin-top: .3rem; + color: #fca5a5; +} + +.history-local-diagnostic__details summary { + width: fit-content; + cursor: pointer; + color: #fda4af; + font-weight: 600; +} + +.history-local-diagnostic__details pre { + max-height: 150px; + margin: .35rem 0 0; + padding: .38rem .42rem; + overflow: auto; + border: 1px solid rgba(248, 113, 113, .25); + background: rgba(15, 23, 42, .72); + color: #fecaca; + font-family: Consolas, monospace; + font-size: .66rem; + line-height: 1.35; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.history-local-diagnostic__retry { + margin-top: .42rem; + padding: .3rem .55rem; + border: 1px solid rgba(248, 113, 113, .6); + border-radius: .35rem; + background: rgba(127, 29, 29, .32); + color: #fee2e2; + font-size: .7rem; + font-weight: 600; + cursor: pointer; +} + +.history-local-diagnostic__retry:hover { + border-color: #fca5a5; + background: rgba(153, 27, 27, .48); +} + .config-panel { position: absolute; bottom: 2.4rem; @@ -1755,12 +1677,36 @@ html, body { border: none; } -.rt-stop-direction-icon { +.rt-highlighted-stop-icon { border: 0; background: transparent; + pointer-events: auto; +} + +.rt-highlighted-stop-marker { + position: relative; + display: block; + width: 100%; + height: 100%; +} + +.rt-highlighted-stop-marker__disc, +.rt-highlighted-stop-marker__ring { + position: absolute; + border-radius: 50%; pointer-events: none; } +.rt-highlighted-stop-marker__ring { + inset: 0; + box-sizing: border-box; + border-style: solid; +} + +.rt-highlighted-stop-marker__disc { + z-index: 1; +} + .rt-stop-direction-arrow { position: absolute; top: 50%; @@ -1770,6 +1716,8 @@ html, body { transform: translate(-50%, -50%) rotate(var(--stop-bearing, 0deg)); transform-origin: 50% 50%; filter: drop-shadow(0 0 1px #0f172a) drop-shadow(0 1px 0 #0f172a); + z-index: 2; + pointer-events: none; } .rt-stop-direction-arrow::before { @@ -1863,53 +1811,6 @@ html, body { -moz-appearance: textfield; } -.num-inline { - display: grid; - grid-template-columns: 1fr 1.6rem 1.6rem; /* input | ? | ? */ - gap: .25rem; - align-items: center; - min-width: 0; -} -.num-inline .config-input { - width: 100% !important; /* pisa width:90px anterior */ - min-width: 0; - box-sizing: border-box; - } - -.config-dual-inline { - display: grid; - grid-template-columns: 1fr 1fr; - gap: .5rem; - align-items: center; - min-width: 0; -} - - .config-dual-inline .config-input { - width: 100% !important; - min-width: 0; - box-sizing: border-box; - text-align: center; - } - -.config-dual-inline--bare { - grid-template-columns: repeat(2, minmax(0, 4.2rem)); - justify-content: end; - gap: .35rem; -} - - .config-dual-inline--bare .config-input { - max-width: none; - text-align: center; - padding-left: .35rem; - padding-right: .35rem; - } - - .config-dual-inline--bare .config-input::placeholder { - color: #94a3b8; - opacity: 1; - font-size: .72rem; - } - .config-dual-labeled { display: grid; grid-template-columns: repeat(2, minmax(0, 4.2rem)); @@ -1941,19 +1842,113 @@ html, body { text-align: center; } -.config-dual-labeled--equilibrado { - grid-template-columns: minmax(0, 4rem); - align-items: end; - justify-content: start; -} - - - /* ✅ Solo esta fila: label arriba + control abajo, ancho completo */ .config-row.config-row-full { grid-template-columns: 1fr !important; } +.config-pendientes-inline { + display: flex; + align-items: center; + justify-content: flex-end; + gap: .28rem; + width: 100%; + min-width: 0; +} + +.config-pendientes-inline .route-elevation-switch { + min-height: 30px; + font-size: .68rem; + font-weight: 600; +} + +.config-pendientes-inline .route-elevation-switch--active .route-elevation-switch-track { + border-color: #3b82f6; + background: #2563eb; +} + +.config-pendientes-settings-button { + display: grid; + place-items: center; + width: 28px; + height: 28px; + flex: 0 0 28px; + padding: 0; + border: 0; + border-radius: 6px; + background: transparent; + color: #94a3b8; + cursor: pointer; + font-family: "Segoe UI Symbol", sans-serif; + font-size: .92rem; + line-height: 1; +} + +.config-pendientes-settings-button:hover, +.config-pendientes-settings-button:focus-visible { + background: rgba(51, 65, 85, .72); + color: #f8fafc; + outline: none; +} + +.config-pendientes-settings-button:disabled { + cursor: default; + opacity: .5; +} + +.pendientes-config-modal-backdrop { + z-index: 5200; +} + +.pendientes-config-modal { + width: min(286px, calc(100vw - 24px)); + padding: .75rem .8rem .8rem; + border-radius: 8px; +} + +.pendientes-config-modal__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: .5rem; + min-height: 30px; + margin-bottom: .65rem; +} + +.pendientes-config-modal__header .custom-modal-title { + margin: 0; + font-size: .86rem; +} + +.pendientes-config-modal__close { + width: 30px; + height: 30px; + flex: 0 0 30px; +} + +.pendientes-config-modal__fields { + display: grid; + gap: .45rem; + min-width: 0; +} + +.pendientes-config-modal__field { + display: grid; + grid-template-columns: minmax(0, 1fr) 86px; + align-items: center; + gap: .65rem; + min-width: 0; + color: #cbd5e1; + font-size: .76rem; +} + +.pendientes-config-modal__field .config-input { + width: 100%; + min-width: 0; + box-sizing: border-box; + text-align: center; +} + .config-row-map-opacity { margin-top: .7rem !important; margin-bottom: 0; @@ -3171,37 +3166,11 @@ html, body { font-size: .73rem; } - .route-visual-options { - justify-content: flex-start; - flex-wrap: nowrap; - gap: 4px; - margin: 0 0 8px; - padding: 2px 3px 2px 6px; - } - .route-elevation-switch { min-height: 44px; font-size: .72rem; } - .route-elevation-settings-trigger { - width: 40px; - height: 40px; - } - - .route-elevation-settings-popover { - right: auto; - left: 0; - width: 250px; - max-width: calc(100vw - 20px); - } - - .route-elevation-setting-row { - grid-template-columns: minmax(0, 1fr) 48px 36px; - min-height: 40px; - font-size: .72rem; - } - .route-elevation-setting-row input { width: 48px; height: 36px; @@ -3283,7 +3252,6 @@ html, body { } .route-panel-overlay--mobile-collapsed .route-priority-switch, - .route-panel-overlay--mobile-collapsed .route-visual-options, .route-panel-overlay--mobile-collapsed .route-alt-list, .route-panel-overlay--mobile-collapsed .route-panel-divider, .route-panel-overlay--mobile-collapsed .route-panel-body { @@ -3400,9 +3368,23 @@ html, body { font-size: 16px; } + .config-pendientes-inline .route-elevation-switch, + .config-pendientes-settings-button { + min-height: 44px; + } + + .config-pendientes-settings-button { + width: 44px; + height: 44px; + flex-basis: 44px; + } + + .pendientes-config-modal__field .config-input { + height: 42px; + font-size: 16px; + } + .config-row > .config-input, - .config-row > .num-inline, - .config-row > .config-dual-inline, .config-row > .config-dual-labeled { width: 100% !important; } diff --git a/RutasDBUS/wwwroot/app.js b/RutasDBUS/wwwroot/app.js index 8496900..a7fb6b5 100644 --- a/RutasDBUS/wwwroot/app.js +++ b/RutasDBUS/wwwroot/app.js @@ -901,29 +901,89 @@ window.rt.ensureItStopsLayer = function () { return true; }; -window.rt.addStopDirectionMarker = function (group, stop, pane, radius) { - if (!group || !stop || !window.L) return; +window.rt.createHighlightedStopMarker = function (stop, pane, options) { + if (!stop || !window.L) return null; - const bearing = Number(stop.bearing); const lat = Number(stop.lat); const lon = Number(stop.lon); - if (!Number.isFinite(bearing) || !Number.isFinite(lat) || !Number.isFinite(lon)) return; + if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null; + + const toPositiveNumber = function (value, fallback) { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; + }; + const toOpacity = function (value, fallback) { + const parsed = Number(value); + return Number.isFinite(parsed) ? Math.min(1, Math.max(0, parsed)) : fallback; + }; + + const radius = toPositiveNumber(options && options.radius, 7); + const weight = toPositiveNumber(options && options.weight, 2); + const color = String((options && options.color) || "#0f172a"); + const fillColor = String((options && options.fillColor) || "#22c55e"); + const opacity = toOpacity(options && options.opacity, 1); + const fillOpacity = toOpacity(options && options.fillOpacity, 1); + const size = Math.max(14, Math.ceil((radius * 2) + weight)); + + const markerElement = document.createElement("span"); + markerElement.className = "rt-highlighted-stop-marker"; + + const disc = document.createElement("span"); + disc.className = "rt-highlighted-stop-marker__disc"; + disc.style.inset = `${weight}px`; + disc.style.backgroundColor = fillColor; + disc.style.opacity = String(fillOpacity); + markerElement.appendChild(disc); + + const ring = document.createElement("span"); + ring.className = "rt-highlighted-stop-marker__ring"; + ring.style.borderColor = color; + ring.style.borderWidth = `${weight}px`; + ring.style.opacity = String(opacity); + markerElement.appendChild(ring); + + const rawBearing = stop.bearing; + const bearing = rawBearing === null || rawBearing === undefined || rawBearing === "" + ? null + : Number(rawBearing); + if (Number.isFinite(bearing)) { + const arrow = document.createElement("span"); + arrow.className = "rt-stop-direction-arrow"; + arrow.style.setProperty("--stop-bearing", `${bearing}deg`); + markerElement.appendChild(arrow); + } - const size = Math.max(14, Number(radius || 7) * 2); const icon = window.L.divIcon({ - className: "rt-stop-direction-icon", - html: ``, + className: "rt-highlighted-stop-icon", + html: markerElement, iconSize: [size, size], iconAnchor: [size / 2, size / 2] }); - window.L.marker([lat, lon], { + const marker = window.L.marker([lat, lon], { pane, icon, - interactive: false, + interactive: true, keyboard: false, + bubblingMouseEvents: false, zIndexOffset: 1000 - }).addTo(group); + }); + + if (stop.code && window.rt._dotNetHelper) { + marker.on("click", function (event) { + try { + if (event && event.originalEvent) { + if (event.originalEvent.preventDefault) event.originalEvent.preventDefault(); + if (event.originalEvent.stopPropagation) event.originalEvent.stopPropagation(); + if (event.originalEvent.stopImmediatePropagation) event.originalEvent.stopImmediatePropagation(); + } + + window.rt._dotNetHelper.invokeMethodAsync("OnHighlightedStopClick", stop.code); + } catch (error) { } + }); + } + + return marker; }; // stops: [{lat, lon, code, bearing}] @@ -949,34 +1009,15 @@ window.rt.toggleItStops = function (id, stops, options) { if (stops && stops.length) { for (let i = 0; i < stops.length; i++) { const s = stops[i]; - - const m = window.L.circleMarker([s.lat, s.lon], { - pane: "rtStopsPane", + const marker = window.rt.createHighlightedStopMarker(s, "rtStopsPane", { radius, color, weight, opacity, fillColor, - fillOpacity, - interactive: true + fillOpacity }); - - if (s.code && window.rt._dotNetHelper) { - m.on("click", function (e) { - try { - if (e && e.originalEvent) { - if (e.originalEvent.preventDefault) e.originalEvent.preventDefault(); - if (e.originalEvent.stopPropagation) e.originalEvent.stopPropagation(); - if (e.originalEvent.stopImmediatePropagation) e.originalEvent.stopImmediatePropagation(); - } - - window.rt._dotNetHelper.invokeMethodAsync("OnHighlightedStopClick", s.code); - } catch (err) { } - }); - } - - m.addTo(group); - window.rt.addStopDirectionMarker(group, s, "rtStopsPane", radius); + if (marker) marker.addTo(group); } } @@ -1027,34 +1068,15 @@ window.rt.drawRouteStops = function (id, stops, options) { for (let i = 0; i < stops.length; i++) { const s = stops[i]; - - const m = window.L.circleMarker([s.lat, s.lon], { - pane: "rtRouteStopsPane", + const marker = window.rt.createHighlightedStopMarker(s, "rtRouteStopsPane", { radius, color, weight, opacity, fillColor, - fillOpacity, - interactive: true + fillOpacity }); - - if (s.code && window.rt._dotNetHelper) { - m.on("click", function (e) { - try { - if (e && e.originalEvent) { - if (e.originalEvent.preventDefault) e.originalEvent.preventDefault(); - if (e.originalEvent.stopPropagation) e.originalEvent.stopPropagation(); - if (e.originalEvent.stopImmediatePropagation) e.originalEvent.stopImmediatePropagation(); - } - - window.rt._dotNetHelper.invokeMethodAsync("OnHighlightedStopClick", s.code); - } catch (err) { } - }); - } - - m.addTo(group); - window.rt.addStopDirectionMarker(group, s, "rtRouteStopsPane", radius); + if (marker) marker.addTo(group); } group.addTo(window.rt._routeStopsLayer); @@ -1537,20 +1559,65 @@ window.rt.initDraggablePopups = function () { applySavedPositions(); }; -window.rt.saveLocalValue = function (key, value) { +window.rt.saveLocalValueResult = function (key, value) { try { - if (!window.localStorage) return; - window.localStorage.setItem(key, value ?? ""); - } catch { } + const storage = window.localStorage; + if (!storage) { + return { + ok: false, + errorName: "StorageUnavailable", + errorMessage: "El navegador no expone localStorage." + }; + } + + storage.setItem(key, value ?? ""); + return { ok: true }; + } catch (error) { + return { + ok: false, + errorName: error?.name ? String(error.name) : "StorageError", + errorMessage: error?.message ? String(error.message) : String(error ?? "Error desconocido al guardar en localStorage.") + }; + } +}; + +window.rt.loadLocalValueResult = function (key) { + try { + const storage = window.localStorage; + if (!storage) { + return { + ok: false, + found: false, + value: null, + errorName: "StorageUnavailable", + errorMessage: "El navegador no expone localStorage." + }; + } + + const value = storage.getItem(key); + return { + ok: true, + found: value !== null, + value + }; + } catch (error) { + return { + ok: false, + found: false, + value: null, + errorName: error?.name ? String(error.name) : "StorageError", + errorMessage: error?.message ? String(error.message) : String(error ?? "Error desconocido al leer localStorage.") + }; + } +}; + +window.rt.saveLocalValue = function (key, value) { + window.rt.saveLocalValueResult(key, value); }; window.rt.loadLocalValue = function (key) { - try { - if (!window.localStorage) return null; - return window.localStorage.getItem(key); - } catch { - return null; - } + const result = window.rt.loadLocalValueResult(key); + return result.ok ? result.value : null; }; window.rt.clearLocalBrowserData = async function () {