Añadidas mejoras en rutas sugeridas, elevaciones e historial

- Añadido interruptor de elevaciones en rutas sugeridas.
- Los tramos a pie mantienen la línea punteada y muestran puntos coloreados por pendiente.
- Se quita el score equilibrado de las tarjetas para limpiar la vista.
- Se destaca la distancia a pie también en modo Equilib. A.
- El factor equilibrado se guarda y restaura desde el historial local y compartido.
- Ajustes visuales y limpieza de estilos asociados.
This commit is contained in:
2026-07-09 14:56:53 +02:00
parent 8b106a8319
commit 7edb2dd2e9
14 changed files with 763 additions and 289 deletions

View File

@@ -55,10 +55,12 @@ public partial class PlanificadorRutas : ComponentBase
private const double CoeficienteEsfuerzoAlturaPorDefecto = 5.2; private const double CoeficienteEsfuerzoAlturaPorDefecto = 5.2;
private double _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; private double _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
private double _pesoTransbordoRankingMinutos = 4.0; private double _pesoTransbordoRankingMinutos = 4.0;
private double _factorEquilibradoAndarBus = 12.0;
private string _factorPenalizacionDistanciaPieTextoEdicion = "1.60"; private string _factorPenalizacionDistanciaPieTextoEdicion = "1.60";
private string _parametroPenalizacionDistanciaPieTextoEdicion = "100"; private string _parametroPenalizacionDistanciaPieTextoEdicion = "100";
private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20"; private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20";
private string _pesoTransbordoRankingTextoEdicion = "4,0"; private string _pesoTransbordoRankingTextoEdicion = "4,0";
private string _factorEquilibradoAndarBusTextoEdicion = "12";
private double _velocidadBusKmH = 16.0; private double _velocidadBusKmH = 16.0;
private int _minutosTransbordo = 5; private int _minutosTransbordo = 5;
private int _margenTransbordoSegundos = 120; private int _margenTransbordoSegundos = 120;
@@ -142,6 +144,25 @@ public partial class PlanificadorRutas : ComponentBase
} }
} }
/// <summary>
/// Expone el factor que convierte metros andando a metros equivalentes de bus en el modo equilibrado.
/// </summary>
private string FactorEquilibradoAndarBusTexto
{
get => _factorEquilibradoAndarBusTextoEdicion;
set
{
var texto = (value ?? "").Replace(',', '.');
_factorEquilibradoAndarBusTextoEdicion = texto;
if (!double.TryParse(texto, NumberStyles.Float, CultureInfo.InvariantCulture, out var valor))
return;
if (double.IsFinite(valor))
_factorEquilibradoAndarBus = Math.Max(0, valor);
}
}
private static bool TryParseDuracionMinutos(string texto, out double minutos) private static bool TryParseDuracionMinutos(string texto, out double minutos)
{ {
minutos = 0; minutos = 0;
@@ -220,6 +241,7 @@ public partial class PlanificadorRutas : ComponentBase
_parametroPenalizacionDistanciaPieTextoEdicion = _parametroPenalizacionDistanciaPieMetros.ToString("0", CultureInfo.InvariantCulture); _parametroPenalizacionDistanciaPieTextoEdicion = _parametroPenalizacionDistanciaPieMetros.ToString("0", CultureInfo.InvariantCulture);
_coeficienteEsfuerzoAlturaTextoEdicion = _coeficienteEsfuerzoAltura.ToString("0.00", CultureInfo.InvariantCulture); _coeficienteEsfuerzoAlturaTextoEdicion = _coeficienteEsfuerzoAltura.ToString("0.00", CultureInfo.InvariantCulture);
_pesoTransbordoRankingTextoEdicion = FormatearDuracionMinutos(_pesoTransbordoRankingMinutos); _pesoTransbordoRankingTextoEdicion = FormatearDuracionMinutos(_pesoTransbordoRankingMinutos);
_factorEquilibradoAndarBusTextoEdicion = _factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture);
} }
/// <summary> /// <summary>
@@ -257,6 +279,16 @@ public partial class PlanificadorRutas : ComponentBase
SincronizarTextosPenalizacionPie(); SincronizarTextosPenalizacionPie();
await PersistirConfiguracionAsync(); await PersistirConfiguracionAsync();
} }
/// <summary>
/// Normaliza el factor equilibrado al salir del campo.
/// </summary>
private async Task OnBlurFactorEquilibradoAndarBus()
{
SincronizarTextosPenalizacionPie();
await PersistirConfiguracionAsync();
await RecalcularRutaEquilibradaTrasCambioFactorAsync();
}
private int _radioCandidatasMetros = 1200; private int _radioCandidatasMetros = 1200;
private int _maxCandidatasPorLado = 12; private int _maxCandidatasPorLado = 12;
@@ -317,6 +349,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 _pintandoElevacionesRuta = false;
private readonly Dictionary<string, List<Parada>> _paradasItinerarioVisiblesPorId = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, List<Parada>> _paradasItinerarioVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, List<Parada>> _paradasRutaVisiblesPorId = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, List<Parada>> _paradasRutaVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
@@ -1016,6 +1050,23 @@ public partial class PlanificadorRutas : ComponentBase
} }
catch { } catch { }
} }
private async Task DrawRouteElevationPointsJsAsync(string id, IReadOnlyList<object> points)
{
try
{
await JS.InvokeVoidAsync("rt.drawElevPoints", $"route_elev_{id}", points, new
{
radius = 4,
opacity = 0.98,
fillOpacity = 0.95,
showLabels = false,
showSlopeTooltips = true,
className = "rt-route-elev-point"
});
}
catch { }
}
/// <summary> /// <summary>
/// Ajusta rutas js. /// Ajusta rutas js.
@@ -1168,55 +1219,6 @@ public partial class PlanificadorRutas : ComponentBase
+ UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieFinAprox, parametros); + UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieFinAprox, parametros);
} }
/// <summary>
/// Estima si una alternativa equilibrada se parece mas al criterio Tiempo o al criterio Andar.
/// </summary>
private string FormatearBalanceEquilibradoAlternativa(AlternativaRuta? alternativa)
{
if (alternativa is null || alternativa.EsSoloAPie)
return string.Empty;
var alternativasBus = _opcionesRuta
.Where(opcion => !opcion.EsSoloAPie)
.ToList();
if (alternativasBus.Count < 2)
return string.Empty;
var duraciones = alternativasBus
.Select(DuracionOrdenacion)
.Where(double.IsFinite)
.ToList();
var costesAndar = alternativasBus
.Select(CalcularCosteAndarEquilibradoSegundos)
.Where(double.IsFinite)
.ToList();
if (duraciones.Count == 0 || costesAndar.Count == 0)
return string.Empty;
var ventajaTiempo = CalcularVentajaNormalizada(
DuracionOrdenacion(alternativa),
duraciones.Min(),
duraciones.Max());
var ventajaAndar = CalcularVentajaNormalizada(
CalcularCosteAndarEquilibradoSegundos(alternativa),
costesAndar.Min(),
costesAndar.Max());
var total = ventajaTiempo + ventajaAndar;
if (!double.IsFinite(total) || total <= 0.000001)
return string.Empty;
var porcentajeTiempo = (int)Math.Round((ventajaTiempo / total) * 100.0);
porcentajeTiempo = Math.Clamp(porcentajeTiempo, 0, 100);
var porcentajeAndar = 100 - porcentajeTiempo;
return $"Tiempo {porcentajeTiempo}% / Andar {porcentajeAndar}%";
}
private double CalcularCosteAndarEquilibradoSegundos(AlternativaRuta? alternativa) private double CalcularCosteAndarEquilibradoSegundos(AlternativaRuta? alternativa)
{ {
if (alternativa is null) if (alternativa is null)
@@ -1229,30 +1231,70 @@ public partial class PlanificadorRutas : ComponentBase
return EstimarDuracionPieSegundos(distanciaCorregida); return EstimarDuracionPieSegundos(distanciaCorregida);
} }
private static double CalcularVentajaNormalizada(double valor, double mejor, double peor) private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa)
{ {
if (!double.IsFinite(valor) || if (alternativa is null || alternativa.EsSoloAPie || alternativa.AlternativaBus is null)
!double.IsFinite(mejor) || return double.PositiveInfinity;
!double.IsFinite(peor) ||
peor <= mejor)
{
return 0;
}
return Math.Clamp((peor - valor) / (peor - mejor), 0.0, 1.0); var distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus);
var distanciaAndando = Math.Max(0, alternativa.DistanciaPieTotalAprox);
var factor = double.IsFinite(_factorEquilibradoAndarBus)
? Math.Max(0, _factorEquilibradoAndarBus)
: 12.0;
var score = distanciaBus + (distanciaAndando * factor);
return double.IsFinite(score) && score >= 0
? score
: double.PositiveInfinity;
} }
private static double CalcularSacrificioNormalizado(double valor, double mejor, double peor) private string FormatearScoreEquilibradoAlternativa(AlternativaRuta? alternativa)
{ {
if (!double.IsFinite(valor) || var score = CalcularScoreEquilibradoFactor(alternativa);
!double.IsFinite(mejor) || if (!double.IsFinite(score))
!double.IsFinite(peor) || return string.Empty;
peor <= mejor)
return $"Score {FormatearDistanciaTarjeta(score)} (x{_factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture)})";
}
private string FactorEquilibradoAndarBusResumen
=> _factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture);
private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync()
{
if (!CriterioPrioritarioEquilibradoActivo)
return;
if (_mapa is not null && _coordenadasOrigen is not null && _coordenadasDestino is not null)
{ {
return 0; _omitirRegistroHistorialEnSiguienteCalculo = true;
await CalcularRutaCompletaAsync();
return;
} }
return Math.Clamp((valor - mejor) / (peor - mejor), 0.0, 1.0); if (_opcionesRuta.Count == 0)
{
StateHasChanged();
return;
}
OrdenarOpcionesRutaSegunCriterio();
var indicePreferido = BuscarIndiceEquilibrado();
if (indicePreferido < 0 || indicePreferido >= _opcionesRuta.Count)
{
StateHasChanged();
return;
}
await CerrarTodasLasPreviewsAsync();
_indiceAlternativaSeleccionada = indicePreferido;
await CerrarPopupsAsync();
ResetEstadoRuta(limpiarOpciones: false, limpiarMensaje: false);
_mensajeRuta = "Representando ruta seleccionada...";
StateHasChanged();
await Task.Yield();
await PintarOpcionRutaCompleta(_opcionesRuta[indicePreferido]);
StateHasChanged();
} }
/// <summary> /// <summary>

View File

