- 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:
@@ -7,14 +7,14 @@
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.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" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<script src="app.js?v=20260716m3" defer></script>
|
||||
<script src="app.js?v=20260720h" defer></script>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text.RegularExpressions;
|
||||
using LeafletForBlazor;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using RutasDBUS.Modelos.Geocodificacion;
|
||||
using RutasDBUS.Modelos.Planificacion;
|
||||
using RutasDBUS.Modelos.RedBus;
|
||||
using RutasDBUS.Servicios;
|
||||
@@ -174,6 +175,8 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
|
||||
if (it is null) return;
|
||||
|
||||
await PintarItItemAsync(it);
|
||||
var coordenadas = ConstruirCoordenadasItinerario(it);
|
||||
await FitRoutesJsAsync(coordenadas);
|
||||
}
|
||||
/// <summary>
|
||||
/// Quita it seleccionado.
|
||||
@@ -254,7 +257,8 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
|
||||
private double _inspectorLat, _inspectorLon;
|
||||
private double? _inspectorElev;
|
||||
private double? _inspectorElevIdee;
|
||||
private string? _inspectorCallejero;
|
||||
private IReadOnlyList<LugarGeocodificadoCercano> _inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
|
||||
private int _versionInspector;
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -47,16 +47,22 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
private int _nivelBrilloMapa = 0;
|
||||
|
||||
// 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 _factorPenalizacionDistanciaPie = 1.6;
|
||||
private double _factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
|
||||
private double _parametroPenalizacionDistanciaPieMetros = 100.0;
|
||||
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 _factorPenalizacionDistanciaPieTextoEdicion = "1.35";
|
||||
private string _parametroPenalizacionDistanciaPieTextoEdicion = "100";
|
||||
private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20";
|
||||
private string _coeficienteEsfuerzoAlturaTextoEdicion = "8.20";
|
||||
private string _pesoTransbordoRankingTextoEdicion = "4,0";
|
||||
private string _factorEquilibradoAndarBusTextoEdicion = "12";
|
||||
private double _velocidadBusKmH = 16.0;
|
||||
@@ -222,10 +228,11 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
? _coeficienteEsfuerzoAltura
|
||||
: CoeficienteEsfuerzoAlturaPorDefecto;
|
||||
|
||||
var pendiente = pendientePorcentaje.Value / 100.0;
|
||||
if (pendiente <= 0)
|
||||
var umbralSubida = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta);
|
||||
if (pendientePorcentaje.Value < umbralSubida)
|
||||
return distanciaMetros.Value;
|
||||
|
||||
var pendiente = pendientePorcentaje.Value / 100.0;
|
||||
var factorConPotencia = Math.Pow(1.0 + pendiente, coeficiente);
|
||||
var esfuerzo = distanciaMetros.Value * factorConPotencia;
|
||||
return double.IsFinite(esfuerzo)
|
||||
@@ -351,9 +358,18 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
private TimeSpan? _tiempoUltimoCalculoRuta = null;
|
||||
private bool _mostrarElevacionesRuta = false;
|
||||
private bool _pintandoElevacionesRuta = false;
|
||||
private bool _mostrarConfiguracionPendientesRuta = false;
|
||||
private bool _panelRutasMovilContraido = false;
|
||||
private double _umbralPendienteSubidaRuta = 3.0;
|
||||
private double _umbralPendienteBajadaRuta = 3.0;
|
||||
private double _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
|
||||
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>> _paradasRutaVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -987,9 +1003,16 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
/// <summary>
|
||||
/// Ajusta rutas js.
|
||||
/// </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>
|
||||
@@ -1043,7 +1066,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
{
|
||||
if (d <= 0) return "-";
|
||||
if (d < 1000) return $"{Math.Truncate(d):0} m";
|
||||
return $"{(d / 1000.0):0.00} km";
|
||||
return $"{(d / 1000.0):0.000} km";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1206,18 +1229,35 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
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)
|
||||
{
|
||||
if (alternativa is null || alternativa.EsSoloAPie || alternativa.AlternativaBus is null)
|
||||
return double.PositiveInfinity;
|
||||
|
||||
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);
|
||||
var distanciaEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(alternativa);
|
||||
var score = distanciaBus + distanciaEquilibradaAndando;
|
||||
return double.IsFinite(score) && score >= 0
|
||||
? score
|
||||
: double.PositiveInfinity;
|
||||
@@ -1232,9 +1272,6 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
return $"Score {FormatearDistanciaTarjeta(score)} (x{_factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture)})";
|
||||
}
|
||||
|
||||
private string FactorEquilibradoAndarBusResumen
|
||||
=> _factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture);
|
||||
|
||||
private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync()
|
||||
{
|
||||
if (!CriterioPrioritarioEquilibradoActivo)
|
||||
@@ -1572,6 +1609,8 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
_hayRutaValida = false;
|
||||
_tramosRuta.Clear();
|
||||
_duracionTotalSegundos = 0;
|
||||
_alturaPendientePositivaSignificativaRutaMetros = 0.0;
|
||||
_distanciaPendientePositivaSignificativaRutaMetros = 0.0;
|
||||
_altitudOrigenResumenRuta = null;
|
||||
_altitudDestinoResumenRuta = null;
|
||||
_callejeroOrigenResumenRuta = null;
|
||||
|
||||
@@ -76,7 +76,7 @@ public partial class PlanificadorRutas
|
||||
try
|
||||
{
|
||||
await Task.Delay(120, ct);
|
||||
var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 10, ct);
|
||||
var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 20, ct);
|
||||
|
||||
if (ct.IsCancellationRequested || version != _versionSugerenciasBusqueda)
|
||||
return;
|
||||
@@ -118,6 +118,10 @@ public partial class PlanificadorRutas
|
||||
_hayRutaValida = false;
|
||||
|
||||
await SincronizarMarcadoresOrigenDestinoJsAsync();
|
||||
await FitRoutesJsAsync(new List<double[]>
|
||||
{
|
||||
new[] { sugerencia.Latitud, sugerencia.Longitud }
|
||||
});
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
|
||||
@@ -232,6 +232,9 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
/// </summary>
|
||||
private async Task PintarItFiltro()
|
||||
{
|
||||
if (!PuedePintarItinerariosFiltrados)
|
||||
return;
|
||||
|
||||
foreach (var it in _itItemsFiltrados)
|
||||
{
|
||||
if (it is null) continue;
|
||||
@@ -290,6 +293,23 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
// Detecta si el click cae realmente sobre el círculo de una parada (en píxeles)
|
||||
@@ -609,6 +629,8 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
|
||||
|
||||
private string _itFiltro = "";
|
||||
private bool PuedePintarItinerariosFiltrados => !string.IsNullOrWhiteSpace(_itFiltro);
|
||||
|
||||
private string ItFiltro
|
||||
{
|
||||
get => _itFiltro;
|
||||
@@ -1182,7 +1204,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
private bool _elevPopupMinimizado = false;
|
||||
|
||||
|
||||
private const string _versionApp = "(v-20260716a)";
|
||||
private const string _versionApp = "(v-20260720c)";
|
||||
|
||||
|
||||
private double? _paradaElevG;
|
||||
@@ -1200,7 +1222,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
_itinerarioClicado = null;
|
||||
|
||||
_paradaClicada = null;
|
||||
_popupInspectorVisible = false;
|
||||
await CerrarInspectorAsync();
|
||||
|
||||
if (_tramoElevDetalle is not null)
|
||||
{
|
||||
|
||||
@@ -18,6 +18,38 @@ public partial class PlanificadorRutas
|
||||
private ModeloHorarioDbusInfo? ModeloHorarioActivo =>
|
||||
_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()
|
||||
{
|
||||
_mostrarModelosHorariosPanel = true;
|
||||
|
||||
@@ -38,6 +38,7 @@ public partial class PlanificadorRutas
|
||||
public double? FactorEquilibradoAndarBus { get; set; }
|
||||
public double? UmbralPendienteSubidaRuta { get; set; }
|
||||
public double? UmbralPendienteBajadaRuta { get; set; }
|
||||
public double? UmbralPisosPendienteRuta { get; set; }
|
||||
public double VelocidadPieKmH { get; set; }
|
||||
public double VelocidadBusKmH { get; set; }
|
||||
public int ElevMaxPuntosPorTramo { get; set; }
|
||||
@@ -140,6 +141,7 @@ public partial class PlanificadorRutas
|
||||
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
|
||||
UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta,
|
||||
UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta,
|
||||
UmbralPisosPendienteRuta = _umbralPisosPendienteRuta,
|
||||
VelocidadPieKmH = _velocidadPieKmH,
|
||||
VelocidadBusKmH = _velocidadBusKmH,
|
||||
ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo,
|
||||
@@ -185,11 +187,17 @@ public partial class PlanificadorRutas
|
||||
_horaSimulada = horaSimulada;
|
||||
|
||||
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))
|
||||
_parametroPenalizacionDistanciaPieMetros = parametro;
|
||||
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
|
||||
: coeficienteAltura;
|
||||
if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo))
|
||||
@@ -197,9 +205,17 @@ public partial class PlanificadorRutas
|
||||
if (config.FactorEquilibradoAndarBus is { } factorEquilibrado && double.IsFinite(factorEquilibrado))
|
||||
_factorEquilibradoAndarBus = Math.Max(0, factorEquilibrado);
|
||||
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))
|
||||
_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);
|
||||
_velocidadBusKmH = Math.Clamp(
|
||||
Math.Abs(config.VelocidadBusKmH - 15.0) < 0.0001 ||
|
||||
@@ -298,13 +314,14 @@ public partial class PlanificadorRutas
|
||||
_minTransbordosPermitidos = 0;
|
||||
_maxTransbordosPermitidos = 3;
|
||||
|
||||
_factorPenalizacionDistanciaPie = 1.6;
|
||||
_factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
|
||||
_parametroPenalizacionDistanciaPieMetros = 100.0;
|
||||
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
||||
_pesoTransbordoRankingMinutos = 4.0;
|
||||
_factorEquilibradoAndarBus = 12.0;
|
||||
_umbralPendienteSubidaRuta = 3.0;
|
||||
_umbralPendienteBajadaRuta = 3.0;
|
||||
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
|
||||
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
|
||||
_umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto;
|
||||
_velocidadPieKmH = 3.6;
|
||||
_velocidadBusKmH = 16.0;
|
||||
_minutosTransbordo = 5;
|
||||
@@ -347,13 +364,14 @@ public partial class PlanificadorRutas
|
||||
_minTransbordosPermitidos = 0;
|
||||
_maxTransbordosPermitidos = 3;
|
||||
|
||||
_factorPenalizacionDistanciaPie = 1.6;
|
||||
_factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
|
||||
_parametroPenalizacionDistanciaPieMetros = 100.0;
|
||||
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
||||
_pesoTransbordoRankingMinutos = 4.0;
|
||||
_factorEquilibradoAndarBus = 12.0;
|
||||
_umbralPendienteSubidaRuta = 3.0;
|
||||
_umbralPendienteBajadaRuta = 3.0;
|
||||
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
|
||||
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
|
||||
_umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto;
|
||||
_velocidadPieKmH = 3.6;
|
||||
_velocidadBusKmH = 16.0;
|
||||
_minutosTransbordo = 5;
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text.RegularExpressions;
|
||||
using LeafletForBlazor;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using RutasDBUS.Modelos.Geocodificacion;
|
||||
using RutasDBUS.Modelos.Planificacion;
|
||||
using RutasDBUS.Modelos.RedBus;
|
||||
using RutasDBUS.Servicios;
|
||||
@@ -1267,10 +1268,26 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
/// </summary>
|
||||
private async Task MostrarInspectorAsync(double lat, double lon)
|
||||
{
|
||||
var versionInspector = ++_versionInspector;
|
||||
_inspectorLat = lat;
|
||||
_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
|
||||
{
|
||||
// -------------------------
|
||||
@@ -1279,7 +1296,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
var kOpen = ElevKey(lat, lon);
|
||||
if (_elevCache.TryGetValue(kOpen, out var cachedOpen))
|
||||
{
|
||||
_inspectorElev = cachedOpen;
|
||||
elevacionOpen = cachedOpen;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1287,7 +1304,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
new List<(double lat, double lon)> { (lat, lon) },
|
||||
CancellationToken.None);
|
||||
|
||||
_inspectorElev = _elevCache.TryGetValue(ElevKey(lat, lon), out var elevOpen)
|
||||
elevacionOpen = _elevCache.TryGetValue(ElevKey(lat, lon), out var elevOpen)
|
||||
? elevOpen
|
||||
: (double?)null;
|
||||
}
|
||||
@@ -1298,7 +1315,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
var kIdee = ClaveElevacionIdee(lat, lon);
|
||||
if (_elevIdeeCache.TryGetValue(kIdee, out var cachedIdee))
|
||||
{
|
||||
_inspectorElevIdee = cachedIdee;
|
||||
elevacionIdee = cachedIdee;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1306,34 +1323,73 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
new List<(double lat, double lon)> { (lat, lon) },
|
||||
CancellationToken.None);
|
||||
|
||||
_inspectorElevIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var elevIdee)
|
||||
elevacionIdee = _elevIdeeCache.TryGetValue(ClaveElevacionIdee(lat, lon), out var elevIdee)
|
||||
? elevIdee
|
||||
: (double?)null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_inspectorElev = null;
|
||||
_inspectorElevIdee = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var lugar = await GeocodificadorLocal.BuscarMasCercanoAsync(
|
||||
lat,
|
||||
lon,
|
||||
CancellationToken.None);
|
||||
_inspectorCallejero = FormatearLugarCallejeroResumen(lugar);
|
||||
var lugares = await tareaLugares;
|
||||
if (versionInspector != _versionInspector)
|
||||
return;
|
||||
|
||||
_inspectorLugaresCercanos = lugares;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_inspectorCallejero = null;
|
||||
if (versionInspector != _versionInspector)
|
||||
return;
|
||||
|
||||
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
|
||||
}
|
||||
|
||||
_popupInspectorVisible = true;
|
||||
if (versionInspector != _versionInspector)
|
||||
return;
|
||||
|
||||
_inspectorElev = elevacionOpen;
|
||||
_inspectorElevIdee = elevacionIdee;
|
||||
await ActualizarMarcadoresInspectorJsAsync();
|
||||
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>
|
||||
/// Gestiona hacer clic en mapa.
|
||||
|
||||
@@ -77,7 +77,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
};
|
||||
|
||||
await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta
|
||||
? "Aplicando elevaciones..."
|
||||
? "Aplicando pendientes..."
|
||||
: "Representando ruta seleccionada...");
|
||||
}
|
||||
|
||||
@@ -96,11 +96,21 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
await PersistirConfiguracionAsync();
|
||||
|
||||
if (_mostrarElevacionesRuta)
|
||||
await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando elevaciones...");
|
||||
await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando pendientes...");
|
||||
else
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task CambiarUmbralPisosPendienteRutaAsync(ChangeEventArgs e)
|
||||
{
|
||||
_umbralPisosPendienteRuta = ParseUmbralPisosPendienteRuta(
|
||||
e.Value,
|
||||
_umbralPisosPendienteRuta);
|
||||
|
||||
await PersistirConfiguracionAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task RepintarRutaSeleccionadaPorElevacionesAsync(string mensaje)
|
||||
{
|
||||
if (_opcionesRuta.Count == 0 ||
|
||||
@@ -111,6 +121,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
return;
|
||||
}
|
||||
|
||||
ReiniciarResumenPendientesRuta();
|
||||
_pintandoElevacionesRuta = true;
|
||||
_mensajeRuta = mensaje;
|
||||
StateHasChanged();
|
||||
@@ -143,7 +154,24 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
private static double NormalizarUmbralPendienteRuta(double valor)
|
||||
=> double.IsFinite(valor)
|
||||
? 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(
|
||||
string id,
|
||||
@@ -152,9 +180,17 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
int versionPintado,
|
||||
bool arrowEnd = true)
|
||||
{
|
||||
var segmentosElevacion = await ConstruirSegmentosElevacionMapaAsync(linea, CancellationToken.None);
|
||||
var resultadoElevacion = await ConstruirSegmentosElevacionMapaAsync(linea, CancellationToken.None);
|
||||
ThrowIfPintadoObsoleto(versionPintado);
|
||||
|
||||
if (_mostrarElevacionesRuta)
|
||||
{
|
||||
_alturaPendientePositivaSignificativaRutaMetros +=
|
||||
resultadoElevacion.AlturaPositivaSignificativaMetros;
|
||||
_distanciaPendientePositivaSignificativaRutaMetros +=
|
||||
resultadoElevacion.DistanciaPositivaSignificativaMetros;
|
||||
}
|
||||
|
||||
await DrawRouteJsAsync(
|
||||
id,
|
||||
linea,
|
||||
@@ -164,20 +200,25 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
dashArray: DashArrayPiePunteado,
|
||||
arrowEnd: arrowEnd,
|
||||
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)
|
||||
return null;
|
||||
return (null, 0.0, 0.0);
|
||||
|
||||
var muestras = await ConstruirMuestrasElevacionAsync(linea, ct);
|
||||
if (muestras.Count < 2)
|
||||
return null;
|
||||
return (null, 0.0, 0.0);
|
||||
|
||||
var segmentos = new List<object>();
|
||||
var hayPendienteCalculada = false;
|
||||
var totalesPendientePositiva = CalcularTotalesPendientePositivaSignificativa(muestras);
|
||||
|
||||
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)
|
||||
{
|
||||
pendiente = (elevActual.Value - elevAnterior.Value) / distancia * 100.0;
|
||||
var diferenciaAltura = elevActual.Value - elevAnterior.Value;
|
||||
pendiente = diferenciaAltura / distancia * 100.0;
|
||||
hayPendienteCalculada = true;
|
||||
}
|
||||
|
||||
@@ -224,9 +266,64 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
});
|
||||
}
|
||||
|
||||
return hayPendienteCalculada && segmentos.Count > 0
|
||||
? segmentos
|
||||
: null;
|
||||
return (
|
||||
hayPendienteCalculada && segmentos.Count > 0 ? segmentos : 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)
|
||||
@@ -259,6 +356,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
{
|
||||
var versionPintado = IniciarNuevaVersionPintadoRuta();
|
||||
var mensajeAnterior = _mensajeRuta;
|
||||
ReiniciarResumenPendientesRuta();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -60,7 +60,9 @@
|
||||
|
||||
<button class="btn-ghost" @onclick="PintarItSeleccionado">Ver</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>
|
||||
|
||||
@@ -344,11 +346,6 @@
|
||||
<div class="popup-parada-body">
|
||||
<div><b>Latitud:</b> @_inspectorLat.ToString("0.000000")</div>
|
||||
<div><b>Longitud:</b> @_inspectorLon.ToString("0.000000")</div>
|
||||
@if (!string.IsNullOrWhiteSpace(_inspectorCallejero))
|
||||
{
|
||||
<div><b>Callejero:</b> @_inspectorCallejero</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<b>Altitud (O):</b>
|
||||
@(_inspectorElev is null
|
||||
@@ -362,9 +359,23 @@
|
||||
? "-"
|
||||
: _inspectorElevIdee.Value.ToString("0.00", CultureInfo.InvariantCulture) + " m")
|
||||
</div>
|
||||
|
||||
@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="() => _popupInspectorVisible = false">×</button>
|
||||
<button class="popup-parada-cerrar" @onclick="CerrarInspectorAsync">×</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -427,6 +438,11 @@
|
||||
}
|
||||
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="
|
||||
width:100%;
|
||||
font-size:0.8rem;
|
||||
@@ -545,10 +561,12 @@
|
||||
double totalDist = 0;
|
||||
double totalEsfuerzoPositivo = 0;
|
||||
double totalEsfuerzo = 0;
|
||||
double totalDistPendPositiva = 0;
|
||||
double totalAlturaPendPositiva = 0;
|
||||
var totalesPendientePositiva = CalcularTotalesPendientePositivaSignificativa(s);
|
||||
double totalDistPendPositiva = totalesPendientePositiva.DistanciaMetros;
|
||||
double totalAlturaPendPositiva = totalesPendientePositiva.AlturaMetros;
|
||||
var hayEsfuerzoPositivo = false;
|
||||
var hayEsfuerzo = false;
|
||||
var umbralPendientePositiva = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta);
|
||||
for (int i = 1; i < s.Count; i++)
|
||||
{
|
||||
var anterior = s[i - 1];
|
||||
@@ -560,12 +578,6 @@
|
||||
if (distancia > 0 && anterior.ElevacionIdee is not null && actual.ElevacionIdee is not null)
|
||||
{
|
||||
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);
|
||||
@@ -574,7 +586,7 @@
|
||||
totalEsfuerzo += esfuerzo.Value;
|
||||
hayEsfuerzo = true;
|
||||
|
||||
if (pendiente > 0)
|
||||
if (pendiente > umbralPendientePositiva)
|
||||
{
|
||||
totalEsfuerzoPositivo += esfuerzo.Value;
|
||||
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><b>O:</b> (ini→fin) = @F(OIni) → @F(OFin) · (min/max) = @F(OMin) / @F(OMax)</div>
|
||||
<div><b>G:</b> (ini→fin) = @F(GIni) → @F(GFin) · (min/max) = @F(GMin) / @F(GMax)</div>
|
||||
<div><b>O:</b> (inicio → fin) = @F(OIni) → @F(OFin) · (mínimo/máximo) = @F(OMin) / @F(OMax)</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;">
|
||||
<b>Suma distancia de puntos:</b> @FDist(totalDist)
|
||||
<b>Suma de distancias entre puntos:</b> @FormatearDistanciaResumenElevacion(totalDist)
|
||||
</div>
|
||||
<div>
|
||||
<b>Suma dist pend positiva:</b> @FDist(totalDistPendPositiva)
|
||||
<b>Suma de distancias con pendiente positiva:</b> @FormatearDistanciaResumenElevacion(totalDistPendPositiva)
|
||||
</div>
|
||||
<div>
|
||||
<b>Suma alturas pend positiva:</b> @(totalAlturaPendPositiva > 0
|
||||
? totalAlturaPendPositiva.ToString("0.00", CultureInfo.InvariantCulture) + " m (" + (totalAlturaPendPositiva / 3.0).ToString("0.0", CultureInfo.InvariantCulture) + " pisos)"
|
||||
<b>Suma de alturas con pendiente positiva:</b> @(totalAlturaPendPositiva > 0
|
||||
? totalAlturaPendPositiva.ToString("0.00", CultureInfo.InvariantCulture) + " m (" + (totalAlturaPendPositiva / MetrosPorPiso).ToString("0.0", CultureInfo.InvariantCulture) + " pisos)"
|
||||
: "-")
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
}
|
||||
@@ -670,33 +673,74 @@
|
||||
<span class="route-elevation-switch-thumb"></span>
|
||||
</span>
|
||||
<span class="route-elevation-switch-text">
|
||||
@(_pintandoElevacionesRuta ? "Elevaciones..." : "Elevaciones")
|
||||
Visualizar Pendientes
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="route-elevation-thresholds" title="Porcentaje minimo para colorear subida o bajada">
|
||||
<label class="route-elevation-threshold">
|
||||
<span>Sube</span>
|
||||
<input type="number"
|
||||
min="0"
|
||||
max="50"
|
||||
step="0.5"
|
||||
<div class="route-elevation-settings">
|
||||
<button type="button"
|
||||
class="route-elevation-settings-trigger @(_mostrarConfiguracionPendientesRuta ? "route-elevation-settings-trigger--active" : "")"
|
||||
title="Configurar pendientes"
|
||||
aria-label="Configurar pendientes"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded="@_mostrarConfiguracionPendientesRuta"
|
||||
disabled="@_pintandoElevacionesRuta"
|
||||
@onclick="() => _mostrarConfiguracionPendientesRuta = !_mostrarConfiguracionPendientesRuta">
|
||||
<span aria-hidden="true">⚙</span>
|
||||
</button>
|
||||
|
||||
@if (_mostrarConfiguracionPendientesRuta)
|
||||
{
|
||||
<div class="route-elevation-settings-popover"
|
||||
role="dialog"
|
||||
aria-label="Umbrales de pendientes"
|
||||
@onclick:stopPropagation="true">
|
||||
<div class="route-elevation-settings-header">
|
||||
<span>Configurar Pendientes</span>
|
||||
<button type="button"
|
||||
class="route-elevation-settings-close"
|
||||
title="Cerrar"
|
||||
aria-label="Cerrar configuración de pendientes"
|
||||
@onclick="() => _mostrarConfiguracionPendientesRuta = false">
|
||||
<span aria-hidden="true">×</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-threshold">
|
||||
<span>Baja</span>
|
||||
<input type="number"
|
||||
min="0"
|
||||
max="50"
|
||||
step="0.5"
|
||||
<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>
|
||||
|
||||
@@ -768,6 +812,20 @@
|
||||
{
|
||||
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) ========= *@
|
||||
<p class="route-main-text">
|
||||
@if (alt.EsSoloAPie)
|
||||
@@ -928,6 +986,7 @@
|
||||
: _duracionTotalSegundos;
|
||||
var dRecta = DistanciaLineaRectaOrigenDestino();
|
||||
var dCorregida = CalcularDistanciaCorregidaAlternativa(altSeleccionada);
|
||||
var dEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(altSeleccionada);
|
||||
var xyzOrigen = FormatearResumenXYZOrigen();
|
||||
var xyzDestino = FormatearResumenXYZDestino();
|
||||
var callejeroOrigen = FormatearResumenCallejeroOrigen();
|
||||
@@ -948,7 +1007,7 @@
|
||||
</p>
|
||||
}
|
||||
|
||||
@if (dRecta > 0 || dCorregida > 0)
|
||||
@if (dRecta > 0 || dCorregida > 0 || dEquilibradaAndando > 0)
|
||||
{
|
||||
<div class="muted" style="font-size:0.75rem;margin-top:4px;">
|
||||
@if (dRecta > 0)
|
||||
@@ -959,6 +1018,10 @@
|
||||
{
|
||||
<div>d.Corregida a pie: @FormatearDistanciaDetalleRuta(dCorregida)</div>
|
||||
}
|
||||
@if (dEquilibradaAndando > 0)
|
||||
{
|
||||
<div>d.Equilib. A: @FormatearDistanciaDetalleRuta(dEquilibradaAndando)</div>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(scoreEquilibradoSeleccionado))
|
||||
{
|
||||
<div>Score equilibrado: @scoreEquilibradoSeleccionado</div>
|
||||
@@ -1177,6 +1240,22 @@
|
||||
<button type="button" class="config-close-btn" @onclick="ToggleConfigPanel">×</button>
|
||||
</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">
|
||||
<label>Selección de paradas (rápida):</label>
|
||||
|
||||
@@ -1254,22 +1333,6 @@
|
||||
</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
|
||||
========================= *@
|
||||
@@ -1470,13 +1533,10 @@
|
||||
<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"
|
||||
title="Factor del modo equilibrado"
|
||||
@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>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class ParametrosPlanificadorRuta
|
||||
public int MinutosPorTransbordo { get; set; } = 5;
|
||||
public int MargenTransbordoSegundos { get; set; } = 120;
|
||||
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 PesoTransbordoRankingMinutos { get; set; } = 4.0;
|
||||
public double FactorEquilibradoAndarBus { get; set; } = 12.0;
|
||||
|
||||
@@ -13,6 +13,7 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio
|
||||
private readonly IWebHostEnvironment _entorno;
|
||||
private readonly SemaphoreSlim _semaforoCarga = new(1, 1);
|
||||
private readonly ConcurrentDictionary<string, LugarGeocodificadoCercano> _cacheCercanos = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, IReadOnlyList<LugarGeocodificadoCercano>> _cacheCercanosPorRadio = new(StringComparer.Ordinal);
|
||||
private IReadOnlyList<EntradaGeocodificada>? _indice;
|
||||
|
||||
public GeocodificadorLocalServicio(IWebHostEnvironment entorno)
|
||||
@@ -109,6 +110,73 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio
|
||||
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)
|
||||
{
|
||||
if (_indice is not null)
|
||||
@@ -411,6 +479,9 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio
|
||||
private static string ClaveCercano(double latitud, double longitud)
|
||||
=> $"{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)
|
||||
{
|
||||
const double radioTierraMetros = 6371000.0;
|
||||
|
||||
@@ -14,5 +14,12 @@ public interface IGeocodificadorLocalServicio
|
||||
double longitud,
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ internal static class UtilidadesTransitoDbus
|
||||
|
||||
var factor = double.IsFinite(parametros.FactorPenalizacionDistanciaPie)
|
||||
? parametros.FactorPenalizacionDistanciaPie
|
||||
: 1.6;
|
||||
: 1.35;
|
||||
var parametroBaseMetros = double.IsFinite(parametros.ParametroPenalizacionDistanciaPieMetros)
|
||||
? Math.Abs(parametros.ParametroPenalizacionDistanciaPieMetros)
|
||||
: 100.0;
|
||||
|
||||
@@ -13,6 +13,63 @@
|
||||
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 {
|
||||
display: inline-block;
|
||||
font-size: 0.9rem;
|
||||
@@ -247,6 +304,14 @@ h1:focus {
|
||||
background: #1d4ed8;
|
||||
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 {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -810,9 +875,9 @@ html, body {
|
||||
.route-visual-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: .35rem .5rem;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
gap: .35rem;
|
||||
margin: -.15rem 0 .4rem;
|
||||
}
|
||||
|
||||
@@ -877,27 +942,109 @@ html, body {
|
||||
opacity: .55;
|
||||
}
|
||||
|
||||
.route-elevation-thresholds {
|
||||
.route-elevation-settings {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .25rem;
|
||||
margin-left: auto;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.route-elevation-threshold {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .18rem;
|
||||
min-height: 24px;
|
||||
.route-elevation-settings-trigger {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 26px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(148, 163, 184, .55);
|
||||
border-radius: 6px;
|
||||
background: rgba(2, 6, 23, .78);
|
||||
color: #cbd5e1;
|
||||
font-size: .64rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
font-family: "Segoe UI Symbol", sans-serif;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.route-elevation-threshold input {
|
||||
width: 40px;
|
||||
.route-elevation-settings-trigger:hover,
|
||||
.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;
|
||||
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-radius: 6px;
|
||||
background: rgba(2, 6, 23, .78);
|
||||
@@ -909,15 +1056,27 @@ html, body {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.route-elevation-threshold input:focus {
|
||||
.route-elevation-setting-row input:focus {
|
||||
border-color: rgba(56, 189, 248, .95);
|
||||
box-shadow: 0 0 0 2px rgba(56, 189, 248, .18);
|
||||
}
|
||||
|
||||
.route-elevation-threshold input:disabled {
|
||||
.route-elevation-setting-row input:disabled {
|
||||
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 */
|
||||
|
||||
.route-alt-list {
|
||||
@@ -1623,16 +1782,9 @@ html, body {
|
||||
}
|
||||
|
||||
.config-dual-labeled--equilibrado {
|
||||
grid-template-columns: minmax(0, 4rem) minmax(0, 4.4rem);
|
||||
grid-template-columns: minmax(0, 4rem);
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.config-equilibrado-help {
|
||||
align-self: end;
|
||||
padding-bottom: .12rem;
|
||||
font-size: .58rem;
|
||||
line-height: 1.08;
|
||||
text-align: left;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
|
||||
@@ -2837,6 +2989,7 @@ html, body {
|
||||
|
||||
.route-visual-options {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 10px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
@@ -2846,14 +2999,27 @@ html, body {
|
||||
font-size: .72rem;
|
||||
}
|
||||
|
||||
.route-elevation-threshold {
|
||||
min-height: 44px;
|
||||
font-size: .68rem;
|
||||
.route-elevation-settings-trigger {
|
||||
width: 40px;
|
||||
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;
|
||||
height: 40px;
|
||||
height: 36px;
|
||||
font-size: .78rem;
|
||||
}
|
||||
|
||||
@@ -3056,8 +3222,7 @@ html, body {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.config-dual-labeled__field span,
|
||||
.config-equilibrado-help {
|
||||
.config-dual-labeled__field span {
|
||||
font-size: .64rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ window.rt.ensureRouteLayer = function () {
|
||||
window.rt._routeLayer = window.L.layerGroup().addTo(map);
|
||||
}
|
||||
if (!window.rt._routeLines) window.rt._routeLines = {};
|
||||
if (!window.rt._routeCoords) window.rt._routeCoords = {};
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -103,6 +104,7 @@ window.rt.clearRoutes = function () {
|
||||
if (!window.rt.ensureRouteLayer()) return;
|
||||
window.rt._routeLayer.clearLayers();
|
||||
window.rt._routeLines = {};
|
||||
window.rt._routeCoords = {};
|
||||
|
||||
if (window.rt._routeStopsLayer) {
|
||||
window.rt._routeStopsLayer.clearLayers();
|
||||
@@ -123,6 +125,19 @@ window.rt.drawRoute = function (id, coords, options) {
|
||||
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 weight = (options && options.weight) || 4;
|
||||
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;
|
||||
if (!map || !window.rt._routeLayer) return;
|
||||
if (!map || !window.L) return false;
|
||||
|
||||
const bounds = window.rt._routeLayer.getBounds();
|
||||
if (bounds && bounds.isValid()) {
|
||||
map.fitBounds(bounds, { padding: [30, 30] });
|
||||
const routePoints = [];
|
||||
if (Array.isArray(coords)) {
|
||||
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 || {};
|
||||
@@ -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.scrollToHorarioProximo = function () {
|
||||
|
||||
Reference in New Issue
Block a user