- Añadidas alternativas que permiten incorporarse directamente al segundo sentido de una línea circular cuando ofrecen mejor puntuación.

- Corregida la pérdida del historial personal al pulsar Historial inmediatamente después de recargar la aplicación.
- Evitadas escrituras y eliminaciones antes de completar la carga de localStorage.
- Oscurecido el amarillo de la línea 40 para mejorar su visibilidad sobre el mapa.
- Actualizada la versión de la aplicación a v-20260713a.
This commit is contained in:
2026-07-13 15:00:47 +02:00
parent 582ab14138
commit 4c71402e24
5 changed files with 240 additions and 15 deletions

View File

@@ -52,7 +52,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
try { await JS.InvokeVoidAsync("rt.blockLeafletDoubleClick"); } catch (Exception ex) { Console.WriteLine("rt.blockLeafletDoubleClick: " + ex); } try { await JS.InvokeVoidAsync("rt.blockLeafletDoubleClick"); } catch (Exception ex) { Console.WriteLine("rt.blockLeafletDoubleClick: " + ex); }
try { await JS.InvokeVoidAsync("rt.initDraggablePopups"); } catch (Exception ex) { Console.WriteLine("rt.initDraggablePopups: " + ex); } try { await JS.InvokeVoidAsync("rt.initDraggablePopups"); } catch (Exception ex) { Console.WriteLine("rt.initDraggablePopups: " + ex); }
await CargarConfiguracionLocalAsync(); await CargarConfiguracionLocalAsync();
await CargarHistorialLocalAsync(); await AsegurarHistorialLocalCargadoAsync();
await CargarHistorialCompartidoAsync(); await CargarHistorialCompartidoAsync();
await CargarModelosHorariosAsync(); await CargarModelosHorariosAsync();

View File