@@ -184,6 +184,7 @@ public partial class PlanificadorRutas
existente.HoraSimulada = actual.HoraSimulada; existente.HoraSimulada = actual.HoraSimulada;
existente.CriterioPrioritarioRuta = actual.CriterioPrioritarioRuta; existente.CriterioPrioritarioRuta = actual.CriterioPrioritarioRuta;
existente.PreferenciaEtiquetaRuta = actual.PreferenciaEtiquetaRuta; existente.PreferenciaEtiquetaRuta = actual.PreferenciaEtiquetaRuta;
existente.FactorEquilibradoAndarBus = actual.FactorEquilibradoAndarBus;
existente.ActivarFiltroLineas = actual.ActivarFiltroLineas; existente.ActivarFiltroLineas = actual.ActivarFiltroLineas;
existente.LineasPermitidas = actual.LineasPermitidas.ToList(); existente.LineasPermitidas = actual.LineasPermitidas.ToList();
existente.FechaActualizacionUtc = DateTime.UtcNow; existente.FechaActualizacionUtc = DateTime.UtcNow;
@@ -369,6 +370,15 @@ public partial class PlanificadorRutas
_criterioPrioritarioRuta = NormalizarCriterioRuta(item.CriterioPrioritarioRuta, item.PreferenciaEtiquetaRuta); _criterioPrioritarioRuta = NormalizarCriterioRuta(item.CriterioPrioritarioRuta, item.PreferenciaEtiquetaRuta);
SincronizarPreferenciaDesdeCriterioRuta(); SincronizarPreferenciaDesdeCriterioRuta();
if (item.FactorEquilibradoAndarBus is { } factorEquilibrado && double.IsFinite(factorEquilibrado))
{
_factorEquilibradoAndarBus = Math.Max(0, factorEquilibrado);
SincronizarTextosPenalizacionPie();
if (_configuracionTemporalPreviaHistorial is not null)
_configuracionTemporalPreviaHistorial.FactorEquilibradoAndarBus = _factorEquilibradoAndarBus;
}
_activarRangoLineas = item.ActivarFiltroLineas; _activarRangoLineas = item.ActivarFiltroLineas;
_lineasPermitidas.Clear(); _lineasPermitidas.Clear();
foreach (var linea in NormalizarLineasHistorial(item.LineasPermitidas)) foreach (var linea in NormalizarLineasHistorial(item.LineasPermitidas))
@@ -445,6 +455,9 @@ public partial class PlanificadorRutas
HoraSimulada = TimeOnly.FromDateTime(ahora).ToString("HH:mm:ss", CultureInfo.InvariantCulture), HoraSimulada = TimeOnly.FromDateTime(ahora).ToString("HH:mm:ss", CultureInfo.InvariantCulture),
CriterioPrioritarioRuta = _criterioPrioritarioRuta, CriterioPrioritarioRuta = _criterioPrioritarioRuta,
PreferenciaEtiquetaRuta = _preferenciaEtiquetaRuta, PreferenciaEtiquetaRuta = _preferenciaEtiquetaRuta,
FactorEquilibradoAndarBus = double.IsFinite(_factorEquilibradoAndarBus)
? Math.Max(0, _factorEquilibradoAndarBus)
: 12.0,
ActivarFiltroLineas = _activarRangoLineas, ActivarFiltroLineas = _activarRangoLineas,
LineasPermitidas = ObtenerLineasPermitidasSeleccionadas().ToList(), LineasPermitidas = ObtenerLineasPermitidasSeleccionadas().ToList(),
FechaCreacionUtc = DateTime.UtcNow, FechaCreacionUtc = DateTime.UtcNow,
@@ -573,6 +586,9 @@ public partial class PlanificadorRutas
var fecha = item.UsarHoraSimulada ? item.FechaSimulada?.Trim() ?? string.Empty : string.Empty; var fecha = item.UsarHoraSimulada ? item.FechaSimulada?.Trim() ?? string.Empty : string.Empty;
var hora = item.UsarHoraSimulada ? item.HoraSimulada?.Trim() ?? string.Empty : string.Empty; var hora = item.UsarHoraSimulada ? item.HoraSimulada?.Trim() ?? string.Empty : string.Empty;
var criterio = NormalizarCriterioRuta(item.CriterioPrioritarioRuta, item.PreferenciaEtiquetaRuta); var criterio = NormalizarCriterioRuta(item.CriterioPrioritarioRuta, item.PreferenciaEtiquetaRuta);
var factorEquilibrado = item.FactorEquilibradoAndarBus is { } factor && double.IsFinite(factor)
? Math.Max(0, factor).ToString("0.######", CultureInfo.InvariantCulture)
: string.Empty;
var lineas = item.ActivarFiltroLineas var lineas = item.ActivarFiltroLineas
? string.Join(",", NormalizarLineasHistorial(item.LineasPermitidas)) ? string.Join(",", NormalizarLineasHistorial(item.LineasPermitidas))
: string.Empty; : string.Empty;
@@ -587,6 +603,7 @@ public partial class PlanificadorRutas
fecha, fecha,
hora, hora,
criterio, criterio,
factorEquilibrado,
item.ActivarFiltroLineas ? "1" : "0", item.ActivarFiltroLineas ? "1" : "0",
lineas); lineas);
} }

View File

@@ -1261,7 +1261,7 @@ public partial class PlanificadorRutas : ComponentBase
private bool _elevPopupMinimizado = false; private bool _elevPopupMinimizado = false;
private const string _versionApp = "(v-20260626a)"; private const string _versionApp = "(v-20260709a)";
private double? _paradaElevG; private double? _paradaElevG;

View File

@@ -35,6 +35,7 @@ public partial class PlanificadorRutas
public double? ParametroPenalizacionDistanciaPieMetros { get; set; } public double? ParametroPenalizacionDistanciaPieMetros { get; set; }
public double? CoeficienteEsfuerzoAltura { get; set; } public double? CoeficienteEsfuerzoAltura { get; set; }
public double? PesoTransbordoRankingMinutos { get; set; } public double? PesoTransbordoRankingMinutos { get; set; }
public double? FactorEquilibradoAndarBus { get; set; }
public double VelocidadPieKmH { get; set; } public double VelocidadPieKmH { get; set; }
public double VelocidadBusKmH { get; set; } public double VelocidadBusKmH { get; set; }
public int ElevMaxPuntosPorTramo { get; set; } public int ElevMaxPuntosPorTramo { get; set; }
@@ -134,6 +135,7 @@ public partial class PlanificadorRutas
ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros, ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros,
CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura, CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura,
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos, PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
VelocidadPieKmH = _velocidadPieKmH, VelocidadPieKmH = _velocidadPieKmH,
VelocidadBusKmH = _velocidadBusKmH, VelocidadBusKmH = _velocidadBusKmH,
ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo, ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo,
@@ -188,6 +190,8 @@ public partial class PlanificadorRutas
: coeficienteAltura; : coeficienteAltura;
if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo)) if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo))
_pesoTransbordoRankingMinutos = Math.Abs(pesoTransbordo - 8.0) < 0.0001 ? 4.0 : Math.Max(0, pesoTransbordo); _pesoTransbordoRankingMinutos = Math.Abs(pesoTransbordo - 8.0) < 0.0001 ? 4.0 : Math.Max(0, pesoTransbordo);
if (config.FactorEquilibradoAndarBus is { } factorEquilibrado && double.IsFinite(factorEquilibrado))
_factorEquilibradoAndarBus = Math.Max(0, factorEquilibrado);
_velocidadPieKmH = Math.Clamp(config.VelocidadPieKmH, 0.5, 25.0); _velocidadPieKmH = Math.Clamp(config.VelocidadPieKmH, 0.5, 25.0);
_velocidadBusKmH = Math.Clamp( _velocidadBusKmH = Math.Clamp(
Math.Abs(config.VelocidadBusKmH - 15.0) < 0.0001 || Math.Abs(config.VelocidadBusKmH - 15.0) < 0.0001 ||
@@ -290,6 +294,7 @@ public partial class PlanificadorRutas
_parametroPenalizacionDistanciaPieMetros = 100.0; _parametroPenalizacionDistanciaPieMetros = 100.0;
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
_pesoTransbordoRankingMinutos = 4.0; _pesoTransbordoRankingMinutos = 4.0;
_factorEquilibradoAndarBus = 12.0;
_velocidadPieKmH = 3.6; _velocidadPieKmH = 3.6;
_velocidadBusKmH = 16.0; _velocidadBusKmH = 16.0;
_minutosTransbordo = 5; _minutosTransbordo = 5;
@@ -336,6 +341,7 @@ public partial class PlanificadorRutas
_parametroPenalizacionDistanciaPieMetros = 100.0; _parametroPenalizacionDistanciaPieMetros = 100.0;
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
_pesoTransbordoRankingMinutos = 4.0; _pesoTransbordoRankingMinutos = 4.0;
_factorEquilibradoAndarBus = 12.0;
_velocidadPieKmH = 3.6; _velocidadPieKmH = 3.6;
_velocidadBusKmH = 16.0; _velocidadBusKmH = 16.0;
_minutosTransbordo = 5; _minutosTransbordo = 5;

View File

@@ -1458,49 +1458,8 @@ public partial class PlanificadorRutas : ComponentBase
if (candidatas.Count == 0) if (candidatas.Count == 0)
return _opcionesRuta.Count > 0 ? 0 : -1; return _opcionesRuta.Count > 0 ? 0 : -1;
var duraciones = candidatas
.Select(x => DuracionOrdenacion(x.opcion))
.Where(double.IsFinite)
.ToList();
var costesAndar = candidatas
.Select(x => CalcularCosteAndarEquilibradoSegundos(x.opcion))
.Where(double.IsFinite)
.ToList();
if (duraciones.Count == 0 || costesAndar.Count == 0)
return candidatas
.OrderBy(x => x.opcion.PuntuacionCoste)
.ThenBy(x => DuracionOrdenacion(x.opcion))
.Select(x => x.indice)
.FirstOrDefault();
var minDuracion = duraciones.Min();
var maxDuracion = duraciones.Max();
var minAndar = costesAndar.Min();
var maxAndar = costesAndar.Max();
return candidatas return candidatas
.Select(x => .OrderBy(x => CalcularScoreEquilibradoFactor(x.opcion))
{
var sacrificioTiempo = CalcularSacrificioNormalizado(
DuracionOrdenacion(x.opcion),
minDuracion,
maxDuracion);
var sacrificioAndar = CalcularSacrificioNormalizado(
CalcularCosteAndarEquilibradoSegundos(x.opcion),
minAndar,
maxAndar);
return new
{
x.indice,
x.opcion,
PeorSacrificio = Math.Max(sacrificioTiempo, sacrificioAndar),
SacrificioMedio = (sacrificioTiempo + sacrificioAndar) / 2.0
};
})
.OrderBy(x => x.PeorSacrificio)
.ThenBy(x => x.SacrificioMedio)
.ThenBy(x => x.opcion.NumeroTransbordosReales) .ThenBy(x => x.opcion.NumeroTransbordosReales)
.ThenBy(x => x.opcion.TiempoEsperaTotalSegundos) .ThenBy(x => x.opcion.TiempoEsperaTotalSegundos)
.ThenBy(x => x.opcion.PuntuacionCoste) .ThenBy(x => x.opcion.PuntuacionCoste)
@@ -1528,6 +1487,14 @@ public partial class PlanificadorRutas : ComponentBase
.ThenBy(TiempoPieOrdenacion) .ThenBy(TiempoPieOrdenacion)
.ThenBy(opcion => opcion.PuntuacionCoste); .ThenBy(opcion => opcion.PuntuacionCoste);
} }
else if (CriterioPrioritarioEquilibradoActivo)
{
rutasBus = rutasBus
.OrderBy(CalcularScoreEquilibradoFactor)
.ThenBy(opcion => opcion.NumeroTransbordosReales)
.ThenBy(opcion => opcion.TiempoEsperaTotalSegundos)
.ThenBy(opcion => opcion.PuntuacionCoste);
}
var soloPie = _opcionesRuta var soloPie = _opcionesRuta
.Where(opcion => opcion.EsSoloAPie) .Where(opcion => opcion.EsSoloAPie)
@@ -2141,6 +2108,7 @@ public partial class PlanificadorRutas : ComponentBase
FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie, FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie,
ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros, ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros,
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos, PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
VelocidadBusKmH = _velocidadBusKmH, VelocidadBusKmH = _velocidadBusKmH,
MinutosPorTransbordo = _minutosTransbordo, MinutosPorTransbordo = _minutosTransbordo,
MargenTransbordoSegundos = Math.Clamp(_margenTransbordoSegundos, 0, 1800), MargenTransbordoSegundos = Math.Clamp(_margenTransbordoSegundos, 0, 1800),

