v-20260916a

- restaurar derivaciones
- añadido la pedida de anchura de calles y cariiles
- correcion paradas bus
- desglose del score
- historial no cambia la hora
- resuarar no cambia modo de historial
This commit is contained in:
2026-09-16 13:26:13 +02:00
parent 9f312d2ca9
commit 7090dfa9be
15 changed files with 867 additions and 83 deletions

View File

@@ -1455,31 +1455,135 @@ public partial class PlanificadorRutas : ComponentBase
private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa) private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa)
{ {
if (alternativa is null) var desglose = CrearDesgloseScoreEquilibrado(alternativa);
return double.PositiveInfinity; return desglose?.ScoreTotal ?? double.PositiveInfinity;
var distanciaBus = 0.0;
if (!alternativa.EsSoloAPie)
{
if (alternativa.AlternativaBus is null)
return double.PositiveInfinity;
distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus);
} }
var distanciaAndando = CalcularDistanciaCorregidaAlternativa(alternativa); private DesgloseScoreEquilibrado? CrearDesgloseScoreEquilibrado(AlternativaRuta? alternativa)
var factor = double.IsFinite(_factorEquilibradoAndarBus) && _factorEquilibradoAndarBus > 0 {
if (alternativa is null || (!alternativa.EsSoloAPie && alternativa.AlternativaBus is null))
return null;
var parametros = CrearParametrosPenalizacionActuales();
var distanciasPie = alternativa.EsSoloAPie
? new[] { alternativa.DistanciaSoloAPie }
: alternativa.DistanciasPiePorTramoAprox.Count > 0
? alternativa.DistanciasPiePorTramoAprox.ToArray()
: new[]
{
alternativa.DistanciaPieInicioAprox,
alternativa.DistanciaPieTransbordosAprox,
alternativa.DistanciaPieFinAprox
};
var tramosAndando = distanciasPie
.Where(distancia => double.IsFinite(distancia) && distancia > 0)
.Select((distancia, indice) => new TramoAndandoScore(
indice + 1,
distancia,
UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(distancia, parametros)))
.ToList();
var distanciaAndandoCorregidaBase = tramosAndando.Sum(x => x.DistanciaCorregidaMetros);
var esfuerzoExtraPendiente = _mostrarElevacionesRuta &&
double.IsFinite(alternativa.EsfuerzoExtraPendienteMetros)
? Math.Max(0, alternativa.EsfuerzoExtraPendienteMetros)
: 0;
var aportacionAndando = distanciaAndandoCorregidaBase + esfuerzoExtraPendiente;
var distanciaBus = alternativa.EsSoloAPie
? 0
: Math.Max(0, alternativa.AlternativaBus!.DistanciaTotalBus);
var factorEquilibrado = double.IsFinite(_factorEquilibradoAndarBus) &&
_factorEquilibradoAndarBus > 0
? _factorEquilibradoAndarBus ? _factorEquilibradoAndarBus
: FactorEquilibradoAndarBusPorDefecto; : FactorEquilibradoAndarBusPorDefecto;
var aportacionBus = distanciaBus / factorEquilibrado;
var penalizacionSegundos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingSegundos(
alternativa.NumeroTransbordosReales,
parametros);
var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros( var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros(
alternativa.NumeroTransbordosReales, alternativa.NumeroTransbordosReales,
CrearParametrosPenalizacionActuales()); parametros);
var score = distanciaAndando + (distanciaBus / factor) + penalizacionTransbordos; var pesoTransbordoMinutos = double.IsFinite(_pesoTransbordoRankingMinutos) &&
return double.IsFinite(score) && score >= 0 _pesoTransbordoRankingMinutos > 0
? score ? _pesoTransbordoRankingMinutos
: double.PositiveInfinity; : 0;
var velocidadBusKmH = double.IsFinite(_velocidadBusKmH) && _velocidadBusKmH > 0
? _velocidadBusKmH
: 16.0;
var factorAndando = double.IsFinite(_factorPenalizacionDistanciaPie)
? _factorPenalizacionDistanciaPie
: 1.8;
var baseAndandoMetros = double.IsFinite(_parametroPenalizacionDistanciaPieMetros)
? Math.Abs(_parametroPenalizacionDistanciaPieMetros)
: 200.0;
if (baseAndandoMetros <= 0)
baseAndandoMetros = 1.0;
var score = aportacionAndando + aportacionBus + penalizacionTransbordos;
if (!double.IsFinite(score) || score < 0)
return null;
return new DesgloseScoreEquilibrado(
tramosAndando,
factorAndando,
baseAndandoMetros,
distanciaAndandoCorregidaBase,
esfuerzoExtraPendiente,
_mostrarElevacionesRuta,
aportacionAndando,
distanciaBus,
factorEquilibrado,
aportacionBus,
alternativa.NumeroTransbordosReales,
pesoTransbordoMinutos,
penalizacionSegundos,
velocidadBusKmH,
penalizacionTransbordos,
score);
} }
private void AbrirDesgloseScore(AlternativaRuta alternativa)
=> _desgloseScoreVisible = CrearDesgloseScoreEquilibrado(alternativa);
private void CerrarDesgloseScore()
=> _desgloseScoreVisible = null;
private static string FormatearMetrosDesgloseScore(double valor)
=> $"{valor.ToString("0.00", CultureInfo.InvariantCulture)} m";
private static string FormatearNumeroDesgloseScore(double valor)
=> valor.ToString("0.##", CultureInfo.InvariantCulture);
private static string FormatearScoreFinalDesglose(double valor)
=> valor.ToString("0", CultureInfo.InvariantCulture);
private sealed record TramoAndandoScore(
int Numero,
double DistanciaRealMetros,
double DistanciaCorregidaMetros);
private sealed record DesgloseScoreEquilibrado(
IReadOnlyList<TramoAndandoScore> TramosAndando,
double FactorAndando,
double BaseAndandoMetros,
double DistanciaAndandoCorregidaBaseMetros,
double EsfuerzoExtraPendienteMetros,
bool PendientesAplicadas,
double AportacionAndandoMetros,
double DistanciaBusMetros,
double FactorEquilibrado,
double AportacionBusMetros,
int NumeroTransbordos,
double PesoTransbordoMinutos,
double PenalizacionTransbordosSegundos,
double VelocidadBusKmH,
double PenalizacionTransbordosMetros,
double ScoreTotal);
private DesgloseScoreEquilibrado? _desgloseScoreVisible;
private string FormatearScoreEquilibradoAlternativa(AlternativaRuta? alternativa) private string FormatearScoreEquilibradoAlternativa(AlternativaRuta? alternativa)
{ {
var score = CalcularScoreEquilibradoFactor(alternativa); var score = CalcularScoreEquilibradoFactor(alternativa);

View File

@@ -40,6 +40,8 @@ public partial class PlanificadorRutas
private RegistroHistorialRuta? _escenarioBaseDerivacionHistorial; private RegistroHistorialRuta? _escenarioBaseDerivacionHistorial;
private bool _mostrarInformacionDerivacionHistorial; private bool _mostrarInformacionDerivacionHistorial;
private int _indicePasoDerivacionSeleccionado = -1; private int _indicePasoDerivacionSeleccionado = -1;
private int _indicePasoDerivacionActivo = -1;
private bool _cargandoPasoDerivacionHistorial;
private string _mensajeHistorial = string.Empty; private string _mensajeHistorial = string.Empty;
private bool _mensajeHistorialEsError = false; private bool _mensajeHistorialEsError = false;
private bool _mostrarHistorialRutas = false; private bool _mostrarHistorialRutas = false;
@@ -76,7 +78,8 @@ public partial class PlanificadorRutas
private sealed record PasoDerivacionHistorial( private sealed record PasoDerivacionHistorial(
int Numero, int Numero,
DateTime FechaUtc, DateTime FechaUtc,
RegistroHistorialRuta Escenario); RegistroHistorialRuta Escenario,
int? NumeroAnterior);
private sealed record CambioDerivacionHistorial( private sealed record CambioDerivacionHistorial(
string Campo, string Campo,
@@ -1084,8 +1087,9 @@ public partial class PlanificadorRutas
return string.Empty; return string.Empty;
var numeroDerivacion = _tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido && var numeroDerivacion = _tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido &&
_pasosDerivacionHistorial.Count > 0 _indicePasoDerivacionActivo >= 0 &&
? $" · Derivación n.º {_pasosDerivacionHistorial.Count}" _indicePasoDerivacionActivo < _pasosDerivacionHistorial.Count
? $" · Derivación n.º {_pasosDerivacionHistorial[_indicePasoDerivacionActivo].Numero}"
: string.Empty; : string.Empty;
return $"Ruta actual derivada del Historial {nombre} n.º {indice + 1}{numeroDerivacion}."; return $"Ruta actual derivada del Historial {nombre} n.º {indice + 1}{numeroDerivacion}.";
} }
@@ -1113,7 +1117,9 @@ public partial class PlanificadorRutas
if (!PuedeMostrarInformacionDerivacionHistorial) if (!PuedeMostrarInformacionDerivacionHistorial)
return; return;
_indicePasoDerivacionSeleccionado = _pasosDerivacionHistorial.Count - 1; _indicePasoDerivacionSeleccionado = _indicePasoDerivacionActivo >= 0
? _indicePasoDerivacionActivo
: _pasosDerivacionHistorial.Count - 1;
_mostrarInformacionDerivacionHistorial = true; _mostrarInformacionDerivacionHistorial = true;
} }
@@ -1130,11 +1136,46 @@ public partial class PlanificadorRutas
_indicePasoDerivacionSeleccionado = indice; _indicePasoDerivacionSeleccionado = indice;
} }
private async Task CargarPasoDerivacionHistorialAsync()
{
if (_cargandoPasoDerivacionHistorial ||
_indicePasoDerivacionSeleccionado < 0 ||
_indicePasoDerivacionSeleccionado >= _pasosDerivacionHistorial.Count)
{
return;
}
var indice = _indicePasoDerivacionSeleccionado;
var paso = _pasosDerivacionHistorial[indice];
_cargandoPasoDerivacionHistorial = true;
try
{
await AplicarRegistroHistorialAsync(ClonarEscenarioHistorial(paso.Escenario));
_indicePasoDerivacionActivo = indice;
_indicePasoDerivacionSeleccionado = indice;
_rutaActualDerivadaDeHistorial = ExisteReferenciaHistorialActual();
_mostrarInformacionDerivacionHistorial = false;
MostrarMensajeHistorial($"Derivación n.º {paso.Numero} cargada.");
}
catch
{
MostrarMensajeHistorial(
$"No he podido cargar la Derivación n.º {paso.Numero}.",
esError: true);
}
finally
{
_cargandoPasoDerivacionHistorial = false;
}
}
private void ReiniciarSeguimientoDerivacionHistorial() private void ReiniciarSeguimientoDerivacionHistorial()
{ {
_pasosDerivacionHistorial.Clear(); _pasosDerivacionHistorial.Clear();
_escenarioBaseDerivacionHistorial = null; _escenarioBaseDerivacionHistorial = null;
_indicePasoDerivacionSeleccionado = -1; _indicePasoDerivacionSeleccionado = -1;
_indicePasoDerivacionActivo = -1;
_cargandoPasoDerivacionHistorial = false;
_mostrarInformacionDerivacionHistorial = false; _mostrarInformacionDerivacionHistorial = false;
} }
@@ -1155,9 +1196,11 @@ public partial class PlanificadorRutas
if (actual is null) if (actual is null)
return; return;
var anterior = _pasosDerivacionHistorial.Count > 0 var pasoAnterior = _indicePasoDerivacionActivo >= 0 &&
? _pasosDerivacionHistorial[^1].Escenario _indicePasoDerivacionActivo < _pasosDerivacionHistorial.Count
: _escenarioBaseDerivacionHistorial; ? _pasosDerivacionHistorial[_indicePasoDerivacionActivo]
: null;
var anterior = pasoAnterior?.Escenario ?? _escenarioBaseDerivacionHistorial;
if (string.Equals( if (string.Equals(
CalcularClaveEscenario(anterior), CalcularClaveEscenario(anterior),
CalcularClaveEscenario(actual), CalcularClaveEscenario(actual),
@@ -1169,20 +1212,21 @@ public partial class PlanificadorRutas
_pasosDerivacionHistorial.Add(new PasoDerivacionHistorial( _pasosDerivacionHistorial.Add(new PasoDerivacionHistorial(
_pasosDerivacionHistorial.Count + 1, _pasosDerivacionHistorial.Count + 1,
DateTime.UtcNow, DateTime.UtcNow,
ClonarEscenarioHistorial(actual))); ClonarEscenarioHistorial(actual),
pasoAnterior?.Numero));
_indicePasoDerivacionSeleccionado = _pasosDerivacionHistorial.Count - 1; _indicePasoDerivacionSeleccionado = _pasosDerivacionHistorial.Count - 1;
_indicePasoDerivacionActivo = _indicePasoDerivacionSeleccionado;
} }
private RegistroHistorialRuta? ObtenerEscenarioAnteriorDerivacion( private RegistroHistorialRuta? ObtenerEscenarioAnteriorDerivacion(
PasoDerivacionHistorial paso) PasoDerivacionHistorial paso)
{ {
var indice = paso.Numero - 1; if (paso.NumeroAnterior is null)
if (indice <= 0)
return _escenarioBaseDerivacionHistorial; return _escenarioBaseDerivacionHistorial;
return indice - 1 < _pasosDerivacionHistorial.Count return _pasosDerivacionHistorial
? _pasosDerivacionHistorial[indice - 1].Escenario .FirstOrDefault(item => item.Numero == paso.NumeroAnterior.Value)
: null; ?.Escenario;
} }
private IReadOnlyList<CambioDerivacionHistorial> ObtenerCambiosDerivacion( private IReadOnlyList<CambioDerivacionHistorial> ObtenerCambiosDerivacion(
@@ -1527,7 +1571,8 @@ public partial class PlanificadorRutas
var configuracionHistorial = _modoConfiguracionHistorial switch var configuracionHistorial = _modoConfiguracionHistorial switch
{ {
ModoConfiguracionHistorial.Guardada => CrearConfiguracionGuardadaHistorial(item), ModoConfiguracionHistorial.Guardada => CrearConfiguracionGuardadaHistorial(item),
ModoConfiguracionHistorial.Predeterminada => CrearConfiguracionPredeterminadaHistorial(), ModoConfiguracionHistorial.Predeterminada =>
CrearConfiguracionPredeterminadaParaHistorial(item),
_ => null _ => null
}; };
@@ -1609,6 +1654,30 @@ public partial class PlanificadorRutas
return configuracion; return configuracion;
} }
private ConfiguracionHistorialRuta CrearConfiguracionPredeterminadaParaHistorial(
RegistroHistorialRuta item)
{
var predeterminada = CrearConfiguracionPredeterminadaHistorial();
var guardada = CrearConfiguracionGuardadaHistorial(item);
predeterminada.UsarHoraSimulada = guardada.UsarHoraSimulada;
if (guardada.UsarHoraSimulada)
{
predeterminada.FechaSimulada = guardada.FechaSimulada;
predeterminada.HoraSimulada = guardada.HoraSimulada;
}
else
{
var ahora = DateTime.Now;
predeterminada.FechaSimulada = DateOnly.FromDateTime(ahora)
.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
predeterminada.HoraSimulada = TimeOnly.FromDateTime(ahora)
.ToString("HH:mm:ss", CultureInfo.InvariantCulture);
}
return predeterminada;
}
private void CapturarConfiguracionTemporalAntesDeHistorial() private void CapturarConfiguracionTemporalAntesDeHistorial()
{ {
if (_configuracionTemporalPreviaHistorial is not null) if (_configuracionTemporalPreviaHistorial is not null)

View File

@@ -652,17 +652,23 @@ public partial class PlanificadorRutas : ComponentBase
var fecha = DateOnly.FromDateTime(now); var fecha = DateOnly.FromDateTime(now);
var hora = TimeOnly.FromDateTime(now); var hora = TimeOnly.FromDateTime(now);
var horarios = await Task.Run(() =>
{
cancellationToken.ThrowIfCancellationRequested();
var deps = Horarios.GetNextDeparturesByCodigo( var deps = Horarios.GetNextDeparturesByCodigo(
paradaDetectada.Codigo, paradaDetectada.Codigo,
fecha, fecha,
hora, hora,
take: null, take: null,
includePast: true); includePast: true);
cancellationToken.ThrowIfCancellationRequested();
return ConstruirListaHorariosConActual(deps, hora);
}, cancellationToken);
if (!EsPopupParadaVigente(paradaDetectada, token, cancellationToken)) if (!EsPopupParadaVigente(paradaDetectada, token, cancellationToken))
return; return;
_nextDeps = ConstruirListaHorariosConActual(deps, hora); _nextDeps = horarios;
_scrollHorariosPendiente = true; _scrollHorariosPendiente = true;
} }
catch (Exception ex) catch (Exception ex)
@@ -686,7 +692,7 @@ public partial class PlanificadorRutas : ComponentBase
token == Volatile.Read(ref _paradaPopupToken) && token == Volatile.Read(ref _paradaPopupToken) &&
string.Equals(_paradaClicada?.Codigo, parada.Codigo, StringComparison.OrdinalIgnoreCase); string.Equals(_paradaClicada?.Codigo, parada.Codigo, StringComparison.OrdinalIgnoreCase);
private async Task CerrarPopupParadaAsync() private Task CerrarPopupParadaAsync()
{ {
Interlocked.Increment(ref _paradaPopupToken); Interlocked.Increment(ref _paradaPopupToken);
_ctsParadaPopup?.Cancel(); _ctsParadaPopup?.Cancel();
@@ -699,8 +705,14 @@ public partial class PlanificadorRutas : ComponentBase
_paradaElevLocal1 = null; _paradaElevLocal1 = null;
_nextDeps.Clear(); _nextDeps.Clear();
_scrollHorariosPendiente = false; _scrollHorariosPendiente = false;
try { await JS.InvokeVoidAsync("rt.clearSelectedStopMarker"); } catch { }
StateHasChanged(); StateHasChanged();
_ = LimpiarMarcadorParadaSeleccionadaAsync();
return Task.CompletedTask;
}
private async Task LimpiarMarcadorParadaSeleccionadaAsync()
{
try { await JS.InvokeVoidAsync("rt.clearSelectedStopMarker"); } catch { }
} }
@@ -1494,7 +1506,7 @@ public partial class PlanificadorRutas : ComponentBase
private bool _elevPopupMinimizado = false; private bool _elevPopupMinimizado = false;
private const string _versionApp = "(v-20260914a)"; private const string _versionApp = "(v-20260916a)";
private double? _paradaElevIdee; private double? _paradaElevIdee;

View File

@@ -1,5 +1,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Globalization; using System.Globalization;
using System.Net;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using RutasDBUS.Modelos.Planificacion; using RutasDBUS.Modelos.Planificacion;
@@ -46,7 +47,7 @@ public partial class PlanificadorRutas
return; return;
} }
_inspectorSuperficieTexto = "No disponible"; _inspectorSuperficieTexto = FormatearSuperficieNoDisponible(" · ");
_inspectorSuperficieCargando = false; _inspectorSuperficieCargando = false;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
@@ -81,7 +82,7 @@ public partial class PlanificadorRutas
private static string FormatearSuperficiePuntoInspector(InformacionSueloTramo? informacion) private static string FormatearSuperficiePuntoInspector(InformacionSueloTramo? informacion)
{ {
if (informacion is null || !informacion.Disponible || informacion.Segmentos.Count == 0) if (informacion is null || !informacion.Disponible || informacion.Segmentos.Count == 0)
return "No disponible"; return FormatearSuperficieNoDisponible(" · ");
var superficies = informacion.Segmentos var superficies = informacion.Segmentos
.Select(segmento => .Select(segmento =>
@@ -89,7 +90,8 @@ public partial class PlanificadorRutas
var uso = string.IsNullOrWhiteSpace(segmento.Uso) var uso = string.IsNullOrWhiteSpace(segmento.Uso)
? "" ? ""
: $" ({segmento.Uso})"; : $" ({segmento.Uso})";
return $"{segmento.Superficie}{uso}"; var atributos = FormatearAtributosVia(segmento, " · ");
return $"{segmento.Superficie}{uso}{atributos}";
}) })
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
@@ -136,7 +138,7 @@ public partial class PlanificadorRutas
} }
catch catch
{ {
superficie = "No disponible"; superficie = FormatearSuperficieNoDisponible("<br>");
} }
if (version != _versionConsultaSuperficieSegmento) if (version != _versionConsultaSuperficieSegmento)
@@ -159,7 +161,7 @@ public partial class PlanificadorRutas
private static string FormatearSuperficieSegmento(InformacionSueloTramo? informacion) private static string FormatearSuperficieSegmento(InformacionSueloTramo? informacion)
{ {
if (informacion is null || !informacion.Disponible || informacion.Segmentos.Count == 0) if (informacion is null || !informacion.Disponible || informacion.Segmentos.Count == 0)
return "No disponible"; return FormatearSuperficieNoDisponible("<br>");
return string.Join( return string.Join(
"<br>", "<br>",
@@ -171,7 +173,40 @@ public partial class PlanificadorRutas
var distancia = segmento.DistanciaMetros < 1000 var distancia = segmento.DistanciaMetros < 1000
? segmento.DistanciaMetros.ToString("0.##", CultureInfo.InvariantCulture) + " m" ? segmento.DistanciaMetros.ToString("0.##", CultureInfo.InvariantCulture) + " m"
: (segmento.DistanciaMetros / 1000.0).ToString("0.###", CultureInfo.InvariantCulture) + " km"; : (segmento.DistanciaMetros / 1000.0).ToString("0.###", CultureInfo.InvariantCulture) + " km";
return $"{segmento.Superficie}{uso}, {distancia}"; var atributos = FormatearAtributosVia(segmento, "<br>");
return $"{segmento.Superficie}{uso}, {distancia}{atributos}";
})); }));
} }
private static string FormatearAtributosVia(SegmentoSueloTramo segmento, string separador)
{
var atributos = new List<string>(2);
var carriles = segmento.NumeroCarriles is > 0
? segmento.NumeroCarriles.Value.ToString(CultureInfo.InvariantCulture)
: "-";
atributos.Add($"Carriles: {carriles}");
if (!string.IsNullOrWhiteSpace(segmento.AnchuraVia))
{
var etiqueta = segmento.AnchuraViaEstimada ? "Anchura estimada" : "Anchura";
atributos.Add($"{etiqueta}: {FormatearAnchuraVia(segmento.AnchuraVia)}");
}
else
{
atributos.Add("Anchura: -");
}
return separador + string.Join(" · ", atributos);
}
private static string FormatearSuperficieNoDisponible(string separador)
=> "No disponible" + FormatearAtributosVia(new SegmentoSueloTramo(), separador);
private static string FormatearAnchuraVia(string anchura)
{
var valor = anchura.Trim();
var texto = double.TryParse(valor, NumberStyles.Float, CultureInfo.InvariantCulture, out _)
? $"{valor} m"
: valor;
return WebUtility.HtmlEncode(texto);
}
} }