@@ -24,6 +24,8 @@ public partial class PlanificadorRutas
private bool _omitirRegistroHistorialEnSiguienteCalculo = false; private bool _omitirRegistroHistorialEnSiguienteCalculo = false;
private string? _historialCompartidoPendienteEliminarId; private string? _historialCompartidoPendienteEliminarId;
private ConfiguracionLocalPlanificador? _configuracionTemporalPreviaHistorial; private ConfiguracionLocalPlanificador? _configuracionTemporalPreviaHistorial;
private Task<bool>? _tareaCargaHistorialLocal;
private bool _historialLocalCargado = false;
private async Task ToggleHistorialPanel() private async Task ToggleHistorialPanel()
{ {
@@ -35,41 +37,69 @@ public partial class PlanificadorRutas
return; return;
} }
if (!await AsegurarHistorialLocalCargadoAsync())
{
MostrarMensajeHistorial("No he podido cargar el historial personal. Intentalo de nuevo.", esError: true);
return;
}
_mostrarHistorialRutas = true; _mostrarHistorialRutas = true;
await CargarHistorialCompartidoAsync(); await CargarHistorialCompartidoAsync();
StartRefrescoHistorialCompartido(); StartRefrescoHistorialCompartido();
} }
private async Task CargarHistorialLocalAsync() private async Task<bool> AsegurarHistorialLocalCargadoAsync()
{
if (_historialLocalCargado)
return true;
_tareaCargaHistorialLocal ??= CargarHistorialLocalDesdeNavegadorAsync();
var tareaCarga = _tareaCargaHistorialLocal;
var cargado = await tareaCarga;
if (!cargado && ReferenceEquals(_tareaCargaHistorialLocal, tareaCarga))
_tareaCargaHistorialLocal = null;
return cargado;
}
private async Task<bool> CargarHistorialLocalDesdeNavegadorAsync()
{ {
try try
{ {
var json = await JS.InvokeAsync<string?>("rt.loadLocalValue", ClaveHistorialLocal); var json = await JS.InvokeAsync<string?>("rt.loadLocalValue", ClaveHistorialLocal);
if (string.IsNullOrWhiteSpace(json)) if (!string.IsNullOrWhiteSpace(json))
return; {
var items = JsonSerializer.Deserialize<List<RegistroHistorialRuta>>(json);
if (items is null)
return false;
var items = JsonSerializer.Deserialize<List<RegistroHistorialRuta>>(json); _historialLocal.Clear();
if (items is null) _historialLocal.AddRange(items
return; .Where(x => !string.IsNullOrWhiteSpace(x.Id))
.OrderByDescending(x => x.FechaActualizacionUtc)
_historialLocal.Clear(); .Take(MaxItemsHistorialLocal));
_historialLocal.AddRange(items }
.Where(x => !string.IsNullOrWhiteSpace(x.Id))
.OrderByDescending(x => x.FechaActualizacionUtc)
.Take(MaxItemsHistorialLocal));
_historialLocalSeleccionadoId = _historialLocal.FirstOrDefault()?.Id ?? string.Empty; _historialLocalSeleccionadoId = _historialLocal.FirstOrDefault()?.Id ?? string.Empty;
_historialLocalCargado = true;
return true;
} }
catch (JSException) catch (JSException)
{ {
return false;
} }
catch (JsonException) catch (JsonException)
{ {
return false;
} }
} }
private async Task PersistirHistorialLocalAsync() private async Task PersistirHistorialLocalAsync()
{ {
if (!_historialLocalCargado)
return;
try try
{ {
var json = JsonSerializer.Serialize(_historialLocal); var json = JsonSerializer.Serialize(_historialLocal);
@@ -157,6 +187,9 @@ public partial class PlanificadorRutas
private async Task RegistrarEscenarioActualEnHistorialLocalAsync() private async Task RegistrarEscenarioActualEnHistorialLocalAsync()
{ {
if (!await AsegurarHistorialLocalCargadoAsync())
return;
var actual = ConstruirRegistroHistorialActual(); var actual = ConstruirRegistroHistorialActual();
if (actual is null) if (actual is null)
return; return;
@@ -287,6 +320,9 @@ public partial class PlanificadorRutas
private async Task EliminarHistorialLocalAsync(string id) private async Task EliminarHistorialLocalAsync(string id)
{ {
if (!await AsegurarHistorialLocalCargadoAsync())
return;
var item = _historialLocal.FirstOrDefault(x => x.Id == id); var item = _historialLocal.FirstOrDefault(x => x.Id == id);
if (item is null) if (item is null)
return; return;

View File

@@ -1261,7 +1261,7 @@ public partial class PlanificadorRutas : ComponentBase
private bool _elevPopupMinimizado = false; private bool _elevPopupMinimizado = false;
private const string _versionApp = "(v-20260710a)"; private const string _versionApp = "(v-20260713a)";
private double? _paradaElevG; private double? _paradaElevG;

View File

@@ -244,6 +244,33 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
Math.Max(16, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 10), Math.Max(16, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 10),
parametros); parametros);
} }
var iniciosCircularesDirectos = BuscarIniciosDirectosTrasCircularConHorario(
candidatasBus,
candidatasOrigen,
instanteSalida,
parametros);
if (iniciosCircularesDirectos.Count > 0)
{
var porFirma = candidatasBus.ToDictionary(
candidata => UtilidadesTransitoDbus.FirmaRutaBus(candidata.RutaBus),
StringComparer.OrdinalIgnoreCase);
foreach (var inicioDirecto in iniciosCircularesDirectos)
{
var firma = UtilidadesTransitoDbus.FirmaRutaBus(inicioDirecto.RutaBus);
if (!porFirma.TryGetValue(firma, out var actual) ||
ResultadoBusquedaEsMejor(inicioDirecto, actual))
{
porFirma[firma] = inicioDirecto;
}
}
// Estos recortes ya proceden del pool seleccionado. No se vuelve a podar para que
// una familia con muchos itinerarios equivalentes no los elimine otra vez.
candidatasBus = porFirma.Values.ToList();
}
} }
candidatasBus = candidatasBus candidatasBus = candidatasBus
@@ -2326,6 +2353,156 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
return total; return total;
} }
/// <summary>
/// Conserva una variante que entra directamente en el segundo sentido de una circular cuando
/// una parada inmediata permite coger el mismo servicio con mejor score equilibrado.
/// </summary>
private List<ResultadoBusquedaRutaBusDbus> BuscarIniciosDirectosTrasCircularConHorario(
IReadOnlyList<ResultadoBusquedaRutaBusDbus> candidatasBase,
IReadOnlyList<ParadaCandidata> candidatasOrigen,
DateTime instanteSalida,
ParametrosPlanificadorRuta parametros)
{
if (candidatasBase.Count == 0 || candidatasOrigen.Count == 0)
return new();
var origenesPorCodigo = candidatasOrigen
.GroupBy(x => x.Parada.Codigo, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
grupo => grupo.Key,
grupo => grupo.OrderBy(x => x.TiempoAPie).ThenBy(x => x.DistanciaAPie).First(),
StringComparer.OrdinalIgnoreCase);
var mejores = new Dictionary<string, ResultadoBusquedaRutaBusDbus>(StringComparer.OrdinalIgnoreCase);
foreach (var candidataBase in candidatasBase)
{
var tramosBase = candidataBase.RutaBus.Tramos;
if (tramosBase.Count < 2)
continue;
var primero = tramosBase[0];
var segundo = tramosBase[1];
if (!primero.EsValido ||
!segundo.EsValido ||
!EsContinuidadCircular(primero.Variante, segundo.Variante) ||
!string.Equals(primero.CodigoParadaFin, segundo.CodigoParadaInicio, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var llegadaPrimeraParada = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, candidataBase.TiempoPieInicio);
if (!ResolverSegmentoHorario(
primero.CodigoParadaInicio,
primero.CodigoParadaFin,
UtilidadesTransitoDbus.LineaCodigoStr(primero.Variante),
primero.Variante.CodigoItinerario,
UtilidadesTransitoDbus.NormalizarSentidoCodigo(primero.Variante.Sentido?.Codigo),
llegadaPrimeraParada,
out _,
out var llegadaIntermedia) ||
!ResolverSegmentoHorario(
segundo.CodigoParadaInicio,
segundo.CodigoParadaFin,
UtilidadesTransitoDbus.LineaCodigoStr(segundo.Variante),
segundo.Variante.CodigoItinerario,
UtilidadesTransitoDbus.NormalizarSentidoCodigo(segundo.Variante.Sentido?.Codigo),
llegadaIntermedia,
out _,
out var llegadaOriginalSegundo))
{
continue;
}
ResultadoBusquedaRutaBusDbus? mejorRecorte = null;
var indiceMaximo = Math.Min(
segundo.IndiceFin - 1,
segundo.IndiceInicio + VentanaRefinamientoParadaInicio);
for (var indiceSubida = segundo.IndiceInicio + 1; indiceSubida <= indiceMaximo; indiceSubida++)
{
var codigoSubida = segundo.Variante.CodigosParadas[indiceSubida];
if (!origenesPorCodigo.TryGetValue(codigoSubida, out var origenDirecto) ||
origenDirecto.DistanciaAPie > candidataBase.DistanciaPieInicio + MaxExcesoAccesoDirectoCircularMetros)
{
continue;
}
var llegadaDirectaParada = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, origenDirecto.TiempoAPie);
if (!ResolverSegmentoHorario(
codigoSubida,
segundo.CodigoParadaFin,
UtilidadesTransitoDbus.LineaCodigoStr(segundo.Variante),
segundo.Variante.CodigoItinerario,
UtilidadesTransitoDbus.NormalizarSentidoCodigo(segundo.Variante.Sentido?.Codigo),
llegadaDirectaParada,
out var salidaDirecta,
out var llegadaDirectaSegundo) ||
llegadaDirectaSegundo > llegadaOriginalSegundo.AddSeconds(ToleranciaMismoBusCircularSegundos))
{
continue;
}
var rutaDirecta = ClonarRutaBus(candidataBase.RutaBus);
rutaDirecta.Tramos.RemoveAt(0);
rutaDirecta.Tramos[0].IndiceInicio = indiceSubida;
rutaDirecta.NumeroTransbordos = UtilidadesTransitoDbus.ContarTransbordosReales(rutaDirecta.Tramos);
rutaDirecta.CodigosParadasTransbordo = ConstruirCodigosTransbordo(rutaDirecta.Tramos);
rutaDirecta.DistanciaTotalBus = 0;
var diferenciaLlegada = (llegadaDirectaSegundo - llegadaOriginalSegundo).TotalSeconds;
var duracionEstimada = Math.Max(0, candidataBase.DuracionEstimadaSegundos + diferenciaLlegada);
var esperaInicial = Math.Max(0, (salidaDirecta - llegadaDirectaParada).TotalSeconds);
var recorte = new ResultadoBusquedaRutaBusDbus
{
ParadaOrigen = origenDirecto.Parada,
ParadaDestino = candidataBase.ParadaDestino,
DistanciaPieInicio = origenDirecto.DistanciaAPie,
TiempoPieInicio = origenDirecto.TiempoAPie,
DistanciaPieFin = candidataBase.DistanciaPieFin,
TiempoPieFin = candidataBase.TiempoPieFin,
DistanciaPieTransbordos = candidataBase.DistanciaPieTransbordos,
TiempoEsperaInicialSegundos = esperaInicial,
TiempoEsperaTransbordosSegundos = candidataBase.TiempoEsperaTransbordosSegundos,
DuracionEstimadaSegundos = duracionEstimada,
PuntuacionCoste = UtilidadesTransitoDbus.CalcularCosteConPenalizacionesRankingSegundos(
duracionEstimada,
parametros,
rutaDirecta.NumeroTransbordos,
origenDirecto.DistanciaAPie,
candidataBase.DistanciaPieTransbordos,
candidataBase.DistanciaPieFin),
RutaBus = rutaDirecta
};
if (CalcularScoreBusquedaEquilibrado(recorte, parametros) + 1e-6 >=
CalcularScoreBusquedaEquilibrado(candidataBase, parametros))
{
continue;
}
if (mejorRecorte is null ||
CalcularScoreBusquedaEquilibrado(recorte, parametros) + 1e-6 <
CalcularScoreBusquedaEquilibrado(mejorRecorte, parametros))
{
mejorRecorte = recorte;
}
}
if (mejorRecorte is null)
continue;
var firma = UtilidadesTransitoDbus.FirmaRutaBus(mejorRecorte.RutaBus);
if (!mejores.TryGetValue(firma, out var actual) ||
CalcularScoreBusquedaEquilibrado(mejorRecorte, parametros) + 1e-6 <
CalcularScoreBusquedaEquilibrado(actual, parametros))
{
mejores[firma] = mejorRecorte;
}
}
return mejores.Values.ToList();
}
/// <summary> /// <summary>
/// Evita empezar una circular yendo hacia atras si se puede coger directamente el tramo siguiente. /// Evita empezar una circular yendo hacia atras si se puede coger directamente el tramo siguiente.
/// </summary> /// </summary>

