- 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:
@@ -185,6 +185,16 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
|
||||
{
|
||||
var it = GetSelectedIt();
|
||||
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:
|
||||
// - 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_..."
|
||||
|
||||
@@ -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<object>? elevationSegments = null)
|
||||
IReadOnlyList<object>? 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 { }
|
||||
|
||||
@@ -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<SugerenciaBusquedaMapa> _sugerenciasBusquedaOrigen = new();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
/// Reúne la geometría de todos los sentidos incluidos en un elemento de itinerario.
|
||||
/// </summary>
|
||||
@@ -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;
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
{
|
||||
<button type="button"
|
||||
class="gm-suggestion"
|
||||
title="@sugerencia.TextoCompleto"
|
||||
@onclick="() => SeleccionarSugerenciaBusquedaAsync(CampoBusquedaMapa.Origen, sugerencia)">
|
||||
<span class="gm-suggestion__type">@sugerencia.Tipo</span>
|
||||
<span class="gm-suggestion__text">
|
||||
@@ -196,6 +197,7 @@
|
||||
{
|
||||
<button type="button"
|
||||
class="gm-suggestion"
|
||||
title="@sugerencia.TextoCompleto"
|
||||
@onclick="() => SeleccionarSugerenciaBusquedaAsync(CampoBusquedaMapa.Destino, sugerencia)">
|
||||
<span class="gm-suggestion__type">@sugerencia.Tipo</span>
|
||||
<span class="gm-suggestion__text">
|
||||
@@ -302,17 +304,28 @@
|
||||
@if (HorariosTaxibusSimuladosVisibles)
|
||||
{
|
||||
<div class="taxibus-notice taxibus-notice--schedule" role="status">
|
||||
<strong>Taxibús:</strong> @TaxibusDbus.MensajeHorarioSimulado
|
||||
<strong>Taxibús:</strong> @MensajeHorarioTaxibusSimuladoVisible
|
||||
</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;">
|
||||
<ul style="margin:0; padding-left:1.1rem;">
|
||||
<div class="stop-schedule-list">
|
||||
<ul>
|
||||
@foreach (var x in NextDepsOrdenados)
|
||||
{
|
||||
var itinerarioPintado = EstaItinerarioSalidaParadaPintado(x);
|
||||
<li id="@x.AnchorId"
|
||||
style="margin:.15rem 0; @(x.EsProximo ? "background: rgba(255,255,255,.12); border-radius:6px; padding:2px 6px;" : "")">
|
||||
<b>@x.Hora</b> - @x.LineaTxt
|
||||
class="stop-schedule-row @(x.EsProximo ? "stop-schedule-row--next" : "")">
|
||||
<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>
|
||||
}
|
||||
</ul>
|
||||
@@ -1085,7 +1098,7 @@
|
||||
<div class="history-panel-body">
|
||||
<label class="history-parameters-toggle @(_usarParametrosActualesHistorial ? "history-parameters-toggle--active" : "")"
|
||||
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"
|
||||
checked="@_usarParametrosActualesHistorial"
|
||||
disabled="@(_aplicandoRegistroHistorial || _cambiandoParametrosHistorial)"
|
||||
@@ -1769,7 +1782,7 @@
|
||||
title="Configurar Pendientes"
|
||||
aria-label="Configurar Pendientes"
|
||||
disabled="@_pintandoElevacionesRuta"
|
||||
@onclick="() => _mostrarConfiguracionPendientesRuta = true">
|
||||
@onclick="AbrirConfiguracionPendientesRuta">
|
||||
<span aria-hidden="true">⚙</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1808,7 +1821,7 @@
|
||||
@if (_mostrarConfiguracionPendientesRuta)
|
||||
{
|
||||
<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="pendientes-config-modal__header">
|
||||
<div class="custom-modal-title">Configurar Pendientes</div>
|
||||
@@ -1816,7 +1829,7 @@
|
||||
class="config-close-btn pendientes-config-modal__close"
|
||||
title="Cerrar"
|
||||
aria-label="Cerrar"
|
||||
@onclick="() => _mostrarConfiguracionPendientesRuta = false">
|
||||
@onclick="CerrarConfiguracionPendientesRuta">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
@@ -1844,14 +1857,21 @@
|
||||
|
||||
<div class="pendientes-config-modal__field">
|
||||
<span class="pendientes-config-modal__threshold-label">
|
||||
<input type="color"
|
||||
class="pendientes-config-modal__swatch"
|
||||
value="@_colorPendienteSubidaSuaveRuta"
|
||||
title="Elegir color de subida"
|
||||
aria-label="Elegir color de subida"
|
||||
disabled="@_pintandoElevacionesRuta"
|
||||
@onchange="@(e => CambiarColorPendienteSubidaRutaAsync(e, NivelColorSubidaRuta.Suave))" />
|
||||
<button type="button"
|
||||
class="pendientes-config-modal__swatch"
|
||||
style="background-color: @_colorPendienteSubidaSuaveRuta"
|
||||
title="Cambiar color de subida"
|
||||
aria-label="Cambiar color de subida"
|
||||
disabled="@_pintandoElevacionesRuta"
|
||||
@onclick="() => AlternarSelectorColorPendienteRuta(NivelColorSubidaRuta.Suave)">
|
||||
</button>
|
||||
<span>Umbral Subida (%)</span>
|
||||
@if (_selectorColorPendienteRuta == NivelColorSubidaRuta.Suave)
|
||||
{
|
||||
<SelectorColorSimple Color="@_colorPendienteSubidaSuaveRuta"
|
||||
Disabled="@_pintandoElevacionesRuta"
|
||||
ColorChanged="@(color => AplicarColorPendienteSubidaRutaAsync(color, NivelColorSubidaRuta.Suave))" />
|
||||
}
|
||||
</span>
|
||||
<input type="text"
|
||||
class="config-input"
|
||||
@@ -1864,14 +1884,21 @@
|
||||
|
||||
<div class="pendientes-config-modal__field">
|
||||
<span class="pendientes-config-modal__threshold-label">
|
||||
<input type="color"
|
||||
class="pendientes-config-modal__swatch"
|
||||
value="@_colorPendienteSubidaMediaRuta"
|
||||
title="Elegir color de subida media"
|
||||
aria-label="Elegir color de subida media"
|
||||
disabled="@_pintandoElevacionesRuta"
|
||||
@onchange="@(e => CambiarColorPendienteSubidaRutaAsync(e, NivelColorSubidaRuta.Media))" />
|
||||
<button type="button"
|
||||
class="pendientes-config-modal__swatch"
|
||||
style="background-color: @_colorPendienteSubidaMediaRuta"
|
||||
title="Cambiar color de subida media"
|
||||
aria-label="Cambiar color de subida media"
|
||||
disabled="@_pintandoElevacionesRuta"
|
||||
@onclick="() => AlternarSelectorColorPendienteRuta(NivelColorSubidaRuta.Media)">
|
||||
</button>
|
||||
<span>Subida Media (%)</span>
|
||||
@if (_selectorColorPendienteRuta == NivelColorSubidaRuta.Media)
|
||||
{
|
||||
<SelectorColorSimple Color="@_colorPendienteSubidaMediaRuta"
|
||||
Disabled="@_pintandoElevacionesRuta"
|
||||
ColorChanged="@(color => AplicarColorPendienteSubidaRutaAsync(color, NivelColorSubidaRuta.Media))" />
|
||||
}
|
||||
</span>
|
||||
<input type="text"
|
||||
class="config-input"
|
||||
@@ -1884,14 +1911,21 @@
|
||||
|
||||
<div class="pendientes-config-modal__field">
|
||||
<span class="pendientes-config-modal__threshold-label">
|
||||
<input type="color"
|
||||
class="pendientes-config-modal__swatch"
|
||||
value="@_colorPendienteSubidaFuerteRuta"
|
||||
title="Elegir color de subida fuerte"
|
||||
aria-label="Elegir color de subida fuerte"
|
||||
disabled="@_pintandoElevacionesRuta"
|
||||
@onchange="@(e => CambiarColorPendienteSubidaRutaAsync(e, NivelColorSubidaRuta.Fuerte))" />
|
||||
<button type="button"
|
||||
class="pendientes-config-modal__swatch"
|
||||
style="background-color: @_colorPendienteSubidaFuerteRuta"
|
||||
title="Cambiar color de subida fuerte"
|
||||
aria-label="Cambiar color de subida fuerte"
|
||||
disabled="@_pintandoElevacionesRuta"
|
||||
@onclick="() => AlternarSelectorColorPendienteRuta(NivelColorSubidaRuta.Fuerte)">
|
||||
</button>
|
||||
<span>Subida Fuerte (%)</span>
|
||||
@if (_selectorColorPendienteRuta == NivelColorSubidaRuta.Fuerte)
|
||||
{
|
||||
<SelectorColorSimple Color="@_colorPendienteSubidaFuerteRuta"
|
||||
Disabled="@_pintandoElevacionesRuta"
|
||||
ColorChanged="@(color => AplicarColorPendienteSubidaRutaAsync(color, NivelColorSubidaRuta.Fuerte))" />
|
||||
}
|
||||
</span>
|
||||
<input type="text"
|
||||
class="config-input"
|
||||
|
||||
58
RutasDBUS/Components/SelectorColorSimple.razor
Normal file
58
RutasDBUS/Components/SelectorColorSimple.razor
Normal 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);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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<FilaExpedicionCsv> expediciones,
|
||||
ICollection<FilaTiempoParadaCsv> 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);
|
||||
|
||||
@@ -10,6 +10,7 @@ public sealed class BuscadorRutasTemporalesDbus
|
||||
{
|
||||
private const int MaxEstadosPorParada = 4;
|
||||
private const int MaxContinuidadesCircularesExtra = 2;
|
||||
private const int MaxBusesSinHorarios = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Busca alternativas de bus entre grupos de paradas candidatas.
|
||||
@@ -58,7 +59,14 @@ public sealed class BuscadorRutasTemporalesDbus
|
||||
rondas.Add(rondaInicial);
|
||||
var marcadas = new HashSet<string>(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.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Recorre un patron en modo horario real.
|
||||
@@ -547,6 +563,64 @@ public sealed class BuscadorRutasTemporalesDbus
|
||||
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)
|
||||
=> codigosParadaRelevantes is null || codigosParadaRelevantes.Contains(codigoParada);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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<AlternativaRuta>();
|
||||
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(
|
||||
ResultadoBusquedaRutaBusDbus resultado,
|
||||
ParametrosPlanificadorRuta? parametros)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<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))
|
||||
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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user