- Integrado el esfuerzo extra por pendiente en la distancia corregida a pie.
- Aplicada la distancia corregida al cálculo de d.Equilib. A, score y ordenación de alternativas. - Conservadas las distancias reales en las tarjetas y descripciones de las rutas. - Añadido el recálculo del ranking al modificar el umbral o los coeficientes de pendientes. - Simplificado el cálculo de elevaciones para utilizar únicamente la fuente G (IDEE). - Añadida la suma de esfuerzo extra al detalle de puntos de elevación. - Reorganizada la cabecera del modal de elevación y compactado su resumen. - Eliminado el factor x12 de la representación visual del score. - Mejorado el aviso de pendientes positivas significativas. - Corregidos los tooltips del punto Z para mostrarse por encima de los marcadores. - Agrupados el interruptor de visualización de pendientes y su botón de configuración. - Personalizadas las barras de desplazamiento de modales y paneles. - Priorizado C:\ProgramData\RutasDBUS para compartir historial, modelos externos y modelo activo entre distintas publicaciones IIS.
This commit is contained in:
@@ -255,7 +255,6 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
|
||||
|
||||
private bool _popupInspectorVisible = false;
|
||||
private double _inspectorLat, _inspectorLon;
|
||||
private double? _inspectorElev;
|
||||
private double? _inspectorElevIdee;
|
||||
private IReadOnlyList<LugarGeocodificadoCercano> _inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
|
||||
private int _versionInspector;
|
||||
|
||||
@@ -274,6 +274,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
{
|
||||
SincronizarTextosPenalizacionPie();
|
||||
await PersistirConfiguracionAsync();
|
||||
await RecalcularRankingTrasCambioEsfuerzoPendienteAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1209,12 +1210,16 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
|
||||
var parametros = CrearParametrosPenalizacionActuales();
|
||||
|
||||
if (alternativa.EsSoloAPie)
|
||||
return UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaSoloAPie, parametros);
|
||||
var distanciaCorregidaBase = alternativa.EsSoloAPie
|
||||
? UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaSoloAPie, parametros)
|
||||
: UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieInicioAprox, parametros)
|
||||
+ UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieTransbordosAprox, parametros)
|
||||
+ UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieFinAprox, parametros);
|
||||
var esfuerzoExtra = double.IsFinite(alternativa.EsfuerzoExtraPendienteMetros)
|
||||
? Math.Max(0, alternativa.EsfuerzoExtraPendienteMetros)
|
||||
: 0;
|
||||
|
||||
return UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieInicioAprox, parametros)
|
||||
+ UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieTransbordosAprox, parametros)
|
||||
+ UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieFinAprox, parametros);
|
||||
return distanciaCorregidaBase + esfuerzoExtra;
|
||||
}
|
||||
|
||||
private double CalcularCosteAndarEquilibradoSegundos(AlternativaRuta? alternativa)
|
||||
@@ -1237,9 +1242,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
if (alternativa is null)
|
||||
return 0;
|
||||
|
||||
var distanciaAndando = alternativa.EsSoloAPie
|
||||
? alternativa.DistanciaSoloAPie
|
||||
: alternativa.DistanciaPieTotalAprox;
|
||||
var distanciaAndando = CalcularDistanciaCorregidaAlternativa(alternativa);
|
||||
var factor = double.IsFinite(_factorEquilibradoAndarBus)
|
||||
? Math.Max(0, _factorEquilibradoAndarBus)
|
||||
: 12.0;
|
||||
@@ -1269,7 +1272,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
if (!double.IsFinite(score))
|
||||
return string.Empty;
|
||||
|
||||
return $"Score {FormatearDistanciaTarjeta(score)} (x{_factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture)})";
|
||||
return $"Score {FormatearDistanciaTarjeta(score)}";
|
||||
}
|
||||
|
||||
private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync()
|
||||
|
||||
@@ -1204,7 +1204,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
private bool _elevPopupMinimizado = false;
|
||||
|
||||
|
||||
private const string _versionApp = "(v-20260720c)";
|
||||
private const string _versionApp = "(v-20260721a)";
|
||||
|
||||
|
||||
private double? _paradaElevG;
|
||||
|
||||
@@ -105,7 +105,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
|
||||
/// <summary>
|
||||
/// Actualiza las alturas (Z) de origen y destino para el resumen del detalle de ruta.
|
||||
/// Prioriza IDEE y usa OpenElevation como respaldo.
|
||||
/// Usa la fuente G (IDEE).
|
||||
/// </summary>
|
||||
private async Task ActualizarAlturasResumenRutaAsync(CancellationToken ct)
|
||||
{
|
||||
@@ -137,20 +137,6 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
? gDst
|
||||
: (double?)null;
|
||||
|
||||
var faltanOpen = new List<(double lat, double lon)>(2);
|
||||
if (zOrigen is null && !_elevCache.ContainsKey(ElevKey(origen.Item1, origen.Item2)))
|
||||
faltanOpen.Add(origen);
|
||||
if (zDestino is null && !_elevCache.ContainsKey(ElevKey(destino.Item1, destino.Item2)))
|
||||
faltanOpen.Add(destino);
|
||||
|
||||
if (faltanOpen.Count > 0)
|
||||
await ObtenerElevacionesPorLoteAsync(faltanOpen, CancellationToken.None);
|
||||
|
||||
if (zOrigen is null && _elevCache.TryGetValue(ElevKey(origen.Item1, origen.Item2), out var oOri))
|
||||
zOrigen = oOri;
|
||||
if (zDestino is null && _elevCache.TryGetValue(ElevKey(destino.Item1, destino.Item2), out var oDst))
|
||||
zDestino = oDst;
|
||||
|
||||
_altitudOrigenResumenRuta = zOrigen;
|
||||
_altitudDestinoResumenRuta = zDestino;
|
||||
}
|
||||
@@ -306,26 +292,11 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
|
||||
|
||||
|
||||
private static readonly ConcurrentDictionary<string, double> _elevCache =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Ajustes (rendimiento)
|
||||
/* private const int ELEV_MAX_PUNTOS_POR_TRAMO = 24; */
|
||||
private int _elevMaxPuntosPorTramo = 999; // configurable
|
||||
private const int ELEV_OPEN_TIMEOUT_MS = 1500;
|
||||
// muestreo por tramo
|
||||
private const int ELEV_BATCH_MAX_PUNTOS = 90; // por request (prudente)
|
||||
private const int ELEV_DECIMALES_CACHE = 6; // redondeo coord para cache
|
||||
|
||||
/// <summary>
|
||||
/// Gestiona clave.
|
||||
/// </summary>
|
||||
private static string ElevKey(double lat, double lon)
|
||||
{
|
||||
double rLat = Math.Round(lat, ELEV_DECIMALES_CACHE);
|
||||
double rLon = Math.Round(lon, ELEV_DECIMALES_CACHE);
|
||||
return $"{rLat.ToString("0.000000", CultureInfo.InvariantCulture)},{rLon.ToString("0.000000", CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
|
||||
private static List<(double lat, double lon)> SampleLine(double[,] line, int maxPoints)
|
||||
{
|
||||
@@ -385,43 +356,92 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
await ObtenerElevacionesIdeePorLoteAsync(missesIdee.Skip(i).Take(ELEV_BATCH_MAX_PUNTOS).ToList(), ct);
|
||||
}
|
||||
|
||||
// --- OpenElevation: best-effort y con timeout corto ---
|
||||
var missesOpen = new List<(double lat, double lon)>();
|
||||
foreach (var (lat, lon) in sampled)
|
||||
{
|
||||
var k = ElevKey(lat, lon);
|
||||
if (!_elevCache.ContainsKey(k))
|
||||
missesOpen.Add((Math.Round(lat, ELEV_DECIMALES_CACHE), Math.Round(lon, ELEV_DECIMALES_CACHE)));
|
||||
}
|
||||
return ConstruirMuestrasElevacionDesdeCache(line);
|
||||
}
|
||||
|
||||
for (int i = 0; i < missesOpen.Count; i += ELEV_BATCH_MAX_PUNTOS)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
await ObtenerElevacionesPorLoteAsync(missesOpen.Skip(i).Take(ELEV_BATCH_MAX_PUNTOS).ToList(), ct);
|
||||
}
|
||||
|
||||
// --- montar resultado (UNA sola vez) ---
|
||||
private List<MuestraElevacion> ConstruirMuestrasElevacionDesdeCache(double[,] line)
|
||||
{
|
||||
var sampled = SampleLine(line, _elevMaxPuntosPorTramo);
|
||||
var res = new List<MuestraElevacion>(sampled.Count);
|
||||
for (int i = 0; i < sampled.Count; i++)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var (lat, lon) = sampled[i];
|
||||
|
||||
var elevOpen = _elevCache.TryGetValue(ElevKey(lat, lon), out var e1) ? e1 : (double?)null;
|
||||
var elevIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var e2) ? e2 : (double?)null;
|
||||
|
||||
|
||||
res.Add(new MuestraElevacion
|
||||
{
|
||||
Indice = i,
|
||||
Latitud = lat,
|
||||
Longitud = lon,
|
||||
Elevacion = elevOpen,
|
||||
ElevacionIdee = elevIdee
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private async Task CalcularEsfuerzoExtraPendienteAlternativasAsync(
|
||||
IReadOnlyList<AlternativaRuta> alternativas,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var lineasPorAlternativa = alternativas
|
||||
.Select(alternativa => new
|
||||
{
|
||||
Alternativa = alternativa,
|
||||
Lineas = (alternativa.EsSoloAPie
|
||||
? alternativa.LineaSoloAPie is { } lineaSolo
|
||||
? new[] { lineaSolo }
|
||||
: Enumerable.Empty<double[,]>()
|
||||
: alternativa.LineasPieCalculoElevacion)
|
||||
.Where(linea => linea.GetLength(0) >= 2)
|
||||
.ToList()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var puntosPendientes = new Dictionary<string, (double lat, double lon)>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var item in lineasPorAlternativa)
|
||||
{
|
||||
foreach (var linea in item.Lineas)
|
||||
{
|
||||
foreach (var (lat, lon) in SampleLine(linea, _elevMaxPuntosPorTramo))
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var clave = ClaveElevacionIdee(lat, lon);
|
||||
if (!_elevIdeeCache.ContainsKey(clave))
|
||||
{
|
||||
puntosPendientes.TryAdd(
|
||||
clave,
|
||||
(Math.Round(lat, ELEV_IDEE_DECIMALES_CACHE), Math.Round(lon, ELEV_IDEE_DECIMALES_CACHE)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var puntos = puntosPendientes.Values.ToList();
|
||||
for (var i = 0; i < puntos.Count; i += ELEV_BATCH_MAX_PUNTOS)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
await ObtenerElevacionesIdeePorLoteAsync(
|
||||
puntos.Skip(i).Take(ELEV_BATCH_MAX_PUNTOS).ToList(),
|
||||
ct);
|
||||
}
|
||||
|
||||
foreach (var item in lineasPorAlternativa)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var esfuerzoExtra = 0.0;
|
||||
foreach (var linea in item.Lineas)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var muestras = ConstruirMuestrasElevacionDesdeCache(linea);
|
||||
esfuerzoExtra += CalcularResumenEsfuerzoElevacion(muestras).EsfuerzoExtraMetros;
|
||||
}
|
||||
|
||||
item.Alternativa.EsfuerzoExtraPendienteMetros = double.IsFinite(esfuerzoExtra)
|
||||
? Math.Max(0, esfuerzoExtra)
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gestiona tramo bus es basura.
|
||||
@@ -539,64 +559,6 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
|
||||
|
||||
|
||||
private sealed class RespuestaOpenElevation
|
||||
{
|
||||
public List<ResultadoOpenElevation>? results { get; set; }
|
||||
}
|
||||
private sealed class ResultadoOpenElevation
|
||||
{
|
||||
public double latitude { get; set; }
|
||||
public double longitude { get; set; }
|
||||
public double elevation { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recupera elevations el lote.
|
||||
/// </summary>
|
||||
private async Task ObtenerElevacionesPorLoteAsync(List<(double lat, double lon)> points, CancellationToken ct)
|
||||
{
|
||||
if (points.Count == 0) return;
|
||||
|
||||
// locations=lat,lon|lat,lon|...
|
||||
var parts = new List<string>(points.Count);
|
||||
foreach (var (lat, lon) in points)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
parts.Add($"{lat.ToString(CultureInfo.InvariantCulture)},{lon.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
string url = "https://openelevation.tecnosis.net/api/v1/lookup?locations=" + string.Join("|", parts);
|
||||
|
||||
try
|
||||
{
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
using var ctsTimeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
ctsTimeout.CancelAfter(ELEV_OPEN_TIMEOUT_MS);
|
||||
|
||||
using var resp = await Http.SendAsync(req, ctsTimeout.Token);
|
||||
if (!resp.IsSuccessStatusCode) return;
|
||||
|
||||
var json = await resp.Content.ReadAsStringAsync(ctsTimeout.Token);
|
||||
|
||||
var data = JsonSerializer.Deserialize<RespuestaOpenElevation>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
if (data?.results is null || data.results.Count == 0) return;
|
||||
|
||||
foreach (var r in data.results)
|
||||
{
|
||||
var k = ElevKey(r.latitude, r.longitude);
|
||||
_elevCache[k] = r.elevation;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// silencioso: elevación es informativa
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gestiona geometria completa variante.
|
||||
/// </summary>
|
||||
@@ -1271,7 +1233,6 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
var versionInspector = ++_versionInspector;
|
||||
_inspectorLat = lat;
|
||||
_inspectorLon = lon;
|
||||
_inspectorElev = null;
|
||||
_inspectorElevIdee = null;
|
||||
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
|
||||
_popupInspectorVisible = true;
|
||||
@@ -1286,29 +1247,9 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
maxResultados: 3,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
double? elevacionOpen = null;
|
||||
double? elevacionIdee = null;
|
||||
try
|
||||
{
|
||||
// -------------------------
|
||||
// OpenElevation (cache primero)
|
||||
// -------------------------
|
||||
var kOpen = ElevKey(lat, lon);
|
||||
if (_elevCache.TryGetValue(kOpen, out var cachedOpen))
|
||||
{
|
||||
elevacionOpen = cachedOpen;
|
||||
}
|
||||
else
|
||||
{
|
||||
await ObtenerElevacionesPorLoteAsync(
|
||||
new List<(double lat, double lon)> { (lat, lon) },
|
||||
CancellationToken.None);
|
||||
|
||||
elevacionOpen = _elevCache.TryGetValue(ElevKey(lat, lon), out var elevOpen)
|
||||
? elevOpen
|
||||
: (double?)null;
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// IDEE (cache primero)
|
||||
// -------------------------
|
||||
@@ -1351,7 +1292,6 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
if (versionInspector != _versionInspector)
|
||||
return;
|
||||
|
||||
_inspectorElev = elevacionOpen;
|
||||
_inspectorElevIdee = elevacionIdee;
|
||||
await ActualizarMarcadoresInspectorJsAsync();
|
||||
StateHasChanged();
|
||||
@@ -1701,6 +1641,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
|
||||
_opcionesRuta.Clear();
|
||||
_opcionesRuta.AddRange(filtradas);
|
||||
await CalcularEsfuerzoExtraPendienteAlternativasAsync(_opcionesRuta, ct);
|
||||
OrdenarOpcionesRutaSegunCriterio();
|
||||
|
||||
if (!resultado.Ok || _opcionesRuta.Count == 0)
|
||||
|
||||
@@ -18,6 +18,19 @@ namespace RutasDBUS.Components.Pages;
|
||||
|
||||
public partial class PlanificadorRutas : ComponentBase
|
||||
{
|
||||
private readonly record struct ResumenEsfuerzoElevacion(
|
||||
double DistanciaTotalMetros,
|
||||
double DistanciaPendientePositivaMetros,
|
||||
double AlturaPendientePositivaMetros,
|
||||
double EsfuerzoPositivo,
|
||||
double EsfuerzoTotal,
|
||||
bool HayEsfuerzoPositivo,
|
||||
bool HayEsfuerzo)
|
||||
{
|
||||
public double EsfuerzoExtraMetros =>
|
||||
Math.Max(0, EsfuerzoPositivo - DistanciaPendientePositivaMetros);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asegura que elevacion tramo este disponible.
|
||||
/// </summary>
|
||||
@@ -40,20 +53,19 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
{
|
||||
var ctNone = CancellationToken.None;
|
||||
|
||||
// 1) Samples bajo demanda, priorizando G y dejando O como best-effort
|
||||
// Samples bajo demanda usando la fuente G.
|
||||
var samples = await ConstruirMuestrasElevacionAsync(tramo.Linea, ctNone);
|
||||
tramo.MuestrasElevacion = samples;
|
||||
|
||||
// 2) Resumen O solo con lo que ya haya llegado, sin bloquear más la UI
|
||||
var muestrasOpen = samples
|
||||
.Where(x => x.Elevacion is not null)
|
||||
.Select(x => x.Elevacion!.Value)
|
||||
var muestrasIdee = samples
|
||||
.Where(x => x.ElevacionIdee is not null)
|
||||
.Select(x => x.ElevacionIdee!.Value)
|
||||
.ToList();
|
||||
|
||||
tramo.ElevInicio = samples.FirstOrDefault()?.Elevacion;
|
||||
tramo.ElevFin = samples.LastOrDefault()?.Elevacion;
|
||||
tramo.ElevMin = muestrasOpen.Count > 0 ? muestrasOpen.Min() : null;
|
||||
tramo.ElevMax = muestrasOpen.Count > 0 ? muestrasOpen.Max() : null;
|
||||
tramo.ElevInicio = samples.FirstOrDefault()?.ElevacionIdee;
|
||||
tramo.ElevFin = samples.LastOrDefault()?.ElevacionIdee;
|
||||
tramo.ElevMin = muestrasIdee.Count > 0 ? muestrasIdee.Min() : null;
|
||||
tramo.ElevMax = muestrasIdee.Count > 0 ? muestrasIdee.Max() : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -95,12 +107,54 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
|
||||
await PersistirConfiguracionAsync();
|
||||
|
||||
if (_mostrarElevacionesRuta)
|
||||
if (esSubida)
|
||||
{
|
||||
await RecalcularRankingTrasCambioEsfuerzoPendienteAsync();
|
||||
}
|
||||
else if (_mostrarElevacionesRuta)
|
||||
await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando pendientes...");
|
||||
else
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task RecalcularRankingTrasCambioEsfuerzoPendienteAsync()
|
||||
{
|
||||
if (_opcionesRuta.Count == 0)
|
||||
{
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
var opcionSeleccionadaAnterior = _indiceAlternativaSeleccionada >= 0 &&
|
||||
_indiceAlternativaSeleccionada < _opcionesRuta.Count
|
||||
? _opcionesRuta[_indiceAlternativaSeleccionada]
|
||||
: null;
|
||||
|
||||
await CalcularEsfuerzoExtraPendienteAlternativasAsync(_opcionesRuta, CancellationToken.None);
|
||||
OrdenarOpcionesRutaSegunCriterio();
|
||||
|
||||
var nuevoIndice = CriterioPrioritarioTiempoActivo && opcionSeleccionadaAnterior is not null
|
||||
? _opcionesRuta.IndexOf(opcionSeleccionadaAnterior)
|
||||
: BuscarIndicePreferidoPorCriterio(_criterioPrioritarioRuta);
|
||||
if (nuevoIndice < 0 || nuevoIndice >= _opcionesRuta.Count)
|
||||
nuevoIndice = 0;
|
||||
|
||||
var cambioSeleccion = _indiceAlternativaSeleccionada != nuevoIndice ||
|
||||
!ReferenceEquals(opcionSeleccionadaAnterior, _opcionesRuta[nuevoIndice]);
|
||||
_indiceAlternativaSeleccionada = nuevoIndice;
|
||||
|
||||
if (_mostrarElevacionesRuta || cambioSeleccion)
|
||||
{
|
||||
await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta
|
||||
? "Aplicando pendientes..."
|
||||
: "Representando ruta seleccionada...");
|
||||
}
|
||||
else
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CambiarUmbralPisosPendienteRutaAsync(ChangeEventArgs e)
|
||||
{
|
||||
_umbralPisosPendienteRuta = ParseUmbralPisosPendienteRuta(
|
||||
@@ -153,7 +207,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
|
||||
private static double NormalizarUmbralPendienteRuta(double valor)
|
||||
=> double.IsFinite(valor)
|
||||
? Math.Clamp(Math.Abs(valor), 0.0, 50.0)
|
||||
? Math.Clamp(Math.Abs(valor), 0.0, 100.0)
|
||||
: UmbralPendienteSubidaRutaPorDefecto;
|
||||
|
||||
private static double ParseUmbralPisosPendienteRuta(object? valor, double actual)
|
||||
@@ -211,7 +265,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
{
|
||||
if (!_mostrarElevacionesRuta || linea.GetLength(0) < 2)
|
||||
return (null, 0.0, 0.0);
|
||||
|
||||
|
||||
var muestras = await ConstruirMuestrasElevacionAsync(linea, ct);
|
||||
if (muestras.Count < 2)
|
||||
return (null, 0.0, 0.0);
|
||||
@@ -274,39 +328,68 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
|
||||
private (double DistanciaMetros, double AlturaMetros)
|
||||
CalcularTotalesPendientePositivaSignificativa(IReadOnlyList<MuestraElevacion>? muestras)
|
||||
{
|
||||
var resumen = CalcularResumenEsfuerzoElevacion(muestras);
|
||||
return (resumen.DistanciaPendientePositivaMetros, resumen.AlturaPendientePositivaMetros);
|
||||
}
|
||||
|
||||
private ResumenEsfuerzoElevacion CalcularResumenEsfuerzoElevacion(
|
||||
IReadOnlyList<MuestraElevacion>? muestras)
|
||||
{
|
||||
if (muestras is null || muestras.Count < 2)
|
||||
return (0.0, 0.0);
|
||||
return default;
|
||||
|
||||
var distanciaTotal = 0.0;
|
||||
var alturaTotal = 0.0;
|
||||
var distanciaPendientePositiva = 0.0;
|
||||
var alturaPendientePositiva = 0.0;
|
||||
var esfuerzoPositivo = 0.0;
|
||||
var esfuerzoTotal = 0.0;
|
||||
var hayEsfuerzoPositivo = false;
|
||||
var hayEsfuerzo = false;
|
||||
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)
|
||||
if (!double.IsFinite(distancia) || distancia <= 0)
|
||||
continue;
|
||||
|
||||
distanciaTotal += distancia;
|
||||
if (anterior.ElevacionIdee is null || actual.ElevacionIdee is null)
|
||||
continue;
|
||||
|
||||
var diferenciaAltura = actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value;
|
||||
var pendiente = diferenciaAltura / distancia * 100.0;
|
||||
if (pendiente <= umbralSubida)
|
||||
var esfuerzo = CalcularEsfuerzoElevacion(distancia, pendiente);
|
||||
if (esfuerzo is null)
|
||||
continue;
|
||||
|
||||
distanciaTotal += distancia;
|
||||
alturaTotal += Math.Max(0, diferenciaAltura);
|
||||
esfuerzoTotal += esfuerzo.Value;
|
||||
hayEsfuerzo = true;
|
||||
|
||||
if (pendiente < umbralSubida)
|
||||
continue;
|
||||
|
||||
distanciaPendientePositiva += distancia;
|
||||
alturaPendientePositiva += Math.Max(0, diferenciaAltura);
|
||||
esfuerzoPositivo += esfuerzo.Value;
|
||||
hayEsfuerzoPositivo = true;
|
||||
}
|
||||
|
||||
return (distanciaTotal, alturaTotal);
|
||||
return new ResumenEsfuerzoElevacion(
|
||||
distanciaTotal,
|
||||
distanciaPendientePositiva,
|
||||
alturaPendientePositiva,
|
||||
esfuerzoPositivo,
|
||||
esfuerzoTotal,
|
||||
hayEsfuerzoPositivo,
|
||||
hayEsfuerzo);
|
||||
}
|
||||
|
||||
private static string FormatearDistanciaResumenElevacion(double distanciaMetros)
|
||||
@@ -327,7 +410,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
}
|
||||
|
||||
private static double? ElevacionPreferidaMapa(MuestraElevacion muestra)
|
||||
=> muestra.ElevacionIdee ?? muestra.Elevacion;
|
||||
=> muestra.ElevacionIdee;
|
||||
|
||||
private (string Categoria, string Color) ClasificarPendienteMapa(double? pendientePorcentaje)
|
||||
{
|
||||
|
||||
@@ -346,13 +346,6 @@
|
||||
<div class="popup-parada-body">
|
||||
<div><b>Latitud:</b> @_inspectorLat.ToString("0.000000")</div>
|
||||
<div><b>Longitud:</b> @_inspectorLon.ToString("0.000000")</div>
|
||||
<div>
|
||||
<b>Altitud (O):</b>
|
||||
@(_inspectorElev is null
|
||||
? "-"
|
||||
: _inspectorElev.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<b>Altitud (G):</b>
|
||||
@(_inspectorElevIdee is null
|
||||
@@ -404,7 +397,11 @@
|
||||
|
||||
<div class="popup-parada-header popup-elev-header draggable-popup-handle">
|
||||
<span class="popup-elev-title">
|
||||
Puntos de elevación · @_tramoElevDetalle.Tipo
|
||||
<span class="popup-elev-title-main">Puntos de elevación</span>
|
||||
<span class="popup-elev-title-threshold">
|
||||
Umbral de subida (Esf.):
|
||||
@NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta).ToString("0.#", CultureInfo.InvariantCulture) %
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="popup-elev-actions">
|
||||
@@ -438,11 +435,6 @@
|
||||
}
|
||||
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;
|
||||
@@ -542,13 +534,6 @@
|
||||
{
|
||||
var s = _tramoElevDetalle.MuestrasElevacion ?? new List<MuestraElevacion>();
|
||||
|
||||
// --- resumen altitudes ---
|
||||
double? OIni = s.Count > 0 ? s[0].Elevacion : null;
|
||||
double? OFin = s.Count > 0 ? s[^1].Elevacion : null;
|
||||
double? OMin = s.Where(x => x.Elevacion is not null).Select(x => x.Elevacion!.Value).DefaultIfEmpty().Min();
|
||||
double? OMax = s.Where(x => x.Elevacion is not null).Select(x => x.Elevacion!.Value).DefaultIfEmpty().Max();
|
||||
if (!s.Any(x => x.Elevacion is not null)) { OMin = null; OMax = null; }
|
||||
|
||||
double? GIni = s.Count > 0 ? s[0].ElevacionIdee : null;
|
||||
double? GFin = s.Count > 0 ? s[^1].ElevacionIdee : null;
|
||||
double? GMin = s.Where(x => x.ElevacionIdee is not null).Select(x => x.ElevacionIdee!.Value).DefaultIfEmpty().Min();
|
||||
@@ -556,61 +541,27 @@
|
||||
if (!s.Any(x => x.ElevacionIdee is not null)) { GMin = null; GMax = null; }
|
||||
|
||||
string F(double? v) => v is null ? "-" : v.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m";
|
||||
|
||||
// --- suma distancias entre puntos ---
|
||||
double totalDist = 0;
|
||||
double totalEsfuerzoPositivo = 0;
|
||||
double totalEsfuerzo = 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];
|
||||
var actual = s[i];
|
||||
var distancia = CalcularDistanciaMetros(anterior.Latitud, anterior.Longitud, actual.Latitud, actual.Longitud);
|
||||
totalDist += distancia;
|
||||
|
||||
double? pendiente = null;
|
||||
if (distancia > 0 && anterior.ElevacionIdee is not null && actual.ElevacionIdee is not null)
|
||||
{
|
||||
pendiente = (actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value) / distancia * 100.0;
|
||||
}
|
||||
|
||||
var esfuerzo = CalcularEsfuerzoElevacion(distancia, pendiente);
|
||||
if (esfuerzo is not null)
|
||||
{
|
||||
totalEsfuerzo += esfuerzo.Value;
|
||||
hayEsfuerzo = true;
|
||||
|
||||
if (pendiente > umbralPendientePositiva)
|
||||
{
|
||||
totalEsfuerzoPositivo += esfuerzo.Value;
|
||||
hayEsfuerzoPositivo = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
var resumenEsfuerzo = CalcularResumenEsfuerzoElevacion(s);
|
||||
|
||||
<div class="muted" style="font-size:0.78rem; margin-top:.5rem;">
|
||||
<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><b>G:</b> Ini/Fin: @F(GIni) / @F(GFin) · Min/Max: @F(GMin) / @F(GMax)</div>
|
||||
<div style="margin-top:.25rem;">
|
||||
<b>Suma de distancias entre puntos:</b> @FormatearDistanciaResumenElevacion(totalDist)
|
||||
<b>Suma de distancias entre puntos:</b> @FormatearDistanciaResumenElevacion(resumenEsfuerzo.DistanciaTotalMetros)
|
||||
</div>
|
||||
<div>
|
||||
<b>Suma de distancias con pendiente positiva:</b> @FormatearDistanciaResumenElevacion(totalDistPendPositiva)
|
||||
<b>Suma de distancias con pendiente positiva:</b> @FormatearDistanciaResumenElevacion(resumenEsfuerzo.DistanciaPendientePositivaMetros)
|
||||
</div>
|
||||
<div>
|
||||
<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)"
|
||||
<b>Suma de alturas con pendiente positiva:</b> @(resumenEsfuerzo.AlturaPendientePositivaMetros > 0
|
||||
? resumenEsfuerzo.AlturaPendientePositivaMetros.ToString("0.00", CultureInfo.InvariantCulture) + " m (" + (resumenEsfuerzo.AlturaPendientePositivaMetros / 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>
|
||||
<b>Suma de esfuerzos positivos:</b> @(resumenEsfuerzo.HayEsfuerzoPositivo ? resumenEsfuerzo.EsfuerzoPositivo.ToString("0.00", CultureInfo.InvariantCulture) : "-")
|
||||
<span style="margin-left:.5rem;"><b>Total:</b> @(resumenEsfuerzo.HayEsfuerzo ? resumenEsfuerzo.EsfuerzoTotal.ToString("0.00", CultureInfo.InvariantCulture) : "-")</span>
|
||||
</div>
|
||||
<div>
|
||||
<b>Suma de esfuerzo extra:</b> @(resumenEsfuerzo.HayEsfuerzo ? resumenEsfuerzo.EsfuerzoExtraMetros.ToString("0.00", CultureInfo.InvariantCulture) : "-")
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -815,11 +766,11 @@
|
||||
@if (MostrarAvisoPendientesPositivasSignificativas)
|
||||
{
|
||||
<div class="route-slope-warning" role="status">
|
||||
<div>Solución con pendientes positivas significativas.</div>
|
||||
<div>Pendientes positivas significativas.</div>
|
||||
<div style="margin-top:.2rem; padding-left:.55rem;">
|
||||
<div>- Distancia con pendiente: @FormatearDistanciaResumenElevacion(_distanciaPendientePositivaSignificativaRutaMetros)</div>
|
||||
<div>- Distancia con subidas: @FormatearDistanciaResumenElevacion(_distanciaPendientePositivaSignificativaRutaMetros)</div>
|
||||
<div>
|
||||
- Altura subida: @_alturaPendientePositivaSignificativaRutaMetros.ToString("0.00", CultureInfo.InvariantCulture) m
|
||||
- Subidas acumuladas: @_alturaPendientePositivaSignificativaRutaMetros.ToString("0.00", CultureInfo.InvariantCulture) m
|
||||
(@((_alturaPendientePositivaSignificativaRutaMetros / MetrosPorPiso).ToString("0.0", CultureInfo.InvariantCulture)) pisos)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user