- historial arreglado

- solucion 1/2
- quitar segundo score y colocar el score bien calculado
- score equilibrado a
- no usar pendientes
- flechas arregaldas con el zoom
- intinerario permitidos hasta el 99
- añadir peso a los transbordos
factor equilibrado a 6
visualizar pendientes en la confi avanzada
cambiar pisos por metros
This commit is contained in:
2026-08-03 12:58:55 +02:00
parent 6145fb635a
commit e9d77ebb38
13 changed files with 1036 additions and 543 deletions

View File

@@ -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();
}
/// <summary>
@@ -285,6 +290,7 @@ public partial class PlanificadorRutas : ComponentBase
{
SincronizarTextosPenalizacionPie();
await PersistirConfiguracionAsync();
await RecalcularRutaEquilibradaTrasCambioFactorAsync();
}
/// <summary>
@@ -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<string, List<Parada>> _paradasItinerarioVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, List<Parada>> _paradasRutaVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
@@ -1351,15 +1357,18 @@ public partial class PlanificadorRutas : ComponentBase
}
/// <summary>
/// Construye los parametros actuales que afectan a la penalizacion peatonal.
/// Construye los parametros actuales que afectan al ranking de rutas.
/// </summary>
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()

View File

@@ -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<bool> CargarHistorialLocalDesdeNavegadorAsync()
{
string? json;
var numeroIntento = ++_numeroIntentoCargaHistorialLocal;
ResultadoStorageNavegador? resultadoLectura;
try
{
json = await JS.InvokeAsync<string?>("rt.loadLocalValue", ClaveHistorialLocal);
using var cts = new CancellationTokenSource(
TimeSpan.FromSeconds(TiempoMaxOperacionHistorialLocalSegundos));
resultadoLectura = await JS.InvokeAsync<ResultadoStorageNavegador?>(
"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<RegistroHistorialRuta>());
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<RegistroHistorialRuta>());
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<RegistroHistorialRuta> 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<RegistroHistorialRuta>();
var idsVistos = new HashSet<string>(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<int>();
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<RegistroHistorialRuta> items)
{
_historialLocal.Clear();
@@ -187,17 +343,19 @@ public partial class PlanificadorRutas
_historialLocalCargado = true;
}
private async Task GuardarHistorialLocalRecuperadoAsync(string jsonOriginal)
private async Task<bool> 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<bool> 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<ResultadoStorageNavegador?>(
"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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -384,6 +384,12 @@ public partial class PlanificadorRutas : ComponentBase
IReadOnlyList<AlternativaRuta> alternativas,
CancellationToken ct)
{
foreach (var alternativa in alternativas)
alternativa.EsfuerzoExtraPendienteMetros = 0;
if (!_mostrarElevacionesRuta || alternativas.Count == 0)
return;
var lineasPorAlternativa = alternativas
.Select(alternativa => new
{

View File

@@ -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,

View File

@@ -613,88 +613,6 @@
</div>
</div>
<div class="route-visual-options">
<label class="route-elevation-switch @(_mostrarElevacionesRuta ? "route-elevation-switch--active" : "")"
title="Colorear tramos a pie segun pendiente">
<input type="checkbox"
checked="@_mostrarElevacionesRuta"
disabled="@(_pintandoElevacionesRuta || _opcionesRuta.Count == 0)"
@onchange="ToggleElevacionesRutaAsync" />
<span class="route-elevation-switch-track">
<span class="route-elevation-switch-thumb"></span>
</span>
<span class="route-elevation-switch-text">
Visualizar Pendientes
</span>
</label>
<div class="route-elevation-settings">
<button type="button"
class="route-elevation-settings-trigger @(_mostrarConfiguracionPendientesRuta ? "route-elevation-settings-trigger--active" : "")"
title="Configurar pendientes"
aria-label="Configurar pendientes"
aria-haspopup="dialog"
aria-expanded="@_mostrarConfiguracionPendientesRuta"
disabled="@_pintandoElevacionesRuta"
@onclick="() => _mostrarConfiguracionPendientesRuta = !_mostrarConfiguracionPendientesRuta">
<span aria-hidden="true">&#9881;</span>
</button>
@if (_mostrarConfiguracionPendientesRuta)
{
<div class="route-elevation-settings-popover"
role="dialog"
aria-label="Umbrales de pendientes"
@onclick:stopPropagation="true">
<div class="route-elevation-settings-header">
<span>Configurar Pendientes</span>
<button type="button"
class="route-elevation-settings-close"
title="Cerrar"
aria-label="Cerrar configuración de pendientes"
@onclick="() => _mostrarConfiguracionPendientesRuta = false">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="route-elevation-settings-fields">
<label class="route-elevation-setting-row">
<span>Subida</span>
<input type="text"
inputmode="decimal"
aria-label="Porcentaje de subida"
value="@_umbralPendienteSubidaRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, true))" />
<span>%</span>
</label>
<label class="route-elevation-setting-row">
<span>Bajada</span>
<input type="text"
inputmode="decimal"
aria-label="Porcentaje de bajada"
value="@_umbralPendienteBajadaRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, false))" />
<span>%</span>
</label>
<label class="route-elevation-setting-row"
title="Pisos acumulados para mostrar el aviso de pendientes positivas">
<span>Aviso</span>
<input type="text"
inputmode="decimal"
aria-label="Pisos para mostrar el aviso"
value="@_umbralPisosPendienteRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="CambiarUmbralPisosPendienteRutaAsync" />
<span>pisos</span>
</label>
</div>
</div>
}
</div>
</div>
@if (_opcionesRuta.Count > 0)
{
<div class="route-alt-list">
@@ -763,16 +681,13 @@
{
var alt = _opcionesRuta[_indiceAlternativaSeleccionada];
@if (MostrarAvisoPendientesPositivasSignificativas)
@if (MostrarAlertaPendientesPositivasSignificativas)
{
<div class="route-slope-warning" role="status">
<div>Pendientes positivas significativas.</div>
<div style="margin-top:.2rem; padding-left:.55rem;">
<div>- Distancia con subidas: @FormatearDistanciaResumenElevacion(_distanciaPendientePositivaSignificativaRutaMetros)</div>
<div>
- Subidas acumuladas: @_alturaPendientePositivaSignificativaRutaMetros.ToString("0.00", CultureInfo.InvariantCulture) m
(@((_alturaPendientePositivaSignificativaRutaMetros / MetrosPorPiso).ToString("0.0", CultureInfo.InvariantCulture)) pisos)
</div>
<div>- Subidas acumuladas: @_alturaPendientePositivaSignificativaRutaMetros.ToString("0.00", CultureInfo.InvariantCulture) m</div>
</div>
</div>
}
@@ -975,7 +890,7 @@
}
@if (!string.IsNullOrWhiteSpace(scoreEquilibradoSeleccionado))
{
<div>Score equilibrado: @scoreEquilibradoSeleccionado</div>
<div>Score equilibrado A: @scoreEquilibradoSeleccionado</div>
}
@if (!string.IsNullOrWhiteSpace(xyzOrigen))
{
@@ -1113,6 +1028,27 @@
}
</div>
}
@if (!string.IsNullOrWhiteSpace(_resumenErrorHistorialLocal))
{
<div class="history-local-diagnostic" role="alert">
<div class="history-local-diagnostic__title">Diagnóstico del historial personal</div>
<div class="history-local-diagnostic__summary">@_resumenErrorHistorialLocal</div>
<details class="history-local-diagnostic__details">
<summary>Detalle técnico</summary>
<pre>@_detalleErrorHistorialLocal</pre>
</details>
@if (!_historialLocalCargado && !_historialLocalCargando)
{
<button type="button"
class="history-local-diagnostic__retry"
@onclick="ReintentarCargaHistorialLocalAsync">
Reintentar carga
</button>
}
</div>
}
</div>
<div class="history-section">
@@ -1262,10 +1198,10 @@
</div>
<div class="config-row">
<label>Bus/paradas:</label>
<label>Paradas:</label>
<div class="config-dual-labeled">
<label class="config-dual-labeled__field">
<span>Seg. parada</span>
<span>Seg./parada</span>
@if (_usarHorariosTeoricos)
{
<input type="number" class="config-input" step="1" min="0" value="0" disabled readonly />
@@ -1348,7 +1284,7 @@
@bind="_itinerarioMin"
@bind:event="onchange"
@bind:after="PersistirConfiguracionAsync">
@foreach (var n in Range(1, 150, 1))
@foreach (var n in Range(1, 99, 1))
{
<option value="@n">@n</option>
}
@@ -1361,7 +1297,7 @@
@bind="_itinerarioMax"
@bind:event="onchange"
@bind:after="PersistirConfiguracionAsync">
@foreach (var n in Range(1, 150, 1))
@foreach (var n in Range(1, 99, 1))
{
<option value="@n">@n</option>
}
@@ -1441,19 +1377,23 @@
</div>
</div>
<div class="config-row config-row--radio-legacy">
<label>Coef. / Param.(m) andando:</label>
<div class="config-dual-inline config-dual-inline--bare">
<input type="number" class="config-input" step="any"
placeholder="Coef."
title="Coeficiente de penalizacion"
@bind="FactorPenalizacionDistanciaPieTexto" @bind:event="oninput"
@onblur="OnBlurFactorPenalizacionDistanciaPie" />
<input type="number" class="config-input" step="any"
placeholder="Param."
title="Parametro base en metros"
@bind="ParametroPenalizacionDistanciaPieTexto" @bind:event="oninput"
@onblur="OnBlurParametroPenalizacionDistanciaPie" />
<div class="config-row">
<label>Andando:</label>
<div class="config-dual-labeled">
<label class="config-dual-labeled__field">
<span>Coef.</span>
<input type="number" class="config-input" step="any"
title="Coeficiente de penalización andando"
@bind="FactorPenalizacionDistanciaPieTexto" @bind:event="oninput"
@onblur="OnBlurFactorPenalizacionDistanciaPie" />
</label>
<label class="config-dual-labeled__field">
<span>Base (m)</span>
<input type="number" class="config-input" step="any"
title="Parámetro base en metros"
@bind="ParametroPenalizacionDistanciaPieTexto" @bind:event="oninput"
@onblur="OnBlurParametroPenalizacionDistanciaPie" />
</label>
</div>
</div>
@@ -1464,7 +1404,7 @@
<span>Peso rank.</span>
<input type="text" class="config-input"
placeholder="4,0"
title="Penalizacion interna por cada transbordo real en minutos decimales"
title="Penalización temporal y distancia equivalente por cada transbordo real"
@bind="PesoTransbordoRankingTexto" @bind:event="oninput"
@onblur="OnBlurPesoTransbordoRanking" />
</label>
@@ -1478,59 +1418,90 @@
</div>
<div class="config-row">
<label>Equilibrado:</label>
<div class="config-dual-labeled config-dual-labeled--equilibrado">
<label>Factor / Método:</label>
<div class="config-dual-labeled">
<label class="config-dual-labeled__field">
<span>Factor</span>
<span>Equilib. A</span>
<input type="number" class="config-input" step="0.1" min="0"
placeholder="12"
placeholder="6"
title="Factor del modo equilibrado"
@bind="FactorEquilibradoAndarBusTexto" @bind:event="oninput"
@onblur="OnBlurFactorEquilibradoAndarBus" />
</label>
<label class="config-dual-labeled__field">
<span>Andando</span>
<select class="config-input"
title="Método de cálculo andando"
@bind="_walkProvider" @bind:after="PersistirConfiguracionAsync">
<option value="@ProveedorCaminata.Osrm">v1</option>
<option value="@ProveedorCaminata.Valhalla">v2</option>
</select>
</label>
</div>
</div>
<div class="config-row">
<label>Vel. Andando/Bus (km/h):</label>
<div class="config-dual-inline config-dual-inline--bare">
<input type="number" class="config-input" step="0.1"
placeholder="Andar"
title="Velocidad andando (km/h)"
@bind="_velocidadPieKmH" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
<input type="number" class="config-input" step="0.1"
placeholder="Bus"
title="Velocidad bus (km/h)"
@bind="_velocidadBusKmH" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
<label>Velocidad:</label>
<div class="config-dual-labeled">
<label class="config-dual-labeled__field">
<span>Andar (km/h)</span>
<input type="number" class="config-input" step="0.1"
title="Velocidad andando (km/h)"
@bind="_velocidadPieKmH" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
</label>
<label class="config-dual-labeled__field">
<span>Bus (km/h)</span>
<input type="number" class="config-input" step="0.1"
title="Velocidad bus (km/h)"
@bind="_velocidadBusKmH" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
</label>
</div>
</div>
<div class="config-row">
<label>Altitud: Max Puntos / Coef</label>
<div class="config-dual-inline config-dual-inline--bare">
<input type="number" class="config-input" min="2" max="999" step="1"
placeholder="Puntos"
title="Máximo de puntos de altitud por tramo"
@bind="_elevMaxPuntosPorTramo" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
<input type="number" class="config-input" step="any"
placeholder="Coef."
title="Coeficiente de esfuerzo por altura"
@bind="CoeficienteEsfuerzoAlturaTexto" @bind:event="oninput"
@onblur="OnBlurCoeficienteEsfuerzoAltura" />
<label>Altitud:</label>
<div class="config-dual-labeled">
<label class="config-dual-labeled__field">
<span>Máx. puntos</span>
<input type="number" class="config-input" min="2" max="999" step="1"
title="Máximo de puntos de altitud por tramo"
@bind="_elevMaxPuntosPorTramo" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
</label>
<label class="config-dual-labeled__field">
<span>Coef.</span>
<input type="number" class="config-input" step="any"
title="Coeficiente de esfuerzo por altura"
@bind="CoeficienteEsfuerzoAlturaTexto" @bind:event="oninput"
@onblur="OnBlurCoeficienteEsfuerzoAltura" />
</label>
</div>
</div>
<div class="config-row">
<label>Método cálculo andando:</label>
<div class="num-inline">
<select class="config-input"
style="width:100%; min-width:0;"
@bind="_walkProvider" @bind:after="PersistirConfiguracionAsync">
<option value="@ProveedorCaminata.Osrm">v1</option>
<option value="@ProveedorCaminata.Valhalla">v2</option>
</select>
</div>
</div>
<div class="config-row config-row-pendientes">
<label>Pendientes:</label>
<div class="config-pendientes-inline">
<label class="route-elevation-switch @(_mostrarElevacionesRuta ? "route-elevation-switch--active" : "")"
title="Activar o desactivar las pendientes en el cálculo">
<input type="checkbox"
checked="@_mostrarElevacionesRuta"
disabled="@_pintandoElevacionesRuta"
@onchange="ToggleElevacionesRutaAsync" />
<span class="route-elevation-switch-track" aria-hidden="true">
<span class="route-elevation-switch-thumb"></span>
</span>
<span class="config-pendientes-inline__label">Visualizar</span>
</label>
<button type="button"
class="config-pendientes-settings-button"
title="Configurar Pendientes"
aria-label="Configurar Pendientes"
disabled="@_pintandoElevacionesRuta"
@onclick="() => _mostrarConfiguracionPendientesRuta = true">
<span aria-hidden="true">⚙</span>
</button>
</div>
</div>
<div class="config-actions-row">
<button class="btn-ghost" @onclick="RecalcularConParametros">
@@ -1562,6 +1533,57 @@
}
</div>
@if (_mostrarConfiguracionPendientesRuta)
{
<div class="custom-modal-backdrop pendientes-config-modal-backdrop"
@onclick="() => _mostrarConfiguracionPendientesRuta = false">
<div class="custom-modal-card pendientes-config-modal" @onclick:stopPropagation="true">
<div class="pendientes-config-modal__header">
<div class="custom-modal-title">Configurar Pendientes</div>
<button type="button"
class="config-close-btn pendientes-config-modal__close"
title="Cerrar"
aria-label="Cerrar"
@onclick="() => _mostrarConfiguracionPendientesRuta = false">
×
</button>
</div>
<div class="pendientes-config-modal__fields">
<label class="pendientes-config-modal__field">
<span>Subida (%)</span>
<input type="text"
class="config-input"
inputmode="decimal"
value="@_umbralPendienteSubidaRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, true))" />
</label>
<label class="pendientes-config-modal__field">
<span>Bajada (%)</span>
<input type="text"
class="config-input"
inputmode="decimal"
value="@_umbralPendienteBajadaRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, false))" />
</label>
<label class="pendientes-config-modal__field">
<span>Alerta (m)</span>
<input type="text"
class="config-input"
inputmode="decimal"
value="@_umbralAlturaAlertaPendienteRutaMetros.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="CambiarUmbralAlturaAlertaPendienteRutaAsync" />
</label>
</div>
</div>
</div>
}
@if (_mostrarModelosHorariosPanel)
{
<div class="custom-modal-backdrop modelos-horarios-modal-backdrop" @onclick="CerrarModelosHorariosPanel">