- Horario tb6 cambiado

- Colores de subida/bajada cambiados
- On defecto en configuracion actual (texto cambiado)
- Coeficiente cambiado a 1.8
- Direccion del bus añadida en el detalle de tramos
- Tooltip del buscador
- Visualizador de lineas añadido en el listado de las paradas
- Palabra RESERVA en mayusculas
- Añadido editor de colores simple
This commit is contained in:
2026-09-01 13:47:08 +02:00
parent 402ce90a95
commit cd3da67a72
18 changed files with 793 additions and 101 deletions

View File

@@ -185,6 +185,16 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
{ {
var it = GetSelectedIt(); var it = GetSelectedIt();
if (it is null) return; if (it is null) return;
await QuitarItItemAsync(it);
}
/// <summary>
/// Quita del mapa todos los sentidos incluidos en un elemento de itinerario.
/// </summary>
private async Task QuitarItItemAsync(ElementoItinerario it)
{
if (it is null) return;
// claves a borrar: // claves a borrar:
// - si seleccionas IV => borra I y V // - si seleccionas IV => borra I y V
@@ -204,6 +214,8 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
{ {
return; return;
} }
QuitarSalidasParadaActivasPorClaves(claves);
foreach (var clave in claves) foreach (var clave in claves)
{ {
@@ -243,6 +255,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
_itPreviewActivos.Clear(); _itPreviewActivos.Clear();
_itPreviewIdsPorClave.Clear(); _itPreviewIdsPorClave.Clear();
_salidasParadaItinerariosActivas.Clear();
LimpiarParadasItinerarioVisibles(); LimpiarParadasItinerarioVisibles();
_previewLineasActivas.Clear(); // <- esto ya lo usas para los "full_..." _previewLineasActivas.Clear(); // <- esto ya lo usas para los "full_..."

View File

@@ -47,7 +47,7 @@ public partial class PlanificadorRutas : ComponentBase
private int _nivelBrilloMapa = 0; private int _nivelBrilloMapa = 0;
// CONFIG // CONFIG
private const double FactorPenalizacionDistanciaPiePorDefecto = 1.35; private const double FactorPenalizacionDistanciaPiePorDefecto = 1.8;
private const double ParametroPenalizacionDistanciaPieMetrosPorDefecto = 200.0; private const double ParametroPenalizacionDistanciaPieMetrosPorDefecto = 200.0;
private const double CoeficienteEsfuerzoAlturaPorDefecto = 5.2; private const double CoeficienteEsfuerzoAlturaPorDefecto = 5.2;
private const double PendienteMaximaEsfuerzoPorcentaje = 80.0; private const double PendienteMaximaEsfuerzoPorcentaje = 80.0;
@@ -55,8 +55,8 @@ public partial class PlanificadorRutas : ComponentBase
private const double UmbralPendienteSubidaRutaPorDefecto = 2.0; private const double UmbralPendienteSubidaRutaPorDefecto = 2.0;
private const double UmbralPendienteSubidaMediaRutaPorDefecto = 5.0; private const double UmbralPendienteSubidaMediaRutaPorDefecto = 5.0;
private const double UmbralPendienteSubidaFuerteRutaPorDefecto = 10.0; private const double UmbralPendienteSubidaFuerteRutaPorDefecto = 10.0;
private const string ColorPendienteSubidaSuaveRutaPorDefecto = "#d946ef"; private const string ColorPendienteSubidaSuaveRutaPorDefecto = "#ec4899";
private const string ColorPendienteSubidaMediaRutaPorDefecto = "#4c1d95"; private const string ColorPendienteSubidaMediaRutaPorDefecto = "#2563eb";
private const string ColorPendienteSubidaFuerteRutaPorDefecto = "#000000"; private const string ColorPendienteSubidaFuerteRutaPorDefecto = "#000000";
private const double UmbralPendienteBajadaRutaPorDefecto = 5.0; private const double UmbralPendienteBajadaRutaPorDefecto = 5.0;
private const double UmbralAlturaAlertaPendienteRutaMetrosPorDefecto = 6.0; private const double UmbralAlturaAlertaPendienteRutaMetrosPorDefecto = 6.0;
@@ -68,7 +68,7 @@ public partial class PlanificadorRutas : ComponentBase
private double _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto; private double _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
private double _pesoTransbordoRankingMinutos = 4.0; private double _pesoTransbordoRankingMinutos = 4.0;
private double _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto; private double _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
private string _factorPenalizacionDistanciaPieTextoEdicion = "1.35"; private string _factorPenalizacionDistanciaPieTextoEdicion = "1.8";
private string _parametroPenalizacionDistanciaPieTextoEdicion = "200"; private string _parametroPenalizacionDistanciaPieTextoEdicion = "200";
private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20"; private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20";
private string _pesoTransbordoRankingTextoEdicion = "4,0"; private string _pesoTransbordoRankingTextoEdicion = "4,0";
@@ -1163,7 +1163,10 @@ public partial class PlanificadorRutas : ComponentBase
string? dashArray = null, string? dashArray = null,
bool arrowEnd = false, bool arrowEnd = false,
string? lineCap = null, string? lineCap = null,
IReadOnlyList<object>? elevationSegments = null) IReadOnlyList<object>? elevationSegments = null,
string? haloColor = null,
int? haloWeight = null,
double? haloOpacity = null)
{ {
try try
{ {
@@ -1171,7 +1174,19 @@ public partial class PlanificadorRutas : ComponentBase
"rt.drawRoute", "rt.drawRoute",
id, id,
LineaToLatLng(linea), LineaToLatLng(linea),
new { color, weight, opacity, dashArray, arrowEnd, lineCap, elevationSegments } new
{
color,
weight,
opacity,
dashArray,
arrowEnd,
lineCap,
elevationSegments,
haloColor,
haloWeight,
haloOpacity
}
); );
} }
catch { } catch { }

View File

@@ -22,6 +22,9 @@ public partial class PlanificadorRutas
public string Fuente { get; init; } = ""; public string Fuente { get; init; } = "";
public double Latitud { get; init; } public double Latitud { get; init; }
public double Longitud { get; init; } public double Longitud { get; init; }
public string TextoCompleto => string.IsNullOrWhiteSpace(Subtitulo)
? Titulo
: $"{Titulo} - {Subtitulo}";
} }
private readonly List<SugerenciaBusquedaMapa> _sugerenciasBusquedaOrigen = new(); private readonly List<SugerenciaBusquedaMapa> _sugerenciasBusquedaOrigen = new();

