Añadido horario simualdo con advertencia a lostaxibus

añadido desseleccion de segmentos y iluminado del mismo

añadido limites nuevos en lugares y ordenes alfabeticos

quitado acalarion b5m

defectos de L5 y maximos al 80%

formula cambiada a 5.2 y division por distancia de bus

dsitancia con z anteriores
This commit is contained in:
2026-08-27 14:55:37 +02:00
parent 49c5d23ace
commit 3a629de887
20 changed files with 1219 additions and 43 deletions

View File

@@ -49,7 +49,8 @@ public partial class PlanificadorRutas : ComponentBase
// CONFIG
private const double FactorPenalizacionDistanciaPiePorDefecto = 1.35;
private const double ParametroPenalizacionDistanciaPieMetrosPorDefecto = 200.0;
private const double CoeficienteEsfuerzoAlturaPorDefecto = 8.2;
private const double CoeficienteEsfuerzoAlturaPorDefecto = 5.2;
private const double PendienteMaximaEsfuerzoPorcentaje = 80.0;
private const double FactorEquilibradoAndarBusPorDefecto = 6.0;
private const double UmbralPendienteSubidaRutaPorDefecto = 2.0;
private const double UmbralPendienteBajadaRutaPorDefecto = 5.0;
@@ -64,7 +65,7 @@ public partial class PlanificadorRutas : ComponentBase
private double _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
private string _factorPenalizacionDistanciaPieTextoEdicion = "1.35";
private string _parametroPenalizacionDistanciaPieTextoEdicion = "200";
private string _coeficienteEsfuerzoAlturaTextoEdicion = "8.20";
private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20";
private string _pesoTransbordoRankingTextoEdicion = "4,0";
private string _factorEquilibradoAndarBusTextoEdicion = "6";
private double _velocidadBusKmH = 16.0;
@@ -234,7 +235,10 @@ public partial class PlanificadorRutas : ComponentBase
if (pendientePorcentaje.Value < umbralSubida)
return distanciaMetros.Value;
var pendiente = pendientePorcentaje.Value / 100.0;
var pendienteLimitadaPorcentaje = Math.Min(
pendientePorcentaje.Value,
PendienteMaximaEsfuerzoPorcentaje);
var pendiente = pendienteLimitadaPorcentaje / 100.0;
var factorConPotencia = Math.Pow(1.0 + pendiente, coeficiente);
var esfuerzo = distanciaMetros.Value * factorConPotencia;
return double.IsFinite(esfuerzo)

View File

@@ -76,7 +76,7 @@ public partial class PlanificadorRutas
try
{
await Task.Delay(120, ct);
var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 20, ct);
var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 50, ct);
if (ct.IsCancellationRequested || version != _versionSugerenciasBusqueda)
return;
@@ -160,7 +160,9 @@ public partial class PlanificadorRutas
.Concat(lugares.Select(CrearSugerenciaLugar))
.GroupBy(sugerencia => NormalizarClaveSugerencia(sugerencia), StringComparer.Ordinal)
.Select(grupo => grupo.First())
.Take(Math.Clamp(maxResultados, 1, 20))
.OrderBy(sugerencia => sugerencia.Titulo, StringComparer.CurrentCultureIgnoreCase)
.ThenBy(sugerencia => sugerencia.Subtitulo, StringComparer.CurrentCultureIgnoreCase)
.Take(Math.Clamp(maxResultados, 1, 50))
.ToList();
}
@@ -247,8 +249,8 @@ public partial class PlanificadorRutas
TextoEntrada = lugar.Nombre,
Titulo = lugar.Nombre,
Subtitulo = string.IsNullOrWhiteSpace(lugar.Subtitulo)
? $"{lugar.Tipo} · {lugar.Fuente}"
: $"{lugar.Subtitulo} · {lugar.Fuente}",
? lugar.Tipo
: lugar.Subtitulo,
Tipo = lugar.Tipo,
Fuente = lugar.Fuente,
Latitud = lugar.Latitud,

View File