View File

@@ -33,7 +33,6 @@ public partial class PlanificadorRutas
predeterminada.CriterioPrioritarioRuta = actual.CriterioPrioritarioRuta; predeterminada.CriterioPrioritarioRuta = actual.CriterioPrioritarioRuta;
predeterminada.PreferenciaEtiquetaRuta = actual.PreferenciaEtiquetaRuta; predeterminada.PreferenciaEtiquetaRuta = actual.PreferenciaEtiquetaRuta;
AplicarConfiguracionLocal(predeterminada); AplicarConfiguracionLocal(predeterminada);
_modoConfiguracionHistorial = ModoConfiguracionHistorial.Actual;
} }
[JSInvokable] [JSInvokable]

View File

@@ -48,7 +48,6 @@
<select class="config-input header-center-input" <select class="config-input header-center-input"
@key="_selectItKey" @key="_selectItKey"
@bind="_itSeleccionadoId"> @bind="_itSeleccionadoId">
@@ -1075,7 +1074,14 @@
} }
@if (!string.IsNullOrWhiteSpace(scoreEquilibradoSeleccionado)) @if (!string.IsNullOrWhiteSpace(scoreEquilibradoSeleccionado))
{ {
<div>Score equilibrado A: @scoreEquilibradoSeleccionado</div> <div class="route-score-detail-line">
<span>Score equilibrado A: @scoreEquilibradoSeleccionado</span>
<button type="button"
class="route-score-info"
title="Ver desglose del score"
aria-label="Ver desglose del score equilibrado A"
@onclick="() => AbrirDesgloseScore(altSeleccionada)">i</button>
</div>
} }
@if (!string.IsNullOrWhiteSpace(xyzOrigen)) @if (!string.IsNullOrWhiteSpace(xyzOrigen))
{ {
@@ -1381,9 +1387,9 @@
var cambiosRespectoAnterior = ObtenerCambiosDerivacion( var cambiosRespectoAnterior = ObtenerCambiosDerivacion(
escenarioAnterior, escenarioAnterior,
pasoDerivacion.Escenario); pasoDerivacion.Escenario);
var textoComparacionAnterior = pasoDerivacion.Numero <= 1 var textoComparacionAnterior = pasoDerivacion.NumeroAnterior is null
? "Respecto al Historial Compartido original" ? "Respecto al Historial Compartido original"
: $"Respecto a la Derivación n.º {pasoDerivacion.Numero - 1}"; : $"Respecto a la Derivación n.º {pasoDerivacion.NumeroAnterior}";
<div class="custom-modal-backdrop history-derivation-backdrop" <div class="custom-modal-backdrop history-derivation-backdrop"
@onclick="CerrarInformacionDerivacionHistorial"> @onclick="CerrarInformacionDerivacionHistorial">
@@ -1410,7 +1416,8 @@
{ {
var indicePaso = paso.Numero - 1; var indicePaso = paso.Numero - 1;
<button type="button" <button type="button"
class="history-derivation-step @(indicePaso == _indicePasoDerivacionSeleccionado ? "history-derivation-step--selected" : "")" class="history-derivation-step @(indicePaso == _indicePasoDerivacionSeleccionado ? "history-derivation-step--selected" : "") @(indicePaso == _indicePasoDerivacionActivo ? "history-derivation-step--active" : "")"
title="@(indicePaso == _indicePasoDerivacionActivo ? "Derivación cargada actualmente" : $"Ver Derivación n.º {paso.Numero}")"
@onclick="() => SeleccionarPasoDerivacionHistorial(indicePaso)"> @onclick="() => SeleccionarPasoDerivacionHistorial(indicePaso)">
Der. @paso.Numero Der. @paso.Numero
</button> </button>
@@ -1418,9 +1425,21 @@
</div> </div>
<div class="history-derivation-selected"> <div class="history-derivation-selected">
<div class="history-derivation-selected__summary">
<strong>Derivación n.º @pasoDerivacion.Numero</strong> <strong>Derivación n.º @pasoDerivacion.Numero</strong>
<span>@pasoDerivacion.FechaUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss")</span> <span>@pasoDerivacion.FechaUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss")</span>
</div> </div>
<button type="button"
class="btn-ghost history-derivation-load"
disabled="@(_cargandoPasoDerivacionHistorial || _indicePasoDerivacionSeleccionado == _indicePasoDerivacionActivo)"
@onclick="CargarPasoDerivacionHistorialAsync">
@(_cargandoPasoDerivacionHistorial
? "Cargando..."
: _indicePasoDerivacionSeleccionado == _indicePasoDerivacionActivo
? "Cargada"
: "Cargar derivación")
</button>
</div>
<section class="history-derivation-comparison"> <section class="history-derivation-comparison">
<h6>Respecto al Historial Compartido original</h6> <h6>Respecto al Historial Compartido original</h6>
@@ -1453,7 +1472,7 @@
} }
</section> </section>
@if (pasoDerivacion.Numero > 1) @if (pasoDerivacion.NumeroAnterior is not null)
{ {
<section class="history-derivation-comparison"> <section class="history-derivation-comparison">
<h6>@textoComparacionAnterior</h6> <h6>@textoComparacionAnterior</h6>
@@ -2256,6 +2275,99 @@
</div> </div>
} }
@if (_desgloseScoreVisible is { } desgloseScore)
{
<div class="custom-modal-backdrop score-breakdown-backdrop" @onclick="CerrarDesgloseScore">
<div class="custom-modal-card score-breakdown-modal"
@onclick:stopPropagation="true"
role="dialog"
aria-modal="true"
aria-label="Desglose del score equilibrado A">
<div class="score-breakdown-header">
<div>
<div class="custom-modal-title">Desglose del score equilibrado A</div>
<div class="score-breakdown-subtitle">
Andando corregido + BUS ponderado + penalización por transbordos
</div>
</div>
<button type="button"
class="score-breakdown-close"
title="Cerrar"
aria-label="Cerrar desglose"
@onclick="CerrarDesgloseScore">×</button>
</div>
<div class="score-breakdown-formula">
Score = @FormatearMetrosDesgloseScore(desgloseScore.AportacionAndandoMetros)
+ @FormatearMetrosDesgloseScore(desgloseScore.AportacionBusMetros)
+ @FormatearMetrosDesgloseScore(desgloseScore.PenalizacionTransbordosMetros)
</div>
<section class="score-breakdown-section">
<h3>Distancia andando corregida</h3>
<div class="score-breakdown-note">
Por tramo: máx(distancia, (distancia / @FormatearNumeroDesgloseScore(desgloseScore.BaseAndandoMetros))<sup>@FormatearNumeroDesgloseScore(desgloseScore.FactorAndando)</sup> × @FormatearNumeroDesgloseScore(desgloseScore.BaseAndandoMetros))
</div>
@if (desgloseScore.TramosAndando.Count == 0)
{
<div class="score-breakdown-row"><span>Sin tramos andando</span><strong>0.00 m</strong></div>
}
else
{
@foreach (var tramo in desgloseScore.TramosAndando)
{
<div class="score-breakdown-row">
<span>Tramo @tramo.Numero: @FormatearMetrosDesgloseScore(tramo.DistanciaRealMetros)</span>
<strong>@FormatearMetrosDesgloseScore(tramo.DistanciaCorregidaMetros)</strong>
</div>
}
}
<div class="score-breakdown-row score-breakdown-row--subtotal">
<span>Corrección base</span>
<strong>@FormatearMetrosDesgloseScore(desgloseScore.DistanciaAndandoCorregidaBaseMetros)</strong>
</div>
<div class="score-breakdown-row">
<span>Esfuerzo extra por pendientes @(!desgloseScore.PendientesAplicadas ? "(no aplicado)" : "")</span>
<strong>+ @FormatearMetrosDesgloseScore(desgloseScore.EsfuerzoExtraPendienteMetros)</strong>
</div>
<div class="score-breakdown-row score-breakdown-row--total">
<span>Aportación andando</span>
<strong>@FormatearMetrosDesgloseScore(desgloseScore.AportacionAndandoMetros)</strong>
</div>
</section>
<section class="score-breakdown-section">
<h3>Distancia en BUS</h3>
<div class="score-breakdown-row">
<span>@FormatearMetrosDesgloseScore(desgloseScore.DistanciaBusMetros) ÷ @FormatearNumeroDesgloseScore(desgloseScore.FactorEquilibrado)</span>
<strong>@FormatearMetrosDesgloseScore(desgloseScore.AportacionBusMetros)</strong>
</div>
</section>
<section class="score-breakdown-section">
<h3>Penalización por transbordos</h3>
<div class="score-breakdown-row">
<span>@desgloseScore.NumeroTransbordos transb. × @FormatearNumeroDesgloseScore(desgloseScore.PesoTransbordoMinutos) min</span>
<strong>@FormatearNumeroDesgloseScore(desgloseScore.PenalizacionTransbordosSegundos) s</strong>
</div>
<div class="score-breakdown-note">
Conversión: @FormatearNumeroDesgloseScore(desgloseScore.PenalizacionTransbordosSegundos) s
× (@FormatearNumeroDesgloseScore(desgloseScore.VelocidadBusKmH) km/h × 1000 / 3600)
</div>
<div class="score-breakdown-row score-breakdown-row--total">
<span>Aportación transbordos</span>
<strong>@FormatearMetrosDesgloseScore(desgloseScore.PenalizacionTransbordosMetros)</strong>
</div>
</section>
<div class="score-breakdown-result">
<span>Score final</span>
<strong>@FormatearScoreFinalDesglose(desgloseScore.ScoreTotal)</strong>
</div>
</div>
</div>
}
@if (_mostrarConfirmacionRestablecer) @if (_mostrarConfirmacionRestablecer)
{ {
<div class="custom-modal-backdrop" @onclick="CancelarConfirmacionRestablecer"> <div class="custom-modal-backdrop" @onclick="CancelarConfirmacionRestablecer">

View File

@@ -12,6 +12,9 @@ public sealed class SegmentoSueloTramo
public double DistanciaMetros { get; init; } public double DistanciaMetros { get; init; }
public string Superficie { get; init; } = "Sin datos"; public string Superficie { get; init; } = "Sin datos";
public string? Uso { get; init; } public string? Uso { get; init; }
public int? NumeroCarriles { get; init; }
public string? AnchuraVia { get; init; }
public bool AnchuraViaEstimada { get; init; }
public bool? SinPavimentar { get; init; } public bool? SinPavimentar { get; init; }
public long? IdCamino { get; init; } public long? IdCamino { get; init; }
public int? IndiceInicioForma { get; init; } public int? IndiceInicioForma { get; init; }

View File

@@ -52,6 +52,7 @@ builder.Services.AddSingleton<ServicioIndiceTransitoDbus>();
builder.Services.AddSingleton<BuscadorRutasTemporalesDbus>(); builder.Services.AddSingleton<BuscadorRutasTemporalesDbus>();
builder.Services.AddSingleton<IPlanificadorRutas, PlanificadorRutasServicio>(); builder.Services.AddSingleton<IPlanificadorRutas, PlanificadorRutasServicio>();
builder.Services.AddSingleton<RutasDBUS.Servicios.Configuracion.ServicioValoresFabrica>(); builder.Services.AddSingleton<RutasDBUS.Servicios.Configuracion.ServicioValoresFabrica>();
builder.Services.AddMemoryCache();
// HttpClient genérico (lo usas en el Razor: @inject HttpClient Http) // HttpClient genérico (lo usas en el Razor: @inject HttpClient Http)
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
@@ -59,6 +60,7 @@ builder.Services.AddHttpClient();
// Clientes HTTP tipados // Clientes HTTP tipados
builder.Services.AddHttpClient<IClienteOsrm, ClienteOsrm>(); builder.Services.AddHttpClient<IClienteOsrm, ClienteOsrm>();
builder.Services.AddHttpClient<IClienteValhalla, ClienteValhalla>(); builder.Services.AddHttpClient<IClienteValhalla, ClienteValhalla>();
builder.Services.AddHttpClient<IClienteAtributosOsm, ClienteAtributosOsm>();
var app = builder.Build(); var app = builder.Build();

View File

@@ -0,0 +1,136 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Caching.Memory;
namespace RutasDBUS.Servicios.Caminata;
public sealed class ClienteAtributosOsm : IClienteAtributosOsm
{
private const int MaximoCaminosPorConsulta = 100;
private readonly HttpClient _httpClient;
private readonly IMemoryCache _cache;
private readonly string _baseUrl;
public ClienteAtributosOsm(
HttpClient httpClient,
IMemoryCache cache,
IConfiguration configuracion)
{
_httpClient = httpClient;
_cache = cache;
_baseUrl = (configuracion["Caminata:Osm:BaseUrl"]
?? "https://api.openstreetmap.org/api/0.6").Trim().TrimEnd('/');
}
public async Task<IReadOnlyDictionary<long, AnchuraViaOsm>> ObtenerAnchurasAsync(
IEnumerable<long> idsCamino,
CancellationToken cancellationToken = default)
{
var ids = idsCamino
.Where(id => id > 0)
.Distinct()
.Take(MaximoCaminosPorConsulta)
.ToList();
var resultado = new Dictionary<long, AnchuraViaOsm>();
var pendientes = new List<long>();
foreach (var id in ids)
{
if (_cache.TryGetValue<ResultadoAnchuraCache>(ClaveCache(id), out var cacheado))
{
if (cacheado?.Dato is not null)
resultado[id] = cacheado.Dato;
}
else
{
pendientes.Add(id);
}
}
if (pendientes.Count == 0)
return resultado;
try
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(TimeSpan.FromSeconds(4));
var idsConsulta = string.Join(',', pendientes);
using var peticion = new HttpRequestMessage(
HttpMethod.Get,
$"{_baseUrl}/ways.json?ways={Uri.EscapeDataString(idsConsulta)}");
peticion.Headers.UserAgent.ParseAdd("RutasDBUS/1.0");
using var respuesta = await _httpClient.SendAsync(peticion, timeout.Token);
if (!respuesta.IsSuccessStatusCode)
return resultado;
var datos = await respuesta.Content.ReadFromJsonAsync<RespuestaOsm>(timeout.Token);
var encontrados = new HashSet<long>();
foreach (var elemento in datos?.Elementos ?? [])
{
encontrados.Add(elemento.Id);
var anchura = ObtenerAnchura(elemento.Etiquetas);
GuardarCache(elemento.Id, anchura);
if (anchura is not null)
resultado[elemento.Id] = anchura;
}
foreach (var id in pendientes.Where(id => !encontrados.Contains(id)))
GuardarCache(id, null);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
// La anchura es informativa: un timeout no debe bloquear el tooltip.
}
catch (HttpRequestException)
{
// La información principal de v2 sigue siendo válida si OSM no responde.
}
catch (System.Text.Json.JsonException)
{
// Una respuesta no válida tampoco debe ocultar la información principal de v2.
}
return resultado;
}
private void GuardarCache(long id, AnchuraViaOsm? dato)
=> _cache.Set(
ClaveCache(id),
new ResultadoAnchuraCache(dato),
dato is null ? TimeSpan.FromHours(6) : TimeSpan.FromDays(1));
private static AnchuraViaOsm? ObtenerAnchura(Dictionary<string, string>? etiquetas)
{
if (etiquetas is null)
return null;
if (etiquetas.TryGetValue("width", out var anchura) && !string.IsNullOrWhiteSpace(anchura))
return new AnchuraViaOsm(anchura.Trim(), false);
if (etiquetas.TryGetValue("est_width", out var estimada) && !string.IsNullOrWhiteSpace(estimada))
return new AnchuraViaOsm(estimada.Trim(), true);
return null;
}
private static string ClaveCache(long id) => $"osm:anchura:{id}";
private sealed record ResultadoAnchuraCache(AnchuraViaOsm? Dato);
private sealed class RespuestaOsm
{
[JsonPropertyName("elements")]
public List<ElementoOsm>? Elementos { get; set; }
}
private sealed class ElementoOsm
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("tags")]
public Dictionary<string, string>? Etiquetas { get; set; }
}
}

