- Rediseñada la configuración de pendientes con un panel compacto accesible desde la rueda.

- Corregida la posición del panel para que no salga fuera de la pantalla.
- Renombrado el interruptor a Visualizar Pendientes.
- Aplicado el porcentaje de subida como umbral para calcular el esfuerzo.
- Mostrado el umbral de subida utilizado dentro de la tabla de elevación.
- Añadido al aviso el total de distancia con pendiente y la altura subida en metros y pisos.
- Evitada la duplicación de acumulados al repintar las pendientes.
- Cambiados los valores por defecto a subida 2 %, bajada 5 % y coeficiente 8,2.
- Añadida la distancia equilibrada andando al detalle técnico.
- Mostradas las distancias en kilómetros con tres decimales.
- Mostrada la fecha del ZIP activo en el acceso de gestión del modelo DBUS.
- Añadidas migraciones para actualizar los antiguos valores predeterminados.
This commit is contained in:
2026-07-20 15:29:25 +02:00
parent 7dc251f44e
commit 7b52dd2c73
16 changed files with 941 additions and 186 deletions

View File

@@ -7,14 +7,14 @@
<base href="/" /> <base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" /> <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="RutasDBUS.styles.css" /> <link rel="stylesheet" href="RutasDBUS.styles.css" />
<link rel="stylesheet" href="app.css?v=20260716m3" /> <link rel="stylesheet" href="app.css?v=20260720h" />
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet /> <HeadOutlet />
</head> </head>
<body> <body>
<Routes /> <Routes />
<script src="app.js?v=20260716m3" defer></script> <script src="app.js?v=20260720h" defer></script>
<script src="_framework/blazor.web.js"></script> <script src="_framework/blazor.web.js"></script>
</body> </body>

View File

@@ -6,6 +6,7 @@ using System.Text.RegularExpressions;
using LeafletForBlazor; using LeafletForBlazor;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using RutasDBUS.Modelos.Geocodificacion;
using RutasDBUS.Modelos.Planificacion; using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus; using RutasDBUS.Modelos.RedBus;
using RutasDBUS.Servicios; using RutasDBUS.Servicios;
@@ -174,6 +175,8 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
if (it is null) return; if (it is null) return;
await PintarItItemAsync(it); await PintarItItemAsync(it);
var coordenadas = ConstruirCoordenadasItinerario(it);
await FitRoutesJsAsync(coordenadas);
} }
/// <summary> /// <summary>
/// Quita it seleccionado. /// Quita it seleccionado.
@@ -254,7 +257,8 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
private double _inspectorLat, _inspectorLon; private double _inspectorLat, _inspectorLon;
private double? _inspectorElev; private double? _inspectorElev;
private double? _inspectorElevIdee; private double? _inspectorElevIdee;
private string? _inspectorCallejero; private IReadOnlyList<LugarGeocodificadoCercano> _inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
private int _versionInspector;
/// <summary> /// <summary>

View File

@@ -47,16 +47,22 @@ public partial class PlanificadorRutas : ComponentBase
private int _nivelBrilloMapa = 0; private int _nivelBrilloMapa = 0;
// CONFIG // CONFIG
private const double FactorPenalizacionDistanciaPiePorDefecto = 1.35;
private const double CoeficienteEsfuerzoAlturaPorDefecto = 8.2;
private const double UmbralPendienteSubidaRutaPorDefecto = 2.0;
private const double UmbralPendienteBajadaRutaPorDefecto = 5.0;
private const double UmbralPisosPendienteRutaPorDefecto = 2.0;
private const double MetrosPorPiso = 3.0;
private double _velocidadPieKmH = 3.6; private double _velocidadPieKmH = 3.6;
private double _factorPenalizacionDistanciaPie = 1.6; private double _factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
private double _parametroPenalizacionDistanciaPieMetros = 100.0; private double _parametroPenalizacionDistanciaPieMetros = 100.0;
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 double _factorEquilibradoAndarBus = 12.0;
private string _factorPenalizacionDistanciaPieTextoEdicion = "1.60"; private string _factorPenalizacionDistanciaPieTextoEdicion = "1.35";
private string _parametroPenalizacionDistanciaPieTextoEdicion = "100"; private string _parametroPenalizacionDistanciaPieTextoEdicion = "100";
private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20"; private string _coeficienteEsfuerzoAlturaTextoEdicion = "8.20";
private string _pesoTransbordoRankingTextoEdicion = "4,0"; private string _pesoTransbordoRankingTextoEdicion = "4,0";
private string _factorEquilibradoAndarBusTextoEdicion = "12"; private string _factorEquilibradoAndarBusTextoEdicion = "12";
private double _velocidadBusKmH = 16.0; private double _velocidadBusKmH = 16.0;
@@ -222,10 +228,11 @@ public partial class PlanificadorRutas : ComponentBase
? _coeficienteEsfuerzoAltura ? _coeficienteEsfuerzoAltura
: CoeficienteEsfuerzoAlturaPorDefecto; : CoeficienteEsfuerzoAlturaPorDefecto;
var pendiente = pendientePorcentaje.Value / 100.0; var umbralSubida = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta);
if (pendiente <= 0) if (pendientePorcentaje.Value < umbralSubida)
return distanciaMetros.Value; return distanciaMetros.Value;
var pendiente = pendientePorcentaje.Value / 100.0;
var factorConPotencia = Math.Pow(1.0 + pendiente, coeficiente); var factorConPotencia = Math.Pow(1.0 + pendiente, coeficiente);
var esfuerzo = distanciaMetros.Value * factorConPotencia; var esfuerzo = distanciaMetros.Value * factorConPotencia;
return double.IsFinite(esfuerzo) return double.IsFinite(esfuerzo)
@@ -351,9 +358,18 @@ public partial class PlanificadorRutas : ComponentBase
private TimeSpan? _tiempoUltimoCalculoRuta = null; private TimeSpan? _tiempoUltimoCalculoRuta = null;
private bool _mostrarElevacionesRuta = false; private bool _mostrarElevacionesRuta = false;
private bool _pintandoElevacionesRuta = false; private bool _pintandoElevacionesRuta = false;
private bool _mostrarConfiguracionPendientesRuta = false;
private bool _panelRutasMovilContraido = false; private bool _panelRutasMovilContraido = false;
private double _umbralPendienteSubidaRuta = 3.0; private double _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
private double _umbralPendienteBajadaRuta = 3.0; private double _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
private double _umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto;
private double _alturaPendientePositivaSignificativaRutaMetros = 0.0;
private double _distanciaPendientePositivaSignificativaRutaMetros = 0.0;
private bool MostrarAvisoPendientesPositivasSignificativas =>
_mostrarElevacionesRuta &&
_alturaPendientePositivaSignificativaRutaMetros >
(NormalizarUmbralPisosPendienteRuta(_umbralPisosPendienteRuta) * MetrosPorPiso);
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);
@@ -987,9 +1003,16 @@ public partial class PlanificadorRutas : ComponentBase
/// <summary> /// <summary>
/// Ajusta rutas js. /// Ajusta rutas js.
/// </summary> /// </summary>
private async Task FitRoutesJsAsync() private async Task FitRoutesJsAsync(IReadOnlyList<double[]>? coordenadas = null)
{ {
try { await JS.InvokeVoidAsync("rt.fitRoutes"); } catch { } try
{
if (coordenadas is null)
await JS.InvokeVoidAsync("rt.fitRoutes");
else
await JS.InvokeVoidAsync("rt.fitRoutes", coordenadas);
}
catch { }
} }
/// <summary> /// <summary>
@@ -1043,7 +1066,7 @@ public partial class PlanificadorRutas : ComponentBase
{ {
if (d <= 0) return "-"; if (d <= 0) return "-";
if (d < 1000) return $"{Math.Truncate(d):0} m"; if (d < 1000) return $"{Math.Truncate(d):0} m";
return $"{(d / 1000.0):0.00} km"; return $"{(d / 1000.0):0.000} km";
} }
/// <summary> /// <summary>
@@ -1206,18 +1229,35 @@ public partial class PlanificadorRutas : ComponentBase
return EstimarDuracionPieSegundos(distanciaCorregida); return EstimarDuracionPieSegundos(distanciaCorregida);
} }
/// <summary>
/// Calcula la aportacion de todos los tramos andando a la distancia equilibrada.
/// </summary>
private double CalcularDistanciaEquilibradaAndandoAlternativa(AlternativaRuta? alternativa)
{
if (alternativa is null)
return 0;
var distanciaAndando = alternativa.EsSoloAPie
? alternativa.DistanciaSoloAPie
: alternativa.DistanciaPieTotalAprox;
var factor = double.IsFinite(_factorEquilibradoAndarBus)
? Math.Max(0, _factorEquilibradoAndarBus)
: 12.0;
var distanciaEquilibrada = Math.Max(0, distanciaAndando) * factor;
return double.IsFinite(distanciaEquilibrada)
? distanciaEquilibrada
: 0;
}
private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa) private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa)
{ {
if (alternativa is null || alternativa.EsSoloAPie || alternativa.AlternativaBus is null) if (alternativa is null || alternativa.EsSoloAPie || alternativa.AlternativaBus is null)
return double.PositiveInfinity; return double.PositiveInfinity;
var distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus); var distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus);
var distanciaAndando = Math.Max(0, alternativa.DistanciaPieTotalAprox); var distanciaEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(alternativa);
var factor = double.IsFinite(_factorEquilibradoAndarBus) var score = distanciaBus + distanciaEquilibradaAndando;
? Math.Max(0, _factorEquilibradoAndarBus)
: 12.0;
var score = distanciaBus + (distanciaAndando * factor);
return double.IsFinite(score) && score >= 0 return double.IsFinite(score) && score >= 0
? score ? score
: double.PositiveInfinity; : double.PositiveInfinity;
@@ -1232,9 +1272,6 @@ public partial class PlanificadorRutas : ComponentBase
return $"Score {FormatearDistanciaTarjeta(score)} (x{_factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture)})"; return $"Score {FormatearDistanciaTarjeta(score)} (x{_factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture)})";
} }
private string FactorEquilibradoAndarBusResumen
=> _factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture);
private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync() private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync()
{ {
if (!CriterioPrioritarioEquilibradoActivo) if (!CriterioPrioritarioEquilibradoActivo)
@@ -1572,6 +1609,8 @@ public partial class PlanificadorRutas : ComponentBase
_hayRutaValida = false; _hayRutaValida = false;
_tramosRuta.Clear(); _tramosRuta.Clear();
_duracionTotalSegundos = 0; _duracionTotalSegundos = 0;
_alturaPendientePositivaSignificativaRutaMetros = 0.0;
_distanciaPendientePositivaSignificativaRutaMetros = 0.0;
_altitudOrigenResumenRuta = null; _altitudOrigenResumenRuta = null;
_altitudDestinoResumenRuta = null; _altitudDestinoResumenRuta = null;
_callejeroOrigenResumenRuta = null; _callejeroOrigenResumenRuta = null;

View File

@@ -76,7 +76,7 @@ public partial class PlanificadorRutas
try try
{ {
await Task.Delay(120, ct); await Task.Delay(120, ct);
var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 10, ct); var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 20, ct);
if (ct.IsCancellationRequested || version != _versionSugerenciasBusqueda) if (ct.IsCancellationRequested || version != _versionSugerenciasBusqueda)
return; return;
@@ -118,6 +118,10 @@ public partial class PlanificadorRutas
_hayRutaValida = false; _hayRutaValida = false;
await SincronizarMarcadoresOrigenDestinoJsAsync(); await SincronizarMarcadoresOrigenDestinoJsAsync();
await FitRoutesJsAsync(new List<double[]>
{
new[] { sugerencia.Latitud, sugerencia.Longitud }
});
StateHasChanged(); StateHasChanged();
} }

