diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs index 95fdae4..c7059e9 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs @@ -185,6 +185,16 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable { var it = GetSelectedIt(); if (it is null) return; + + await QuitarItItemAsync(it); + } + + /// + /// Quita del mapa todos los sentidos incluidos en un elemento de itinerario. + /// + private async Task QuitarItItemAsync(ElementoItinerario it) + { + if (it is null) return; // claves a borrar: // - si seleccionas IV => borra I y V @@ -204,6 +214,8 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable { return; } + + QuitarSalidasParadaActivasPorClaves(claves); foreach (var clave in claves) { @@ -243,6 +255,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable _itPreviewActivos.Clear(); _itPreviewIdsPorClave.Clear(); + _salidasParadaItinerariosActivas.Clear(); LimpiarParadasItinerarioVisibles(); _previewLineasActivas.Clear(); // <- esto ya lo usas para los "full_..." diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs index f2c7431..003a2a9 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs @@ -47,7 +47,7 @@ public partial class PlanificadorRutas : ComponentBase private int _nivelBrilloMapa = 0; // CONFIG - private const double FactorPenalizacionDistanciaPiePorDefecto = 1.35; + private const double FactorPenalizacionDistanciaPiePorDefecto = 1.8; private const double ParametroPenalizacionDistanciaPieMetrosPorDefecto = 200.0; private const double CoeficienteEsfuerzoAlturaPorDefecto = 5.2; private const double PendienteMaximaEsfuerzoPorcentaje = 80.0; @@ -55,8 +55,8 @@ public partial class PlanificadorRutas : ComponentBase private const double UmbralPendienteSubidaRutaPorDefecto = 2.0; private const double UmbralPendienteSubidaMediaRutaPorDefecto = 5.0; private const double UmbralPendienteSubidaFuerteRutaPorDefecto = 10.0; - private const string ColorPendienteSubidaSuaveRutaPorDefecto = "#d946ef"; - private const string ColorPendienteSubidaMediaRutaPorDefecto = "#4c1d95"; + private const string ColorPendienteSubidaSuaveRutaPorDefecto = "#ec4899"; + private const string ColorPendienteSubidaMediaRutaPorDefecto = "#2563eb"; private const string ColorPendienteSubidaFuerteRutaPorDefecto = "#000000"; private const double UmbralPendienteBajadaRutaPorDefecto = 5.0; private const double UmbralAlturaAlertaPendienteRutaMetrosPorDefecto = 6.0; @@ -68,7 +68,7 @@ public partial class PlanificadorRutas : ComponentBase private double _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; private double _pesoTransbordoRankingMinutos = 4.0; private double _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto; - private string _factorPenalizacionDistanciaPieTextoEdicion = "1.35"; + private string _factorPenalizacionDistanciaPieTextoEdicion = "1.8"; private string _parametroPenalizacionDistanciaPieTextoEdicion = "200"; private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20"; private string _pesoTransbordoRankingTextoEdicion = "4,0"; @@ -1163,7 +1163,10 @@ public partial class PlanificadorRutas : ComponentBase string? dashArray = null, bool arrowEnd = false, string? lineCap = null, - IReadOnlyList? elevationSegments = null) + IReadOnlyList? elevationSegments = null, + string? haloColor = null, + int? haloWeight = null, + double? haloOpacity = null) { try { @@ -1171,7 +1174,19 @@ public partial class PlanificadorRutas : ComponentBase "rt.drawRoute", id, LineaToLatLng(linea), - new { color, weight, opacity, dashArray, arrowEnd, lineCap, elevationSegments } + new + { + color, + weight, + opacity, + dashArray, + arrowEnd, + lineCap, + elevationSegments, + haloColor, + haloWeight, + haloOpacity + } ); } catch { } diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs index e99e430..cfe6bf9 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs @@ -22,6 +22,9 @@ public partial class PlanificadorRutas public string Fuente { get; init; } = ""; public double Latitud { get; init; } public double Longitud { get; init; } + public string TextoCompleto => string.IsNullOrWhiteSpace(Subtitulo) + ? Titulo + : $"{Titulo} - {Subtitulo}"; } private readonly List _sugerenciasBusquedaOrigen = new(); diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs index 2050104..9e88a77 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs @@ -41,7 +41,7 @@ public partial class PlanificadorRutas private bool _refrescandoHistorialCompartido = false; private bool _omitirRegistroHistorialEnSiguienteCalculo = false; private bool _aplicandoRegistroHistorial = false; - private bool _usarParametrosActualesHistorial = false; + private bool _usarParametrosActualesHistorial = true; private bool _cambiandoParametrosHistorial = false; private string? _historialCompartidoPendienteEliminarId; private ConfiguracionLocalPlanificador? _configuracionTemporalPreviaHistorial; @@ -1488,7 +1488,7 @@ public partial class PlanificadorRutas var configuracionPrevia = _configuracionTemporalPreviaHistorial; _configuracionTemporalPreviaHistorial = null; _configuracionParametrosActualesHistorial = null; - _usarParametrosActualesHistorial = false; + _usarParametrosActualesHistorial = true; if (configuracionPrevia is null) return; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs index 296b966..bc8bde3 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs @@ -293,6 +293,113 @@ public partial class PlanificadorRutas : ComponentBase } } + /// + /// Alterna desde el popup de parada el itinerario asociado a una salida. + /// + private readonly Dictionary _salidasParadaItinerariosActivas = + new(StringComparer.OrdinalIgnoreCase); + + private async Task ToggleItinerarioSalidaParadaAsync(ProximaSalidaVm salida) + { + var itinerario = ObtenerItinerarioSalidaParada(salida); + if (itinerario is null) + return; + + var salidaId = CrearIdSalidaParada(salida); + var claveItinerario = ItClave(itinerario.Variantes[0]); + var esSalidaActiva = _salidasParadaItinerariosActivas.TryGetValue(salidaId, out var claveActiva) && + string.Equals(claveActiva, claveItinerario, StringComparison.OrdinalIgnoreCase); + var estaPintado = EstaItinerarioPintado(itinerario); + + if (esSalidaActiva && estaPintado) + { + await QuitarItItemAsync(itinerario); + } + else + { + if (!estaPintado) + { + await PintarItItemAsync(itinerario); + await FitRoutesJsAsync(ConstruirCoordenadasItinerario(itinerario)); + } + + QuitarSalidasParadaActivasPorClaves([claveItinerario]); + _salidasParadaItinerariosActivas[salidaId] = claveItinerario; + } + + if (_itItemsFiltrados.Any(x => x.Id.Equals(itinerario.Id, StringComparison.OrdinalIgnoreCase))) + { + _itSeleccionadoId = itinerario.Id; + _selectItKey++; + } + + StateHasChanged(); + } + + private bool EstaItinerarioSalidaParadaPintado(ProximaSalidaVm salida) + { + var itinerario = ObtenerItinerarioSalidaParada(salida); + return itinerario is not null && + _salidasParadaItinerariosActivas.TryGetValue(CrearIdSalidaParada(salida), out var claveActiva) && + itinerario.Variantes.Any(variante => + string.Equals(claveActiva, ItClave(variante), StringComparison.OrdinalIgnoreCase)) && + EstaItinerarioPintado(itinerario); + } + + private void QuitarSalidasParadaActivasPorClaves(IEnumerable claves) + { + var clavesAEliminar = new HashSet(claves, StringComparer.OrdinalIgnoreCase); + foreach (var salidaId in _salidasParadaItinerariosActivas + .Where(x => clavesAEliminar.Contains(x.Value)) + .Select(x => x.Key) + .ToList()) + { + _salidasParadaItinerariosActivas.Remove(salidaId); + } + } + + private static string CrearIdSalidaParada(ProximaSalidaVm salida) + => string.Join('|', + salida.ExpId, + salida.RutaId, + salida.Hora, + salida.ItinerarioClave); + + private bool EstaItinerarioPintado(ElementoItinerario itinerario) + => itinerario.Variantes.Any(variante => + _itPreviewIdsPorClave.TryGetValue(ItClave(variante), out var ids) && ids.Count > 0); + + private ElementoItinerario? ObtenerItinerarioSalidaParada(ProximaSalidaVm salida) + { + var claves = new HashSet(StringComparer.OrdinalIgnoreCase) + { + NormalizarClaveItinerarioPopup(salida.ItinerarioClave), + NormalizarClaveItinerarioPopup(salida.LineaTxt) + }; + claves.RemoveWhere(string.IsNullOrWhiteSpace); + + return _itItems.FirstOrDefault(item => + item.Variantes.Count == 1 && + claves.Contains(NormalizarClaveItinerarioPopup(ItClave(item.Variantes[0])))); + } + + private static string NormalizarClaveItinerarioPopup(string? texto) + { + if (string.IsNullOrWhiteSpace(texto)) + return ""; + + var clave = texto.Trim(); + var separadorNombre = clave.IndexOf(" - ", StringComparison.Ordinal); + if (separadorNombre >= 0) + clave = clave[..separadorNombre]; + + const string prefijoLinea = "Línea "; + if (clave.StartsWith(prefijoLinea, StringComparison.OrdinalIgnoreCase)) + clave = clave[prefijoLinea.Length..]; + + return clave.Replace(" ", "", StringComparison.Ordinal).ToUpperInvariant(); + } + /// /// Reúne la geometría de todos los sentidos incluidos en un elemento de itinerario. /// @@ -345,7 +452,7 @@ public partial class PlanificadorRutas : ComponentBase private async Task MostrarPopupParadaAsync(Parada paradaDetectada) { var myToken = Interlocked.Increment(ref _paradaPopupToken); - + _paradaClicada = null; _paradaElevIdee = null; _paradaElevLocal5 = null; @@ -1202,7 +1309,7 @@ public partial class PlanificadorRutas : ComponentBase private bool _elevPopupMinimizado = false; - private const string _versionApp = "(v-20260831a)"; + private const string _versionApp = "(v-20260901a)"; private double? _paradaElevIdee; diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs index 4462bf5..58e30e4 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs @@ -8,8 +8,10 @@ namespace RutasDBUS.Components.Pages; public partial class PlanificadorRutas { private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1"; + private const int VersionPersistenciaFactorPenalizacionPie = 1; private const int VersionPersistenciaFactorEquilibrado = 2; private const int VersionPersistenciaPendientes = 2; + private const int VersionPersistenciaColoresPendientes = 1; private const int VersionPersistenciaCoeficienteEsfuerzo = 1; private const int VersionPersistenciaFuenteElevacion = 1; private bool _configuracionLocalCargada = false; @@ -38,6 +40,7 @@ public partial class PlanificadorRutas public string? FechaSimulada { get; set; } public string? HoraSimulada { get; set; } public double? FactorPenalizacionDistanciaPie { get; set; } + public int? VersionFactorPenalizacionPie { get; set; } public double? ParametroPenalizacionDistanciaPieMetros { get; set; } public double? CoeficienteEsfuerzoAltura { get; set; } public int? VersionCoeficienteEsfuerzo { get; set; } @@ -48,6 +51,7 @@ public partial class PlanificadorRutas public string? FuenteElevacionCalculo { get; set; } public int? VersionFuenteElevacion { get; set; } public int? VersionPendientes { get; set; } + public int? VersionColoresPendientes { get; set; } public double? UmbralPendienteSubidaRuta { get; set; } public double? UmbralPendienteSubidaMediaRuta { get; set; } public double? UmbralPendienteSubidaFuerteRuta { get; set; } @@ -161,6 +165,7 @@ public partial class PlanificadorRutas FechaSimulada = _fechaSimulada.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), HoraSimulada = _horaSimulada.ToString("HH:mm:ss", CultureInfo.InvariantCulture), FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie, + VersionFactorPenalizacionPie = VersionPersistenciaFactorPenalizacionPie, ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros, CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura, VersionCoeficienteEsfuerzo = VersionPersistenciaCoeficienteEsfuerzo, @@ -171,6 +176,7 @@ public partial class PlanificadorRutas FuenteElevacionCalculo = _fuenteElevacionCalculo, VersionFuenteElevacion = VersionPersistenciaFuenteElevacion, VersionPendientes = VersionPersistenciaPendientes, + VersionColoresPendientes = VersionPersistenciaColoresPendientes, UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta, UmbralPendienteSubidaMediaRuta = _umbralPendienteSubidaMediaRuta, UmbralPendienteSubidaFuerteRuta = _umbralPendienteSubidaFuerteRuta, @@ -224,9 +230,14 @@ public partial class PlanificadorRutas if (TimeOnly.TryParse(config.HoraSimulada, out var horaSimulada)) _horaSimulada = horaSimulada; + var migrarFactorPenalizacionPie = + (config.VersionFactorPenalizacionPie ?? 0) < VersionPersistenciaFactorPenalizacionPie; if (config.FactorPenalizacionDistanciaPie is { } factor && double.IsFinite(factor)) _factorPenalizacionDistanciaPie = - Math.Abs(factor - 1.3) < 0.0001 || Math.Abs(factor - 1.6) < 0.0001 + migrarFactorPenalizacionPie && + (Math.Abs(factor - 1.3) < 0.0001 || + Math.Abs(factor - 1.35) < 0.0001 || + Math.Abs(factor - 1.6) < 0.0001) ? FactorPenalizacionDistanciaPiePorDefecto : factor; if (config.ParametroPenalizacionDistanciaPieMetros is { } parametro && double.IsFinite(parametro)) @@ -282,15 +293,23 @@ public partial class PlanificadorRutas double.IsFinite(umbralSubidaFuerte) ? NormalizarUmbralPendienteRuta(umbralSubidaFuerte) : UmbralPendienteSubidaFuerteRutaPorDefecto; - _colorPendienteSubidaSuaveRuta = NormalizarColorHexRuta( - config.ColorPendienteSubidaSuaveRuta, - ColorPendienteSubidaSuaveRutaPorDefecto); - _colorPendienteSubidaMediaRuta = NormalizarColorHexRuta( - config.ColorPendienteSubidaMediaRuta, - ColorPendienteSubidaMediaRutaPorDefecto); - _colorPendienteSubidaFuerteRuta = NormalizarColorHexRuta( - config.ColorPendienteSubidaFuerteRuta, - ColorPendienteSubidaFuerteRutaPorDefecto); + var migrarColoresPendientes = + (config.VersionColoresPendientes ?? 0) < VersionPersistenciaColoresPendientes; + _colorPendienteSubidaSuaveRuta = migrarColoresPendientes + ? ColorPendienteSubidaSuaveRutaPorDefecto + : NormalizarColorHexRuta( + config.ColorPendienteSubidaSuaveRuta, + ColorPendienteSubidaSuaveRutaPorDefecto); + _colorPendienteSubidaMediaRuta = migrarColoresPendientes + ? ColorPendienteSubidaMediaRutaPorDefecto + : NormalizarColorHexRuta( + config.ColorPendienteSubidaMediaRuta, + ColorPendienteSubidaMediaRutaPorDefecto); + _colorPendienteSubidaFuerteRuta = migrarColoresPendientes + ? ColorPendienteSubidaFuerteRutaPorDefecto + : NormalizarColorHexRuta( + config.ColorPendienteSubidaFuerteRuta, + ColorPendienteSubidaFuerteRutaPorDefecto); NormalizarOrdenUmbralesColorSubidaRuta(); if (config.UmbralPendienteBajadaRuta is { } umbralBajada && double.IsFinite(umbralBajada)) _umbralPendienteBajadaRuta = diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs index 4c07ab0..c44a4be 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs @@ -141,22 +141,36 @@ public partial class PlanificadorRutas : ComponentBase Fuerte } - private async Task CambiarColorPendienteSubidaRutaAsync( - ChangeEventArgs e, + private NivelColorSubidaRuta? _selectorColorPendienteRuta; + + private void AbrirConfiguracionPendientesRuta() + { + _selectorColorPendienteRuta = null; + _mostrarConfiguracionPendientesRuta = true; + } + + private void CerrarConfiguracionPendientesRuta() + { + _selectorColorPendienteRuta = null; + _mostrarConfiguracionPendientesRuta = false; + } + + private void AlternarSelectorColorPendienteRuta(NivelColorSubidaRuta nivel) + => _selectorColorPendienteRuta = _selectorColorPendienteRuta == nivel ? null : nivel; + + private string ObtenerColorPendienteSubidaRuta(NivelColorSubidaRuta nivel) + => nivel switch + { + NivelColorSubidaRuta.Media => _colorPendienteSubidaMediaRuta, + NivelColorSubidaRuta.Fuerte => _colorPendienteSubidaFuerteRuta, + _ => _colorPendienteSubidaSuaveRuta + }; + + private async Task AplicarColorPendienteSubidaRutaAsync( + string color, NivelColorSubidaRuta nivel) { - var (actual, porDefecto) = nivel switch - { - NivelColorSubidaRuta.Media => - (_colorPendienteSubidaMediaRuta, ColorPendienteSubidaMediaRutaPorDefecto), - NivelColorSubidaRuta.Fuerte => - (_colorPendienteSubidaFuerteRuta, ColorPendienteSubidaFuerteRutaPorDefecto), - _ => - (_colorPendienteSubidaSuaveRuta, ColorPendienteSubidaSuaveRutaPorDefecto) - }; - var nuevo = NormalizarColorHexRuta( - Convert.ToString(e.Value, CultureInfo.InvariantCulture), - NormalizarColorHexRuta(actual, porDefecto)); + var nuevo = NormalizarColorHexRuta(color, ObtenerColorPendienteSubidaRuta(nivel)); switch (nivel) { @@ -171,6 +185,7 @@ public partial class PlanificadorRutas : ComponentBase break; } + _selectorColorPendienteRuta = null; await PersistirConfiguracionAsync(); StateHasChanged(); } @@ -355,7 +370,10 @@ public partial class PlanificadorRutas : ComponentBase dashArray: DashArrayPiePunteado, arrowEnd: arrowEnd, lineCap: "round", - elevationSegments: resultadoElevacion.Segmentos); + elevationSegments: resultadoElevacion.Segmentos, + haloColor: "#ffffff", + haloWeight: weight + 3, + haloOpacity: 0.9); } private async Task<( @@ -520,6 +538,15 @@ public partial class PlanificadorRutas : ComponentBase _distanciaPendientePositivaSignificativaRutaMetros = 0.0; } + private string ObtenerNombreDestinoItinerario(InfoVariante variante) + { + if (variante.CodigosParadas.Count == 0) + return string.Empty; + + var ultimaParada = ResolverParadaFisica(variante.CodigosParadas[^1]); + return ultimaParada.Nombre?.Trim() ?? string.Empty; + } + private double? ElevacionPreferidaMapa(MuestraElevacion muestra) => ObtenerElevacionFuenteCalculo(muestra); @@ -561,7 +588,7 @@ public partial class PlanificadorRutas : ComponentBase ColorPendienteSubidaSuaveRutaPorDefecto)); if (pendientePorcentaje.Value <= -umbralBajada) - return ("bajada", "#2563eb"); + return ("bajada", "#16a34a"); return ("llano", "#dc2626"); } @@ -829,6 +856,10 @@ public partial class PlanificadorRutas : ComponentBase var tramo = tramosBus[i]; var esTransbordoReal = i > 0 && UtilidadesTransitoDbus.EsTransbordoReal(tramosBus[i - 1], tramo); var infoVariante = tramo.Variante; + var nombreDestinoItinerario = ObtenerNombreDestinoItinerario(infoVariante); + var textoDestinoItinerario = string.IsNullOrWhiteSpace(nombreDestinoItinerario) + ? string.Empty + : $" → {nombreDestinoItinerario}"; var lineaGeom = lineasBus[i]; string codParadaIni = infoVariante.CodigosParadas[tramo.IndiceInicio]; @@ -1100,7 +1131,7 @@ public partial class PlanificadorRutas : ComponentBase { Tipo = "Bus", Descripcion = CapitalizarPalabrasClave( - $"{HHmm(depT.Value)} -> {HHmm(arrT.Value)} - Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre})." + $"{HHmm(depT.Value)} -> {HHmm(arrT.Value)} - Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre}){textoDestinoItinerario}." ), DistanciaMetros = distanciaTramo, DuracionSegundos = durBus, @@ -1120,7 +1151,7 @@ public partial class PlanificadorRutas : ComponentBase { Tipo = "Bus", Descripcion = CapitalizarPalabrasClave( - $"Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre})." + $"Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre}){textoDestinoItinerario}." ), DistanciaMetros = distanciaTramo, DuracionSegundos = duracionBusSegundos, diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs b/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs index 4ef9498..9106093 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs @@ -222,6 +222,12 @@ public partial class PlanificadorRutas : ComponentBase private bool HorariosTaxibusSimuladosVisibles => _nextDeps.Any(salida => TaxibusDbus.EsExpedicionSimulada(salida.ExpId)); + private string MensajeHorarioTaxibusSimuladoVisible + => TaxibusDbus.ObtenerMensajeHorarioSimulado( + _nextDeps + .Where(salida => TaxibusDbus.EsExpedicionSimulada(salida.ExpId)) + .SelectMany(salida => new[] { salida.RutaId, salida.LineaTxt })); + private static string ObtenerAvisoTaxibus(AlternativaRuta alternativa) { var lineasTaxibus = alternativa.AlternativaBus?.Tramos @@ -233,8 +239,13 @@ public partial class PlanificadorRutas : ComponentBase if (lineasTaxibus is not { Count: > 0 }) return string.Empty; - return lineasTaxibus.Any(linea => linea.HorarioSimulado) - ? TaxibusDbus.MensajeHorarioSimulado + var lineasSimuladas = lineasTaxibus + .Where(linea => linea.HorarioSimulado) + .Select(linea => linea.Codigo) + .ToList(); + + return lineasSimuladas.Count > 0 + ? TaxibusDbus.ObtenerMensajeHorarioSimulado(lineasSimuladas) : TaxibusDbus.MensajeReserva; } diff --git a/RutasDBUS/Components/Pages/PlanificadorRutas.razor b/RutasDBUS/Components/Pages/PlanificadorRutas.razor index d69a086..b0dc62c 100644 --- a/RutasDBUS/Components/Pages/PlanificadorRutas.razor +++ b/RutasDBUS/Components/Pages/PlanificadorRutas.razor @@ -161,6 +161,7 @@ { } @@ -1085,7 +1098,7 @@
@@ -1808,7 +1821,7 @@ @if (_mostrarConfiguracionPendientesRuta) {
+ @onclick="CerrarConfiguracionPendientesRuta">
Configurar Pendientes
@@ -1816,7 +1829,7 @@ class="config-close-btn pendientes-config-modal__close" title="Cerrar" aria-label="Cerrar" - @onclick="() => _mostrarConfiguracionPendientesRuta = false"> + @onclick="CerrarConfiguracionPendientesRuta"> ×
@@ -1844,14 +1857,21 @@
- + Umbral Subida (%) + @if (_selectorColorPendienteRuta == NivelColorSubidaRuta.Suave) + { + + } - + Subida Media (%) + @if (_selectorColorPendienteRuta == NivelColorSubidaRuta.Media) + { + + } - + Subida Fuerte (%) + @if (_selectorColorPendienteRuta == NivelColorSubidaRuta.Fuerte) + { + + } +
Colores básicos
+ +
+ @foreach (var color in ColoresPredefinidos) + { + var seleccionado = string.Equals(Color, color.Hex, StringComparison.OrdinalIgnoreCase); + + } +
+ + +
+ +@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) {