- Integrados los modelos locales de elevación L5, L2 y L1, manteniendo la fuente G para comparación.

- Añadida la selección del método de cálculo de alturas, con L1 como opción predeterminada.
- Mostradas las alturas G, L5, L2 y L1 en puntos Z y paradas, dejando en +info únicamente la fuente seleccionada.
- Añadidos límites de tiempo, caché y control de reintentos para evitar bloqueos de la API de elevaciones.
- Incorporado el filtrado de itinerarios por sentido: ambos, solo ida, solo vuelta o ninguno.
- Conservadas las alternativas exclusivamente a pie cuando los filtros impiden utilizar autobuses.
- Incluidas las soluciones solo a pie dentro del ranking cuando resultan mejores que las alternativas en bus.
- Mejorada la selección de paradas y la eliminación de recorridos circulares redundantes.
- Corregidos los tramos peatonales inexistentes: los recorridos mostrados como 0 m ya no generan pendientes ni penalizaciones en el score.
- Alineado el cálculo final a pie con la parada real utilizada por la solución.
- Eliminada la unidad km del score para mostrarlo correctamente como puntuación.
- Mejorados los textos, umbrales, avisos y presentación de la configuración de pendientes.
- La activación de Usar pendientes ya no modifica el score ni la ruta hasta pulsar Recalcular.
- Limitado el historial personal a 25 registros y añadido control por tamaño.
- Añadida recuperación automática, copia de respaldo y diagnóstico para historiales locales dañados o demasiado grandes.
- Numerado también el historial personal e indicado el número del último registro cargado.
- Mejorados los mensajes de estado del historial personal y compartido.
This commit is contained in:
2026-08-07 13:56:19 +02:00
parent e9d77ebb38
commit e51f2b70e2
21 changed files with 1283 additions and 275 deletions

View File

@@ -256,6 +256,9 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
private bool _popupInspectorVisible = false;
private double _inspectorLat, _inspectorLon;
private double? _inspectorElevIdee;
private double? _inspectorElevLocal5;
private double? _inspectorElevLocal2;
private double? _inspectorElevLocal1;
private IReadOnlyList<LugarGeocodificadoCercano> _inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
private int _versionInspector;

View File

@@ -276,11 +276,6 @@ public partial class PlanificadorRutas : ComponentBase
{
SincronizarTextosPenalizacionPie();
await PersistirConfiguracionAsync();
if (_mostrarElevacionesRuta)
await RecalcularRankingTrasCambioEsfuerzoPendienteAsync();
else
StateHasChanged();
}
/// <summary>
@@ -290,7 +285,6 @@ public partial class PlanificadorRutas : ComponentBase
{
SincronizarTextosPenalizacionPie();
await PersistirConfiguracionAsync();
await RecalcularRutaEquilibradaTrasCambioFactorAsync();
}
/// <summary>
@@ -300,7 +294,6 @@ public partial class PlanificadorRutas : ComponentBase
{
SincronizarTextosPenalizacionPie();
await PersistirConfiguracionAsync();
await RecalcularRutaEquilibradaTrasCambioFactorAsync();
}
private int _radioCandidatasMetros = 1200;
@@ -310,6 +303,7 @@ public partial class PlanificadorRutas : ComponentBase
private bool _activarRangoItinerarios = true;
private int _itinerarioMin = 1;
private int _itinerarioMax = 99;
private FiltroSentidoItinerario _filtroSentidoItinerario = FiltroSentidoItinerario.Ambos;
// Estado
private string _textoBusquedaOrigen = string.Empty;
@@ -364,7 +358,8 @@ public partial class PlanificadorRutas : ComponentBase
private int _indiceAlternativaSeleccionada = 0;
private DateTime? _ultimaHoraReferenciaRuta = null;
private TimeSpan? _tiempoUltimoCalculoRuta = null;
private bool _mostrarElevacionesRuta = false;
private bool _mostrarElevacionesRuta = true;
private bool _mostrarElevacionesRutaConfiguracion = true;
private bool _pintandoElevacionesRuta = false;
private bool _mostrarConfiguracionPendientesRuta = false;
private bool _panelRutasMovilContraido = false;
@@ -1356,6 +1351,16 @@ public partial class PlanificadorRutas : ComponentBase
return $"{Math.Max(0, tiempo.TotalSeconds).ToString("0.00", CultureInfo.InvariantCulture)} s";
}
private string FormatearAlturaSubidaAlerta()
{
var alturaMetros = Math.Max(0, _alturaPendientePositivaSignificativaRutaMetros);
var alturaRedondeada = Math.Round(alturaMetros, MidpointRounding.AwayFromZero);
var pisos = alturaMetros / MetrosPorPiso;
return $"{alturaRedondeada.ToString("0", CultureInfo.InvariantCulture)} m " +
$"({pisos.ToString("0.0", CultureInfo.InvariantCulture)} pisos)";
}
/// <summary>
/// Construye los parametros actuales que afectan al ranking de rutas.
/// </summary>
@@ -1436,10 +1441,18 @@ public partial class PlanificadorRutas : ComponentBase
private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa)
{
if (alternativa is null || alternativa.EsSoloAPie || alternativa.AlternativaBus is null)
if (alternativa is null)
return double.PositiveInfinity;
var distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus);
var distanciaBus = 0.0;
if (!alternativa.EsSoloAPie)
{
if (alternativa.AlternativaBus is null)
return double.PositiveInfinity;
distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus);
}
var distanciaEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(alternativa);
var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros(
alternativa.NumeroTransbordosReales,
@@ -1456,7 +1469,7 @@ public partial class PlanificadorRutas : ComponentBase
if (!double.IsFinite(score))
return string.Empty;
return FormatearDistanciaTarjeta(score);
return (score / 1000.0).ToString("0.000", CultureInfo.InvariantCulture);
}
private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync()

View File