View File

@@ -66,6 +66,148 @@ public partial class PlanificadorRutas : ComponentBase
StateHasChanged(); StateHasChanged();
} }
} }
private async Task ToggleElevacionesRutaAsync(ChangeEventArgs e)
{
_mostrarElevacionesRuta = e.Value switch
{
bool valor => valor,
string texto when bool.TryParse(texto, out var valor) => valor,
_ => !_mostrarElevacionesRuta
};
if (_opcionesRuta.Count == 0 ||
_indiceAlternativaSeleccionada < 0 ||
_indiceAlternativaSeleccionada >= _opcionesRuta.Count)
{
StateHasChanged();
return;
}
_pintandoElevacionesRuta = true;
_mensajeRuta = _mostrarElevacionesRuta
? "Aplicando elevaciones..."
: "Representando ruta seleccionada...";
StateHasChanged();
await Task.Yield();
try
{
await CerrarPopupsAsync();
await PintarOpcionRutaCompleta(_opcionesRuta[_indiceAlternativaSeleccionada]);
}
finally
{
_pintandoElevacionesRuta = false;
StateHasChanged();
}
}
private async Task DrawWalkingRouteJsAsync(
string id,
double[,] linea,
int weight,
int versionPintado,
bool arrowEnd = true)
{
var puntosElevacion = await ConstruirPuntosElevacionMapaAsync(linea, CancellationToken.None);
ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync(
id,
linea,
"#dc2626",
weight,
0.9,
dashArray: DashArrayPiePunteado,
arrowEnd: arrowEnd,
lineCap: "round");
if (puntosElevacion is { Count: > 0 })
{
ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteElevationPointsJsAsync(id, puntosElevacion);
}
}
private async Task<List<object>?> ConstruirPuntosElevacionMapaAsync(double[,] linea, CancellationToken ct)
{
if (!_mostrarElevacionesRuta || linea.GetLength(0) < 2)
return null;
var muestras = await ConstruirMuestrasElevacionAsync(linea, ct);
if (muestras.Count < 2)
return null;
var puntos = new List<object>();
var hayPendienteCalculada = false;
for (var i = 1; i < muestras.Count; i++)
{
ct.ThrowIfCancellationRequested();
var anterior = muestras[i - 1];
var actual = muestras[i];
var distancia = CalcularDistanciaMetros(
anterior.Latitud,
anterior.Longitud,
actual.Latitud,
actual.Longitud);
if (distancia <= 0.5)
continue;
var elevAnterior = ElevacionPreferidaMapa(anterior);
var elevActual = ElevacionPreferidaMapa(actual);
double? pendiente = null;
if (elevAnterior is not null && elevActual is not null)
{
pendiente = (elevActual.Value - elevAnterior.Value) / distancia * 100.0;
hayPendienteCalculada = true;
}
var (categoria, color) = ClasificarPendienteMapa(pendiente);
puntos.Add(new
{
lat = actual.Latitud,
lon = actual.Longitud,
color,
category = categoria,
slope = pendiente,
label = pendiente is null
? "-"
: pendiente.Value.ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture) + " %"
});
}
return hayPendienteCalculada && puntos.Count > 0
? puntos
: null;
}
private static double? ElevacionPreferidaMapa(MuestraElevacion muestra)
=> muestra.ElevacionIdee ?? muestra.Elevacion;
private static (string Categoria, string Color) ClasificarPendienteMapa(double? pendientePorcentaje)
{
if (pendientePorcentaje is null || !double.IsFinite(pendientePorcentaje.Value))
return ("sin-datos", "#94a3b8");
if (pendientePorcentaje.Value >= 8.0)
return ("subida-fuerte", "#ef4444");
if (pendientePorcentaje.Value >= 3.0)
return ("subida", "#f97316");
if (pendientePorcentaje.Value <= -8.0)
return ("bajada-fuerte", "#1d4ed8");
if (pendientePorcentaje.Value <= -3.0)
return ("bajada", "#38bdf8");
return ("llano", "#facc15");
}
@@ -95,6 +237,8 @@ public partial class PlanificadorRutas : ComponentBase
await PintarParadasSolucionSeleccionadaAsync(opcion, versionPintado); await PintarParadasSolucionSeleccionadaAsync(opcion, versionPintado);
} }
ThrowIfPintadoObsoleto(versionPintado);
await ActualizarAlturasResumenRutaAsync(CancellationToken.None);
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
await CerrarPopupsAsync(); await CerrarPopupsAsync();
@@ -207,7 +351,7 @@ public partial class PlanificadorRutas : ComponentBase
}); });
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync("walk_full", linea, "#dc2626", 5, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round"); await DrawWalkingRouteJsAsync("walk_full", linea, 5, versionPintado);
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
_mensajeRuta = CapitalizarPalabrasClave( _mensajeRuta = CapitalizarPalabrasClave(
@@ -307,7 +451,7 @@ public partial class PlanificadorRutas : ComponentBase
}); });
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync("walk_1", w1.line, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round"); await DrawWalkingRouteJsAsync("walk_1", w1.line, 4, versionPintado);
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
} }
@@ -384,7 +528,7 @@ public partial class PlanificadorRutas : ComponentBase
}); });
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync($"walk_union_{i}", lineaUnion, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round"); await DrawWalkingRouteJsAsync($"walk_union_{i}", lineaUnion, 4, versionPintado);
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaUnion); ultimoPuntoDibujado = ObtenerUltimoPunto(lineaUnion);
} }
@@ -435,7 +579,7 @@ public partial class PlanificadorRutas : ComponentBase
}); });
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync($"walk_transfer_real_{i}", lineaTransferencia, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round"); await DrawWalkingRouteJsAsync($"walk_transfer_real_{i}", lineaTransferencia, 4, versionPintado);
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaTransferencia); ultimoPuntoDibujado = ObtenerUltimoPunto(lineaTransferencia);
@@ -478,7 +622,7 @@ public partial class PlanificadorRutas : ComponentBase
}); });
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync($"walk_transfer_{i}", wt.line, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round"); await DrawWalkingRouteJsAsync($"walk_transfer_{i}", wt.line, 4, versionPintado);
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
} }
} }
@@ -688,7 +832,7 @@ public partial class PlanificadorRutas : ComponentBase
}); });
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync("walk_union_final", lineaUnionFinal, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round"); await DrawWalkingRouteJsAsync("walk_union_final", lineaUnionFinal, 4, versionPintado);
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
EstablecerPrimerPunto(w2.line, ObtenerUltimoPunto(lineaUnionFinal)); EstablecerPrimerPunto(w2.line, ObtenerUltimoPunto(lineaUnionFinal));
} }
@@ -715,7 +859,7 @@ public partial class PlanificadorRutas : ComponentBase
}); });
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync("walk_2", w2.line, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round"); await DrawWalkingRouteJsAsync("walk_2", w2.line, 4, versionPintado);
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
} }

View File

