- 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 bool _popupInspectorVisible = false;
|
||||||
private double _inspectorLat, _inspectorLon;
|
private double _inspectorLat, _inspectorLon;
|
||||||
private double? _inspectorElevIdee;
|
private double? _inspectorElevIdee;
|
||||||
|
private double? _inspectorElevLocal5;
|
||||||
|
private double? _inspectorElevLocal2;
|
||||||
|
private double? _inspectorElevLocal1;
|
||||||
private IReadOnlyList<LugarGeocodificadoCercano> _inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
|
private IReadOnlyList<LugarGeocodificadoCercano> _inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
|
||||||
private int _versionInspector;
|
private int _versionInspector;
|
||||||
|
|
||||||
|
|||||||
@@ -276,11 +276,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
{
|
{
|
||||||
SincronizarTextosPenalizacionPie();
|
SincronizarTextosPenalizacionPie();
|
||||||
await PersistirConfiguracionAsync();
|
await PersistirConfiguracionAsync();
|
||||||
|
|
||||||
if (_mostrarElevacionesRuta)
|
|
||||||
await RecalcularRankingTrasCambioEsfuerzoPendienteAsync();
|
|
||||||
else
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -290,7 +285,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
{
|
{
|
||||||
SincronizarTextosPenalizacionPie();
|
SincronizarTextosPenalizacionPie();
|
||||||
await PersistirConfiguracionAsync();
|
await PersistirConfiguracionAsync();
|
||||||
await RecalcularRutaEquilibradaTrasCambioFactorAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -300,7 +294,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
{
|
{
|
||||||
SincronizarTextosPenalizacionPie();
|
SincronizarTextosPenalizacionPie();
|
||||||
await PersistirConfiguracionAsync();
|
await PersistirConfiguracionAsync();
|
||||||
await RecalcularRutaEquilibradaTrasCambioFactorAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int _radioCandidatasMetros = 1200;
|
private int _radioCandidatasMetros = 1200;
|
||||||
@@ -310,6 +303,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
private bool _activarRangoItinerarios = true;
|
private bool _activarRangoItinerarios = true;
|
||||||
private int _itinerarioMin = 1;
|
private int _itinerarioMin = 1;
|
||||||
private int _itinerarioMax = 99;
|
private int _itinerarioMax = 99;
|
||||||
|
private FiltroSentidoItinerario _filtroSentidoItinerario = FiltroSentidoItinerario.Ambos;
|
||||||
|
|
||||||
// Estado
|
// Estado
|
||||||
private string _textoBusquedaOrigen = string.Empty;
|
private string _textoBusquedaOrigen = string.Empty;
|
||||||
@@ -364,7 +358,8 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
private int _indiceAlternativaSeleccionada = 0;
|
private int _indiceAlternativaSeleccionada = 0;
|
||||||
private DateTime? _ultimaHoraReferenciaRuta = null;
|
private DateTime? _ultimaHoraReferenciaRuta = null;
|
||||||
private TimeSpan? _tiempoUltimoCalculoRuta = null;
|
private TimeSpan? _tiempoUltimoCalculoRuta = null;
|
||||||
private bool _mostrarElevacionesRuta = false;
|
private bool _mostrarElevacionesRuta = true;
|
||||||
|
private bool _mostrarElevacionesRutaConfiguracion = true;
|
||||||
private bool _pintandoElevacionesRuta = false;
|
private bool _pintandoElevacionesRuta = false;
|
||||||
private bool _mostrarConfiguracionPendientesRuta = false;
|
private bool _mostrarConfiguracionPendientesRuta = false;
|
||||||
private bool _panelRutasMovilContraido = 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";
|
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>
|
/// <summary>
|
||||||
/// Construye los parametros actuales que afectan al ranking de rutas.
|
/// Construye los parametros actuales que afectan al ranking de rutas.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1436,10 +1441,18 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
|
|
||||||
private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa)
|
private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa)
|
||||||
{
|
{
|
||||||
if (alternativa is null || alternativa.EsSoloAPie || alternativa.AlternativaBus is null)
|
if (alternativa is null)
|
||||||
return double.PositiveInfinity;
|
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 distanciaEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(alternativa);
|
||||||
var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros(
|
var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros(
|
||||||
alternativa.NumeroTransbordosReales,
|
alternativa.NumeroTransbordosReales,
|
||||||
@@ -1456,7 +1469,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
if (!double.IsFinite(score))
|
if (!double.IsFinite(score))
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
return FormatearDistanciaTarjeta(score);
|
return (score / 1000.0).ToString("0.000", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync()
|
private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
@@ -10,7 +11,8 @@ public partial class PlanificadorRutas
|
|||||||
{
|
{
|
||||||
private const string ClaveHistorialLocal = "rutasdbus.planificador.routeHistory.v1";
|
private const string ClaveHistorialLocal = "rutasdbus.planificador.routeHistory.v1";
|
||||||
private const string ClaveRespaldoHistorialLocal = "rutasdbus.planificador.routeHistory.recovery.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 IntervaloRefrescoHistorialCompartidoSegundos = 3;
|
||||||
private const int TiempoMaxOperacionHistorialLocalSegundos = 8;
|
private const int TiempoMaxOperacionHistorialLocalSegundos = 8;
|
||||||
private static readonly JsonSerializerOptions OpcionesLecturaHistorialLocal = new()
|
private static readonly JsonSerializerOptions OpcionesLecturaHistorialLocal = new()
|
||||||
@@ -57,6 +59,42 @@ public partial class PlanificadorRutas
|
|||||||
|
|
||||||
[JsonPropertyName("errorMessage")]
|
[JsonPropertyName("errorMessage")]
|
||||||
public string? ErrorMessage { get; set; }
|
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()
|
private async Task ToggleHistorialPanel()
|
||||||
@@ -125,7 +163,10 @@ public partial class PlanificadorRutas
|
|||||||
resultadoLectura = await JS.InvokeAsync<ResultadoStorageNavegador?>(
|
resultadoLectura = await JS.InvokeAsync<ResultadoStorageNavegador?>(
|
||||||
"rt.loadLocalValueResult",
|
"rt.loadLocalValueResult",
|
||||||
cts.Token,
|
cts.Token,
|
||||||
ClaveHistorialLocal);
|
ClaveHistorialLocal,
|
||||||
|
MaxItemsHistorialLocal,
|
||||||
|
MaxBytesHistorialLocal,
|
||||||
|
ClaveRespaldoHistorialLocal);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
{
|
{
|
||||||
@@ -134,7 +175,8 @@ public partial class PlanificadorRutas
|
|||||||
new TimeoutException(
|
new TimeoutException(
|
||||||
$"La lectura no respondio en {TiempoMaxOperacionHistorialLocalSegundos} segundos.",
|
$"La lectura no respondio en {TiempoMaxOperacionHistorialLocalSegundos} segundos.",
|
||||||
ex),
|
ex),
|
||||||
$"Intento de carga: {numeroIntento}.");
|
$"Intento de carga: {numeroIntento}. " +
|
||||||
|
FormatearEstadisticasStorage(null));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -142,7 +184,8 @@ public partial class PlanificadorRutas
|
|||||||
RegistrarErrorHistorialLocal(
|
RegistrarErrorHistorialLocal(
|
||||||
"lectura de localStorage",
|
"lectura de localStorage",
|
||||||
ex,
|
ex,
|
||||||
$"Intento de carga: {numeroIntento}.");
|
$"Intento de carga: {numeroIntento}. " +
|
||||||
|
FormatearEstadisticasStorage(null));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +195,8 @@ public partial class PlanificadorRutas
|
|||||||
"lectura de localStorage",
|
"lectura de localStorage",
|
||||||
"RespuestaVacia",
|
"RespuestaVacia",
|
||||||
"JavaScript no devolvio ningun resultado de lectura.",
|
"JavaScript no devolvio ningun resultado de lectura.",
|
||||||
$"Intento de carga: {numeroIntento}.");
|
$"Intento de carga: {numeroIntento}. " +
|
||||||
|
FormatearEstadisticasStorage(null));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +206,8 @@ public partial class PlanificadorRutas
|
|||||||
"lectura de localStorage",
|
"lectura de localStorage",
|
||||||
resultadoLectura.ErrorName ?? "StorageError",
|
resultadoLectura.ErrorName ?? "StorageError",
|
||||||
resultadoLectura.ErrorMessage ?? "El navegador no ha indicado el motivo del error.",
|
resultadoLectura.ErrorMessage ?? "El navegador no ha indicado el motivo del error.",
|
||||||
$"Intento de carga: {numeroIntento}.");
|
$"Intento de carga: {numeroIntento}. " +
|
||||||
|
FormatearEstadisticasStorage(resultadoLectura));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +229,9 @@ public partial class PlanificadorRutas
|
|||||||
|
|
||||||
if (registrosOmitidos > 0)
|
if (registrosOmitidos > 0)
|
||||||
{
|
{
|
||||||
var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(json);
|
var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(
|
||||||
|
json,
|
||||||
|
resultadoLectura.RespaldoGuardado);
|
||||||
var detalleFalloRecuperacion = recuperacionGuardada
|
var detalleFalloRecuperacion = recuperacionGuardada
|
||||||
? null
|
? null
|
||||||
: _detalleErrorHistorialLocal;
|
: _detalleErrorHistorialLocal;
|
||||||
@@ -197,7 +244,8 @@ public partial class PlanificadorRutas
|
|||||||
$"Se omitieron {registrosOmitidos} registros que no se podian utilizar.",
|
$"Se omitieron {registrosOmitidos} registros que no se podian utilizar.",
|
||||||
$"Primer fallo: {primerErrorRegistro ?? "sin detalle"}. " +
|
$"Primer fallo: {primerErrorRegistro ?? "sin detalle"}. " +
|
||||||
$"Registros recuperados: {_historialLocal.Count}. " +
|
$"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")}." +
|
$"Copia de recuperacion: {(recuperacionGuardada ? "guardada" : "no guardada")}." +
|
||||||
(string.IsNullOrWhiteSpace(detalleFalloRecuperacion)
|
(string.IsNullOrWhiteSpace(detalleFalloRecuperacion)
|
||||||
? string.Empty
|
? string.Empty
|
||||||
@@ -209,6 +257,14 @@ public partial class PlanificadorRutas
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
LimpiarErrorHistorialLocal();
|
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;
|
return _historialLocalCargado;
|
||||||
@@ -220,7 +276,9 @@ public partial class PlanificadorRutas
|
|||||||
ArgumentException)
|
ArgumentException)
|
||||||
{
|
{
|
||||||
FinalizarCargaHistorialLocal(Array.Empty<RegistroHistorialRuta>());
|
FinalizarCargaHistorialLocal(Array.Empty<RegistroHistorialRuta>());
|
||||||
var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(json);
|
var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(
|
||||||
|
json,
|
||||||
|
resultadoLectura.RespaldoGuardado);
|
||||||
var detalleFalloRecuperacion = recuperacionGuardada
|
var detalleFalloRecuperacion = recuperacionGuardada
|
||||||
? null
|
? null
|
||||||
: _detalleErrorHistorialLocal;
|
: _detalleErrorHistorialLocal;
|
||||||
@@ -230,7 +288,8 @@ public partial class PlanificadorRutas
|
|||||||
RegistrarErrorHistorialLocal(
|
RegistrarErrorHistorialLocal(
|
||||||
"deserializacion del historial",
|
"deserializacion del historial",
|
||||||
ex,
|
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")}." +
|
$"Copia de recuperacion: {(recuperacionGuardada ? "guardada" : "no guardada")}." +
|
||||||
(string.IsNullOrWhiteSpace(detalleFalloRecuperacion)
|
(string.IsNullOrWhiteSpace(detalleFalloRecuperacion)
|
||||||
? string.Empty
|
? string.Empty
|
||||||
@@ -248,7 +307,8 @@ public partial class PlanificadorRutas
|
|||||||
RegistrarErrorHistorialLocal(
|
RegistrarErrorHistorialLocal(
|
||||||
"fallo inesperado al procesar el historial",
|
"fallo inesperado al procesar el historial",
|
||||||
ex,
|
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(
|
MostrarMensajeHistorial(
|
||||||
"El codigo ha producido un error inesperado al cargar el historial. Consulta el diagnostico; los datos originales no se han modificado.",
|
"El codigo ha producido un error inesperado al cargar el historial. Consulta el diagnostico; los datos originales no se han modificado.",
|
||||||
esError: true);
|
esError: true);
|
||||||
@@ -343,19 +403,43 @@ public partial class PlanificadorRutas
|
|||||||
_historialLocalCargado = true;
|
_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,
|
ClaveRespaldoHistorialLocal,
|
||||||
jsonOriginal,
|
jsonOriginal,
|
||||||
"guardado de la copia de recuperacion"))
|
"guardado de la copia de recuperacion"))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var jsonRecuperado = JsonSerializer.Serialize(_historialLocal);
|
var preparado = PrepararHistorialLocalParaGuardar(_historialLocal);
|
||||||
|
if (preparado.Items.Count != _historialLocal.Count)
|
||||||
|
FinalizarCargaHistorialLocal(preparado.Items);
|
||||||
|
|
||||||
return await GuardarValorHistorialLocalAsync(
|
return await GuardarValorHistorialLocalAsync(
|
||||||
ClaveHistorialLocal,
|
ClaveHistorialLocal,
|
||||||
jsonRecuperado,
|
preparado.Json,
|
||||||
"guardado del historial recuperado");
|
"guardado del historial recuperado",
|
||||||
|
preparado.Items.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PersistirHistorialLocalAsync()
|
private async Task PersistirHistorialLocalAsync()
|
||||||
@@ -363,11 +447,34 @@ public partial class PlanificadorRutas
|
|||||||
if (!_historialLocalCargado)
|
if (!_historialLocalCargado)
|
||||||
return;
|
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(
|
if (!await GuardarValorHistorialLocalAsync(
|
||||||
ClaveHistorialLocal,
|
ClaveHistorialLocal,
|
||||||
json,
|
preparado.Json,
|
||||||
"persistencia del historial"))
|
"persistencia del historial",
|
||||||
|
preparado.Items.Count))
|
||||||
{
|
{
|
||||||
MostrarMensajeHistorial(
|
MostrarMensajeHistorial(
|
||||||
"No se ha podido guardar el historial personal. Consulta el diagnostico.",
|
"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
|
try
|
||||||
{
|
{
|
||||||
using var cts = new CancellationTokenSource(
|
using var cts = new CancellationTokenSource(
|
||||||
@@ -394,7 +509,7 @@ public partial class PlanificadorRutas
|
|||||||
fase,
|
fase,
|
||||||
resultado?.ErrorName ?? "RespuestaVacia",
|
resultado?.ErrorName ?? "RespuestaVacia",
|
||||||
resultado?.ErrorMessage ?? "JavaScript no devolvio un resultado valido al guardar.",
|
resultado?.ErrorMessage ?? "JavaScript no devolvio un resultado valido al guardar.",
|
||||||
$"Tamano que se intentaba guardar: {valor.Length} caracteres.");
|
contexto);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
@@ -404,7 +519,7 @@ public partial class PlanificadorRutas
|
|||||||
new TimeoutException(
|
new TimeoutException(
|
||||||
$"La escritura no respondio en {TiempoMaxOperacionHistorialLocalSegundos} segundos.",
|
$"La escritura no respondio en {TiempoMaxOperacionHistorialLocalSegundos} segundos.",
|
||||||
ex),
|
ex),
|
||||||
$"Tamano que se intentaba guardar: {valor.Length} caracteres.");
|
contexto);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -412,7 +527,7 @@ public partial class PlanificadorRutas
|
|||||||
RegistrarErrorHistorialLocal(
|
RegistrarErrorHistorialLocal(
|
||||||
fase,
|
fase,
|
||||||
ex,
|
ex,
|
||||||
$"Tamano que se intentaba guardar: {valor.Length} caracteres.");
|
contexto);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -618,9 +733,6 @@ public partial class PlanificadorRutas
|
|||||||
_historialLocal.Insert(0, existente);
|
_historialLocal.Insert(0, existente);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (_historialLocal.Count > MaxItemsHistorialLocal)
|
|
||||||
_historialLocal.RemoveAt(_historialLocal.Count - 1);
|
|
||||||
|
|
||||||
await PersistirHistorialLocalAsync();
|
await PersistirHistorialLocalAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,9 +747,10 @@ public partial class PlanificadorRutas
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var numeroPersonal = _historialLocal.FindIndex(x => x.Id == id) + 1;
|
||||||
await AplicarRegistroHistorialAsync(item);
|
await AplicarRegistroHistorialAsync(item);
|
||||||
SeleccionarHistorialLocal(id);
|
SeleccionarHistorialLocal(id);
|
||||||
MostrarMensajeHistorial("Combinacion personal cargada.");
|
MostrarMensajeHistorial($"Historial Personal Cargado: n.º {numeroPersonal}.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -660,9 +773,10 @@ public partial class PlanificadorRutas
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var numeroCompartido = _historialCompartido.FindIndex(x => x.Id == id) + 1;
|
||||||
await AplicarRegistroHistorialAsync(item);
|
await AplicarRegistroHistorialAsync(item);
|
||||||
SeleccionarHistorialCompartido(id);
|
SeleccionarHistorialCompartido(id);
|
||||||
MostrarMensajeHistorial("Combinación compartida cargada.");
|
MostrarMensajeHistorial($"Historial Compartido Cargado: n.º {numeroCompartido}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SeleccionarHistorialLocal(string id)
|
private void SeleccionarHistorialLocal(string id)
|
||||||
|
|||||||
@@ -347,43 +347,41 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
var myToken = Interlocked.Increment(ref _paradaPopupToken);
|
var myToken = Interlocked.Increment(ref _paradaPopupToken);
|
||||||
|
|
||||||
_paradaClicada = null;
|
_paradaClicada = null;
|
||||||
_paradaElevG = null;
|
_paradaElevIdee = null;
|
||||||
|
_paradaElevLocal5 = null;
|
||||||
|
_paradaElevLocal2 = null;
|
||||||
|
_paradaElevLocal1 = null;
|
||||||
|
|
||||||
// limpiar horarios del popup
|
// limpiar horarios del popup
|
||||||
_nextDeps.Clear();
|
_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();
|
StateHasChanged();
|
||||||
|
|
||||||
double? elevG = null;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var kIdee = ClaveElevacionIdee(paradaDetectada.Latitud, paradaDetectada.Longitud);
|
if (!_elevIdeeCache.ContainsKey(clave))
|
||||||
|
|
||||||
if (_elevIdeeCache.TryGetValue(kIdee, out var g))
|
|
||||||
{
|
|
||||||
elevG = g;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
await ObtenerElevacionesIdeePorLoteAsync(
|
await ObtenerElevacionesIdeePorLoteAsync(
|
||||||
new List<(double lat, double lon)> { (paradaDetectada.Latitud, paradaDetectada.Longitud) },
|
new List<(double lat, double lon)> { punto },
|
||||||
CancellationToken.None);
|
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
|
catch
|
||||||
{
|
{
|
||||||
elevG = null;
|
_paradaElevIdee = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (myToken != _paradaPopupToken) return;
|
if (myToken != _paradaPopupToken) return;
|
||||||
|
StateHasChanged();
|
||||||
_paradaElevG = elevG;
|
|
||||||
_paradaClicada = paradaDetectada;
|
|
||||||
|
|
||||||
// ==========================
|
// ==========================
|
||||||
// HORARIOS (ZIP) -> usando tu AhoraLocal() (real o simulada)
|
// HORARIOS (ZIP) -> usando tu AhoraLocal() (real o simulada)
|
||||||
@@ -1204,10 +1202,13 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
private bool _elevPopupMinimizado = false;
|
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.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
using RutasDBUS.Modelos.Planificacion;
|
||||||
|
|
||||||
namespace RutasDBUS.Components.Pages;
|
namespace RutasDBUS.Components.Pages;
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ public partial class PlanificadorRutas
|
|||||||
{
|
{
|
||||||
private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1";
|
private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1";
|
||||||
private const int VersionPersistenciaFactorEquilibrado = 2;
|
private const int VersionPersistenciaFactorEquilibrado = 2;
|
||||||
|
private const int VersionPersistenciaPendientes = 2;
|
||||||
private bool _configuracionLocalCargada = false;
|
private bool _configuracionLocalCargada = false;
|
||||||
private bool _mostrarConfirmacionRestablecer = false;
|
private bool _mostrarConfirmacionRestablecer = false;
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ public partial class PlanificadorRutas
|
|||||||
public bool ActivarRangoItinerarios { get; set; }
|
public bool ActivarRangoItinerarios { get; set; }
|
||||||
public int ItinerarioMin { get; set; }
|
public int ItinerarioMin { get; set; }
|
||||||
public int ItinerarioMax { get; set; }
|
public int ItinerarioMax { get; set; }
|
||||||
|
public string? FiltroSentidoItinerario { get; set; }
|
||||||
public bool ActivarRangoTransbordos { get; set; }
|
public bool ActivarRangoTransbordos { get; set; }
|
||||||
public int MinTransbordosPermitidos { get; set; }
|
public int MinTransbordosPermitidos { get; set; }
|
||||||
public int MaxTransbordosPermitidos { get; set; }
|
public int MaxTransbordosPermitidos { get; set; }
|
||||||
@@ -39,6 +42,8 @@ public partial class PlanificadorRutas
|
|||||||
public double? FactorEquilibradoAndarBus { get; set; }
|
public double? FactorEquilibradoAndarBus { get; set; }
|
||||||
public int? VersionFactorEquilibrado { get; set; }
|
public int? VersionFactorEquilibrado { get; set; }
|
||||||
public bool? MostrarElevacionesRuta { get; set; }
|
public bool? MostrarElevacionesRuta { get; set; }
|
||||||
|
public string? FuenteElevacionCalculo { get; set; }
|
||||||
|
public int? VersionPendientes { get; set; }
|
||||||
public double? UmbralPendienteSubidaRuta { get; set; }
|
public double? UmbralPendienteSubidaRuta { get; set; }
|
||||||
public double? UmbralPendienteBajadaRuta { get; set; }
|
public double? UmbralPendienteBajadaRuta { get; set; }
|
||||||
public double? UmbralAlturaAlertaPendienteRutaMetros { get; set; }
|
public double? UmbralAlturaAlertaPendienteRutaMetros { get; set; }
|
||||||
@@ -117,6 +122,12 @@ public partial class PlanificadorRutas
|
|||||||
await PersistirConfiguracionAsync();
|
await PersistirConfiguracionAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnFuenteElevacionChangedAsync()
|
||||||
|
{
|
||||||
|
_fuenteElevacionCalculo = NormalizarFuenteElevacion(_fuenteElevacionCalculo);
|
||||||
|
await PersistirConfiguracionAsync();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Construye la instantánea persistible a partir del estado actual.
|
/// Construye la instantánea persistible a partir del estado actual.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -133,6 +144,7 @@ public partial class PlanificadorRutas
|
|||||||
ActivarRangoItinerarios = _activarRangoItinerarios,
|
ActivarRangoItinerarios = _activarRangoItinerarios,
|
||||||
ItinerarioMin = _itinerarioMin,
|
ItinerarioMin = _itinerarioMin,
|
||||||
ItinerarioMax = _itinerarioMax,
|
ItinerarioMax = _itinerarioMax,
|
||||||
|
FiltroSentidoItinerario = _filtroSentidoItinerario.ToString(),
|
||||||
ActivarRangoTransbordos = _activarRangoTransbordos,
|
ActivarRangoTransbordos = _activarRangoTransbordos,
|
||||||
MinTransbordosPermitidos = _minTransbordosPermitidos,
|
MinTransbordosPermitidos = _minTransbordosPermitidos,
|
||||||
MaxTransbordosPermitidos = _maxTransbordosPermitidos,
|
MaxTransbordosPermitidos = _maxTransbordosPermitidos,
|
||||||
@@ -145,7 +157,9 @@ public partial class PlanificadorRutas
|
|||||||
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
|
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
|
||||||
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
|
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
|
||||||
VersionFactorEquilibrado = VersionPersistenciaFactorEquilibrado,
|
VersionFactorEquilibrado = VersionPersistenciaFactorEquilibrado,
|
||||||
MostrarElevacionesRuta = _mostrarElevacionesRuta,
|
MostrarElevacionesRuta = _mostrarElevacionesRutaConfiguracion,
|
||||||
|
FuenteElevacionCalculo = _fuenteElevacionCalculo,
|
||||||
|
VersionPendientes = VersionPersistenciaPendientes,
|
||||||
UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta,
|
UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta,
|
||||||
UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta,
|
UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta,
|
||||||
UmbralAlturaAlertaPendienteRutaMetros = _umbralAlturaAlertaPendienteRutaMetros,
|
UmbralAlturaAlertaPendienteRutaMetros = _umbralAlturaAlertaPendienteRutaMetros,
|
||||||
@@ -182,6 +196,7 @@ public partial class PlanificadorRutas
|
|||||||
_activarRangoItinerarios = config.ActivarRangoItinerarios;
|
_activarRangoItinerarios = config.ActivarRangoItinerarios;
|
||||||
_itinerarioMin = Math.Clamp(config.ItinerarioMin, 1, 99);
|
_itinerarioMin = Math.Clamp(config.ItinerarioMin, 1, 99);
|
||||||
_itinerarioMax = Math.Clamp(config.ItinerarioMax, 1, 99);
|
_itinerarioMax = Math.Clamp(config.ItinerarioMax, 1, 99);
|
||||||
|
_filtroSentidoItinerario = NormalizarFiltroSentidoItinerario(config.FiltroSentidoItinerario);
|
||||||
|
|
||||||
_activarRangoTransbordos = config.ActivarRangoTransbordos;
|
_activarRangoTransbordos = config.ActivarRangoTransbordos;
|
||||||
_minTransbordosPermitidos = Math.Clamp(config.MinTransbordosPermitidos, 0, 10);
|
_minTransbordosPermitidos = Math.Clamp(config.MinTransbordosPermitidos, 0, 10);
|
||||||
@@ -217,7 +232,17 @@ public partial class PlanificadorRutas
|
|||||||
Math.Abs(factorEquilibrado - 12.0) < 0.0001
|
Math.Abs(factorEquilibrado - 12.0) < 0.0001
|
||||||
? FactorEquilibradoAndarBusPorDefecto
|
? FactorEquilibradoAndarBusPorDefecto
|
||||||
: Math.Max(0, factorEquilibrado);
|
: 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))
|
if (config.UmbralPendienteSubidaRuta is { } umbralSubida && double.IsFinite(umbralSubida))
|
||||||
_umbralPendienteSubidaRuta = Math.Abs(umbralSubida - 3.0) < 0.0001
|
_umbralPendienteSubidaRuta = Math.Abs(umbralSubida - 3.0) < 0.0001
|
||||||
? UmbralPendienteSubidaRutaPorDefecto
|
? UmbralPendienteSubidaRutaPorDefecto
|
||||||
@@ -262,6 +287,12 @@ public partial class PlanificadorRutas
|
|||||||
SincronizarSegundosPorParadaConHorarios();
|
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(
|
private static IReadOnlyList<int> NormalizarLineasPermitidas(
|
||||||
IReadOnlyList<int>? lineas,
|
IReadOnlyList<int>? lineas,
|
||||||
int lineaMinLegacy,
|
int lineaMinLegacy,
|
||||||
@@ -331,6 +362,7 @@ public partial class PlanificadorRutas
|
|||||||
_activarRangoItinerarios = true;
|
_activarRangoItinerarios = true;
|
||||||
_itinerarioMin = 1;
|
_itinerarioMin = 1;
|
||||||
_itinerarioMax = 99;
|
_itinerarioMax = 99;
|
||||||
|
_filtroSentidoItinerario = FiltroSentidoItinerario.Ambos;
|
||||||
|
|
||||||
_activarRangoTransbordos = true;
|
_activarRangoTransbordos = true;
|
||||||
_minTransbordosPermitidos = 0;
|
_minTransbordosPermitidos = 0;
|
||||||
@@ -341,7 +373,8 @@ public partial class PlanificadorRutas
|
|||||||
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
||||||
_pesoTransbordoRankingMinutos = 4.0;
|
_pesoTransbordoRankingMinutos = 4.0;
|
||||||
_factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
|
_factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
|
||||||
_mostrarElevacionesRuta = false;
|
_mostrarElevacionesRutaConfiguracion = true;
|
||||||
|
_fuenteElevacionCalculo = FuenteElevacionLocal1;
|
||||||
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
|
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
|
||||||
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
|
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
|
||||||
_umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
|
_umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
|
||||||
@@ -382,6 +415,7 @@ public partial class PlanificadorRutas
|
|||||||
_activarRangoItinerarios = true;
|
_activarRangoItinerarios = true;
|
||||||
_itinerarioMin = 1;
|
_itinerarioMin = 1;
|
||||||
_itinerarioMax = 99;
|
_itinerarioMax = 99;
|
||||||
|
_filtroSentidoItinerario = FiltroSentidoItinerario.Ambos;
|
||||||
|
|
||||||
_activarRangoTransbordos = true;
|
_activarRangoTransbordos = true;
|
||||||
_minTransbordosPermitidos = 0;
|
_minTransbordosPermitidos = 0;
|
||||||
@@ -392,7 +426,8 @@ public partial class PlanificadorRutas
|
|||||||
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
||||||
_pesoTransbordoRankingMinutos = 4.0;
|
_pesoTransbordoRankingMinutos = 4.0;
|
||||||
_factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
|
_factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
|
||||||
_mostrarElevacionesRuta = false;
|
_mostrarElevacionesRutaConfiguracion = true;
|
||||||
|
_fuenteElevacionCalculo = FuenteElevacionLocal1;
|
||||||
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
|
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
|
||||||
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
|
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
|
||||||
_umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
|
_umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using RutasDBUS.Modelos.Planificacion;
|
|||||||
using RutasDBUS.Modelos.RedBus;
|
using RutasDBUS.Modelos.RedBus;
|
||||||
using RutasDBUS.Servicios;
|
using RutasDBUS.Servicios;
|
||||||
using RutasDBUS.Servicios.Caminata;
|
using RutasDBUS.Servicios.Caminata;
|
||||||
|
using RutasDBUS.Servicios.Elevacion;
|
||||||
using RutasDBUS.Servicios.Horarios;
|
using RutasDBUS.Servicios.Horarios;
|
||||||
using RutasDBUS.Servicios.Planificacion;
|
using RutasDBUS.Servicios.Planificacion;
|
||||||
using RutasDBUS.Servicios.RedBus;
|
using RutasDBUS.Servicios.RedBus;
|
||||||
@@ -104,8 +105,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Actualiza las alturas (Z) de origen y destino para el resumen del detalle de ruta.
|
/// Actualiza las alturas de origen y destino con la fuente seleccionada para el calculo.
|
||||||
/// Usa la fuente G (IDEE).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task ActualizarAlturasResumenRutaAsync(CancellationToken ct)
|
private async Task ActualizarAlturasResumenRutaAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -121,24 +121,11 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var faltanIdee = new List<(double lat, double lon)>(2);
|
var puntos = new List<(double lat, double lon)> { origen, destino };
|
||||||
if (!_elevIdeeCache.ContainsKey(ClaveElevacionIdee(origen.Item1, origen.Item2)))
|
await AsegurarElevacionesFuenteCalculoAsync(puntos, ct);
|
||||||
faltanIdee.Add(origen);
|
|
||||||
if (!_elevIdeeCache.ContainsKey(ClaveElevacionIdee(destino.Item1, destino.Item2)))
|
|
||||||
faltanIdee.Add(destino);
|
|
||||||
|
|
||||||
if (faltanIdee.Count > 0)
|
_altitudOrigenResumenRuta = ObtenerElevacionFuenteCalculo(origen.Item1, origen.Item2);
|
||||||
await ObtenerElevacionesIdeePorLoteAsync(faltanIdee, CancellationToken.None);
|
_altitudDestinoResumenRuta = ObtenerElevacionFuenteCalculo(destino.Item1, destino.Item2);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -186,7 +173,22 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
|
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<string, double> _elevIdeeCache =
|
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
|
// IDEE: en el correo indican que 6 decimales es suficiente
|
||||||
private const int ELEV_IDEE_DECIMALES_CACHE = 6;
|
private const int ELEV_IDEE_DECIMALES_CACHE = 6;
|
||||||
@@ -201,6 +203,157 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
return $"{rLat.ToString("0.000000", CultureInfo.InvariantCulture)},{rLon.ToString("0.000000", CultureInfo.InvariantCulture)}";
|
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
|
private sealed class RespuestaElevacionIdee
|
||||||
{
|
{
|
||||||
public string? id { get; set; }
|
public string? id { get; set; }
|
||||||
@@ -212,7 +365,21 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task ObtenerElevacionesIdeePorLoteAsync(List<(double lat, double lon)> points, CancellationToken ct)
|
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]
|
// Construir FeatureCollection GeoJSON, OJO: coordinates = [lon, lat]
|
||||||
var features = new List<object>(points.Count);
|
var features = new List<object>(points.Count);
|
||||||
@@ -259,14 +426,20 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
using var limite = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
|
limite.CancelAfter(TimeSpan.FromSeconds(TiempoMaximoIdeeSegundos));
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Post, url);
|
using var req = new HttpRequestMessage(HttpMethod.Post, url);
|
||||||
req.Headers.Accept.ParseAdd("application/json");
|
req.Headers.Accept.ParseAdd("application/json");
|
||||||
req.Content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json");
|
req.Content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
using var resp = await Http.SendAsync(req, ct);
|
using var resp = await Http.SendAsync(req, limite.Token);
|
||||||
if (!resp.IsSuccessStatusCode) return;
|
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
|
var data = JsonSerializer.Deserialize<RespuestaElevacionIdee>(json, new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
@@ -283,14 +456,33 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
var k = ClaveElevacionIdee(lat, lon);
|
var k = ClaveElevacionIdee(lat, lon);
|
||||||
_elevIdeeCache[k] = data.values[i];
|
_elevIdeeCache[k] = data.values[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref _ideeReintentarDespuesTicksUtc, 0);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
PosponerConsultasIdee(TimeSpan.FromSeconds(45));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
PosponerConsultasIdee(TimeSpan.FromSeconds(20));
|
||||||
// silencioso: elevación es informativa
|
// 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)
|
// Ajustes (rendimiento)
|
||||||
/* private const int ELEV_MAX_PUNTOS_POR_TRAMO = 24; */
|
/* private const int ELEV_MAX_PUNTOS_POR_TRAMO = 24; */
|
||||||
@@ -340,21 +532,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
private async Task<List<MuestraElevacion>> ConstruirMuestrasElevacionAsync(double[,] line, CancellationToken ct)
|
private async Task<List<MuestraElevacion>> ConstruirMuestrasElevacionAsync(double[,] line, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var sampled = SampleLine(line, _elevMaxPuntosPorTramo);
|
var sampled = SampleLine(line, _elevMaxPuntosPorTramo);
|
||||||
|
await AsegurarElevacionesFuenteCalculoAsync(sampled, ct);
|
||||||
// --- 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ConstruirMuestrasElevacionDesdeCache(line);
|
return ConstruirMuestrasElevacionDesdeCache(line);
|
||||||
}
|
}
|
||||||
@@ -367,13 +545,25 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
{
|
{
|
||||||
var (lat, lon) = sampled[i];
|
var (lat, lon) = sampled[i];
|
||||||
var elevIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var e2) ? e2 : (double?)null;
|
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
|
res.Add(new MuestraElevacion
|
||||||
{
|
{
|
||||||
Indice = i,
|
Indice = i,
|
||||||
Latitud = lat,
|
Latitud = lat,
|
||||||
Longitud = lon,
|
Longitud = lon,
|
||||||
ElevacionIdee = elevIdee
|
ElevacionIdee = elevIdee,
|
||||||
|
ElevacionLocal5 = elevLocal5,
|
||||||
|
ElevacionLocal2 = elevLocal2,
|
||||||
|
ElevacionLocal1 = elevLocal1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,24 +603,15 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
var clave = ClaveElevacionIdee(lat, lon);
|
var clave = ClaveElevacionIdee(lat, lon);
|
||||||
if (!_elevIdeeCache.ContainsKey(clave))
|
puntosPendientes.TryAdd(
|
||||||
{
|
clave,
|
||||||
puntosPendientes.TryAdd(
|
(Math.Round(lat, ELEV_IDEE_DECIMALES_CACHE), Math.Round(lon, ELEV_IDEE_DECIMALES_CACHE)));
|
||||||
clave,
|
|
||||||
(Math.Round(lat, ELEV_IDEE_DECIMALES_CACHE), Math.Round(lon, ELEV_IDEE_DECIMALES_CACHE)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var puntos = puntosPendientes.Values.ToList();
|
var puntos = puntosPendientes.Values.ToList();
|
||||||
for (var i = 0; i < puntos.Count; i += ELEV_BATCH_MAX_PUNTOS)
|
await AsegurarElevacionesFuenteCalculoAsync(puntos, ct);
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
await ObtenerElevacionesIdeePorLoteAsync(
|
|
||||||
puntos.Skip(i).Take(ELEV_BATCH_MAX_PUNTOS).ToList(),
|
|
||||||
ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var item in lineasPorAlternativa)
|
foreach (var item in lineasPorAlternativa)
|
||||||
{
|
{
|
||||||
@@ -1029,12 +1210,12 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
|
|
||||||
if (string.Equals(criterio, CriterioRutaAndar, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(criterio, CriterioRutaAndar, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return BuscarPrimerIndiceBusOrdenadoPor(
|
return BuscarPrimerIndiceOrdenadoPor(
|
||||||
opcion => CalcularCosteAndarEquilibradoSegundos(opcion),
|
opcion => CalcularCosteAndarEquilibradoSegundos(opcion),
|
||||||
opcion => DuracionOrdenacion(opcion));
|
opcion => DuracionOrdenacion(opcion));
|
||||||
}
|
}
|
||||||
|
|
||||||
return BuscarPrimerIndiceBusOrdenadoPor(
|
return BuscarPrimerIndiceOrdenadoPor(
|
||||||
opcion => DuracionOrdenacion(opcion),
|
opcion => DuracionOrdenacion(opcion),
|
||||||
opcion => TiempoPieOrdenacion(opcion));
|
opcion => TiempoPieOrdenacion(opcion));
|
||||||
}
|
}
|
||||||
@@ -1043,7 +1224,6 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
{
|
{
|
||||||
var candidatas = _opcionesRuta
|
var candidatas = _opcionesRuta
|
||||||
.Select((opcion, indice) => new { opcion, indice })
|
.Select((opcion, indice) => new { opcion, indice })
|
||||||
.Where(x => !x.opcion.EsSoloAPie)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (candidatas.Count == 0)
|
if (candidatas.Count == 0)
|
||||||
@@ -1063,38 +1243,33 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
if (_opcionesRuta.Count <= 1)
|
if (_opcionesRuta.Count <= 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IEnumerable<AlternativaRuta> rutasBus = _opcionesRuta.Where(opcion => !opcion.EsSoloAPie);
|
IEnumerable<AlternativaRuta> ordenadas = _opcionesRuta;
|
||||||
if (CriterioPrioritarioAndarActivo)
|
if (CriterioPrioritarioAndarActivo)
|
||||||
{
|
{
|
||||||
rutasBus = rutasBus
|
ordenadas = ordenadas
|
||||||
.OrderBy(CalcularCosteAndarEquilibradoSegundos)
|
.OrderBy(CalcularCosteAndarEquilibradoSegundos)
|
||||||
.ThenBy(DuracionOrdenacion)
|
.ThenBy(DuracionOrdenacion)
|
||||||
.ThenBy(opcion => opcion.PuntuacionCoste);
|
.ThenBy(opcion => opcion.PuntuacionCoste);
|
||||||
}
|
}
|
||||||
else if (CriterioPrioritarioTiempoActivo)
|
else if (CriterioPrioritarioTiempoActivo)
|
||||||
{
|
{
|
||||||
rutasBus = rutasBus
|
ordenadas = ordenadas
|
||||||
.OrderBy(DuracionOrdenacion)
|
.OrderBy(DuracionOrdenacion)
|
||||||
.ThenBy(TiempoPieOrdenacion)
|
.ThenBy(TiempoPieOrdenacion)
|
||||||
.ThenBy(opcion => opcion.PuntuacionCoste);
|
.ThenBy(opcion => opcion.PuntuacionCoste);
|
||||||
}
|
}
|
||||||
else if (CriterioPrioritarioEquilibradoActivo)
|
else if (CriterioPrioritarioEquilibradoActivo)
|
||||||
{
|
{
|
||||||
rutasBus = rutasBus
|
ordenadas = ordenadas
|
||||||
.OrderBy(CalcularScoreEquilibradoFactor)
|
.OrderBy(CalcularScoreEquilibradoFactor)
|
||||||
.ThenBy(opcion => opcion.NumeroTransbordosReales)
|
.ThenBy(opcion => opcion.NumeroTransbordosReales)
|
||||||
.ThenBy(opcion => opcion.TiempoEsperaTotalSegundos)
|
.ThenBy(opcion => opcion.TiempoEsperaTotalSegundos)
|
||||||
.ThenBy(opcion => opcion.PuntuacionCoste);
|
.ThenBy(opcion => opcion.PuntuacionCoste);
|
||||||
}
|
}
|
||||||
|
|
||||||
var soloPie = _opcionesRuta
|
var listaOrdenada = ordenadas.ToList();
|
||||||
.Where(opcion => opcion.EsSoloAPie)
|
|
||||||
.OrderBy(DuracionOrdenacion)
|
|
||||||
.ThenBy(opcion => opcion.DistanciaSoloAPie);
|
|
||||||
|
|
||||||
var ordenadas = rutasBus.Concat(soloPie).ToList();
|
|
||||||
_opcionesRuta.Clear();
|
_opcionesRuta.Clear();
|
||||||
_opcionesRuta.AddRange(ordenadas);
|
_opcionesRuta.AddRange(listaOrdenada);
|
||||||
RenumerarEtiquetasGenericasOpcionesRuta();
|
RenumerarEtiquetasGenericasOpcionesRuta();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1121,23 +1296,17 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int BuscarPrimerIndiceBusOrdenadoPor(
|
private int BuscarPrimerIndiceOrdenadoPor(
|
||||||
Func<AlternativaRuta, double> criterioPrincipal,
|
Func<AlternativaRuta, double> criterioPrincipal,
|
||||||
Func<AlternativaRuta, double> criterioSecundario)
|
Func<AlternativaRuta, double> criterioSecundario)
|
||||||
{
|
{
|
||||||
var indiceBus = _opcionesRuta
|
return _opcionesRuta
|
||||||
.Select((opcion, indice) => new { opcion, indice })
|
.Select((opcion, indice) => new { opcion, indice })
|
||||||
.Where(x => !x.opcion.EsSoloAPie)
|
|
||||||
.OrderBy(x => criterioPrincipal(x.opcion))
|
.OrderBy(x => criterioPrincipal(x.opcion))
|
||||||
.ThenBy(x => criterioSecundario(x.opcion))
|
.ThenBy(x => criterioSecundario(x.opcion))
|
||||||
.ThenBy(x => x.opcion.PuntuacionCoste)
|
.ThenBy(x => x.opcion.PuntuacionCoste)
|
||||||
.Select(x => x.indice)
|
.Select(x => x.indice)
|
||||||
.FirstOrDefault(-1);
|
.FirstOrDefault(-1);
|
||||||
|
|
||||||
if (indiceBus >= 0)
|
|
||||||
return indiceBus;
|
|
||||||
|
|
||||||
return _opcionesRuta.Count > 0 ? 0 : -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double DuracionOrdenacion(AlternativaRuta alternativa)
|
private double DuracionOrdenacion(AlternativaRuta alternativa)
|
||||||
@@ -1240,9 +1409,24 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
_inspectorLat = lat;
|
_inspectorLat = lat;
|
||||||
_inspectorLon = lon;
|
_inspectorLon = lon;
|
||||||
_inspectorElevIdee = null;
|
_inspectorElevIdee = null;
|
||||||
|
_inspectorElevLocal5 = null;
|
||||||
|
_inspectorElevLocal2 = null;
|
||||||
|
_inspectorElevLocal1 = null;
|
||||||
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
|
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
|
||||||
_popupInspectorVisible = true;
|
_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();
|
await ActualizarMarcadoresInspectorJsAsync();
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
@@ -1558,6 +1742,8 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null)
|
if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
_mostrarElevacionesRuta = _mostrarElevacionesRutaConfiguracion;
|
||||||
|
|
||||||
if (!_aplicandoRegistroHistorial)
|
if (!_aplicandoRegistroHistorial)
|
||||||
LimpiarSeleccionHistorial();
|
LimpiarSeleccionHistorial();
|
||||||
|
|
||||||
@@ -1613,6 +1799,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
ActivarRangoItinerarios = _activarRangoItinerarios,
|
ActivarRangoItinerarios = _activarRangoItinerarios,
|
||||||
ItinerarioMin = _itinerarioMin,
|
ItinerarioMin = _itinerarioMin,
|
||||||
ItinerarioMax = _itinerarioMax,
|
ItinerarioMax = _itinerarioMax,
|
||||||
|
FiltroSentidoItinerario = _filtroSentidoItinerario,
|
||||||
ActivarRangoTransbordos = _activarRangoTransbordos,
|
ActivarRangoTransbordos = _activarRangoTransbordos,
|
||||||
MinTransbordosPermitidos = _minTransbordosPermitidos,
|
MinTransbordosPermitidos = _minTransbordosPermitidos,
|
||||||
MaxTransbordosPermitidos = _maxTransbordosPermitidos,
|
MaxTransbordosPermitidos = _maxTransbordosPermitidos,
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
{
|
{
|
||||||
if (tramo is null) return;
|
if (tramo is null) return;
|
||||||
|
|
||||||
// Si ya está calculado, nada
|
// Si la fuente seleccionada ya está calculada, no hace falta cargar las demás.
|
||||||
if (tramo.MuestrasElevacion is not null && tramo.MuestrasElevacion.Count > 0) return;
|
if (tramo.MuestrasElevacion is { Count: > 0 }
|
||||||
|
&& tramo.MuestrasElevacion.All(muestra => ObtenerElevacionFuenteCalculo(muestra) is not null))
|
||||||
|
return;
|
||||||
|
|
||||||
// Necesitamos la geometría
|
// Necesitamos la geometría
|
||||||
if (tramo.Linea is null || tramo.Linea.GetLength(0) <= 0) return;
|
if (tramo.Linea is null || tramo.Linea.GetLength(0) <= 0) return;
|
||||||
@@ -53,19 +55,19 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
{
|
{
|
||||||
var ctNone = CancellationToken.None;
|
var ctNone = CancellationToken.None;
|
||||||
|
|
||||||
// Samples bajo demanda usando la fuente G.
|
|
||||||
var samples = await ConstruirMuestrasElevacionAsync(tramo.Linea, ctNone);
|
var samples = await ConstruirMuestrasElevacionAsync(tramo.Linea, ctNone);
|
||||||
tramo.MuestrasElevacion = samples;
|
tramo.MuestrasElevacion = samples;
|
||||||
|
|
||||||
var muestrasIdee = samples
|
var muestrasFuenteCalculo = samples
|
||||||
.Where(x => x.ElevacionIdee is not null)
|
.Select(ObtenerElevacionFuenteCalculo)
|
||||||
.Select(x => x.ElevacionIdee!.Value)
|
.Where(elevacion => elevacion is not null)
|
||||||
|
.Select(elevacion => elevacion!.Value)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
tramo.ElevInicio = samples.FirstOrDefault()?.ElevacionIdee;
|
tramo.ElevInicio = samples.Count > 0 ? ObtenerElevacionFuenteCalculo(samples[0]) : null;
|
||||||
tramo.ElevFin = samples.LastOrDefault()?.ElevacionIdee;
|
tramo.ElevFin = samples.Count > 0 ? ObtenerElevacionFuenteCalculo(samples[^1]) : null;
|
||||||
tramo.ElevMin = muestrasIdee.Count > 0 ? muestrasIdee.Min() : null;
|
tramo.ElevMin = muestrasFuenteCalculo.Count > 0 ? muestrasFuenteCalculo.Min() : null;
|
||||||
tramo.ElevMax = muestrasIdee.Count > 0 ? muestrasIdee.Max() : null;
|
tramo.ElevMax = muestrasFuenteCalculo.Count > 0 ? muestrasFuenteCalculo.Max() : null;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -85,36 +87,15 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
{
|
{
|
||||||
bool valor => valor,
|
bool valor => valor,
|
||||||
string texto when bool.TryParse(texto, out var valor) => valor,
|
string texto when bool.TryParse(texto, out var valor) => valor,
|
||||||
_ => !_mostrarElevacionesRuta
|
_ => !_mostrarElevacionesRutaConfiguracion
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_mostrarElevacionesRuta == mostrarElevaciones)
|
if (_mostrarElevacionesRutaConfiguracion == mostrarElevaciones)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_mostrarElevacionesRuta = mostrarElevaciones;
|
_mostrarElevacionesRutaConfiguracion = mostrarElevaciones;
|
||||||
await PersistirConfiguracionAsync();
|
await PersistirConfiguracionAsync();
|
||||||
if (_opcionesRuta.Count == 0)
|
|
||||||
{
|
|
||||||
StateHasChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_pintandoElevacionesRuta = true;
|
|
||||||
_mensajeRuta = _mostrarElevacionesRuta
|
|
||||||
? "Aplicando pendientes..."
|
|
||||||
: "Recalculando sin pendientes...";
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
await Task.Yield();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await RecalcularRankingTrasCambioEsfuerzoPendienteAsync(forzarRepintado: true);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_pintandoElevacionesRuta = false;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CambiarUmbralElevacionRutaAsync(ChangeEventArgs e, bool esSubida)
|
private async Task CambiarUmbralElevacionRutaAsync(ChangeEventArgs e, bool esSubida)
|
||||||
@@ -130,15 +111,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
_umbralPendienteBajadaRuta = nuevo;
|
_umbralPendienteBajadaRuta = nuevo;
|
||||||
|
|
||||||
await PersistirConfiguracionAsync();
|
await PersistirConfiguracionAsync();
|
||||||
|
StateHasChanged();
|
||||||
if (esSubida && _mostrarElevacionesRuta)
|
|
||||||
{
|
|
||||||
await RecalcularRankingTrasCambioEsfuerzoPendienteAsync();
|
|
||||||
}
|
|
||||||
else if (_mostrarElevacionesRuta)
|
|
||||||
await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando pendientes...");
|
|
||||||
else
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RecalcularRankingTrasCambioEsfuerzoPendienteAsync(bool forzarRepintado = false)
|
private async Task RecalcularRankingTrasCambioEsfuerzoPendienteAsync(bool forzarRepintado = false)
|
||||||
@@ -385,10 +358,12 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
distanciaTotal += distancia;
|
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;
|
continue;
|
||||||
|
|
||||||
var diferenciaAltura = actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value;
|
var diferenciaAltura = elevacionActual.Value - elevacionAnterior.Value;
|
||||||
var pendiente = diferenciaAltura / distancia * 100.0;
|
var pendiente = diferenciaAltura / distancia * 100.0;
|
||||||
var esfuerzo = CalcularEsfuerzoElevacion(distancia, pendiente);
|
var esfuerzo = CalcularEsfuerzoElevacion(distancia, pendiente);
|
||||||
if (esfuerzo is null)
|
if (esfuerzo is null)
|
||||||
@@ -427,14 +402,22 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
return $"{(distanciaMetros / 1000.0):0.000} km";
|
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()
|
private void ReiniciarResumenPendientesRuta()
|
||||||
{
|
{
|
||||||
_alturaPendientePositivaSignificativaRutaMetros = 0.0;
|
_alturaPendientePositivaSignificativaRutaMetros = 0.0;
|
||||||
_distanciaPendientePositivaSignificativaRutaMetros = 0.0;
|
_distanciaPendientePositivaSignificativaRutaMetros = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double? ElevacionPreferidaMapa(MuestraElevacion muestra)
|
private double? ElevacionPreferidaMapa(MuestraElevacion muestra)
|
||||||
=> muestra.ElevacionIdee;
|
=> ObtenerElevacionFuenteCalculo(muestra);
|
||||||
|
|
||||||
private (string Categoria, string Color) ClasificarPendienteMapa(double? pendientePorcentaje)
|
private (string Categoria, string Color) ClasificarPendienteMapa(double? pendientePorcentaje)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -248,11 +248,23 @@
|
|||||||
}
|
}
|
||||||
@if (_paradaClicada.Altura > 0)
|
@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)
|
@if (_usarHorariosTeoricos)
|
||||||
@@ -303,7 +315,7 @@
|
|||||||
|
|
||||||
</div>
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -347,10 +359,46 @@
|
|||||||
<div><b>Latitud:</b> @_inspectorLat.ToString("0.000000")</div>
|
<div><b>Latitud:</b> @_inspectorLat.ToString("0.000000")</div>
|
||||||
<div><b>Longitud:</b> @_inspectorLon.ToString("0.000000")</div>
|
<div><b>Longitud:</b> @_inspectorLon.ToString("0.000000")</div>
|
||||||
<div>
|
<div>
|
||||||
<b>Altitud (G):</b>
|
<b>Altura (G):</b>
|
||||||
@(_inspectorElevIdee is null
|
@(_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>
|
</div>
|
||||||
|
|
||||||
@if (_inspectorLugaresCercanos.Count > 0)
|
@if (_inspectorLugaresCercanos.Count > 0)
|
||||||
@@ -401,6 +449,7 @@
|
|||||||
<span class="popup-elev-title-threshold">
|
<span class="popup-elev-title-threshold">
|
||||||
Umbral de subida (Esf.):
|
Umbral de subida (Esf.):
|
||||||
@NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta).ToString("0.#", CultureInfo.InvariantCulture) %
|
@NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta).ToString("0.#", CultureInfo.InvariantCulture) %
|
||||||
|
· Cálculo: @EtiquetaFuenteElevacionCalculo
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -425,7 +474,9 @@
|
|||||||
<div class="popup-parada-body popup-elev-table-wrap">
|
<div class="popup-parada-body popup-elev-table-wrap">
|
||||||
@* ===== tu contenido actual tal cual ===== *@
|
@* ===== 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>
|
<div class="muted">Cargando puntos…</div>
|
||||||
}
|
}
|
||||||
@@ -435,6 +486,10 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@if (_tramoElevDetalle.ElevacionCargando)
|
||||||
|
{
|
||||||
|
<div class="muted elev-comparison-loading">Cargando altura seleccionada…</div>
|
||||||
|
}
|
||||||
<table class="popup-elev-table" style="
|
<table class="popup-elev-table" style="
|
||||||
width:100%;
|
width:100%;
|
||||||
font-size:0.8rem;
|
font-size:0.8rem;
|
||||||
@@ -444,20 +499,20 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
">
|
">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style="width:34px;" />
|
<col style="width:30px;" />
|
||||||
<col style="width:76px;" />
|
<col style="width:84px;" />
|
||||||
|
<col style="width:58px;" />
|
||||||
|
<col style="width:70px;" />
|
||||||
|
<col style="width:72px;" />
|
||||||
<col style="width:62px;" />
|
<col style="width:62px;" />
|
||||||
<col style="width:80px;" />
|
|
||||||
<col style="width:78px;" />
|
|
||||||
<col style="width:66px;" />
|
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="padding:4px 4px;">#</th>
|
<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;">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;">Dist (m)</th>
|
||||||
<th style="padding:4px 5px; text-align:right;">Esf.</th>
|
<th style="padding:4px 5px; text-align:right;">Esf.</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -467,6 +522,7 @@
|
|||||||
@for (int i = 0; i < _tramoElevDetalle.MuestrasElevacion.Count; i++)
|
@for (int i = 0; i < _tramoElevDetalle.MuestrasElevacion.Count; i++)
|
||||||
{
|
{
|
||||||
var p = _tramoElevDetalle.MuestrasElevacion[i];
|
var p = _tramoElevDetalle.MuestrasElevacion[i];
|
||||||
|
var elevacionSeleccionada = ObtenerElevacionFuenteCalculo(p);
|
||||||
|
|
||||||
double? distPrev = null;
|
double? distPrev = null;
|
||||||
double? deltaAlturaI = null;
|
double? deltaAlturaI = null;
|
||||||
@@ -477,10 +533,12 @@
|
|||||||
var prev = _tramoElevDetalle.MuestrasElevacion[i - 1];
|
var prev = _tramoElevDetalle.MuestrasElevacion[i - 1];
|
||||||
|
|
||||||
distPrev = CalcularDistanciaMetros(prev.Latitud, prev.Longitud, p.Latitud, p.Longitud);
|
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)
|
if (distPrev > 0)
|
||||||
pendienteI = deltaAlturaI.Value / distPrev.Value * 100.0;
|
pendienteI = deltaAlturaI.Value / distPrev.Value * 100.0;
|
||||||
@@ -504,10 +562,10 @@
|
|||||||
@(i == 0 ? "Ini" :
|
@(i == 0 ? "Ini" :
|
||||||
i == _tramoElevDetalle.MuestrasElevacion.Count - 1 ? "Fin" :
|
i == _tramoElevDetalle.MuestrasElevacion.Count - 1 ? "Fin" :
|
||||||
p.Indice.ToString())
|
p.Indice.ToString())
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style="padding:3px 5px; text-align:right;">
|
<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>
|
||||||
|
|
||||||
<td style="padding:3px 5px; text-align:right;">
|
<td style="padding:3px 5px; text-align:right;">
|
||||||
@@ -533,18 +591,21 @@
|
|||||||
@if (_tramoElevDetalle is not null)
|
@if (_tramoElevDetalle is not null)
|
||||||
{
|
{
|
||||||
var s = _tramoElevDetalle.MuestrasElevacion ?? new List<MuestraElevacion>();
|
var s = _tramoElevDetalle.MuestrasElevacion ?? new List<MuestraElevacion>();
|
||||||
|
var alturasSeleccionadas = s.Select(ObtenerElevacionFuenteCalculo).ToList();
|
||||||
double? GIni = s.Count > 0 ? s[0].ElevacionIdee : null;
|
double? alturaIni = alturasSeleccionadas.Count > 0 ? alturasSeleccionadas[0] : null;
|
||||||
double? GFin = s.Count > 0 ? s[^1].ElevacionIdee : null;
|
double? alturaFin = alturasSeleccionadas.Count > 0 ? alturasSeleccionadas[^1] : null;
|
||||||
double? GMin = s.Where(x => x.ElevacionIdee is not null).Select(x => x.ElevacionIdee!.Value).DefaultIfEmpty().Min();
|
var alturasValidas = alturasSeleccionadas
|
||||||
double? GMax = s.Where(x => x.ElevacionIdee is not null).Select(x => x.ElevacionIdee!.Value).DefaultIfEmpty().Max();
|
.Where(altura => altura is not null)
|
||||||
if (!s.Any(x => x.ElevacionIdee is not null)) { GMin = null; GMax = 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";
|
string F(double? v) => v is null ? "-" : v.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m";
|
||||||
var resumenEsfuerzo = CalcularResumenEsfuerzoElevacion(s);
|
var resumenEsfuerzo = CalcularResumenEsfuerzoElevacion(s);
|
||||||
|
|
||||||
<div class="muted" style="font-size:0.78rem; margin-top:.5rem;">
|
<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;">
|
<div style="margin-top:.25rem;">
|
||||||
<b>Suma de distancias entre puntos:</b> @FormatearDistanciaResumenElevacion(resumenEsfuerzo.DistanciaTotalMetros)
|
<b>Suma de distancias entre puntos:</b> @FormatearDistanciaResumenElevacion(resumenEsfuerzo.DistanciaTotalMetros)
|
||||||
</div>
|
</div>
|
||||||
@@ -684,10 +745,10 @@
|
|||||||
@if (MostrarAlertaPendientesPositivasSignificativas)
|
@if (MostrarAlertaPendientesPositivasSignificativas)
|
||||||
{
|
{
|
||||||
<div class="route-slope-warning" role="status">
|
<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 style="margin-top:.2rem; padding-left:.55rem;">
|
||||||
<div>- Distancia con subidas: @FormatearDistanciaResumenElevacion(_distanciaPendientePositivaSignificativaRutaMetros)</div>
|
<div>- Distancia con subidas: @FormatearDistanciaMetrosElevacion(_distanciaPendientePositivaSignificativaRutaMetros)</div>
|
||||||
<div>- Subidas acumuladas: @_alturaPendientePositivaSignificativaRutaMetros.ToString("0.00", CultureInfo.InvariantCulture) m</div>
|
<div>- Subidas acumuladas: @FormatearAlturaSubidaAlerta()</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1002,13 +1063,17 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="history-list">
|
<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" : "")">
|
<div class="history-item @(item.Id == _historialLocalSeleccionadoId ? "history-item--selected" : "")">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="history-item-main"
|
class="history-item-main history-item-main--numbered"
|
||||||
@onclick="() => CargarHistorialLocalAsync(item.Id)">
|
@onclick="() => CargarHistorialLocalAsync(item.Id)">
|
||||||
@TextoOpcionHistorial(item)
|
<span class="history-item-number">@numeroPersonal.</span>
|
||||||
|
<span class="history-item-text">@TextoOpcionHistorial(item)</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button"
|
<button type="button"
|
||||||
@@ -1177,7 +1242,7 @@
|
|||||||
{
|
{
|
||||||
if (_horariosOk)
|
if (_horariosOk)
|
||||||
{
|
{
|
||||||
<span class="muted">OK</span>
|
<span class="muted">activados</span>
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrWhiteSpace(_horariosError))
|
else if (!string.IsNullOrWhiteSpace(_horariosError))
|
||||||
{
|
{
|
||||||
@@ -1273,14 +1338,13 @@
|
|||||||
<div class="line-filter">
|
<div class="line-filter">
|
||||||
<label class="line-filter__toggle">
|
<label class="line-filter__toggle">
|
||||||
<input type="checkbox" @bind="_activarRangoItinerarios" @bind:after="PersistirConfiguracionAsync" />
|
<input type="checkbox" @bind="_activarRangoItinerarios" @bind:after="PersistirConfiguracionAsync" />
|
||||||
<span>Aplicar rango de itinerarios utilizables:</span>
|
<span>Aplicar itinerarios/sentidos:</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="line-filter__range">
|
<div class="line-filter__range line-filter__range--itinerarios">
|
||||||
<span class="muted">Rango:</span>
|
|
||||||
|
|
||||||
<select class="config-input"
|
<select class="config-input"
|
||||||
style="width:90px"
|
style="width:90px"
|
||||||
|
aria-label="Itinerario minimo"
|
||||||
@bind="_itinerarioMin"
|
@bind="_itinerarioMin"
|
||||||
@bind:event="onchange"
|
@bind:event="onchange"
|
||||||
@bind:after="PersistirConfiguracionAsync">
|
@bind:after="PersistirConfiguracionAsync">
|
||||||
@@ -1294,6 +1358,7 @@
|
|||||||
|
|
||||||
<select class="config-input"
|
<select class="config-input"
|
||||||
style="width:90px"
|
style="width:90px"
|
||||||
|
aria-label="Itinerario maximo"
|
||||||
@bind="_itinerarioMax"
|
@bind="_itinerarioMax"
|
||||||
@bind:event="onchange"
|
@bind:event="onchange"
|
||||||
@bind:after="PersistirConfiguracionAsync">
|
@bind:after="PersistirConfiguracionAsync">
|
||||||
@@ -1302,6 +1367,18 @@
|
|||||||
<option value="@n">@n</option>
|
<option value="@n">@n</option>
|
||||||
}
|
}
|
||||||
</select>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1401,7 +1478,7 @@
|
|||||||
<label>Transbordo:</label>
|
<label>Transbordo:</label>
|
||||||
<div class="config-dual-labeled">
|
<div class="config-dual-labeled">
|
||||||
<label class="config-dual-labeled__field">
|
<label class="config-dual-labeled__field">
|
||||||
<span>Peso rank.</span>
|
<span>Penal. (min)</span>
|
||||||
<input type="text" class="config-input"
|
<input type="text" class="config-input"
|
||||||
placeholder="4,0"
|
placeholder="4,0"
|
||||||
title="Penalización temporal y distancia equivalente por cada transbordo real"
|
title="Penalización temporal y distancia equivalente por cada transbordo real"
|
||||||
@@ -1459,12 +1536,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-row">
|
<div class="config-row">
|
||||||
<label>Altitud:</label>
|
<label>Altura:</label>
|
||||||
<div class="config-dual-labeled">
|
<div class="config-dual-labeled">
|
||||||
<label class="config-dual-labeled__field">
|
<label class="config-dual-labeled__field">
|
||||||
<span>Máx. puntos</span>
|
<span>Máx. puntos</span>
|
||||||
<input type="number" class="config-input" min="2" max="999" step="1"
|
<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" />
|
@bind="_elevMaxPuntosPorTramo" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
|
||||||
</label>
|
</label>
|
||||||
<label class="config-dual-labeled__field">
|
<label class="config-dual-labeled__field">
|
||||||
@@ -1478,20 +1555,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-row config-row-pendientes">
|
<div class="config-row config-row-pendientes">
|
||||||
<label>Pendientes:</label>
|
<label>Usar pendientes:</label>
|
||||||
<div class="config-pendientes-inline">
|
<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">
|
title="Activar o desactivar las pendientes en el cálculo">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
checked="@_mostrarElevacionesRuta"
|
checked="@_mostrarElevacionesRutaConfiguracion"
|
||||||
disabled="@_pintandoElevacionesRuta"
|
disabled="@_pintandoElevacionesRuta"
|
||||||
@onchange="ToggleElevacionesRutaAsync" />
|
@onchange="ToggleElevacionesRutaAsync" />
|
||||||
<span class="route-elevation-switch-track" aria-hidden="true">
|
<span class="route-elevation-switch-track" aria-hidden="true">
|
||||||
<span class="route-elevation-switch-thumb"></span>
|
<span class="route-elevation-switch-thumb"></span>
|
||||||
</span>
|
</span>
|
||||||
<span class="config-pendientes-inline__label">Visualizar</span>
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<span class="config-pendientes-inline__label">Config.</span>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="config-pendientes-settings-button"
|
class="config-pendientes-settings-button"
|
||||||
title="Configurar Pendientes"
|
title="Configurar Pendientes"
|
||||||
@@ -1551,7 +1628,27 @@
|
|||||||
|
|
||||||
<div class="pendientes-config-modal__fields">
|
<div class="pendientes-config-modal__fields">
|
||||||
<label class="pendientes-config-modal__field">
|
<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"
|
<input type="text"
|
||||||
class="config-input"
|
class="config-input"
|
||||||
inputmode="decimal"
|
inputmode="decimal"
|
||||||
@@ -1561,7 +1658,7 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="pendientes-config-modal__field">
|
<label class="pendientes-config-modal__field">
|
||||||
<span>Bajada (%)</span>
|
<span>Umbral Bajada (%)</span>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="config-input"
|
class="config-input"
|
||||||
inputmode="decimal"
|
inputmode="decimal"
|
||||||
@@ -1571,7 +1668,7 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="pendientes-config-modal__field">
|
<label class="pendientes-config-modal__field">
|
||||||
<span>Alerta (m)</span>
|
<span>Umbral Alerta (m)</span>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="config-input"
|
class="config-input"
|
||||||
inputmode="decimal"
|
inputmode="decimal"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using RutasDBUS.Servicios.Caminata;
|
using RutasDBUS.Servicios.Caminata;
|
||||||
|
using RutasDBUS.Servicios.Elevacion;
|
||||||
using RutasDBUS.Servicios.Geocodificacion;
|
using RutasDBUS.Servicios.Geocodificacion;
|
||||||
using RutasDBUS.Servicios.Historial;
|
using RutasDBUS.Servicios.Historial;
|
||||||
using RutasDBUS.Servicios.Horarios;
|
using RutasDBUS.Servicios.Horarios;
|
||||||
@@ -19,6 +20,7 @@ public partial class PlanificadorRutas : ComponentBase
|
|||||||
[Inject] protected IPlanificadorRutas PlanificadorRutasServicio { get; set; } = default!;
|
[Inject] protected IPlanificadorRutas PlanificadorRutasServicio { get; set; } = default!;
|
||||||
[Inject] protected IHistorialRutasCompartidoService HistorialRutasCompartidoService { get; set; } = default!;
|
[Inject] protected IHistorialRutasCompartidoService HistorialRutasCompartidoService { get; set; } = default!;
|
||||||
[Inject] protected IGeocodificadorLocalServicio GeocodificadorLocal { 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 IClienteOsrm EsrmCliente { get; set; } = default!;
|
||||||
[Inject] protected IJSRuntime JS { get; set; } = default!;
|
[Inject] protected IJSRuntime JS { get; set; } = default!;
|
||||||
[Inject] protected IClienteValhalla ClienteValhalla { get; set; } = default!;
|
[Inject] protected IClienteValhalla ClienteValhalla { get; set; } = default!;
|
||||||
|
|||||||
@@ -6,4 +6,7 @@ public sealed class MuestraElevacion
|
|||||||
public double Latitud { get; set; }
|
public double Latitud { get; set; }
|
||||||
public double Longitud { get; set; }
|
public double Longitud { get; set; }
|
||||||
public double? ElevacionIdee { get; set; }
|
public double? ElevacionIdee { get; set; }
|
||||||
|
public double? ElevacionLocal5 { get; set; }
|
||||||
|
public double? ElevacionLocal2 { get; set; }
|
||||||
|
public double? ElevacionLocal1 { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ public sealed class ParadaCandidata
|
|||||||
public double TiempoAPie { get; set; }
|
public double TiempoAPie { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum FiltroSentidoItinerario
|
||||||
|
{
|
||||||
|
Ambos,
|
||||||
|
SoloIda,
|
||||||
|
SoloVuelta,
|
||||||
|
Ninguno
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -41,6 +49,8 @@ public sealed class ParametrosPlanificadorRuta
|
|||||||
public bool ActivarRangoItinerarios { get; set; } = true;
|
public bool ActivarRangoItinerarios { get; set; } = true;
|
||||||
public int ItinerarioMin { get; set; } = 1;
|
public int ItinerarioMin { get; set; } = 1;
|
||||||
public int ItinerarioMax { get; set; } = 80;
|
public int ItinerarioMax { get; set; } = 80;
|
||||||
|
public FiltroSentidoItinerario FiltroSentidoItinerario { get; set; } =
|
||||||
|
FiltroSentidoItinerario.Ambos;
|
||||||
|
|
||||||
// filtro de transbordos
|
// filtro de transbordos
|
||||||
public bool ActivarRangoTransbordos { get; set; } = true;
|
public bool ActivarRangoTransbordos { get; set; } = true;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text;
|
|||||||
using RutasDBUS.Components;
|
using RutasDBUS.Components;
|
||||||
using RutasDBUS.Modelos.RedBus;
|
using RutasDBUS.Modelos.RedBus;
|
||||||
using RutasDBUS.Servicios.Caminata;
|
using RutasDBUS.Servicios.Caminata;
|
||||||
|
using RutasDBUS.Servicios.Elevacion;
|
||||||
using RutasDBUS.Servicios.Historial;
|
using RutasDBUS.Servicios.Historial;
|
||||||
using RutasDBUS.Servicios.Horarios;
|
using RutasDBUS.Servicios.Horarios;
|
||||||
using RutasDBUS.Servicios.Importacion;
|
using RutasDBUS.Servicios.Importacion;
|
||||||
@@ -44,6 +45,7 @@ builder.Services.AddSingleton<CreadorRedTransporteDbus>();
|
|||||||
builder.Services.AddSingleton<IRedBusServicio, RedBusServicio>();
|
builder.Services.AddSingleton<IRedBusServicio, RedBusServicio>();
|
||||||
builder.Services.AddSingleton<IHistorialRutasCompartidoService, ServicioHistorialRutasCompartido>();
|
builder.Services.AddSingleton<IHistorialRutasCompartidoService, ServicioHistorialRutasCompartido>();
|
||||||
builder.Services.AddSingleton<IGeocodificadorLocalServicio, GeocodificadorLocalServicio>();
|
builder.Services.AddSingleton<IGeocodificadorLocalServicio, GeocodificadorLocalServicio>();
|
||||||
|
builder.Services.AddSingleton<IServicioElevacionLocal, ServicioElevacionMdtLocal>();
|
||||||
builder.Services.AddSingleton<ServicioHorariosDbus>();
|
builder.Services.AddSingleton<ServicioHorariosDbus>();
|
||||||
builder.Services.AddSingleton<CreadorIndiceTransitoDbus>();
|
builder.Services.AddSingleton<CreadorIndiceTransitoDbus>();
|
||||||
builder.Services.AddSingleton<ServicioIndiceTransitoDbus>();
|
builder.Services.AddSingleton<ServicioIndiceTransitoDbus>();
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BitMiracle.LibTiff.NET" Version="2.4.660" />
|
||||||
<PackageReference Include="LeafletForBlazor" Version="2.2.8.4" />
|
<PackageReference Include="LeafletForBlazor" Version="2.2.8.4" />
|
||||||
|
<PackageReference Include="ProjNET" Version="2.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
24
RutasDBUS/Servicios/Elevacion/IServicioElevacionLocal.cs
Normal file
24
RutasDBUS/Servicios/Elevacion/IServicioElevacionLocal.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace RutasDBUS.Servicios.Elevacion;
|
||||||
|
|
||||||
|
public enum ModeloElevacionLocal
|
||||||
|
{
|
||||||
|
Local5,
|
||||||
|
Local2,
|
||||||
|
Local1
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IServicioElevacionLocal
|
||||||
|
{
|
||||||
|
bool EstaDisponible(ModeloElevacionLocal modelo);
|
||||||
|
string ObtenerArchivo(ModeloElevacionLocal modelo);
|
||||||
|
string? ObtenerError(ModeloElevacionLocal modelo);
|
||||||
|
|
||||||
|
double? ObtenerElevacion(
|
||||||
|
ModeloElevacionLocal modelo,
|
||||||
|
double latitud,
|
||||||
|
double longitud);
|
||||||
|
|
||||||
|
IReadOnlyList<double?> ObtenerElevaciones(
|
||||||
|
ModeloElevacionLocal modelo,
|
||||||
|
IReadOnlyList<(double Latitud, double Longitud)> puntos);
|
||||||
|
}
|
||||||
362
RutasDBUS/Servicios/Elevacion/ServicioElevacionMdtLocal.cs
Normal file
362
RutasDBUS/Servicios/Elevacion/ServicioElevacionMdtLocal.cs
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
using BitMiracle.LibTiff.Classic;
|
||||||
|
using ProjNet.CoordinateSystems;
|
||||||
|
using ProjNet.CoordinateSystems.Transformations;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Elevacion;
|
||||||
|
|
||||||
|
public sealed class ServicioElevacionMdtLocal : IServicioElevacionLocal, IDisposable
|
||||||
|
{
|
||||||
|
private const string RutaMdt05PorDefecto =
|
||||||
|
@"C:\ProgramData\RutasDBUS\Modelos\Elevacion\Local5\PNOA-MDT05-ETRS89-HU30-0064-LID.TIF";
|
||||||
|
private const string RutaMdt02PorDefecto =
|
||||||
|
@"C:\ProgramData\RutasDBUS\Modelos\Elevacion\Local2\PNOA-MDT02-ETRS89-HU30-0064-DBUS-COB2-2017.TIF";
|
||||||
|
private const string RutaMdt01PorDefecto =
|
||||||
|
@"C:\ProgramData\RutasDBUS\Modelos\Elevacion\Local1\GEOEUSKADI-MDT01-ETRS89-HU30-0064-DBUS-2017.TIF";
|
||||||
|
|
||||||
|
private readonly LectorMdtGeoTiff _local5;
|
||||||
|
private readonly LectorMdtGeoTiff _local2;
|
||||||
|
private readonly LectorMdtGeoTiff _local1;
|
||||||
|
|
||||||
|
public ServicioElevacionMdtLocal(
|
||||||
|
IConfiguration configuracion,
|
||||||
|
ILogger<ServicioElevacionMdtLocal> logger)
|
||||||
|
{
|
||||||
|
_local5 = new LectorMdtGeoTiff(
|
||||||
|
"L5",
|
||||||
|
configuracion["Elevacion:Local:ArchivoMdt05"] ?? RutaMdt05PorDefecto,
|
||||||
|
logger);
|
||||||
|
_local2 = new LectorMdtGeoTiff(
|
||||||
|
"L2",
|
||||||
|
configuracion["Elevacion:Local:ArchivoMdt02"] ?? RutaMdt02PorDefecto,
|
||||||
|
logger);
|
||||||
|
_local1 = new LectorMdtGeoTiff(
|
||||||
|
"L1",
|
||||||
|
configuracion["Elevacion:Local:ArchivoMdt01"] ?? RutaMdt01PorDefecto,
|
||||||
|
logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool EstaDisponible(ModeloElevacionLocal modelo)
|
||||||
|
=> ObtenerLector(modelo).Disponible;
|
||||||
|
|
||||||
|
public string ObtenerArchivo(ModeloElevacionLocal modelo)
|
||||||
|
=> ObtenerLector(modelo).Archivo;
|
||||||
|
|
||||||
|
public string? ObtenerError(ModeloElevacionLocal modelo)
|
||||||
|
=> ObtenerLector(modelo).Error;
|
||||||
|
|
||||||
|
public double? ObtenerElevacion(
|
||||||
|
ModeloElevacionLocal modelo,
|
||||||
|
double latitud,
|
||||||
|
double longitud)
|
||||||
|
=> ObtenerLector(modelo).ObtenerElevacion(latitud, longitud);
|
||||||
|
|
||||||
|
public IReadOnlyList<double?> ObtenerElevaciones(
|
||||||
|
ModeloElevacionLocal modelo,
|
||||||
|
IReadOnlyList<(double Latitud, double Longitud)> puntos)
|
||||||
|
=> ObtenerLector(modelo).ObtenerElevaciones(puntos);
|
||||||
|
|
||||||
|
private LectorMdtGeoTiff ObtenerLector(ModeloElevacionLocal modelo)
|
||||||
|
=> modelo switch
|
||||||
|
{
|
||||||
|
ModeloElevacionLocal.Local5 => _local5,
|
||||||
|
ModeloElevacionLocal.Local2 => _local2,
|
||||||
|
ModeloElevacionLocal.Local1 => _local1,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(modelo), modelo, null)
|
||||||
|
};
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_local5.Dispose();
|
||||||
|
_local2.Dispose();
|
||||||
|
_local1.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LectorMdtGeoTiff : IDisposable
|
||||||
|
{
|
||||||
|
private const int MaxTeselasEnCache = 32;
|
||||||
|
|
||||||
|
private readonly object _sync = new();
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly string _nombreModelo;
|
||||||
|
private readonly Dictionary<int, TeselaCache> _teselas = new();
|
||||||
|
private readonly LinkedList<int> _ordenTeselas = new();
|
||||||
|
private readonly MathTransform? _transformacionUtm;
|
||||||
|
private readonly Tiff? _tiff;
|
||||||
|
private readonly int _ancho;
|
||||||
|
private readonly int _alto;
|
||||||
|
private readonly int _anchoTesela;
|
||||||
|
private readonly int _altoTesela;
|
||||||
|
private readonly double _origenX;
|
||||||
|
private readonly double _origenY;
|
||||||
|
private readonly double _pasoX;
|
||||||
|
private readonly double _pasoY;
|
||||||
|
private readonly double _sinDatos = -32767.0;
|
||||||
|
|
||||||
|
private sealed record TeselaCache(byte[] Datos, LinkedListNode<int> Nodo);
|
||||||
|
|
||||||
|
public LectorMdtGeoTiff(string nombreModelo, string archivo, ILogger logger)
|
||||||
|
{
|
||||||
|
_nombreModelo = nombreModelo;
|
||||||
|
_logger = logger;
|
||||||
|
Archivo = archivo;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(Archivo))
|
||||||
|
throw new FileNotFoundException(
|
||||||
|
$"No se ha encontrado el modelo de elevacion local {nombreModelo}.",
|
||||||
|
Archivo);
|
||||||
|
|
||||||
|
Tiff.SetErrorHandler(new GestorMensajesTiff(logger));
|
||||||
|
_tiff = Tiff.Open(Archivo, "r")
|
||||||
|
?? throw new InvalidDataException(
|
||||||
|
$"No se ha podido abrir el modelo {nombreModelo} como GeoTIFF.");
|
||||||
|
|
||||||
|
_ancho = CampoEntero(_tiff, TiffTag.IMAGEWIDTH);
|
||||||
|
_alto = CampoEntero(_tiff, TiffTag.IMAGELENGTH);
|
||||||
|
_anchoTesela = CampoEntero(_tiff, TiffTag.TILEWIDTH);
|
||||||
|
_altoTesela = CampoEntero(_tiff, TiffTag.TILELENGTH);
|
||||||
|
|
||||||
|
var bits = CampoEntero(_tiff, TiffTag.BITSPERSAMPLE);
|
||||||
|
var muestrasPorPixel = CampoEntero(_tiff, TiffTag.SAMPLESPERPIXEL);
|
||||||
|
var formato = (SampleFormat)_tiff.GetField(TiffTag.SAMPLEFORMAT)[0].ToInt();
|
||||||
|
if (bits != 32 || muestrasPorPixel != 1 || formato != SampleFormat.IEEEFP)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"Formato {nombreModelo} no admitido: {bits} bits, " +
|
||||||
|
$"{muestrasPorPixel} muestra(s), {formato}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var escala = CampoDoble(_tiff, TiffTag.GEOTIFF_MODELPIXELSCALETAG, 3);
|
||||||
|
var enlace = CampoDoble(_tiff, TiffTag.GEOTIFF_MODELTIEPOINTTAG, 6);
|
||||||
|
_pasoX = escala[0];
|
||||||
|
_pasoY = escala[1];
|
||||||
|
_origenX = enlace[3] - enlace[0] * _pasoX;
|
||||||
|
_origenY = enlace[4] + enlace[1] * _pasoY;
|
||||||
|
|
||||||
|
var sinDatos = _tiff.GetField(TiffTag.GDAL_NODATA);
|
||||||
|
if (sinDatos is { Length: > 1 } &&
|
||||||
|
double.TryParse(
|
||||||
|
sinDatos[1].ToString().TrimEnd('\0'),
|
||||||
|
System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out var valorSinDatos))
|
||||||
|
{
|
||||||
|
_sinDatos = valorSinDatos;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fabrica = new CoordinateTransformationFactory();
|
||||||
|
var utm30 = ProjectedCoordinateSystem.WGS84_UTM(30, true);
|
||||||
|
_transformacionUtm = fabrica
|
||||||
|
.CreateFromCoordinateSystems(GeographicCoordinateSystem.WGS84, utm30)
|
||||||
|
.MathTransform;
|
||||||
|
|
||||||
|
Disponible = true;
|
||||||
|
logger.LogInformation(
|
||||||
|
"Modelo local {Modelo} cargado: {Archivo}; {Ancho}x{Alto}; paso {PasoX}x{PasoY} m.",
|
||||||
|
nombreModelo,
|
||||||
|
Archivo,
|
||||||
|
_ancho,
|
||||||
|
_alto,
|
||||||
|
_pasoX,
|
||||||
|
_pasoY);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Error = ex.Message;
|
||||||
|
logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"El modelo local {Modelo} no esta disponible en {Archivo}.",
|
||||||
|
nombreModelo,
|
||||||
|
Archivo);
|
||||||
|
_tiff?.Dispose();
|
||||||
|
_tiff = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Disponible { get; }
|
||||||
|
public string Archivo { get; }
|
||||||
|
public string? Error { get; }
|
||||||
|
|
||||||
|
public double? ObtenerElevacion(double latitud, double longitud)
|
||||||
|
=> ObtenerElevaciones([(latitud, longitud)])[0];
|
||||||
|
|
||||||
|
public IReadOnlyList<double?> ObtenerElevaciones(
|
||||||
|
IReadOnlyList<(double Latitud, double Longitud)> puntos)
|
||||||
|
{
|
||||||
|
var resultado = new double?[puntos.Count];
|
||||||
|
if (!Disponible || _tiff is null || _transformacionUtm is null || puntos.Count == 0)
|
||||||
|
return resultado;
|
||||||
|
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < puntos.Count; i++)
|
||||||
|
{
|
||||||
|
var punto = puntos[i];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var utm = _transformacionUtm.Transform([punto.Longitud, punto.Latitud]);
|
||||||
|
resultado[i] = InterpolarElevacion(utm[0], utm[1]);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
ex,
|
||||||
|
"No se pudo consultar {Modelo} en {Latitud}, {Longitud}.",
|
||||||
|
_nombreModelo,
|
||||||
|
punto.Latitud,
|
||||||
|
punto.Longitud);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultado;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double? InterpolarElevacion(double x, double y)
|
||||||
|
{
|
||||||
|
var columna = (x - _origenX) / _pasoX;
|
||||||
|
var fila = (_origenY - y) / _pasoY;
|
||||||
|
var columna0 = (int)Math.Floor(columna);
|
||||||
|
var fila0 = (int)Math.Floor(fila);
|
||||||
|
var fraccionX = columna - columna0;
|
||||||
|
var fraccionY = fila - fila0;
|
||||||
|
|
||||||
|
var muestras = new[]
|
||||||
|
{
|
||||||
|
(Valor: LeerPixel(columna0, fila0), Peso: (1.0 - fraccionX) * (1.0 - fraccionY)),
|
||||||
|
(Valor: LeerPixel(columna0 + 1, fila0), Peso: fraccionX * (1.0 - fraccionY)),
|
||||||
|
(Valor: LeerPixel(columna0, fila0 + 1), Peso: (1.0 - fraccionX) * fraccionY),
|
||||||
|
(Valor: LeerPixel(columna0 + 1, fila0 + 1), Peso: fraccionX * fraccionY)
|
||||||
|
};
|
||||||
|
|
||||||
|
var suma = 0.0;
|
||||||
|
var sumaPesos = 0.0;
|
||||||
|
foreach (var muestra in muestras)
|
||||||
|
{
|
||||||
|
if (muestra.Valor is null || muestra.Peso <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
suma += muestra.Valor.Value * muestra.Peso;
|
||||||
|
sumaPesos += muestra.Peso;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sumaPesos > 0 ? suma / sumaPesos : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double? LeerPixel(int columna, int fila)
|
||||||
|
{
|
||||||
|
if (_tiff is null || columna < 0 || fila < 0 || columna >= _ancho || fila >= _alto)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var indiceTesela = _tiff.ComputeTile(columna, fila, 0, 0);
|
||||||
|
var datos = ObtenerTesela(indiceTesela);
|
||||||
|
if (datos is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var columnaLocal = columna % _anchoTesela;
|
||||||
|
var filaLocal = fila % _altoTesela;
|
||||||
|
var desplazamiento = (filaLocal * _anchoTesela + columnaLocal) * sizeof(float);
|
||||||
|
if (desplazamiento < 0 || desplazamiento + sizeof(float) > datos.Length)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var valor = BitConverter.ToSingle(datos, desplazamiento);
|
||||||
|
if (!float.IsFinite(valor) || Math.Abs(valor - _sinDatos) < 0.001)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return valor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[]? ObtenerTesela(int indiceTesela)
|
||||||
|
{
|
||||||
|
if (_tiff is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (_teselas.TryGetValue(indiceTesela, out var existente))
|
||||||
|
{
|
||||||
|
_ordenTeselas.Remove(existente.Nodo);
|
||||||
|
_ordenTeselas.AddFirst(existente.Nodo);
|
||||||
|
return existente.Datos;
|
||||||
|
}
|
||||||
|
|
||||||
|
var datos = new byte[_tiff.TileSize()];
|
||||||
|
var leidos = _tiff.ReadEncodedTile(indiceTesela, datos, 0, datos.Length);
|
||||||
|
if (leidos <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var nodo = _ordenTeselas.AddFirst(indiceTesela);
|
||||||
|
_teselas[indiceTesela] = new TeselaCache(datos, nodo);
|
||||||
|
|
||||||
|
while (_teselas.Count > MaxTeselasEnCache && _ordenTeselas.Last is { } ultimo)
|
||||||
|
{
|
||||||
|
_ordenTeselas.RemoveLast();
|
||||||
|
_teselas.Remove(ultimo.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return datos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CampoEntero(Tiff tiff, TiffTag etiqueta)
|
||||||
|
=> tiff.GetField(etiqueta)?[0].ToInt()
|
||||||
|
?? throw new InvalidDataException($"Falta la etiqueta TIFF {etiqueta}.");
|
||||||
|
|
||||||
|
private static double[] CampoDoble(Tiff tiff, TiffTag etiqueta, int minimo)
|
||||||
|
{
|
||||||
|
var campo = tiff.GetField(etiqueta);
|
||||||
|
var valores = campo is { Length: > 1 } ? campo[1].ToDoubleArray() : null;
|
||||||
|
if (valores is null || valores.Length < minimo)
|
||||||
|
throw new InvalidDataException($"Falta la etiqueta GeoTIFF {etiqueta}.");
|
||||||
|
|
||||||
|
return valores;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_teselas.Clear();
|
||||||
|
_ordenTeselas.Clear();
|
||||||
|
_tiff?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class GestorMensajesTiff(ILogger logger) : TiffErrorHandler
|
||||||
|
{
|
||||||
|
public override void WarningHandler(
|
||||||
|
Tiff tif,
|
||||||
|
string method,
|
||||||
|
string format,
|
||||||
|
params object[] args)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WarningHandlerExt(
|
||||||
|
Tiff tif,
|
||||||
|
object clientData,
|
||||||
|
string method,
|
||||||
|
string format,
|
||||||
|
params object[] args)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ErrorHandler(
|
||||||
|
Tiff tif,
|
||||||
|
string method,
|
||||||
|
string format,
|
||||||
|
params object[] args)
|
||||||
|
{
|
||||||
|
logger.LogError("TIFF {Metodo}: {Mensaje}", method, string.Format(format, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ErrorHandlerExt(
|
||||||
|
Tiff tif,
|
||||||
|
object clientData,
|
||||||
|
string method,
|
||||||
|
string format,
|
||||||
|
params object[] args)
|
||||||
|
{
|
||||||
|
ErrorHandler(tif, method, format, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1040,6 +1040,13 @@ public sealed class BuscadorRutasTemporalesDbus
|
|||||||
var maximoItinerario = Math.Max(parametros.ItinerarioMin, parametros.ItinerarioMax);
|
var maximoItinerario = Math.Max(parametros.ItinerarioMin, parametros.ItinerarioMax);
|
||||||
if (patron.CodigoItinerario < minimoItinerario || patron.CodigoItinerario > maximoItinerario)
|
if (patron.CodigoItinerario < minimoItinerario || patron.CodigoItinerario > maximoItinerario)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (!UtilidadesTransitoDbus.SentidoPermitido(
|
||||||
|
patron.Sentido,
|
||||||
|
parametros.FiltroSentidoItinerario))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
|||||||
new(StringComparer.OrdinalIgnoreCase);
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
private const double UmbralCosturaVisualMetros = 12.0;
|
private const double UmbralCosturaVisualMetros = 12.0;
|
||||||
private const double UmbralUnionPeatonalAutomaticaMetros = 250.0;
|
private const double UmbralUnionPeatonalAutomaticaMetros = 250.0;
|
||||||
|
private const double DistanciaMinimaTramoPieVisibleMetros = 1.0;
|
||||||
private const double ToleranciaEmpateRapidezSegundos = 5.0;
|
private const double ToleranciaEmpateRapidezSegundos = 5.0;
|
||||||
private const double MejoraMinimaAccesoExtensionCircularMetros = 20.0;
|
private const double MejoraMinimaAccesoExtensionCircularMetros = 20.0;
|
||||||
private const double MaxExcesoAccesoDirectoCircularMetros = 120.0;
|
private const double MaxExcesoAccesoDirectoCircularMetros = 120.0;
|
||||||
@@ -119,21 +120,28 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var alternativasSoloPie = await CrearAlternativasSoloPieAsync(origenLatLon, destinoLatLon, parametros);
|
||||||
var candidatasOrigen = await SeleccionarCandidatasAsync(indiceTransito, origenLatLon[0], origenLatLon[1], parametros, ct);
|
var candidatasOrigen = await SeleccionarCandidatasAsync(indiceTransito, origenLatLon[0], origenLatLon[1], parametros, ct);
|
||||||
var candidatasDestino = await SeleccionarCandidatasAsync(indiceTransito, destinoLatLon[0], destinoLatLon[1], parametros, ct);
|
var candidatasDestino = await SeleccionarCandidatasAsync(indiceTransito, destinoLatLon[0], destinoLatLon[1], parametros, ct);
|
||||||
|
|
||||||
if (candidatasOrigen.Count == 0 || candidatasDestino.Count == 0)
|
if (candidatasOrigen.Count == 0 || candidatasDestino.Count == 0)
|
||||||
{
|
{
|
||||||
|
var finalesSoloPie = SeleccionarAlternativasFinales(
|
||||||
|
Array.Empty<AlternativaRuta>(),
|
||||||
|
alternativasSoloPie,
|
||||||
|
parametros);
|
||||||
|
|
||||||
return new ResultadoPlanificadorRuta
|
return new ResultadoPlanificadorRuta
|
||||||
{
|
{
|
||||||
Ok = false,
|
Ok = finalesSoloPie.Count > 0,
|
||||||
Mensaje = "No hay paradas validas cerca del origen o del destino con los filtros actuales.",
|
Mensaje = finalesSoloPie.Count > 0
|
||||||
Alternativas = new(),
|
? "Solo hay alternativas a pie con los filtros actuales."
|
||||||
|
: "No hay paradas validas cerca del origen o del destino con los filtros actuales.",
|
||||||
|
Alternativas = finalesSoloPie,
|
||||||
IndicePreferido = 0
|
IndicePreferido = 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var alternativasSoloPie = await CrearAlternativasSoloPieAsync(origenLatLon, destinoLatLon, parametros);
|
|
||||||
var instanteSalida = UtilidadesTransitoDbus.ResolverInstanteSalida(parametros);
|
var instanteSalida = UtilidadesTransitoDbus.ResolverInstanteSalida(parametros);
|
||||||
var calendariosActivos = parametros.UsarHorariosReales
|
var calendariosActivos = parametros.UsarHorariosReales
|
||||||
? _servicioHorarios.GetActiveCalendars(DateOnly.FromDateTime(instanteSalida))
|
? _servicioHorarios.GetActiveCalendars(DateOnly.FromDateTime(instanteSalida))
|
||||||
@@ -2121,12 +2129,17 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
|||||||
|
|
||||||
private static void AgregarLineaPieCalculoElevacion(
|
private static void AgregarLineaPieCalculoElevacion(
|
||||||
AlternativaRuta alternativa,
|
AlternativaRuta alternativa,
|
||||||
|
double distanciaMetros,
|
||||||
double[,]? linea,
|
double[,]? linea,
|
||||||
double latitudOrigen,
|
double latitudOrigen,
|
||||||
double longitudOrigen,
|
double longitudOrigen,
|
||||||
double latitudDestino,
|
double latitudDestino,
|
||||||
double longitudDestino)
|
double longitudDestino)
|
||||||
{
|
{
|
||||||
|
// Las distancias inferiores a un metro se muestran como 0 m y no forman un tramo a pie real.
|
||||||
|
if (!EsDistanciaPieVisible(distanciaMetros))
|
||||||
|
return;
|
||||||
|
|
||||||
if (linea is not null && linea.GetLength(0) >= 2)
|
if (linea is not null && linea.GetLength(0) >= 2)
|
||||||
{
|
{
|
||||||
alternativa.LineasPieCalculoElevacion.Add(linea);
|
alternativa.LineasPieCalculoElevacion.Add(linea);
|
||||||
@@ -2148,10 +2161,18 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool EsDistanciaPieVisible(double distanciaMetros)
|
||||||
|
=> double.IsFinite(distanciaMetros) &&
|
||||||
|
distanciaMetros >= DistanciaMinimaTramoPieVisibleMetros;
|
||||||
|
|
||||||
private static void AgregarDistanciaPiePorTramo(AlternativaRuta alternativa, double distanciaMetros)
|
private static void AgregarDistanciaPiePorTramo(AlternativaRuta alternativa, double distanciaMetros)
|
||||||
{
|
{
|
||||||
if (double.IsFinite(distanciaMetros) && distanciaMetros > 0)
|
if (!double.IsFinite(distanciaMetros))
|
||||||
alternativa.DistanciasPiePorTramoAprox.Add(distanciaMetros);
|
return;
|
||||||
|
|
||||||
|
// Se conserva la posicion del tramo para distinguir una lista recalculada sin caminatas reales.
|
||||||
|
alternativa.DistanciasPiePorTramoAprox.Add(
|
||||||
|
EsDistanciaPieVisible(distanciaMetros) ? distanciaMetros : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AcumularDistanciaPieEnPrimerTramo(AlternativaRuta alternativa, double distanciaMetros)
|
private static void AcumularDistanciaPieEnPrimerTramo(AlternativaRuta alternativa, double distanciaMetros)
|
||||||
@@ -2220,16 +2241,17 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
|||||||
var cursor = instanteSalida;
|
var cursor = instanteSalida;
|
||||||
|
|
||||||
var acceso = await WalkOsrmCached(origenLatLon[0], origenLatLon[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
|
var acceso = await WalkOsrmCached(origenLatLon[0], origenLatLon[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
|
||||||
|
alternativa.DistanciaPieInicioAprox = acceso.distancia > 0
|
||||||
|
? acceso.distancia
|
||||||
|
: UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
|
||||||
AgregarLineaPieCalculoElevacion(
|
AgregarLineaPieCalculoElevacion(
|
||||||
alternativa,
|
alternativa,
|
||||||
|
alternativa.DistanciaPieInicioAprox,
|
||||||
acceso.linea,
|
acceso.linea,
|
||||||
origenLatLon[0],
|
origenLatLon[0],
|
||||||
origenLatLon[1],
|
origenLatLon[1],
|
||||||
paradaInicioBus.Latitud,
|
paradaInicioBus.Latitud,
|
||||||
paradaInicioBus.Longitud);
|
paradaInicioBus.Longitud);
|
||||||
alternativa.DistanciaPieInicioAprox = acceso.distancia > 0
|
|
||||||
? acceso.distancia
|
|
||||||
: UtilidadesTransitoDbus.CalcularDistanciaMetros(origenLatLon[0], origenLatLon[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
|
|
||||||
AgregarDistanciaPiePorTramo(alternativa, alternativa.DistanciaPieInicioAprox);
|
AgregarDistanciaPiePorTramo(alternativa, alternativa.DistanciaPieInicioAprox);
|
||||||
alternativa.TiempoPieInicio = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(alternativa.DistanciaPieInicioAprox, parametros);
|
alternativa.TiempoPieInicio = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(alternativa.DistanciaPieInicioAprox, parametros);
|
||||||
total += alternativa.TiempoPieInicio;
|
total += alternativa.TiempoPieInicio;
|
||||||
@@ -2282,18 +2304,19 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
|||||||
puntoInicioTramo![0],
|
puntoInicioTramo![0],
|
||||||
puntoInicioTramo[1]);
|
puntoInicioTramo[1]);
|
||||||
|
|
||||||
|
var distanciaTransferencia = transferencia.distancia > 0
|
||||||
|
? transferencia.distancia
|
||||||
|
: distanciaUnion;
|
||||||
|
|
||||||
AgregarLineaPieCalculoElevacion(
|
AgregarLineaPieCalculoElevacion(
|
||||||
alternativa,
|
alternativa,
|
||||||
|
distanciaTransferencia,
|
||||||
transferencia.linea,
|
transferencia.linea,
|
||||||
puntoUltimaLlegada[0],
|
puntoUltimaLlegada[0],
|
||||||
puntoUltimaLlegada[1],
|
puntoUltimaLlegada[1],
|
||||||
puntoInicioTramo[0],
|
puntoInicioTramo[0],
|
||||||
puntoInicioTramo[1]);
|
puntoInicioTramo[1]);
|
||||||
|
|
||||||
var distanciaTransferencia = transferencia.distancia > 0
|
|
||||||
? transferencia.distancia
|
|
||||||
: distanciaUnion;
|
|
||||||
|
|
||||||
if (indice > 0 &&
|
if (indice > 0 &&
|
||||||
esTransbordoReal &&
|
esTransbordoReal &&
|
||||||
distanciaTransferencia > UmbralUnionPeatonalAutomaticaMetros)
|
distanciaTransferencia > UmbralUnionPeatonalAutomaticaMetros)
|
||||||
@@ -2412,17 +2435,26 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
|||||||
paradaFinBus);
|
paradaFinBus);
|
||||||
alternativa.DestinoParada = paradaFinBus;
|
alternativa.DestinoParada = paradaFinBus;
|
||||||
|
|
||||||
var salidaFinal = await WalkOsrmCached(puntoUltimaLlegada[0], puntoUltimaLlegada[1], destinoLatLon[0], destinoLatLon[1]);
|
var salidaFinal = await WalkOsrmCached(
|
||||||
AgregarLineaPieCalculoElevacion(
|
paradaFinBus.Latitud,
|
||||||
alternativa,
|
paradaFinBus.Longitud,
|
||||||
salidaFinal.linea,
|
|
||||||
puntoUltimaLlegada[0],
|
|
||||||
puntoUltimaLlegada[1],
|
|
||||||
destinoLatLon[0],
|
destinoLatLon[0],
|
||||||
destinoLatLon[1]);
|
destinoLatLon[1]);
|
||||||
alternativa.DistanciaPieFinAprox = salidaFinal.distancia > 0
|
alternativa.DistanciaPieFinAprox = salidaFinal.distancia > 0
|
||||||
? salidaFinal.distancia
|
? salidaFinal.distancia
|
||||||
: UtilidadesTransitoDbus.CalcularDistanciaMetros(puntoUltimaLlegada[0], puntoUltimaLlegada[1], destinoLatLon[0], destinoLatLon[1]);
|
: UtilidadesTransitoDbus.CalcularDistanciaMetros(
|
||||||
|
paradaFinBus.Latitud,
|
||||||
|
paradaFinBus.Longitud,
|
||||||
|
destinoLatLon[0],
|
||||||
|
destinoLatLon[1]);
|
||||||
|
AgregarLineaPieCalculoElevacion(
|
||||||
|
alternativa,
|
||||||
|
alternativa.DistanciaPieFinAprox,
|
||||||
|
salidaFinal.linea,
|
||||||
|
paradaFinBus.Latitud,
|
||||||
|
paradaFinBus.Longitud,
|
||||||
|
destinoLatLon[0],
|
||||||
|
destinoLatLon[1]);
|
||||||
AgregarDistanciaPiePorTramo(alternativa, alternativa.DistanciaPieFinAprox);
|
AgregarDistanciaPiePorTramo(alternativa, alternativa.DistanciaPieFinAprox);
|
||||||
alternativa.TiempoPieFin = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(alternativa.DistanciaPieFinAprox, parametros);
|
alternativa.TiempoPieFin = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(alternativa.DistanciaPieFinAprox, parametros);
|
||||||
total += alternativa.TiempoPieFin;
|
total += alternativa.TiempoPieFin;
|
||||||
@@ -3621,6 +3653,13 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
|||||||
var maximoItinerario = Math.Max(parametros.ItinerarioMin, parametros.ItinerarioMax);
|
var maximoItinerario = Math.Max(parametros.ItinerarioMin, parametros.ItinerarioMax);
|
||||||
if (patron.CodigoItinerario < minimoItinerario || patron.CodigoItinerario > maximoItinerario)
|
if (patron.CodigoItinerario < minimoItinerario || patron.CodigoItinerario > maximoItinerario)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (!UtilidadesTransitoDbus.SentidoPermitido(
|
||||||
|
patron.Sentido,
|
||||||
|
parametros.FiltroSentidoItinerario))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -76,6 +76,24 @@ internal static class UtilidadesTransitoDbus
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Comprueba si un sentido entra en el filtro de itinerarios seleccionado.
|
||||||
|
/// </summary>
|
||||||
|
public static bool SentidoPermitido(
|
||||||
|
string? sentido,
|
||||||
|
FiltroSentidoItinerario filtro)
|
||||||
|
{
|
||||||
|
var sentidoNormalizado = NormalizarSentidoCodigo(sentido);
|
||||||
|
return filtro switch
|
||||||
|
{
|
||||||
|
FiltroSentidoItinerario.Ambos => true,
|
||||||
|
FiltroSentidoItinerario.SoloIda => sentidoNormalizado == "I",
|
||||||
|
FiltroSentidoItinerario.SoloVuelta => sentidoNormalizado == "V",
|
||||||
|
FiltroSentidoItinerario.Ninguno => false,
|
||||||
|
_ => true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extrae el codigo de itinerario a partir de la ruta y del patron.
|
/// Extrae el codigo de itinerario a partir de la ruta y del patron.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -24,6 +24,11 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"Elevacion": {
|
"Elevacion": {
|
||||||
|
"Local": {
|
||||||
|
"ArchivoMdt05": "C:\\ProgramData\\RutasDBUS\\Modelos\\Elevacion\\Local5\\PNOA-MDT05-ETRS89-HU30-0064-LID.TIF",
|
||||||
|
"ArchivoMdt02": "C:\\ProgramData\\RutasDBUS\\Modelos\\Elevacion\\Local2\\PNOA-MDT02-ETRS89-HU30-0064-DBUS-COB2-2017.TIF",
|
||||||
|
"ArchivoMdt01": "C:\\ProgramData\\RutasDBUS\\Modelos\\Elevacion\\Local1\\GEOEUSKADI-MDT01-ETRS89-HU30-0064-DBUS-2017.TIF"
|
||||||
|
},
|
||||||
"OpenElevation": {
|
"OpenElevation": {
|
||||||
"UrlLookup": "https://openelevation.tecnosis.net/api/v1/lookup"
|
"UrlLookup": "https://openelevation.tecnosis.net/api/v1/lookup"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1856,6 +1856,15 @@ html, body {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-row.config-row-pendientes {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 132px;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-row-pendientes > label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.config-pendientes-inline .route-elevation-switch {
|
.config-pendientes-inline .route-elevation-switch {
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
font-size: .68rem;
|
font-size: .68rem;
|
||||||
@@ -1901,7 +1910,7 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pendientes-config-modal {
|
.pendientes-config-modal {
|
||||||
width: min(286px, calc(100vw - 24px));
|
width: min(300px, calc(100vw - 24px));
|
||||||
padding: .75rem .8rem .8rem;
|
padding: .75rem .8rem .8rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
@@ -1934,7 +1943,7 @@ html, body {
|
|||||||
|
|
||||||
.pendientes-config-modal__field {
|
.pendientes-config-modal__field {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 86px;
|
grid-template-columns: minmax(0, 1fr) 98px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: .65rem;
|
gap: .65rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -2213,6 +2222,15 @@ html, body {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.line-filter__range.line-filter__range--itinerarios {
|
||||||
|
grid-template-columns: 56px auto 56px minmax(0, 1fr);
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-filter__range--itinerarios .line-filter__direction {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.line-filter__range .config-input {
|
.line-filter__range .config-input {
|
||||||
width: 100% !important; /* pisa el 90px */
|
width: 100% !important; /* pisa el 90px */
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -2548,14 +2566,15 @@ html, body {
|
|||||||
|
|
||||||
|
|
||||||
.popup-elev-topright {
|
.popup-elev-topright {
|
||||||
--elev-table-width: 412px;
|
--elev-table-width: 466px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 14px;
|
left: 14px;
|
||||||
bottom:40px; /* mismo “offset” pero abajo */
|
bottom:40px; /* mismo “offset” pero abajo */
|
||||||
right: auto;
|
right: auto;
|
||||||
top: auto;
|
top: auto;
|
||||||
transform: none;
|
transform: none;
|
||||||
width: max-content;
|
box-sizing: border-box;
|
||||||
|
width: min(calc(var(--elev-table-width) + 1.6rem + 2px), calc(100vw - 28px));
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: calc(100vw - 28px);
|
max-width: calc(100vw - 28px);
|
||||||
z-index: 1200;
|
z-index: 1200;
|
||||||
@@ -2570,13 +2589,19 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.popup-elev-table-wrap {
|
.popup-elev-table-wrap {
|
||||||
width: var(--elev-table-width);
|
box-sizing: border-box;
|
||||||
max-width: calc(100vw - 56px);
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.elev-comparison-loading {
|
||||||
|
padding: 2px 4px 5px;
|
||||||
|
font-size: .7rem;
|
||||||
|
}
|
||||||
|
|
||||||
.popup-elev-table thead th {
|
.popup-elev-table thead th {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -3514,12 +3539,17 @@ html, body {
|
|||||||
padding-left: 2px !important;
|
padding-left: 2px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-elev-table col:nth-child(1) { width: 26px !important; }
|
.popup-elev-table thead th {
|
||||||
.popup-elev-table col:nth-child(2) { width: 44px !important; }
|
line-height: 1.05;
|
||||||
.popup-elev-table col:nth-child(3) { width: 39px !important; }
|
white-space: normal;
|
||||||
.popup-elev-table col:nth-child(4) { width: 52px !important; }
|
}
|
||||||
.popup-elev-table col:nth-child(5) { width: 56px !important; }
|
|
||||||
.popup-elev-table col:nth-child(6) { width: 46px !important; }
|
.popup-elev-table col:nth-child(1) { width: 24px !important; }
|
||||||
|
.popup-elev-table col:nth-child(2) { width: 58px !important; }
|
||||||
|
.popup-elev-table col:nth-child(3) { width: 42px !important; }
|
||||||
|
.popup-elev-table col:nth-child(4) { width: 48px !important; }
|
||||||
|
.popup-elev-table col:nth-child(5) { width: 52px !important; }
|
||||||
|
.popup-elev-table col:nth-child(6) { width: 44px !important; }
|
||||||
|
|
||||||
.elev-bubble {
|
.elev-bubble {
|
||||||
right: 8px;
|
right: 8px;
|
||||||
|
|||||||
@@ -1581,7 +1581,14 @@ window.rt.saveLocalValueResult = function (key, value) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.rt.loadLocalValueResult = function (key) {
|
window.rt.loadLocalValueResult = function (key, maxItems, maxBytes, recoveryKey) {
|
||||||
|
let originalItemCount = null;
|
||||||
|
let itemCount = null;
|
||||||
|
let originalByteLength = null;
|
||||||
|
let byteLength = null;
|
||||||
|
let trimmed = false;
|
||||||
|
let backupSaved = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const storage = window.localStorage;
|
const storage = window.localStorage;
|
||||||
if (!storage) {
|
if (!storage) {
|
||||||
@@ -1594,11 +1601,69 @@ window.rt.loadLocalValueResult = function (key) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = storage.getItem(key);
|
const originalValue = storage.getItem(key);
|
||||||
|
let value = originalValue;
|
||||||
|
const normalizedMaxItems = Number.isInteger(maxItems) && maxItems > 0
|
||||||
|
? maxItems
|
||||||
|
: null;
|
||||||
|
const normalizedMaxBytes = Number.isInteger(maxBytes) && maxBytes > 0
|
||||||
|
? maxBytes
|
||||||
|
: null;
|
||||||
|
originalByteLength = originalValue === null ? 0 : new Blob([originalValue]).size;
|
||||||
|
byteLength = originalByteLength;
|
||||||
|
|
||||||
|
if (value !== null && value.trim() !== "" &&
|
||||||
|
(normalizedMaxItems !== null || normalizedMaxBytes !== null)) {
|
||||||
|
let parsedValue = null;
|
||||||
|
try {
|
||||||
|
parsedValue = JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
// El servidor conserva su recuperacion detallada para JSON danado.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(parsedValue)) {
|
||||||
|
originalItemCount = parsedValue.length;
|
||||||
|
const items = normalizedMaxItems === null
|
||||||
|
? parsedValue.slice()
|
||||||
|
: parsedValue.slice(0, normalizedMaxItems);
|
||||||
|
value = JSON.stringify(items);
|
||||||
|
byteLength = new Blob([value]).size;
|
||||||
|
|
||||||
|
while (normalizedMaxBytes !== null && items.length > 0 && byteLength > normalizedMaxBytes) {
|
||||||
|
items.pop();
|
||||||
|
value = JSON.stringify(items);
|
||||||
|
byteLength = new Blob([value]).size;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemCount = items.length;
|
||||||
|
trimmed = itemCount < originalItemCount ||
|
||||||
|
(normalizedMaxBytes !== null && originalByteLength > normalizedMaxBytes);
|
||||||
|
|
||||||
|
if (trimmed) {
|
||||||
|
if (typeof recoveryKey !== "string" || recoveryKey.trim() === "") {
|
||||||
|
throw new Error("No se ha indicado una clave para respaldar el historial antes de recortarlo.");
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.setItem(recoveryKey, originalValue);
|
||||||
|
backupSaved = true;
|
||||||
|
storage.setItem(key, value);
|
||||||
|
} else {
|
||||||
|
value = originalValue;
|
||||||
|
byteLength = originalByteLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
found: value !== null,
|
found: value !== null,
|
||||||
value
|
value,
|
||||||
|
trimmed,
|
||||||
|
backupSaved,
|
||||||
|
originalItemCount,
|
||||||
|
itemCount,
|
||||||
|
originalByteLength,
|
||||||
|
byteLength
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
@@ -1606,7 +1671,13 @@ window.rt.loadLocalValueResult = function (key) {
|
|||||||
found: false,
|
found: false,
|
||||||
value: null,
|
value: null,
|
||||||
errorName: error?.name ? String(error.name) : "StorageError",
|
errorName: error?.name ? String(error.name) : "StorageError",
|
||||||
errorMessage: error?.message ? String(error.message) : String(error ?? "Error desconocido al leer localStorage.")
|
errorMessage: error?.message ? String(error.message) : String(error ?? "Error desconocido al leer localStorage."),
|
||||||
|
trimmed,
|
||||||
|
backupSaved,
|
||||||
|
originalItemCount,
|
||||||
|
itemCount,
|
||||||
|
originalByteLength,
|
||||||
|
byteLength
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user