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:
@@ -55,10 +55,12 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
private const double CoeficienteEsfuerzoAlturaPorDefecto = 5.2;
|
||||
private double _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
||||
private double _pesoTransbordoRankingMinutos = 4.0;
|
||||
private double _factorEquilibradoAndarBus = 12.0;
|
||||
private string _factorPenalizacionDistanciaPieTextoEdicion = "1.60";
|
||||
private string _parametroPenalizacionDistanciaPieTextoEdicion = "100";
|
||||
private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20";
|
||||
private string _pesoTransbordoRankingTextoEdicion = "4,0";
|
||||
private string _factorEquilibradoAndarBusTextoEdicion = "12";
|
||||
private double _velocidadBusKmH = 16.0;
|
||||
private int _minutosTransbordo = 5;
|
||||
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)
|
||||
{
|
||||
minutos = 0;
|
||||
@@ -220,6 +241,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
_parametroPenalizacionDistanciaPieTextoEdicion = _parametroPenalizacionDistanciaPieMetros.ToString("0", CultureInfo.InvariantCulture);
|
||||
_coeficienteEsfuerzoAlturaTextoEdicion = _coeficienteEsfuerzoAltura.ToString("0.00", CultureInfo.InvariantCulture);
|
||||
_pesoTransbordoRankingTextoEdicion = FormatearDuracionMinutos(_pesoTransbordoRankingMinutos);
|
||||
_factorEquilibradoAndarBusTextoEdicion = _factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -257,6 +279,16 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
SincronizarTextosPenalizacionPie();
|
||||
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 _maxCandidatasPorLado = 12;
|
||||
@@ -317,6 +349,8 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
private int _indiceAlternativaSeleccionada = 0;
|
||||
private DateTime? _ultimaHoraReferenciaRuta = 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>> _paradasRutaVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -1016,6 +1050,23 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
}
|
||||
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>
|
||||
/// Ajusta rutas js.
|
||||
@@ -1168,55 +1219,6 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
+ 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)
|
||||
{
|
||||
if (alternativa is null)
|
||||
@@ -1229,30 +1231,70 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
return EstimarDuracionPieSegundos(distanciaCorregida);
|
||||
}
|
||||
|
||||
private static double CalcularVentajaNormalizada(double valor, double mejor, double peor)
|
||||
private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa)
|
||||
{
|
||||
if (!double.IsFinite(valor) ||
|
||||
!double.IsFinite(mejor) ||
|
||||
!double.IsFinite(peor) ||
|
||||
peor <= mejor)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (alternativa is null || alternativa.EsSoloAPie || alternativa.AlternativaBus is null)
|
||||
return double.PositiveInfinity;
|
||||
|
||||
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) ||
|
||||
!double.IsFinite(mejor) ||
|
||||
!double.IsFinite(peor) ||
|
||||
peor <= mejor)
|
||||
var score = CalcularScoreEquilibradoFactor(alternativa);
|
||||
if (!double.IsFinite(score))
|
||||
return string.Empty;
|
||||
|
||||
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>
|
||||
|
||||
@@ -184,6 +184,7 @@ public partial class PlanificadorRutas
|
||||
existente.HoraSimulada = actual.HoraSimulada;
|
||||
existente.CriterioPrioritarioRuta = actual.CriterioPrioritarioRuta;
|
||||
existente.PreferenciaEtiquetaRuta = actual.PreferenciaEtiquetaRuta;
|
||||
existente.FactorEquilibradoAndarBus = actual.FactorEquilibradoAndarBus;
|
||||
existente.ActivarFiltroLineas = actual.ActivarFiltroLineas;
|
||||
existente.LineasPermitidas = actual.LineasPermitidas.ToList();
|
||||
existente.FechaActualizacionUtc = DateTime.UtcNow;
|
||||
@@ -369,6 +370,15 @@ public partial class PlanificadorRutas
|
||||
_criterioPrioritarioRuta = NormalizarCriterioRuta(item.CriterioPrioritarioRuta, item.PreferenciaEtiquetaRuta);
|
||||
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;
|
||||
_lineasPermitidas.Clear();
|
||||
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),
|
||||
CriterioPrioritarioRuta = _criterioPrioritarioRuta,
|
||||
PreferenciaEtiquetaRuta = _preferenciaEtiquetaRuta,
|
||||
FactorEquilibradoAndarBus = double.IsFinite(_factorEquilibradoAndarBus)
|
||||
? Math.Max(0, _factorEquilibradoAndarBus)
|
||||
: 12.0,
|
||||
ActivarFiltroLineas = _activarRangoLineas,
|
||||
LineasPermitidas = ObtenerLineasPermitidasSeleccionadas().ToList(),
|
||||
FechaCreacionUtc = DateTime.UtcNow,
|
||||
@@ -573,6 +586,9 @@ public partial class PlanificadorRutas
|
||||
var fecha = item.UsarHoraSimulada ? item.FechaSimulada?.Trim() ?? string.Empty : string.Empty;
|
||||
var hora = item.UsarHoraSimulada ? item.HoraSimulada?.Trim() ?? string.Empty : string.Empty;
|
||||
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
|
||||
? string.Join(",", NormalizarLineasHistorial(item.LineasPermitidas))
|
||||
: string.Empty;
|
||||
@@ -587,6 +603,7 @@ public partial class PlanificadorRutas
|
||||
fecha,
|
||||
hora,
|
||||
criterio,
|
||||
factorEquilibrado,
|
||||
item.ActivarFiltroLineas ? "1" : "0",
|
||||
lineas);
|
||||
}
|
||||
|
||||
@@ -1261,7 +1261,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
private bool _elevPopupMinimizado = false;
|
||||
|
||||
|
||||
private const string _versionApp = "(v-20260626a)";
|
||||
private const string _versionApp = "(v-20260709a)";
|
||||
|
||||
|
||||
private double? _paradaElevG;
|
||||
|
||||
@@ -35,6 +35,7 @@ public partial class PlanificadorRutas
|
||||
public double? ParametroPenalizacionDistanciaPieMetros { get; set; }
|
||||
public double? CoeficienteEsfuerzoAltura { get; set; }
|
||||
public double? PesoTransbordoRankingMinutos { get; set; }
|
||||
public double? FactorEquilibradoAndarBus { get; set; }
|
||||
public double VelocidadPieKmH { get; set; }
|
||||
public double VelocidadBusKmH { get; set; }
|
||||
public int ElevMaxPuntosPorTramo { get; set; }
|
||||
@@ -134,6 +135,7 @@ public partial class PlanificadorRutas
|
||||
ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros,
|
||||
CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura,
|
||||
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
|
||||
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
|
||||
VelocidadPieKmH = _velocidadPieKmH,
|
||||
VelocidadBusKmH = _velocidadBusKmH,
|
||||
ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo,
|
||||
@@ -188,6 +190,8 @@ public partial class PlanificadorRutas
|
||||
: coeficienteAltura;
|
||||
if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(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);
|
||||
_velocidadBusKmH = Math.Clamp(
|
||||
Math.Abs(config.VelocidadBusKmH - 15.0) < 0.0001 ||
|
||||
@@ -290,6 +294,7 @@ public partial class PlanificadorRutas
|
||||
_parametroPenalizacionDistanciaPieMetros = 100.0;
|
||||
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
||||
_pesoTransbordoRankingMinutos = 4.0;
|
||||
_factorEquilibradoAndarBus = 12.0;
|
||||
_velocidadPieKmH = 3.6;
|
||||
_velocidadBusKmH = 16.0;
|
||||
_minutosTransbordo = 5;
|
||||
@@ -336,6 +341,7 @@ public partial class PlanificadorRutas
|
||||
_parametroPenalizacionDistanciaPieMetros = 100.0;
|
||||
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
||||
_pesoTransbordoRankingMinutos = 4.0;
|
||||
_factorEquilibradoAndarBus = 12.0;
|
||||
_velocidadPieKmH = 3.6;
|
||||
_velocidadBusKmH = 16.0;
|
||||
_minutosTransbordo = 5;
|
||||
|
||||
@@ -1458,49 +1458,8 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
if (candidatas.Count == 0)
|
||||
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
|
||||
.Select(x =>
|
||||
{
|
||||
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)
|
||||
.OrderBy(x => CalcularScoreEquilibradoFactor(x.opcion))
|
||||
.ThenBy(x => x.opcion.NumeroTransbordosReales)
|
||||
.ThenBy(x => x.opcion.TiempoEsperaTotalSegundos)
|
||||
.ThenBy(x => x.opcion.PuntuacionCoste)
|
||||
@@ -1528,6 +1487,14 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
.ThenBy(TiempoPieOrdenacion)
|
||||
.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
|
||||
.Where(opcion => opcion.EsSoloAPie)
|
||||
@@ -2141,6 +2108,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie,
|
||||
ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros,
|
||||
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
|
||||
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
|
||||
VelocidadBusKmH = _velocidadBusKmH,
|
||||
MinutosPorTransbordo = _minutosTransbordo,
|
||||
MargenTransbordoSegundos = Math.Clamp(_margenTransbordoSegundos, 0, 1800),
|
||||
|
||||
@@ -66,6 +66,148 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
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);
|
||||
}
|
||||
|
||||
ThrowIfPintadoObsoleto(versionPintado);
|
||||
await ActualizarAlturasResumenRutaAsync(CancellationToken.None);
|
||||
ThrowIfPintadoObsoleto(versionPintado);
|
||||
await CerrarPopupsAsync();
|
||||
|
||||
@@ -207,7 +351,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
_mensajeRuta = CapitalizarPalabrasClave(
|
||||
@@ -307,7 +451,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -384,7 +528,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
});
|
||||
|
||||
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);
|
||||
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaUnion);
|
||||
}
|
||||
@@ -435,7 +579,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaTransferencia);
|
||||
@@ -478,7 +622,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -688,7 +832,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
});
|
||||
|
||||
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);
|
||||
EstablecerPrimerPunto(w2.line, ObtenerUltimoPunto(lineaUnionFinal));
|
||||
}
|
||||
@@ -715,7 +859,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
class="route-priority-option @(CriterioPrioritarioEquilibradoActivo ? "route-priority-option-active" : "")"
|
||||
title="Mantener el criterio equilibrado actual"
|
||||
@onclick="@(() => CambiarCriterioPrioritarioRutasAsync(CriterioRutaEquilibrado))">
|
||||
Equilibrado*
|
||||
Equilib. A
|
||||
</button>
|
||||
<button type="button"
|
||||
class="route-priority-option @(CriterioPrioritarioTiempoActivo ? "route-priority-option-active" : "")"
|
||||
@@ -639,6 +639,22 @@
|
||||
</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)
|
||||
{
|
||||
<div class="route-alt-list">
|
||||
@@ -684,20 +700,9 @@
|
||||
{
|
||||
<span>@FormatearDistanciaTarjeta(alt.AlternativaBus!.DistanciaTotalBus) en bus</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.
|
||||
</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>
|
||||
|
||||
@@ -881,8 +886,8 @@
|
||||
var xyzOrigen = FormatearResumenXYZOrigen();
|
||||
var xyzDestino = FormatearResumenXYZDestino();
|
||||
var tiempoCalculo = FormatearTiempoCalculoRuta();
|
||||
var balanceEquilibradoSeleccionado = CriterioPrioritarioEquilibradoActivo
|
||||
? FormatearBalanceEquilibradoAlternativa(altSeleccionada)
|
||||
var scoreEquilibradoSeleccionado = CriterioPrioritarioEquilibradoActivo
|
||||
? FormatearScoreEquilibradoAlternativa(altSeleccionada)
|
||||
: string.Empty;
|
||||
// var costePenalizado = ObtenerCostePenalizadoAlternativa(altSeleccionada);
|
||||
// var incrementoPenalizacion = Math.Max(0, costePenalizado - Math.Max(0, duracionVisible));
|
||||
@@ -907,9 +912,9 @@
|
||||
{
|
||||
<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))
|
||||
{
|
||||
@@ -1152,24 +1157,29 @@
|
||||
}
|
||||
</label>
|
||||
|
||||
<div class="line-filter__range line-filter__range--segundos-parada">
|
||||
<span class="muted">Segundos por parada: </span>
|
||||
<div class="num-inline">
|
||||
@if (_usarHorariosTeoricos)
|
||||
{
|
||||
<input type="number" class="config-input" step="1" min="0" value="0" disabled readonly />
|
||||
<button type="button" class="step-btn" disabled>▲</button>
|
||||
<button type="button" class="step-btn" disabled>▼</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="number" class="config-input" step="1" min="0"
|
||||
@bind="_segundosPorParadaBus" @bind:event="oninput" @bind:after="OnSegundosPorParadaBusChanged" />
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<label>Bus/paradas:</label>
|
||||
<div class="config-dual-labeled">
|
||||
<label class="config-dual-labeled__field">
|
||||
<span>Seg. parada</span>
|
||||
@if (_usarHorariosTeoricos)
|
||||
{
|
||||
<input type="number" class="config-input" step="1" min="0" value="0" disabled readonly />
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="number" class="config-input" step="1" min="0"
|
||||
@bind="_segundosPorParadaBus" @bind:event="oninput" @bind:after="OnSegundosPorParadaBusChanged" />
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -1346,7 +1356,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<div class="config-row config-row--radio-legacy">
|
||||
<label>Coef. / Param.(m) andando:</label>
|
||||
<div class="config-dual-inline config-dual-inline--bare">
|
||||
<input type="number" class="config-input" step="any"
|
||||
@@ -1382,6 +1392,23 @@
|
||||
</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">
|
||||
<label>Vel. Andando/Bus (km/h):</label>
|
||||
<div class="config-dual-inline config-dual-inline--bare">
|
||||
@@ -1410,20 +1437,6 @@
|
||||
@onblur="OnBlurCoeficienteEsfuerzoAltura" />
|
||||
</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">
|
||||
<label>Minutos por Transbordo:</label>
|
||||
<div class="num-inline">
|
||||
|
||||
@@ -18,6 +18,7 @@ public sealed class RegistroHistorialRuta
|
||||
public string? HoraSimulada { get; set; }
|
||||
public string? CriterioPrioritarioRuta { get; set; }
|
||||
public string? PreferenciaEtiquetaRuta { get; set; }
|
||||
public double? FactorEquilibradoAndarBus { get; set; }
|
||||
public bool ActivarFiltroLineas { get; set; }
|
||||
public List<int> LineasPermitidas { get; set; } = new();
|
||||
public DateTime FechaCreacionUtc { get; set; } = DateTime.UtcNow;
|
||||
|
||||
@@ -26,6 +26,7 @@ public sealed class ParametrosPlanificadorRuta
|
||||
public double FactorPenalizacionDistanciaPie { get; set; } = 1.6;
|
||||
public double ParametroPenalizacionDistanciaPieMetros { get; set; } = 100.0;
|
||||
public double PesoTransbordoRankingMinutos { get; set; } = 4.0;
|
||||
public double FactorEquilibradoAndarBus { get; set; } = 12.0;
|
||||
|
||||
// selección candidatas
|
||||
public int RadioParadasCandidatasMetros { get; set; } = 1200;
|
||||
|
||||
@@ -86,6 +86,7 @@ public sealed class ServicioHistorialRutasCompartido : IHistorialRutasCompartido
|
||||
existente.HoraSimulada = registro.HoraSimulada;
|
||||
existente.CriterioPrioritarioRuta = registro.CriterioPrioritarioRuta;
|
||||
existente.PreferenciaEtiquetaRuta = registro.PreferenciaEtiquetaRuta;
|
||||
existente.FactorEquilibradoAndarBus = registro.FactorEquilibradoAndarBus;
|
||||
existente.ActivarFiltroLineas = registro.ActivarFiltroLineas;
|
||||
existente.LineasPermitidas = NormalizarLineas(registro.LineasPermitidas).ToList();
|
||||
|
||||
@@ -296,6 +297,7 @@ public sealed class ServicioHistorialRutasCompartido : IHistorialRutasCompartido
|
||||
HoraSimulada = item.HoraSimulada,
|
||||
CriterioPrioritarioRuta = item.CriterioPrioritarioRuta,
|
||||
PreferenciaEtiquetaRuta = item.PreferenciaEtiquetaRuta,
|
||||
FactorEquilibradoAndarBus = item.FactorEquilibradoAndarBus,
|
||||
ActivarFiltroLineas = item.ActivarFiltroLineas,
|
||||
LineasPermitidas = NormalizarLineas(item.LineasPermitidas).ToList(),
|
||||
FechaCreacionUtc = item.FechaCreacionUtc,
|
||||
@@ -313,6 +315,9 @@ public sealed class ServicioHistorialRutasCompartido : IHistorialRutasCompartido
|
||||
var criterio = NormalizarTextoClave(item.CriterioPrioritarioRuta);
|
||||
if (string.IsNullOrWhiteSpace(criterio))
|
||||
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
|
||||
? string.Join(",", NormalizarLineas(item.LineasPermitidas))
|
||||
: string.Empty;
|
||||
@@ -327,6 +332,7 @@ public sealed class ServicioHistorialRutasCompartido : IHistorialRutasCompartido
|
||||
fecha,
|
||||
hora,
|
||||
criterio,
|
||||
factorEquilibrado,
|
||||
item.ActivarFiltroLineas ? "1" : "0",
|
||||
lineas);
|
||||
}
|
||||
|
||||
@@ -704,11 +704,13 @@ public sealed class BuscadorRutasTemporalesDbus
|
||||
if (!origenPorCodigo.TryGetValue(primeraParada, out var origen))
|
||||
continue;
|
||||
|
||||
var distanciaTotalBus = CalcularDistanciaBusEstado(estadoDestino);
|
||||
var ruta = new RutaBusAlternativa
|
||||
{
|
||||
Tramos = tramosBus,
|
||||
NumeroTransbordos = numeroTransbordos,
|
||||
CodigosParadasTransbordo = ConstruirCodigosTransbordo(tramosBus)
|
||||
CodigosParadasTransbordo = ConstruirCodigosTransbordo(tramosBus),
|
||||
DistanciaTotalBus = distanciaTotalBus
|
||||
};
|
||||
|
||||
var duracionEstimada = Math.Max(0, (estadoDestino.Llegada - instanteSalida).TotalSeconds) + destino.TiempoAPie;
|
||||
@@ -748,7 +750,8 @@ public sealed class BuscadorRutasTemporalesDbus
|
||||
|
||||
return SeleccionarPoolBusqueda(
|
||||
mejores.Values,
|
||||
Math.Max(12, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 8));
|
||||
Math.Max(12, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 8),
|
||||
parametros);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -756,7 +759,8 @@ public sealed class BuscadorRutasTemporalesDbus
|
||||
/// </summary>
|
||||
private static List<ResultadoBusquedaRutaBusDbus> SeleccionarPoolBusqueda(
|
||||
IEnumerable<ResultadoBusquedaRutaBusDbus> candidatas,
|
||||
int maxPorCoste)
|
||||
int maxPorCoste,
|
||||
ParametrosPlanificadorRuta parametros)
|
||||
{
|
||||
var lista = candidatas.ToList();
|
||||
if (lista.Count == 0)
|
||||
@@ -766,6 +770,7 @@ public sealed class BuscadorRutasTemporalesDbus
|
||||
var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var limiteCoste = Math.Max(1, maxPorCoste);
|
||||
var limiteAndar = Math.Max(8, limiteCoste / 2);
|
||||
var limiteEquilibrado = Math.Max(8, limiteCoste / 2);
|
||||
|
||||
Agregar(lista
|
||||
.OrderBy(x => x.PuntuacionCoste)
|
||||
@@ -781,6 +786,13 @@ public sealed class BuscadorRutasTemporalesDbus
|
||||
.ThenBy(x => x.RutaBus.NumeroTransbordos)
|
||||
.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
|
||||
.OrderBy(x => x.PuntuacionCoste)
|
||||
.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>
|
||||
/// Reconstruye los tramos de bus de una ruta.
|
||||
/// </summary>
|
||||
|
||||
@@ -30,6 +30,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly ConcurrentDictionary<string, (List<double[,]> lineas, double distancia)> _cacheLineasBusPlanificacion =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, Parada> _cacheParadasEstimacion =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private const double UmbralCosturaVisualMetros = 12.0;
|
||||
private const double UmbralUnionPeatonalAutomaticaMetros = 250.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 ToleranciaDominanciaFinalSegundos = 45.0;
|
||||
private const double ToleranciaDominanciaFinalMetros = 40.0;
|
||||
private const double ToleranciaDominanciaFinalBusMetros = 120.0;
|
||||
private const double MejoraClaraDominanciaFinalSegundos = 90.0;
|
||||
private const double MejoraClaraDominanciaFinalMetros = 120.0;
|
||||
private const double AhorroClaroAndandoFinalMetros = 250.0;
|
||||
@@ -79,6 +82,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
_cacheDuracionBusRefinamiento.Clear();
|
||||
_cacheSegmentosHorarios.Clear();
|
||||
_cacheLineasBusPlanificacion.Clear();
|
||||
_cacheParadasEstimacion.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -208,7 +212,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
|
||||
candidatasBus = SeleccionarPoolBusqueda(
|
||||
porFirma.Values,
|
||||
Math.Max(16, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 10));
|
||||
Math.Max(16, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 10),
|
||||
parametros);
|
||||
}
|
||||
|
||||
var extensionesCircularesInicio = BuscarExtensionesCircularesInicialesConHorario(
|
||||
@@ -236,7 +241,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
|
||||
candidatasBus = SeleccionarPoolBusqueda(
|
||||
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())
|
||||
.ToList();
|
||||
|
||||
alternativasBus = SeleccionarPoolFinal(DeduplicarAlternativasEquivalentes(alternativasBus));
|
||||
alternativasBus = SeleccionarPoolFinal(DeduplicarAlternativasEquivalentes(alternativasBus), parametros);
|
||||
|
||||
var finales = SeleccionarAlternativasFinales(alternativasBus, alternativasSoloPie, parametros);
|
||||
if (finales.Count == 0)
|
||||
@@ -444,14 +450,14 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SeleccionarPoolBusqueda(mejores.Values, maxResultados);
|
||||
return SeleccionarPoolBusqueda(mejores.Values, maxResultados, parametros);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -612,7 +618,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
}
|
||||
}
|
||||
|
||||
return SeleccionarPoolBusqueda(mejores.Values, maxResultados);
|
||||
return SeleccionarPoolBusqueda(mejores.Values, maxResultados, parametros);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -681,7 +687,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
}
|
||||
}
|
||||
|
||||
return SeleccionarPoolBusqueda(mejores.Values, maxResultados);
|
||||
return SeleccionarPoolBusqueda(mejores.Values, maxResultados, parametros);
|
||||
|
||||
void ExplorarPrimeraPiernaDirecta(
|
||||
ParadaCandidata origen,
|
||||
@@ -1169,7 +1175,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
|
||||
void PodarMejores()
|
||||
{
|
||||
mejores = SeleccionarPoolBusqueda(mejores.Values, maxResultados * 2)
|
||||
mejores = SeleccionarPoolBusqueda(mejores.Values, maxResultados * 2, parametros)
|
||||
.ToDictionary(
|
||||
x => UtilidadesTransitoDbus.FirmaRutaBus(x.RutaBus),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
@@ -1194,7 +1200,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
var maxResultados = Math.Max(24, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 12);
|
||||
var basesExtension = SeleccionarPoolBusqueda(
|
||||
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)
|
||||
{
|
||||
@@ -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)
|
||||
@@ -1397,9 +1404,10 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
/// <summary>
|
||||
/// Conserva candidatas buenas por coste y tambien un cupo por menor caminata.
|
||||
/// </summary>
|
||||
private static List<ResultadoBusquedaRutaBusDbus> SeleccionarPoolBusqueda(
|
||||
private List<ResultadoBusquedaRutaBusDbus> SeleccionarPoolBusqueda(
|
||||
IEnumerable<ResultadoBusquedaRutaBusDbus> candidatas,
|
||||
int maxPorCoste)
|
||||
int maxPorCoste,
|
||||
ParametrosPlanificadorRuta? parametros = null)
|
||||
{
|
||||
var lista = candidatas.ToList();
|
||||
if (lista.Count == 0)
|
||||
@@ -1409,6 +1417,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var limiteCoste = Math.Max(1, maxPorCoste);
|
||||
var limiteAndar = Math.Max(8, limiteCoste / 2);
|
||||
var limiteEquilibrado = Math.Max(8, limiteCoste / 2);
|
||||
var duracionMinima = lista
|
||||
.Select(x => x.DuracionEstimadaSegundos)
|
||||
.Where(double.IsFinite)
|
||||
@@ -1432,6 +1441,13 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
.ThenBy(x => x.RutaBus.NumeroTransbordos)
|
||||
.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
|
||||
.Where(x => x.DuracionEstimadaSegundos <= limiteDuracionAndarRazonable)
|
||||
.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(
|
||||
IReadOnlyList<ResultadoBusquedaRutaBusDbus> candidatasBus,
|
||||
IReadOnlyList<ParadaCandidata> candidatasOrigen,
|
||||
@@ -1700,11 +1735,19 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
.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 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
|
||||
.OrderBy(x => x.PuntuacionCoste)
|
||||
.ThenBy(x => x.DuracionTotalAproxSegundos)
|
||||
@@ -2683,6 +2726,59 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
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)
|
||||
{
|
||||
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 modoMenosAndar = string.Equals(preferenciaEtiquetaRuta, "Menos andar", StringComparison.OrdinalIgnoreCase);
|
||||
var alternativasBusRazonables = FiltrarAlternativasRazonablesFinales(alternativasBus, masRapidaBus);
|
||||
var alternativasBusParaAndar = modoMenosAndar
|
||||
var modoEquilibrado = string.IsNullOrWhiteSpace(preferenciaEtiquetaRuta);
|
||||
var alternativasBusParaAndar = modoMenosAndar || modoEquilibrado
|
||||
? AmpliarAlternativasPorAhorroAndando(alternativasBus, alternativasBusRazonables)
|
||||
: alternativasBusRazonables;
|
||||
var menosTransbordos = alternativasBusRazonables
|
||||
@@ -2775,9 +2872,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
modoMenosAndar ? null : masRapidaBus,
|
||||
parametros);
|
||||
var priorizarAndar = string.Equals(preferenciaEtiquetaRuta, "Menos andar", StringComparison.OrdinalIgnoreCase);
|
||||
var modoEquilibrado = string.IsNullOrWhiteSpace(preferenciaEtiquetaRuta);
|
||||
var equilibrada = modoEquilibrado
|
||||
? ElegirEquilibradaRazonable(alternativasBusRazonables, parametros)
|
||||
? ElegirEquilibradaRazonable(alternativasBusParaAndar, parametros)
|
||||
: null;
|
||||
|
||||
if (priorizarAndar)
|
||||
@@ -2788,8 +2884,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
}
|
||||
else if (modoEquilibrado)
|
||||
{
|
||||
AddAlternativaBus(finales, usadas, masRapidaBus, "M\u00e1s r\u00e1pida");
|
||||
AddAlternativaBus(finales, usadas, equilibrada, "Equilibrada");
|
||||
AddAlternativaBus(finales, usadas, masRapidaBus, "M\u00e1s r\u00e1pida");
|
||||
AddAlternativaBus(finales, usadas, menosAndar, "Menos andar");
|
||||
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)
|
||||
: priorizarTiempo
|
||||
? 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);
|
||||
|
||||
foreach (var alternativa in candidatasRelleno)
|
||||
@@ -2841,8 +2937,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
return null;
|
||||
|
||||
return alternativasBus
|
||||
.OrderBy(x => CalcularScoreEquilibrado(x, alternativasBus, parametros))
|
||||
.ThenBy(x => CalcularSacrificioMedioEquilibrado(x, alternativasBus, parametros))
|
||||
.OrderBy(x => CalcularScoreEquilibrado(x, parametros))
|
||||
.ThenBy(x => x.NumeroTransbordosReales)
|
||||
.ThenBy(x => x.TiempoEsperaTotalSegundos)
|
||||
.ThenBy(x => x.PuntuacionCoste)
|
||||
@@ -2851,69 +2946,28 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
|
||||
private static double CalcularScoreEquilibrado(
|
||||
AlternativaRuta alternativa,
|
||||
IReadOnlyList<AlternativaRuta> alternativasBus,
|
||||
ParametrosPlanificadorRuta parametros)
|
||||
{
|
||||
var (sacrificioTiempo, sacrificioAndar) = CalcularSacrificiosEquilibrado(
|
||||
alternativa,
|
||||
alternativasBus,
|
||||
parametros);
|
||||
if (alternativa.AlternativaBus is null)
|
||||
return double.PositiveInfinity;
|
||||
|
||||
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(
|
||||
AlternativaRuta alternativa,
|
||||
IReadOnlyList<AlternativaRuta> alternativasBus,
|
||||
ParametrosPlanificadorRuta parametros)
|
||||
{
|
||||
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);
|
||||
var score = distanciaBus + (distanciaAndando * factor);
|
||||
return double.IsFinite(score) && score >= 0
|
||||
? score
|
||||
: double.PositiveInfinity;
|
||||
}
|
||||
|
||||
private static double CalcularCosteAndarEquilibradoSegundos(
|
||||
AlternativaRuta alternativa,
|
||||
ParametrosPlanificadorRuta parametros)
|
||||
{
|
||||
var distanciaCorregida =
|
||||
UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieInicioAprox, parametros) +
|
||||
UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieTransbordosAprox, parametros) +
|
||||
UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieFinAprox, parametros);
|
||||
var distanciaCorregida = CalcularDistanciaAndandoCorregidaMetros(alternativa, parametros);
|
||||
|
||||
if (!double.IsFinite(distanciaCorregida) || distanciaCorregida < 0)
|
||||
return double.PositiveInfinity;
|
||||
@@ -2921,18 +2975,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
return UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaCorregida, parametros);
|
||||
}
|
||||
|
||||
private static double CalcularSacrificioNormalizado(double valor, double mejor, double peor)
|
||||
{
|
||||
if (!double.IsFinite(valor) ||
|
||||
!double.IsFinite(mejor) ||
|
||||
!double.IsFinite(peor) ||
|
||||
peor <= mejor)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.Clamp((valor - mejor) / (peor - mejor), 0.0, 1.0);
|
||||
}
|
||||
private static double CalcularDistanciaAndandoCorregidaMetros(
|
||||
AlternativaRuta alternativa,
|
||||
ParametrosPlanificadorRuta parametros)
|
||||
=> UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieInicioAprox, parametros)
|
||||
+ UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieTransbordosAprox, parametros)
|
||||
+ UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieFinAprox, parametros);
|
||||
|
||||
private static double DuracionVisibleOrdenacion(AlternativaRuta alternativa)
|
||||
=> double.IsFinite(alternativa.DuracionTotalAproxSegundos)
|
||||
@@ -3049,6 +3097,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
{
|
||||
var duracionCandidata = DuracionVisibleOrdenacion(candidata);
|
||||
var pieCandidata = candidata.DistanciaPieTotalAprox;
|
||||
var busCandidata = candidata.AlternativaBus?.DistanciaTotalBus ?? double.PositiveInfinity;
|
||||
|
||||
foreach (var otra in alternativas)
|
||||
{
|
||||
@@ -3060,9 +3109,11 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
|
||||
var duracionOtra = DuracionVisibleOrdenacion(otra);
|
||||
var pieOtra = otra.DistanciaPieTotalAprox;
|
||||
var busOtra = otra.AlternativaBus?.DistanciaTotalBus ?? double.PositiveInfinity;
|
||||
var noPierdeTiempo = duracionOtra <= duracionCandidata + ToleranciaDominanciaFinalSegundos;
|
||||
var noPierdeAndando = pieOtra <= pieCandidata + ToleranciaDominanciaFinalMetros;
|
||||
if (!noPierdeTiempo || !noPierdeAndando)
|
||||
var noPierdeBus = busOtra <= busCandidata + ToleranciaDominanciaFinalBusMetros;
|
||||
if (!noPierdeTiempo || !noPierdeAndando || !noPierdeBus)
|
||||
continue;
|
||||
|
||||
var mejoraTransbordo = otra.NumeroTransbordosReales < candidata.NumeroTransbordosReales;
|
||||
|
||||
@@ -807,6 +807,74 @@ html, body {
|
||||
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 */
|
||||
|
||||
.route-alt-list {
|
||||
@@ -872,12 +940,6 @@ html, body {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.route-alt-balance {
|
||||
color: #bfdbfe;
|
||||
white-space: nowrap;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Separador fino dentro del panel */
|
||||
|
||||
.route-panel-divider {
|
||||
@@ -1549,6 +1611,19 @@ html, body {
|
||||
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 {
|
||||
@@ -1870,11 +1945,6 @@ html, body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.line-filter__range--segundos-parada {
|
||||
grid-template-columns: auto minmax(120px, 150px);
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.line-filter__selector-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2023,6 +2093,65 @@ html, body {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ window.rt.clearRoutes = function () {
|
||||
if (!window.rt.ensureRouteLayer()) return;
|
||||
window.rt._routeLayer.clearLayers();
|
||||
window.rt._routeLines = {};
|
||||
if (window.rt.clearRouteElevPoints) window.rt.clearRouteElevPoints();
|
||||
|
||||
if (window.rt._routeStopsLayer) {
|
||||
window.rt._routeStopsLayer.clearLayers();
|
||||
@@ -112,13 +113,11 @@ window.rt.clearRoutes = function () {
|
||||
window.rt.drawRoute = function (id, coords, options) {
|
||||
if (!window.rt.ensureRouteLayer()) return;
|
||||
|
||||
// borrar línea anterior si existe
|
||||
if (window.rt._routeLines[id]) {
|
||||
window.rt._routeLayer.removeLayer(window.rt._routeLines[id]);
|
||||
delete window.rt._routeLines[id];
|
||||
}
|
||||
|
||||
// borrar flecha anterior si existe
|
||||
const arrowId = id + "__arrow";
|
||||
if (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 weight = (options && options.weight) || 4;
|
||||
const opacity = (options && options.opacity) || 0.9;
|
||||
|
||||
const dashArray = options && options.dashArray;
|
||||
const lineCap = options && options.lineCap;
|
||||
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 };
|
||||
if (dashArray) lineOpts.dashArray = dashArray;
|
||||
if (lineCap) lineOpts.lineCap = lineCap;
|
||||
|
||||
const line = window.L.polyline(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) { }
|
||||
});
|
||||
|
||||
const line = buildLine(coords, lineOpts);
|
||||
line.addTo(window.rt._routeLayer);
|
||||
window.rt._routeLines[id] = line;
|
||||
|
||||
// Flecha SOLO al final
|
||||
if (arrowEnd && coords && coords.length >= 2) {
|
||||
const a = coords[coords.length - 2];
|
||||
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 arrowClass = dashArray ? "rt-arrow rt-arrow--dashed" : "rt-arrow";
|
||||
const arrowInnerHtml = dashArray ? '<span class="rt-arrow-dash-head"></span>' : '';
|
||||
|
||||
const icon = window.L.divIcon({
|
||||
className: "rt-arrow-icon",
|
||||
html: `<div class="${arrowClass}" style="
|
||||
@@ -277,6 +281,18 @@ window.rt.clearElevPoints = function (id) {
|
||||
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 }]
|
||||
// options: { radius, opacity, fillOpacity, showLabels, labelEvery }
|
||||
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 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();
|
||||
|
||||
if (points && points.length) {
|
||||
for (let i = 0; i < points.length; 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], {
|
||||
radius: radius,
|
||||
weight: 1,
|
||||
opacity: opacity,
|
||||
fillOpacity: fillOpacity
|
||||
fillOpacity: fillOpacity,
|
||||
color: pointColor,
|
||||
fillColor: pointColor,
|
||||
className: className
|
||||
});
|
||||
|
||||
if (showLabels && every > 0 && (i % every === 0)) {
|
||||
@@ -342,6 +368,15 @@ window.rt.drawElevPoints = function (id, points, options) {
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user