@@ -30,6 +30,10 @@ public partial class PlanificadorRutas
private TipoReferenciaHistorial _tipoReferenciaHistorial;
private string _historialReferenciaId = string.Empty;
private bool _rutaActualDerivadaDeHistorial;
private readonly List<PasoDerivacionHistorial> _pasosDerivacionHistorial = new();
private RegistroHistorialRuta? _escenarioBaseDerivacionHistorial;
private bool _mostrarInformacionDerivacionHistorial;
private int _indicePasoDerivacionSeleccionado = -1;
private string _mensajeHistorial = string.Empty;
private bool _mensajeHistorialEsError = false;
private bool _mostrarHistorialRutas = false;
@@ -53,6 +57,17 @@ public partial class PlanificadorRutas
Compartido
}
private sealed record PasoDerivacionHistorial(
int Numero,
DateTime FechaUtc,
RegistroHistorialRuta Escenario);
private sealed record CambioDerivacionHistorial(
string Campo,
string ValorAnterior,
string ValorActual,
string Detalle);
private sealed class ResultadoStorageNavegador
{
[JsonPropertyName("ok")]
@@ -113,6 +128,7 @@ public partial class PlanificadorRutas
{
_versionAperturaHistorial++;
_mostrarHistorialRutas = false;
_mostrarInformacionDerivacionHistorial = false;
_historialLocalCargando = false;
StopRefrescoHistorialCompartido();
await RestaurarConfiguracionTemporalHistorialAsync();
@@ -834,13 +850,27 @@ public partial class PlanificadorRutas
private void ConfirmarDerivacionHistorial()
{
_rutaActualDerivadaDeHistorial = ExisteReferenciaHistorialActual();
if (_rutaActualDerivadaDeHistorial &&
_tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido)
{
RegistrarPasoDerivacionHistorialActual();
}
}
private void EstablecerReferenciaHistorial(TipoReferenciaHistorial tipo, string id)
{
ReiniciarSeguimientoDerivacionHistorial();
_tipoReferenciaHistorial = tipo;
_historialReferenciaId = id;
_rutaActualDerivadaDeHistorial = false;
if (tipo == TipoReferenciaHistorial.Compartido)
{
var compartida = _historialCompartido.FirstOrDefault(item =>
string.Equals(item.Id, id, StringComparison.OrdinalIgnoreCase));
if (compartida is not null)
_escenarioBaseDerivacionHistorial = ClonarEscenarioHistorial(compartida);
}
}
private void LimpiarReferenciaHistorial()
@@ -848,6 +878,7 @@ public partial class PlanificadorRutas
_tipoReferenciaHistorial = TipoReferenciaHistorial.Ninguna;
_historialReferenciaId = string.Empty;
_rutaActualDerivadaDeHistorial = false;
ReiniciarSeguimientoDerivacionHistorial();
}
private bool ExisteReferenciaHistorialActual()
@@ -884,11 +915,265 @@ public partial class PlanificadorRutas
_ => (string.Empty, -1)
};
return indice >= 0
? $"Ruta actual derivada del Historial {nombre} n.º {indice + 1}."
if (indice < 0)
return string.Empty;
var numeroDerivacion = _tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido &&
_pasosDerivacionHistorial.Count > 0
? $" · Derivación n.º {_pasosDerivacionHistorial.Count}"
: string.Empty;
return $"Ruta actual derivada del Historial {nombre} n.º {indice + 1}{numeroDerivacion}.";
}
private bool PuedeMostrarInformacionDerivacionHistorial =>
_rutaActualDerivadaDeHistorial &&
_tipoReferenciaHistorial == TipoReferenciaHistorial.Compartido &&
_escenarioBaseDerivacionHistorial is not null &&
_pasosDerivacionHistorial.Count > 0;
private int IndiceHistorialCompartidoReferencia =>
_historialCompartido.FindIndex(item => string.Equals(
item.Id,
_historialReferenciaId,
StringComparison.OrdinalIgnoreCase));
private PasoDerivacionHistorial? PasoDerivacionSeleccionado =>
_indicePasoDerivacionSeleccionado >= 0 &&
_indicePasoDerivacionSeleccionado < _pasosDerivacionHistorial.Count
? _pasosDerivacionHistorial[_indicePasoDerivacionSeleccionado]
: null;
private void AbrirInformacionDerivacionHistorial()
{
if (!PuedeMostrarInformacionDerivacionHistorial)
return;
_indicePasoDerivacionSeleccionado = _pasosDerivacionHistorial.Count - 1;
_mostrarInformacionDerivacionHistorial = true;
}
private void CerrarInformacionDerivacionHistorial()
{
_mostrarInformacionDerivacionHistorial = false;
}
private void SeleccionarPasoDerivacionHistorial(int indice)
{
if (indice < 0 || indice >= _pasosDerivacionHistorial.Count)
return;
_indicePasoDerivacionSeleccionado = indice;
}
private void ReiniciarSeguimientoDerivacionHistorial()
{
_pasosDerivacionHistorial.Clear();
_escenarioBaseDerivacionHistorial = null;
_indicePasoDerivacionSeleccionado = -1;
_mostrarInformacionDerivacionHistorial = false;
}
private void RegistrarPasoDerivacionHistorialActual()
{
_escenarioBaseDerivacionHistorial ??= _historialCompartido
.FirstOrDefault(item => string.Equals(
item.Id,
_historialReferenciaId,
StringComparison.OrdinalIgnoreCase)) is { } compartida
? ClonarEscenarioHistorial(compartida)
: null;
if (_escenarioBaseDerivacionHistorial is null)
return;
var actual = ConstruirRegistroHistorialActual();
if (actual is null)
return;
var anterior = _pasosDerivacionHistorial.Count > 0
? _pasosDerivacionHistorial[^1].Escenario
: _escenarioBaseDerivacionHistorial;
if (string.Equals(
CalcularClaveEscenario(anterior),
CalcularClaveEscenario(actual),
StringComparison.OrdinalIgnoreCase))
{
return;
}
_pasosDerivacionHistorial.Add(new PasoDerivacionHistorial(
_pasosDerivacionHistorial.Count + 1,
DateTime.UtcNow,
ClonarEscenarioHistorial(actual)));
_indicePasoDerivacionSeleccionado = _pasosDerivacionHistorial.Count - 1;
}
private RegistroHistorialRuta? ObtenerEscenarioAnteriorDerivacion(
PasoDerivacionHistorial paso)
{
var indice = paso.Numero - 1;
if (indice <= 0)
return _escenarioBaseDerivacionHistorial;
return indice - 1 < _pasosDerivacionHistorial.Count
? _pasosDerivacionHistorial[indice - 1].Escenario
: null;
}
private IReadOnlyList<CambioDerivacionHistorial> ObtenerCambiosDerivacion(
RegistroHistorialRuta? anterior,
RegistroHistorialRuta actual)
{
if (anterior is null)
return Array.Empty<CambioDerivacionHistorial>();
var cambios = new List<CambioDerivacionHistorial>();
AgregarCambioPuntoDerivacion(
cambios,
"Origen",
anterior.OrigenTexto,
anterior.OrigenLatitud,
anterior.OrigenLongitud,
actual.OrigenTexto,
actual.OrigenLatitud,
actual.OrigenLongitud);
AgregarCambioPuntoDerivacion(
cambios,
"Destino",
anterior.DestinoTexto,
anterior.DestinoLatitud,
anterior.DestinoLongitud,
actual.DestinoTexto,
actual.DestinoLatitud,
actual.DestinoLongitud);
AgregarCambioTextoDerivacion(
cambios,
"Fecha y hora",
FormatearFechaHoraComparacionDerivacion(anterior),
FormatearFechaHoraComparacionDerivacion(actual));
AgregarCambioTextoDerivacion(
cambios,
"Modo",
FormatearModoHistorial(anterior),
FormatearModoHistorial(actual));
AgregarCambioTextoDerivacion(
cambios,
"Factor Equilib. A",
FormatearFactorDerivacion(anterior),
FormatearFactorDerivacion(actual));
AgregarCambioTextoDerivacion(
cambios,
"Horarios",
anterior.UsarHorariosTeoricos ? "Activados" : "Desactivados",
actual.UsarHorariosTeoricos ? "Activados" : "Desactivados");
AgregarCambioTextoDerivacion(
cambios,
"Líneas utilizables",
FormatearLineasDerivacion(anterior),
FormatearLineasDerivacion(actual));
return cambios;
}
private static void AgregarCambioPuntoDerivacion(
ICollection<CambioDerivacionHistorial> cambios,
string campo,
string textoAnterior,
double latitudAnterior,
double longitudAnterior,
string textoActual,
double latitudActual,
double longitudActual)
{
var distancia = CalcularDistanciaMetros(
latitudAnterior,
longitudAnterior,
latitudActual,
longitudActual);
if (!double.IsFinite(distancia) || distancia < 0.5)
return;
cambios.Add(new CambioDerivacionHistorial(
campo,
FormatearPuntoDerivacion(textoAnterior, latitudAnterior, longitudAnterior),
FormatearPuntoDerivacion(textoActual, latitudActual, longitudActual),
$"Desplazado {FormatearDistanciaDerivacion(distancia)}"));
}
private static void AgregarCambioTextoDerivacion(
ICollection<CambioDerivacionHistorial> cambios,
string campo,
string? valorAnterior,
string? valorActual)
{
var anterior = string.IsNullOrWhiteSpace(valorAnterior) ? "-" : valorAnterior.Trim();
var actual = string.IsNullOrWhiteSpace(valorActual) ? "-" : valorActual.Trim();
if (string.Equals(anterior, actual, StringComparison.OrdinalIgnoreCase))
return;
cambios.Add(new CambioDerivacionHistorial(campo, anterior, actual, string.Empty));
}
private static string FormatearPuntoDerivacion(
string texto,
double latitud,
double longitud)
{
var nombre = string.IsNullOrWhiteSpace(texto) ? "Punto del mapa" : texto.Trim();
return $"{nombre} · {latitud.ToString("0.000000", CultureInfo.InvariantCulture)}, " +
longitud.ToString("0.000000", CultureInfo.InvariantCulture);
}
private static string FormatearDistanciaDerivacion(double distanciaMetros)
=> distanciaMetros >= 1000
? $"{(distanciaMetros / 1000.0).ToString("0.000", CultureInfo.InvariantCulture)} km"
: $"{Math.Round(distanciaMetros).ToString("0", CultureInfo.InvariantCulture)} m";
private static string FormatearFechaHoraComparacionDerivacion(RegistroHistorialRuta item)
=> item.UsarHoraSimulada
? FormatearHoraReferenciaHistorial(item)
: "Fecha y hora actuales";
private static string FormatearFactorDerivacion(RegistroHistorialRuta item)
=> item.FactorEquilibradoAndarBus is { } factor && double.IsFinite(factor) && factor > 0
? factor.ToString("0.##", CultureInfo.InvariantCulture)
: "-";
private static string FormatearLineasDerivacion(RegistroHistorialRuta item)
{
if (!item.ActivarFiltroLineas)
return "Todas";
var lineas = NormalizarLineasHistorial(item.LineasPermitidas);
return lineas.Count == 0
? "Ninguna"
: string.Join(", ", lineas);
}
private static RegistroHistorialRuta ClonarEscenarioHistorial(RegistroHistorialRuta item)
=> new()
{
Id = item.Id,
OrigenTexto = item.OrigenTexto,
DestinoTexto = item.DestinoTexto,
OrigenLatitud = item.OrigenLatitud,
OrigenLongitud = item.OrigenLongitud,
DestinoLatitud = item.DestinoLatitud,
DestinoLongitud = item.DestinoLongitud,
UsarHorariosTeoricos = item.UsarHorariosTeoricos,
UsarHoraSimulada = item.UsarHoraSimulada,
FechaSimulada = item.FechaSimulada,
HoraSimulada = item.HoraSimulada,
CriterioPrioritarioRuta = item.CriterioPrioritarioRuta,
PreferenciaEtiquetaRuta = item.PreferenciaEtiquetaRuta,
FactorEquilibradoAndarBus = item.FactorEquilibradoAndarBus,
ActivarFiltroLineas = item.ActivarFiltroLineas,
LineasPermitidas = item.LineasPermitidas.ToList(),
FechaCreacionUtc = item.FechaCreacionUtc,
FechaActualizacionUtc = item.FechaActualizacionUtc
};
private async Task CompartirEscenarioActualAsync()
{
var actual = ConstruirRegistroHistorialActual()

View File

@@ -0,0 +1,35 @@
using System.Globalization;
namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas
{
private PuntoInspectorAnterior? _inspectorPuntoAnterior;
private sealed record PuntoInspectorAnterior(
double Latitud,
double Longitud,
double? ElevacionIdee,
double? ElevacionLocal5,
double? ElevacionLocal2,
double? ElevacionLocal1);
private double? ObtenerElevacionPuntoInspectorAnterior(PuntoInspectorAnterior punto)
=> UsarElevacionLocal1ParaCalculo
? punto.ElevacionLocal1
: UsarElevacionLocal2ParaCalculo
? punto.ElevacionLocal2
: UsarElevacionLocal5ParaCalculo
? punto.ElevacionLocal5
: punto.ElevacionIdee;
private static string FormatearDistanciaPuntoInspector(double distanciaMetros)
=> distanciaMetros < 1000
? distanciaMetros.ToString("0.0", CultureInfo.InvariantCulture) + " m"
: (distanciaMetros / 1000.0).ToString("0.000", CultureInfo.InvariantCulture) + " km";
private static string FormatearAlturaPuntoInspector(double? altura)
=> altura is { } valor
? valor.ToString("0.00", CultureInfo.InvariantCulture) + " m"
: "-";
}

View File

@@ -1202,7 +1202,7 @@ public partial class PlanificadorRutas : ComponentBase
private bool _elevPopupMinimizado = false;
private const string _versionApp = "(v-20260825b)";
private const string _versionApp = "(v-20260827b)";
private double? _paradaElevIdee;

View File

@@ -10,6 +10,8 @@ public partial class PlanificadorRutas
private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1";
private const int VersionPersistenciaFactorEquilibrado = 2;
private const int VersionPersistenciaPendientes = 2;
private const int VersionPersistenciaCoeficienteEsfuerzo = 1;
private const int VersionPersistenciaFuenteElevacion = 1;
private bool _configuracionLocalCargada = false;
private bool _mostrarConfirmacionRestablecer = false;
@@ -38,11 +40,13 @@ public partial class PlanificadorRutas
public double? FactorPenalizacionDistanciaPie { get; set; }
public double? ParametroPenalizacionDistanciaPieMetros { get; set; }
public double? CoeficienteEsfuerzoAltura { get; set; }
public int? VersionCoeficienteEsfuerzo { get; set; }
public double? PesoTransbordoRankingMinutos { get; set; }
public double? FactorEquilibradoAndarBus { get; set; }
public int? VersionFactorEquilibrado { get; set; }
public bool? MostrarElevacionesRuta { get; set; }
public string? FuenteElevacionCalculo { get; set; }
public int? VersionFuenteElevacion { get; set; }
public int? VersionPendientes { get; set; }
public double? UmbralPendienteSubidaRuta { get; set; }
public double? UmbralPendienteBajadaRuta { get; set; }
@@ -154,11 +158,13 @@ public partial class PlanificadorRutas
FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie,
ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros,
CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura,
VersionCoeficienteEsfuerzo = VersionPersistenciaCoeficienteEsfuerzo,
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
VersionFactorEquilibrado = VersionPersistenciaFactorEquilibrado,
MostrarElevacionesRuta = _mostrarElevacionesRutaConfiguracion,
FuenteElevacionCalculo = _fuenteElevacionCalculo,
VersionFuenteElevacion = VersionPersistenciaFuenteElevacion,
VersionPendientes = VersionPersistenciaPendientes,
UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta,
UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta,
@@ -218,11 +224,15 @@ public partial class PlanificadorRutas
Math.Abs(parametro - 100.0) < 0.0001
? ParametroPenalizacionDistanciaPieMetrosPorDefecto
: parametro;
var migrarCoeficienteEsfuerzo =
(config.VersionCoeficienteEsfuerzo ?? 0) < VersionPersistenciaCoeficienteEsfuerzo;
if (config.CoeficienteEsfuerzoAltura is { } coeficienteAltura && double.IsFinite(coeficienteAltura))
_coeficienteEsfuerzoAltura =
Math.Abs(coeficienteAltura - 1.6) < 0.0001 ||
migrarCoeficienteEsfuerzo &&
(Math.Abs(coeficienteAltura - 1.6) < 0.0001 ||
Math.Abs(coeficienteAltura - 5.2) < 0.0001 ||
Math.Abs(coeficienteAltura - 9.2) < 0.0001
Math.Abs(coeficienteAltura - 8.2) < 0.0001 ||
Math.Abs(coeficienteAltura - 9.2) < 0.0001)
? CoeficienteEsfuerzoAlturaPorDefecto
: coeficienteAltura;
if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo))
@@ -239,11 +249,14 @@ public partial class PlanificadorRutas
? true
: config.MostrarElevacionesRuta ?? true;
_mostrarElevacionesRutaConfiguracion = _mostrarElevacionesRuta;
_fuenteElevacionCalculo = migrarValoresPendientes &&
(string.IsNullOrWhiteSpace(config.FuenteElevacionCalculo) ||
string.Equals(config.FuenteElevacionCalculo, FuenteElevacionLocal5, StringComparison.OrdinalIgnoreCase) ||
string.Equals(config.FuenteElevacionCalculo, "local", StringComparison.OrdinalIgnoreCase))
? FuenteElevacionLocal1
var migrarFuenteElevacion =
(config.VersionFuenteElevacion ?? 0) < VersionPersistenciaFuenteElevacion;
_fuenteElevacionCalculo = migrarFuenteElevacion &&
string.Equals(
config.FuenteElevacionCalculo,
FuenteElevacionLocal1,
StringComparison.OrdinalIgnoreCase)
? FuenteElevacionLocal5
: NormalizarFuenteElevacion(config.FuenteElevacionCalculo);
if (config.UmbralPendienteSubidaRuta is { } umbralSubida && double.IsFinite(umbralSubida))
_umbralPendienteSubidaRuta = Math.Abs(umbralSubida - 3.0) < 0.0001
@@ -376,7 +389,7 @@ public partial class PlanificadorRutas
_pesoTransbordoRankingMinutos = 4.0;
_factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
_mostrarElevacionesRutaConfiguracion = true;
_fuenteElevacionCalculo = FuenteElevacionLocal1;
_fuenteElevacionCalculo = FuenteElevacionLocal5;
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
_umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
@@ -429,7 +442,7 @@ public partial class PlanificadorRutas
_pesoTransbordoRankingMinutos = 4.0;
_factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
_mostrarElevacionesRutaConfiguracion = true;
_fuenteElevacionCalculo = FuenteElevacionLocal1;
_fuenteElevacionCalculo = FuenteElevacionLocal5;
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
_umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;