View File

@@ -232,6 +232,9 @@ public partial class PlanificadorRutas : ComponentBase
/// </summary> /// </summary>
private async Task PintarItFiltro() private async Task PintarItFiltro()
{ {
if (!PuedePintarItinerariosFiltrados)
return;
foreach (var it in _itItemsFiltrados) foreach (var it in _itItemsFiltrados)
{ {
if (it is null) continue; if (it is null) continue;
@@ -289,6 +292,23 @@ public partial class PlanificadorRutas : ComponentBase
RegistrarPreview(clave, subId); RegistrarPreview(clave, subId);
} }
} }
/// <summary>
/// Reúne la geometría de todos los sentidos incluidos en un elemento de itinerario.
/// </summary>
private List<double[]> ConstruirCoordenadasItinerario(ElementoItinerario it)
{
var coordenadas = new List<double[]>();
foreach (var variante in it.Variantes)
{
var geometria = ConstruirGeometriaCompletaVariante(variante);
if (geometria is not null)
coordenadas.AddRange(LineaToLatLng(geometria));
}
return coordenadas;
}
private Parada? _paradaPrimerClickPendiente; private Parada? _paradaPrimerClickPendiente;
@@ -609,6 +629,8 @@ public partial class PlanificadorRutas : ComponentBase
private string _itFiltro = ""; private string _itFiltro = "";
private bool PuedePintarItinerariosFiltrados => !string.IsNullOrWhiteSpace(_itFiltro);
private string ItFiltro private string ItFiltro
{ {
get => _itFiltro; get => _itFiltro;
@@ -1181,8 +1203,8 @@ public partial class PlanificadorRutas : ComponentBase
private bool _elevPopupMinimizado = false; private bool _elevPopupMinimizado = false;
private const string _versionApp = "(v-20260716a)"; private const string _versionApp = "(v-20260720c)";
private double? _paradaElevG; private double? _paradaElevG;
@@ -1200,7 +1222,7 @@ public partial class PlanificadorRutas : ComponentBase
_itinerarioClicado = null; _itinerarioClicado = null;
_paradaClicada = null; _paradaClicada = null;
_popupInspectorVisible = false; await CerrarInspectorAsync();
if (_tramoElevDetalle is not null) if (_tramoElevDetalle is not null)
{ {

View File

@@ -18,6 +18,38 @@ public partial class PlanificadorRutas
private ModeloHorarioDbusInfo? ModeloHorarioActivo => private ModeloHorarioDbusInfo? ModeloHorarioActivo =>
_modelosHorarios.FirstOrDefault(modelo => modelo.Activo); _modelosHorarios.FirstOrDefault(modelo => modelo.Activo);
private string FechaModeloHorarioActivo =>
FormatearFechaModeloHorario(ModeloHorarioActivo?.NombreArchivo);
private static string FormatearFechaModeloHorario(string? nombreArchivo)
{
if (string.IsNullOrWhiteSpace(nombreArchivo))
return "Sin modelo cargado";
for (var indice = nombreArchivo.Length - 8; indice >= 0; indice--)
{
if (indice > 0 && char.IsDigit(nombreArchivo[indice - 1]))
continue;
var fin = indice + 8;
if (fin < nombreArchivo.Length && char.IsDigit(nombreArchivo[fin]))
continue;
var candidata = nombreArchivo.Substring(indice, 8);
if (DateTime.TryParseExact(
candidata,
"yyyyMMdd",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out _))
{
return candidata;
}
}
return "Fecha no disponible";
}
private void AbrirModelosHorariosPanel() private void AbrirModelosHorariosPanel()
{ {
_mostrarModelosHorariosPanel = true; _mostrarModelosHorariosPanel = true;

View File

@@ -38,6 +38,7 @@ public partial class PlanificadorRutas
public double? FactorEquilibradoAndarBus { get; set; } public double? FactorEquilibradoAndarBus { get; set; }
public double? UmbralPendienteSubidaRuta { get; set; } public double? UmbralPendienteSubidaRuta { get; set; }
public double? UmbralPendienteBajadaRuta { get; set; } public double? UmbralPendienteBajadaRuta { get; set; }
public double? UmbralPisosPendienteRuta { 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; }
@@ -140,6 +141,7 @@ public partial class PlanificadorRutas
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus, FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta, UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta,
UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta, UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta,
UmbralPisosPendienteRuta = _umbralPisosPendienteRuta,
VelocidadPieKmH = _velocidadPieKmH, VelocidadPieKmH = _velocidadPieKmH,
VelocidadBusKmH = _velocidadBusKmH, VelocidadBusKmH = _velocidadBusKmH,
ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo, ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo,
@@ -185,11 +187,17 @@ public partial class PlanificadorRutas
_horaSimulada = horaSimulada; _horaSimulada = horaSimulada;
if (config.FactorPenalizacionDistanciaPie is { } factor && double.IsFinite(factor)) if (config.FactorPenalizacionDistanciaPie is { } factor && double.IsFinite(factor))
_factorPenalizacionDistanciaPie = Math.Abs(factor - 1.3) < 0.0001 ? 1.6 : factor; _factorPenalizacionDistanciaPie =
Math.Abs(factor - 1.3) < 0.0001 || Math.Abs(factor - 1.6) < 0.0001
? FactorPenalizacionDistanciaPiePorDefecto
: factor;
if (config.ParametroPenalizacionDistanciaPieMetros is { } parametro && double.IsFinite(parametro)) if (config.ParametroPenalizacionDistanciaPieMetros is { } parametro && double.IsFinite(parametro))
_parametroPenalizacionDistanciaPieMetros = parametro; _parametroPenalizacionDistanciaPieMetros = parametro;
if (config.CoeficienteEsfuerzoAltura is { } coeficienteAltura && double.IsFinite(coeficienteAltura)) if (config.CoeficienteEsfuerzoAltura is { } coeficienteAltura && double.IsFinite(coeficienteAltura))
_coeficienteEsfuerzoAltura = Math.Abs(coeficienteAltura - 1.6) < 0.0001 _coeficienteEsfuerzoAltura =
Math.Abs(coeficienteAltura - 1.6) < 0.0001 ||
Math.Abs(coeficienteAltura - 5.2) < 0.0001 ||
Math.Abs(coeficienteAltura - 9.2) < 0.0001
? CoeficienteEsfuerzoAlturaPorDefecto ? CoeficienteEsfuerzoAlturaPorDefecto
: coeficienteAltura; : coeficienteAltura;
if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo)) if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo))
@@ -197,9 +205,17 @@ public partial class PlanificadorRutas
if (config.FactorEquilibradoAndarBus is { } factorEquilibrado && double.IsFinite(factorEquilibrado)) if (config.FactorEquilibradoAndarBus is { } factorEquilibrado && double.IsFinite(factorEquilibrado))
_factorEquilibradoAndarBus = Math.Max(0, factorEquilibrado); _factorEquilibradoAndarBus = Math.Max(0, factorEquilibrado);
if (config.UmbralPendienteSubidaRuta is { } umbralSubida && double.IsFinite(umbralSubida)) if (config.UmbralPendienteSubidaRuta is { } umbralSubida && double.IsFinite(umbralSubida))
_umbralPendienteSubidaRuta = NormalizarUmbralPendienteRuta(umbralSubida); _umbralPendienteSubidaRuta = Math.Abs(umbralSubida - 3.0) < 0.0001
? UmbralPendienteSubidaRutaPorDefecto
: NormalizarUmbralPendienteRuta(umbralSubida);
if (config.UmbralPendienteBajadaRuta is { } umbralBajada && double.IsFinite(umbralBajada)) if (config.UmbralPendienteBajadaRuta is { } umbralBajada && double.IsFinite(umbralBajada))
_umbralPendienteBajadaRuta = NormalizarUmbralPendienteRuta(umbralBajada); _umbralPendienteBajadaRuta =
Math.Abs(umbralBajada - 3.0) < 0.0001 ||
Math.Abs(umbralBajada - 6.0) < 0.0001
? UmbralPendienteBajadaRutaPorDefecto
: NormalizarUmbralPendienteRuta(umbralBajada);
if (config.UmbralPisosPendienteRuta is { } umbralPisos && double.IsFinite(umbralPisos))
_umbralPisosPendienteRuta = NormalizarUmbralPisosPendienteRuta(umbralPisos);
_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 ||
@@ -298,13 +314,14 @@ public partial class PlanificadorRutas
_minTransbordosPermitidos = 0; _minTransbordosPermitidos = 0;
_maxTransbordosPermitidos = 3; _maxTransbordosPermitidos = 3;
_factorPenalizacionDistanciaPie = 1.6; _factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
_parametroPenalizacionDistanciaPieMetros = 100.0; _parametroPenalizacionDistanciaPieMetros = 100.0;
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
_pesoTransbordoRankingMinutos = 4.0; _pesoTransbordoRankingMinutos = 4.0;
_factorEquilibradoAndarBus = 12.0; _factorEquilibradoAndarBus = 12.0;
_umbralPendienteSubidaRuta = 3.0; _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
_umbralPendienteBajadaRuta = 3.0; _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
_umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto;
_velocidadPieKmH = 3.6; _velocidadPieKmH = 3.6;
_velocidadBusKmH = 16.0; _velocidadBusKmH = 16.0;
_minutosTransbordo = 5; _minutosTransbordo = 5;
@@ -347,13 +364,14 @@ public partial class PlanificadorRutas
_minTransbordosPermitidos = 0; _minTransbordosPermitidos = 0;
_maxTransbordosPermitidos = 3; _maxTransbordosPermitidos = 3;
_factorPenalizacionDistanciaPie = 1.6; _factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
_parametroPenalizacionDistanciaPieMetros = 100.0; _parametroPenalizacionDistanciaPieMetros = 100.0;
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
_pesoTransbordoRankingMinutos = 4.0; _pesoTransbordoRankingMinutos = 4.0;
_factorEquilibradoAndarBus = 12.0; _factorEquilibradoAndarBus = 12.0;
_umbralPendienteSubidaRuta = 3.0; _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
_umbralPendienteBajadaRuta = 3.0; _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
_umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto;
_velocidadPieKmH = 3.6; _velocidadPieKmH = 3.6;
_velocidadBusKmH = 16.0; _velocidadBusKmH = 16.0;
_minutosTransbordo = 5; _minutosTransbordo = 5;

View File

@@ -6,6 +6,7 @@ using System.Text.RegularExpressions;
using LeafletForBlazor; using LeafletForBlazor;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using RutasDBUS.Modelos.Geocodificacion;
using RutasDBUS.Modelos.Planificacion; using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus; using RutasDBUS.Modelos.RedBus;
using RutasDBUS.Servicios; using RutasDBUS.Servicios;
@@ -1267,10 +1268,26 @@ public partial class PlanificadorRutas : ComponentBase
/// </summary> /// </summary>
private async Task MostrarInspectorAsync(double lat, double lon) private async Task MostrarInspectorAsync(double lat, double lon)
{ {
var versionInspector = ++_versionInspector;
_inspectorLat = lat; _inspectorLat = lat;
_inspectorLon = lon; _inspectorLon = lon;
_inspectorCallejero = null; _inspectorElev = null;
_inspectorElevIdee = null;
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
_popupInspectorVisible = true;
await ActualizarMarcadoresInspectorJsAsync();
StateHasChanged();
var tareaLugares = GeocodificadorLocal.BuscarCercanosAsync(
lat,
lon,
radioMetros: 20,
maxResultados: 3,
cancellationToken: CancellationToken.None);
double? elevacionOpen = null;
double? elevacionIdee = null;
try try
{ {
// ------------------------- // -------------------------
@@ -1279,7 +1296,7 @@ public partial class PlanificadorRutas : ComponentBase
var kOpen = ElevKey(lat, lon); var kOpen = ElevKey(lat, lon);
if (_elevCache.TryGetValue(kOpen, out var cachedOpen)) if (_elevCache.TryGetValue(kOpen, out var cachedOpen))
{ {
_inspectorElev = cachedOpen; elevacionOpen = cachedOpen;
} }
else else
{ {
@@ -1287,7 +1304,7 @@ public partial class PlanificadorRutas : ComponentBase
new List<(double lat, double lon)> { (lat, lon) }, new List<(double lat, double lon)> { (lat, lon) },
CancellationToken.None); CancellationToken.None);
_inspectorElev = _elevCache.TryGetValue(ElevKey(lat, lon), out var elevOpen) elevacionOpen = _elevCache.TryGetValue(ElevKey(lat, lon), out var elevOpen)
? elevOpen ? elevOpen
: (double?)null; : (double?)null;
} }
@@ -1298,7 +1315,7 @@ public partial class PlanificadorRutas : ComponentBase
var kIdee = ClaveElevacionIdee(lat, lon); var kIdee = ClaveElevacionIdee(lat, lon);
if (_elevIdeeCache.TryGetValue(kIdee, out var cachedIdee)) if (_elevIdeeCache.TryGetValue(kIdee, out var cachedIdee))
{ {
_inspectorElevIdee = cachedIdee; elevacionIdee = cachedIdee;
} }
else else
{ {
@@ -1306,33 +1323,72 @@ public partial class PlanificadorRutas : ComponentBase
new List<(double lat, double lon)> { (lat, lon) }, new List<(double lat, double lon)> { (lat, lon) },
CancellationToken.None); CancellationToken.None);
_inspectorElevIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var elevIdee) elevacionIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var elevIdee)
? elevIdee ? elevIdee
: (double?)null; : (double?)null;
} }
} }
catch catch
{ {
_inspectorElev = null;
_inspectorElevIdee = null;
} }
try try
{ {
var lugar = await GeocodificadorLocal.BuscarMasCercanoAsync( var lugares = await tareaLugares;
lat, if (versionInspector != _versionInspector)
lon, return;
CancellationToken.None);
_inspectorCallejero = FormatearLugarCallejeroResumen(lugar); _inspectorLugaresCercanos = lugares;
} }
catch catch
{ {
_inspectorCallejero = null; if (versionInspector != _versionInspector)
return;
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
} }
_popupInspectorVisible = true; if (versionInspector != _versionInspector)
return;
_inspectorElev = elevacionOpen;
_inspectorElevIdee = elevacionIdee;
await ActualizarMarcadoresInspectorJsAsync();
StateHasChanged(); StateHasChanged();
} }
private async Task ActualizarMarcadoresInspectorJsAsync()
{
var lugares = _inspectorLugaresCercanos
.Select(cercano => new
{
latitud = cercano.Lugar.Latitud,
longitud = cercano.Lugar.Longitud,
texto = FormatearLugarCallejeroResumen(cercano)
})
.ToList();
try
{
await JS.InvokeVoidAsync(
"rt.setInspectorMarkers",
_inspectorLat,
_inspectorLon,
lugares);
}
catch
{
}
}
private async Task CerrarInspectorAsync()
{
_versionInspector++;
_popupInspectorVisible = false;
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
try { await JS.InvokeVoidAsync("rt.clearInspectorMarkers"); } catch { }
}
/// <summary> /// <summary>

