- Rediseñada la configuración de pendientes con un panel compacto accesible desde la rueda.

- Corregida la posición del panel para que no salga fuera de la pantalla.
- Renombrado el interruptor a Visualizar Pendientes.
- Aplicado el porcentaje de subida como umbral para calcular el esfuerzo.
- Mostrado el umbral de subida utilizado dentro de la tabla de elevación.
- Añadido al aviso el total de distancia con pendiente y la altura subida en metros y pisos.
- Evitada la duplicación de acumulados al repintar las pendientes.
- Cambiados los valores por defecto a subida 2 %, bajada 5 % y coeficiente 8,2.
- Añadida la distancia equilibrada andando al detalle técnico.
- Mostradas las distancias en kilómetros con tres decimales.
- Mostrada la fecha del ZIP activo en el acceso de gestión del modelo DBUS.
- Añadidas migraciones para actualizar los antiguos valores predeterminados.
This commit is contained in:
2026-07-20 15:29:25 +02:00
parent 7dc251f44e
commit 7b52dd2c73
16 changed files with 941 additions and 186 deletions

View File

@@ -7,14 +7,14 @@
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="RutasDBUS.styles.css" />
<link rel="stylesheet" href="app.css?v=20260716m3" />
<link rel="stylesheet" href="app.css?v=20260720h" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="app.js?v=20260716m3" defer></script>
<script src="app.js?v=20260720h" defer></script>
<script src="_framework/blazor.web.js"></script>
</body>

View File

@@ -6,6 +6,7 @@ using System.Text.RegularExpressions;
using LeafletForBlazor;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using RutasDBUS.Modelos.Geocodificacion;
using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus;
using RutasDBUS.Servicios;
@@ -174,6 +175,8 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
if (it is null) return;
await PintarItItemAsync(it);
var coordenadas = ConstruirCoordenadasItinerario(it);
await FitRoutesJsAsync(coordenadas);
}
/// <summary>
/// Quita it seleccionado.
@@ -254,7 +257,8 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
private double _inspectorLat, _inspectorLon;
private double? _inspectorElev;
private double? _inspectorElevIdee;
private string? _inspectorCallejero;
private IReadOnlyList<LugarGeocodificadoCercano> _inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
private int _versionInspector;
/// <summary>

View File