@@ -628,7 +628,7 @@
class="route-priority-option @(CriterioPrioritarioEquilibradoActivo ? "route-priority-option-active" : "")" class="route-priority-option @(CriterioPrioritarioEquilibradoActivo ? "route-priority-option-active" : "")"
title="Mantener el criterio equilibrado actual" title="Mantener el criterio equilibrado actual"
@onclick="@(() => CambiarCriterioPrioritarioRutasAsync(CriterioRutaEquilibrado))"> @onclick="@(() => CambiarCriterioPrioritarioRutasAsync(CriterioRutaEquilibrado))">
Equilibrado* Equilib. A
</button> </button>
<button type="button" <button type="button"
class="route-priority-option @(CriterioPrioritarioTiempoActivo ? "route-priority-option-active" : "")" class="route-priority-option @(CriterioPrioritarioTiempoActivo ? "route-priority-option-active" : "")"
@@ -639,6 +639,22 @@
</div> </div>
</div> </div>
<div class="route-visual-options">
<label class="route-elevation-switch @(_mostrarElevacionesRuta ? "route-elevation-switch--active" : "")"
title="Colorear tramos a pie segun pendiente">
<input type="checkbox"
checked="@_mostrarElevacionesRuta"
disabled="@(_pintandoElevacionesRuta || _opcionesRuta.Count == 0)"
@onchange="ToggleElevacionesRutaAsync" />
<span class="route-elevation-switch-track">
<span class="route-elevation-switch-thumb"></span>
</span>
<span class="route-elevation-switch-text">
@(_pintandoElevacionesRuta ? "Elevaciones..." : "Elevaciones")
</span>
</label>
</div>
@if (_opcionesRuta.Count > 0) @if (_opcionesRuta.Count > 0)
{ {
<div class="route-alt-list"> <div class="route-alt-list">
@@ -684,20 +700,9 @@
{ {
<span>@FormatearDistanciaTarjeta(alt.AlternativaBus!.DistanciaTotalBus) en bus</span> <span>@FormatearDistanciaTarjeta(alt.AlternativaBus!.DistanciaTotalBus) en bus</span>
<span> · @alt.NumeroTransbordosReales Transbordos</span> <span> · @alt.NumeroTransbordosReales Transbordos</span>
<span class="route-alt-walk @(CriterioPrioritarioAndarActivo ? "route-alt-walk-emphasis" : "")"> <span class="route-alt-walk @((CriterioPrioritarioAndarActivo || CriterioPrioritarioEquilibradoActivo) ? "route-alt-walk-emphasis" : "")">
· @FormatearDistanciaTarjeta(alt.DistanciaPieTotalAprox) a pie aprox. · @FormatearDistanciaTarjeta(alt.DistanciaPieTotalAprox) a pie aprox.
</span> </span>
@if (CriterioPrioritarioEquilibradoActivo)
{
var balanceEquilibrado = FormatearBalanceEquilibradoAlternativa(alt);
if (!string.IsNullOrWhiteSpace(balanceEquilibrado))
{
<span class="route-alt-balance"
title="Comparacion dentro de esta lista: cercania a la solucion mas rapida frente a la que menos camina">
- Eq. @balanceEquilibrado
</span>
}
}
} }
</div> </div>
@@ -881,8 +886,8 @@
var xyzOrigen = FormatearResumenXYZOrigen(); var xyzOrigen = FormatearResumenXYZOrigen();
var xyzDestino = FormatearResumenXYZDestino(); var xyzDestino = FormatearResumenXYZDestino();
var tiempoCalculo = FormatearTiempoCalculoRuta(); var tiempoCalculo = FormatearTiempoCalculoRuta();
var balanceEquilibradoSeleccionado = CriterioPrioritarioEquilibradoActivo var scoreEquilibradoSeleccionado = CriterioPrioritarioEquilibradoActivo
? FormatearBalanceEquilibradoAlternativa(altSeleccionada) ? FormatearScoreEquilibradoAlternativa(altSeleccionada)
: string.Empty; : string.Empty;
// var costePenalizado = ObtenerCostePenalizadoAlternativa(altSeleccionada); // var costePenalizado = ObtenerCostePenalizadoAlternativa(altSeleccionada);
// var incrementoPenalizacion = Math.Max(0, costePenalizado - Math.Max(0, duracionVisible)); // var incrementoPenalizacion = Math.Max(0, costePenalizado - Math.Max(0, duracionVisible));
@@ -907,9 +912,9 @@
{ {
<div>d.Corregida a pie: @FormatearDistanciaDetalleRuta(dCorregida)</div> <div>d.Corregida a pie: @FormatearDistanciaDetalleRuta(dCorregida)</div>
} }
@if (!string.IsNullOrWhiteSpace(balanceEquilibradoSeleccionado)) @if (!string.IsNullOrWhiteSpace(scoreEquilibradoSeleccionado))
{ {
<div>Balance equilibrado: @balanceEquilibradoSeleccionado</div> <div>Score equilibrado: @scoreEquilibradoSeleccionado</div>
} }
@if (!string.IsNullOrWhiteSpace(xyzOrigen)) @if (!string.IsNullOrWhiteSpace(xyzOrigen))
{ {
@@ -1152,24 +1157,29 @@
} }
</label> </label>
<div class="line-filter__range line-filter__range--segundos-parada"> </div>
<span class="muted">Segundos por parada:&nbsp;</span> </div>
<div class="num-inline">
@if (_usarHorariosTeoricos) <div class="config-row">
{ <label>Bus/paradas:</label>
<input type="number" class="config-input" step="1" min="0" value="0" disabled readonly /> <div class="config-dual-labeled">
<button type="button" class="step-btn" disabled>▲</button> <label class="config-dual-labeled__field">
<button type="button" class="step-btn" disabled>▼</button> <span>Seg. parada</span>
} @if (_usarHorariosTeoricos)
else {
{ <input type="number" class="config-input" step="1" min="0" value="0" disabled readonly />
<input type="number" class="config-input" step="1" min="0" }
@bind="_segundosPorParadaBus" @bind:event="oninput" @bind:after="OnSegundosPorParadaBusChanged" /> else
<button type="button" class="step-btn" @onclick="async () => { _segundosPorParadaBus = _segundosPorParadaBus + 1; await OnSegundosPorParadaBusChanged(); }">▲</button> {
<button type="button" class="step-btn" @onclick="async () => { _segundosPorParadaBus = Math.Max(0, _segundosPorParadaBus - 1); await OnSegundosPorParadaBusChanged(); }">▼</button> <input type="number" class="config-input" step="1" min="0"
} @bind="_segundosPorParadaBus" @bind:event="oninput" @bind:after="OnSegundosPorParadaBusChanged" />
</div> }
</div> </label>
<label class="config-dual-labeled__field">
<span>Radio (m)</span>
<input type="number" class="config-input" min="200" max="3500" step="50"
@bind="_radioCandidatasMetros" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
</label>
</div> </div>
</div> </div>
@@ -1346,7 +1356,7 @@
</div> </div>
</div> </div>
<div class="config-row"> <div class="config-row config-row--radio-legacy">
<label>Coef. / Param.(m) andando:</label> <label>Coef. / Param.(m) andando:</label>
<div class="config-dual-inline config-dual-inline--bare"> <div class="config-dual-inline config-dual-inline--bare">
<input type="number" class="config-input" step="any" <input type="number" class="config-input" step="any"
@@ -1382,6 +1392,23 @@
</div> </div>
</div> </div>
<div class="config-row">
<label>Equilibrado:</label>
<div class="config-dual-labeled config-dual-labeled--equilibrado">
<label class="config-dual-labeled__field">
<span>Factor</span>
<input type="number" class="config-input" step="0.1" min="0"
placeholder="12"
title="1 metro andando pesa como este numero de metros en bus"
@bind="FactorEquilibradoAndarBusTexto" @bind:event="oninput"
@onblur="OnBlurFactorEquilibradoAndarBus" />
</label>
<span class="muted config-equilibrado-help" title="Formula: metros en bus + metros andando x factor">
1 m andando = @FactorEquilibradoAndarBusResumen m bus
</span>
</div>
</div>
<div class="config-row"> <div class="config-row">
<label>Vel. Andando/Bus (km/h):</label> <label>Vel. Andando/Bus (km/h):</label>
<div class="config-dual-inline config-dual-inline--bare"> <div class="config-dual-inline config-dual-inline--bare">
@@ -1410,20 +1437,6 @@
@onblur="OnBlurCoeficienteEsfuerzoAltura" /> @onblur="OnBlurCoeficienteEsfuerzoAltura" />
</div> </div>
</div> </div>
<div class="config-row">
<label>Radio paradas candidatas (m):</label>
<div class="num-inline">
<input type="number" class="config-input" min="200" max="3500" step="50"
@bind="_radioCandidatasMetros" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
<button type="button" class="step-btn"
@onclick="async () => { _radioCandidatasMetros = Math.Min(3500, _radioCandidatasMetros + 50); await PersistirConfiguracionAsync(); }">▲</button>
<button type="button" class="step-btn"
@onclick="async () => { _radioCandidatasMetros = Math.Max(200, _radioCandidatasMetros - 50); await PersistirConfiguracionAsync(); }">▼</button>
</div>
</div>
@* <div class="config-row"> @* <div class="config-row">
<label>Minutos por Transbordo:</label> <label>Minutos por Transbordo:</label>
<div class="num-inline"> <div class="num-inline">

View File

@@ -18,6 +18,7 @@ public sealed class RegistroHistorialRuta
public string? HoraSimulada { get; set; } public string? HoraSimulada { get; set; }
public string? CriterioPrioritarioRuta { get; set; } public string? CriterioPrioritarioRuta { get; set; }
public string? PreferenciaEtiquetaRuta { get; set; } public string? PreferenciaEtiquetaRuta { get; set; }
public double? FactorEquilibradoAndarBus { get; set; }
public bool ActivarFiltroLineas { get; set; } public bool ActivarFiltroLineas { get; set; }
public List<int> LineasPermitidas { get; set; } = new(); public List<int> LineasPermitidas { get; set; } = new();
public DateTime FechaCreacionUtc { get; set; } = DateTime.UtcNow; public DateTime FechaCreacionUtc { get; set; } = DateTime.UtcNow;

View File

@@ -26,6 +26,7 @@ public sealed class ParametrosPlanificadorRuta
public double FactorPenalizacionDistanciaPie { get; set; } = 1.6; public double FactorPenalizacionDistanciaPie { get; set; } = 1.6;
public double ParametroPenalizacionDistanciaPieMetros { get; set; } = 100.0; public double ParametroPenalizacionDistanciaPieMetros { get; set; } = 100.0;
public double PesoTransbordoRankingMinutos { get; set; } = 4.0; public double PesoTransbordoRankingMinutos { get; set; } = 4.0;
public double FactorEquilibradoAndarBus { get; set; } = 12.0;
// selección candidatas // selección candidatas
public int RadioParadasCandidatasMetros { get; set; } = 1200; public int RadioParadasCandidatasMetros { get; set; } = 1200;

View File

@@ -86,6 +86,7 @@ public sealed class ServicioHistorialRutasCompartido : IHistorialRutasCompartido
existente.HoraSimulada = registro.HoraSimulada; existente.HoraSimulada = registro.HoraSimulada;
existente.CriterioPrioritarioRuta = registro.CriterioPrioritarioRuta; existente.CriterioPrioritarioRuta = registro.CriterioPrioritarioRuta;
existente.PreferenciaEtiquetaRuta = registro.PreferenciaEtiquetaRuta; existente.PreferenciaEtiquetaRuta = registro.PreferenciaEtiquetaRuta;
existente.FactorEquilibradoAndarBus = registro.FactorEquilibradoAndarBus;
existente.ActivarFiltroLineas = registro.ActivarFiltroLineas; existente.ActivarFiltroLineas = registro.ActivarFiltroLineas;
existente.LineasPermitidas = NormalizarLineas(registro.LineasPermitidas).ToList(); existente.LineasPermitidas = NormalizarLineas(registro.LineasPermitidas).ToList();
@@ -296,6 +297,7 @@ public sealed class ServicioHistorialRutasCompartido : IHistorialRutasCompartido
HoraSimulada = item.HoraSimulada, HoraSimulada = item.HoraSimulada,
CriterioPrioritarioRuta = item.CriterioPrioritarioRuta, CriterioPrioritarioRuta = item.CriterioPrioritarioRuta,
PreferenciaEtiquetaRuta = item.PreferenciaEtiquetaRuta, PreferenciaEtiquetaRuta = item.PreferenciaEtiquetaRuta,
FactorEquilibradoAndarBus = item.FactorEquilibradoAndarBus,
ActivarFiltroLineas = item.ActivarFiltroLineas, ActivarFiltroLineas = item.ActivarFiltroLineas,
LineasPermitidas = NormalizarLineas(item.LineasPermitidas).ToList(), LineasPermitidas = NormalizarLineas(item.LineasPermitidas).ToList(),
FechaCreacionUtc = item.FechaCreacionUtc, FechaCreacionUtc = item.FechaCreacionUtc,
@@ -313,6 +315,9 @@ public sealed class ServicioHistorialRutasCompartido : IHistorialRutasCompartido
var criterio = NormalizarTextoClave(item.CriterioPrioritarioRuta); var criterio = NormalizarTextoClave(item.CriterioPrioritarioRuta);
if (string.IsNullOrWhiteSpace(criterio)) if (string.IsNullOrWhiteSpace(criterio))
criterio = NormalizarTextoClave(item.PreferenciaEtiquetaRuta); criterio = NormalizarTextoClave(item.PreferenciaEtiquetaRuta);
var factorEquilibrado = item.FactorEquilibradoAndarBus is { } factor && double.IsFinite(factor)
? Math.Max(0, factor).ToString("0.######", CultureInfo.InvariantCulture)
: string.Empty;
var lineas = item.ActivarFiltroLineas var lineas = item.ActivarFiltroLineas
? string.Join(",", NormalizarLineas(item.LineasPermitidas)) ? string.Join(",", NormalizarLineas(item.LineasPermitidas))
: string.Empty; : string.Empty;
@@ -327,6 +332,7 @@ public sealed class ServicioHistorialRutasCompartido : IHistorialRutasCompartido
fecha, fecha,
hora, hora,
criterio, criterio,
factorEquilibrado,
item.ActivarFiltroLineas ? "1" : "0", item.ActivarFiltroLineas ? "1" : "0",
lineas); lineas);
} }