View File

@@ -41,7 +41,7 @@ public partial class PlanificadorRutas
private bool _refrescandoHistorialCompartido = false; private bool _refrescandoHistorialCompartido = false;
private bool _omitirRegistroHistorialEnSiguienteCalculo = false; private bool _omitirRegistroHistorialEnSiguienteCalculo = false;
private bool _aplicandoRegistroHistorial = false; private bool _aplicandoRegistroHistorial = false;
private bool _usarParametrosActualesHistorial = false; private bool _usarParametrosActualesHistorial = true;
private bool _cambiandoParametrosHistorial = false; private bool _cambiandoParametrosHistorial = false;
private string? _historialCompartidoPendienteEliminarId; private string? _historialCompartidoPendienteEliminarId;
private ConfiguracionLocalPlanificador? _configuracionTemporalPreviaHistorial; private ConfiguracionLocalPlanificador? _configuracionTemporalPreviaHistorial;
@@ -1488,7 +1488,7 @@ public partial class PlanificadorRutas
var configuracionPrevia = _configuracionTemporalPreviaHistorial; var configuracionPrevia = _configuracionTemporalPreviaHistorial;
_configuracionTemporalPreviaHistorial = null; _configuracionTemporalPreviaHistorial = null;
_configuracionParametrosActualesHistorial = null; _configuracionParametrosActualesHistorial = null;
_usarParametrosActualesHistorial = false; _usarParametrosActualesHistorial = true;
if (configuracionPrevia is null) if (configuracionPrevia is null)
return; return;

View File