View File

@@ -19,14 +19,19 @@ public class ClienteValhalla : IClienteValhalla
private const double PasoComprobacionExclusionMetros = 2.0; private const double PasoComprobacionExclusionMetros = 2.0;
private const double ToleranciaBordeExclusionMetros = 1.5; private const double ToleranciaBordeExclusionMetros = 1.5;
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly IClienteAtributosOsm? _clienteAtributosOsm;
private readonly string _baseUrl; private readonly string _baseUrl;
/// <summary> /// <summary>
/// Inicializa una nueva instancia de la clase ClienteValhalla. /// Inicializa una nueva instancia de la clase ClienteValhalla.
/// </summary> /// </summary>
public ClienteValhalla(HttpClient httpClient, IConfiguration configuracion) public ClienteValhalla(
HttpClient httpClient,
IConfiguration configuracion,
IClienteAtributosOsm? clienteAtributosOsm = null)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_clienteAtributosOsm = clienteAtributosOsm;
_baseUrl = configuracion["Caminata:Valhalla:BaseUrl"] _baseUrl = configuracion["Caminata:Valhalla:BaseUrl"]
?? throw new InvalidOperationException( ?? throw new InvalidOperationException(
@@ -501,6 +506,7 @@ public class ClienteValhalla : IClienteValhalla
"edge.surface", "edge.surface",
"edge.unpaved", "edge.unpaved",
"edge.use", "edge.use",
"edge.lane_count",
"edge.way_id", "edge.way_id",
"edge.begin_shape_index", "edge.begin_shape_index",
"edge.end_shape_index" "edge.end_shape_index"
@@ -527,7 +533,32 @@ public class ClienteValhalla : IClienteValhalla
return CrearInformacionSueloNoDisponible("El servicio de superficies no ha respondido correctamente."); return CrearInformacionSueloNoDisponible("El servicio de superficies no ha respondido correctamente.");
var data = await respuesta.Content.ReadFromJsonAsync<RespuestaAtributosSuperficieValhalla>(timeout.Token); var data = await respuesta.Content.ReadFromJsonAsync<RespuestaAtributosSuperficieValhalla>(timeout.Token);
return ConvertirInformacionSuelo(data); IReadOnlyDictionary<long, AnchuraViaOsm>? anchuras = null;
if (_clienteAtributosOsm is not null && data?.Aristas is not null)
{
try
{
anchuras = await _clienteAtributosOsm.ObtenerAnchurasAsync(
data.Aristas
.Where(arista => arista.IdCamino is > 0)
.Select(arista => arista.IdCamino!.Value),
cancellationToken);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
// La anchura es opcional y no debe ocultar los datos de superficie de v2.
}
catch (HttpRequestException)
{
// La anchura es opcional y no debe ocultar los datos de superficie de v2.
}
catch (JsonException)
{
// La anchura es opcional y no debe ocultar los datos de superficie de v2.
}
}
return ConvertirInformacionSuelo(data, anchuras);
} }
catch catch
{ {
@@ -559,7 +590,9 @@ public class ClienteValhalla : IClienteValhalla
return forma; return forma;
} }
private static InformacionSueloTramo ConvertirInformacionSuelo(RespuestaAtributosSuperficieValhalla? data) private static InformacionSueloTramo ConvertirInformacionSuelo(
RespuestaAtributosSuperficieValhalla? data,
IReadOnlyDictionary<long, AnchuraViaOsm>? anchuras = null)
{ {
if (data?.Aristas is null || data.Aristas.Count == 0) if (data?.Aristas is null || data.Aristas.Count == 0)
return CrearInformacionSueloNoDisponible("No hay datos de superficie para este tramo."); return CrearInformacionSueloNoDisponible("No hay datos de superficie para este tramo.");
@@ -573,8 +606,17 @@ public class ClienteValhalla : IClienteValhalla
var superficie = arista.Superficie ?? "No disponible"; var superficie = arista.Superficie ?? "No disponible";
var uso = arista.Uso; var uso = arista.Uso;
AnchuraViaOsm? anchura = null;
if (arista.IdCamino is long idCamino && anchuras is not null)
anchuras.TryGetValue(idCamino, out anchura);
if (segmentos.Count > 0 && EsMismaSuperficie(segmentos[^1], superficie, uso, arista.SinPavimentar)) if (segmentos.Count > 0 && EsMismaSuperficie(
segmentos[^1],
superficie,
uso,
arista.NumeroCarriles,
anchura,
arista.SinPavimentar))
{ {
var anterior = segmentos[^1]; var anterior = segmentos[^1];
segmentos[^1] = new SegmentoSueloTramo segmentos[^1] = new SegmentoSueloTramo
@@ -582,6 +624,9 @@ public class ClienteValhalla : IClienteValhalla
DistanciaMetros = anterior.DistanciaMetros + distanciaMetros, DistanciaMetros = anterior.DistanciaMetros + distanciaMetros,
Superficie = anterior.Superficie, Superficie = anterior.Superficie,
Uso = anterior.Uso, Uso = anterior.Uso,
NumeroCarriles = anterior.NumeroCarriles,
AnchuraVia = anterior.AnchuraVia,
AnchuraViaEstimada = anterior.AnchuraViaEstimada,
SinPavimentar = anterior.SinPavimentar, SinPavimentar = anterior.SinPavimentar,
IdCamino = anterior.IdCamino == arista.IdCamino ? anterior.IdCamino : null, IdCamino = anterior.IdCamino == arista.IdCamino ? anterior.IdCamino : null,
IndiceInicioForma = anterior.IndiceInicioForma, IndiceInicioForma = anterior.IndiceInicioForma,
@@ -595,6 +640,9 @@ public class ClienteValhalla : IClienteValhalla
DistanciaMetros = distanciaMetros, DistanciaMetros = distanciaMetros,
Superficie = superficie, Superficie = superficie,
Uso = uso, Uso = uso,
NumeroCarriles = arista.NumeroCarriles,
AnchuraVia = anchura?.Valor,
AnchuraViaEstimada = anchura?.EsEstimada ?? false,
SinPavimentar = arista.SinPavimentar, SinPavimentar = arista.SinPavimentar,
IdCamino = arista.IdCamino, IdCamino = arista.IdCamino,
IndiceInicioForma = arista.IndiceInicioForma, IndiceInicioForma = arista.IndiceInicioForma,
@@ -617,9 +665,14 @@ public class ClienteValhalla : IClienteValhalla
SegmentoSueloTramo anterior, SegmentoSueloTramo anterior,
string superficie, string superficie,
string? uso, string? uso,
int? numeroCarriles,
AnchuraViaOsm? anchura,
bool? sinPavimentar) bool? sinPavimentar)
=> string.Equals(anterior.Superficie, superficie, StringComparison.Ordinal) => string.Equals(anterior.Superficie, superficie, StringComparison.Ordinal)
&& string.Equals(anterior.Uso, uso, StringComparison.Ordinal) && string.Equals(anterior.Uso, uso, StringComparison.Ordinal)
&& anterior.NumeroCarriles == numeroCarriles
&& string.Equals(anterior.AnchuraVia, anchura?.Valor, StringComparison.Ordinal)
&& anterior.AnchuraViaEstimada == (anchura?.EsEstimada ?? false)
&& anterior.SinPavimentar == sinPavimentar; && anterior.SinPavimentar == sinPavimentar;
private static double ConvertirLongitudMetros(double? longitud, string? unidades) private static double ConvertirLongitudMetros(double? longitud, string? unidades)
@@ -647,21 +700,6 @@ public class ClienteValhalla : IClienteValhalla
_ => "Sin datos" _ => "Sin datos"
}; };
private static string? EtiquetaUso(string? uso)
=> (uso ?? "").Trim().ToLowerInvariant() switch
{
"sidewalk" => "Acera",
"footway" => "Camino peatonal",
"steps" => "Escaleras",
"path" => "Senda",
"track" => "Pista",
"road" => "Calzada",
"service_other" => "Vía de servicio",
"other" => "Otro",
"" => null,
var valor => valor
};
private static InformacionSueloTramo CrearInformacionSueloNoDisponible(string mensaje) private static InformacionSueloTramo CrearInformacionSueloNoDisponible(string mensaje)
=> new() => new()
{ {
@@ -754,6 +792,9 @@ public class ClienteValhalla : IClienteValhalla
[JsonPropertyName("use")] [JsonPropertyName("use")]
public string? Uso { get; set; } public string? Uso { get; set; }
[JsonPropertyName("lane_count")]
public int? NumeroCarriles { get; set; }
[JsonPropertyName("way_id")] [JsonPropertyName("way_id")]
public long? IdCamino { get; set; } public long? IdCamino { get; set; }

View File

@@ -0,0 +1,10 @@
namespace RutasDBUS.Servicios.Caminata;
public sealed record AnchuraViaOsm(string Valor, bool EsEstimada);
public interface IClienteAtributosOsm
{
Task<IReadOnlyDictionary<long, AnchuraViaOsm>> ObtenerAnchurasAsync(
IEnumerable<long> idsCamino,
CancellationToken cancellationToken = default);
}

View File

@@ -3398,13 +3398,6 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
finales.Add(alternativaPie); finales.Add(alternativaPie);
} }
var globalMasRapida = finales
.OrderBy(x => DuracionVisibleOrdenacion(x))
.ThenBy(x => x.PuntuacionCoste)
.FirstOrDefault();
if (globalMasRapida is not null && globalMasRapida.EsSoloAPie)
globalMasRapida.Etiqueta = "M\u00e1s r\u00e1pida";
return finales; return finales;
} }