@@ -47,16 +47,22 @@ public partial class PlanificadorRutas : ComponentBase
private int _nivelBrilloMapa = 0;
// CONFIG
private const double FactorPenalizacionDistanciaPiePorDefecto = 1.35;
private const double CoeficienteEsfuerzoAlturaPorDefecto = 8.2;
private const double UmbralPendienteSubidaRutaPorDefecto = 2.0;
private const double UmbralPendienteBajadaRutaPorDefecto = 5.0;
private const double UmbralPisosPendienteRutaPorDefecto = 2.0;
private const double MetrosPorPiso = 3.0;
private double _velocidadPieKmH = 3.6;
private double _factorPenalizacionDistanciaPie = 1.6;
private double _factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
private double _parametroPenalizacionDistanciaPieMetros = 100.0;
private const double CoeficienteEsfuerzoAlturaPorDefecto = 5.2;
private double _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
private double _pesoTransbordoRankingMinutos = 4.0;
private double _factorEquilibradoAndarBus = 12.0;
private string _factorPenalizacionDistanciaPieTextoEdicion = "1.60";
private string _factorPenalizacionDistanciaPieTextoEdicion = "1.35";
private string _parametroPenalizacionDistanciaPieTextoEdicion = "100";
private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20";
private string _coeficienteEsfuerzoAlturaTextoEdicion = "8.20";
private string _pesoTransbordoRankingTextoEdicion = "4,0";
private string _factorEquilibradoAndarBusTextoEdicion = "12";
private double _velocidadBusKmH = 16.0;
@@ -222,10 +228,11 @@ public partial class PlanificadorRutas : ComponentBase
? _coeficienteEsfuerzoAltura
: CoeficienteEsfuerzoAlturaPorDefecto;
var pendiente = pendientePorcentaje.Value / 100.0;
if (pendiente <= 0)
var umbralSubida = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta);
if (pendientePorcentaje.Value < umbralSubida)
return distanciaMetros.Value;
var pendiente = pendientePorcentaje.Value / 100.0;
var factorConPotencia = Math.Pow(1.0 + pendiente, coeficiente);
var esfuerzo = distanciaMetros.Value * factorConPotencia;
return double.IsFinite(esfuerzo)
@@ -351,9 +358,18 @@ public partial class PlanificadorRutas : ComponentBase
private TimeSpan? _tiempoUltimoCalculoRuta = null;
private bool _mostrarElevacionesRuta = false;
private bool _pintandoElevacionesRuta = false;
private bool _mostrarConfiguracionPendientesRuta = false;
private bool _panelRutasMovilContraido = false;
private double _umbralPendienteSubidaRuta = 3.0;
private double _umbralPendienteBajadaRuta = 3.0;
private double _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
private double _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
private double _umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto;
private double _alturaPendientePositivaSignificativaRutaMetros = 0.0;
private double _distanciaPendientePositivaSignificativaRutaMetros = 0.0;
private bool MostrarAvisoPendientesPositivasSignificativas =>
_mostrarElevacionesRuta &&
_alturaPendientePositivaSignificativaRutaMetros >
(NormalizarUmbralPisosPendienteRuta(_umbralPisosPendienteRuta) * MetrosPorPiso);
private readonly Dictionary<string, List<Parada>> _paradasItinerarioVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, List<Parada>> _paradasRutaVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
@@ -987,9 +1003,16 @@ public partial class PlanificadorRutas : ComponentBase
/// <summary>
/// Ajusta rutas js.
/// </summary>
private async Task FitRoutesJsAsync()
private async Task FitRoutesJsAsync(IReadOnlyList<double[]>? coordenadas = null)
{
try { await JS.InvokeVoidAsync("rt.fitRoutes"); } catch { }
try
{
if (coordenadas is null)
await JS.InvokeVoidAsync("rt.fitRoutes");
else
await JS.InvokeVoidAsync("rt.fitRoutes", coordenadas);
}
catch { }
}
/// <summary>
@@ -1043,7 +1066,7 @@ public partial class PlanificadorRutas : ComponentBase
{
if (d <= 0) return "-";
if (d < 1000) return $"{Math.Truncate(d):0} m";
return $"{(d / 1000.0):0.00} km";
return $"{(d / 1000.0):0.000} km";
}
/// <summary>
@@ -1206,18 +1229,35 @@ public partial class PlanificadorRutas : ComponentBase
return EstimarDuracionPieSegundos(distanciaCorregida);
}
/// <summary>
/// Calcula la aportacion de todos los tramos andando a la distancia equilibrada.
/// </summary>
private double CalcularDistanciaEquilibradaAndandoAlternativa(AlternativaRuta? alternativa)
{
if (alternativa is null)
return 0;
var distanciaAndando = alternativa.EsSoloAPie
? alternativa.DistanciaSoloAPie
: alternativa.DistanciaPieTotalAprox;
var factor = double.IsFinite(_factorEquilibradoAndarBus)
? Math.Max(0, _factorEquilibradoAndarBus)
: 12.0;
var distanciaEquilibrada = Math.Max(0, distanciaAndando) * factor;
return double.IsFinite(distanciaEquilibrada)
? distanciaEquilibrada
: 0;
}
private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa)
{
if (alternativa is null || alternativa.EsSoloAPie || alternativa.AlternativaBus is null)
return double.PositiveInfinity;
var distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus);
var distanciaAndando = Math.Max(0, alternativa.DistanciaPieTotalAprox);
var factor = double.IsFinite(_factorEquilibradoAndarBus)
? Math.Max(0, _factorEquilibradoAndarBus)
: 12.0;
var score = distanciaBus + (distanciaAndando * factor);
var distanciaEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(alternativa);
var score = distanciaBus + distanciaEquilibradaAndando;
return double.IsFinite(score) && score >= 0
? score
: double.PositiveInfinity;
@@ -1232,9 +1272,6 @@ public partial class PlanificadorRutas : ComponentBase
return $"Score {FormatearDistanciaTarjeta(score)} (x{_factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture)})";
}
private string FactorEquilibradoAndarBusResumen
=> _factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture);
private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync()
{
if (!CriterioPrioritarioEquilibradoActivo)
@@ -1572,6 +1609,8 @@ public partial class PlanificadorRutas : ComponentBase
_hayRutaValida = false;
_tramosRuta.Clear();
_duracionTotalSegundos = 0;
_alturaPendientePositivaSignificativaRutaMetros = 0.0;
_distanciaPendientePositivaSignificativaRutaMetros = 0.0;
_altitudOrigenResumenRuta = null;
_altitudDestinoResumenRuta = null;
_callejeroOrigenResumenRuta = null;

View File

@@ -76,7 +76,7 @@ public partial class PlanificadorRutas
try
{
await Task.Delay(120, ct);
var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 10, ct);
var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 20, ct);
if (ct.IsCancellationRequested || version != _versionSugerenciasBusqueda)
return;
@@ -118,6 +118,10 @@ public partial class PlanificadorRutas
_hayRutaValida = false;
await SincronizarMarcadoresOrigenDestinoJsAsync();
await FitRoutesJsAsync(new List<double[]>
{
new[] { sugerencia.Latitud, sugerencia.Longitud }
});
StateHasChanged();
}

View File