View File

@@ -704,11 +704,13 @@ public sealed class BuscadorRutasTemporalesDbus
if (!origenPorCodigo.TryGetValue(primeraParada, out var origen)) if (!origenPorCodigo.TryGetValue(primeraParada, out var origen))
continue; continue;
var distanciaTotalBus = CalcularDistanciaBusEstado(estadoDestino);
var ruta = new RutaBusAlternativa var ruta = new RutaBusAlternativa
{ {
Tramos = tramosBus, Tramos = tramosBus,
NumeroTransbordos = numeroTransbordos, NumeroTransbordos = numeroTransbordos,
CodigosParadasTransbordo = ConstruirCodigosTransbordo(tramosBus) CodigosParadasTransbordo = ConstruirCodigosTransbordo(tramosBus),
DistanciaTotalBus = distanciaTotalBus
}; };
var duracionEstimada = Math.Max(0, (estadoDestino.Llegada - instanteSalida).TotalSeconds) + destino.TiempoAPie; var duracionEstimada = Math.Max(0, (estadoDestino.Llegada - instanteSalida).TotalSeconds) + destino.TiempoAPie;
@@ -748,7 +750,8 @@ public sealed class BuscadorRutasTemporalesDbus
return SeleccionarPoolBusqueda( return SeleccionarPoolBusqueda(
mejores.Values, mejores.Values,
Math.Max(12, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 8)); Math.Max(12, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 8),
parametros);
} }
/// <summary> /// <summary>
@@ -756,7 +759,8 @@ public sealed class BuscadorRutasTemporalesDbus
/// </summary> /// </summary>
private static List<ResultadoBusquedaRutaBusDbus> SeleccionarPoolBusqueda( private static List<ResultadoBusquedaRutaBusDbus> SeleccionarPoolBusqueda(
IEnumerable<ResultadoBusquedaRutaBusDbus> candidatas, IEnumerable<ResultadoBusquedaRutaBusDbus> candidatas,
int maxPorCoste) int maxPorCoste,
ParametrosPlanificadorRuta parametros)
{ {
var lista = candidatas.ToList(); var lista = candidatas.ToList();
if (lista.Count == 0) if (lista.Count == 0)
@@ -766,6 +770,7 @@ public sealed class BuscadorRutasTemporalesDbus
var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var limiteCoste = Math.Max(1, maxPorCoste); var limiteCoste = Math.Max(1, maxPorCoste);
var limiteAndar = Math.Max(8, limiteCoste / 2); var limiteAndar = Math.Max(8, limiteCoste / 2);
var limiteEquilibrado = Math.Max(8, limiteCoste / 2);
Agregar(lista Agregar(lista
.OrderBy(x => x.PuntuacionCoste) .OrderBy(x => x.PuntuacionCoste)
@@ -781,6 +786,13 @@ public sealed class BuscadorRutasTemporalesDbus
.ThenBy(x => x.RutaBus.NumeroTransbordos) .ThenBy(x => x.RutaBus.NumeroTransbordos)
.Take(limiteAndar)); .Take(limiteAndar));
Agregar(lista
.OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
.ThenBy(x => x.DuracionEstimadaSegundos)
.ThenBy(x => x.RutaBus.NumeroTransbordos)
.ThenBy(x => x.PuntuacionCoste)
.Take(limiteEquilibrado));
return seleccionadas return seleccionadas
.OrderBy(x => x.PuntuacionCoste) .OrderBy(x => x.PuntuacionCoste)
.ThenBy(x => x.DuracionEstimadaSegundos) .ThenBy(x => x.DuracionEstimadaSegundos)
@@ -799,6 +811,55 @@ public sealed class BuscadorRutasTemporalesDbus
} }
} }
private static double CalcularScoreBusquedaEquilibrado(
ResultadoBusquedaRutaBusDbus resultado,
ParametrosPlanificadorRuta parametros)
{
var distanciaBus = Math.Max(0, resultado.RutaBus.DistanciaTotalBus);
var distanciaAndando = Math.Max(0, resultado.DistanciaPieTotal);
var factor = double.IsFinite(parametros.FactorEquilibradoAndarBus)
? Math.Max(0, parametros.FactorEquilibradoAndarBus)
: 12.0;
var score = distanciaBus + (distanciaAndando * factor);
return double.IsFinite(score) && score >= 0
? score
: double.PositiveInfinity;
}
private static double CalcularDistanciaBusEstado(EstadoBusquedaParadaDbus estadoDestino)
{
var distancia = 0.0;
var cursor = estadoDestino;
while (cursor is not null)
{
if (cursor.Movimiento is MovimientoBusDbus movimiento)
distancia += CalcularDistanciaBusPatron(movimiento.Patron, movimiento.IndiceSubida, movimiento.IndiceBajada);
cursor = cursor.EstadoAnterior;
}
return double.IsFinite(distancia) && distancia > 0
? distancia
: 0;
}
private static double CalcularDistanciaBusPatron(PatronTransitoDbus patron, int indiceInicio, int indiceFin)
{
if (indiceInicio < 0 ||
indiceFin <= indiceInicio ||
indiceFin >= patron.DistanciaAcumuladaPorParadaMetros.Count)
{
return 0;
}
var distancia = patron.DistanciaAcumuladaPorParadaMetros[indiceFin] -
patron.DistanciaAcumuladaPorParadaMetros[indiceInicio];
return double.IsFinite(distancia) && distancia > 0
? distancia
: 0;
}
/// <summary> /// <summary>
/// Reconstruye los tramos de bus de una ruta. /// Reconstruye los tramos de bus de una ruta.
/// </summary> /// </summary>

View File