View File

@@ -77,7 +77,7 @@ public partial class PlanificadorRutas : ComponentBase
}; };
await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta
? "Aplicando elevaciones..." ? "Aplicando pendientes..."
: "Representando ruta seleccionada..."); : "Representando ruta seleccionada...");
} }
@@ -96,11 +96,21 @@ public partial class PlanificadorRutas : ComponentBase
await PersistirConfiguracionAsync(); await PersistirConfiguracionAsync();
if (_mostrarElevacionesRuta) if (_mostrarElevacionesRuta)
await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando elevaciones..."); await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando pendientes...");
else else
StateHasChanged(); StateHasChanged();
} }
private async Task CambiarUmbralPisosPendienteRutaAsync(ChangeEventArgs e)
{
_umbralPisosPendienteRuta = ParseUmbralPisosPendienteRuta(
e.Value,
_umbralPisosPendienteRuta);
await PersistirConfiguracionAsync();
StateHasChanged();
}
private async Task RepintarRutaSeleccionadaPorElevacionesAsync(string mensaje) private async Task RepintarRutaSeleccionadaPorElevacionesAsync(string mensaje)
{ {
if (_opcionesRuta.Count == 0 || if (_opcionesRuta.Count == 0 ||
@@ -111,6 +121,7 @@ public partial class PlanificadorRutas : ComponentBase
return; return;
} }
ReiniciarResumenPendientesRuta();
_pintandoElevacionesRuta = true; _pintandoElevacionesRuta = true;
_mensajeRuta = mensaje; _mensajeRuta = mensaje;
StateHasChanged(); StateHasChanged();
@@ -143,7 +154,24 @@ public partial class PlanificadorRutas : ComponentBase
private static double NormalizarUmbralPendienteRuta(double valor) private static double NormalizarUmbralPendienteRuta(double valor)
=> double.IsFinite(valor) => double.IsFinite(valor)
? Math.Clamp(Math.Abs(valor), 0.0, 50.0) ? Math.Clamp(Math.Abs(valor), 0.0, 50.0)
: 3.0; : UmbralPendienteSubidaRutaPorDefecto;
private static double ParseUmbralPisosPendienteRuta(object? valor, double actual)
{
var texto = Convert.ToString(valor, CultureInfo.InvariantCulture)?.Replace(',', '.');
if (!double.TryParse(texto, NumberStyles.Float, CultureInfo.InvariantCulture, out var nuevo) ||
!double.IsFinite(nuevo))
{
return NormalizarUmbralPisosPendienteRuta(actual);
}
return NormalizarUmbralPisosPendienteRuta(nuevo);
}
private static double NormalizarUmbralPisosPendienteRuta(double valor)
=> double.IsFinite(valor)
? Math.Clamp(Math.Abs(valor), 0.0, 100.0)
: UmbralPisosPendienteRutaPorDefecto;
private async Task DrawWalkingRouteJsAsync( private async Task DrawWalkingRouteJsAsync(
string id, string id,
@@ -152,9 +180,17 @@ public partial class PlanificadorRutas : ComponentBase
int versionPintado, int versionPintado,
bool arrowEnd = true) bool arrowEnd = true)
{ {
var segmentosElevacion = await ConstruirSegmentosElevacionMapaAsync(linea, CancellationToken.None); var resultadoElevacion = await ConstruirSegmentosElevacionMapaAsync(linea, CancellationToken.None);
ThrowIfPintadoObsoleto(versionPintado); ThrowIfPintadoObsoleto(versionPintado);
if (_mostrarElevacionesRuta)
{
_alturaPendientePositivaSignificativaRutaMetros +=
resultadoElevacion.AlturaPositivaSignificativaMetros;
_distanciaPendientePositivaSignificativaRutaMetros +=
resultadoElevacion.DistanciaPositivaSignificativaMetros;
}
await DrawRouteJsAsync( await DrawRouteJsAsync(
id, id,
linea, linea,
@@ -164,20 +200,25 @@ public partial class PlanificadorRutas : ComponentBase
dashArray: DashArrayPiePunteado, dashArray: DashArrayPiePunteado,
arrowEnd: arrowEnd, arrowEnd: arrowEnd,
lineCap: "round", lineCap: "round",
elevationSegments: segmentosElevacion); elevationSegments: resultadoElevacion.Segmentos);
} }
private async Task<List<object>?> ConstruirSegmentosElevacionMapaAsync(double[,] linea, CancellationToken ct) private async Task<(
List<object>? Segmentos,
double AlturaPositivaSignificativaMetros,
double DistanciaPositivaSignificativaMetros)>
ConstruirSegmentosElevacionMapaAsync(double[,] linea, CancellationToken ct)
{ {
if (!_mostrarElevacionesRuta || linea.GetLength(0) < 2) if (!_mostrarElevacionesRuta || linea.GetLength(0) < 2)
return null; return (null, 0.0, 0.0);
var muestras = await ConstruirMuestrasElevacionAsync(linea, ct); var muestras = await ConstruirMuestrasElevacionAsync(linea, ct);
if (muestras.Count < 2) if (muestras.Count < 2)
return null; return (null, 0.0, 0.0);
var segmentos = new List<object>(); var segmentos = new List<object>();
var hayPendienteCalculada = false; var hayPendienteCalculada = false;
var totalesPendientePositiva = CalcularTotalesPendientePositivaSignificativa(muestras);
for (var i = 1; i < muestras.Count; i++) for (var i = 1; i < muestras.Count; i++)
{ {
@@ -200,7 +241,8 @@ public partial class PlanificadorRutas : ComponentBase
if (elevAnterior is not null && elevActual is not null) if (elevAnterior is not null && elevActual is not null)
{ {
pendiente = (elevActual.Value - elevAnterior.Value) / distancia * 100.0; var diferenciaAltura = elevActual.Value - elevAnterior.Value;
pendiente = diferenciaAltura / distancia * 100.0;
hayPendienteCalculada = true; hayPendienteCalculada = true;
} }
@@ -224,9 +266,64 @@ public partial class PlanificadorRutas : ComponentBase
}); });
} }
return hayPendienteCalculada && segmentos.Count > 0 return (
? segmentos hayPendienteCalculada && segmentos.Count > 0 ? segmentos : null,
: null; totalesPendientePositiva.AlturaMetros,
totalesPendientePositiva.DistanciaMetros);
}
private (double DistanciaMetros, double AlturaMetros)
CalcularTotalesPendientePositivaSignificativa(IReadOnlyList<MuestraElevacion>? muestras)
{
if (muestras is null || muestras.Count < 2)
return (0.0, 0.0);
var distanciaTotal = 0.0;
var alturaTotal = 0.0;
var umbralSubida = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta);
for (var i = 1; i < muestras.Count; i++)
{
var anterior = muestras[i - 1];
var actual = muestras[i];
if (anterior.ElevacionIdee is null || actual.ElevacionIdee is null)
continue;
var distancia = CalcularDistanciaMetros(
anterior.Latitud,
anterior.Longitud,
actual.Latitud,
actual.Longitud);
if (distancia <= 0)
continue;
var diferenciaAltura = actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value;
var pendiente = diferenciaAltura / distancia * 100.0;
if (pendiente <= umbralSubida)
continue;
distanciaTotal += distancia;
alturaTotal += Math.Max(0, diferenciaAltura);
}
return (distanciaTotal, alturaTotal);
}
private static string FormatearDistanciaResumenElevacion(double distanciaMetros)
{
if (!double.IsFinite(distanciaMetros) || distanciaMetros <= 0)
return "-";
if (distanciaMetros < 950)
return $"{distanciaMetros:0.00} m";
return $"{(distanciaMetros / 1000.0):0.000} km";
}
private void ReiniciarResumenPendientesRuta()
{
_alturaPendientePositivaSignificativaRutaMetros = 0.0;
_distanciaPendientePositivaSignificativaRutaMetros = 0.0;
} }
private static double? ElevacionPreferidaMapa(MuestraElevacion muestra) private static double? ElevacionPreferidaMapa(MuestraElevacion muestra)
@@ -259,6 +356,7 @@ public partial class PlanificadorRutas : ComponentBase
{ {
var versionPintado = IniciarNuevaVersionPintadoRuta(); var versionPintado = IniciarNuevaVersionPintadoRuta();
var mensajeAnterior = _mensajeRuta; var mensajeAnterior = _mensajeRuta;
ReiniciarResumenPendientesRuta();
try try
{ {

View File

@@ -60,7 +60,9 @@
<button class="btn-ghost" @onclick="PintarItSeleccionado">Ver</button> <button class="btn-ghost" @onclick="PintarItSeleccionado">Ver</button>
<button class="btn-ghost" @onclick="QuitarItSeleccionado">Ocultar</button> <button class="btn-ghost" @onclick="QuitarItSeleccionado">Ocultar</button>
<button class="btn-ghost" @onclick="PintarItFiltro">Ver filtro</button> <button class="btn-ghost"
disabled="@(!PuedePintarItinerariosFiltrados)"
@onclick="PintarItFiltro">Ver filtro</button>
<button class="btn-ghost" @onclick="QuitarTodosItinerarios">Ocultar Todos</button> <button class="btn-ghost" @onclick="QuitarTodosItinerarios">Ocultar Todos</button>
@@ -344,29 +346,38 @@
<div class="popup-parada-body"> <div class="popup-parada-body">
<div><b>Latitud:</b> @_inspectorLat.ToString("0.000000")</div> <div><b>Latitud:</b> @_inspectorLat.ToString("0.000000")</div>
<div><b>Longitud:</b> @_inspectorLon.ToString("0.000000")</div> <div><b>Longitud:</b> @_inspectorLon.ToString("0.000000")</div>
@if (!string.IsNullOrWhiteSpace(_inspectorCallejero))
{
<div><b>Callejero:</b> @_inspectorCallejero</div>
}
<div> <div>
<b>Altitud (O):</b> <b>Altitud (O):</b>
@(_inspectorElev is null @(_inspectorElev is null
? "-" ? "-"
: _inspectorElev.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m") : _inspectorElev.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
</div> </div>
<div> <div>
<b>Altitud (G):</b> <b>Altitud (G):</b>
@(_inspectorElevIdee is null @(_inspectorElevIdee is null
? "-" ? "-"
: _inspectorElevIdee.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m") : _inspectorElevIdee.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
</div> </div>
</div>
<button class="popup-parada-cerrar" @onclick="() => _popupInspectorVisible = false">×</button> @if (_inspectorLugaresCercanos.Count > 0)
{
<div class="inspector-address-list">
@for (var indiceLugar = 0; indiceLugar < _inspectorLugaresCercanos.Count; indiceLugar++)
{
var lugarCercano = _inspectorLugaresCercanos[indiceLugar];
<div class="inspector-address-item">
<span class="inspector-address-number">@(indiceLugar + 1)</span>
<span>@FormatearLugarCallejeroResumen(lugarCercano)</span>
</div>
}
</div>
}
</div>
<button class="popup-parada-cerrar" @onclick="CerrarInspectorAsync">×</button>
</div> </div>
} }
</div> </div>
} }
@@ -427,6 +438,11 @@
} }
else else
{ {
<div class="muted" style="font-size:0.72rem; margin:0 0 .35rem;">
<b>Umbral de subida (Esf.):</b>
@NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta).ToString("0.#", CultureInfo.InvariantCulture) %
</div>
<table class="popup-elev-table" style=" <table class="popup-elev-table" style="
width:100%; width:100%;
font-size:0.8rem; font-size:0.8rem;
@@ -545,10 +561,12 @@
double totalDist = 0; double totalDist = 0;
double totalEsfuerzoPositivo = 0; double totalEsfuerzoPositivo = 0;
double totalEsfuerzo = 0; double totalEsfuerzo = 0;
double totalDistPendPositiva = 0; var totalesPendientePositiva = CalcularTotalesPendientePositivaSignificativa(s);
double totalAlturaPendPositiva = 0; double totalDistPendPositiva = totalesPendientePositiva.DistanciaMetros;
double totalAlturaPendPositiva = totalesPendientePositiva.AlturaMetros;
var hayEsfuerzoPositivo = false; var hayEsfuerzoPositivo = false;
var hayEsfuerzo = false; var hayEsfuerzo = false;
var umbralPendientePositiva = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta);
for (int i = 1; i < s.Count; i++) for (int i = 1; i < s.Count; i++)
{ {
var anterior = s[i - 1]; var anterior = s[i - 1];
@@ -560,12 +578,6 @@
if (distancia > 0 && anterior.ElevacionIdee is not null && actual.ElevacionIdee is not null) if (distancia > 0 && anterior.ElevacionIdee is not null && actual.ElevacionIdee is not null)
{ {
pendiente = (actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value) / distancia * 100.0; pendiente = (actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value) / distancia * 100.0;
if (pendiente > 0)
{
totalDistPendPositiva += distancia;
totalAlturaPendPositiva += Math.Max(0, actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value);
}
} }
var esfuerzo = CalcularEsfuerzoElevacion(distancia, pendiente); var esfuerzo = CalcularEsfuerzoElevacion(distancia, pendiente);
@@ -574,7 +586,7 @@
totalEsfuerzo += esfuerzo.Value; totalEsfuerzo += esfuerzo.Value;
hayEsfuerzo = true; hayEsfuerzo = true;
if (pendiente > 0) if (pendiente > umbralPendientePositiva)
{ {
totalEsfuerzoPositivo += esfuerzo.Value; totalEsfuerzoPositivo += esfuerzo.Value;
hayEsfuerzoPositivo = true; hayEsfuerzoPositivo = true;
@@ -582,32 +594,23 @@
} }
} }
string FDist(double d)
{
if (d <= 0) return "-";
if (d < 950)
return $"{d:0.00} m";
return $"{(d / 1000.0):0.000} km";
}
<div class="muted" style="font-size:0.78rem; margin-top:.5rem;"> <div class="muted" style="font-size:0.78rem; margin-top:.5rem;">
<div><b>O:</b> (inifin) = @F(OIni) → @F(OFin) · (min/max) = @F(OMin) / @F(OMax)</div> <div><b>O:</b> (inicio → fin) = @F(OIni) → @F(OFin) · (mínimo/máximo) = @F(OMin) / @F(OMax)</div>
<div><b>G:</b> (inifin) = @F(GIni) → @F(GFin) · (min/max) = @F(GMin) / @F(GMax)</div> <div><b>G:</b> (inicio → fin) = @F(GIni) → @F(GFin) · (mínimo/máximo) = @F(GMin) / @F(GMax)</div>
<div style="margin-top:.25rem;"> <div style="margin-top:.25rem;">
<b>Suma distancia de puntos:</b> @FDist(totalDist) <b>Suma de distancias entre puntos:</b> @FormatearDistanciaResumenElevacion(totalDist)
</div> </div>
<div> <div>
<b>Suma dist pend positiva:</b> @FDist(totalDistPendPositiva) <b>Suma de distancias con pendiente positiva:</b> @FormatearDistanciaResumenElevacion(totalDistPendPositiva)
</div> </div>
<div> <div>
<b>Suma alturas pend positiva:</b> @(totalAlturaPendPositiva > 0 <b>Suma de alturas con pendiente positiva:</b> @(totalAlturaPendPositiva > 0
? totalAlturaPendPositiva.ToString("0.00", CultureInfo.InvariantCulture) + " m (" + (totalAlturaPendPositiva / 3.0).ToString("0.0", CultureInfo.InvariantCulture) + " pisos)" ? totalAlturaPendPositiva.ToString("0.00", CultureInfo.InvariantCulture) + " m (" + (totalAlturaPendPositiva / MetrosPorPiso).ToString("0.0", CultureInfo.InvariantCulture) + " pisos)"
: "-") : "-")
</div> </div>
<div> <div>
<b>Suma de esfuerzos positivos:</b> @(hayEsfuerzoPositivo ? totalEsfuerzoPositivo.ToString("0.00", CultureInfo.InvariantCulture) : "-") <b>Suma de esfuerzos positivos:</b> @(hayEsfuerzoPositivo ? totalEsfuerzoPositivo.ToString("0.00", CultureInfo.InvariantCulture) : "-")
<span style="margin-left:.5rem;"><b>total:</b> @(hayEsfuerzo ? totalEsfuerzo.ToString("0.00", CultureInfo.InvariantCulture) : "-")</span> <span style="margin-left:.5rem;"><b>Total:</b> @(hayEsfuerzo ? totalEsfuerzo.ToString("0.00", CultureInfo.InvariantCulture) : "-")</span>
</div> </div>
</div> </div>
} }
@@ -670,33 +673,74 @@
<span class="route-elevation-switch-thumb"></span> <span class="route-elevation-switch-thumb"></span>
</span> </span>
<span class="route-elevation-switch-text"> <span class="route-elevation-switch-text">
@(_pintandoElevacionesRuta ? "Elevaciones..." : "Elevaciones") Visualizar Pendientes
</span> </span>
</label> </label>
<div class="route-elevation-thresholds" title="Porcentaje minimo para colorear subida o bajada"> <div class="route-elevation-settings">
<label class="route-elevation-threshold"> <button type="button"
<span>Sube</span> class="route-elevation-settings-trigger @(_mostrarConfiguracionPendientesRuta ? "route-elevation-settings-trigger--active" : "")"
<input type="number" title="Configurar pendientes"
min="0" aria-label="Configurar pendientes"
max="50" aria-haspopup="dialog"
step="0.5" aria-expanded="@_mostrarConfiguracionPendientesRuta"
value="@_umbralPendienteSubidaRuta.ToString("0.#", CultureInfo.InvariantCulture)" disabled="@_pintandoElevacionesRuta"
disabled="@_pintandoElevacionesRuta" @onclick="() => _mostrarConfiguracionPendientesRuta = !_mostrarConfiguracionPendientesRuta">
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, true))" /> <span aria-hidden="true">&#9881;</span>
<span>%</span> </button>
</label>
<label class="route-elevation-threshold"> @if (_mostrarConfiguracionPendientesRuta)
<span>Baja</span> {
<input type="number" <div class="route-elevation-settings-popover"
min="0" role="dialog"
max="50" aria-label="Umbrales de pendientes"
step="0.5" @onclick:stopPropagation="true">
value="@_umbralPendienteBajadaRuta.ToString("0.#", CultureInfo.InvariantCulture)" <div class="route-elevation-settings-header">
disabled="@_pintandoElevacionesRuta" <span>Configurar Pendientes</span>
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, false))" /> <button type="button"
<span>%</span> class="route-elevation-settings-close"
</label> title="Cerrar"
aria-label="Cerrar configuración de pendientes"
@onclick="() => _mostrarConfiguracionPendientesRuta = false">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="route-elevation-settings-fields">
<label class="route-elevation-setting-row">
<span>Subida</span>
<input type="text"
inputmode="decimal"
aria-label="Porcentaje de subida"
value="@_umbralPendienteSubidaRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, true))" />
<span>%</span>
</label>
<label class="route-elevation-setting-row">
<span>Bajada</span>
<input type="text"
inputmode="decimal"
aria-label="Porcentaje de bajada"
value="@_umbralPendienteBajadaRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, false))" />
<span>%</span>
</label>
<label class="route-elevation-setting-row"
title="Pisos acumulados para mostrar el aviso de pendientes positivas">
<span>Aviso</span>
<input type="text"
inputmode="decimal"
aria-label="Pisos para mostrar el aviso"
value="@_umbralPisosPendienteRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="CambiarUmbralPisosPendienteRutaAsync" />
<span>pisos</span>
</label>
</div>
</div>
}
</div> </div>
</div> </div>
@@ -768,6 +812,20 @@
{ {
var alt = _opcionesRuta[_indiceAlternativaSeleccionada]; var alt = _opcionesRuta[_indiceAlternativaSeleccionada];
@if (MostrarAvisoPendientesPositivasSignificativas)
{
<div class="route-slope-warning" role="status">
<div>Solución con pendientes positivas significativas.</div>
<div style="margin-top:.2rem; padding-left:.55rem;">
<div>- Distancia con pendiente: @FormatearDistanciaResumenElevacion(_distanciaPendientePositivaSignificativaRutaMetros)</div>
<div>
- Altura subida: @_alturaPendientePositivaSignificativaRutaMetros.ToString("0.00", CultureInfo.InvariantCulture) m
(@((_alturaPendientePositivaSignificativaRutaMetros / MetrosPorPiso).ToString("0.0", CultureInfo.InvariantCulture)) pisos)
</div>
</div>
</div>
}
@* ========= RESUMEN BONITO ARRIBA (único) ========= *@ @* ========= RESUMEN BONITO ARRIBA (único) ========= *@
<p class="route-main-text"> <p class="route-main-text">
@if (alt.EsSoloAPie) @if (alt.EsSoloAPie)
@@ -928,6 +986,7 @@
: _duracionTotalSegundos; : _duracionTotalSegundos;
var dRecta = DistanciaLineaRectaOrigenDestino(); var dRecta = DistanciaLineaRectaOrigenDestino();
var dCorregida = CalcularDistanciaCorregidaAlternativa(altSeleccionada); var dCorregida = CalcularDistanciaCorregidaAlternativa(altSeleccionada);
var dEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(altSeleccionada);
var xyzOrigen = FormatearResumenXYZOrigen(); var xyzOrigen = FormatearResumenXYZOrigen();
var xyzDestino = FormatearResumenXYZDestino(); var xyzDestino = FormatearResumenXYZDestino();
var callejeroOrigen = FormatearResumenCallejeroOrigen(); var callejeroOrigen = FormatearResumenCallejeroOrigen();
@@ -948,7 +1007,7 @@
</p> </p>
} }
@if (dRecta > 0 || dCorregida > 0) @if (dRecta > 0 || dCorregida > 0 || dEquilibradaAndando > 0)
{ {
<div class="muted" style="font-size:0.75rem;margin-top:4px;"> <div class="muted" style="font-size:0.75rem;margin-top:4px;">
@if (dRecta > 0) @if (dRecta > 0)
@@ -959,6 +1018,10 @@
{ {
<div>d.Corregida a pie: @FormatearDistanciaDetalleRuta(dCorregida)</div> <div>d.Corregida a pie: @FormatearDistanciaDetalleRuta(dCorregida)</div>
} }
@if (dEquilibradaAndando > 0)
{
<div>d.Equilib. A: @FormatearDistanciaDetalleRuta(dEquilibradaAndando)</div>
}
@if (!string.IsNullOrWhiteSpace(scoreEquilibradoSeleccionado)) @if (!string.IsNullOrWhiteSpace(scoreEquilibradoSeleccionado))
{ {
<div>Score equilibrado: @scoreEquilibradoSeleccionado</div> <div>Score equilibrado: @scoreEquilibradoSeleccionado</div>
@@ -1177,6 +1240,22 @@
<button type="button" class="config-close-btn" @onclick="ToggleConfigPanel">×</button> <button type="button" class="config-close-btn" @onclick="ToggleConfigPanel">×</button>
</div> </div>
<div class="config-row config-row-full">
<div class="modelos-horarios">
<button type="button"
class="modelos-horarios__toggle"
@onclick="AbrirModelosHorariosPanel">
<span class="modelos-horarios__toggle-label">Modelo:</span>
<span class="modelos-horarios__activo">
@FechaModeloHorarioActivo
</span>
<span class="modelos-horarios__toggle-action">
Gestionar
</span>
</button>
</div>
</div>
@* <div class="config-row config-row-full"> @* <div class="config-row config-row-full">
<label>Selección de paradas (rápida):</label> <label>Selección de paradas (rápida):</label>
@@ -1254,22 +1333,6 @@
</div> </div>
</div> </div>
<div class="config-row config-row-full">
<div class="modelos-horarios">
<button type="button"
class="modelos-horarios__toggle"
@onclick="AbrirModelosHorariosPanel">
<span class="modelos-horarios__toggle-label">Modelo DBUS</span>
<span class="modelos-horarios__activo">
@(ModeloHorarioActivo?.NombreArchivo ?? "Sin modelo cargado")
</span>
<span class="modelos-horarios__toggle-action">
Gestionar
</span>
</button>
</div>
</div>
@* ========================= @* =========================
RANGO DE LÍNEAS RANGO DE LÍNEAS
========================= *@ ========================= *@
@@ -1470,13 +1533,10 @@
<span>Factor</span> <span>Factor</span>
<input type="number" class="config-input" step="0.1" min="0" <input type="number" class="config-input" step="0.1" min="0"
placeholder="12" placeholder="12"
title="1 metro andando pesa como este numero de metros en bus" title="Factor del modo equilibrado"
@bind="FactorEquilibradoAndarBusTexto" @bind:event="oninput" @bind="FactorEquilibradoAndarBusTexto" @bind:event="oninput"
@onblur="OnBlurFactorEquilibradoAndarBus" /> @onblur="OnBlurFactorEquilibradoAndarBus" />
</label> </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> </div>

View File

@@ -23,7 +23,7 @@ public sealed class ParametrosPlanificadorRuta
public int MinutosPorTransbordo { get; set; } = 5; public int MinutosPorTransbordo { get; set; } = 5;
public int MargenTransbordoSegundos { get; set; } = 120; public int MargenTransbordoSegundos { get; set; } = 120;
public int SegundosPorParadaBus { get; set; } = 30; public int SegundosPorParadaBus { get; set; } = 30;
public double FactorPenalizacionDistanciaPie { get; set; } = 1.6; public double FactorPenalizacionDistanciaPie { get; set; } = 1.35;
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; public double FactorEquilibradoAndarBus { get; set; } = 12.0;

View File

@@ -13,6 +13,7 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio
private readonly IWebHostEnvironment _entorno; private readonly IWebHostEnvironment _entorno;
private readonly SemaphoreSlim _semaforoCarga = new(1, 1); private readonly SemaphoreSlim _semaforoCarga = new(1, 1);
private readonly ConcurrentDictionary<string, LugarGeocodificadoCercano> _cacheCercanos = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, LugarGeocodificadoCercano> _cacheCercanos = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, IReadOnlyList<LugarGeocodificadoCercano>> _cacheCercanosPorRadio = new(StringComparer.Ordinal);
private IReadOnlyList<EntradaGeocodificada>? _indice; private IReadOnlyList<EntradaGeocodificada>? _indice;
public GeocodificadorLocalServicio(IWebHostEnvironment entorno) public GeocodificadorLocalServicio(IWebHostEnvironment entorno)
@@ -109,6 +110,73 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio
return cercano; return cercano;
} }
public async Task<IReadOnlyList<LugarGeocodificadoCercano>> BuscarCercanosAsync(
double latitud,
double longitud,
double radioMetros = 20,
int maxResultados = 3,
CancellationToken cancellationToken = default)
{
if (!double.IsFinite(latitud) || !double.IsFinite(longitud))
return Array.Empty<LugarGeocodificadoCercano>();
radioMetros = double.IsFinite(radioMetros)
? Math.Clamp(radioMetros, 0, 1000)
: 20;
maxResultados = Math.Clamp(maxResultados, 1, 10);
var clave = ClaveCercanos(latitud, longitud, radioMetros, maxResultados);
if (_cacheCercanosPorRadio.TryGetValue(clave, out var cacheados))
return cacheados;
var indice = await ObtenerIndiceAsync(cancellationToken);
if (indice.Count == 0)
return Array.Empty<LugarGeocodificadoCercano>();
LugarGeocodificadoCercano? masCercano = null;
var dentroDelRadio = new List<LugarGeocodificadoCercano>();
foreach (var entrada in indice)
{
cancellationToken.ThrowIfCancellationRequested();
var lugar = entrada.Lugar;
var distancia = DistanciaHaversineMetros(latitud, longitud, lugar.Latitud, lugar.Longitud);
if (masCercano is null || distancia < masCercano.DistanciaMetros)
{
masCercano = new LugarGeocodificadoCercano
{
Lugar = lugar,
DistanciaMetros = distancia
};
}
if (distancia <= radioMetros)
{
dentroDelRadio.Add(new LugarGeocodificadoCercano
{
Lugar = lugar,
DistanciaMetros = distancia
});
}
}
IReadOnlyList<LugarGeocodificadoCercano> resultado = dentroDelRadio.Count > 0
? dentroDelRadio
.OrderBy(x => x.DistanciaMetros)
.Take(maxResultados)
.ToArray()
: masCercano is null
? Array.Empty<LugarGeocodificadoCercano>()
: new[] { masCercano! };
if (resultado.Count > 0)
_cacheCercanos.TryAdd(ClaveCercano(latitud, longitud), resultado[0]);
_cacheCercanosPorRadio.TryAdd(clave, resultado);
return resultado;
}
private async Task<IReadOnlyList<EntradaGeocodificada>> ObtenerIndiceAsync(CancellationToken cancellationToken) private async Task<IReadOnlyList<EntradaGeocodificada>> ObtenerIndiceAsync(CancellationToken cancellationToken)
{ {
if (_indice is not null) if (_indice is not null)
@@ -411,6 +479,9 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio
private static string ClaveCercano(double latitud, double longitud) private static string ClaveCercano(double latitud, double longitud)
=> $"{latitud.ToString("0.000000", CultureInfo.InvariantCulture)}|{longitud.ToString("0.000000", CultureInfo.InvariantCulture)}"; => $"{latitud.ToString("0.000000", CultureInfo.InvariantCulture)}|{longitud.ToString("0.000000", CultureInfo.InvariantCulture)}";
private static string ClaveCercanos(double latitud, double longitud, double radioMetros, int maxResultados)
=> $"{ClaveCercano(latitud, longitud)}|{radioMetros.ToString("0.##", CultureInfo.InvariantCulture)}|{maxResultados}";
private static double DistanciaHaversineMetros(double lat1, double lon1, double lat2, double lon2) private static double DistanciaHaversineMetros(double lat1, double lon1, double lat2, double lon2)
{ {
const double radioTierraMetros = 6371000.0; const double radioTierraMetros = 6371000.0;

View File

@@ -14,5 +14,12 @@ public interface IGeocodificadorLocalServicio
double longitud, double longitud,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
Task<IReadOnlyList<LugarGeocodificadoCercano>> BuscarCercanosAsync(
double latitud,
double longitud,
double radioMetros = 20,
int maxResultados = 3,
CancellationToken cancellationToken = default);
Task<int> ContarLugaresAsync(CancellationToken cancellationToken = default); Task<int> ContarLugaresAsync(CancellationToken cancellationToken = default);
} }

View File

@@ -313,7 +313,7 @@ internal static class UtilidadesTransitoDbus
var factor = double.IsFinite(parametros.FactorPenalizacionDistanciaPie) var factor = double.IsFinite(parametros.FactorPenalizacionDistanciaPie)
? parametros.FactorPenalizacionDistanciaPie ? parametros.FactorPenalizacionDistanciaPie
: 1.6; : 1.35;
var parametroBaseMetros = double.IsFinite(parametros.ParametroPenalizacionDistanciaPieMetros) var parametroBaseMetros = double.IsFinite(parametros.ParametroPenalizacionDistanciaPieMetros)
? Math.Abs(parametros.ParametroPenalizacionDistanciaPieMetros) ? Math.Abs(parametros.ParametroPenalizacionDistanciaPieMetros)
: 100.0; : 100.0;

View File

@@ -13,6 +13,63 @@
font-size: 1rem; font-size: 1rem;
} }
.inspector-address-list {
display: grid;
gap: 4px;
margin: 5px 0 7px;
}
.popup-parada-body .inspector-address-item {
display: grid;
grid-template-columns: 22px minmax(0, 1fr);
align-items: center;
gap: 6px;
margin: 0;
line-height: 1.25;
}
.inspector-address-number,
.rt-inspector-place-marker {
display: grid;
place-items: center;
box-sizing: border-box;
border: 2px solid #ecfeff;
border-radius: 50%;
color: #ffffff;
background: #0f766e;
font-weight: 800;
line-height: 1;
box-shadow: 0 0 0 2px rgba(15, 23, 42, .78), 0 4px 10px rgba(15, 23, 42, .3);
}
.inspector-address-number {
width: 20px;
height: 20px;
font-size: .68rem;
}
.inspector-address-item > span:last-child {
min-width: 0;
overflow-wrap: anywhere;
}
.rt-inspector-place-icon {
border: 0 !important;
background: transparent !important;
}
.rt-inspector-place-marker {
width: 24px;
height: 24px;
font-size: .72rem;
}
.rt-inspector-tooltip {
max-width: min(280px, calc(100vw - 28px));
white-space: normal;
font-size: .75rem;
}
.history-open-icon { .history-open-icon {
display: inline-block; display: inline-block;
font-size: 0.9rem; font-size: 0.9rem;
@@ -247,6 +304,14 @@ h1:focus {
background: #1d4ed8; background: #1d4ed8;
color: #fff; color: #fff;
} }
.btn-ghost:disabled,
.btn-ghost:disabled:hover {
background: #111827;
color: #94a3b8;
cursor: not-allowed;
opacity: .48;
}
.page, main, .full-page, .content, article.content { .page, main, .full-page, .content, article.content {
overflow: hidden; overflow: hidden;
} }
@@ -810,9 +875,9 @@ html, body {
.route-visual-options { .route-visual-options {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-start;
flex-wrap: wrap; flex-wrap: nowrap;
gap: .35rem .5rem; gap: .35rem;
margin: -.15rem 0 .4rem; margin: -.15rem 0 .4rem;
} }
@@ -877,27 +942,109 @@ html, body {
opacity: .55; opacity: .55;
} }
.route-elevation-thresholds { .route-elevation-settings {
position: relative;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: .25rem; margin-left: auto;
pointer-events: auto; pointer-events: auto;
} }
.route-elevation-threshold { .route-elevation-settings-trigger {
display: inline-flex; display: grid;
align-items: center; place-items: center;
gap: .18rem; width: 26px;
min-height: 24px; height: 24px;
padding: 0;
border: 1px solid rgba(148, 163, 184, .55);
border-radius: 6px;
background: rgba(2, 6, 23, .78);
color: #cbd5e1; color: #cbd5e1;
font-size: .64rem; cursor: pointer;
font-weight: 700; font-family: "Segoe UI Symbol", sans-serif;
white-space: nowrap; font-size: 1rem;
line-height: 1;
} }
.route-elevation-threshold input { .route-elevation-settings-trigger:hover,
width: 40px; .route-elevation-settings-trigger--active {
border-color: rgba(56, 189, 248, .9);
background: rgba(30, 64, 175, .55);
color: #f8fafc;
}
.route-elevation-settings-trigger:disabled {
cursor: default;
opacity: .55;
}
.route-elevation-settings-popover {
position: absolute;
z-index: 20;
top: calc(100% + 6px);
right: 0;
left: auto;
width: 238px;
max-width: calc(100vw - 24px);
box-sizing: border-box;
padding: .55rem;
border: 1px solid rgba(148, 163, 184, .55);
border-radius: 7px;
background: rgba(15, 23, 42, .99);
box-shadow: 0 14px 32px rgba(0, 0, 0, .5);
color: #e5e7eb;
}
.route-elevation-settings-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: .5rem;
margin-bottom: .45rem;
font-size: .72rem;
font-weight: 800;
}
.route-elevation-settings-close {
display: grid;
place-items: center;
width: 22px;
height: 22px; height: 22px;
padding: 0;
border: 0;
border-radius: 4px;
background: transparent;
color: #cbd5e1;
cursor: pointer;
font-size: 1rem;
line-height: 1;
}
.route-elevation-settings-close:hover {
background: rgba(71, 85, 105, .55);
color: #fff;
}
.route-elevation-settings-fields {
display: grid;
gap: .35rem;
}
.route-elevation-setting-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 44px 32px;
align-items: center;
gap: .35rem;
min-height: 26px;
color: #cbd5e1;
font-size: .68rem;
font-weight: 700;
}
.route-elevation-setting-row input {
width: 44px;
height: 22px;
box-sizing: border-box;
border: 1px solid rgba(148, 163, 184, .55); border: 1px solid rgba(148, 163, 184, .55);
border-radius: 6px; border-radius: 6px;
background: rgba(2, 6, 23, .78); background: rgba(2, 6, 23, .78);
@@ -909,15 +1056,27 @@ html, body {
outline: none; outline: none;
} }
.route-elevation-threshold input:focus { .route-elevation-setting-row input:focus {
border-color: rgba(56, 189, 248, .95); border-color: rgba(56, 189, 248, .95);
box-shadow: 0 0 0 2px rgba(56, 189, 248, .18); box-shadow: 0 0 0 2px rgba(56, 189, 248, .18);
} }
.route-elevation-threshold input:disabled { .route-elevation-setting-row input:disabled {
opacity: .55; opacity: .55;
} }
.route-slope-warning {
margin: 0 0 .45rem;
padding: .36rem .5rem;
border-left: 3px solid #f59e0b;
border-radius: 4px;
background: rgba(245, 158, 11, .13);
color: #fde68a;
font-size: .72rem;
font-weight: 800;
line-height: 1.25;
}
/* Lista de alternativas de ruta */ /* Lista de alternativas de ruta */
.route-alt-list { .route-alt-list {
@@ -1623,16 +1782,9 @@ html, body {
} }
.config-dual-labeled--equilibrado { .config-dual-labeled--equilibrado {
grid-template-columns: minmax(0, 4rem) minmax(0, 4.4rem); grid-template-columns: minmax(0, 4rem);
align-items: end; align-items: end;
} justify-content: start;
.config-equilibrado-help {
align-self: end;
padding-bottom: .12rem;
font-size: .58rem;
line-height: 1.08;
text-align: left;
} }
@@ -2837,6 +2989,7 @@ html, body {
.route-visual-options { .route-visual-options {
justify-content: flex-start; justify-content: flex-start;
flex-wrap: wrap;
gap: 6px 10px; gap: 6px 10px;
margin: 0 0 8px; margin: 0 0 8px;
} }
@@ -2846,14 +2999,27 @@ html, body {
font-size: .72rem; font-size: .72rem;
} }
.route-elevation-threshold { .route-elevation-settings-trigger {
min-height: 44px; width: 40px;
font-size: .68rem; height: 40px;
} }
.route-elevation-threshold input { .route-elevation-settings-popover {
right: 0;
left: auto;
width: 250px;
max-width: calc(100vw - 20px);
}
.route-elevation-setting-row {
grid-template-columns: minmax(0, 1fr) 48px 36px;
min-height: 40px;
font-size: .72rem;
}
.route-elevation-setting-row input {
width: 48px; width: 48px;
height: 40px; height: 36px;
font-size: .78rem; font-size: .78rem;
} }
@@ -3056,8 +3222,7 @@ html, body {
width: 100% !important; width: 100% !important;
} }
.config-dual-labeled__field span, .config-dual-labeled__field span {
.config-equilibrado-help {
font-size: .64rem; font-size: .64rem;
} }

View File

@@ -96,6 +96,7 @@ window.rt.ensureRouteLayer = function () {
window.rt._routeLayer = window.L.layerGroup().addTo(map); window.rt._routeLayer = window.L.layerGroup().addTo(map);
} }
if (!window.rt._routeLines) window.rt._routeLines = {}; if (!window.rt._routeLines) window.rt._routeLines = {};
if (!window.rt._routeCoords) window.rt._routeCoords = {};
return true; return true;
}; };
@@ -103,6 +104,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 = {};
window.rt._routeCoords = {};
if (window.rt._routeStopsLayer) { if (window.rt._routeStopsLayer) {
window.rt._routeStopsLayer.clearLayers(); window.rt._routeStopsLayer.clearLayers();
@@ -123,6 +125,19 @@ window.rt.drawRoute = function (id, coords, options) {
delete window.rt._routeLines[arrowId]; delete window.rt._routeLines[arrowId];
} }
const routeCoords = Array.isArray(coords)
? coords
.map(function (point) {
return [Number(point && point[0]), Number(point && point[1])];
})
.filter(function (point) {
return Number.isFinite(point[0]) && Number.isFinite(point[1]);
})
: [];
if (routeCoords.length > 0) window.rt._routeCoords[id] = routeCoords;
else delete window.rt._routeCoords[id];
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;
@@ -307,14 +322,53 @@ window.rt.drawRoute = function (id, coords, options) {
} }
}; };
window.rt.fitRoutes = function () { window.rt.fitRoutes = function (coords) {
const map = window.__rtLeafletMap; const map = window.__rtLeafletMap;
if (!map || !window.rt._routeLayer) return; if (!map || !window.L) return false;
const bounds = window.rt._routeLayer.getBounds(); const routePoints = [];
if (bounds && bounds.isValid()) { if (Array.isArray(coords)) {
map.fitBounds(bounds, { padding: [30, 30] }); coords.forEach(function (point) {
routePoints.push(point);
});
} else {
if (!window.rt._routeCoords) return false;
Object.values(window.rt._routeCoords).forEach(function (routeCoords) {
if (!Array.isArray(routeCoords)) return;
routeCoords.forEach(function (point) {
routePoints.push(point);
});
});
} }
const points = routePoints
.filter(function (point) {
return Array.isArray(point)
&& point.length >= 2
&& Number.isFinite(Number(point[0]))
&& Number.isFinite(Number(point[1]));
})
.map(function (point) {
return [Number(point[0]), Number(point[1])];
});
if (points.length === 0) return false;
const visibleBounds = map.getBounds();
const algunPuntoVisible = visibleBounds && points.some(function (point) {
return visibleBounds.contains(window.L.latLng(point[0], point[1]));
});
if (algunPuntoVisible) return false;
const geometryBounds = window.L.latLngBounds(points);
if (!geometryBounds.isValid()) return false;
map.panTo(geometryBounds.getCenter(), {
animate: true,
duration: 0.35
});
return true;
}; };
window.rt = window.rt || {}; window.rt = window.rt || {};
@@ -1084,6 +1138,131 @@ window.rt.clearEndpointMarkers = function (kind) {
}; };
// -----------------------------------------------------------------------------
// Punto Z y lugares cercanos del callejero
// -----------------------------------------------------------------------------
window.rt.ensureInspectorLayer = function () {
const map = window.__rtLeafletMap;
if (!map || !window.L) return false;
if (!map.getPane("rtInspectorPane")) {
const pane = map.createPane("rtInspectorPane");
pane.style.zIndex = 790;
}
if (!window.rt._inspectorLayer) {
window.rt._inspectorLayer = window.L.layerGroup().addTo(map);
}
return true;
};
window.rt.setInspectorMarkers = function (lat, lon, places) {
if (!window.rt.ensureInspectorLayer()) return;
window.rt._inspectorLayer.clearLayers();
const zLat = Number(lat);
const zLon = Number(lon);
if (!Number.isFinite(zLat) || !Number.isFinite(zLon)) return;
const puntoZ = window.L.circleMarker([zLat, zLon], {
pane: "rtInspectorPane",
radius: 12,
color: "#713f12",
weight: 3,
opacity: 1,
fillColor: "#fde047",
fillOpacity: 0.42,
bubblingMouseEvents: false
});
puntoZ.bindTooltip("Punto Z", {
direction: "top",
opacity: 0.95,
className: "rt-inspector-tooltip"
});
puntoZ.addTo(window.rt._inspectorLayer);
const lugares = Array.isArray(places) ? places.slice(0, 3) : [];
const coincidencias = {};
lugares.forEach(function (place) {
const placeLat = Number(place && place.latitud);
const placeLon = Number(place && place.longitud);
if (!Number.isFinite(placeLat) || !Number.isFinite(placeLon)) return;
const clave = `${placeLat.toFixed(6)}|${placeLon.toFixed(6)}`;
coincidencias[clave] = (coincidencias[clave] || 0) + 1;
});
const posicionCoincidente = {};
lugares.forEach(function (place, index) {
const placeLat = Number(place && place.latitud);
const placeLon = Number(place && place.longitud);
if (!Number.isFinite(placeLat) || !Number.isFinite(placeLon)) return;
const clave = `${placeLat.toFixed(6)}|${placeLon.toFixed(6)}`;
const totalCoincidentes = coincidencias[clave] || 1;
const posicion = posicionCoincidente[clave] || 0;
posicionCoincidente[clave] = posicion + 1;
let offsetX = 0;
if (totalCoincidentes === 2)
offsetX = posicion === 0 ? -14 : 14;
else if (totalCoincidentes >= 3)
offsetX = [-26, 0, 26][Math.min(posicion, 2)];
window.L.polyline(
[[zLat, zLon], [placeLat, placeLon]],
{
pane: "rtInspectorPane",
color: "#0f766e",
weight: 1.5,
opacity: 0.75,
dashArray: "4 5",
interactive: false
})
.addTo(window.rt._inspectorLayer);
const numero = index + 1;
const size = 24;
const icon = window.L.divIcon({
className: "rt-inspector-place-icon",
html: `<span class="rt-inspector-place-marker">${numero}</span>`,
iconSize: [size, size],
iconAnchor: [(size / 2) - offsetX, size / 2]
});
const texto = place && typeof place.texto === "string"
? place.texto
: `Lugar ${numero}`;
const marker = window.L.marker([placeLat, placeLon], {
pane: "rtInspectorPane",
icon,
title: texto,
keyboard: false,
bubblingMouseEvents: false,
zIndexOffset: 20 + numero
});
const tooltip = document.createElement("span");
tooltip.textContent = texto;
marker.bindTooltip(tooltip, {
direction: "top",
offset: [offsetX, -12],
opacity: 0.95,
className: "rt-inspector-tooltip"
});
marker.addTo(window.rt._inspectorLayer);
});
};
window.rt.clearInspectorMarkers = function () {
if (window.rt._inspectorLayer) {
window.rt._inspectorLayer.clearLayers();
}
};
window.rt = window.rt || {}; window.rt = window.rt || {};
window.rt.scrollToHorarioProximo = function () { window.rt.scrollToHorarioProximo = function () {