@@ -1,4 +1,5 @@
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.JSInterop;
@@ -10,7 +11,8 @@ public partial class PlanificadorRutas
{
private const string ClaveHistorialLocal = "rutasdbus.planificador.routeHistory.v1";
private const string ClaveRespaldoHistorialLocal = "rutasdbus.planificador.routeHistory.recovery.v1";
private const int MaxItemsHistorialLocal = 100;
private const int MaxItemsHistorialLocal = 25;
private const int MaxBytesHistorialLocal = 24 * 1024;
private const int IntervaloRefrescoHistorialCompartidoSegundos = 3;
private const int TiempoMaxOperacionHistorialLocalSegundos = 8;
private static readonly JsonSerializerOptions OpcionesLecturaHistorialLocal = new()
@@ -57,6 +59,42 @@ public partial class PlanificadorRutas
[JsonPropertyName("errorMessage")]
public string? ErrorMessage { get; set; }
[JsonPropertyName("trimmed")]
public bool Recortado { get; set; }
[JsonPropertyName("backupSaved")]
public bool RespaldoGuardado { get; set; }
[JsonPropertyName("originalItemCount")]
public int? RegistrosOriginales { get; set; }
[JsonPropertyName("itemCount")]
public int? RegistrosFinales { get; set; }
[JsonPropertyName("originalByteLength")]
public int? BytesOriginales { get; set; }
[JsonPropertyName("byteLength")]
public int? BytesFinales { get; set; }
}
private static string FormatearEstadisticasStorage(ResultadoStorageNavegador? resultado)
{
var registrosOriginales = resultado?.RegistrosOriginales?.ToString(CultureInfo.InvariantCulture)
?? "no disponible";
var registrosFinales = resultado?.RegistrosFinales?.ToString(CultureInfo.InvariantCulture)
?? "no disponible";
var bytesOriginales = resultado?.BytesOriginales?.ToString(CultureInfo.InvariantCulture)
?? "no disponible";
var bytesFinales = resultado?.BytesFinales?.ToString(CultureInfo.InvariantCulture)
?? "no disponible";
return $"Registros: {registrosFinales}/{registrosOriginales}. " +
$"Tamano UTF-8: {bytesFinales}/{bytesOriginales} bytes. " +
$"Recortado: {resultado?.Recortado == true}. " +
$"Respaldo guardado: {resultado?.RespaldoGuardado == true}. " +
$"Limites: {MaxItemsHistorialLocal} registros y {MaxBytesHistorialLocal} bytes.";
}
private async Task ToggleHistorialPanel()
@@ -125,7 +163,10 @@ public partial class PlanificadorRutas
resultadoLectura = await JS.InvokeAsync<ResultadoStorageNavegador?>(
"rt.loadLocalValueResult",
cts.Token,
ClaveHistorialLocal);
ClaveHistorialLocal,
MaxItemsHistorialLocal,
MaxBytesHistorialLocal,
ClaveRespaldoHistorialLocal);
}
catch (OperationCanceledException ex)
{
@@ -134,7 +175,8 @@ public partial class PlanificadorRutas
new TimeoutException(
$"La lectura no respondio en {TiempoMaxOperacionHistorialLocalSegundos} segundos.",
ex),
$"Intento de carga: {numeroIntento}.");
$"Intento de carga: {numeroIntento}. " +
FormatearEstadisticasStorage(null));
return false;
}
catch (Exception ex)
@@ -142,7 +184,8 @@ public partial class PlanificadorRutas
RegistrarErrorHistorialLocal(
"lectura de localStorage",
ex,
$"Intento de carga: {numeroIntento}.");
$"Intento de carga: {numeroIntento}. " +
FormatearEstadisticasStorage(null));
return false;
}
@@ -152,7 +195,8 @@ public partial class PlanificadorRutas
"lectura de localStorage",
"RespuestaVacia",
"JavaScript no devolvio ningun resultado de lectura.",
$"Intento de carga: {numeroIntento}.");
$"Intento de carga: {numeroIntento}. " +
FormatearEstadisticasStorage(null));
return false;
}
@@ -162,7 +206,8 @@ public partial class PlanificadorRutas
"lectura de localStorage",
resultadoLectura.ErrorName ?? "StorageError",
resultadoLectura.ErrorMessage ?? "El navegador no ha indicado el motivo del error.",
$"Intento de carga: {numeroIntento}.");
$"Intento de carga: {numeroIntento}. " +
FormatearEstadisticasStorage(resultadoLectura));
return false;
}
@@ -184,7 +229,9 @@ public partial class PlanificadorRutas
if (registrosOmitidos > 0)
{
var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(json);
var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(
json,
resultadoLectura.RespaldoGuardado);
var detalleFalloRecuperacion = recuperacionGuardada
? null
: _detalleErrorHistorialLocal;
@@ -197,7 +244,8 @@ public partial class PlanificadorRutas
$"Se omitieron {registrosOmitidos} registros que no se podian utilizar.",
$"Primer fallo: {primerErrorRegistro ?? "sin detalle"}. " +
$"Registros recuperados: {_historialLocal.Count}. " +
$"Tamano leido: {json.Length} caracteres. " +
$"Tamano leido: {Encoding.UTF8.GetByteCount(json)} bytes y {json.Length} caracteres. " +
FormatearEstadisticasStorage(resultadoLectura) + " " +
$"Copia de recuperacion: {(recuperacionGuardada ? "guardada" : "no guardada")}." +
(string.IsNullOrWhiteSpace(detalleFalloRecuperacion)
? string.Empty
@@ -209,6 +257,14 @@ public partial class PlanificadorRutas
else
{
LimpiarErrorHistorialLocal();
if (resultadoLectura.Recortado)
{
MostrarMensajeHistorial(
$"Historial personal ajustado a {resultadoLectura.RegistrosFinales ?? _historialLocal.Count} registro(s). " +
"Se ha guardado una copia de recuperacion.");
Console.WriteLine(
$"[Historial personal] Ajuste automatico. {FormatearEstadisticasStorage(resultadoLectura)}");
}
}
return _historialLocalCargado;
@@ -220,7 +276,9 @@ public partial class PlanificadorRutas
ArgumentException)
{
FinalizarCargaHistorialLocal(Array.Empty<RegistroHistorialRuta>());
var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(json);
var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(
json,
resultadoLectura.RespaldoGuardado);
var detalleFalloRecuperacion = recuperacionGuardada
? null
: _detalleErrorHistorialLocal;
@@ -230,7 +288,8 @@ public partial class PlanificadorRutas
RegistrarErrorHistorialLocal(
"deserializacion del historial",
ex,
$"Tamano leido: {json.Length} caracteres. " +
$"Tamano leido: {Encoding.UTF8.GetByteCount(json)} bytes y {json.Length} caracteres. " +
FormatearEstadisticasStorage(resultadoLectura) + " " +
$"Copia de recuperacion: {(recuperacionGuardada ? "guardada" : "no guardada")}." +
(string.IsNullOrWhiteSpace(detalleFalloRecuperacion)
? string.Empty
@@ -248,7 +307,8 @@ public partial class PlanificadorRutas
RegistrarErrorHistorialLocal(
"fallo inesperado al procesar el historial",
ex,
$"Tamano leido: {json.Length} caracteres. El contenido original no se ha modificado.");
$"Tamano leido: {Encoding.UTF8.GetByteCount(json)} bytes y {json.Length} caracteres. " +
$"{FormatearEstadisticasStorage(resultadoLectura)} 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);
@@ -343,19 +403,43 @@ public partial class PlanificadorRutas
_historialLocalCargado = true;
}
private async Task<bool> GuardarHistorialLocalRecuperadoAsync(string jsonOriginal)
private static (List<RegistroHistorialRuta> Items, string Json, int Bytes) PrepararHistorialLocalParaGuardar(
IEnumerable<RegistroHistorialRuta> origen)
{
if (!await GuardarValorHistorialLocalAsync(
var items = origen.Take(MaxItemsHistorialLocal).ToList();
var json = JsonSerializer.Serialize(items);
var bytes = Encoding.UTF8.GetByteCount(json);
while (items.Count > 0 && bytes > MaxBytesHistorialLocal)
{
items.RemoveAt(items.Count - 1);
json = JsonSerializer.Serialize(items);
bytes = Encoding.UTF8.GetByteCount(json);
}
return (items, json, bytes);
}
private async Task<bool> GuardarHistorialLocalRecuperadoAsync(
string jsonOriginal,
bool respaldoOriginalYaGuardado)
{
if (!respaldoOriginalYaGuardado &&
!await GuardarValorHistorialLocalAsync(
ClaveRespaldoHistorialLocal,
jsonOriginal,
"guardado de la copia de recuperacion"))
return false;
var jsonRecuperado = JsonSerializer.Serialize(_historialLocal);
var preparado = PrepararHistorialLocalParaGuardar(_historialLocal);
if (preparado.Items.Count != _historialLocal.Count)
FinalizarCargaHistorialLocal(preparado.Items);
return await GuardarValorHistorialLocalAsync(
ClaveHistorialLocal,
jsonRecuperado,
"guardado del historial recuperado");
preparado.Json,
"guardado del historial recuperado",
preparado.Items.Count);
}
private async Task PersistirHistorialLocalAsync()
@@ -363,11 +447,34 @@ public partial class PlanificadorRutas
if (!_historialLocalCargado)
return;
var json = JsonSerializer.Serialize(_historialLocal);
var itemsOriginales = _historialLocal.ToList();
var jsonOriginal = JsonSerializer.Serialize(itemsOriginales);
var preparado = PrepararHistorialLocalParaGuardar(itemsOriginales);
var fueRecortado = preparado.Items.Count != itemsOriginales.Count;
if (fueRecortado)
{
var respaldoGuardado = await GuardarValorHistorialLocalAsync(
ClaveRespaldoHistorialLocal,
jsonOriginal,
"respaldo previo al limite del historial",
itemsOriginales.Count);
if (!respaldoGuardado)
{
MostrarMensajeHistorial(
"No se ha podido limitar el historial porque fallo su copia de recuperacion. Consulta el diagnostico.",
esError: true);
return;
}
FinalizarCargaHistorialLocal(preparado.Items);
}
if (!await GuardarValorHistorialLocalAsync(
ClaveHistorialLocal,
json,
"persistencia del historial"))
preparado.Json,
"persistencia del historial",
preparado.Items.Count))
{
MostrarMensajeHistorial(
"No se ha podido guardar el historial personal. Consulta el diagnostico.",
@@ -375,8 +482,16 @@ public partial class PlanificadorRutas
}
}
private async Task<bool> GuardarValorHistorialLocalAsync(string clave, string valor, string fase)
private async Task<bool> GuardarValorHistorialLocalAsync(
string clave,
string valor,
string fase,
int? registros = null)
{
var tamanoBytes = Encoding.UTF8.GetByteCount(valor);
var contexto = $"Registros: {registros?.ToString(CultureInfo.InvariantCulture) ?? "no disponible"}. " +
$"Tamano UTF-8: {tamanoBytes} bytes. Caracteres: {valor.Length}.";
try
{
using var cts = new CancellationTokenSource(
@@ -394,7 +509,7 @@ public partial class PlanificadorRutas
fase,
resultado?.ErrorName ?? "RespuestaVacia",
resultado?.ErrorMessage ?? "JavaScript no devolvio un resultado valido al guardar.",
$"Tamano que se intentaba guardar: {valor.Length} caracteres.");
contexto);
return false;
}
catch (OperationCanceledException ex)
@@ -404,7 +519,7 @@ public partial class PlanificadorRutas
new TimeoutException(
$"La escritura no respondio en {TiempoMaxOperacionHistorialLocalSegundos} segundos.",
ex),
$"Tamano que se intentaba guardar: {valor.Length} caracteres.");
contexto);
return false;
}
catch (Exception ex)
@@ -412,7 +527,7 @@ public partial class PlanificadorRutas
RegistrarErrorHistorialLocal(
fase,
ex,
$"Tamano que se intentaba guardar: {valor.Length} caracteres.");
contexto);
return false;
}
}
@@ -618,9 +733,6 @@ public partial class PlanificadorRutas
_historialLocal.Insert(0, existente);
}
while (_historialLocal.Count > MaxItemsHistorialLocal)
_historialLocal.RemoveAt(_historialLocal.Count - 1);
await PersistirHistorialLocalAsync();
}
@@ -635,9 +747,10 @@ public partial class PlanificadorRutas
return;
}
var numeroPersonal = _historialLocal.FindIndex(x => x.Id == id) + 1;
await AplicarRegistroHistorialAsync(item);
SeleccionarHistorialLocal(id);
MostrarMensajeHistorial("Combinacion personal cargada.");
MostrarMensajeHistorial($"Historial Personal Cargado: n.º {numeroPersonal}.");
}
catch (Exception ex)
{
@@ -660,9 +773,10 @@ public partial class PlanificadorRutas
return;
}
var numeroCompartido = _historialCompartido.FindIndex(x => x.Id == id) + 1;
await AplicarRegistroHistorialAsync(item);
SeleccionarHistorialCompartido(id);
MostrarMensajeHistorial("Combinación compartida cargada.");
MostrarMensajeHistorial($"Historial Compartido Cargado: n.º {numeroCompartido}.");
}
private void SeleccionarHistorialLocal(string id)