@@ -30,6 +30,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
new(StringComparer.OrdinalIgnoreCase); new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, (List<double[,]> lineas, double distancia)> _cacheLineasBusPlanificacion = private static readonly ConcurrentDictionary<string, (List<double[,]> lineas, double distancia)> _cacheLineasBusPlanificacion =
new(StringComparer.OrdinalIgnoreCase); new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, Parada> _cacheParadasEstimacion =
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 ToleranciaEmpateRapidezSegundos = 5.0; private const double ToleranciaEmpateRapidezSegundos = 5.0;
@@ -46,6 +48,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
private const double UmbralAccesoDestinoTransbordoRobustoMetros = 350.0; private const double UmbralAccesoDestinoTransbordoRobustoMetros = 350.0;
private const double ToleranciaDominanciaFinalSegundos = 45.0; private const double ToleranciaDominanciaFinalSegundos = 45.0;
private const double ToleranciaDominanciaFinalMetros = 40.0; private const double ToleranciaDominanciaFinalMetros = 40.0;
private const double ToleranciaDominanciaFinalBusMetros = 120.0;
private const double MejoraClaraDominanciaFinalSegundos = 90.0; private const double MejoraClaraDominanciaFinalSegundos = 90.0;
private const double MejoraClaraDominanciaFinalMetros = 120.0; private const double MejoraClaraDominanciaFinalMetros = 120.0;
private const double AhorroClaroAndandoFinalMetros = 250.0; private const double AhorroClaroAndandoFinalMetros = 250.0;
@@ -79,6 +82,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
_cacheDuracionBusRefinamiento.Clear(); _cacheDuracionBusRefinamiento.Clear();
_cacheSegmentosHorarios.Clear(); _cacheSegmentosHorarios.Clear();
_cacheLineasBusPlanificacion.Clear(); _cacheLineasBusPlanificacion.Clear();
_cacheParadasEstimacion.Clear();
} }
/// <summary> /// <summary>
@@ -208,7 +212,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
candidatasBus = SeleccionarPoolBusqueda( candidatasBus = SeleccionarPoolBusqueda(
porFirma.Values, porFirma.Values,
Math.Max(16, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 10)); Math.Max(16, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 10),
parametros);
} }
var extensionesCircularesInicio = BuscarExtensionesCircularesInicialesConHorario( var extensionesCircularesInicio = BuscarExtensionesCircularesInicialesConHorario(
@@ -236,7 +241,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
candidatasBus = SeleccionarPoolBusqueda( candidatasBus = SeleccionarPoolBusqueda(
porFirma.Values, porFirma.Values,
Math.Max(16, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 10)); Math.Max(16, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 10),
parametros);
} }
} }
@@ -287,7 +293,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
.Select(grupo => grupo.OrderBy(x => x.PuntuacionCoste).First()) .Select(grupo => grupo.OrderBy(x => x.PuntuacionCoste).First())
.ToList(); .ToList();
alternativasBus = SeleccionarPoolFinal(DeduplicarAlternativasEquivalentes(alternativasBus)); alternativasBus = SeleccionarPoolFinal(DeduplicarAlternativasEquivalentes(alternativasBus), parametros);
var finales = SeleccionarAlternativasFinales(alternativasBus, alternativasSoloPie, parametros); var finales = SeleccionarAlternativasFinales(alternativasBus, alternativasSoloPie, parametros);
if (finales.Count == 0) if (finales.Count == 0)
@@ -444,14 +450,14 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
if (mejores.Count > maxResultados * 3) if (mejores.Count > maxResultados * 3)
{ {
mejores = SeleccionarPoolBusqueda(mejores.Values, maxResultados * 2) mejores = SeleccionarPoolBusqueda(mejores.Values, maxResultados * 2, parametros)
.ToDictionary(x => UtilidadesTransitoDbus.FirmaRutaBus(x.RutaBus), StringComparer.OrdinalIgnoreCase); .ToDictionary(x => UtilidadesTransitoDbus.FirmaRutaBus(x.RutaBus), StringComparer.OrdinalIgnoreCase);
} }
} }
} }
} }
return SeleccionarPoolBusqueda(mejores.Values, maxResultados); return SeleccionarPoolBusqueda(mejores.Values, maxResultados, parametros);
} }
/// <summary> /// <summary>
@@ -612,7 +618,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
} }
} }
return SeleccionarPoolBusqueda(mejores.Values, maxResultados); return SeleccionarPoolBusqueda(mejores.Values, maxResultados, parametros);
} }
/// <summary> /// <summary>
@@ -681,7 +687,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
} }
} }
return SeleccionarPoolBusqueda(mejores.Values, maxResultados); return SeleccionarPoolBusqueda(mejores.Values, maxResultados, parametros);
void ExplorarPrimeraPiernaDirecta( void ExplorarPrimeraPiernaDirecta(
ParadaCandidata origen, ParadaCandidata origen,
@@ -1169,7 +1175,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
void PodarMejores() void PodarMejores()
{ {
mejores = SeleccionarPoolBusqueda(mejores.Values, maxResultados * 2) mejores = SeleccionarPoolBusqueda(mejores.Values, maxResultados * 2, parametros)
.ToDictionary( .ToDictionary(
x => UtilidadesTransitoDbus.FirmaRutaBus(x.RutaBus), x => UtilidadesTransitoDbus.FirmaRutaBus(x.RutaBus),
StringComparer.OrdinalIgnoreCase); StringComparer.OrdinalIgnoreCase);
@@ -1194,7 +1200,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
var maxResultados = Math.Max(24, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 12); var maxResultados = Math.Max(24, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 12);
var basesExtension = SeleccionarPoolBusqueda( var basesExtension = SeleccionarPoolBusqueda(
candidatasBase, candidatasBase,
Math.Max(12, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 4)); Math.Max(12, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 4),
parametros);
foreach (var baseRuta in basesExtension) foreach (var baseRuta in basesExtension)
{ {
@@ -1331,7 +1338,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
} }
} }
return SeleccionarPoolBusqueda(mejores.Values, maxResultados); return SeleccionarPoolBusqueda(mejores.Values, maxResultados, parametros);
} }
private static PatronTransitoDbus? ResolverPatronDesdeTramo(IndiceTransitoDbus indiceTransito, TramoBus tramo) private static PatronTransitoDbus? ResolverPatronDesdeTramo(IndiceTransitoDbus indiceTransito, TramoBus tramo)
@@ -1397,9 +1404,10 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
/// <summary> /// <summary>
/// Conserva candidatas buenas por coste y tambien un cupo por menor caminata. /// Conserva candidatas buenas por coste y tambien un cupo por menor caminata.
/// </summary> /// </summary>
private static List<ResultadoBusquedaRutaBusDbus> SeleccionarPoolBusqueda( private List<ResultadoBusquedaRutaBusDbus> SeleccionarPoolBusqueda(
IEnumerable<ResultadoBusquedaRutaBusDbus> candidatas, IEnumerable<ResultadoBusquedaRutaBusDbus> candidatas,
int maxPorCoste) int maxPorCoste,
ParametrosPlanificadorRuta? parametros = null)
{ {
var lista = candidatas.ToList(); var lista = candidatas.ToList();
if (lista.Count == 0) if (lista.Count == 0)
@@ -1409,6 +1417,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var limiteCoste = Math.Max(1, maxPorCoste); var limiteCoste = Math.Max(1, maxPorCoste);
var limiteAndar = Math.Max(8, limiteCoste / 2); var limiteAndar = Math.Max(8, limiteCoste / 2);
var limiteEquilibrado = Math.Max(8, limiteCoste / 2);
var duracionMinima = lista var duracionMinima = lista
.Select(x => x.DuracionEstimadaSegundos) .Select(x => x.DuracionEstimadaSegundos)
.Where(double.IsFinite) .Where(double.IsFinite)
@@ -1432,6 +1441,13 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
.ThenBy(x => x.RutaBus.NumeroTransbordos) .ThenBy(x => x.RutaBus.NumeroTransbordos)
.Take(limiteAndar)); .Take(limiteAndar));
Agregar(lista
.OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
.ThenBy(x => x.DuracionEstimadaSegundos)
.ThenBy(x => x.RutaBus.NumeroTransbordos)
.ThenBy(x => x.PuntuacionCoste)
.Take(limiteEquilibrado));
Agregar(lista Agregar(lista
.Where(x => x.DuracionEstimadaSegundos <= limiteDuracionAndarRazonable) .Where(x => x.DuracionEstimadaSegundos <= limiteDuracionAndarRazonable)
.OrderBy(x => x.DistanciaPieTotal) .OrderBy(x => x.DistanciaPieTotal)
@@ -1465,6 +1481,25 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
} }
} }
private double CalcularScoreBusquedaEquilibrado(
ResultadoBusquedaRutaBusDbus resultado,
ParametrosPlanificadorRuta? parametros)
{
var distanciaBus = Math.Max(0, resultado.RutaBus.DistanciaTotalBus);
if (distanciaBus <= 0 && resultado.RutaBus.Tramos.Count > 0)
distanciaBus = CalcularDistanciaBusCandidata(resultado.RutaBus.Tramos);
var distanciaAndando = Math.Max(0, resultado.DistanciaPieTotal);
var factor = parametros is not null && double.IsFinite(parametros.FactorEquilibradoAndarBus)
? Math.Max(0, parametros.FactorEquilibradoAndarBus)
: 12.0;
var score = distanciaBus + (distanciaAndando * factor);
return double.IsFinite(score) && score >= 0
? score
: double.PositiveInfinity;
}
private static bool DebeBuscarTransbordosRobustos( private static bool DebeBuscarTransbordosRobustos(
IReadOnlyList<ResultadoBusquedaRutaBusDbus> candidatasBus, IReadOnlyList<ResultadoBusquedaRutaBusDbus> candidatasBus,
IReadOnlyList<ParadaCandidata> candidatasOrigen, IReadOnlyList<ParadaCandidata> candidatasOrigen,
@@ -1700,11 +1735,19 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
.ToList(); .ToList();
} }
private static List<AlternativaRuta> SeleccionarPoolFinal(IReadOnlyList<AlternativaRuta> alternativas) private static List<AlternativaRuta> SeleccionarPoolFinal(
IReadOnlyList<AlternativaRuta> alternativas,
ParametrosPlanificadorRuta parametros)
{ {
var seleccionadas = new List<AlternativaRuta>(); var seleccionadas = new List<AlternativaRuta>();
var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Agregar(alternativas
.OrderBy(x => CalcularScoreEquilibrado(x, parametros))
.ThenBy(x => x.PuntuacionCoste)
.ThenBy(x => x.DuracionTotalAproxSegundos)
.Take(16));
Agregar(alternativas Agregar(alternativas
.OrderBy(x => x.PuntuacionCoste) .OrderBy(x => x.PuntuacionCoste)
.ThenBy(x => x.DuracionTotalAproxSegundos) .ThenBy(x => x.DuracionTotalAproxSegundos)
@@ -2683,6 +2726,59 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
return lineas; return lineas;
} }
private double CalcularDistanciaBusCandidata(IReadOnlyList<TramoBus> tramos)
{
if (tramos.Count == 0)
return 0;
var distanciaTotal = 0.0;
foreach (var tramo in tramos)
{
if (!tramo.EsValido)
continue;
var codigos = tramo.Variante.CodigosParadas;
var indiceInicio = Math.Clamp(tramo.IndiceInicio, 0, codigos.Count - 1);
var indiceFin = Math.Clamp(tramo.IndiceFin, 0, codigos.Count - 1);
for (var indice = indiceInicio + 1; indice <= indiceFin; indice++)
{
if (!TryResolverParadaEstimacion(codigos[indice - 1], out var origen) ||
!TryResolverParadaEstimacion(codigos[indice], out var destino))
{
continue;
}
distanciaTotal += UtilidadesTransitoDbus.CalcularDistanciaMetros(
origen.Latitud,
origen.Longitud,
destino.Latitud,
destino.Longitud);
}
}
return double.IsFinite(distanciaTotal) && distanciaTotal > 0
? distanciaTotal
: 0;
}
private bool TryResolverParadaEstimacion(string codigoParada, out Parada parada)
{
if (_cacheParadasEstimacion.TryGetValue(codigoParada, out parada!))
return true;
var encontrada = _redBusServicio.Paradas
.FirstOrDefault(p => p.Codigo.Equals(codigoParada, StringComparison.OrdinalIgnoreCase));
if (encontrada is null)
{
parada = default!;
return false;
}
_cacheParadasEstimacion[codigoParada] = encontrada;
parada = encontrada;
return true;
}
private static string CrearClaveLineasBus(IReadOnlyList<TramoBus> tramos) private static string CrearClaveLineasBus(IReadOnlyList<TramoBus> tramos)
{ {
return string.Join(";", tramos.Select(tramo => return string.Join(";", tramos.Select(tramo =>
@@ -2761,7 +2857,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
var priorizarTiempo = string.Equals(preferenciaEtiquetaRuta, "M\u00e1s r\u00e1pida", StringComparison.OrdinalIgnoreCase); var priorizarTiempo = string.Equals(preferenciaEtiquetaRuta, "M\u00e1s r\u00e1pida", StringComparison.OrdinalIgnoreCase);
var modoMenosAndar = string.Equals(preferenciaEtiquetaRuta, "Menos andar", StringComparison.OrdinalIgnoreCase); var modoMenosAndar = string.Equals(preferenciaEtiquetaRuta, "Menos andar", StringComparison.OrdinalIgnoreCase);
var alternativasBusRazonables = FiltrarAlternativasRazonablesFinales(alternativasBus, masRapidaBus); var alternativasBusRazonables = FiltrarAlternativasRazonablesFinales(alternativasBus, masRapidaBus);
var alternativasBusParaAndar = modoMenosAndar var modoEquilibrado = string.IsNullOrWhiteSpace(preferenciaEtiquetaRuta);
var alternativasBusParaAndar = modoMenosAndar || modoEquilibrado
? AmpliarAlternativasPorAhorroAndando(alternativasBus, alternativasBusRazonables) ? AmpliarAlternativasPorAhorroAndando(alternativasBus, alternativasBusRazonables)
: alternativasBusRazonables; : alternativasBusRazonables;
var menosTransbordos = alternativasBusRazonables var menosTransbordos = alternativasBusRazonables
@@ -2775,9 +2872,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
modoMenosAndar ? null : masRapidaBus, modoMenosAndar ? null : masRapidaBus,
parametros); parametros);
var priorizarAndar = string.Equals(preferenciaEtiquetaRuta, "Menos andar", StringComparison.OrdinalIgnoreCase); var priorizarAndar = string.Equals(preferenciaEtiquetaRuta, "Menos andar", StringComparison.OrdinalIgnoreCase);
var modoEquilibrado = string.IsNullOrWhiteSpace(preferenciaEtiquetaRuta);
var equilibrada = modoEquilibrado var equilibrada = modoEquilibrado
? ElegirEquilibradaRazonable(alternativasBusRazonables, parametros) ? ElegirEquilibradaRazonable(alternativasBusParaAndar, parametros)
: null; : null;
if (priorizarAndar) if (priorizarAndar)
@@ -2788,8 +2884,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
} }
else if (modoEquilibrado) else if (modoEquilibrado)
{ {
AddAlternativaBus(finales, usadas, masRapidaBus, "M\u00e1s r\u00e1pida");
AddAlternativaBus(finales, usadas, equilibrada, "Equilibrada"); AddAlternativaBus(finales, usadas, equilibrada, "Equilibrada");
AddAlternativaBus(finales, usadas, masRapidaBus, "M\u00e1s r\u00e1pida");
AddAlternativaBus(finales, usadas, menosAndar, "Menos andar"); AddAlternativaBus(finales, usadas, menosAndar, "Menos andar");
AddAlternativaBus(finales, usadas, menosTransbordos, "Menos transbordos"); AddAlternativaBus(finales, usadas, menosTransbordos, "Menos transbordos");
} }
@@ -2804,7 +2900,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
? alternativasBusParaAndar.OrderBy(x => CalcularCosteAndarEquilibradoSegundos(x, parametros)).ThenBy(x => x.PuntuacionCoste) ? alternativasBusParaAndar.OrderBy(x => CalcularCosteAndarEquilibradoSegundos(x, parametros)).ThenBy(x => x.PuntuacionCoste)
: priorizarTiempo : priorizarTiempo
? alternativasBusRazonables.OrderBy(x => DuracionVisibleOrdenacion(x)).ThenBy(x => x.DistanciaPieTotalAprox).ThenBy(x => x.PuntuacionCoste) ? alternativasBusRazonables.OrderBy(x => DuracionVisibleOrdenacion(x)).ThenBy(x => x.DistanciaPieTotalAprox).ThenBy(x => x.PuntuacionCoste)
: alternativasBusRazonables.OrderBy(x => CalcularScoreEquilibrado(x, alternativasBusRazonables, parametros)) : alternativasBusParaAndar.OrderBy(x => CalcularScoreEquilibrado(x, parametros))
.ThenBy(x => x.PuntuacionCoste); .ThenBy(x => x.PuntuacionCoste);
foreach (var alternativa in candidatasRelleno) foreach (var alternativa in candidatasRelleno)
@@ -2841,8 +2937,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
return null; return null;
return alternativasBus return alternativasBus
.OrderBy(x => CalcularScoreEquilibrado(x, alternativasBus, parametros)) .OrderBy(x => CalcularScoreEquilibrado(x, parametros))
.ThenBy(x => CalcularSacrificioMedioEquilibrado(x, alternativasBus, parametros))
.ThenBy(x => x.NumeroTransbordosReales) .ThenBy(x => x.NumeroTransbordosReales)
.ThenBy(x => x.TiempoEsperaTotalSegundos) .ThenBy(x => x.TiempoEsperaTotalSegundos)
.ThenBy(x => x.PuntuacionCoste) .ThenBy(x => x.PuntuacionCoste)
@@ -2851,69 +2946,28 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
private static double CalcularScoreEquilibrado( private static double CalcularScoreEquilibrado(
AlternativaRuta alternativa, AlternativaRuta alternativa,
IReadOnlyList<AlternativaRuta> alternativasBus,
ParametrosPlanificadorRuta parametros) ParametrosPlanificadorRuta parametros)
{ {
var (sacrificioTiempo, sacrificioAndar) = CalcularSacrificiosEquilibrado( if (alternativa.AlternativaBus is null)
alternativa, return double.PositiveInfinity;
alternativasBus,
parametros);
return Math.Max(sacrificioTiempo, sacrificioAndar); var distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus);
} var distanciaAndando = Math.Max(0, alternativa.DistanciaPieTotalAprox);
var factor = double.IsFinite(parametros.FactorEquilibradoAndarBus)
? Math.Max(0, parametros.FactorEquilibradoAndarBus)
: 12.0;
private static double CalcularSacrificioMedioEquilibrado( var score = distanciaBus + (distanciaAndando * factor);
AlternativaRuta alternativa, return double.IsFinite(score) && score >= 0
IReadOnlyList<AlternativaRuta> alternativasBus, ? score
ParametrosPlanificadorRuta parametros) : double.PositiveInfinity;
{
var (sacrificioTiempo, sacrificioAndar) = CalcularSacrificiosEquilibrado(
alternativa,
alternativasBus,
parametros);
return (sacrificioTiempo + sacrificioAndar) / 2.0;
}
private static (double Tiempo, double Andar) CalcularSacrificiosEquilibrado(
AlternativaRuta alternativa,
IReadOnlyList<AlternativaRuta> alternativasBus,
ParametrosPlanificadorRuta parametros)
{
var duraciones = alternativasBus
.Select(DuracionVisibleOrdenacion)
.Where(double.IsFinite)
.ToList();
var costesAndar = alternativasBus
.Select(x => CalcularCosteAndarEquilibradoSegundos(x, parametros))
.Where(double.IsFinite)
.ToList();
if (duraciones.Count == 0 || costesAndar.Count == 0)
return (0, 0);
var sacrificioTiempo = CalcularSacrificioNormalizado(
DuracionVisibleOrdenacion(alternativa),
duraciones.Min(),
duraciones.Max());
var sacrificioAndar = CalcularSacrificioNormalizado(
CalcularCosteAndarEquilibradoSegundos(alternativa, parametros),
costesAndar.Min(),
costesAndar.Max());
return (sacrificioTiempo, sacrificioAndar);
} }
private static double CalcularCosteAndarEquilibradoSegundos( private static double CalcularCosteAndarEquilibradoSegundos(
AlternativaRuta alternativa, AlternativaRuta alternativa,
ParametrosPlanificadorRuta parametros) ParametrosPlanificadorRuta parametros)
{ {
var distanciaCorregida = var distanciaCorregida = CalcularDistanciaAndandoCorregidaMetros(alternativa, parametros);
UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieInicioAprox, parametros) +
UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieTransbordosAprox, parametros) +
UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieFinAprox, parametros);
if (!double.IsFinite(distanciaCorregida) || distanciaCorregida < 0) if (!double.IsFinite(distanciaCorregida) || distanciaCorregida < 0)
return double.PositiveInfinity; return double.PositiveInfinity;
@@ -2921,18 +2975,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
return UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaCorregida, parametros); return UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaCorregida, parametros);
} }
private static double CalcularSacrificioNormalizado(double valor, double mejor, double peor) private static double CalcularDistanciaAndandoCorregidaMetros(
{ AlternativaRuta alternativa,
if (!double.IsFinite(valor) || ParametrosPlanificadorRuta parametros)
!double.IsFinite(mejor) || => UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieInicioAprox, parametros)
!double.IsFinite(peor) || + UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieTransbordosAprox, parametros)
peor <= mejor) + UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieFinAprox, parametros);
{
return 0;
}
return Math.Clamp((valor - mejor) / (peor - mejor), 0.0, 1.0);
}
private static double DuracionVisibleOrdenacion(AlternativaRuta alternativa) private static double DuracionVisibleOrdenacion(AlternativaRuta alternativa)
=> double.IsFinite(alternativa.DuracionTotalAproxSegundos) => double.IsFinite(alternativa.DuracionTotalAproxSegundos)
@@ -3049,6 +3097,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
{ {
var duracionCandidata = DuracionVisibleOrdenacion(candidata); var duracionCandidata = DuracionVisibleOrdenacion(candidata);
var pieCandidata = candidata.DistanciaPieTotalAprox; var pieCandidata = candidata.DistanciaPieTotalAprox;
var busCandidata = candidata.AlternativaBus?.DistanciaTotalBus ?? double.PositiveInfinity;
foreach (var otra in alternativas) foreach (var otra in alternativas)
{ {
@@ -3060,9 +3109,11 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
var duracionOtra = DuracionVisibleOrdenacion(otra); var duracionOtra = DuracionVisibleOrdenacion(otra);
var pieOtra = otra.DistanciaPieTotalAprox; var pieOtra = otra.DistanciaPieTotalAprox;
var busOtra = otra.AlternativaBus?.DistanciaTotalBus ?? double.PositiveInfinity;
var noPierdeTiempo = duracionOtra <= duracionCandidata + ToleranciaDominanciaFinalSegundos; var noPierdeTiempo = duracionOtra <= duracionCandidata + ToleranciaDominanciaFinalSegundos;
var noPierdeAndando = pieOtra <= pieCandidata + ToleranciaDominanciaFinalMetros; var noPierdeAndando = pieOtra <= pieCandidata + ToleranciaDominanciaFinalMetros;
if (!noPierdeTiempo || !noPierdeAndando) var noPierdeBus = busOtra <= busCandidata + ToleranciaDominanciaFinalBusMetros;
if (!noPierdeTiempo || !noPierdeAndando || !noPierdeBus)
continue; continue;
var mejoraTransbordo = otra.NumeroTransbordosReales < candidata.NumeroTransbordosReales; var mejoraTransbordo = otra.NumeroTransbordosReales < candidata.NumeroTransbordosReales;

View File

@@ -807,6 +807,74 @@ html, body {
background: #2563eb; background: #2563eb;
} }
.route-visual-options {
display: flex;
align-items: center;
justify-content: flex-end;
margin: -.15rem 0 .4rem;
}
.route-elevation-switch {
display: inline-flex;
align-items: center;
gap: .38rem;
min-height: 24px;
color: #cbd5e1;
font-size: .7rem;
font-weight: 700;
cursor: pointer;
pointer-events: auto;
user-select: none;
}
.route-elevation-switch input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.route-elevation-switch-track {
position: relative;
width: 34px;
height: 18px;
flex: 0 0 34px;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, .5);
background: rgba(2, 6, 23, .7);
transition: background .15s ease, border-color .15s ease;
}
.route-elevation-switch-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 12px;
height: 12px;
border-radius: 999px;
background: #94a3b8;
box-shadow: 0 1px 4px rgba(0, 0, 0, .35);
transition: transform .15s ease, background .15s ease;
}
.route-elevation-switch--active {
color: #f8fafc;
}
.route-elevation-switch--active .route-elevation-switch-track {
border-color: rgba(251, 191, 36, .85);
background: linear-gradient(90deg, rgba(56, 189, 248, .75), rgba(249, 115, 22, .85));
}
.route-elevation-switch--active .route-elevation-switch-thumb {
transform: translateX(16px);
background: #f8fafc;
}
.route-elevation-switch:has(input:disabled) {
cursor: default;
opacity: .55;
}
/* Lista de alternativas de ruta */ /* Lista de alternativas de ruta */
.route-alt-list { .route-alt-list {
@@ -872,12 +940,6 @@ html, body {
font-weight: 800; font-weight: 800;
} }
.route-alt-balance {
color: #bfdbfe;
white-space: nowrap;
font-weight: 700;
}
/* Separador fino dentro del panel */ /* Separador fino dentro del panel */
.route-panel-divider { .route-panel-divider {
@@ -1549,6 +1611,19 @@ html, body {
text-align: center; text-align: center;
} }
.config-dual-labeled--equilibrado {
grid-template-columns: minmax(0, 4rem) minmax(0, 4.4rem);
align-items: end;
}
.config-equilibrado-help {
align-self: end;
padding-bottom: .12rem;
font-size: .58rem;
line-height: 1.08;
text-align: left;
}
.num-stepper { .num-stepper {
@@ -1870,11 +1945,6 @@ html, body {
min-width: 0; min-width: 0;
} }
.line-filter__range--segundos-parada {
grid-template-columns: auto minmax(120px, 150px);
margin-top: .5rem;
}
.line-filter__selector-head { .line-filter__selector-head {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -2023,6 +2093,65 @@ html, body {
pointer-events: none; pointer-events: none;
} }
.leaflet-tooltip.rt-elev-tooltip {
background: rgba(15, 23, 42, .92) !important;
border: 1px solid rgba(226, 232, 240, .35) !important;
border-radius: 6px !important;
color: #f8fafc !important;
font-size: .68rem !important;
font-weight: 800 !important;
padding: 2px 6px !important;
box-shadow: 0 8px 18px rgba(0, 0, 0, .35) !important;
}
.leaflet-tooltip.rt-elev-tooltip:before {
display: none !important;
}
.rt-route-elev-point {
stroke: #020617 !important;
stroke-width: 1.8px !important;
filter: drop-shadow(0 1px 2px rgba(2, 6, 23, .75));
}
.rt-route-elev-point--subida,
.rt-route-elev-point--subida-fuerte {
animation: rt-route-elev-pulse-up 1.45s ease-in-out infinite;
}
.rt-route-elev-point--bajada,
.rt-route-elev-point--bajada-fuerte {
animation: rt-route-elev-pulse-down 1.45s ease-in-out infinite;
}
.rt-route-elev-point--llano {
opacity: .88;
}
@keyframes rt-route-elev-pulse-up {
0%, 100% {
stroke-width: 1.8px;
opacity: .95;
}
50% {
stroke-width: 4px;
opacity: .72;
}
}
@keyframes rt-route-elev-pulse-down {
0%, 100% {
stroke-width: 1.8px;
opacity: .95;
}
50% {
stroke-width: 3.5px;
opacity: .68;
}
}

View File

@@ -103,6 +103,7 @@ window.rt.clearRoutes = function () {
if (!window.rt.ensureRouteLayer()) return; if (!window.rt.ensureRouteLayer()) return;
window.rt._routeLayer.clearLayers(); window.rt._routeLayer.clearLayers();
window.rt._routeLines = {}; window.rt._routeLines = {};
if (window.rt.clearRouteElevPoints) window.rt.clearRouteElevPoints();
if (window.rt._routeStopsLayer) { if (window.rt._routeStopsLayer) {
window.rt._routeStopsLayer.clearLayers(); window.rt._routeStopsLayer.clearLayers();
@@ -112,13 +113,11 @@ window.rt.clearRoutes = function () {
window.rt.drawRoute = function (id, coords, options) { window.rt.drawRoute = function (id, coords, options) {
if (!window.rt.ensureRouteLayer()) return; if (!window.rt.ensureRouteLayer()) return;
// borrar línea anterior si existe
if (window.rt._routeLines[id]) { if (window.rt._routeLines[id]) {
window.rt._routeLayer.removeLayer(window.rt._routeLines[id]); window.rt._routeLayer.removeLayer(window.rt._routeLines[id]);
delete window.rt._routeLines[id]; delete window.rt._routeLines[id];
} }
// borrar flecha anterior si existe
const arrowId = id + "__arrow"; const arrowId = id + "__arrow";
if (window.rt._routeLines[arrowId]) { if (window.rt._routeLines[arrowId]) {
window.rt._routeLayer.removeLayer(window.rt._routeLines[arrowId]); window.rt._routeLayer.removeLayer(window.rt._routeLines[arrowId]);
@@ -128,38 +127,40 @@ window.rt.drawRoute = function (id, coords, options) {
const color = (options && options.color) || "#000"; const color = (options && options.color) || "#000";
const weight = (options && options.weight) || 4; const weight = (options && options.weight) || 4;
const opacity = (options && options.opacity) || 0.9; const opacity = (options && options.opacity) || 0.9;
const dashArray = options && options.dashArray; const dashArray = options && options.dashArray;
const lineCap = options && options.lineCap; const lineCap = options && options.lineCap;
const arrowEnd = !!(options && options.arrowEnd); const arrowEnd = !!(options && options.arrowEnd);
const arrowClass = dashArray ? "rt-arrow rt-arrow--dashed" : "rt-arrow";
const arrowInnerHtml = dashArray ? '<span class="rt-arrow-dash-head"></span>' : ''; const attachRouteClick = function (line) {
line.on("click", function (e) {
try {
if (e && e.originalEvent) {
if (e.originalEvent.preventDefault) e.originalEvent.preventDefault();
if (e.originalEvent.stopPropagation) e.originalEvent.stopPropagation();
if (e.originalEvent.stopImmediatePropagation) e.originalEvent.stopImmediatePropagation();
}
if (window.rt._dotNetHelper) {
window.rt._dotNetHelper.invokeMethodAsync("OnPreviewItinerarioClick", id);
}
} catch (e) { }
});
};
const buildLine = function (lineCoords, lineOpts) {
const line = window.L.polyline(lineCoords, lineOpts);
attachRouteClick(line);
return line;
};
const lineOpts = { color, weight, opacity }; const lineOpts = { color, weight, opacity };
if (dashArray) lineOpts.dashArray = dashArray; if (dashArray) lineOpts.dashArray = dashArray;
if (lineCap) lineOpts.lineCap = lineCap; if (lineCap) lineOpts.lineCap = lineCap;
const line = window.L.polyline(coords, lineOpts); const line = buildLine(coords, lineOpts);
// ✅ Hacer la línea clicable y avisar a Blazor (igual que drawPreviewLine)
line.on("click", function (e) {
try {
if (e && e.originalEvent) {
if (e.originalEvent.preventDefault) e.originalEvent.preventDefault();
if (e.originalEvent.stopPropagation) e.originalEvent.stopPropagation();
if (e.originalEvent.stopImmediatePropagation) e.originalEvent.stopImmediatePropagation();
}
if (window.rt._dotNetHelper) {
window.rt._dotNetHelper.invokeMethodAsync("OnPreviewItinerarioClick", id);
}
} catch (e) { }
});
line.addTo(window.rt._routeLayer); line.addTo(window.rt._routeLayer);
window.rt._routeLines[id] = line; window.rt._routeLines[id] = line;
// Flecha SOLO al final
if (arrowEnd && coords && coords.length >= 2) { if (arrowEnd && coords && coords.length >= 2) {
const a = coords[coords.length - 2]; const a = coords[coords.length - 2];
const b = coords[coords.length - 1]; const b = coords[coords.length - 1];
@@ -174,6 +175,9 @@ window.rt.drawRoute = function (id, coords, options) {
const size = Math.max(14, Math.min(28, weight * 4)); // tamaño ligado al grosor const size = Math.max(14, Math.min(28, weight * 4)); // tamaño ligado al grosor
const arrowClass = dashArray ? "rt-arrow rt-arrow--dashed" : "rt-arrow";
const arrowInnerHtml = dashArray ? '<span class="rt-arrow-dash-head"></span>' : '';
const icon = window.L.divIcon({ const icon = window.L.divIcon({
className: "rt-arrow-icon", className: "rt-arrow-icon",
html: `<div class="${arrowClass}" style=" html: `<div class="${arrowClass}" style="
@@ -277,6 +281,18 @@ window.rt.clearElevPoints = function (id) {
window.rt._elevGroups = {}; window.rt._elevGroups = {};
}; };
window.rt.clearRouteElevPoints = function () {
if (!window.rt._elevLayer || !window.rt._elevGroups) return;
Object.keys(window.rt._elevGroups)
.filter(id => id.indexOf("route_elev_") === 0)
.forEach(id => {
const group = window.rt._elevGroups[id];
if (group) window.rt._elevLayer.removeLayer(group);
delete window.rt._elevGroups[id];
});
};
// points: [{ lat, lon, label }] // points: [{ lat, lon, label }]
// options: { radius, opacity, fillOpacity, showLabels, labelEvery } // options: { radius, opacity, fillOpacity, showLabels, labelEvery }
window.rt.drawElevPoints = function (id, points, options) { window.rt.drawElevPoints = function (id, points, options) {
@@ -294,18 +310,28 @@ window.rt.drawElevPoints = function (id, points, options) {
const showLabels = options && options.showLabels !== false; // default true const showLabels = options && options.showLabels !== false; // default true
const every = (options && options.labelEvery) || 1; // default 1 const every = (options && options.labelEvery) || 1; // default 1
const showSlopeTooltips = !!(options && options.showSlopeTooltips);
const baseClassName = (options && options.className) || "";
const defaultColor = (options && options.color) || "#facc15";
const group = window.L.layerGroup(); const group = window.L.layerGroup();
if (points && points.length) { if (points && points.length) {
for (let i = 0; i < points.length; i++) { for (let i = 0; i < points.length; i++) {
const p = points[i]; const p = points[i];
const pointColor = (p && p.color) || defaultColor;
const category = (p && p.category) ? String(p.category) : "";
const categoryClass = category ? `${baseClassName}--${category}` : "";
const className = [baseClassName, categoryClass, p && p.className].filter(Boolean).join(" ");
const m = window.L.circleMarker([p.lat, p.lon], { const m = window.L.circleMarker([p.lat, p.lon], {
radius: radius, radius: radius,
weight: 1, weight: 1,
opacity: opacity, opacity: opacity,
fillOpacity: fillOpacity fillOpacity: fillOpacity,
color: pointColor,
fillColor: pointColor,
className: className
}); });
if (showLabels && every > 0 && (i % every === 0)) { if (showLabels && every > 0 && (i % every === 0)) {
@@ -342,6 +368,15 @@ window.rt.drawElevPoints = function (id, points, options) {
className: "rt-elev-label" className: "rt-elev-label"
}); });
} }
else if (showSlopeTooltips && p && typeof p.slope === "number" && Number.isFinite(p.slope)) {
const label = (p.slope > 0 ? "+" : "") + p.slope.toFixed(1) + "%";
m.bindTooltip(label, {
sticky: true,
direction: "top",
opacity: 0.92,
className: "rt-elev-tooltip"
});
}
m.addTo(group); m.addTo(group);
} }