@@ -232,6 +232,9 @@ public partial class PlanificadorRutas : ComponentBase
/// </summary>
private async Task PintarItFiltro()
{
if (!PuedePintarItinerariosFiltrados)
return;
foreach (var it in _itItemsFiltrados)
{
if (it is null) continue;
@@ -289,6 +292,23 @@ public partial class PlanificadorRutas : ComponentBase
RegistrarPreview(clave, subId);
}
}
/// <summary>
/// Reúne la geometría de todos los sentidos incluidos en un elemento de itinerario.
/// </summary>
private List<double[]> ConstruirCoordenadasItinerario(ElementoItinerario it)
{
var coordenadas = new List<double[]>();
foreach (var variante in it.Variantes)
{
var geometria = ConstruirGeometriaCompletaVariante(variante);
if (geometria is not null)
coordenadas.AddRange(LineaToLatLng(geometria));
}
return coordenadas;
}
private Parada? _paradaPrimerClickPendiente;
@@ -609,6 +629,8 @@ public partial class PlanificadorRutas : ComponentBase
private string _itFiltro = "";
private bool PuedePintarItinerariosFiltrados => !string.IsNullOrWhiteSpace(_itFiltro);
private string ItFiltro
{
get => _itFiltro;
@@ -1181,8 +1203,8 @@ public partial class PlanificadorRutas : ComponentBase
private bool _elevPopupMinimizado = false;
private const string _versionApp = "(v-20260716a)";
private const string _versionApp = "(v-20260720c)";
private double? _paradaElevG;
@@ -1200,7 +1222,7 @@ public partial class PlanificadorRutas : ComponentBase
_itinerarioClicado = null;
_paradaClicada = null;
_popupInspectorVisible = false;
await CerrarInspectorAsync();
if (_tramoElevDetalle is not null)
{

View File

@@ -18,6 +18,38 @@ public partial class PlanificadorRutas
private ModeloHorarioDbusInfo? ModeloHorarioActivo =>
_modelosHorarios.FirstOrDefault(modelo => modelo.Activo);
private string FechaModeloHorarioActivo =>
FormatearFechaModeloHorario(ModeloHorarioActivo?.NombreArchivo);
private static string FormatearFechaModeloHorario(string? nombreArchivo)
{
if (string.IsNullOrWhiteSpace(nombreArchivo))
return "Sin modelo cargado";
for (var indice = nombreArchivo.Length - 8; indice >= 0; indice--)
{
if (indice > 0 && char.IsDigit(nombreArchivo[indice - 1]))
continue;
var fin = indice + 8;
if (fin < nombreArchivo.Length && char.IsDigit(nombreArchivo[fin]))
continue;
var candidata = nombreArchivo.Substring(indice, 8);
if (DateTime.TryParseExact(
candidata,
"yyyyMMdd",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out _))
{
return candidata;
}
}
return "Fecha no disponible";
}
private void AbrirModelosHorariosPanel()
{
_mostrarModelosHorariosPanel = true;

View File

@@ -38,6 +38,7 @@ public partial class PlanificadorRutas
public double? FactorEquilibradoAndarBus { get; set; }
public double? UmbralPendienteSubidaRuta { get; set; }
public double? UmbralPendienteBajadaRuta { get; set; }
public double? UmbralPisosPendienteRuta { get; set; }
public double VelocidadPieKmH { get; set; }
public double VelocidadBusKmH { get; set; }
public int ElevMaxPuntosPorTramo { get; set; }
@@ -140,6 +141,7 @@ public partial class PlanificadorRutas
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta,
UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta,
UmbralPisosPendienteRuta = _umbralPisosPendienteRuta,
VelocidadPieKmH = _velocidadPieKmH,
VelocidadBusKmH = _velocidadBusKmH,
ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo,
@@ -185,11 +187,17 @@ public partial class PlanificadorRutas
_horaSimulada = horaSimulada;
if (config.FactorPenalizacionDistanciaPie is { } factor && double.IsFinite(factor))
_factorPenalizacionDistanciaPie = Math.Abs(factor - 1.3) < 0.0001 ? 1.6 : factor;
_factorPenalizacionDistanciaPie =
Math.Abs(factor - 1.3) < 0.0001 || Math.Abs(factor - 1.6) < 0.0001
? FactorPenalizacionDistanciaPiePorDefecto
: factor;
if (config.ParametroPenalizacionDistanciaPieMetros is { } parametro && double.IsFinite(parametro))
_parametroPenalizacionDistanciaPieMetros = parametro;
if (config.CoeficienteEsfuerzoAltura is { } coeficienteAltura && double.IsFinite(coeficienteAltura))
_coeficienteEsfuerzoAltura = Math.Abs(coeficienteAltura - 1.6) < 0.0001
_coeficienteEsfuerzoAltura =
Math.Abs(coeficienteAltura - 1.6) < 0.0001 ||
Math.Abs(coeficienteAltura - 5.2) < 0.0001 ||
Math.Abs(coeficienteAltura - 9.2) < 0.0001
? CoeficienteEsfuerzoAlturaPorDefecto
: coeficienteAltura;
if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo))
@@ -197,9 +205,17 @@ public partial class PlanificadorRutas
if (config.FactorEquilibradoAndarBus is { } factorEquilibrado && double.IsFinite(factorEquilibrado))
_factorEquilibradoAndarBus = Math.Max(0, factorEquilibrado);
if (config.UmbralPendienteSubidaRuta is { } umbralSubida && double.IsFinite(umbralSubida))
_umbralPendienteSubidaRuta = NormalizarUmbralPendienteRuta(umbralSubida);
_umbralPendienteSubidaRuta = Math.Abs(umbralSubida - 3.0) < 0.0001
? UmbralPendienteSubidaRutaPorDefecto
: NormalizarUmbralPendienteRuta(umbralSubida);
if (config.UmbralPendienteBajadaRuta is { } umbralBajada && double.IsFinite(umbralBajada))
_umbralPendienteBajadaRuta = NormalizarUmbralPendienteRuta(umbralBajada);
_umbralPendienteBajadaRuta =
Math.Abs(umbralBajada - 3.0) < 0.0001 ||
Math.Abs(umbralBajada - 6.0) < 0.0001
? UmbralPendienteBajadaRutaPorDefecto
: NormalizarUmbralPendienteRuta(umbralBajada);
if (config.UmbralPisosPendienteRuta is { } umbralPisos && double.IsFinite(umbralPisos))
_umbralPisosPendienteRuta = NormalizarUmbralPisosPendienteRuta(umbralPisos);
_velocidadPieKmH = Math.Clamp(config.VelocidadPieKmH, 0.5, 25.0);
_velocidadBusKmH = Math.Clamp(
Math.Abs(config.VelocidadBusKmH - 15.0) < 0.0001 ||
@@ -298,13 +314,14 @@ public partial class PlanificadorRutas
_minTransbordosPermitidos = 0;
_maxTransbordosPermitidos = 3;
_factorPenalizacionDistanciaPie = 1.6;
_factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
_parametroPenalizacionDistanciaPieMetros = 100.0;
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
_pesoTransbordoRankingMinutos = 4.0;
_factorEquilibradoAndarBus = 12.0;
_umbralPendienteSubidaRuta = 3.0;
_umbralPendienteBajadaRuta = 3.0;
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
_umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto;
_velocidadPieKmH = 3.6;
_velocidadBusKmH = 16.0;
_minutosTransbordo = 5;
@@ -347,13 +364,14 @@ public partial class PlanificadorRutas
_minTransbordosPermitidos = 0;
_maxTransbordosPermitidos = 3;
_factorPenalizacionDistanciaPie = 1.6;
_factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
_parametroPenalizacionDistanciaPieMetros = 100.0;
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
_pesoTransbordoRankingMinutos = 4.0;
_factorEquilibradoAndarBus = 12.0;
_umbralPendienteSubidaRuta = 3.0;
_umbralPendienteBajadaRuta = 3.0;
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
_umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto;
_velocidadPieKmH = 3.6;
_velocidadBusKmH = 16.0;
_minutosTransbordo = 5;

View File

@@ -6,6 +6,7 @@ using System.Text.RegularExpressions;
using LeafletForBlazor;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using RutasDBUS.Modelos.Geocodificacion;
using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus;
using RutasDBUS.Servicios;
@@ -1267,10 +1268,26 @@ public partial class PlanificadorRutas : ComponentBase
/// </summary>
private async Task MostrarInspectorAsync(double lat, double lon)
{
var versionInspector = ++_versionInspector;
_inspectorLat = lat;
_inspectorLon = lon;
_inspectorCallejero = null;
_inspectorElev = null;
_inspectorElevIdee = null;
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
_popupInspectorVisible = true;
await ActualizarMarcadoresInspectorJsAsync();
StateHasChanged();
var tareaLugares = GeocodificadorLocal.BuscarCercanosAsync(
lat,
lon,
radioMetros: 20,
maxResultados: 3,
cancellationToken: CancellationToken.None);
double? elevacionOpen = null;
double? elevacionIdee = null;
try
{
// -------------------------
@@ -1279,7 +1296,7 @@ public partial class PlanificadorRutas : ComponentBase
var kOpen = ElevKey(lat, lon);
if (_elevCache.TryGetValue(kOpen, out var cachedOpen))
{
_inspectorElev = cachedOpen;
elevacionOpen = cachedOpen;
}
else
{
@@ -1287,7 +1304,7 @@ public partial class PlanificadorRutas : ComponentBase
new List<(double lat, double lon)> { (lat, lon) },
CancellationToken.None);
_inspectorElev = _elevCache.TryGetValue(ElevKey(lat, lon), out var elevOpen)
elevacionOpen = _elevCache.TryGetValue(ElevKey(lat, lon), out var elevOpen)
? elevOpen
: (double?)null;
}
@@ -1298,7 +1315,7 @@ public partial class PlanificadorRutas : ComponentBase
var kIdee = ClaveElevacionIdee(lat, lon);
if (_elevIdeeCache.TryGetValue(kIdee, out var cachedIdee))
{
_inspectorElevIdee = cachedIdee;
elevacionIdee = cachedIdee;
}
else
{
@@ -1306,33 +1323,72 @@ public partial class PlanificadorRutas : ComponentBase
new List<(double lat, double lon)> { (lat, lon) },
CancellationToken.None);
_inspectorElevIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var elevIdee)
elevacionIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var elevIdee)
? elevIdee
: (double?)null;
}
}
catch
{
_inspectorElev = null;
_inspectorElevIdee = null;
}
try
{
var lugar = await GeocodificadorLocal.BuscarMasCercanoAsync(
lat,
lon,
CancellationToken.None);
_inspectorCallejero = FormatearLugarCallejeroResumen(lugar);
var lugares = await tareaLugares;
if (versionInspector != _versionInspector)
return;
_inspectorLugaresCercanos = lugares;
}
catch
{
_inspectorCallejero = null;
if (versionInspector != _versionInspector)
return;
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
}
_popupInspectorVisible = true;
if (versionInspector != _versionInspector)
return;
_inspectorElev = elevacionOpen;
_inspectorElevIdee = elevacionIdee;
await ActualizarMarcadoresInspectorJsAsync();
StateHasChanged();
}
private async Task ActualizarMarcadoresInspectorJsAsync()
{
var lugares = _inspectorLugaresCercanos
.Select(cercano => new
{
latitud = cercano.Lugar.Latitud,
longitud = cercano.Lugar.Longitud,
texto = FormatearLugarCallejeroResumen(cercano)
})
.ToList();
try
{
await JS.InvokeVoidAsync(
"rt.setInspectorMarkers",
_inspectorLat,
_inspectorLon,
lugares);
}
catch
{
}
}
private async Task CerrarInspectorAsync()
{
_versionInspector++;
_popupInspectorVisible = false;
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
try { await JS.InvokeVoidAsync("rt.clearInspectorMarkers"); } catch { }
}
/// <summary>