View File

@@ -347,43 +347,41 @@ public partial class PlanificadorRutas : ComponentBase
var myToken = Interlocked.Increment(ref _paradaPopupToken);
_paradaClicada = null;
_paradaElevG = null;
_paradaElevIdee = null;
_paradaElevLocal5 = null;
_paradaElevLocal2 = null;
_paradaElevLocal1 = null;
// limpiar horarios del popup
_nextDeps.Clear();
var punto = (paradaDetectada.Latitud, paradaDetectada.Longitud);
ObtenerElevacionesLocalesComparativasPorLote(new[] { punto });
var clave = ClaveElevacionIdee(punto.Latitud, punto.Longitud);
_paradaElevLocal5 = _elevLocal5Cache.TryGetValue(clave, out var local5) ? local5 : null;
_paradaElevLocal2 = _elevLocal2Cache.TryGetValue(clave, out var local2) ? local2 : null;
_paradaElevLocal1 = _elevLocal1Cache.TryGetValue(clave, out var local1) ? local1 : null;
_paradaClicada = paradaDetectada;
StateHasChanged();
double? elevG = null;
try
{
var kIdee = ClaveElevacionIdee(paradaDetectada.Latitud, paradaDetectada.Longitud);
if (_elevIdeeCache.TryGetValue(kIdee, out var g))
{
elevG = g;
}
else
if (!_elevIdeeCache.ContainsKey(clave))
{
await ObtenerElevacionesIdeePorLoteAsync(
new List<(double lat, double lon)> { (paradaDetectada.Latitud, paradaDetectada.Longitud) },
new List<(double lat, double lon)> { punto },
CancellationToken.None);
elevG = _elevIdeeCache.TryGetValue(
ClaveElevacionIdee(paradaDetectada.Latitud, paradaDetectada.Longitud),
out g) ? g : (double?)null;
}
_paradaElevIdee = _elevIdeeCache.TryGetValue(clave, out var idee) ? idee : null;
}
catch
{
elevG = null;
_paradaElevIdee = null;
}
if (myToken != _paradaPopupToken) return;
_paradaElevG = elevG;
_paradaClicada = paradaDetectada;
StateHasChanged();
// ==========================
// HORARIOS (ZIP) -> usando tu AhoraLocal() (real o simulada)
@@ -1204,10 +1202,13 @@ public partial class PlanificadorRutas : ComponentBase
private bool _elevPopupMinimizado = false;
private const string _versionApp = "(v-20260731a)";
private const string _versionApp = "(v-20260807b)";
private double? _paradaElevG;
private double? _paradaElevIdee;
private double? _paradaElevLocal5;
private double? _paradaElevLocal2;
private double? _paradaElevLocal1;

View File