View File

@@ -13,6 +13,8 @@ namespace RutasDBUS.Servicios.RedBus;
public class RedBusServicio : IRedBusServicio public class RedBusServicio : IRedBusServicio
{ {
private const string ColorVisualLinea40Argb = "FF9A7B00";
private readonly IWebHostEnvironment _entorno; private readonly IWebHostEnvironment _entorno;
private readonly OpcionesRedBus _opciones; private readonly OpcionesRedBus _opciones;
private readonly ILogger<RedBusServicio> _logger; private readonly ILogger<RedBusServicio> _logger;
@@ -98,7 +100,11 @@ public class RedBusServicio : IRedBusServicio
_paradas.AddRange(network.Paradas); _paradas.AddRange(network.Paradas);
foreach (var kvp in network.Trayectos) foreach (var kvp in network.Trayectos)
_trayectos[kvp.Key] = kvp.Value; _trayectos[kvp.Key] = kvp.Value;
_lineas.AddRange(network.Lineas); foreach (var linea in network.Lineas)
{
AjustarColorVisualLinea(linea);
_lineas.Add(linea);
}
if (_paradas.Count == 0 || _lineas.Count == 0) if (_paradas.Count == 0 || _lineas.Count == 0)
{ {
@@ -174,6 +180,12 @@ public class RedBusServicio : IRedBusServicio
_gridParadas.Clear(); _gridParadas.Clear();
} }
private static void AjustarColorVisualLinea(LineaBus linea)
{
if (string.Equals(linea.Codigo, "40", StringComparison.OrdinalIgnoreCase))
linea.ColorArgb = ColorVisualLinea40Argb;
}
// ========================================================= // =========================================================
// ZIP // ZIP
// ========================================================= // =========================================================