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

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

View File

@@ -256,6 +256,9 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
private bool _popupInspectorVisible = false; private 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;

View File

@@ -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()

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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)
{ {

View File

@@ -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"

View File

@@ -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!;

View File

@@ -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; }
} }

View File

@@ -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;

View File

@@ -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>();

View File

@@ -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>

View 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);
}

View 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);
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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"
}, },

View File

@@ -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;

View File

@@ -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
}; };
} }
}; };