_mostrarConfiguracionPendientesRuta = false">
+ @onclick="CerrarConfiguracionPendientesRuta">
@@ -1844,14 +1857,21 @@
+
+@code {
+ private static readonly (string Nombre, string Hex)[] ColoresPredefinidos =
+ [
+ ("Verde", "#16a34a"),
+ ("Amarillo", "#eab308"),
+ ("Naranja", "#f97316"),
+ ("Rojo", "#dc2626"),
+ ("Rosa", "#ec4899"),
+ ("Violeta", "#7c3aed"),
+ ("Azul", "#2563eb"),
+ ("Cian", "#0891b2"),
+ ("Gris", "#64748b"),
+ ("Negro", "#000000")
+ ];
+
+ [Parameter]
+ public string Color { get; set; } = "#000000";
+
+ [Parameter]
+ public bool Disabled { get; set; }
+
+ [Parameter]
+ public EventCallback
ColorChanged { get; set; }
+
+ private Task SeleccionarColorAsync(string color)
+ => ColorChanged.InvokeAsync(color);
+
+ private Task CambiarColorExactoAsync(ChangeEventArgs e)
+ => ColorChanged.InvokeAsync(Convert.ToString(e.Value) ?? Color);
+}
diff --git a/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs b/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs
index fde8307..8d6189f 100644
--- a/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs
+++ b/RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs
@@ -31,7 +31,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.35;
+ public double FactorPenalizacionDistanciaPie { get; set; } = 1.8;
public double ParametroPenalizacionDistanciaPieMetros { get; set; } = 200.0;
public double PesoTransbordoRankingMinutos { get; set; } = 4.0;
// Divisor aplicado a la distancia en bus dentro del score equilibrado.
diff --git a/RutasDBUS/Servicios/Importacion/SimuladorHorariosTaxibus.cs b/RutasDBUS/Servicios/Importacion/SimuladorHorariosTaxibus.cs
index 6b505e6..e35f60b 100644
--- a/RutasDBUS/Servicios/Importacion/SimuladorHorariosTaxibus.cs
+++ b/RutasDBUS/Servicios/Importacion/SimuladorHorariosTaxibus.cs
@@ -8,9 +8,11 @@ namespace RutasDBUS.Servicios.Importacion;
internal static class SimuladorHorariosTaxibus
{
private const string IdCalendario = "TAXIBUS_SIM_TODOS_LOS_DIAS";
- private const int FrecuenciaMinutos = 30;
+ private const int FrecuenciaMinutosTb6 = 10;
+ private const int FrecuenciaMinutosRestoTaxibus = 30;
private const int MinutoPrimeraSalida = 7 * 60;
- private const int MinutoFinServicioExclusivo = 24 * 60;
+ private const int MinutoFinServicioExclusivoTb6 = 22 * 60;
+ private const int MinutoFinServicioExclusivoRestoTaxibus = 24 * 60;
private const double VelocidadMetrosSegundo = 16_000.0 / 3_600.0;
private const int SegundosMinimosEntreParadas = 30;
@@ -65,10 +67,14 @@ internal static class SimuladorHorariosTaxibus
for (var indicePlantilla = 0; indicePlantilla < plantillas.Count; indicePlantilla++)
{
+ var esTb6 = TaxibusDbus.EsLineaTb6(ruta.Abreviatura) ||
+ TaxibusDbus.EsLineaTb6(ruta.Id);
CrearExpedicionesSimuladas(
ruta,
plantillas[indicePlantilla],
indicePlantilla,
+ esTb6 ? FrecuenciaMinutosTb6 : FrecuenciaMinutosRestoTaxibus,
+ esTb6 ? MinutoFinServicioExclusivoTb6 : MinutoFinServicioExclusivoRestoTaxibus,
expediciones,
tiemposParada);
}
@@ -134,13 +140,15 @@ internal static class SimuladorHorariosTaxibus
FilaRutaCsv ruta,
PlantillaTaxibus plantilla,
int indicePlantilla,
+ int frecuenciaMinutos,
+ int minutoFinServicioExclusivo,
ICollection expediciones,
ICollection tiemposParada)
{
var indiceSalida = 0;
for (var minutoInicio = MinutoPrimeraSalida;
- minutoInicio < MinutoFinServicioExclusivo;
- minutoInicio += FrecuenciaMinutos, indiceSalida++)
+ minutoInicio < minutoFinServicioExclusivo;
+ minutoInicio += frecuenciaMinutos, indiceSalida++)
{
var segundosInicio = minutoInicio * 60;
var horaInicio = HoraDesdeSegundos(segundosInicio);
diff --git a/RutasDBUS/Servicios/Planificacion/BuscadorRutasTemporalesDbus.cs b/RutasDBUS/Servicios/Planificacion/BuscadorRutasTemporalesDbus.cs
index a265d8b..876bd39 100644
--- a/RutasDBUS/Servicios/Planificacion/BuscadorRutasTemporalesDbus.cs
+++ b/RutasDBUS/Servicios/Planificacion/BuscadorRutasTemporalesDbus.cs
@@ -10,6 +10,7 @@ public sealed class BuscadorRutasTemporalesDbus
{
private const int MaxEstadosPorParada = 4;
private const int MaxContinuidadesCircularesExtra = 2;
+ private const int MaxBusesSinHorarios = 4;
///
/// Busca alternativas de bus entre grupos de paradas candidatas.
@@ -58,7 +59,14 @@ public sealed class BuscadorRutasTemporalesDbus
rondas.Add(rondaInicial);
var marcadas = new HashSet(rondaInicial.Values.Select(estado => estado.CodigoParada), StringComparer.OrdinalIgnoreCase);
var maximoBuses = ResolverMaximoBuses(parametros);
- var codigosParadaRelevantes = CrearFiltroParadasRelevantes(indice, candidatasDestino, parametros);
+ var codigosParadaRelevantes = parametros.UsarHorariosReales
+ ? CrearFiltroParadasRelevantes(indice, candidatasDestino, parametros)
+ : CrearFiltroParadasRelevantesSinHorarios(
+ indice,
+ candidatasDestino,
+ parametros,
+ maximoBuses,
+ cancellationToken);
for (var ronda = 1; ronda <= maximoBuses && marcadas.Count > 0; ronda++)
{
@@ -137,7 +145,15 @@ public sealed class BuscadorRutasTemporalesDbus
/// Calcula el maximo de buses a considerar.
///
private static int ResolverMaximoBuses(ParametrosPlanificadorRuta parametros)
- => Math.Max(2, Math.Max(0, parametros.MaxTransbordosPermitidos) + 1 + MaxContinuidadesCircularesExtra);
+ {
+ var maximoConfigurado = Math.Max(
+ 2,
+ Math.Max(0, parametros.MaxTransbordosPermitidos) + 1 + MaxContinuidadesCircularesExtra);
+
+ return parametros.UsarHorariosReales
+ ? maximoConfigurado
+ : Math.Min(maximoConfigurado, MaxBusesSinHorarios);
+ }
///
/// Recorre un patron en modo horario real.
@@ -547,6 +563,64 @@ public sealed class BuscadorRutasTemporalesDbus
return relevantes;
}
+ ///
+ /// Conserva, en modo aproximado, solo paradas desde las que aun se puede alcanzar el destino.
+ /// La expansion inversa respeta el maximo acotado de tramos de bus.
+ ///
+ private static HashSet CrearFiltroParadasRelevantesSinHorarios(
+ IndiceTransitoDbus indice,
+ IReadOnlyList candidatasDestino,
+ ParametrosPlanificadorRuta parametros,
+ int maximoBuses,
+ CancellationToken cancellationToken)
+ {
+ var alcanzables = new HashSet(
+ candidatasDestino.Select(x => x.Parada.Codigo),
+ StringComparer.OrdinalIgnoreCase);
+
+ for (var ronda = 0; ronda < maximoBuses; ronda++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var objetivosBus = new HashSet(alcanzables, StringComparer.OrdinalIgnoreCase);
+ foreach (var transferenciaOrigen in indice.TransferenciasPorParada)
+ {
+ if (transferenciaOrigen.Value.Any(transferencia =>
+ alcanzables.Contains(transferencia.CodigoParadaDestino)))
+ {
+ objetivosBus.Add(transferenciaOrigen.Key);
+ }
+ }
+
+ alcanzables.UnionWith(objetivosBus);
+ var nuevas = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var patron in indice.Patrones)
+ {
+ if (!PatronPermitido(patron, parametros))
+ continue;
+
+ var conduceAlDestino = false;
+ for (var indiceParada = patron.CodigosParada.Count - 1; indiceParada >= 0; indiceParada--)
+ {
+ var codigoParada = patron.CodigosParada[indiceParada];
+ if (objetivosBus.Contains(codigoParada))
+ conduceAlDestino = true;
+
+ if (conduceAlDestino)
+ nuevas.Add(codigoParada);
+ }
+ }
+
+ if (nuevas.IsSubsetOf(alcanzables))
+ break;
+
+ alcanzables.UnionWith(nuevas);
+ }
+
+ return alcanzables;
+ }
+
private static bool ParadaEsRelevante(string codigoParada, IReadOnlySet? codigosParadaRelevantes)
=> codigosParadaRelevantes is null || codigosParadaRelevantes.Contains(codigoParada);
diff --git a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs
index c8af80e..84b91a7 100644
--- a/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs
+++ b/RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs
@@ -56,6 +56,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
private const double FactorAhorroClaroAndandoFinal = 0.75;
private const double UmbralAccesoCercanoRefinamientoInicioMetros = 120.0;
private const double MaxIncrementoAccesoCercanoRefinamientoInicioMetros = 60.0;
+ private const int MaxCandidatasRefinamientoSinHorarios = 20;
///
/// Inicializa una nueva instancia del planificador.
@@ -287,6 +288,13 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
MaxContinuidadesCircularesConsecutivas))
.ToList();
+ if (!parametros.UsarHorariosReales)
+ {
+ candidatasBus = SeleccionarCandidatasRefinamientoSinHorarios(
+ candidatasBus,
+ parametros);
+ }
+
var alternativasBus = new List();
foreach (var candidataBus in candidatasBus)
{
@@ -1530,6 +1538,121 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
}
}
+ ///
+ /// Limita el refinamiento exacto del modo sin horarios conservando rutas variadas.
+ ///
+ private List SeleccionarCandidatasRefinamientoSinHorarios(
+ IEnumerable candidatas,
+ ParametrosPlanificadorRuta parametros)
+ {
+ var mejoresPorFirma = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var candidata in candidatas)
+ {
+ var firma = UtilidadesTransitoDbus.FirmaRutaBus(candidata.RutaBus);
+ if (!mejoresPorFirma.TryGetValue(firma, out var actual) ||
+ ResultadoBusquedaEsMejor(candidata, actual))
+ {
+ mejoresPorFirma[firma] = candidata;
+ }
+ }
+
+ var lista = mejoresPorFirma.Values.ToList();
+ if (lista.Count <= MaxCandidatasRefinamientoSinHorarios)
+ return OrdenarCandidatasRefinamiento(lista, parametros);
+
+ var seleccionadas = new List(MaxCandidatasRefinamientoSinHorarios);
+ var firmasSeleccionadas = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ Agregar(lista
+ .GroupBy(x => x.ParadaOrigen.Codigo, StringComparer.OrdinalIgnoreCase)
+ .Select(grupo => grupo
+ .OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
+ .ThenBy(x => x.PuntuacionCoste)
+ .ThenBy(x => x.DistanciaPieTotal)
+ .First())
+ .OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
+ .Take(6));
+
+ Agregar(lista
+ .OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
+ .ThenBy(x => x.RutaBus.NumeroTransbordos)
+ .ThenBy(x => x.PuntuacionCoste)
+ .Take(5));
+
+ Agregar(lista
+ .OrderBy(x => x.PuntuacionCoste)
+ .ThenBy(x => x.DuracionEstimadaSegundos)
+ .ThenBy(x => x.DistanciaPieTotal)
+ .Take(3));
+
+ Agregar(lista
+ .OrderBy(x => x.DistanciaPieTotal)
+ .ThenBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
+ .ThenBy(x => x.DuracionEstimadaSegundos)
+ .Take(3));
+
+ Agregar(lista
+ .Where(x => UtilidadesTransitoDbus.ContarTransbordosReales(x.RutaBus.Tramos) == 0)
+ .OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
+ .ThenBy(x => x.DuracionEstimadaSegundos)
+ .Take(2));
+
+ Agregar(lista
+ .OrderBy(x => x.DuracionEstimadaSegundos)
+ .ThenBy(x => x.PuntuacionCoste)
+ .Take(2));
+
+ Agregar(lista
+ .GroupBy(CrearClavePrimeraLineaCandidata, StringComparer.OrdinalIgnoreCase)
+ .Select(grupo => grupo
+ .OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
+ .ThenBy(x => x.PuntuacionCoste)
+ .First())
+ .OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros)));
+
+ Agregar(lista
+ .OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
+ .ThenBy(x => x.PuntuacionCoste));
+
+ return OrdenarCandidatasRefinamiento(seleccionadas, parametros);
+
+ void Agregar(IEnumerable origen)
+ {
+ foreach (var candidata in origen)
+ {
+ if (seleccionadas.Count >= MaxCandidatasRefinamientoSinHorarios)
+ return;
+
+ var firma = UtilidadesTransitoDbus.FirmaRutaBus(candidata.RutaBus);
+ if (firmasSeleccionadas.Add(firma))
+ seleccionadas.Add(candidata);
+ }
+ }
+ }
+
+ private List OrdenarCandidatasRefinamiento(
+ IEnumerable candidatas,
+ ParametrosPlanificadorRuta parametros)
+ => candidatas
+ .OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
+ .ThenBy(x => x.PuntuacionCoste)
+ .ThenBy(x => x.DuracionEstimadaSegundos)
+ .ThenBy(x => x.DistanciaPieTotal)
+ .ToList();
+
+ private static string CrearClavePrimeraLineaCandidata(ResultadoBusquedaRutaBusDbus candidata)
+ {
+ var primerTramo = candidata.RutaBus.Tramos.FirstOrDefault();
+ if (primerTramo is null)
+ return candidata.ParadaOrigen.Codigo;
+
+ return string.Join('|',
+ candidata.ParadaOrigen.Codigo,
+ UtilidadesTransitoDbus.LineaCodigoStr(primerTramo.Variante),
+ primerTramo.Variante.CodigoItinerario,
+ UtilidadesTransitoDbus.NormalizarSentidoCodigo(primerTramo.Variante.Sentido?.Codigo));
+ }
+
private double CalcularScoreBusquedaEquilibrado(
ResultadoBusquedaRutaBusDbus resultado,
ParametrosPlanificadorRuta? parametros)
diff --git a/RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs b/RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs
index a1707b2..b93af09 100644
--- a/RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs
+++ b/RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs
@@ -331,7 +331,7 @@ internal static class UtilidadesTransitoDbus
var factor = double.IsFinite(parametros.FactorPenalizacionDistanciaPie)
? parametros.FactorPenalizacionDistanciaPie
- : 1.35;
+ : 1.8;
var parametroBaseMetros = double.IsFinite(parametros.ParametroPenalizacionDistanciaPieMetros)
? Math.Abs(parametros.ParametroPenalizacionDistanciaPieMetros)
: 200.0;
diff --git a/RutasDBUS/Servicios/TaxibusDbus.cs b/RutasDBUS/Servicios/TaxibusDbus.cs
index 437985e..e29690c 100644
--- a/RutasDBUS/Servicios/TaxibusDbus.cs
+++ b/RutasDBUS/Servicios/TaxibusDbus.cs
@@ -6,24 +6,56 @@ namespace RutasDBUS.Servicios;
public static class TaxibusDbus
{
public const string PrefijoExpedicionSimulada = "TAXIBUS_SIM_";
- public const string MensajeReserva = "Esta l\u00ednea requiere solicitud de reserva.";
+ public const string MensajeReserva = "Esta l\u00ednea requiere solicitud de RESERVA.";
+ public const string MensajeHorarioSimuladoTb6 =
+ "Horario provisional simulado cada 10 min, de 07:00 a 22:00. Esta l\u00ednea requiere solicitud de RESERVA.";
public const string MensajeHorarioSimulado =
- "Horario provisional simulado cada 30 min, de 07:00 a 00:00. Esta l\u00ednea requiere solicitud de reserva.";
+ "Horario provisional simulado cada 30 min, de 07:00 a 00:00. Esta l\u00ednea requiere solicitud de RESERVA.";
public static bool EsLinea(string? codigo)
+ => EsLineaTb6(codigo) || EsLineaTb7(codigo);
+
+ public static bool EsLineaTb6(string? codigo)
+ {
+ var normalizado = NormalizarCodigoLinea(codigo);
+ return normalizado.Equals("TB6", StringComparison.OrdinalIgnoreCase) ||
+ normalizado.Equals("6", StringComparison.OrdinalIgnoreCase);
+ }
+
+ public static string ObtenerMensajeHorarioSimulado(IEnumerable codigosLinea)
+ {
+ var codigos = codigosLinea.Where(codigo => !string.IsNullOrWhiteSpace(codigo)).ToList();
+ var mensajes = new List(2);
+
+ if (codigos.Any(EsLineaTb6))
+ mensajes.Add(MensajeHorarioSimuladoTb6);
+
+ if (codigos.Any(codigo => EsLineaTb7(codigo)))
+ mensajes.Add(MensajeHorarioSimulado);
+
+ return mensajes.Count > 0
+ ? string.Join(" ", mensajes)
+ : MensajeHorarioSimulado;
+ }
+
+ private static bool EsLineaTb7(string? codigo)
+ {
+ var normalizado = NormalizarCodigoLinea(codigo);
+ return normalizado.Equals("TB7", StringComparison.OrdinalIgnoreCase) ||
+ normalizado.Equals("7", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static string NormalizarCodigoLinea(string? codigo)
{
if (string.IsNullOrWhiteSpace(codigo))
- return false;
+ return string.Empty;
var normalizado = codigo.Trim();
var separador = normalizado.IndexOf('.');
if (separador > 0)
normalizado = normalizado[..separador];
- return normalizado.Equals("TB6", StringComparison.OrdinalIgnoreCase) ||
- normalizado.Equals("TB7", StringComparison.OrdinalIgnoreCase) ||
- normalizado.Equals("6", StringComparison.OrdinalIgnoreCase) ||
- normalizado.Equals("7", StringComparison.OrdinalIgnoreCase);
+ return normalizado;
}
public static bool EsExpedicionSimulada(string? idExpedicion)
diff --git a/RutasDBUS/wwwroot/app.css b/RutasDBUS/wwwroot/app.css
index 11397ec..ec477d2 100644
--- a/RutasDBUS/wwwroot/app.css
+++ b/RutasDBUS/wwwroot/app.css
@@ -791,6 +791,69 @@ html, body {
margin-bottom: .15rem;
}
+.stop-schedule-list {
+ max-height: 220px;
+ overflow-y: auto;
+ border: 1px solid rgba(255,255,255,.12);
+ border-radius: 6px;
+ padding: .3rem;
+ scroll-behavior: smooth;
+}
+
+.stop-schedule-list ul {
+ display: grid;
+ gap: 2px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.stop-schedule-row {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 26px;
+ align-items: center;
+ gap: 5px;
+ min-height: 28px;
+ padding: 2px 3px 2px 6px;
+ border-radius: 5px;
+}
+
+.stop-schedule-row--next {
+ background: rgba(255,255,255,.12);
+}
+
+.stop-schedule-row__text {
+ min-width: 0;
+ line-height: 1.25;
+}
+
+.stop-itinerary-toggle {
+ display: grid;
+ place-items: center;
+ width: 26px;
+ height: 26px;
+ padding: 0;
+ border: 1px solid rgba(148,163,184,.45);
+ border-radius: 5px;
+ color: #cbd5e1;
+ background: rgba(15,23,42,.62);
+ font-size: .82rem;
+ line-height: 1;
+}
+
+.stop-itinerary-toggle:hover {
+ border-color: #38bdf8;
+ color: #ffffff;
+ background: rgba(14,116,144,.34);
+}
+
+.stop-itinerary-toggle.is-active {
+ border-color: #38bdf8;
+ color: #ffffff;
+ background: #0369a1;
+ box-shadow: 0 0 0 1px rgba(125,211,252,.24);
+}
+
.popup-parada-cerrar {
position: absolute;
top: .3rem;
@@ -1799,6 +1862,7 @@ html, body {
.history-panel *,
.config-panel,
.config-panel *,
+ .gm-suggestions,
.custom-modal-card,
.custom-modal-card *,
.popup-stack-left,
@@ -1817,6 +1881,7 @@ html, body {
.history-panel *,
.config-panel,
.config-panel *,
+ .gm-suggestions,
.custom-modal-card,
.custom-modal-card *,
.popup-stack-left,
@@ -1835,6 +1900,7 @@ html, body {
.history-panel *,
.config-panel,
.config-panel *,
+ .gm-suggestions,
.custom-modal-card,
.custom-modal-card *,
.popup-stack-left,
@@ -1853,6 +1919,7 @@ html, body {
.history-panel *,
.config-panel,
.config-panel *,
+ .gm-suggestions,
.custom-modal-card,
.custom-modal-card *,
.popup-stack-left,
@@ -1872,6 +1939,7 @@ html, body {
.history-panel *,
.config-panel,
.config-panel *,
+ .gm-suggestions,
.custom-modal-card,
.custom-modal-card *,
.popup-stack-left,
@@ -2280,6 +2348,7 @@ html, body {
align-items: center;
gap: .38rem;
min-width: 0;
+ position: relative;
}
.pendientes-config-modal__swatch {
@@ -2289,22 +2358,9 @@ html, body {
padding: 0;
border: 1px solid rgba(226, 232, 240, .7);
border-radius: 2px;
- background: transparent;
box-sizing: border-box;
cursor: pointer;
- overflow: hidden;
-}
-
-.pendientes-config-modal__swatch::-webkit-color-swatch-wrapper {
- padding: 0;
-}
-
-.pendientes-config-modal__swatch::-webkit-color-swatch {
- border: 0;
-}
-
-.pendientes-config-modal__swatch::-moz-color-swatch {
- border: 0;
+ appearance: none;
}
.pendientes-config-modal__swatch:hover,
@@ -2319,6 +2375,77 @@ html, body {
opacity: .55;
}
+.pendientes-color-picker {
+ position: absolute;
+ z-index: 30;
+ top: calc(100% + .45rem);
+ left: 0;
+ width: 188px;
+ padding: .6rem;
+ border: 1px solid rgba(148, 163, 184, .42);
+ border-radius: 6px;
+ background: #0f172a;
+ box-shadow: 0 14px 34px rgba(2, 6, 23, .72);
+}
+
+.pendientes-color-picker__title {
+ margin-bottom: .48rem;
+ color: #e2e8f0;
+ font-size: .72rem;
+ font-weight: 700;
+}
+
+.pendientes-color-picker__presets {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: .45rem;
+}
+
+.pendientes-color-picker__preset {
+ width: 28px;
+ height: 28px;
+ padding: 0;
+ justify-self: center;
+ border: 1px solid rgba(248, 250, 252, .55);
+ border-radius: 4px;
+ cursor: pointer;
+ appearance: none;
+}
+
+.pendientes-color-picker__preset:hover,
+.pendientes-color-picker__preset:focus-visible {
+ outline: 2px solid #7dd3fc;
+ outline-offset: 2px;
+}
+
+.pendientes-color-picker__preset.is-selected {
+ border-color: #f8fafc;
+ box-shadow: 0 0 0 2px #38bdf8;
+}
+
+.pendientes-color-picker__advanced {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: .65rem;
+ margin-top: .58rem;
+ padding-top: .52rem;
+ border-top: 1px solid rgba(148, 163, 184, .24);
+ color: #cbd5e1;
+ font-size: .72rem;
+ cursor: pointer;
+}
+
+.pendientes-color-picker__advanced input[type="color"] {
+ width: 34px;
+ height: 25px;
+ padding: 1px;
+ border: 1px solid rgba(226, 232, 240, .55);
+ border-radius: 3px;
+ background: transparent;
+ cursor: pointer;
+}
+
.config-row-map-opacity {
margin-top: .7rem !important;
margin-bottom: 0;
diff --git a/RutasDBUS/wwwroot/app.js b/RutasDBUS/wwwroot/app.js
index 5ca8bfd..d50c7c4 100644
--- a/RutasDBUS/wwwroot/app.js
+++ b/RutasDBUS/wwwroot/app.js
@@ -278,6 +278,11 @@ window.rt.drawRoute = function (id, coords, options) {
const dashArray = options && options.dashArray;
const lineCap = options && options.lineCap;
const arrowEnd = !!(options && options.arrowEnd);
+ const haloColor = options && options.haloColor;
+ const haloWeight = Number(options && options.haloWeight);
+ const haloOpacity = Number(options && options.haloOpacity);
+ const hasHalo = typeof haloColor === "string" && haloColor.length > 0 &&
+ Number.isFinite(haloWeight) && haloWeight > weight;
const elevationSegments = Array.isArray(options && options.elevationSegments)
? options.elevationSegments
: [];
@@ -380,6 +385,19 @@ window.rt.drawRoute = function (id, coords, options) {
};
if (dashArray) segmentOpts.dashArray = dashArray;
+ if (hasHalo) {
+ const haloOpts = {
+ color: haloColor,
+ weight: haloWeight,
+ opacity: Number.isFinite(haloOpacity) ? haloOpacity : opacity,
+ lineCap: lineCap || "round",
+ interactive: false,
+ bubblingMouseEvents: false
+ };
+ if (dashArray) haloOpts.dashArray = dashArray;
+ window.L.polyline(segmentCoords, haloOpts).addTo(group);
+ }
+
const segmentLine = buildLine(segmentCoords, segmentOpts);
const originalSegmentStyle = {
color: segmentColor,
@@ -441,8 +459,27 @@ window.rt.drawRoute = function (id, coords, options) {
if (lineCap) lineOpts.lineCap = lineCap;
const line = buildLine(coords, lineOpts);
- line.addTo(window.rt._routeLayer);
- window.rt._routeLines[id] = line;
+ if (hasHalo) {
+ const group = window.L.layerGroup();
+ const haloOpts = {
+ color: haloColor,
+ weight: haloWeight,
+ opacity: Number.isFinite(haloOpacity) ? haloOpacity : opacity,
+ lineCap: lineCap || "round",
+ interactive: false,
+ bubblingMouseEvents: false
+ };
+ if (dashArray) haloOpts.dashArray = dashArray;
+
+ window.L.polyline(coords, haloOpts).addTo(group);
+ line.addTo(group);
+ group.addTo(window.rt._routeLayer);
+ window.rt._routeLines[id] = group;
+ }
+ else {
+ line.addTo(window.rt._routeLayer);
+ window.rt._routeLines[id] = line;
+ }
}
if (arrowEnd && coords && coords.length >= 2) {