View File

@@ -188,7 +188,7 @@ public partial class PlanificadorRutas : ComponentBase
private const string FuenteElevacionLocal1 = "local1";
private const string FuenteElevacionIdee = "idee";
private const int TiempoMaximoIdeeSegundos = 10;
private string _fuenteElevacionCalculo = FuenteElevacionLocal1;
private string _fuenteElevacionCalculo = FuenteElevacionLocal5;
// IDEE: en el correo indican que 6 decimales es suficiente
private const int ELEV_IDEE_DECIMALES_CACHE = 6;
@@ -217,7 +217,7 @@ public partial class PlanificadorRutas : ComponentBase
return FuenteElevacionLocal5;
}
return FuenteElevacionLocal1;
return FuenteElevacionLocal5;
}
private bool ElevacionLocal5Disponible
@@ -1519,6 +1519,16 @@ public partial class PlanificadorRutas : ComponentBase
/// </summary>
private async Task MostrarInspectorAsync(double lat, double lon)
{
_inspectorPuntoAnterior = _popupInspectorVisible
? new PuntoInspectorAnterior(
_inspectorLat,
_inspectorLon,
_inspectorElevIdee,
_inspectorElevLocal5,
_inspectorElevLocal2,
_inspectorElevLocal1)
: null;
var versionInspector = ++_versionInspector;
_inspectorLat = lat;
_inspectorLon = lon;
@@ -1629,6 +1639,7 @@ public partial class PlanificadorRutas : ComponentBase
{
_versionInspector++;
_popupInspectorVisible = false;
_inspectorPuntoAnterior = null;
_inspectorLugaresCercanos = Array.Empty<LugarGeocodificadoCercano>();
try { await JS.InvokeVoidAsync("rt.clearInspectorMarkers"); } catch { }

View File

@@ -218,6 +218,26 @@ public partial class PlanificadorRutas : ComponentBase
string ItinerarioClave);
private List<ProximaSalidaVm> _nextDeps = new();
private bool HorariosTaxibusSimuladosVisibles
=> _nextDeps.Any(salida => TaxibusDbus.EsExpedicionSimulada(salida.ExpId));
private static string ObtenerAvisoTaxibus(AlternativaRuta alternativa)
{
var lineasTaxibus = alternativa.AlternativaBus?.Tramos
.Select(tramo => tramo.Variante.Linea)
.Where(linea => TaxibusDbus.EsLinea(linea.Codigo))
.Distinct()
.ToList();
if (lineasTaxibus is not { Count: > 0 })
return string.Empty;
return lineasTaxibus.Any(linea => linea.HorarioSimulado)
? TaxibusDbus.MensajeHorarioSimulado
: TaxibusDbus.MensajeReserva;
}
/// <summary>
/// Gestiona toggle hora simulada.
/// </summary>

View File

@@ -299,6 +299,13 @@
</button>
</div>
@if (HorariosTaxibusSimuladosVisibles)
{
<div class="taxibus-notice taxibus-notice--schedule" role="status">
<strong>Taxibús:</strong> @TaxibusDbus.MensajeHorarioSimulado
</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;">
@foreach (var x in NextDepsOrdenados)
@@ -401,6 +408,29 @@
}
</div>
@if (_inspectorPuntoAnterior is { } puntoAnterior)
{
var distanciaPuntoAnterior = CalcularDistanciaMetros(
puntoAnterior.Latitud,
puntoAnterior.Longitud,
_inspectorLat,
_inspectorLon);
var elevacionPuntoAnterior = ObtenerElevacionPuntoInspectorAnterior(puntoAnterior);
<div class="inspector-previous-point">
<div class="inspector-previous-point__distance">
<b>Distancia al punto Z anterior:</b>
@FormatearDistanciaPuntoInspector(distanciaPuntoAnterior)
</div>
<div class="inspector-previous-point__details">
<span>Punto anterior</span>
<span>Lat: @puntoAnterior.Latitud.ToString("0.000000", CultureInfo.InvariantCulture)</span>
<span>Lon: @puntoAnterior.Longitud.ToString("0.000000", CultureInfo.InvariantCulture)</span>
<span>@EtiquetaFuenteElevacionCalculo: @FormatearAlturaPuntoInspector(elevacionPuntoAnterior)</span>
</div>
</div>
}
@if (_inspectorLugaresCercanos.Count > 0)
{
<div class="inspector-address-list">
@@ -741,6 +771,7 @@
@if (_opcionesRuta.Count > 0 && _indiceAlternativaSeleccionada >= 0 && _indiceAlternativaSeleccionada < _opcionesRuta.Count)
{
var alt = _opcionesRuta[_indiceAlternativaSeleccionada];
var avisoTaxibus = ObtenerAvisoTaxibus(alt);
@if (MostrarAlertaPendientesPositivasSignificativas)
{
@@ -753,6 +784,13 @@
</div>
}
@if (!string.IsNullOrWhiteSpace(avisoTaxibus))
{
<div class="taxibus-notice" role="status">
<strong>Taxibús:</strong> @avisoTaxibus
</div>
}
@* ========= RESUMEN BONITO ARRIBA (único) ========= *@
<p class="route-main-text">
@if (alt.EsSoloAPie)
@@ -1162,7 +1200,15 @@
{
<div class="history-derived-note">
<span class="history-derived-marker" aria-hidden="true">*</span>
<span>@textoReferenciaDerivada</span>
<span class="history-derived-note__text">@textoReferenciaDerivada</span>
@if (PuedeMostrarInformacionDerivacionHistorial)
{
<button type="button"
class="history-derived-info-btn"
@onclick="AbrirInformacionDerivacionHistorial">
Info
</button>
}
</div>
}
@@ -1198,6 +1244,126 @@
</div>
}
@if (_mostrarInformacionDerivacionHistorial &&
_escenarioBaseDerivacionHistorial is not null &&
PasoDerivacionSeleccionado is { } pasoDerivacion)
{
var cambiosRespectoOriginal = ObtenerCambiosDerivacion(
_escenarioBaseDerivacionHistorial,
pasoDerivacion.Escenario);
var escenarioAnterior = ObtenerEscenarioAnteriorDerivacion(pasoDerivacion);
var cambiosRespectoAnterior = ObtenerCambiosDerivacion(
escenarioAnterior,
pasoDerivacion.Escenario);
var textoComparacionAnterior = pasoDerivacion.Numero <= 1
? "Respecto al Historial Compartido original"
: $"Respecto a la Derivación n.º {pasoDerivacion.Numero - 1}";
<div class="custom-modal-backdrop history-derivation-backdrop"
@onclick="CerrarInformacionDerivacionHistorial">
<div class="custom-modal-card history-derivation-modal"
@onclick:stopPropagation="true">
<div class="history-derivation-modal__header">
<div>
<div class="custom-modal-title">Cambios de la derivación</div>
<div class="history-derivation-modal__source">
Historial Compartido n.º @(IndiceHistorialCompartidoReferencia + 1)
</div>
</div>
<button type="button"
class="config-close-btn"
title="Cerrar"
aria-label="Cerrar"
@onclick="CerrarInformacionDerivacionHistorial">
&times;
</button>
</div>
<div class="history-derivation-steps" aria-label="Derivaciones registradas">
@foreach (var paso in _pasosDerivacionHistorial)
{
var indicePaso = paso.Numero - 1;
<button type="button"
class="history-derivation-step @(indicePaso == _indicePasoDerivacionSeleccionado ? "history-derivation-step--selected" : "")"
@onclick="() => SeleccionarPasoDerivacionHistorial(indicePaso)">
Der. @paso.Numero
</button>
}
</div>
<div class="history-derivation-selected">
<strong>Derivación n.º @pasoDerivacion.Numero</strong>
<span>@pasoDerivacion.FechaUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss")</span>
</div>
<section class="history-derivation-comparison">
<h6>Respecto al Historial Compartido original</h6>
@if (cambiosRespectoOriginal.Count == 0)
{
<div class="history-derivation-no-changes">Sin cambios registrados.</div>
}
else
{
@foreach (var cambio in cambiosRespectoOriginal)
{
<div class="history-derivation-change">
<div class="history-derivation-change__heading">
<strong>@cambio.Campo</strong>
@if (!string.IsNullOrWhiteSpace(cambio.Detalle))
{
<span>@cambio.Detalle</span>
}
</div>
<div class="history-derivation-change__value">
<span>Antes</span>
<div>@cambio.ValorAnterior</div>
</div>
<div class="history-derivation-change__value">
<span>Ahora</span>
<div>@cambio.ValorActual</div>
</div>
</div>
}
}
</section>
@if (pasoDerivacion.Numero > 1)
{
<section class="history-derivation-comparison">
<h6>@textoComparacionAnterior</h6>
@if (cambiosRespectoAnterior.Count == 0)
{
<div class="history-derivation-no-changes">Sin cambios respecto a la anterior.</div>
}
else
{
@foreach (var cambio in cambiosRespectoAnterior)
{
<div class="history-derivation-change">
<div class="history-derivation-change__heading">
<strong>@cambio.Campo</strong>
@if (!string.IsNullOrWhiteSpace(cambio.Detalle))
{
<span>@cambio.Detalle</span>
}
</div>
<div class="history-derivation-change__value">
<span>Antes</span>
<div>@cambio.ValorAnterior</div>
</div>
<div class="history-derivation-change__value">
<span>Ahora</span>
<div>@cambio.ValorActual</div>
</div>
</div>
}
}
</section>
}
</div>
</div>
}
<!-- CONFIG -->
@* <button class="config-toggle-btn" @onclick="ToggleConfigPanel">⚙</button>
*@
@@ -1309,7 +1475,7 @@
<div class="line-filter">
<label class="line-filter__toggle">
<input type="checkbox" @bind="_activarRangoLineas" @bind:after="PersistirConfiguracionAsync" />
<span>Aplicar lineas utilizables:</span>
<span>Utiliza las siguientes líneas:</span>
</label>
@if (_activarRangoLineas)
@@ -1651,15 +1817,15 @@
disabled="@_pintandoElevacionesRuta"
@bind="_fuenteElevacionCalculo"
@bind:after="OnFuenteElevacionChangedAsync">
<option value="@FuenteElevacionIdee">Altura (G)</option>
<option value="@FuenteElevacionIdee">G</option>
<option value="@FuenteElevacionLocal5" disabled="@(!ElevacionLocal5Disponible)">
Altura (L5)@(!ElevacionLocal5Disponible ? " - no disponible" : string.Empty)
L5@(!ElevacionLocal5Disponible ? " - no disponible" : string.Empty)
</option>
<option value="@FuenteElevacionLocal2" disabled="@(!ElevacionLocal2Disponible)">
Altura (L2)@(!ElevacionLocal2Disponible ? " - no disponible" : string.Empty)
L2@(!ElevacionLocal2Disponible ? " - no disponible" : string.Empty)
</option>
<option value="@FuenteElevacionLocal1" disabled="@(!ElevacionLocal1Disponible)">
Altura (L1)@(!ElevacionLocal1Disponible ? " - no disponible" : string.Empty)
L1@(!ElevacionLocal1Disponible ? " - no disponible" : string.Empty)
</option>
</select>
</label>

View File

@@ -16,6 +16,8 @@ public sealed class ArchivoImportacionDbus
public IReadOnlyList<FilaTiempoRealCsv> TiemposReales { get; init; } = Array.Empty<FilaTiempoRealCsv>();
public IReadOnlyList<FilaTransbordoCsv> Transbordos { get; init; } = Array.Empty<FilaTransbordoCsv>();
public IReadOnlyList<FilaVehiculoCsv> Vehiculos { get; init; } = Array.Empty<FilaVehiculoCsv>();
public IReadOnlySet<string> RutasConHorarioSimulado { get; init; } =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public IReadOnlyDictionary<int, FilaParadaCsv> ParadasPorId { get; init; } = new Dictionary<int, FilaParadaCsv>();
public IReadOnlyDictionary<string, FilaRutaCsv> RutasPorId { get; init; } =

View File

@@ -17,6 +17,9 @@ public class LineaBus
/// <summary>Color de fondo en formato ARGB (ej. FF0000FF).</summary>
public string ColorArgb { get; set; } = "FF0000FF";
/// <summary>Indica si el horario activo se ha completado con una frecuencia provisional.</summary>
public bool HorarioSimulado { get; set; }
/// <summary>Código del itinerario por defecto.</summary>
public int CodigoItinerarioPorDefecto { get; set; }

View File

@@ -43,9 +43,10 @@ public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio
if (tokens.Length == 0)
return Array.Empty<LugarGeocodificado>();
maxResultados = Math.Clamp(maxResultados, 1, 30);
maxResultados = Math.Clamp(maxResultados, 1, 50);
return indice
.Where(entrada => EsDonostia(entrada.Municipio))
.Select(entrada => new
{
Entrada = entrada,

View File

@@ -534,17 +534,24 @@ namespace RutasDBUS.Servicios.Horarios
/// </summary>
private bool CoincideLineaBusqueda(FilaExpedicion exp, string idRutaPedido, string lineaPedida)
{
return CoincideLineaExpedicion(exp, idRutaPedido) ||
CoincideLineaExpedicion(exp, lineaPedida);
}
private bool CoincideLineaExpedicion(FilaExpedicion exp, string? lineaPedida)
{
if (string.IsNullOrWhiteSpace(lineaPedida) ||
string.Equals(lineaPedida, "?", StringComparison.Ordinal))
{
return false;
}
var idRutaExp = NormalizarLinea(exp.IdRuta);
var codigoVisibleExp = NormalizarLinea(ResolverCodigoLineaVisible(exp.IdRuta));
var lineaNormalizada = NormalizarLinea(lineaPedida);
return (!string.IsNullOrWhiteSpace(idRutaPedido) &&
!string.Equals(idRutaPedido, "?", StringComparison.Ordinal) &&
(string.Equals(idRutaExp, idRutaPedido, StringComparison.OrdinalIgnoreCase) ||
string.Equals(codigoVisibleExp, idRutaPedido, StringComparison.OrdinalIgnoreCase))) ||
(!string.IsNullOrWhiteSpace(lineaPedida) &&
!string.Equals(lineaPedida, "?", StringComparison.Ordinal) &&
(string.Equals(idRutaExp, lineaPedida, StringComparison.OrdinalIgnoreCase) ||
string.Equals(codigoVisibleExp, lineaPedida, StringComparison.OrdinalIgnoreCase)));
return string.Equals(idRutaExp, lineaNormalizada, StringComparison.OrdinalIgnoreCase) ||
string.Equals(codigoVisibleExp, lineaNormalizada, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
@@ -763,8 +770,7 @@ namespace RutasDBUS.Servicios.Horarios
if (!activeCals.Contains(exp.IdCal))
continue;
var lineaExp = NormalizarLinea(exp.IdRuta);
if (!string.Equals(lineaExp, lineaPedida, StringComparison.OrdinalIgnoreCase))
if (!CoincideLineaExpedicion(exp, lineaPedida))
continue;
var sentidoExp = DireccionToSentido(exp.Direccion);

View File

@@ -109,6 +109,11 @@ public sealed class ServicioArchivoImportacionDbus
var tiemposReales = await ReadTiemposRealesAsync(FindEntry(zip, "tiempo_real.csv"), ct);
var transbordos = await ReadTransbordosAsync(FindEntry(zip, "transbordos.csv"), ct);
var vehiculos = await ReadVehiculosAsync(FindEntry(zip, "vehiculos.csv"), ct);
var rutasConHorarioSimulado = SimuladorHorariosTaxibus.AplicarSiFaltanHorarios(
calendarios,
expediciones,
rutas,
tiemposParada);
return new ArchivoImportacionDbus
{
@@ -126,6 +131,7 @@ public sealed class ServicioArchivoImportacionDbus
TiemposReales = tiemposReales,
Transbordos = transbordos,
Vehiculos = vehiculos,
RutasConHorarioSimulado = rutasConHorarioSimulado,
ParadasPorId = paradas.ToDictionary(x => x.Id),
RutasPorId = rutas.ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase),
ExpedicionesPorId = expediciones.ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase),

View File

@@ -0,0 +1,200 @@
using RutasDBUS.Modelos.Importacion;
namespace RutasDBUS.Servicios.Importacion;
/// <summary>
/// Completa las plantillas sin horario de TB6 y TB7 con una frecuencia provisional.
/// </summary>
internal static class SimuladorHorariosTaxibus
{
private const string IdCalendario = "TAXIBUS_SIM_TODOS_LOS_DIAS";
private const int FrecuenciaMinutos = 30;
private const double VelocidadMetrosSegundo = 16_000.0 / 3_600.0;
private const int SegundosMinimosEntreParadas = 30;
public static IReadOnlySet<string> AplicarSiFaltanHorarios(
List<FilaCalendarioCsv> calendarios,
List<FilaExpedicionCsv> expediciones,
IReadOnlyList<FilaRutaCsv> rutas,
List<FilaTiempoParadaCsv> tiemposParada)
{
var rutasSimuladas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var tiemposOriginalesPorExpedicion = tiemposParada
.GroupBy(tiempo => tiempo.IdExp, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
grupo => grupo.Key,
grupo => grupo.OrderBy(tiempo => tiempo.SecPar).ToList(),
StringComparer.OrdinalIgnoreCase);
foreach (var ruta in rutas.Where(EsRutaTaxibus))
{
var expedicionesRuta = expediciones
.Where(expedicion => string.Equals(
expedicion.IdRuta,
ruta.Id,
StringComparison.OrdinalIgnoreCase))
.OrderBy(expedicion => expedicion.Id, StringComparer.OrdinalIgnoreCase)
.ToList();
if (expedicionesRuta.Count == 0 ||
TieneHorarioOficial(expedicionesRuta, tiemposOriginalesPorExpedicion))
{
continue;
}
var plantillas = expedicionesRuta
.Select(expedicion => new PlantillaTaxibus(
expedicion,
tiemposOriginalesPorExpedicion.TryGetValue(expedicion.Id, out var tiempos)
? tiempos
: new List<FilaTiempoParadaCsv>()))
.Where(plantilla => plantilla.Tiempos.Count >= 2)
.ToList();
if (plantillas.Count == 0)
continue;
var idsPlantilla = expedicionesRuta
.Select(expedicion => expedicion.Id)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
expediciones.RemoveAll(expedicion => idsPlantilla.Contains(expedicion.Id));
tiemposParada.RemoveAll(tiempo => idsPlantilla.Contains(tiempo.IdExp));
for (var indicePlantilla = 0; indicePlantilla < plantillas.Count; indicePlantilla++)
{
CrearExpedicionesSimuladas(
ruta,
plantillas[indicePlantilla],
indicePlantilla,
expediciones,
tiemposParada);
}
rutasSimuladas.Add(ruta.Id);
}
if (rutasSimuladas.Count > 0)
{
calendarios.RemoveAll(calendario =>
calendario.Id.Equals(IdCalendario, StringComparison.OrdinalIgnoreCase));
calendarios.Add(new FilaCalendarioCsv(
IdCalendario,
2,
2,
2,
2,
2,
2,
2,
new DateOnly(2000, 1, 1),
new DateOnly(2099, 12, 31)));
}
return rutasSimuladas;
}
private static bool EsRutaTaxibus(FilaRutaCsv ruta)
=> TaxibusDbus.EsLinea(ruta.Abreviatura) || TaxibusDbus.EsLinea(ruta.Id);
private static bool TieneHorarioOficial(
IReadOnlyList<FilaExpedicionCsv> expediciones,
IReadOnlyDictionary<string, List<FilaTiempoParadaCsv>> tiemposPorExpedicion)
{
foreach (var expedicion in expediciones)
{
if (!string.IsNullOrWhiteSpace(expedicion.IdCal) &&
!expedicion.IdCal.Equals("0000", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (expedicion.HSalida is { } salida && salida != TimeOnly.MinValue)
return true;
if (!tiemposPorExpedicion.TryGetValue(expedicion.Id, out var tiempos))
continue;
if (tiempos.Any(tiempo =>
EsHoraReal(tiempo.HoraLlegada) || EsHoraReal(tiempo.HoraSalida)))
{
return true;
}
}
return false;
}
private static bool EsHoraReal(TimeOnly? hora)
=> hora is { } valor && valor != TimeOnly.MinValue;
private static void CrearExpedicionesSimuladas(
FilaRutaCsv ruta,
PlantillaTaxibus plantilla,
int indicePlantilla,
ICollection<FilaExpedicionCsv> expediciones,
ICollection<FilaTiempoParadaCsv> tiemposParada)
{
var cantidadSalidas = 24 * 60 / FrecuenciaMinutos;
for (var indiceSalida = 0; indiceSalida < cantidadSalidas; indiceSalida++)
{
var segundosInicio = indiceSalida * FrecuenciaMinutos * 60;
var horaInicio = HoraDesdeSegundos(segundosInicio);
var idExpedicion =
$"{TaxibusDbus.PrefijoExpedicionSimulada}{ruta.Id}_{indicePlantilla:00}_{indiceSalida:00}";
expediciones.Add(new FilaExpedicionCsv(
idExpedicion,
plantilla.Expedicion.IdRuta,
plantilla.Expedicion.DirDestino,
plantilla.Expedicion.Direccion,
plantilla.Expedicion.CambioExp,
plantilla.Expedicion.IdPatronExp,
horaInicio,
IdCalendario));
var segundosPaso = segundosInicio;
for (var indiceParada = 0; indiceParada < plantilla.Tiempos.Count; indiceParada++)
{
var tiempoPlantilla = plantilla.Tiempos[indiceParada];
var horaPaso = HoraDesdeSegundos(segundosPaso);
tiemposParada.Add(new FilaTiempoParadaCsv(
idExpedicion,
horaPaso,
horaPaso,
tiempoPlantilla.DistanciaSiguienteParada,
tiempoPlantilla.IdTiempoPar,
tiempoPlantilla.SecPar,
tiempoPlantilla.TipoSubidaPar,
tiempoPlantilla.TipoBajadaPar,
tiempoPlantilla.NuevaDirDestinoPar));
if (indiceParada < plantilla.Tiempos.Count - 1)
segundosPaso += CalcularSegundosEntreParadas(tiempoPlantilla.DistanciaSiguienteParada);
}
}
}
private static int CalcularSegundosEntreParadas(double distanciaMetros)
{
if (!double.IsFinite(distanciaMetros) || distanciaMetros <= 0)
return SegundosMinimosEntreParadas;
return Math.Max(
SegundosMinimosEntreParadas,
(int)Math.Round(
distanciaMetros / VelocidadMetrosSegundo,
MidpointRounding.AwayFromZero));
}
private static TimeOnly HoraDesdeSegundos(int segundos)
{
var segundosDia = ((segundos % (24 * 3600)) + (24 * 3600)) % (24 * 3600);
return TimeOnly.FromTimeSpan(TimeSpan.FromSeconds(segundosDia));
}
private sealed record PlantillaTaxibus(
FilaExpedicionCsv Expedicion,
IReadOnlyList<FilaTiempoParadaCsv> Tiempos);
}

View File

@@ -3363,8 +3363,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
.Where(x => x.DistanciaPieTotalAprox <= limiteAhorroClaro)
.OrderBy(x => x.DistanciaPieTotalAprox)
.ThenBy(x => DuracionVisibleOrdenacion(x))
.ThenBy(x => x.PuntuacionCoste)
.Take(8));
.ThenBy(x => x.PuntuacionCoste));
}
}

View File

@@ -57,6 +57,7 @@ public sealed class CreadorRedTransporteDbus
: CodigoSinDecimal(row.Id),
Nombre = !string.IsNullOrWhiteSpace(row.Nombre) ? row.Nombre : (row.Descripcion ?? ""),
ColorArgb = NormalizarColorArgb(row.Color),
HorarioSimulado = archive.RutasConHorarioSimulado.Contains(row.Id ?? ""),
CodigoItinerarioPorDefecto = 0
};

View File

@@ -0,0 +1,32 @@
namespace RutasDBUS.Servicios;
/// <summary>
/// Identificacion y textos comunes de las lineas Taxibus.
/// </summary>
public static class TaxibusDbus
{
public const string PrefijoExpedicionSimulada = "TAXIBUS_SIM_";
public const string MensajeReserva = "Esta l\u00ednea requiere solicitud de reserva.";
public const string MensajeHorarioSimulado =
"Horario provisional simulado cada 30 min. Esta l\u00ednea requiere solicitud de reserva.";
public static bool EsLinea(string? codigo)
{
if (string.IsNullOrWhiteSpace(codigo))
return false;
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);
}
public static bool EsExpedicionSimulada(string? idExpedicion)
=> !string.IsNullOrWhiteSpace(idExpedicion) &&
idExpedicion.StartsWith(PrefijoExpedicionSimulada, StringComparison.OrdinalIgnoreCase);
}

View File

@@ -53,6 +53,33 @@
overflow-wrap: anywhere;
}
.inspector-previous-point {
display: grid;
gap: 4px;
margin: 7px 0 6px;
padding: 7px 0 2px;
border-top: 1px solid rgba(148, 163, 184, .25);
}
.inspector-previous-point__distance {
line-height: 1.3;
}
.inspector-previous-point__details {
display: flex;
flex-wrap: wrap;
gap: 2px 8px;
color: #94a3b8;
font-size: .68rem;
line-height: 1.25;
}
.inspector-previous-point__details > span:first-child {
flex-basis: 100%;
color: #cbd5e1;
font-weight: 700;
}
.rt-inspector-place-icon {
border: 0 !important;
background: transparent !important;
@@ -373,6 +400,19 @@ html, body {
cursor: default !important;
}
/* Leaflet enfoca cada tramo SVG con una caja rectangular al pulsarlo. */
.leaflet-interactive.rt-elev-segment:focus {
outline: none !important;
}
.rt-elev-segment-halo {
pointer-events: none;
}
.rt-elev-segment-halo--outer {
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .75));
}
/* Evitar cursor de mano en puntos, polilíneas, polígonos de Leaflet */
.leaflet-marker-icon,
.leaflet-marker-shadow,
@@ -946,6 +986,27 @@ html, body {
line-height: 1.25;
}
.taxibus-notice {
margin: 0 0 .45rem;
padding: .38rem .5rem;
border-left: 3px solid #38bdf8;
border-radius: 4px;
background: rgba(14, 116, 144, .2);
color: #e0f2fe;
font-size: .72rem;
font-weight: 600;
line-height: 1.3;
}
.taxibus-notice strong {
color: #7dd3fc;
font-weight: 800;
}
.taxibus-notice--schedule {
margin: .3rem 0 .4rem;
}
/* Lista de alternativas de ruta */
.route-alt-list {
@@ -1243,13 +1304,185 @@ html, body {
}
.history-derived-note {
display: flex;
align-items: baseline;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: .35rem;
width: 100%;
font-size: .76rem;
color: #fcd34d;
}
.history-derived-note__text {
min-width: 0;
overflow-wrap: anywhere;
}
.history-derived-info-btn {
flex: 0 0 auto;
min-height: 26px;
padding: .2rem .5rem;
border: 1px solid rgba(251, 191, 36, .5);
border-radius: 5px;
background: rgba(120, 53, 15, .24);
color: #fde68a;
font-size: .7rem;
font-weight: 700;
line-height: 1;
}
.history-derived-info-btn:hover,
.history-derived-info-btn:focus-visible {
border-color: rgba(251, 191, 36, .86);
background: rgba(146, 64, 14, .38);
color: #fef3c7;
outline: none;
}
.history-derivation-backdrop {
z-index: 5700;
}
.custom-modal-card.history-derivation-modal {
width: min(620px, calc(100vw - 2rem));
max-height: min(760px, calc(100dvh - 2rem));
overflow-y: auto;
}
.history-derivation-modal__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: .75rem;
margin-bottom: .75rem;
}
.history-derivation-modal__header .custom-modal-title {
margin-bottom: .15rem;
}
.history-derivation-modal__source {
color: #94a3b8;
font-size: .75rem;
}
.history-derivation-steps {
display: flex;
gap: .35rem;
padding: .15rem 0 .55rem;
overflow-x: auto;
}
.history-derivation-step {
flex: 0 0 auto;
min-height: 30px;
padding: .25rem .55rem;
border: 1px solid rgba(148, 163, 184, .32);
border-radius: 5px;
background: rgba(30, 41, 59, .52);
color: #cbd5e1;
font-size: .72rem;
font-weight: 650;
}
.history-derivation-step:hover,
.history-derivation-step:focus-visible {
border-color: rgba(56, 189, 248, .7);
color: #e0f2fe;
outline: none;
}
.history-derivation-step--selected {
border-color: rgba(251, 191, 36, .8);
background: rgba(120, 53, 15, .3);
color: #fef3c7;
}
.history-derivation-selected {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: .75rem;
padding: .5rem 0;
border-top: 1px solid rgba(148, 163, 184, .2);
border-bottom: 1px solid rgba(148, 163, 184, .2);
color: #f8fafc;
}
.history-derivation-selected span {
color: #94a3b8;
font-size: .72rem;
white-space: nowrap;
}
.history-derivation-comparison {
padding-top: .7rem;
}
.history-derivation-comparison + .history-derivation-comparison {
margin-top: .25rem;
border-top: 1px solid rgba(148, 163, 184, .2);
}
.history-derivation-comparison h6 {
margin: 0 0 .35rem;
color: #cbd5e1;
font-size: .78rem;
font-weight: 700;
}
.history-derivation-change {
padding: .48rem 0;
border-bottom: 1px solid rgba(148, 163, 184, .14);
}
.history-derivation-change:last-child {
border-bottom: 0;
}
.history-derivation-change__heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: .75rem;
margin-bottom: .28rem;
color: #e2e8f0;
font-size: .76rem;
}
.history-derivation-change__heading span {
color: #fbbf24;
font-size: .7rem;
text-align: right;
}
.history-derivation-change__value {
display: grid;
grid-template-columns: 54px minmax(0, 1fr);
gap: .4rem;
padding: .08rem 0;
color: #cbd5e1;
font-size: .72rem;
line-height: 1.35;
}
.history-derivation-change__value > span {
color: #64748b;
font-weight: 700;
text-transform: uppercase;
}
.history-derivation-change__value > div {
min-width: 0;
overflow-wrap: anywhere;
}
.history-derivation-no-changes {
padding: .35rem 0;
color: #94a3b8;
font-size: .74rem;
}
.history-item-action,
.history-item-delete {
flex: 0 0 32px;

View File

@@ -100,8 +100,137 @@ window.rt.ensureRouteLayer = function () {
return true;
};
window.rt.clearElevationSegmentSelection = function () {
const selected = window.rt._selectedElevationSegment;
if (!selected) return;
try {
if (selected.line && selected.originalStyle) {
selected.line.setStyle(selected.originalStyle);
}
if (selected.group && Array.isArray(selected.endpoints)) {
selected.endpoints.forEach(function (endpoint) {
selected.group.removeLayer(endpoint);
});
}
if (selected.group && Array.isArray(selected.halos)) {
selected.halos.forEach(function (halo) {
selected.group.removeLayer(halo);
});
}
} catch { }
window.rt._selectedElevationSegment = null;
};
window.rt.ensureElevationSegmentSelectionDismiss = function () {
if (window.rt._elevationSelectionDismissHandler) return;
window.rt._elevationSelectionDismissHandler = function (event) {
const selected = window.rt._selectedElevationSegment;
if (!selected) return;
const selectedPath = selected.line && selected.line._path;
const target = event && event.target;
if (selectedPath && target &&
(target === selectedPath ||
(typeof selectedPath.contains === "function" && selectedPath.contains(target)))) {
return;
}
window.rt.clearElevationSegmentSelection();
};
document.addEventListener(
"click",
window.rt._elevationSelectionDismissHandler,
true);
};
window.rt.ensureElevationSegmentSelectionDismiss();
window.rt.selectElevationSegment = function (
routeId,
line,
coords,
group,
originalStyle,
baseWeight) {
window.rt.clearElevationSegmentSelection();
if (!line || !Array.isArray(coords) || coords.length < 2 || !group) return;
const selectionColor = originalStyle && originalStyle.color
? originalStyle.color
: "#dc2626";
const selectedWeight = Math.max(6, Number(baseWeight || 4) + 2);
const outerHalo = window.L.polyline(coords, {
color: "#111827",
weight: selectedWeight + 7,
opacity: 0.94,
dashArray: null,
lineCap: "round",
interactive: false,
className: "rt-elev-segment-halo rt-elev-segment-halo--outer"
});
const innerHalo = window.L.polyline(coords, {
color: "#f8fafc",
weight: selectedWeight + 4,
opacity: 0.96,
dashArray: null,
lineCap: "round",
interactive: false,
className: "rt-elev-segment-halo rt-elev-segment-halo--inner"
});
outerHalo.addTo(group);
innerHalo.addTo(group);
line.setStyle({
color: selectionColor,
weight: selectedWeight,
opacity: 1,
dashArray: null,
lineCap: "round"
});
const endpointOptions = {
radius: 4,
color: "#ffffff",
weight: 2,
opacity: 1,
fillColor: selectionColor,
fillOpacity: 1,
interactive: false,
className: "rt-elev-segment-endpoint"
};
const endpoints = [
window.L.circleMarker(coords[0], endpointOptions),
window.L.circleMarker(coords[coords.length - 1], endpointOptions)
];
endpoints.forEach(function (endpoint) {
endpoint.addTo(group);
});
try { line.bringToFront(); } catch { }
endpoints.forEach(function (endpoint) {
try { endpoint.bringToFront(); } catch { }
});
window.rt._selectedElevationSegment = {
routeId,
line,
group,
halos: [outerHalo, innerHalo],
endpoints,
originalStyle
};
};
window.rt.clearRoutes = function () {
if (!window.rt.ensureRouteLayer()) return;
window.rt.clearElevationSegmentSelection();
window.rt._routeLayer.clearLayers();
window.rt._routeLines = {};
window.rt._routeCoords = {};
@@ -114,6 +243,11 @@ window.rt.clearRoutes = function () {
window.rt.drawRoute = function (id, coords, options) {
if (!window.rt.ensureRouteLayer()) return;
if (window.rt._selectedElevationSegment &&
window.rt._selectedElevationSegment.routeId === id) {
window.rt.clearElevationSegmentSelection();
}
if (window.rt._routeLines[id]) {
window.rt._routeLayer.removeLayer(window.rt._routeLines[id]);
delete window.rt._routeLines[id];
@@ -241,11 +375,19 @@ window.rt.drawRoute = function (id, coords, options) {
weight,
opacity,
lineCap: lineCap || "round",
className: "rt-elev-segment",
bubblingMouseEvents: false
};
if (dashArray) segmentOpts.dashArray = dashArray;
const segmentLine = buildLine(segmentCoords, segmentOpts);
const originalSegmentStyle = {
color: segmentColor,
weight,
opacity,
lineCap: lineCap || "round",
dashArray: dashArray || null
};
const label = elevationTooltip(segment, 0.5);
if (label) {
@@ -262,6 +404,21 @@ window.rt.drawRoute = function (id, coords, options) {
segmentLine.on("mouseover", updateTooltipAtCursor);
segmentLine.on("mousemove", updateTooltipAtCursor);
segmentLine.on("click", function (e) {
window.rt.selectElevationSegment(
id,
segmentLine,
segmentCoords,
group,
originalSegmentStyle,
weight);
window.requestAnimationFrame(function () {
try {
if (segmentLine._path && typeof segmentLine._path.blur === "function") {
segmentLine._path.blur();
}
} catch { }
});
if (!window.rt.isMobileLayout || !window.rt.isMobileLayout()) return;
updateTooltipAtCursor(e);
try { segmentLine.openTooltip(e && e.latlng); } catch { }