- 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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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!;
|
||||
|
||||
Reference in New Issue
Block a user