View File

@@ -20,6 +20,9 @@
}, },
"Valhalla": { "Valhalla": {
"BaseUrl": "http://192.168.41.12:8002" "BaseUrl": "http://192.168.41.12:8002"
},
"Osm": {
"BaseUrl": "https://api.openstreetmap.org/api/0.6"
} }
}, },

View File

@@ -1417,6 +1417,147 @@ html, body {
.fabrica-cambios { max-height: 45dvh; overflow-y: auto; font-size: .8rem; overflow-wrap: anywhere; } .fabrica-cambios { max-height: 45dvh; overflow-y: auto; font-size: .8rem; overflow-wrap: anywhere; }
.fabrica-cambios > div { padding: 5px 0; border-bottom: 1px solid #374151; } .fabrica-cambios > div { padding: 5px 0; border-bottom: 1px solid #374151; }
.route-score-detail-line {
display: inline-flex;
align-items: center;
gap: 5px;
}
.route-score-info,
.score-breakdown-close {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
padding: 0;
color: #cbd5e1;
border: 1px solid rgba(148, 163, 184, .55);
background: rgba(15, 23, 42, .7);
cursor: pointer;
}
.route-score-info {
width: 18px;
height: 18px;
border-radius: 50%;
font-size: .68rem;
font-weight: 800;
line-height: 1;
}
.route-score-info:hover,
.route-score-info:focus-visible,
.score-breakdown-close:hover,
.score-breakdown-close:focus-visible {
color: #fff;
border-color: #38bdf8;
outline: none;
}
.score-breakdown-backdrop {
z-index: 5750;
}
.custom-modal-card.score-breakdown-modal {
width: min(590px, calc(100vw - 2rem));
max-height: min(780px, calc(100dvh - 2rem));
padding: 1rem;
overflow-y: auto;
}
.score-breakdown-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: .75rem;
}
.score-breakdown-header .custom-modal-title {
margin-bottom: .15rem;
}
.score-breakdown-subtitle,
.score-breakdown-note {
color: #94a3b8;
font-size: .75rem;
}
.score-breakdown-close {
width: 28px;
height: 28px;
border-radius: 50%;
font-size: 1rem;
}
.score-breakdown-formula {
margin-top: .85rem;
padding: .7rem 0;
color: #e2e8f0;
border-top: 1px solid rgba(148, 163, 184, .22);
border-bottom: 1px solid rgba(148, 163, 184, .22);
font-size: .82rem;
font-weight: 650;
}
.score-breakdown-section {
padding: .8rem 0;
border-bottom: 1px solid rgba(148, 163, 184, .18);
}
.score-breakdown-section h3 {
margin: 0 0 .45rem;
color: #f8fafc;
font-size: .84rem;
letter-spacing: 0;
}
.score-breakdown-section .score-breakdown-note {
margin-bottom: .45rem;
overflow-wrap: anywhere;
}
.score-breakdown-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: baseline;
gap: .75rem;
padding: .25rem 0;
color: #cbd5e1;
font-size: .79rem;
}
.score-breakdown-row strong {
color: #f8fafc;
white-space: nowrap;
}
.score-breakdown-row--subtotal {
margin-top: .25rem;
padding-top: .45rem;
border-top: 1px dashed rgba(148, 163, 184, .2);
}
.score-breakdown-row--total {
color: #e2e8f0;
font-weight: 700;
}
.score-breakdown-result {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
padding-top: .85rem;
color: #f8fafc;
font-size: .95rem;
font-weight: 750;
}
.score-breakdown-result strong {
color: #facc15;
font-size: 1.12rem;
}
/* LISTA DE TRAMOS TIPO “DIRECCIONES” CON NÚMEROS */ /* LISTA DE TRAMOS TIPO “DIRECCIONES” CON NÚMEROS */
.lista-tramos-ruta { .lista-tramos-ruta {
@@ -1727,9 +1868,13 @@ html, body {
color: #fef3c7; color: #fef3c7;
} }
.history-derivation-step--active {
box-shadow: inset 0 -2px #22c55e;
}
.history-derivation-selected { .history-derivation-selected {
display: flex; display: flex;
align-items: baseline; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: .75rem; gap: .75rem;
padding: .5rem 0; padding: .5rem 0;
@@ -1738,12 +1883,31 @@ html, body {
color: #f8fafc; color: #f8fafc;
} }
.history-derivation-selected span { .history-derivation-selected__summary {
display: flex;
flex-direction: column;
gap: .1rem;
min-width: 0;
}
.history-derivation-selected__summary span {
color: #94a3b8; color: #94a3b8;
font-size: .72rem; font-size: .72rem;
white-space: nowrap; white-space: nowrap;
} }
.btn-ghost.history-derivation-load {
flex: 0 0 auto;
min-height: 30px;
padding: .3rem .75rem;
border-color: rgba(59, 130, 246, .72);
background: rgba(29, 78, 216, .34);
}
.btn-ghost.history-derivation-load:hover {
background: #1d4ed8;
}
.history-derivation-comparison { .history-derivation-comparison {
padding-top: .7rem; padding-top: .7rem;
} }

View File

@@ -1,6 +1,7 @@
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using RutasDBUS.Components.Pages; using RutasDBUS.Components.Pages;
using RutasDBUS.Modelos.Historial; using RutasDBUS.Modelos.Historial;
@@ -180,6 +181,55 @@ Check((bool)puntoDentro.Invoke(null, new object?[] { 43.3155, -1.9845, parametro
"Las paradas interiores a una zona se detectan antes de calcular"); "Las paradas interiores a una zona se detectan antes de calcular");
var page = new PlanificadorRutas(); var page = new PlanificadorRutas();
var flags = BindingFlags.Instance | BindingFlags.NonPublic; var flags = BindingFlags.Instance | BindingFlags.NonPublic;
var fabricaHistorial = (ConfiguracionHistorialRuta)baseMethod.Invoke(null, null)!;
fabricaHistorial.FactorEquilibradoAndarBus = 6;
typeof(PlanificadorRutas).GetField("_configuracionFabrica", flags)!.SetValue(page, fabricaHistorial);
var crearPredeterminadaHistorial = typeof(PlanificadorRutas).GetMethod(
"CrearConfiguracionPredeterminadaParaHistorial", flags)!;
var configuracionGuardada = fabricaHistorial.Clonar();
configuracionGuardada.UsarHoraSimulada = true;
configuracionGuardada.FechaSimulada = "2026-04-08";
configuracionGuardada.HoraSimulada = "08:46:17";
configuracionGuardada.FactorEquilibradoAndarBus = 99;
var registroConHora = new RegistroHistorialRuta { Configuracion = configuracionGuardada };
var predeterminadaConHora = (ConfiguracionHistorialRuta)crearPredeterminadaHistorial.Invoke(
page, new object[] { registroConHora })!;
Check(predeterminadaConHora.UsarHoraSimulada &&
predeterminadaConHora.FechaSimulada == "2026-04-08" &&
predeterminadaConHora.HoraSimulada == "08:46:17" &&
predeterminadaConHora.FactorEquilibradoAndarBus == 6,
"Predeterminada conserva solo la fecha y hora simuladas del historial");
configuracionGuardada.UsarHoraSimulada = false;
configuracionGuardada.FechaSimulada = "2020-01-01";
configuracionGuardada.HoraSimulada = "01:02:03";
var predeterminadaSinHora = (ConfiguracionHistorialRuta)crearPredeterminadaHistorial.Invoke(
page, new object[] { registroConHora })!;
Check(!predeterminadaSinHora.UsarHoraSimulada &&
predeterminadaSinHora.FechaSimulada == DateOnly.FromDateTime(DateTime.Now)
.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture) &&
predeterminadaSinHora.HoraSimulada != "01:02:03",
"Predeterminada usa la hora actual cuando el historial no tenia simulacion");
typeof(PlanificadorRutas).GetField("_factorEquilibradoAndarBus", flags)!.SetValue(page, 6.0);
typeof(PlanificadorRutas).GetField("_factorPenalizacionDistanciaPie", flags)!.SetValue(page, 1.8);
typeof(PlanificadorRutas).GetField("_parametroPenalizacionDistanciaPieMetros", flags)!.SetValue(page, 200.0);
typeof(PlanificadorRutas).GetField("_pesoTransbordoRankingMinutos", flags)!.SetValue(page, 6.0);
typeof(PlanificadorRutas).GetField("_velocidadBusKmH", flags)!.SetValue(page, 16.0);
typeof(PlanificadorRutas).GetField("_mostrarElevacionesRuta", flags)!.SetValue(page, true);
var alternativaDesglose = new AlternativaRuta
{
AlternativaBus = new RutaBusAlternativa { DistanciaTotalBus = 6000 },
DistanciasPiePorTramoAprox = new List<double> { 100, 300 },
EsfuerzoExtraPendienteMetros = 25,
NumeroTransbordosReales = 1
};
var calcularScore = typeof(PlanificadorRutas).GetMethod("CalcularScoreEquilibradoFactor", flags)!;
var crearDesglose = typeof(PlanificadorRutas).GetMethod("CrearDesgloseScoreEquilibrado", flags)!;
var scoreCalculado = (double)calcularScore.Invoke(page, new object?[] { alternativaDesglose })!;
var desgloseCalculado = crearDesglose.Invoke(page, new object?[] { alternativaDesglose })!;
var scoreDesglosado = (double)desgloseCalculado.GetType().GetProperty("ScoreTotal")!
.GetValue(desgloseCalculado)!;
Check(Math.Abs(scoreCalculado - scoreDesglosado) < 0.0001,
"El desglose y el ranking usan exactamente el mismo score");
var formatearEtiqueta = typeof(PlanificadorRutas).GetMethod("FormatearEtiquetaAlternativaTarjeta", flags)!; var formatearEtiqueta = typeof(PlanificadorRutas).GetMethod("FormatearEtiquetaAlternativaTarjeta", flags)!;
Check((string)formatearEtiqueta.Invoke(page, new object[] Check((string)formatearEtiqueta.Invoke(page, new object[]
{ new AlternativaRuta { Etiqueta = "Equilibrada" }, 0 })! == "Opción 1", { new AlternativaRuta { Etiqueta = "Equilibrada" }, 0 })! == "Opción 1",
@@ -206,6 +256,12 @@ Check(!(bool)typeof(PlanificadorRutas).GetField("_activarRangoLineas", flags)!.G
(int)typeof(PlanificadorRutas).GetField("_itinerarioMin", flags)!.GetValue(page)! == defaults.ItinerarioMin && (int)typeof(PlanificadorRutas).GetField("_itinerarioMin", flags)!.GetValue(page)! == defaults.ItinerarioMin &&
(int)typeof(PlanificadorRutas).GetField("_itinerarioMax", flags)!.GetValue(page)! == defaults.ItinerarioMax, (int)typeof(PlanificadorRutas).GetField("_itinerarioMax", flags)!.GetValue(page)! == defaults.ItinerarioMax,
"Restaurar aplica tambien los valores de lineas e itinerarios"); "Restaurar aplica tambien los valores de lineas e itinerarios");
var campoModoHistorial = typeof(PlanificadorRutas).GetField("_modoConfiguracionHistorial", flags)!;
var modoGuardada = Enum.Parse(campoModoHistorial.FieldType, "Guardada");
campoModoHistorial.SetValue(page, modoGuardada);
typeof(PlanificadorRutas).GetMethod("AplicarValoresFabricaConservandoSeleccion", flags)!.Invoke(page, null);
Check(Equals(campoModoHistorial.GetValue(page), modoGuardada),
"Restaurar valores conserva el modo de configuracion del historial");
var options = (List<AlternativaRuta>)typeof(PlanificadorRutas).GetField("_opcionesRuta", flags)!.GetValue(page)!; var options = (List<AlternativaRuta>)typeof(PlanificadorRutas).GetField("_opcionesRuta", flags)!.GetValue(page)!;
var selected = typeof(PlanificadorRutas).GetField("_indiceAlternativaSeleccionada", flags)!; var selected = typeof(PlanificadorRutas).GetField("_indiceAlternativaSeleccionada", flags)!;
var remember = typeof(PlanificadorRutas).GetMethod("ObtenerProveedorSoloAPieParaRecalculo", flags)!; var remember = typeof(PlanificadorRutas).GetMethod("ObtenerProveedorSoloAPieParaRecalculo", flags)!;
@@ -221,6 +277,26 @@ selected.SetValue(page, 0);
Check(Remember(true) is null, "Seleccionar bus libera la preferencia andando"); Check(Remember(true) is null, "Seleccionar bus libera la preferencia andando");
options.Clear(); options.Clear();
Check(Remember(false) is null, "Nueva seleccion de criterio no hereda la preferencia andando"); Check(Remember(false) is null, "Nueva seleccion de criterio no hereda la preferencia andando");
var osmHandler = new OsmWidthHandler();
using var osmCache = new MemoryCache(new MemoryCacheOptions());
var osmConfig = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["Caminata:Osm:BaseUrl"] = "https://osm.test/api/0.6"
}).Build();
var osmClient = new ClienteAtributosOsm(new HttpClient(osmHandler), osmCache, osmConfig);
var anchos = await osmClient.ObtenerAnchurasAsync([101, 102, 103]);
Check(anchos[101] == new AnchuraViaOsm("4.5", false) &&
anchos[102] == new AnchuraViaOsm("2", true) &&
!anchos.ContainsKey(103),
"La anchura exacta y la estimada se leen de las etiquetas OSM");
await osmClient.ObtenerAnchurasAsync([101, 102, 103]);
Check(osmHandler.Calls == 1, "Las anchuras OSM se reutilizan desde cache");
var formatearSuperficieSinDatos = typeof(PlanificadorRutas).GetMethod(
"FormatearSuperficieSegmento", BindingFlags.Static | BindingFlags.NonPublic)!;
var superficieSinDatos = (string)formatearSuperficieSinDatos.Invoke(null, [null])!;
Check(superficieSinDatos.Contains("Carriles: -") &&
superficieSinDatos.Contains("Anchura: -"),
"El tooltip mantiene todos los atributos cuando no hay datos de via");
Console.WriteLine("Pruebas completadas."); Console.WriteLine("Pruebas completadas.");
sealed class FakeValhalla : IClienteValhalla sealed class FakeValhalla : IClienteValhalla
@@ -246,6 +322,31 @@ sealed class FakeValhalla : IClienteValhalla
} }
} }
sealed class OsmWidthHandler : HttpMessageHandler
{
public int Calls { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
Calls++;
const string contenido = """
{
"elements": [
{ "type": "way", "id": 101, "tags": { "width": "4.5" } },
{ "type": "way", "id": 102, "tags": { "est_width": "2" } },
{ "type": "way", "id": 103, "tags": { "highway": "residential" } }
]
}
""";
return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(contenido, System.Text.Encoding.UTF8, "application/json")
});
}
}
sealed class RetryValhallaHandler : HttpMessageHandler sealed class RetryValhallaHandler : HttpMessageHandler
{ {
public List<string> Bodies { get; } = new(); public List<string> Bodies { get; } = new();