- 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:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
var items = JsonSerializer.Deserialize<List<RegistroHistorialRuta>>(json);
|
||||||
if (items is null)
|
if (items is null)
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
_historialLocal.Clear();
|
_historialLocal.Clear();
|
||||||
_historialLocal.AddRange(items
|
_historialLocal.AddRange(items
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.Id))
|
.Where(x => !string.IsNullOrWhiteSpace(x.Id))
|
||||||
.OrderByDescending(x => x.FechaActualizacionUtc)
|
.OrderByDescending(x => x.FechaActualizacionUtc)
|
||||||
.Take(MaxItemsHistorialLocal));
|
.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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
// =========================================================
|
// =========================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user