View File

@@ -77,7 +77,7 @@ public partial class PlanificadorRutas : ComponentBase
};
await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta
? "Aplicando elevaciones..."
? "Aplicando pendientes..."
: "Representando ruta seleccionada...");
}
@@ -96,11 +96,21 @@ public partial class PlanificadorRutas : ComponentBase
await PersistirConfiguracionAsync();
if (_mostrarElevacionesRuta)
await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando elevaciones...");
await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando pendientes...");
else
StateHasChanged();
}
private async Task CambiarUmbralPisosPendienteRutaAsync(ChangeEventArgs e)
{
_umbralPisosPendienteRuta = ParseUmbralPisosPendienteRuta(
e.Value,
_umbralPisosPendienteRuta);
await PersistirConfiguracionAsync();
StateHasChanged();
}
private async Task RepintarRutaSeleccionadaPorElevacionesAsync(string mensaje)
{
if (_opcionesRuta.Count == 0 ||
@@ -111,6 +121,7 @@ public partial class PlanificadorRutas : ComponentBase
return;
}
ReiniciarResumenPendientesRuta();
_pintandoElevacionesRuta = true;
_mensajeRuta = mensaje;
StateHasChanged();
@@ -143,7 +154,24 @@ public partial class PlanificadorRutas : ComponentBase
private static double NormalizarUmbralPendienteRuta(double valor)
=> double.IsFinite(valor)
? Math.Clamp(Math.Abs(valor), 0.0, 50.0)
: 3.0;
: UmbralPendienteSubidaRutaPorDefecto;
private static double ParseUmbralPisosPendienteRuta(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 NormalizarUmbralPisosPendienteRuta(nuevo);
}
private static double NormalizarUmbralPisosPendienteRuta(double valor)
=> double.IsFinite(valor)
? Math.Clamp(Math.Abs(valor), 0.0, 100.0)
: UmbralPisosPendienteRutaPorDefecto;
private async Task DrawWalkingRouteJsAsync(
string id,
@@ -152,9 +180,17 @@ public partial class PlanificadorRutas : ComponentBase
int versionPintado,
bool arrowEnd = true)
{
var segmentosElevacion = await ConstruirSegmentosElevacionMapaAsync(linea, CancellationToken.None);
var resultadoElevacion = await ConstruirSegmentosElevacionMapaAsync(linea, CancellationToken.None);
ThrowIfPintadoObsoleto(versionPintado);
if (_mostrarElevacionesRuta)
{
_alturaPendientePositivaSignificativaRutaMetros +=
resultadoElevacion.AlturaPositivaSignificativaMetros;
_distanciaPendientePositivaSignificativaRutaMetros +=
resultadoElevacion.DistanciaPositivaSignificativaMetros;
}
await DrawRouteJsAsync(
id,
linea,
@@ -164,20 +200,25 @@ public partial class PlanificadorRutas : ComponentBase
dashArray: DashArrayPiePunteado,
arrowEnd: arrowEnd,
lineCap: "round",
elevationSegments: segmentosElevacion);
elevationSegments: resultadoElevacion.Segmentos);
}
private async Task<List<object>?> ConstruirSegmentosElevacionMapaAsync(double[,] linea, CancellationToken ct)
private async Task<(
List<object>? Segmentos,
double AlturaPositivaSignificativaMetros,
double DistanciaPositivaSignificativaMetros)>
ConstruirSegmentosElevacionMapaAsync(double[,] linea, CancellationToken ct)
{
if (!_mostrarElevacionesRuta || linea.GetLength(0) < 2)
return null;
return (null, 0.0, 0.0);
var muestras = await ConstruirMuestrasElevacionAsync(linea, ct);
if (muestras.Count < 2)
return null;
return (null, 0.0, 0.0);
var segmentos = new List<object>();
var hayPendienteCalculada = false;
var totalesPendientePositiva = CalcularTotalesPendientePositivaSignificativa(muestras);
for (var i = 1; i < muestras.Count; i++)
{
@@ -200,7 +241,8 @@ public partial class PlanificadorRutas : ComponentBase
if (elevAnterior is not null && elevActual is not null)
{
pendiente = (elevActual.Value - elevAnterior.Value) / distancia * 100.0;
var diferenciaAltura = elevActual.Value - elevAnterior.Value;
pendiente = diferenciaAltura / distancia * 100.0;
hayPendienteCalculada = true;
}
@@ -224,9 +266,64 @@ public partial class PlanificadorRutas : ComponentBase
});
}
return hayPendienteCalculada && segmentos.Count > 0
? segmentos
: null;
return (
hayPendienteCalculada && segmentos.Count > 0 ? segmentos : null,
totalesPendientePositiva.AlturaMetros,
totalesPendientePositiva.DistanciaMetros);
}
private (double DistanciaMetros, double AlturaMetros)
CalcularTotalesPendientePositivaSignificativa(IReadOnlyList<MuestraElevacion>? muestras)
{
if (muestras is null || muestras.Count < 2)
return (0.0, 0.0);
var distanciaTotal = 0.0;
var alturaTotal = 0.0;
var umbralSubida = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta);
for (var i = 1; i < muestras.Count; i++)
{
var anterior = muestras[i - 1];
var actual = muestras[i];
if (anterior.ElevacionIdee is null || actual.ElevacionIdee is null)
continue;
var distancia = CalcularDistanciaMetros(
anterior.Latitud,
anterior.Longitud,
actual.Latitud,
actual.Longitud);
if (distancia <= 0)
continue;
var diferenciaAltura = actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value;
var pendiente = diferenciaAltura / distancia * 100.0;
if (pendiente <= umbralSubida)
continue;
distanciaTotal += distancia;
alturaTotal += Math.Max(0, diferenciaAltura);
}
return (distanciaTotal, alturaTotal);
}
private static string FormatearDistanciaResumenElevacion(double distanciaMetros)
{
if (!double.IsFinite(distanciaMetros) || distanciaMetros <= 0)
return "-";
if (distanciaMetros < 950)
return $"{distanciaMetros:0.00} m";
return $"{(distanciaMetros / 1000.0):0.000} km";
}
private void ReiniciarResumenPendientesRuta()
{
_alturaPendientePositivaSignificativaRutaMetros = 0.0;
_distanciaPendientePositivaSignificativaRutaMetros = 0.0;
}
private static double? ElevacionPreferidaMapa(MuestraElevacion muestra)
@@ -259,6 +356,7 @@ public partial class PlanificadorRutas : ComponentBase
{
var versionPintado = IniciarNuevaVersionPintadoRuta();
var mensajeAnterior = _mensajeRuta;
ReiniciarResumenPendientesRuta();
try
{

View File

@@ -60,7 +60,9 @@
<button class="btn-ghost" @onclick="PintarItSeleccionado">Ver</button>
<button class="btn-ghost" @onclick="QuitarItSeleccionado">Ocultar</button>
<button class="btn-ghost" @onclick="PintarItFiltro">Ver filtro</button>
<button class="btn-ghost"
disabled="@(!PuedePintarItinerariosFiltrados)"
@onclick="PintarItFiltro">Ver filtro</button>
<button class="btn-ghost" @onclick="QuitarTodosItinerarios">Ocultar Todos</button>
@@ -344,29 +346,38 @@
<div class="popup-parada-body">
<div><b>Latitud:</b> @_inspectorLat.ToString("0.000000")</div>
<div><b>Longitud:</b> @_inspectorLon.ToString("0.000000")</div>
@if (!string.IsNullOrWhiteSpace(_inspectorCallejero))
{
<div><b>Callejero:</b> @_inspectorCallejero</div>
}
<div>
<b>Altitud (O):</b>
@(_inspectorElev is null
? "-"
: _inspectorElev.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
</div>
</div>
<div>
<b>Altitud (G):</b>
@(_inspectorElevIdee is null
? "-"
: _inspectorElevIdee.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
</div>
</div>
</div>
<button class="popup-parada-cerrar" @onclick="() => _popupInspectorVisible = false">×</button>
@if (_inspectorLugaresCercanos.Count > 0)
{
<div class="inspector-address-list">
@for (var indiceLugar = 0; indiceLugar < _inspectorLugaresCercanos.Count; indiceLugar++)
{
var lugarCercano = _inspectorLugaresCercanos[indiceLugar];
<div class="inspector-address-item">
<span class="inspector-address-number">@(indiceLugar + 1)</span>
<span>@FormatearLugarCallejeroResumen(lugarCercano)</span>
</div>
}
</div>
}
</div>
<button class="popup-parada-cerrar" @onclick="CerrarInspectorAsync">×</button>
</div>
}
}
</div>
}
@@ -427,6 +438,11 @@
}
else
{
<div class="muted" style="font-size:0.72rem; margin:0 0 .35rem;">
<b>Umbral de subida (Esf.):</b>
@NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta).ToString("0.#", CultureInfo.InvariantCulture) %
</div>
<table class="popup-elev-table" style="
width:100%;
font-size:0.8rem;
@@ -545,10 +561,12 @@
double totalDist = 0;
double totalEsfuerzoPositivo = 0;
double totalEsfuerzo = 0;
double totalDistPendPositiva = 0;
double totalAlturaPendPositiva = 0;
var totalesPendientePositiva = CalcularTotalesPendientePositivaSignificativa(s);
double totalDistPendPositiva = totalesPendientePositiva.DistanciaMetros;
double totalAlturaPendPositiva = totalesPendientePositiva.AlturaMetros;
var hayEsfuerzoPositivo = false;
var hayEsfuerzo = false;
var umbralPendientePositiva = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta);
for (int i = 1; i < s.Count; i++)
{
var anterior = s[i - 1];
@@ -560,12 +578,6 @@
if (distancia > 0 && anterior.ElevacionIdee is not null && actual.ElevacionIdee is not null)
{
pendiente = (actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value) / distancia * 100.0;
if (pendiente > 0)
{
totalDistPendPositiva += distancia;
totalAlturaPendPositiva += Math.Max(0, actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value);
}
}
var esfuerzo = CalcularEsfuerzoElevacion(distancia, pendiente);
@@ -574,7 +586,7 @@
totalEsfuerzo += esfuerzo.Value;
hayEsfuerzo = true;
if (pendiente > 0)
if (pendiente > umbralPendientePositiva)
{
totalEsfuerzoPositivo += esfuerzo.Value;
hayEsfuerzoPositivo = true;
@@ -582,32 +594,23 @@
}
}
string FDist(double d)
{
if (d <= 0) return "-";
if (d < 950)
return $"{d:0.00} m";
return $"{(d / 1000.0):0.000} km";
}
<div class="muted" style="font-size:0.78rem; margin-top:.5rem;">
<div><b>O:</b> (inifin) = @F(OIni) → @F(OFin) · (min/max) = @F(OMin) / @F(OMax)</div>
<div><b>G:</b> (inifin) = @F(GIni) → @F(GFin) · (min/max) = @F(GMin) / @F(GMax)</div>
<div><b>O:</b> (inicio → fin) = @F(OIni) → @F(OFin) · (mínimo/máximo) = @F(OMin) / @F(OMax)</div>
<div><b>G:</b> (inicio → fin) = @F(GIni) → @F(GFin) · (mínimo/máximo) = @F(GMin) / @F(GMax)</div>
<div style="margin-top:.25rem;">
<b>Suma distancia de puntos:</b> @FDist(totalDist)
<b>Suma de distancias entre puntos:</b> @FormatearDistanciaResumenElevacion(totalDist)
</div>
<div>
<b>Suma dist pend positiva:</b> @FDist(totalDistPendPositiva)
<b>Suma de distancias con pendiente positiva:</b> @FormatearDistanciaResumenElevacion(totalDistPendPositiva)
</div>
<div>
<b>Suma alturas pend positiva:</b> @(totalAlturaPendPositiva > 0
? totalAlturaPendPositiva.ToString("0.00", CultureInfo.InvariantCulture) + " m (" + (totalAlturaPendPositiva / 3.0).ToString("0.0", CultureInfo.InvariantCulture) + " pisos)"
<b>Suma de alturas con pendiente positiva:</b> @(totalAlturaPendPositiva > 0
? totalAlturaPendPositiva.ToString("0.00", CultureInfo.InvariantCulture) + " m (" + (totalAlturaPendPositiva / MetrosPorPiso).ToString("0.0", CultureInfo.InvariantCulture) + " pisos)"
: "-")
</div>
<div>
<b>Suma de esfuerzos positivos:</b> @(hayEsfuerzoPositivo ? totalEsfuerzoPositivo.ToString("0.00", CultureInfo.InvariantCulture) : "-")
<span style="margin-left:.5rem;"><b>total:</b> @(hayEsfuerzo ? totalEsfuerzo.ToString("0.00", CultureInfo.InvariantCulture) : "-")</span>
<span style="margin-left:.5rem;"><b>Total:</b> @(hayEsfuerzo ? totalEsfuerzo.ToString("0.00", CultureInfo.InvariantCulture) : "-")</span>
</div>
</div>
}
@@ -670,33 +673,74 @@
<span class="route-elevation-switch-thumb"></span>
</span>
<span class="route-elevation-switch-text">
@(_pintandoElevacionesRuta ? "Elevaciones..." : "Elevaciones")
Visualizar Pendientes
</span>
</label>
<div class="route-elevation-thresholds" title="Porcentaje minimo para colorear subida o bajada">
<label class="route-elevation-threshold">
<span>Sube</span>
<input type="number"
min="0"
max="50"
step="0.5"
value="@_umbralPendienteSubidaRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, true))" />
<span>%</span>
</label>
<label class="route-elevation-threshold">
<span>Baja</span>
<input type="number"
min="0"
max="50"
step="0.5"
value="@_umbralPendienteBajadaRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, false))" />
<span>%</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>
@@ -768,6 +812,20 @@
{
var alt = _opcionesRuta[_indiceAlternativaSeleccionada];
@if (MostrarAvisoPendientesPositivasSignificativas)
{
<div class="route-slope-warning" role="status">
<div>Solución con pendientes positivas significativas.</div>
<div style="margin-top:.2rem; padding-left:.55rem;">
<div>- Distancia con pendiente: @FormatearDistanciaResumenElevacion(_distanciaPendientePositivaSignificativaRutaMetros)</div>
<div>
- Altura subida: @_alturaPendientePositivaSignificativaRutaMetros.ToString("0.00", CultureInfo.InvariantCulture) m
(@((_alturaPendientePositivaSignificativaRutaMetros / MetrosPorPiso).ToString("0.0", CultureInfo.InvariantCulture)) pisos)
</div>
</div>
</div>
}
@* ========= RESUMEN BONITO ARRIBA (único) ========= *@
<p class="route-main-text">
@if (alt.EsSoloAPie)
@@ -928,6 +986,7 @@
: _duracionTotalSegundos;
var dRecta = DistanciaLineaRectaOrigenDestino();
var dCorregida = CalcularDistanciaCorregidaAlternativa(altSeleccionada);
var dEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(altSeleccionada);
var xyzOrigen = FormatearResumenXYZOrigen();
var xyzDestino = FormatearResumenXYZDestino();
var callejeroOrigen = FormatearResumenCallejeroOrigen();
@@ -948,7 +1007,7 @@
</p>
}
@if (dRecta > 0 || dCorregida > 0)
@if (dRecta > 0 || dCorregida > 0 || dEquilibradaAndando > 0)
{
<div class="muted" style="font-size:0.75rem;margin-top:4px;">
@if (dRecta > 0)
@@ -959,6 +1018,10 @@
{
<div>d.Corregida a pie: @FormatearDistanciaDetalleRuta(dCorregida)</div>
}
@if (dEquilibradaAndando > 0)
{
<div>d.Equilib. A: @FormatearDistanciaDetalleRuta(dEquilibradaAndando)</div>
}
@if (!string.IsNullOrWhiteSpace(scoreEquilibradoSeleccionado))
{
<div>Score equilibrado: @scoreEquilibradoSeleccionado</div>
@@ -1177,6 +1240,22 @@
<button type="button" class="config-close-btn" @onclick="ToggleConfigPanel">×</button>
</div>
<div class="config-row config-row-full">
<div class="modelos-horarios">
<button type="button"
class="modelos-horarios__toggle"
@onclick="AbrirModelosHorariosPanel">
<span class="modelos-horarios__toggle-label">Modelo:</span>
<span class="modelos-horarios__activo">
@FechaModeloHorarioActivo
</span>
<span class="modelos-horarios__toggle-action">
Gestionar
</span>
</button>
</div>
</div>
@* <div class="config-row config-row-full">
<label>Selección de paradas (rápida):</label>
@@ -1254,22 +1333,6 @@
</div>
</div>
<div class="config-row config-row-full">
<div class="modelos-horarios">
<button type="button"
class="modelos-horarios__toggle"
@onclick="AbrirModelosHorariosPanel">
<span class="modelos-horarios__toggle-label">Modelo DBUS</span>
<span class="modelos-horarios__activo">
@(ModeloHorarioActivo?.NombreArchivo ?? "Sin modelo cargado")
</span>
<span class="modelos-horarios__toggle-action">
Gestionar
</span>
</button>
</div>
</div>
@* =========================
RANGO DE LÍNEAS
========================= *@
@@ -1470,13 +1533,10 @@
<span>Factor</span>
<input type="number" class="config-input" step="0.1" min="0"
placeholder="12"
title="1 metro andando pesa como este numero de metros en bus"
title="Factor del modo equilibrado"
@bind="FactorEquilibradoAndarBusTexto" @bind:event="oninput"
@onblur="OnBlurFactorEquilibradoAndarBus" />
</label>
<span class="muted config-equilibrado-help" title="Formula: metros en bus + metros andando x factor">
1 m andando = @FactorEquilibradoAndarBusResumen m bus
</span>
</div>
</div>