@@ -293,6 +293,113 @@ public partial class PlanificadorRutas : ComponentBase
} }
} }
/// <summary>
/// Alterna desde el popup de parada el itinerario asociado a una salida.
/// </summary>
private readonly Dictionary<string, string> _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<string> claves)
{
var clavesAEliminar = new HashSet<string>(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<string>(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();
}
/// <summary> /// <summary>
/// Reúne la geometría de todos los sentidos incluidos en un elemento de itinerario. /// Reúne la geometría de todos los sentidos incluidos en un elemento de itinerario.
/// </summary> /// </summary>
@@ -345,7 +452,7 @@ public partial class PlanificadorRutas : ComponentBase
private async Task MostrarPopupParadaAsync(Parada paradaDetectada) private async Task MostrarPopupParadaAsync(Parada paradaDetectada)
{ {
var myToken = Interlocked.Increment(ref _paradaPopupToken); var myToken = Interlocked.Increment(ref _paradaPopupToken);
_paradaClicada = null; _paradaClicada = null;
_paradaElevIdee = null; _paradaElevIdee = null;
_paradaElevLocal5 = null; _paradaElevLocal5 = null;
@@ -1202,7 +1309,7 @@ public partial class PlanificadorRutas : ComponentBase
private bool _elevPopupMinimizado = false; private bool _elevPopupMinimizado = false;
private const string _versionApp = "(v-20260831a)"; private const string _versionApp = "(v-20260901a)";
private double? _paradaElevIdee; private double? _paradaElevIdee;

View File

@@ -8,8 +8,10 @@ namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas public partial class PlanificadorRutas
{ {
private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1"; private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1";
private const int VersionPersistenciaFactorPenalizacionPie = 1;
private const int VersionPersistenciaFactorEquilibrado = 2; private const int VersionPersistenciaFactorEquilibrado = 2;
private const int VersionPersistenciaPendientes = 2; private const int VersionPersistenciaPendientes = 2;
private const int VersionPersistenciaColoresPendientes = 1;
private const int VersionPersistenciaCoeficienteEsfuerzo = 1; private const int VersionPersistenciaCoeficienteEsfuerzo = 1;
private const int VersionPersistenciaFuenteElevacion = 1; private const int VersionPersistenciaFuenteElevacion = 1;
private bool _configuracionLocalCargada = false; private bool _configuracionLocalCargada = false;
@@ -38,6 +40,7 @@ public partial class PlanificadorRutas
public string? FechaSimulada { get; set; } public string? FechaSimulada { get; set; }
public string? HoraSimulada { get; set; } public string? HoraSimulada { get; set; }
public double? FactorPenalizacionDistanciaPie { get; set; } public double? FactorPenalizacionDistanciaPie { get; set; }
public int? VersionFactorPenalizacionPie { get; set; }
public double? ParametroPenalizacionDistanciaPieMetros { get; set; } public double? ParametroPenalizacionDistanciaPieMetros { get; set; }
public double? CoeficienteEsfuerzoAltura { get; set; } public double? CoeficienteEsfuerzoAltura { get; set; }
public int? VersionCoeficienteEsfuerzo { get; set; } public int? VersionCoeficienteEsfuerzo { get; set; }
@@ -48,6 +51,7 @@ public partial class PlanificadorRutas
public string? FuenteElevacionCalculo { get; set; } public string? FuenteElevacionCalculo { get; set; }
public int? VersionFuenteElevacion { get; set; } public int? VersionFuenteElevacion { get; set; }
public int? VersionPendientes { get; set; } public int? VersionPendientes { get; set; }
public int? VersionColoresPendientes { get; set; }
public double? UmbralPendienteSubidaRuta { get; set; } public double? UmbralPendienteSubidaRuta { get; set; }
public double? UmbralPendienteSubidaMediaRuta { get; set; } public double? UmbralPendienteSubidaMediaRuta { get; set; }
public double? UmbralPendienteSubidaFuerteRuta { get; set; } public double? UmbralPendienteSubidaFuerteRuta { get; set; }
@@ -161,6 +165,7 @@ public partial class PlanificadorRutas
FechaSimulada = _fechaSimulada.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), FechaSimulada = _fechaSimulada.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
HoraSimulada = _horaSimulada.ToString("HH:mm:ss", CultureInfo.InvariantCulture), HoraSimulada = _horaSimulada.ToString("HH:mm:ss", CultureInfo.InvariantCulture),
FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie, FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie,
VersionFactorPenalizacionPie = VersionPersistenciaFactorPenalizacionPie,
ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros, ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros,
CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura, CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura,
VersionCoeficienteEsfuerzo = VersionPersistenciaCoeficienteEsfuerzo, VersionCoeficienteEsfuerzo = VersionPersistenciaCoeficienteEsfuerzo,
@@ -171,6 +176,7 @@ public partial class PlanificadorRutas
FuenteElevacionCalculo = _fuenteElevacionCalculo, FuenteElevacionCalculo = _fuenteElevacionCalculo,
VersionFuenteElevacion = VersionPersistenciaFuenteElevacion, VersionFuenteElevacion = VersionPersistenciaFuenteElevacion,
VersionPendientes = VersionPersistenciaPendientes, VersionPendientes = VersionPersistenciaPendientes,
VersionColoresPendientes = VersionPersistenciaColoresPendientes,
UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta, UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta,
UmbralPendienteSubidaMediaRuta = _umbralPendienteSubidaMediaRuta, UmbralPendienteSubidaMediaRuta = _umbralPendienteSubidaMediaRuta,
UmbralPendienteSubidaFuerteRuta = _umbralPendienteSubidaFuerteRuta, UmbralPendienteSubidaFuerteRuta = _umbralPendienteSubidaFuerteRuta,
@@ -224,9 +230,14 @@ public partial class PlanificadorRutas
if (TimeOnly.TryParse(config.HoraSimulada, out var horaSimulada)) if (TimeOnly.TryParse(config.HoraSimulada, out var horaSimulada))
_horaSimulada = horaSimulada; _horaSimulada = horaSimulada;
var migrarFactorPenalizacionPie =
(config.VersionFactorPenalizacionPie ?? 0) < VersionPersistenciaFactorPenalizacionPie;
if (config.FactorPenalizacionDistanciaPie is { } factor && double.IsFinite(factor)) if (config.FactorPenalizacionDistanciaPie is { } factor && double.IsFinite(factor))
_factorPenalizacionDistanciaPie = _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 ? FactorPenalizacionDistanciaPiePorDefecto
: factor; : factor;
if (config.ParametroPenalizacionDistanciaPieMetros is { } parametro && double.IsFinite(parametro)) if (config.ParametroPenalizacionDistanciaPieMetros is { } parametro && double.IsFinite(parametro))
@@ -282,15 +293,23 @@ public partial class PlanificadorRutas
double.IsFinite(umbralSubidaFuerte) double.IsFinite(umbralSubidaFuerte)
? NormalizarUmbralPendienteRuta(umbralSubidaFuerte) ? NormalizarUmbralPendienteRuta(umbralSubidaFuerte)
: UmbralPendienteSubidaFuerteRutaPorDefecto; : UmbralPendienteSubidaFuerteRutaPorDefecto;
_colorPendienteSubidaSuaveRuta = NormalizarColorHexRuta( var migrarColoresPendientes =
config.ColorPendienteSubidaSuaveRuta, (config.VersionColoresPendientes ?? 0) < VersionPersistenciaColoresPendientes;
ColorPendienteSubidaSuaveRutaPorDefecto); _colorPendienteSubidaSuaveRuta = migrarColoresPendientes
_colorPendienteSubidaMediaRuta = NormalizarColorHexRuta( ? ColorPendienteSubidaSuaveRutaPorDefecto
config.ColorPendienteSubidaMediaRuta, : NormalizarColorHexRuta(
ColorPendienteSubidaMediaRutaPorDefecto); config.ColorPendienteSubidaSuaveRuta,
_colorPendienteSubidaFuerteRuta = NormalizarColorHexRuta( ColorPendienteSubidaSuaveRutaPorDefecto);
config.ColorPendienteSubidaFuerteRuta, _colorPendienteSubidaMediaRuta = migrarColoresPendientes
ColorPendienteSubidaFuerteRutaPorDefecto); ? ColorPendienteSubidaMediaRutaPorDefecto
: NormalizarColorHexRuta(
config.ColorPendienteSubidaMediaRuta,
ColorPendienteSubidaMediaRutaPorDefecto);
_colorPendienteSubidaFuerteRuta = migrarColoresPendientes
? ColorPendienteSubidaFuerteRutaPorDefecto
: NormalizarColorHexRuta(
config.ColorPendienteSubidaFuerteRuta,
ColorPendienteSubidaFuerteRutaPorDefecto);
NormalizarOrdenUmbralesColorSubidaRuta(); NormalizarOrdenUmbralesColorSubidaRuta();
if (config.UmbralPendienteBajadaRuta is { } umbralBajada && double.IsFinite(umbralBajada)) if (config.UmbralPendienteBajadaRuta is { } umbralBajada && double.IsFinite(umbralBajada))
_umbralPendienteBajadaRuta = _umbralPendienteBajadaRuta =

View File

@@ -141,22 +141,36 @@ public partial class PlanificadorRutas : ComponentBase
Fuerte Fuerte
} }
private async Task CambiarColorPendienteSubidaRutaAsync( private NivelColorSubidaRuta? _selectorColorPendienteRuta;
ChangeEventArgs e,
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) NivelColorSubidaRuta nivel)
{ {
var (actual, porDefecto) = nivel switch var nuevo = NormalizarColorHexRuta(color, ObtenerColorPendienteSubidaRuta(nivel));
{
NivelColorSubidaRuta.Media =>
(_colorPendienteSubidaMediaRuta, ColorPendienteSubidaMediaRutaPorDefecto),
NivelColorSubidaRuta.Fuerte =>
(_colorPendienteSubidaFuerteRuta, ColorPendienteSubidaFuerteRutaPorDefecto),
_ =>
(_colorPendienteSubidaSuaveRuta, ColorPendienteSubidaSuaveRutaPorDefecto)
};
var nuevo = NormalizarColorHexRuta(
Convert.ToString(e.Value, CultureInfo.InvariantCulture),
NormalizarColorHexRuta(actual, porDefecto));
switch (nivel) switch (nivel)
{ {
@@ -171,6 +185,7 @@ public partial class PlanificadorRutas : ComponentBase
break; break;
} }
_selectorColorPendienteRuta = null;
await PersistirConfiguracionAsync(); await PersistirConfiguracionAsync();
StateHasChanged(); StateHasChanged();
} }
@@ -355,7 +370,10 @@ public partial class PlanificadorRutas : ComponentBase
dashArray: DashArrayPiePunteado, dashArray: DashArrayPiePunteado,
arrowEnd: arrowEnd, arrowEnd: arrowEnd,
lineCap: "round", lineCap: "round",
elevationSegments: resultadoElevacion.Segmentos); elevationSegments: resultadoElevacion.Segmentos,
haloColor: "#ffffff",
haloWeight: weight + 3,
haloOpacity: 0.9);
} }
private async Task<( private async Task<(
@@ -520,6 +538,15 @@ public partial class PlanificadorRutas : ComponentBase
_distanciaPendientePositivaSignificativaRutaMetros = 0.0; _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) private double? ElevacionPreferidaMapa(MuestraElevacion muestra)
=> ObtenerElevacionFuenteCalculo(muestra); => ObtenerElevacionFuenteCalculo(muestra);
@@ -561,7 +588,7 @@ public partial class PlanificadorRutas : ComponentBase
ColorPendienteSubidaSuaveRutaPorDefecto)); ColorPendienteSubidaSuaveRutaPorDefecto));
if (pendientePorcentaje.Value <= -umbralBajada) if (pendientePorcentaje.Value <= -umbralBajada)
return ("bajada", "#2563eb"); return ("bajada", "#16a34a");
return ("llano", "#dc2626"); return ("llano", "#dc2626");
} }
@@ -829,6 +856,10 @@ public partial class PlanificadorRutas : ComponentBase
var tramo = tramosBus[i]; var tramo = tramosBus[i];
var esTransbordoReal = i > 0 && UtilidadesTransitoDbus.EsTransbordoReal(tramosBus[i - 1], tramo); var esTransbordoReal = i > 0 && UtilidadesTransitoDbus.EsTransbordoReal(tramosBus[i - 1], tramo);
var infoVariante = tramo.Variante; var infoVariante = tramo.Variante;
var nombreDestinoItinerario = ObtenerNombreDestinoItinerario(infoVariante);
var textoDestinoItinerario = string.IsNullOrWhiteSpace(nombreDestinoItinerario)
? string.Empty
: $" → {nombreDestinoItinerario}";
var lineaGeom = lineasBus[i]; var lineaGeom = lineasBus[i];
string codParadaIni = infoVariante.CodigosParadas[tramo.IndiceInicio]; string codParadaIni = infoVariante.CodigosParadas[tramo.IndiceInicio];
@@ -1100,7 +1131,7 @@ public partial class PlanificadorRutas : ComponentBase
{ {
Tipo = "Bus", Tipo = "Bus",
Descripcion = CapitalizarPalabrasClave( 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, DistanciaMetros = distanciaTramo,
DuracionSegundos = durBus, DuracionSegundos = durBus,
@@ -1120,7 +1151,7 @@ public partial class PlanificadorRutas : ComponentBase
{ {
Tipo = "Bus", Tipo = "Bus",
Descripcion = CapitalizarPalabrasClave( Descripcion = CapitalizarPalabrasClave(
$"Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre})." $"Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre}){textoDestinoItinerario}."
), ),
DistanciaMetros = distanciaTramo, DistanciaMetros = distanciaTramo,
DuracionSegundos = duracionBusSegundos, DuracionSegundos = duracionBusSegundos,

View File

@@ -222,6 +222,12 @@ public partial class PlanificadorRutas : ComponentBase
private bool HorariosTaxibusSimuladosVisibles private bool HorariosTaxibusSimuladosVisibles
=> _nextDeps.Any(salida => TaxibusDbus.EsExpedicionSimulada(salida.ExpId)); => _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) private static string ObtenerAvisoTaxibus(AlternativaRuta alternativa)
{ {
var lineasTaxibus = alternativa.AlternativaBus?.Tramos var lineasTaxibus = alternativa.AlternativaBus?.Tramos
@@ -233,8 +239,13 @@ public partial class PlanificadorRutas : ComponentBase
if (lineasTaxibus is not { Count: > 0 }) if (lineasTaxibus is not { Count: > 0 })
return string.Empty; return string.Empty;
return lineasTaxibus.Any(linea => linea.HorarioSimulado) var lineasSimuladas = lineasTaxibus
? TaxibusDbus.MensajeHorarioSimulado .Where(linea => linea.HorarioSimulado)
.Select(linea => linea.Codigo)
.ToList();
return lineasSimuladas.Count > 0
? TaxibusDbus.ObtenerMensajeHorarioSimulado(lineasSimuladas)
: TaxibusDbus.MensajeReserva; : TaxibusDbus.MensajeReserva;
} }

View File

@@ -161,6 +161,7 @@
{ {
<button type="button" <button type="button"
class="gm-suggestion" class="gm-suggestion"
title="@sugerencia.TextoCompleto"
@onclick="() => SeleccionarSugerenciaBusquedaAsync(CampoBusquedaMapa.Origen, sugerencia)"> @onclick="() => SeleccionarSugerenciaBusquedaAsync(CampoBusquedaMapa.Origen, sugerencia)">
<span class="gm-suggestion__type">@sugerencia.Tipo</span> <span class="gm-suggestion__type">@sugerencia.Tipo</span>
<span class="gm-suggestion__text"> <span class="gm-suggestion__text">
@@ -196,6 +197,7 @@
{ {
<button type="button" <button type="button"
class="gm-suggestion" class="gm-suggestion"
title="@sugerencia.TextoCompleto"
@onclick="() => SeleccionarSugerenciaBusquedaAsync(CampoBusquedaMapa.Destino, sugerencia)"> @onclick="() => SeleccionarSugerenciaBusquedaAsync(CampoBusquedaMapa.Destino, sugerencia)">
<span class="gm-suggestion__type">@sugerencia.Tipo</span> <span class="gm-suggestion__type">@sugerencia.Tipo</span>
<span class="gm-suggestion__text"> <span class="gm-suggestion__text">
@@ -302,17 +304,28 @@
@if (HorariosTaxibusSimuladosVisibles) @if (HorariosTaxibusSimuladosVisibles)
{ {
<div class="taxibus-notice taxibus-notice--schedule" role="status"> <div class="taxibus-notice taxibus-notice--schedule" role="status">
<strong>Taxibús:</strong> @TaxibusDbus.MensajeHorarioSimulado <strong>Taxibús:</strong> @MensajeHorarioTaxibusSimuladoVisible
</div> </div>
} }
<div style="max-height: 220px; overflow-y: auto; border: 1px solid rgba(255,255,255,.12); border-radius: 6px; padding: .35rem .5rem; scroll-behavior: smooth;"> <div class="stop-schedule-list">
<ul style="margin:0; padding-left:1.1rem;"> <ul>
@foreach (var x in NextDepsOrdenados) @foreach (var x in NextDepsOrdenados)
{ {
var itinerarioPintado = EstaItinerarioSalidaParadaPintado(x);
<li id="@x.AnchorId" <li id="@x.AnchorId"
style="margin:.15rem 0; @(x.EsProximo ? "background: rgba(255,255,255,.12); border-radius:6px; padding:2px 6px;" : "")"> class="stop-schedule-row @(x.EsProximo ? "stop-schedule-row--next" : "")">
<b>@x.Hora</b> - @x.LineaTxt <span class="stop-schedule-row__text">
<b>@x.Hora</b> - @x.LineaTxt
</span>
<button type="button"
class="stop-itinerary-toggle @(itinerarioPintado ? "is-active" : "")"
title="@(itinerarioPintado ? "Ocultar itinerario" : "Ver itinerario")"
aria-label="@(itinerarioPintado ? "Ocultar itinerario" : "Ver itinerario")"
aria-pressed="@itinerarioPintado"
@onclick="() => ToggleItinerarioSalidaParadaAsync(x)">
<span aria-hidden="true">👁</span>
</button>
</li> </li>
} }
</ul> </ul>
@@ -1085,7 +1098,7 @@
<div class="history-panel-body"> <div class="history-panel-body">
<label class="history-parameters-toggle @(_usarParametrosActualesHistorial ? "history-parameters-toggle--active" : "")" <label class="history-parameters-toggle @(_usarParametrosActualesHistorial ? "history-parameters-toggle--active" : "")"
title="Recalcular las rutas del historial con la configuración actual"> title="Recalcular las rutas del historial con la configuración actual">
<span class="history-parameters-toggle__label">Usar parámetros actuales</span> <span class="history-parameters-toggle__label">Usar configuración actual</span>
<input type="checkbox" <input type="checkbox"
checked="@_usarParametrosActualesHistorial" checked="@_usarParametrosActualesHistorial"
disabled="@(_aplicandoRegistroHistorial || _cambiandoParametrosHistorial)" disabled="@(_aplicandoRegistroHistorial || _cambiandoParametrosHistorial)"
@@ -1769,7 +1782,7 @@
title="Configurar Pendientes" title="Configurar Pendientes"
aria-label="Configurar Pendientes" aria-label="Configurar Pendientes"
disabled="@_pintandoElevacionesRuta" disabled="@_pintandoElevacionesRuta"
@onclick="() => _mostrarConfiguracionPendientesRuta = true"> @onclick="AbrirConfiguracionPendientesRuta">
<span aria-hidden="true">⚙</span> <span aria-hidden="true">⚙</span>
</button> </button>
</div> </div>
@@ -1808,7 +1821,7 @@
@if (_mostrarConfiguracionPendientesRuta) @if (_mostrarConfiguracionPendientesRuta)
{ {
<div class="custom-modal-backdrop pendientes-config-modal-backdrop" <div class="custom-modal-backdrop pendientes-config-modal-backdrop"
@onclick="() => _mostrarConfiguracionPendientesRuta = false"> @onclick="CerrarConfiguracionPendientesRuta">
<div class="custom-modal-card pendientes-config-modal" @onclick:stopPropagation="true"> <div class="custom-modal-card pendientes-config-modal" @onclick:stopPropagation="true">
<div class="pendientes-config-modal__header"> <div class="pendientes-config-modal__header">
<div class="custom-modal-title">Configurar Pendientes</div> <div class="custom-modal-title">Configurar Pendientes</div>
@@ -1816,7 +1829,7 @@
class="config-close-btn pendientes-config-modal__close" class="config-close-btn pendientes-config-modal__close"
title="Cerrar" title="Cerrar"
aria-label="Cerrar" aria-label="Cerrar"
@onclick="() => _mostrarConfiguracionPendientesRuta = false"> @onclick="CerrarConfiguracionPendientesRuta">
× ×
</button> </button>
</div> </div>
@@ -1844,14 +1857,21 @@
<div class="pendientes-config-modal__field"> <div class="pendientes-config-modal__field">
<span class="pendientes-config-modal__threshold-label"> <span class="pendientes-config-modal__threshold-label">
<input type="color" <button type="button"
class="pendientes-config-modal__swatch" class="pendientes-config-modal__swatch"
value="@_colorPendienteSubidaSuaveRuta" style="background-color: @_colorPendienteSubidaSuaveRuta"
title="Elegir color de subida" title="Cambiar color de subida"
aria-label="Elegir color de subida" aria-label="Cambiar color de subida"
disabled="@_pintandoElevacionesRuta" disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarColorPendienteSubidaRutaAsync(e, NivelColorSubidaRuta.Suave))" /> @onclick="() => AlternarSelectorColorPendienteRuta(NivelColorSubidaRuta.Suave)">
</button>
<span>Umbral Subida (%)</span> <span>Umbral Subida (%)</span>
@if (_selectorColorPendienteRuta == NivelColorSubidaRuta.Suave)
{
<SelectorColorSimple Color="@_colorPendienteSubidaSuaveRuta"
Disabled="@_pintandoElevacionesRuta"
ColorChanged="@(color => AplicarColorPendienteSubidaRutaAsync(color, NivelColorSubidaRuta.Suave))" />
}
</span> </span>
<input type="text" <input type="text"
class="config-input" class="config-input"
@@ -1864,14 +1884,21 @@
<div class="pendientes-config-modal__field"> <div class="pendientes-config-modal__field">
<span class="pendientes-config-modal__threshold-label"> <span class="pendientes-config-modal__threshold-label">
<input type="color" <button type="button"
class="pendientes-config-modal__swatch" class="pendientes-config-modal__swatch"
value="@_colorPendienteSubidaMediaRuta" style="background-color: @_colorPendienteSubidaMediaRuta"
title="Elegir color de subida media" title="Cambiar color de subida media"
aria-label="Elegir color de subida media" aria-label="Cambiar color de subida media"
disabled="@_pintandoElevacionesRuta" disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarColorPendienteSubidaRutaAsync(e, NivelColorSubidaRuta.Media))" /> @onclick="() => AlternarSelectorColorPendienteRuta(NivelColorSubidaRuta.Media)">
</button>
<span>Subida Media (%)</span> <span>Subida Media (%)</span>
@if (_selectorColorPendienteRuta == NivelColorSubidaRuta.Media)
{
<SelectorColorSimple Color="@_colorPendienteSubidaMediaRuta"
Disabled="@_pintandoElevacionesRuta"
ColorChanged="@(color => AplicarColorPendienteSubidaRutaAsync(color, NivelColorSubidaRuta.Media))" />
}
</span> </span>
<input type="text" <input type="text"
class="config-input" class="config-input"
@@ -1884,14 +1911,21 @@
<div class="pendientes-config-modal__field"> <div class="pendientes-config-modal__field">
<span class="pendientes-config-modal__threshold-label"> <span class="pendientes-config-modal__threshold-label">
<input type="color" <button type="button"
class="pendientes-config-modal__swatch" class="pendientes-config-modal__swatch"
value="@_colorPendienteSubidaFuerteRuta" style="background-color: @_colorPendienteSubidaFuerteRuta"
title="Elegir color de subida fuerte" title="Cambiar color de subida fuerte"
aria-label="Elegir color de subida fuerte" aria-label="Cambiar color de subida fuerte"
disabled="@_pintandoElevacionesRuta" disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarColorPendienteSubidaRutaAsync(e, NivelColorSubidaRuta.Fuerte))" /> @onclick="() => AlternarSelectorColorPendienteRuta(NivelColorSubidaRuta.Fuerte)">
</button>
<span>Subida Fuerte (%)</span> <span>Subida Fuerte (%)</span>
@if (_selectorColorPendienteRuta == NivelColorSubidaRuta.Fuerte)
{
<SelectorColorSimple Color="@_colorPendienteSubidaFuerteRuta"
Disabled="@_pintandoElevacionesRuta"
ColorChanged="@(color => AplicarColorPendienteSubidaRutaAsync(color, NivelColorSubidaRuta.Fuerte))" />
}
</span> </span>
<input type="text" <input type="text"
class="config-input" class="config-input"

View File

@@ -0,0 +1,58 @@
<div class="pendientes-color-picker" @onclick:stopPropagation="true">
<div class="pendientes-color-picker__title">Colores básicos</div>
<div class="pendientes-color-picker__presets">
@foreach (var color in ColoresPredefinidos)
{
var seleccionado = string.Equals(Color, color.Hex, StringComparison.OrdinalIgnoreCase);
<button type="button"
class="pendientes-color-picker__preset @(seleccionado ? "is-selected" : null)"
style="background-color: @color.Hex"
title="@color.Nombre"
aria-label="Usar color @color.Nombre"
disabled="@Disabled"
@onclick="() => SeleccionarColorAsync(color.Hex)">
</button>
}
</div>
<label class="pendientes-color-picker__advanced">
<span>Color exacto</span>
<input type="color"
value="@Color"
aria-label="Elegir color exacto"
disabled="@Disabled"
@onchange="CambiarColorExactoAsync" />
</label>
</div>
@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<string> ColorChanged { get; set; }
private Task SeleccionarColorAsync(string color)
=> ColorChanged.InvokeAsync(color);
private Task CambiarColorExactoAsync(ChangeEventArgs e)
=> ColorChanged.InvokeAsync(Convert.ToString(e.Value) ?? Color);
}

View File

@@ -31,7 +31,7 @@ public sealed class ParametrosPlanificadorRuta
public int MinutosPorTransbordo { get; set; } = 5; public int MinutosPorTransbordo { get; set; } = 5;
public int MargenTransbordoSegundos { get; set; } = 120; public int MargenTransbordoSegundos { get; set; } = 120;
public int SegundosPorParadaBus { get; set; } = 30; public int SegundosPorParadaBus { get; set; } = 30;
public double FactorPenalizacionDistanciaPie { get; set; } = 1.35; public double FactorPenalizacionDistanciaPie { get; set; } = 1.8;
public double ParametroPenalizacionDistanciaPieMetros { get; set; } = 200.0; public double ParametroPenalizacionDistanciaPieMetros { get; set; } = 200.0;
public double PesoTransbordoRankingMinutos { get; set; } = 4.0; public double PesoTransbordoRankingMinutos { get; set; } = 4.0;
// Divisor aplicado a la distancia en bus dentro del score equilibrado. // Divisor aplicado a la distancia en bus dentro del score equilibrado.

View File

@@ -8,9 +8,11 @@ namespace RutasDBUS.Servicios.Importacion;
internal static class SimuladorHorariosTaxibus internal static class SimuladorHorariosTaxibus
{ {
private const string IdCalendario = "TAXIBUS_SIM_TODOS_LOS_DIAS"; 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 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 double VelocidadMetrosSegundo = 16_000.0 / 3_600.0;
private const int SegundosMinimosEntreParadas = 30; private const int SegundosMinimosEntreParadas = 30;
@@ -65,10 +67,14 @@ internal static class SimuladorHorariosTaxibus
for (var indicePlantilla = 0; indicePlantilla < plantillas.Count; indicePlantilla++) for (var indicePlantilla = 0; indicePlantilla < plantillas.Count; indicePlantilla++)
{ {
var esTb6 = TaxibusDbus.EsLineaTb6(ruta.Abreviatura) ||
TaxibusDbus.EsLineaTb6(ruta.Id);
CrearExpedicionesSimuladas( CrearExpedicionesSimuladas(
ruta, ruta,
plantillas[indicePlantilla], plantillas[indicePlantilla],
indicePlantilla, indicePlantilla,
esTb6 ? FrecuenciaMinutosTb6 : FrecuenciaMinutosRestoTaxibus,
esTb6 ? MinutoFinServicioExclusivoTb6 : MinutoFinServicioExclusivoRestoTaxibus,
expediciones, expediciones,
tiemposParada); tiemposParada);
} }
@@ -134,13 +140,15 @@ internal static class SimuladorHorariosTaxibus
FilaRutaCsv ruta, FilaRutaCsv ruta,
PlantillaTaxibus plantilla, PlantillaTaxibus plantilla,
int indicePlantilla, int indicePlantilla,
int frecuenciaMinutos,
int minutoFinServicioExclusivo,
ICollection<FilaExpedicionCsv> expediciones, ICollection<FilaExpedicionCsv> expediciones,
ICollection<FilaTiempoParadaCsv> tiemposParada) ICollection<FilaTiempoParadaCsv> tiemposParada)
{ {
var indiceSalida = 0; var indiceSalida = 0;
for (var minutoInicio = MinutoPrimeraSalida; for (var minutoInicio = MinutoPrimeraSalida;
minutoInicio < MinutoFinServicioExclusivo; minutoInicio < minutoFinServicioExclusivo;
minutoInicio += FrecuenciaMinutos, indiceSalida++) minutoInicio += frecuenciaMinutos, indiceSalida++)
{ {
var segundosInicio = minutoInicio * 60; var segundosInicio = minutoInicio * 60;
var horaInicio = HoraDesdeSegundos(segundosInicio); var horaInicio = HoraDesdeSegundos(segundosInicio);

View File

@@ -10,6 +10,7 @@ public sealed class BuscadorRutasTemporalesDbus
{ {
private const int MaxEstadosPorParada = 4; private const int MaxEstadosPorParada = 4;
private const int MaxContinuidadesCircularesExtra = 2; private const int MaxContinuidadesCircularesExtra = 2;
private const int MaxBusesSinHorarios = 4;
/// <summary> /// <summary>
/// Busca alternativas de bus entre grupos de paradas candidatas. /// Busca alternativas de bus entre grupos de paradas candidatas.
@@ -58,7 +59,14 @@ public sealed class BuscadorRutasTemporalesDbus
rondas.Add(rondaInicial); rondas.Add(rondaInicial);
var marcadas = new HashSet<string>(rondaInicial.Values.Select(estado => estado.CodigoParada), StringComparer.OrdinalIgnoreCase); var marcadas = new HashSet<string>(rondaInicial.Values.Select(estado => estado.CodigoParada), StringComparer.OrdinalIgnoreCase);
var maximoBuses = ResolverMaximoBuses(parametros); 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++) 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. /// Calcula el maximo de buses a considerar.
/// </summary> /// </summary>
private static int ResolverMaximoBuses(ParametrosPlanificadorRuta parametros) 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);
}
/// <summary> /// <summary>
/// Recorre un patron en modo horario real. /// Recorre un patron en modo horario real.
@@ -547,6 +563,64 @@ public sealed class BuscadorRutasTemporalesDbus
return relevantes; return relevantes;
} }
/// <summary>
/// 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.
/// </summary>
private static HashSet<string> CrearFiltroParadasRelevantesSinHorarios(
IndiceTransitoDbus indice,
IReadOnlyList<ParadaCandidata> candidatasDestino,
ParametrosPlanificadorRuta parametros,
int maximoBuses,
CancellationToken cancellationToken)
{
var alcanzables = new HashSet<string>(
candidatasDestino.Select(x => x.Parada.Codigo),
StringComparer.OrdinalIgnoreCase);
for (var ronda = 0; ronda < maximoBuses; ronda++)
{
cancellationToken.ThrowIfCancellationRequested();
var objetivosBus = new HashSet<string>(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<string>(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<string>? codigosParadaRelevantes) private static bool ParadaEsRelevante(string codigoParada, IReadOnlySet<string>? codigosParadaRelevantes)
=> codigosParadaRelevantes is null || codigosParadaRelevantes.Contains(codigoParada); => codigosParadaRelevantes is null || codigosParadaRelevantes.Contains(codigoParada);

View File

@@ -56,6 +56,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
private const double FactorAhorroClaroAndandoFinal = 0.75; private const double FactorAhorroClaroAndandoFinal = 0.75;
private const double UmbralAccesoCercanoRefinamientoInicioMetros = 120.0; private const double UmbralAccesoCercanoRefinamientoInicioMetros = 120.0;
private const double MaxIncrementoAccesoCercanoRefinamientoInicioMetros = 60.0; private const double MaxIncrementoAccesoCercanoRefinamientoInicioMetros = 60.0;
private const int MaxCandidatasRefinamientoSinHorarios = 20;
/// <summary> /// <summary>
/// Inicializa una nueva instancia del planificador. /// Inicializa una nueva instancia del planificador.
@@ -287,6 +288,13 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
MaxContinuidadesCircularesConsecutivas)) MaxContinuidadesCircularesConsecutivas))
.ToList(); .ToList();
if (!parametros.UsarHorariosReales)
{
candidatasBus = SeleccionarCandidatasRefinamientoSinHorarios(
candidatasBus,
parametros);
}
var alternativasBus = new List<AlternativaRuta>(); var alternativasBus = new List<AlternativaRuta>();
foreach (var candidataBus in candidatasBus) foreach (var candidataBus in candidatasBus)
{ {
@@ -1530,6 +1538,121 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
} }
} }
/// <summary>
/// Limita el refinamiento exacto del modo sin horarios conservando rutas variadas.
/// </summary>
private List<ResultadoBusquedaRutaBusDbus> SeleccionarCandidatasRefinamientoSinHorarios(
IEnumerable<ResultadoBusquedaRutaBusDbus> candidatas,
ParametrosPlanificadorRuta parametros)
{
var mejoresPorFirma = new Dictionary<string, ResultadoBusquedaRutaBusDbus>(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<ResultadoBusquedaRutaBusDbus>(MaxCandidatasRefinamientoSinHorarios);
var firmasSeleccionadas = new HashSet<string>(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<ResultadoBusquedaRutaBusDbus> 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<ResultadoBusquedaRutaBusDbus> OrdenarCandidatasRefinamiento(
IEnumerable<ResultadoBusquedaRutaBusDbus> 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( private double CalcularScoreBusquedaEquilibrado(
ResultadoBusquedaRutaBusDbus resultado, ResultadoBusquedaRutaBusDbus resultado,
ParametrosPlanificadorRuta? parametros) ParametrosPlanificadorRuta? parametros)

View File

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

View File

@@ -6,24 +6,56 @@ namespace RutasDBUS.Servicios;
public static class TaxibusDbus public static class TaxibusDbus
{ {
public const string PrefijoExpedicionSimulada = "TAXIBUS_SIM_"; 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 = 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) 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<string?> codigosLinea)
{
var codigos = codigosLinea.Where(codigo => !string.IsNullOrWhiteSpace(codigo)).ToList();
var mensajes = new List<string>(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)) if (string.IsNullOrWhiteSpace(codigo))
return false; return string.Empty;
var normalizado = codigo.Trim(); var normalizado = codigo.Trim();
var separador = normalizado.IndexOf('.'); var separador = normalizado.IndexOf('.');
if (separador > 0) if (separador > 0)
normalizado = normalizado[..separador]; normalizado = normalizado[..separador];
return normalizado.Equals("TB6", StringComparison.OrdinalIgnoreCase) || return normalizado;
normalizado.Equals("TB7", StringComparison.OrdinalIgnoreCase) ||
normalizado.Equals("6", StringComparison.OrdinalIgnoreCase) ||
normalizado.Equals("7", StringComparison.OrdinalIgnoreCase);
} }
public static bool EsExpedicionSimulada(string? idExpedicion) public static bool EsExpedicionSimulada(string? idExpedicion)

View File

@@ -791,6 +791,69 @@ html, body {
margin-bottom: .15rem; 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 { .popup-parada-cerrar {
position: absolute; position: absolute;
top: .3rem; top: .3rem;
@@ -1799,6 +1862,7 @@ html, body {
.history-panel *, .history-panel *,
.config-panel, .config-panel,
.config-panel *, .config-panel *,
.gm-suggestions,
.custom-modal-card, .custom-modal-card,
.custom-modal-card *, .custom-modal-card *,
.popup-stack-left, .popup-stack-left,
@@ -1817,6 +1881,7 @@ html, body {
.history-panel *, .history-panel *,
.config-panel, .config-panel,
.config-panel *, .config-panel *,
.gm-suggestions,
.custom-modal-card, .custom-modal-card,
.custom-modal-card *, .custom-modal-card *,
.popup-stack-left, .popup-stack-left,
@@ -1835,6 +1900,7 @@ html, body {
.history-panel *, .history-panel *,
.config-panel, .config-panel,
.config-panel *, .config-panel *,
.gm-suggestions,
.custom-modal-card, .custom-modal-card,
.custom-modal-card *, .custom-modal-card *,
.popup-stack-left, .popup-stack-left,
@@ -1853,6 +1919,7 @@ html, body {
.history-panel *, .history-panel *,
.config-panel, .config-panel,
.config-panel *, .config-panel *,
.gm-suggestions,
.custom-modal-card, .custom-modal-card,
.custom-modal-card *, .custom-modal-card *,
.popup-stack-left, .popup-stack-left,
@@ -1872,6 +1939,7 @@ html, body {
.history-panel *, .history-panel *,
.config-panel, .config-panel,
.config-panel *, .config-panel *,
.gm-suggestions,
.custom-modal-card, .custom-modal-card,
.custom-modal-card *, .custom-modal-card *,
.popup-stack-left, .popup-stack-left,
@@ -2280,6 +2348,7 @@ html, body {
align-items: center; align-items: center;
gap: .38rem; gap: .38rem;
min-width: 0; min-width: 0;
position: relative;
} }
.pendientes-config-modal__swatch { .pendientes-config-modal__swatch {
@@ -2289,22 +2358,9 @@ html, body {
padding: 0; padding: 0;
border: 1px solid rgba(226, 232, 240, .7); border: 1px solid rgba(226, 232, 240, .7);
border-radius: 2px; border-radius: 2px;
background: transparent;
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; cursor: pointer;
overflow: hidden; appearance: none;
}
.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;
} }
.pendientes-config-modal__swatch:hover, .pendientes-config-modal__swatch:hover,
@@ -2319,6 +2375,77 @@ html, body {
opacity: .55; 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 { .config-row-map-opacity {
margin-top: .7rem !important; margin-top: .7rem !important;
margin-bottom: 0; margin-bottom: 0;

View File

@@ -278,6 +278,11 @@ window.rt.drawRoute = function (id, coords, options) {
const dashArray = options && options.dashArray; const dashArray = options && options.dashArray;
const lineCap = options && options.lineCap; const lineCap = options && options.lineCap;
const arrowEnd = !!(options && options.arrowEnd); 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) const elevationSegments = Array.isArray(options && options.elevationSegments)
? options.elevationSegments ? options.elevationSegments
: []; : [];
@@ -380,6 +385,19 @@ window.rt.drawRoute = function (id, coords, options) {
}; };
if (dashArray) segmentOpts.dashArray = dashArray; 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 segmentLine = buildLine(segmentCoords, segmentOpts);
const originalSegmentStyle = { const originalSegmentStyle = {
color: segmentColor, color: segmentColor,
@@ -441,8 +459,27 @@ window.rt.drawRoute = function (id, coords, options) {
if (lineCap) lineOpts.lineCap = lineCap; if (lineCap) lineOpts.lineCap = lineCap;
const line = buildLine(coords, lineOpts); const line = buildLine(coords, lineOpts);
line.addTo(window.rt._routeLayer); if (hasHalo) {
window.rt._routeLines[id] = line; 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) { if (arrowEnd && coords && coords.length >= 2) {