@@ -1,6 +1,7 @@
using System.Globalization;
using System.Text.Json;
using Microsoft.JSInterop;
using RutasDBUS.Modelos.Planificacion;
namespace RutasDBUS.Components.Pages;
@@ -8,6 +9,7 @@ public partial class PlanificadorRutas
{
private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1";
private const int VersionPersistenciaFactorEquilibrado = 2;
private const int VersionPersistenciaPendientes = 2;
private bool _configuracionLocalCargada = false;
private bool _mostrarConfirmacionRestablecer = false;
@@ -26,6 +28,7 @@ public partial class PlanificadorRutas
public bool ActivarRangoItinerarios { get; set; }
public int ItinerarioMin { get; set; }
public int ItinerarioMax { get; set; }
public string? FiltroSentidoItinerario { get; set; }
public bool ActivarRangoTransbordos { get; set; }
public int MinTransbordosPermitidos { get; set; }
public int MaxTransbordosPermitidos { get; set; }
@@ -39,6 +42,8 @@ public partial class PlanificadorRutas
public double? FactorEquilibradoAndarBus { get; set; }
public int? VersionFactorEquilibrado { get; set; }
public bool? MostrarElevacionesRuta { get; set; }
public string? FuenteElevacionCalculo { get; set; }
public int? VersionPendientes { get; set; }
public double? UmbralPendienteSubidaRuta { get; set; }
public double? UmbralPendienteBajadaRuta { get; set; }
public double? UmbralAlturaAlertaPendienteRutaMetros { get; set; }
@@ -117,6 +122,12 @@ public partial class PlanificadorRutas
await PersistirConfiguracionAsync();
}
private async Task OnFuenteElevacionChangedAsync()
{
_fuenteElevacionCalculo = NormalizarFuenteElevacion(_fuenteElevacionCalculo);
await PersistirConfiguracionAsync();
}
/// <summary>
/// Construye la instantánea persistible a partir del estado actual.
/// </summary>
@@ -133,6 +144,7 @@ public partial class PlanificadorRutas
ActivarRangoItinerarios = _activarRangoItinerarios,
ItinerarioMin = _itinerarioMin,
ItinerarioMax = _itinerarioMax,
FiltroSentidoItinerario = _filtroSentidoItinerario.ToString(),
ActivarRangoTransbordos = _activarRangoTransbordos,
MinTransbordosPermitidos = _minTransbordosPermitidos,
MaxTransbordosPermitidos = _maxTransbordosPermitidos,
@@ -145,7 +157,9 @@ public partial class PlanificadorRutas
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
VersionFactorEquilibrado = VersionPersistenciaFactorEquilibrado,
MostrarElevacionesRuta = _mostrarElevacionesRuta,
MostrarElevacionesRuta = _mostrarElevacionesRutaConfiguracion,
FuenteElevacionCalculo = _fuenteElevacionCalculo,
VersionPendientes = VersionPersistenciaPendientes,
UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta,
UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta,
UmbralAlturaAlertaPendienteRutaMetros = _umbralAlturaAlertaPendienteRutaMetros,
@@ -182,6 +196,7 @@ public partial class PlanificadorRutas
_activarRangoItinerarios = config.ActivarRangoItinerarios;
_itinerarioMin = Math.Clamp(config.ItinerarioMin, 1, 99);
_itinerarioMax = Math.Clamp(config.ItinerarioMax, 1, 99);
_filtroSentidoItinerario = NormalizarFiltroSentidoItinerario(config.FiltroSentidoItinerario);
_activarRangoTransbordos = config.ActivarRangoTransbordos;
_minTransbordosPermitidos = Math.Clamp(config.MinTransbordosPermitidos, 0, 10);
@@ -217,7 +232,17 @@ public partial class PlanificadorRutas
Math.Abs(factorEquilibrado - 12.0) < 0.0001
? FactorEquilibradoAndarBusPorDefecto
: Math.Max(0, factorEquilibrado);
_mostrarElevacionesRuta = config.MostrarElevacionesRuta ?? false;
var migrarValoresPendientes = (config.VersionPendientes ?? 0) < VersionPersistenciaPendientes;
_mostrarElevacionesRuta = migrarValoresPendientes
? true
: config.MostrarElevacionesRuta ?? true;
_mostrarElevacionesRutaConfiguracion = _mostrarElevacionesRuta;
_fuenteElevacionCalculo = migrarValoresPendientes &&
(string.IsNullOrWhiteSpace(config.FuenteElevacionCalculo) ||
string.Equals(config.FuenteElevacionCalculo, FuenteElevacionLocal5, StringComparison.OrdinalIgnoreCase) ||
string.Equals(config.FuenteElevacionCalculo, "local", StringComparison.OrdinalIgnoreCase))
? FuenteElevacionLocal1
: NormalizarFuenteElevacion(config.FuenteElevacionCalculo);
if (config.UmbralPendienteSubidaRuta is { } umbralSubida && double.IsFinite(umbralSubida))
_umbralPendienteSubidaRuta = Math.Abs(umbralSubida - 3.0) < 0.0001
? UmbralPendienteSubidaRutaPorDefecto
@@ -262,6 +287,12 @@ public partial class PlanificadorRutas
SincronizarSegundosPorParadaConHorarios();
}
private static FiltroSentidoItinerario NormalizarFiltroSentidoItinerario(string? valor)
=> Enum.TryParse<FiltroSentidoItinerario>(valor, ignoreCase: true, out var filtro) &&
Enum.IsDefined(typeof(FiltroSentidoItinerario), filtro)
? filtro
: FiltroSentidoItinerario.Ambos;
private static IReadOnlyList<int> NormalizarLineasPermitidas(
IReadOnlyList<int>? lineas,
int lineaMinLegacy,
@@ -331,6 +362,7 @@ public partial class PlanificadorRutas
_activarRangoItinerarios = true;
_itinerarioMin = 1;
_itinerarioMax = 99;
_filtroSentidoItinerario = FiltroSentidoItinerario.Ambos;
_activarRangoTransbordos = true;
_minTransbordosPermitidos = 0;
@@ -341,7 +373,8 @@ public partial class PlanificadorRutas
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
_pesoTransbordoRankingMinutos = 4.0;
_factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
_mostrarElevacionesRuta = false;
_mostrarElevacionesRutaConfiguracion = true;
_fuenteElevacionCalculo = FuenteElevacionLocal1;
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
_umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
@@ -382,6 +415,7 @@ public partial class PlanificadorRutas
_activarRangoItinerarios = true;
_itinerarioMin = 1;
_itinerarioMax = 99;
_filtroSentidoItinerario = FiltroSentidoItinerario.Ambos;
_activarRangoTransbordos = true;
_minTransbordosPermitidos = 0;
@@ -392,7 +426,8 @@ public partial class PlanificadorRutas
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
_pesoTransbordoRankingMinutos = 4.0;
_factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
_mostrarElevacionesRuta = false;
_mostrarElevacionesRutaConfiguracion = true;
_fuenteElevacionCalculo = FuenteElevacionLocal1;
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
_umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;

View File

@@ -11,6 +11,7 @@ using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus;
using RutasDBUS.Servicios;
using RutasDBUS.Servicios.Caminata;
using RutasDBUS.Servicios.Elevacion;
using RutasDBUS.Servicios.Horarios;
using RutasDBUS.Servicios.Planificacion;
using RutasDBUS.Servicios.RedBus;
@@ -104,8 +105,7 @@ public partial class PlanificadorRutas : ComponentBase
}
/// <summary>
/// Actualiza las alturas (Z) de origen y destino para el resumen del detalle de ruta.
/// Usa la fuente G (IDEE).
/// Actualiza las alturas de origen y destino con la fuente seleccionada para el calculo.
/// </summary>
private async Task ActualizarAlturasResumenRutaAsync(CancellationToken ct)
{
@@ -121,24 +121,11 @@ public partial class PlanificadorRutas : ComponentBase
try
{
var faltanIdee = new List<(double lat, double lon)>(2);
if (!_elevIdeeCache.ContainsKey(ClaveElevacionIdee(origen.Item1, origen.Item2)))
faltanIdee.Add(origen);
if (!_elevIdeeCache.ContainsKey(ClaveElevacionIdee(destino.Item1, destino.Item2)))
faltanIdee.Add(destino);
var puntos = new List<(double lat, double lon)> { origen, destino };
await AsegurarElevacionesFuenteCalculoAsync(puntos, ct);
if (faltanIdee.Count > 0)
await ObtenerElevacionesIdeePorLoteAsync(faltanIdee, CancellationToken.None);
double? zOrigen = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(origen.Item1, origen.Item2), out var gOri)
? gOri
: (double?)null;
double? zDestino = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(destino.Item1, destino.Item2), out var gDst)
? gDst
: (double?)null;
_altitudOrigenResumenRuta = zOrigen;
_altitudDestinoResumenRuta = zDestino;
_altitudOrigenResumenRuta = ObtenerElevacionFuenteCalculo(origen.Item1, origen.Item2);
_altitudDestinoResumenRuta = ObtenerElevacionFuenteCalculo(destino.Item1, destino.Item2);
}
catch
{
@@ -186,7 +173,22 @@ public partial class PlanificadorRutas : ComponentBase
private static readonly ConcurrentDictionary<string, double> _elevIdeeCache =
new(StringComparer.OrdinalIgnoreCase);
new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, double> _elevLocal5Cache =
new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, double> _elevLocal2Cache =
new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, double> _elevLocal1Cache =
new(StringComparer.OrdinalIgnoreCase);
private static readonly SemaphoreSlim _semaforoElevacionIdee = new(1, 1);
private static long _ideeReintentarDespuesTicksUtc;
private const string FuenteElevacionLocal5 = "local5";
private const string FuenteElevacionLocal2 = "local2";
private const string FuenteElevacionLocal1 = "local1";
private const string FuenteElevacionIdee = "idee";
private const int TiempoMaximoIdeeSegundos = 10;
private string _fuenteElevacionCalculo = FuenteElevacionLocal1;
// IDEE: en el correo indican que 6 decimales es suficiente
private const int ELEV_IDEE_DECIMALES_CACHE = 6;
@@ -200,7 +202,158 @@ public partial class PlanificadorRutas : ComponentBase
double rLon = Math.Round(lon, ELEV_IDEE_DECIMALES_CACHE);
return $"{rLat.ToString("0.000000", CultureInfo.InvariantCulture)},{rLon.ToString("0.000000", CultureInfo.InvariantCulture)}";
}
private static string NormalizarFuenteElevacion(string? fuente)
{
if (string.Equals(fuente, FuenteElevacionIdee, StringComparison.OrdinalIgnoreCase))
return FuenteElevacionIdee;
if (string.Equals(fuente, FuenteElevacionLocal1, StringComparison.OrdinalIgnoreCase))
return FuenteElevacionLocal1;
if (string.Equals(fuente, FuenteElevacionLocal2, StringComparison.OrdinalIgnoreCase))
return FuenteElevacionLocal2;
if (string.Equals(fuente, FuenteElevacionLocal5, StringComparison.OrdinalIgnoreCase) ||
string.Equals(fuente, "local", StringComparison.OrdinalIgnoreCase))
{
return FuenteElevacionLocal5;
}
return FuenteElevacionLocal1;
}
private bool ElevacionLocal5Disponible
=> ElevacionLocal.EstaDisponible(ModeloElevacionLocal.Local5);
private bool ElevacionLocal2Disponible
=> ElevacionLocal.EstaDisponible(ModeloElevacionLocal.Local2);
private bool ElevacionLocal1Disponible
=> ElevacionLocal.EstaDisponible(ModeloElevacionLocal.Local1);
private bool UsarElevacionLocal5ParaCalculo
=> string.Equals(_fuenteElevacionCalculo, FuenteElevacionLocal5, StringComparison.OrdinalIgnoreCase)
&& ElevacionLocal5Disponible;
private bool UsarElevacionLocal2ParaCalculo
=> string.Equals(_fuenteElevacionCalculo, FuenteElevacionLocal2, StringComparison.OrdinalIgnoreCase)
&& ElevacionLocal2Disponible;
private bool UsarElevacionLocal1ParaCalculo
=> string.Equals(_fuenteElevacionCalculo, FuenteElevacionLocal1, StringComparison.OrdinalIgnoreCase)
&& ElevacionLocal1Disponible;
private string EtiquetaFuenteElevacionCalculo
=> UsarElevacionLocal1ParaCalculo
? "Altura (L1)"
: UsarElevacionLocal2ParaCalculo
? "Altura (L2)"
: UsarElevacionLocal5ParaCalculo
? "Altura (L5)"
: "Altura (G)";
private double? ObtenerElevacionFuenteCalculo(double lat, double lon)
{
var clave = ClaveElevacionIdee(lat, lon);
if (UsarElevacionLocal1ParaCalculo)
return _elevLocal1Cache.TryGetValue(clave, out var local1) ? local1 : null;
if (UsarElevacionLocal2ParaCalculo)
return _elevLocal2Cache.TryGetValue(clave, out var local2) ? local2 : null;
if (UsarElevacionLocal5ParaCalculo)
return _elevLocal5Cache.TryGetValue(clave, out var local5) ? local5 : null;
return _elevIdeeCache.TryGetValue(clave, out var idee) ? idee : null;
}
private double? ObtenerElevacionFuenteCalculo(MuestraElevacion muestra)
=> UsarElevacionLocal1ParaCalculo
? muestra.ElevacionLocal1
: UsarElevacionLocal2ParaCalculo
? muestra.ElevacionLocal2
: UsarElevacionLocal5ParaCalculo
? muestra.ElevacionLocal5
: muestra.ElevacionIdee;
private void ObtenerElevacionesLocalesPorLote(
ModeloElevacionLocal modelo,
ConcurrentDictionary<string, double> cache,
IReadOnlyList<(double lat, double lon)> puntos)
{
if (!ElevacionLocal.EstaDisponible(modelo) || puntos.Count == 0)
return;
var pendientes = puntos
.Where(punto => !cache.ContainsKey(ClaveElevacionIdee(punto.lat, punto.lon)))
.Distinct()
.ToList();
if (pendientes.Count == 0)
return;
var elevaciones = ElevacionLocal.ObtenerElevaciones(modelo, pendientes);
for (var i = 0; i < pendientes.Count && i < elevaciones.Count; i++)
{
if (elevaciones[i] is { } elevacion && double.IsFinite(elevacion))
cache[ClaveElevacionIdee(pendientes[i].lat, pendientes[i].lon)] = elevacion;
}
}
private void ObtenerElevacionesLocal5PorLote(IReadOnlyList<(double lat, double lon)> puntos)
=> ObtenerElevacionesLocalesPorLote(
ModeloElevacionLocal.Local5,
_elevLocal5Cache,
puntos);
private void ObtenerElevacionesLocal2PorLote(IReadOnlyList<(double lat, double lon)> puntos)
=> ObtenerElevacionesLocalesPorLote(
ModeloElevacionLocal.Local2,
_elevLocal2Cache,
puntos);
private void ObtenerElevacionesLocal1PorLote(IReadOnlyList<(double lat, double lon)> puntos)
=> ObtenerElevacionesLocalesPorLote(
ModeloElevacionLocal.Local1,
_elevLocal1Cache,
puntos);
private void ObtenerElevacionesLocalesComparativasPorLote(
IReadOnlyList<(double lat, double lon)> puntos)
{
ObtenerElevacionesLocal5PorLote(puntos);
ObtenerElevacionesLocal2PorLote(puntos);
ObtenerElevacionesLocal1PorLote(puntos);
}
private async Task AsegurarElevacionesFuenteCalculoAsync(
IReadOnlyList<(double lat, double lon)> puntos,
CancellationToken ct)
{
if (UsarElevacionLocal1ParaCalculo)
{
ObtenerElevacionesLocal1PorLote(puntos);
return;
}
if (UsarElevacionLocal2ParaCalculo)
{
ObtenerElevacionesLocal2PorLote(puntos);
return;
}
if (UsarElevacionLocal5ParaCalculo)
{
ObtenerElevacionesLocal5PorLote(puntos);
return;
}
var faltan = puntos
.Where(punto => !_elevIdeeCache.ContainsKey(ClaveElevacionIdee(punto.lat, punto.lon)))
.Distinct()
.ToList();
for (var i = 0; i < faltan.Count; i += ELEV_BATCH_MAX_PUNTOS)
{
ct.ThrowIfCancellationRequested();
await ObtenerElevacionesIdeePorLoteAsync(
faltan.Skip(i).Take(ELEV_BATCH_MAX_PUNTOS).ToList(),
ct);
}
}
private sealed class RespuestaElevacionIdee
{
public string? id { get; set; }
@@ -212,7 +365,21 @@ public partial class PlanificadorRutas : ComponentBase
/// </summary>
private async Task ObtenerElevacionesIdeePorLoteAsync(List<(double lat, double lon)> points, CancellationToken ct)
{
if (points.Count == 0) return;
if (points.Count == 0 || DateTime.UtcNow.Ticks < Interlocked.Read(ref _ideeReintentarDespuesTicksUtc))
return;
await _semaforoElevacionIdee.WaitAsync(ct);
try
{
if (DateTime.UtcNow.Ticks < Interlocked.Read(ref _ideeReintentarDespuesTicksUtc))
return;
points = points
.Where(punto => !_elevIdeeCache.ContainsKey(ClaveElevacionIdee(punto.lat, punto.lon)))
.Distinct()
.ToList();
if (points.Count == 0)
return;
// Construir FeatureCollection GeoJSON, OJO: coordinates = [lon, lat]
var features = new List<object>(points.Count);
@@ -259,14 +426,20 @@ public partial class PlanificadorRutas : ComponentBase
try
{
using var limite = CancellationTokenSource.CreateLinkedTokenSource(ct);
limite.CancelAfter(TimeSpan.FromSeconds(TiempoMaximoIdeeSegundos));
using var req = new HttpRequestMessage(HttpMethod.Post, url);
req.Headers.Accept.ParseAdd("application/json");
req.Content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json");
using var resp = await Http.SendAsync(req, ct);
if (!resp.IsSuccessStatusCode) return;
using var resp = await Http.SendAsync(req, limite.Token);
if (!resp.IsSuccessStatusCode)
{
PosponerConsultasIdee(TimeSpan.FromSeconds(15));
return;
}
var json = await resp.Content.ReadAsStringAsync(ct);
var json = await resp.Content.ReadAsStringAsync(limite.Token);
var data = JsonSerializer.Deserialize<RespuestaElevacionIdee>(json, new JsonSerializerOptions
{
@@ -283,15 +456,34 @@ public partial class PlanificadorRutas : ComponentBase
var k = ClaveElevacionIdee(lat, lon);
_elevIdeeCache[k] = data.values[i];
}
Interlocked.Exchange(ref _ideeReintentarDespuesTicksUtc, 0);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
}
catch (OperationCanceledException)
{
PosponerConsultasIdee(TimeSpan.FromSeconds(45));
}
catch
{
PosponerConsultasIdee(TimeSpan.FromSeconds(20));
// silencioso: elevación es informativa
}
}
finally
{
_semaforoElevacionIdee.Release();
}
}
private static void PosponerConsultasIdee(TimeSpan demora)
=> Interlocked.Exchange(
ref _ideeReintentarDespuesTicksUtc,
DateTime.UtcNow.Add(demora).Ticks);
// Ajustes (rendimiento)
/* private const int ELEV_MAX_PUNTOS_POR_TRAMO = 24; */
private int _elevMaxPuntosPorTramo = 999; // configurable
@@ -340,21 +532,7 @@ public partial class PlanificadorRutas : ComponentBase
private async Task<List<MuestraElevacion>> ConstruirMuestrasElevacionAsync(double[,] line, CancellationToken ct)
{
var sampled = SampleLine(line, _elevMaxPuntosPorTramo);
// --- IDEE: pedir misses primero, porque es la fuente importante ---
var missesIdee = new List<(double lat, double lon)>();
foreach (var (lat, lon) in sampled)
{
var k = ClaveElevacionIdee(lat, lon);
if (!_elevIdeeCache.ContainsKey(k))
missesIdee.Add((Math.Round(lat, ELEV_IDEE_DECIMALES_CACHE), Math.Round(lon, ELEV_IDEE_DECIMALES_CACHE)));
}
for (int i = 0; i < missesIdee.Count; i += ELEV_BATCH_MAX_PUNTOS)
{
ct.ThrowIfCancellationRequested();
await ObtenerElevacionesIdeePorLoteAsync(missesIdee.Skip(i).Take(ELEV_BATCH_MAX_PUNTOS).ToList(), ct);
}
await AsegurarElevacionesFuenteCalculoAsync(sampled, ct);
return ConstruirMuestrasElevacionDesdeCache(line);
}
@@ -367,13 +545,25 @@ public partial class PlanificadorRutas : ComponentBase
{
var (lat, lon) = sampled[i];
var elevIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var e2) ? e2 : (double?)null;
var elevLocal5 = _elevLocal5Cache.TryGetValue(ClaveElevacionIdee(lat, lon), out var local5)
? local5
: (double?)null;
var elevLocal2 = _elevLocal2Cache.TryGetValue(ClaveElevacionIdee(lat, lon), out var local2)
? local2
: (double?)null;
var elevLocal1 = _elevLocal1Cache.TryGetValue(ClaveElevacionIdee(lat, lon), out var local1)
? local1
: (double?)null;
res.Add(new MuestraElevacion
{
Indice = i,
Latitud = lat,
Longitud = lon,
ElevacionIdee = elevIdee
ElevacionIdee = elevIdee,
ElevacionLocal5 = elevLocal5,
ElevacionLocal2 = elevLocal2,
ElevacionLocal1 = elevLocal1
});
}
@@ -413,24 +603,15 @@ public partial class PlanificadorRutas : ComponentBase
{
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)));
}
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);
}
await AsegurarElevacionesFuenteCalculoAsync(puntos, ct);
foreach (var item in lineasPorAlternativa)
{
@@ -1029,12 +1210,12 @@ public partial class PlanificadorRutas : ComponentBase
if (string.Equals(criterio, CriterioRutaAndar, StringComparison.OrdinalIgnoreCase))
{
return BuscarPrimerIndiceBusOrdenadoPor(
return BuscarPrimerIndiceOrdenadoPor(
opcion => CalcularCosteAndarEquilibradoSegundos(opcion),
opcion => DuracionOrdenacion(opcion));
}
return BuscarPrimerIndiceBusOrdenadoPor(
return BuscarPrimerIndiceOrdenadoPor(
opcion => DuracionOrdenacion(opcion),
opcion => TiempoPieOrdenacion(opcion));
}
@@ -1043,7 +1224,6 @@ public partial class PlanificadorRutas : ComponentBase
{
var candidatas = _opcionesRuta
.Select((opcion, indice) => new { opcion, indice })
.Where(x => !x.opcion.EsSoloAPie)
.ToList();
if (candidatas.Count == 0)
@@ -1063,38 +1243,33 @@ public partial class PlanificadorRutas : ComponentBase
if (_opcionesRuta.Count <= 1)
return;
IEnumerable<AlternativaRuta> rutasBus = _opcionesRuta.Where(opcion => !opcion.EsSoloAPie);
IEnumerable<AlternativaRuta> ordenadas = _opcionesRuta;
if (CriterioPrioritarioAndarActivo)
{
rutasBus = rutasBus
ordenadas = ordenadas
.OrderBy(CalcularCosteAndarEquilibradoSegundos)
.ThenBy(DuracionOrdenacion)
.ThenBy(opcion => opcion.PuntuacionCoste);
}
else if (CriterioPrioritarioTiempoActivo)
{
rutasBus = rutasBus
ordenadas = ordenadas
.OrderBy(DuracionOrdenacion)
.ThenBy(TiempoPieOrdenacion)
.ThenBy(opcion => opcion.PuntuacionCoste);
}
else if (CriterioPrioritarioEquilibradoActivo)
{
rutasBus = rutasBus
ordenadas = ordenadas
.OrderBy(CalcularScoreEquilibradoFactor)
.ThenBy(opcion => opcion.NumeroTransbordosReales)
.ThenBy(opcion => opcion.TiempoEsperaTotalSegundos)
.ThenBy(opcion => opcion.PuntuacionCoste);
}
var soloPie = _opcionesRuta
.Where(opcion => opcion.EsSoloAPie)
.OrderBy(DuracionOrdenacion)
.ThenBy(opcion => opcion.DistanciaSoloAPie);
var ordenadas = rutasBus.Concat(soloPie).ToList();
var listaOrdenada = ordenadas.ToList();
_opcionesRuta.Clear();
_opcionesRuta.AddRange(ordenadas);
_opcionesRuta.AddRange(listaOrdenada);
RenumerarEtiquetasGenericasOpcionesRuta();
}
@@ -1121,23 +1296,17 @@ public partial class PlanificadorRutas : ComponentBase
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
private int BuscarPrimerIndiceBusOrdenadoPor(
private int BuscarPrimerIndiceOrdenadoPor(
Func<AlternativaRuta, double> criterioPrincipal,
Func<AlternativaRuta, double> criterioSecundario)
{
var indiceBus = _opcionesRuta
return _opcionesRuta
.Select((opcion, indice) => new { opcion, indice })
.Where(x => !x.opcion.EsSoloAPie)
.OrderBy(x => criterioPrincipal(x.opcion))
.ThenBy(x => criterioSecundario(x.opcion))
.ThenBy(x => x.opcion.PuntuacionCoste)
.Select(x => x.indice)
.FirstOrDefault(-1);
if (indiceBus >= 0)
return indiceBus;
return _opcionesRuta.Count > 0 ? 0 : -1;
}
private double DuracionOrdenacion(AlternativaRuta alternativa)
@@ -1240,9 +1409,24 @@ public partial class PlanificadorRutas : ComponentBase
_inspectorLat = lat;
_inspectorLon = lon;
_inspectorElevIdee = null;
_inspectorElevLocal5 = null;
_inspectorElevLocal2 = null;
_inspectorElevLocal1 = null;
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
_popupInspectorVisible = true;
ObtenerElevacionesLocalesComparativasPorLote(new[] { (lat, lon) });
var claveLocal = ClaveElevacionIdee(lat, lon);
_inspectorElevLocal5 = _elevLocal5Cache.TryGetValue(claveLocal, out var elevLocal5)
? elevLocal5
: null;
_inspectorElevLocal2 = _elevLocal2Cache.TryGetValue(claveLocal, out var elevLocal2)
? elevLocal2
: null;
_inspectorElevLocal1 = _elevLocal1Cache.TryGetValue(claveLocal, out var elevLocal1)
? elevLocal1
: null;
await ActualizarMarcadoresInspectorJsAsync();
StateHasChanged();
@@ -1558,6 +1742,8 @@ public partial class PlanificadorRutas : ComponentBase
if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null)
return;
_mostrarElevacionesRuta = _mostrarElevacionesRutaConfiguracion;
if (!_aplicandoRegistroHistorial)
LimpiarSeleccionHistorial();
@@ -1613,6 +1799,7 @@ public partial class PlanificadorRutas : ComponentBase
ActivarRangoItinerarios = _activarRangoItinerarios,
ItinerarioMin = _itinerarioMin,
ItinerarioMax = _itinerarioMax,
FiltroSentidoItinerario = _filtroSentidoItinerario,
ActivarRangoTransbordos = _activarRangoTransbordos,
MinTransbordosPermitidos = _minTransbordosPermitidos,
MaxTransbordosPermitidos = _maxTransbordosPermitidos,

View File

@@ -38,8 +38,10 @@ public partial class PlanificadorRutas : ComponentBase
{
if (tramo is null) return;
// Si ya está calculado, nada
if (tramo.MuestrasElevacion is not null && tramo.MuestrasElevacion.Count > 0) return;
// Si la fuente seleccionada ya está calculada, no hace falta cargar las demás.
if (tramo.MuestrasElevacion is { Count: > 0 }
&& tramo.MuestrasElevacion.All(muestra => ObtenerElevacionFuenteCalculo(muestra) is not null))
return;
// Necesitamos la geometría
if (tramo.Linea is null || tramo.Linea.GetLength(0) <= 0) return;
@@ -52,20 +54,20 @@ public partial class PlanificadorRutas : ComponentBase
try
{
var ctNone = CancellationToken.None;
// Samples bajo demanda usando la fuente G.
var samples = await ConstruirMuestrasElevacionAsync(tramo.Linea, ctNone);
tramo.MuestrasElevacion = samples;
var muestrasIdee = samples
.Where(x => x.ElevacionIdee is not null)
.Select(x => x.ElevacionIdee!.Value)
var muestrasFuenteCalculo = samples
.Select(ObtenerElevacionFuenteCalculo)
.Where(elevacion => elevacion is not null)
.Select(elevacion => elevacion!.Value)
.ToList();
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;
tramo.ElevInicio = samples.Count > 0 ? ObtenerElevacionFuenteCalculo(samples[0]) : null;
tramo.ElevFin = samples.Count > 0 ? ObtenerElevacionFuenteCalculo(samples[^1]) : null;
tramo.ElevMin = muestrasFuenteCalculo.Count > 0 ? muestrasFuenteCalculo.Min() : null;
tramo.ElevMax = muestrasFuenteCalculo.Count > 0 ? muestrasFuenteCalculo.Max() : null;
}
catch
{
@@ -85,36 +87,15 @@ public partial class PlanificadorRutas : ComponentBase
{
bool valor => valor,
string texto when bool.TryParse(texto, out var valor) => valor,
_ => !_mostrarElevacionesRuta
_ => !_mostrarElevacionesRutaConfiguracion
};
if (_mostrarElevacionesRuta == mostrarElevaciones)
if (_mostrarElevacionesRutaConfiguracion == mostrarElevaciones)
return;
_mostrarElevacionesRuta = mostrarElevaciones;
_mostrarElevacionesRutaConfiguracion = mostrarElevaciones;
await PersistirConfiguracionAsync();
if (_opcionesRuta.Count == 0)
{
StateHasChanged();
return;
}
_pintandoElevacionesRuta = true;
_mensajeRuta = _mostrarElevacionesRuta
? "Aplicando pendientes..."
: "Recalculando sin pendientes...";
StateHasChanged();
await Task.Yield();
try
{
await RecalcularRankingTrasCambioEsfuerzoPendienteAsync(forzarRepintado: true);
}
finally
{
_pintandoElevacionesRuta = false;
StateHasChanged();
}
}
private async Task CambiarUmbralElevacionRutaAsync(ChangeEventArgs e, bool esSubida)
@@ -130,15 +111,7 @@ public partial class PlanificadorRutas : ComponentBase
_umbralPendienteBajadaRuta = nuevo;
await PersistirConfiguracionAsync();
if (esSubida && _mostrarElevacionesRuta)
{
await RecalcularRankingTrasCambioEsfuerzoPendienteAsync();
}
else if (_mostrarElevacionesRuta)
await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando pendientes...");
else
StateHasChanged();
StateHasChanged();
}
private async Task RecalcularRankingTrasCambioEsfuerzoPendienteAsync(bool forzarRepintado = false)
@@ -385,10 +358,12 @@ public partial class PlanificadorRutas : ComponentBase
continue;
distanciaTotal += distancia;
if (anterior.ElevacionIdee is null || actual.ElevacionIdee is null)
var elevacionAnterior = ObtenerElevacionFuenteCalculo(anterior);
var elevacionActual = ObtenerElevacionFuenteCalculo(actual);
if (elevacionAnterior is null || elevacionActual is null)
continue;
var diferenciaAltura = actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value;
var diferenciaAltura = elevacionActual.Value - elevacionAnterior.Value;
var pendiente = diferenciaAltura / distancia * 100.0;
var esfuerzo = CalcularEsfuerzoElevacion(distancia, pendiente);
if (esfuerzo is null)
@@ -427,14 +402,22 @@ public partial class PlanificadorRutas : ComponentBase
return $"{(distanciaMetros / 1000.0):0.000} km";
}
private static string FormatearDistanciaMetrosElevacion(double distanciaMetros)
{
if (!double.IsFinite(distanciaMetros) || distanciaMetros <= 0)
return "-";
return $"{distanciaMetros:0.00} m";
}
private void ReiniciarResumenPendientesRuta()
{
_alturaPendientePositivaSignificativaRutaMetros = 0.0;
_distanciaPendientePositivaSignificativaRutaMetros = 0.0;
}
private static double? ElevacionPreferidaMapa(MuestraElevacion muestra)
=> muestra.ElevacionIdee;
private double? ElevacionPreferidaMapa(MuestraElevacion muestra)
=> ObtenerElevacionFuenteCalculo(muestra);
private (string Categoria, string Color) ClasificarPendienteMapa(double? pendientePorcentaje)
{

View File

@@ -248,11 +248,23 @@
}
@if (_paradaClicada.Altura > 0)
{
<div><b>Altitud (J):</b> @_paradaClicada.Altura.ToString("0") m</div>
<div><b>Altura (J):</b> @_paradaClicada.Altura.ToString("0") m</div>
}
@if (_paradaElevG is not null)
@if (_paradaElevIdee is not null)
{
<div><b>Altitud (G):</b> @_paradaElevG.Value.ToString("0.00", CultureInfo.InvariantCulture) m</div>
<div><b>Altura (G):</b> @_paradaElevIdee.Value.ToString("0.00", CultureInfo.InvariantCulture) m</div>
}
@if (_paradaElevLocal5 is not null)
{
<div><b>Altura (L5):</b> @_paradaElevLocal5.Value.ToString("0.00", CultureInfo.InvariantCulture) m</div>
}
@if (_paradaElevLocal2 is not null)
{
<div><b>Altura (L2):</b> @_paradaElevLocal2.Value.ToString("0.00", CultureInfo.InvariantCulture) m</div>
}
@if (_paradaElevLocal1 is not null)
{
<div><b>Altura (L1):</b> @_paradaElevLocal1.Value.ToString("0.00", CultureInfo.InvariantCulture) m</div>
}
@if (_usarHorariosTeoricos)
@@ -303,7 +315,7 @@
</div>
<button class="popup-parada-cerrar" @onclick="() => { _paradaClicada = null; _paradaElevG = null; }">
<button class="popup-parada-cerrar" @onclick="() => { _paradaClicada = null; _paradaElevIdee = null; _paradaElevLocal5 = null; _paradaElevLocal2 = null; _paradaElevLocal1 = null; }">
×
</button>
</div>
@@ -347,10 +359,46 @@
<div><b>Latitud:</b> @_inspectorLat.ToString("0.000000")</div>
<div><b>Longitud:</b> @_inspectorLon.ToString("0.000000")</div>
<div>
<b>Altitud (G):</b>
<b>Altura (G):</b>
@(_inspectorElevIdee is null
? "-"
: _inspectorElevIdee.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
? "-"
: _inspectorElevIdee.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
</div>
<div>
<b>Altura (L5):</b>
@(_inspectorElevLocal5 is null
? "-"
: _inspectorElevLocal5.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
@if (_inspectorElevIdee is not null && _inspectorElevLocal5 is not null)
{
<span class="muted">
- Dif: @((_inspectorElevLocal5.Value - _inspectorElevIdee.Value).ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture)) m
</span>
}
</div>
<div>
<b>Altura (L2):</b>
@(_inspectorElevLocal2 is null
? "-"
: _inspectorElevLocal2.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
@if (_inspectorElevIdee is not null && _inspectorElevLocal2 is not null)
{
<span class="muted">
- Dif: @((_inspectorElevLocal2.Value - _inspectorElevIdee.Value).ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture)) m
</span>
}
</div>
<div>
<b>Altura (L1):</b>
@(_inspectorElevLocal1 is null
? "-"
: _inspectorElevLocal1.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
@if (_inspectorElevIdee is not null && _inspectorElevLocal1 is not null)
{
<span class="muted">
- Dif: @((_inspectorElevLocal1.Value - _inspectorElevIdee.Value).ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture)) m
</span>
}
</div>
@if (_inspectorLugaresCercanos.Count > 0)
@@ -401,6 +449,7 @@
<span class="popup-elev-title-threshold">
Umbral de subida (Esf.):
@NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta).ToString("0.#", CultureInfo.InvariantCulture) %
· Cálculo: @EtiquetaFuenteElevacionCalculo
</span>
</span>
@@ -425,7 +474,9 @@
<div class="popup-parada-body popup-elev-table-wrap">
@* ===== tu contenido actual tal cual ===== *@
@if (_tramoElevDetalle.ElevacionCargando)
@if (_tramoElevDetalle.ElevacionCargando
&& (_tramoElevDetalle.MuestrasElevacion is null
|| _tramoElevDetalle.MuestrasElevacion.Count == 0))
{
<div class="muted">Cargando puntos…</div>
}
@@ -435,6 +486,10 @@
}
else
{
@if (_tramoElevDetalle.ElevacionCargando)
{
<div class="muted elev-comparison-loading">Cargando altura seleccionada…</div>
}
<table class="popup-elev-table" style="
width:100%;
font-size:0.8rem;
@@ -444,20 +499,20 @@
white-space: nowrap;
">
<colgroup>
<col style="width:34px;" />
<col style="width:76px;" />
<col style="width:30px;" />
<col style="width:84px;" />
<col style="width:58px;" />
<col style="width:70px;" />
<col style="width:72px;" />
<col style="width:62px;" />
<col style="width:80px;" />
<col style="width:78px;" />
<col style="width:66px;" />
</colgroup>
<thead>
<tr>
<th style="padding:4px 4px;">#</th>
<th style="padding:4px 5px; text-align:right;">Alt (G)</th>
<th style="padding:4px 5px; text-align:right;">@EtiquetaFuenteElevacionCalculo</th>
<th style="padding:4px 5px; text-align:right;">dAlt</th>
<th style="padding:4px 5px; text-align:right;">Pend (G)</th>
<th style="padding:4px 5px; text-align:right;">Pend</th>
<th style="padding:4px 5px; text-align:right;">Dist (m)</th>
<th style="padding:4px 5px; text-align:right;">Esf.</th>
</tr>
@@ -467,6 +522,7 @@
@for (int i = 0; i < _tramoElevDetalle.MuestrasElevacion.Count; i++)
{
var p = _tramoElevDetalle.MuestrasElevacion[i];
var elevacionSeleccionada = ObtenerElevacionFuenteCalculo(p);
double? distPrev = null;
double? deltaAlturaI = null;
@@ -477,10 +533,12 @@
var prev = _tramoElevDetalle.MuestrasElevacion[i - 1];
distPrev = CalcularDistanciaMetros(prev.Latitud, prev.Longitud, p.Latitud, p.Longitud);
var elevacionPrevia = ObtenerElevacionFuenteCalculo(prev);
var elevacionActual = ObtenerElevacionFuenteCalculo(p);
if (prev.ElevacionIdee is not null && p.ElevacionIdee is not null)
if (elevacionPrevia is not null && elevacionActual is not null)
{
deltaAlturaI = p.ElevacionIdee.Value - prev.ElevacionIdee.Value;
deltaAlturaI = elevacionActual.Value - elevacionPrevia.Value;
if (distPrev > 0)
pendienteI = deltaAlturaI.Value / distPrev.Value * 100.0;
@@ -504,10 +562,10 @@
@(i == 0 ? "Ini" :
i == _tramoElevDetalle.MuestrasElevacion.Count - 1 ? "Fin" :
p.Indice.ToString())
</td>
</td>
<td style="padding:3px 5px; text-align:right;">
@(p.ElevacionIdee is null ? "-" : p.ElevacionIdee.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
@(elevacionSeleccionada is null ? "-" : elevacionSeleccionada.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
</td>
<td style="padding:3px 5px; text-align:right;">
@@ -533,18 +591,21 @@
@if (_tramoElevDetalle is not null)
{
var s = _tramoElevDetalle.MuestrasElevacion ?? new List<MuestraElevacion>();
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();
double? GMax = s.Where(x => x.ElevacionIdee is not null).Select(x => x.ElevacionIdee!.Value).DefaultIfEmpty().Max();
if (!s.Any(x => x.ElevacionIdee is not null)) { GMin = null; GMax = null; }
var alturasSeleccionadas = s.Select(ObtenerElevacionFuenteCalculo).ToList();
double? alturaIni = alturasSeleccionadas.Count > 0 ? alturasSeleccionadas[0] : null;
double? alturaFin = alturasSeleccionadas.Count > 0 ? alturasSeleccionadas[^1] : null;
var alturasValidas = alturasSeleccionadas
.Where(altura => altura is not null)
.Select(altura => altura!.Value)
.ToList();
double? alturaMin = alturasValidas.Count > 0 ? alturasValidas.Min() : null;
double? alturaMax = alturasValidas.Count > 0 ? alturasValidas.Max() : null;
string F(double? v) => v is null ? "-" : v.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m";
var resumenEsfuerzo = CalcularResumenEsfuerzoElevacion(s);
<div class="muted" style="font-size:0.78rem; margin-top:.5rem;">
<div><b>G:</b> Ini/Fin: @F(GIni) / @F(GFin) · Min/Max: @F(GMin) / @F(GMax)</div>
<div><b>@EtiquetaFuenteElevacionCalculo:</b> Ini/Fin: @F(alturaIni) / @F(alturaFin) · Min/Max: @F(alturaMin) / @F(alturaMax)</div>
<div style="margin-top:.25rem;">
<b>Suma de distancias entre puntos:</b> @FormatearDistanciaResumenElevacion(resumenEsfuerzo.DistanciaTotalMetros)
</div>
@@ -684,10 +745,10 @@
@if (MostrarAlertaPendientesPositivasSignificativas)
{
<div class="route-slope-warning" role="status">
<div>Pendientes positivas significativas.</div>
<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</div>
<div>- Distancia con subidas: @FormatearDistanciaMetrosElevacion(_distanciaPendientePositivaSignificativaRutaMetros)</div>
<div>- Subidas acumuladas: @FormatearAlturaSubidaAlerta()</div>
</div>
</div>
}
@@ -1002,13 +1063,17 @@
else
{
<div class="history-list">
@foreach (var item in _historialLocal)
@for (int i = 0; i < _historialLocal.Count; i++)
{
var item = _historialLocal[i];
var numeroPersonal = i + 1;
<div class="history-item @(item.Id == _historialLocalSeleccionadoId ? "history-item--selected" : "")">
<button type="button"
class="history-item-main"
class="history-item-main history-item-main--numbered"
@onclick="() => CargarHistorialLocalAsync(item.Id)">
@TextoOpcionHistorial(item)
<span class="history-item-number">@numeroPersonal.</span>
<span class="history-item-text">@TextoOpcionHistorial(item)</span>
</button>
<button type="button"
@@ -1177,7 +1242,7 @@
{
if (_horariosOk)
{
<span class="muted">OK</span>
<span class="muted">activados</span>
}
else if (!string.IsNullOrWhiteSpace(_horariosError))
{
@@ -1273,14 +1338,13 @@
<div class="line-filter">
<label class="line-filter__toggle">
<input type="checkbox" @bind="_activarRangoItinerarios" @bind:after="PersistirConfiguracionAsync" />
<span>Aplicar rango de itinerarios utilizables:</span>
<span>Aplicar itinerarios/sentidos:</span>
</label>
<div class="line-filter__range">
<span class="muted">Rango:</span>
<div class="line-filter__range line-filter__range--itinerarios">
<select class="config-input"
style="width:90px"
aria-label="Itinerario minimo"
@bind="_itinerarioMin"
@bind:event="onchange"
@bind:after="PersistirConfiguracionAsync">
@@ -1294,6 +1358,7 @@
<select class="config-input"
style="width:90px"
aria-label="Itinerario maximo"
@bind="_itinerarioMax"
@bind:event="onchange"
@bind:after="PersistirConfiguracionAsync">
@@ -1302,6 +1367,18 @@
<option value="@n">@n</option>
}
</select>
<select class="config-input line-filter__direction"
title="Sentidos de itinerario permitidos"
aria-label="Sentido de itinerario"
@bind="_filtroSentidoItinerario"
@bind:event="onchange"
@bind:after="PersistirConfiguracionAsync">
<option value="@FiltroSentidoItinerario.Ambos">Ambos</option>
<option value="@FiltroSentidoItinerario.SoloIda">Solo Ida</option>
<option value="@FiltroSentidoItinerario.SoloVuelta">Solo Vuelta</option>
<option value="@FiltroSentidoItinerario.Ninguno">Ninguno</option>
</select>
</div>
</div>
</div>
@@ -1401,7 +1478,7 @@
<label>Transbordo:</label>
<div class="config-dual-labeled">
<label class="config-dual-labeled__field">
<span>Peso rank.</span>
<span>Penal. (min)</span>
<input type="text" class="config-input"
placeholder="4,0"
title="Penalización temporal y distancia equivalente por cada transbordo real"
@@ -1459,12 +1536,12 @@
</div>
<div class="config-row">
<label>Altitud:</label>
<label>Altura:</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"
title="Máximo de puntos de altura por tramo"
@bind="_elevMaxPuntosPorTramo" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
</label>
<label class="config-dual-labeled__field">
@@ -1478,20 +1555,20 @@
</div>
<div class="config-row config-row-pendientes">
<label>Pendientes:</label>
<label>Usar pendientes:</label>
<div class="config-pendientes-inline">
<label class="route-elevation-switch @(_mostrarElevacionesRuta ? "route-elevation-switch--active" : "")"
<label class="route-elevation-switch @(_mostrarElevacionesRutaConfiguracion ? "route-elevation-switch--active" : "")"
title="Activar o desactivar las pendientes en el cálculo">
<input type="checkbox"
checked="@_mostrarElevacionesRuta"
checked="@_mostrarElevacionesRutaConfiguracion"
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>
<span class="config-pendientes-inline__label">Config.</span>
<button type="button"
class="config-pendientes-settings-button"
title="Configurar Pendientes"
@@ -1551,7 +1628,27 @@
<div class="pendientes-config-modal__fields">
<label class="pendientes-config-modal__field">
<span>Subida (%)</span>
<span>Método Cálculo Alturas</span>
<select class="config-input"
title="Altura usada para calcular pendientes y rutas"
disabled="@_pintandoElevacionesRuta"
@bind="_fuenteElevacionCalculo"
@bind:after="OnFuenteElevacionChangedAsync">
<option value="@FuenteElevacionIdee">Altura (G)</option>
<option value="@FuenteElevacionLocal5" disabled="@(!ElevacionLocal5Disponible)">
Altura (L5)@(!ElevacionLocal5Disponible ? " - no disponible" : string.Empty)
</option>
<option value="@FuenteElevacionLocal2" disabled="@(!ElevacionLocal2Disponible)">
Altura (L2)@(!ElevacionLocal2Disponible ? " - no disponible" : string.Empty)
</option>
<option value="@FuenteElevacionLocal1" disabled="@(!ElevacionLocal1Disponible)">
Altura (L1)@(!ElevacionLocal1Disponible ? " - no disponible" : string.Empty)
</option>
</select>
</label>
<label class="pendientes-config-modal__field">
<span>Umbral Subida (%)</span>
<input type="text"
class="config-input"
inputmode="decimal"
@@ -1561,7 +1658,7 @@
</label>
<label class="pendientes-config-modal__field">
<span>Bajada (%)</span>
<span>Umbral Bajada (%)</span>
<input type="text"
class="config-input"
inputmode="decimal"
@@ -1571,7 +1668,7 @@
</label>
<label class="pendientes-config-modal__field">
<span>Alerta (m)</span>
<span>Umbral Alerta (m)</span>
<input type="text"
class="config-input"
inputmode="decimal"

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using RutasDBUS.Servicios.Caminata;
using RutasDBUS.Servicios.Elevacion;
using RutasDBUS.Servicios.Geocodificacion;
using RutasDBUS.Servicios.Historial;
using RutasDBUS.Servicios.Horarios;
@@ -19,6 +20,7 @@ public partial class PlanificadorRutas : ComponentBase
[Inject] protected IPlanificadorRutas PlanificadorRutasServicio { get; set; } = default!;
[Inject] protected IHistorialRutasCompartidoService HistorialRutasCompartidoService { get; set; } = default!;
[Inject] protected IGeocodificadorLocalServicio GeocodificadorLocal { get; set; } = default!;
[Inject] protected IServicioElevacionLocal ElevacionLocal { get; set; } = default!;
[Inject] protected IClienteOsrm EsrmCliente { get; set; } = default!;
[Inject] protected IJSRuntime JS { get; set; } = default!;
[Inject] protected IClienteValhalla ClienteValhalla { get; set; } = default!;