Initial clean RutasDBUS app

This commit is contained in:
2026-07-07 14:18:51 +02:00
commit 8b106a8319
76 changed files with 28216 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="RutasDBUS.styles.css" />
<link rel="stylesheet" href="app.css?v=20260626b" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="app.js?v=20260626b" defer></script>
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,23 @@
@inherits LayoutComponentBase
<div class="page">
@* <div class="sidebar">
<NavMenu />
</div> *@
<main>
@* <div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
*@
<article class="content">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

View File

@@ -0,0 +1,96 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

@@ -0,0 +1,118 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">RutasDBUS</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>
@* <div class="map-overlay-card">
<strong>Flujo de selección</strong>
<p>@TextoPasoActual</p>
<div class="mini">
<div>
<b>Origen:</b>
@if (_paradaOrigenSeleccionada is not null)
{
<span class="muted">
@_paradaOrigenSeleccionada.Nombre (@_paradaOrigenSeleccionada.Codigo)
</span>
}
else if (_coordenadasOrigen is not null)
{
<span class="muted">Seleccionado en el mapa</span>
}
else
{
<span class="muted">Pendiente</span>
}
</div>
<div>
<b>Destino:</b>
@if (_paradaDestinoSeleccionada is not null)
{
<span class="muted">
@_paradaDestinoSeleccionada.Nombre (@_paradaDestinoSeleccionada.Codigo)
</span>
}
else if (_coordenadasDestino is not null)
{
<span class="muted">Seleccionado en el mapa</span>
}
else
{
<span class="muted">Pendiente</span>
}
</div>
@if (_primerTransbordoseleccionado is not null)
{
<div>
<b>1er Transbordo:</b>
<span class="muted">
@_primerTransbordoseleccionado.Nombre (@_primerTransbordoseleccionado.Codigo)
</span>
</div>
}
@* Info de la última parada clicada *@
@* @if (_paradaClicada is not null)
{
<hr style="border-color:rgba(55,65,81,.6);margin:.35rem 0;" />
<div>
<b>Parada clicada:</b>
<span class="muted">
@_paradaClicada.Nombre (@_paradaClicada.Codigo)
</span>
</div>
<div class="mini">
@if (!string.IsNullOrWhiteSpace(_paradaClicada.Abreviatura))
{
<span>Abrev.: @_paradaClicada.Abreviatura</span>
}
@if (!string.IsNullOrWhiteSpace(_paradaClicada.NombreCorto))
{
<span> · Corto: @_paradaClicada.NombreCorto</span>
}
@if (_paradaClicada.Altura > 0)
{
<span> · Altura: @_paradaClicada.Altura.ToString("0") m</span>
}
</div>
}
</div>
<div class="mt-2">
<button class="btn-ghost btn-sm"
style="padding:.15rem .6rem;font-size:.72rem"
@onclick="ReiniciarSeleccion">
Limpiar selección
</button>
</div>
</div> *@

View File

@@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -0,0 +1,496 @@
using System.Collections.Concurrent;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using LeafletForBlazor;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus;
using RutasDBUS.Servicios;
using RutasDBUS.Servicios.Caminata;
using RutasDBUS.Servicios.Horarios;
using RutasDBUS.Servicios.Planificacion;
using RutasDBUS.Servicios.RedBus;
namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas : ComponentBase, IDisposable
{
private DotNetObjectReference<PlanificadorRutas>? _dotRef;
private enum HorarioOrden
{
PorHora = 0,
PorItinerario = 1
}
private HorarioOrden _horarioOrden = HorarioOrden.PorHora;
private string HorarioOrdenIcono =>
_horarioOrden == HorarioOrden.PorHora ? "\u23F0" : "\U0001F68C";
private string HorarioOrdenTooltip =>
_horarioOrden == HorarioOrden.PorHora
? "Orden actual: por hora. Pulsar para ordenar por itinerario"
: "Orden actual: por itinerario. Pulsar para ordenar por hora";
/// <summary>
/// Gestiona after render.
/// </summary>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_dotRef = DotNetObjectReference.Create(this);
try { await JS.InvokeVoidAsync("initBrilloMapa", _dotRef); } catch (Exception ex) { Console.WriteLine("initBrilloMapa: " + ex); }
try { await JS.InvokeVoidAsync("initAltInspector", _dotRef); } catch (Exception ex) { Console.WriteLine("initAltInspector: " + ex); }
try { await JS.InvokeVoidAsync("rt.setDotNetHelper", _dotRef); } catch (Exception ex) { Console.WriteLine("rt.setDotNetHelper: " + 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); }
await CargarConfiguracionLocalAsync();
await CargarHistorialLocalAsync();
await CargarHistorialCompartidoAsync();
await CargarModelosHorariosAsync();
if (_usarHoraSimulada)
await RefrescarHoraUIAsync();
else
StartClock();
if (_usarHorariosTeoricos)
await EnsureHorariosLoadedAsync();
_tareaCargaRed = RedBusServicio.CargarRedAsync();
StateHasChanged();
return;
}
_tareaCargaRed ??= RedBusServicio.CargarRedAsync();
if (!_tareaCargaRed.IsCompleted) return;
await _tareaCargaRed;
if (RedBusServicio.Paradas.Count > 0)
EnsureBusIndexBuilt();
if (!_zoomLimitsAplicados && _mapa is not null)
{
_zoomLimitsAplicados = true;
await JS.InvokeVoidAsync("rt.setZoomLimits", 3, 19);
}
try { await JS.InvokeVoidAsync("rt.addZoomIndicator", new { fakeZoom19 = true }); } catch { }
if (!_paradasPintadas && !_pintandoParadas && _mapa is not null && RedBusServicio.Paradas.Count > 0)
{
_pintandoParadas = true;
try
{
if (_mostrarParadas)
await DibujarParadasAsync();
else
await OcultarParadasAsync();
_paradasPintadas = true;
}
finally
{
_pintandoParadas = false;
}
StateHasChanged();
if (_itItems.Count == 0 && RedBusServicio.Paradas.Count > 0)
{
BuildItinerariosDropdown();
SincronizarSeleccionItinerarioConFiltro();
StateHasChanged();
}
}
// ✅ ESTO VA FUERA
if (_scrollHorariosPendiente)
{
_scrollHorariosPendiente = false;
try { await JS.InvokeVoidAsync("rt.scrollToHorarioProximo"); } catch { }
}
}
private bool _scrollHorariosPendiente = false;
// =============================
// PREVIEWS ITINERARIOS (por sentido real)
// =============================
// ids reales pintados (subIds): it_xxx_IV_I, it_xxx_IV_V, it_xxx_I_3, etc.
private readonly HashSet<string> _itPreviewActivos = new(StringComparer.OrdinalIgnoreCase);
// Mapa: clave lógica "26.12-I" -> ids reales pintados (pueden ser 1 o varios)
private readonly Dictionary<string, HashSet<string>> _itPreviewIdsPorClave =
new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gestiona clave.
/// </summary>
private static string ItClave(InfoVariante v)
{
var linea = v.Linea?.Codigo?.ToString() ?? "?";
var itin = v.CodigoItinerario.ToString(CultureInfo.InvariantCulture);
var sentido = NormalizarSentidoCodigo(v.Sentido?.Codigo); // I / V
return $"{linea}.{itin}-{sentido}";
}
/// <summary>
/// Gestiona vista previa.
/// </summary>
private void RegistrarPreview(string clave, string subId)
{
_itPreviewActivos.Add(subId);
if (!_itPreviewIdsPorClave.TryGetValue(clave, out var set))
{
set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_itPreviewIdsPorClave[clave] = set;
}
set.Add(subId);
}
/// <summary>
/// Obtiene seleccionada it.
/// </summary>
private ElementoItinerario? GetSelectedIt()
=> _itItems.FirstOrDefault(x => x.Id.Equals(_itSeleccionadoId, StringComparison.OrdinalIgnoreCase));
/// <summary>
/// Pinta it seleccionado.
/// </summary>
private async Task PintarItSeleccionado()
{
var it = GetSelectedIt();
if (it is null) return;
await PintarItItemAsync(it);
}
/// <summary>
/// Quita it seleccionado.
/// </summary>
private async Task QuitarItSeleccionado()
{
var it = GetSelectedIt();
if (it is null) return;
// claves a borrar:
// - si seleccionas IV => borra I y V
// - si seleccionas I/V => borra solo ese sentido
var claves = new List<string>();
if (it.Variantes.Count == 2)
{
claves.Add(ItClave(it.Variantes[0])); // I
claves.Add(ItClave(it.Variantes[1])); // V
}
else if (it.Variantes.Count == 1)
{
claves.Add(ItClave(it.Variantes[0])); // solo I o solo V
}
else
{
return;
}
foreach (var clave in claves)
{
if (!_itPreviewIdsPorClave.TryGetValue(clave, out var ids) || ids.Count == 0)
continue;
// borrar TODOS los subIds reales asociados a ese sentido (aunque se pintaran desde IV)
foreach (var id in ids.ToList())
{
await JS.InvokeVoidAsync("rt.drawPreviewLine", id, new List<double[]>(), new { });
await JS.InvokeVoidAsync("rt.toggleItStops", id, new List<object>(), new { });
QuitarParadasItinerarioVisibles(id);
_itPreviewActivos.Remove(id);
ids.Remove(id);
// si el popup abierto corresponde a lo borrado, cerrarlo
if (!string.IsNullOrWhiteSpace(_itinerarioClicadoSubId) &&
string.Equals(_itinerarioClicadoSubId, id, StringComparison.OrdinalIgnoreCase))
{
_itinerarioClicado = null;
_itinerarioClicadoSubId = null;
}
}
if (ids.Count == 0)
_itPreviewIdsPorClave.Remove(clave);
}
}
/// <summary>
/// Gestiona todas las vistas previas.
/// </summary>
private async Task CerrarTodasLasPreviewsAsync()
{
try { await JS.InvokeVoidAsync("rt.clearPreview"); } catch { }
_itPreviewActivos.Clear();
_itPreviewIdsPorClave.Clear();
LimpiarParadasItinerarioVisibles();
_previewLineasActivas.Clear(); // <- esto ya lo usas para los "full_..."
}
private bool _inspectorActivo = false;
private bool _popupInspectorVisible = false;
private double _inspectorLat, _inspectorLon;
private double? _inspectorElev;
private double? _inspectorElevIdee;
/// <summary>
/// Gestiona vista previa itinerario clic.
/// </summary>
[JSInvokable]
public async Task OnPreviewItinerarioClick(string previewId)
{
if (string.IsNullOrWhiteSpace(previewId))
return;
try
{
// Cerrar popup de parada si está abierto
// Guardamos el subId real pulsado (ej: it_xxx_I o full_...)
_itinerarioClicadoSubId = previewId;
// 1) Si viene de previews del desplegable/filtro (it_...)
// Puede venir:
// - id directo: "it_26_12_I_3"
// - subId combinado: "it_26_12_IV_I" / "it_26_12_IV_V"
ElementoItinerario? encontrado = null;
// coincidencia exacta
encontrado = _itItems.FirstOrDefault(x =>
x.Id.Equals(previewId, StringComparison.OrdinalIgnoreCase));
// si no, probar quitando sufijo _I / _V de subId combinado
if (encontrado is null)
{
if (previewId.EndsWith("_I", StringComparison.OrdinalIgnoreCase) ||
previewId.EndsWith("_V", StringComparison.OrdinalIgnoreCase))
{
var baseId = previewId.Substring(0, previewId.Length - 2);
encontrado = _itItems.FirstOrDefault(x =>
x.Id.Equals(baseId, StringComparison.OrdinalIgnoreCase));
}
}
// 2) Si no se encuentra, puede ser preview azul de tramo de ruta: "full_..."
// En ese caso intentamos construir info simple desde el id.
if (encontrado is null && previewId.StartsWith("full_", StringComparison.OrdinalIgnoreCase))
{
// formato: full_{linea}_{itin}_{sentido}_{variante}
var parts = previewId.Split('_', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 5)
{
var linea = parts[1];
var itinTxt = parts[2];
var sentidoRaw = parts[3]; // "Ida"/"Vuelta" o código
var varTxt = parts[4];
int.TryParse(itinTxt, out var itinNum);
var sentido = NormalizarSentidoCodigo(sentidoRaw);
var info = _infoVariantes.FirstOrDefault(v =>
string.Equals(v.Linea.Codigo?.ToString(), linea, StringComparison.OrdinalIgnoreCase) &&
v.CodigoItinerario == itinNum &&
string.Equals(NormalizarSentidoCodigo(v.Sentido?.Codigo), sentido, StringComparison.OrdinalIgnoreCase) &&
string.Equals(v.Variante.Codigo.ToString(), varTxt, StringComparison.OrdinalIgnoreCase));
if (info is not null)
{
encontrado = new ElementoItinerario
{
Id = previewId,
Text = $"{linea}.{itinNum}-{sentido} — {info.Linea.Nombre}",
Variantes = new List<InfoVariante> { info }
};
}
}
}
_itinerarioClicado = encontrado;
await InvokeAsync(StateHasChanged);
}
catch
{
// silencioso
}
}
/// <summary>
/// Abre la información de una parada destacada pulsada directamente sobre la solución o el itinerario.
/// </summary>
[JSInvokable]
public async Task OnHighlightedStopClick(string codigoParada)
{
if (string.IsNullOrWhiteSpace(codigoParada))
return;
if (!_paradaByCode.TryGetValue(codigoParada, out var parada))
return;
await MostrarPopupParadaAsync(parada);
}
/// <summary>
/// Gestiona inspector activo.
/// </summary>
[JSInvokable]
public void SetInspectorActivo(bool activo)
{
_inspectorActivo = activo;
StateHasChanged();
}
/// <summary>
/// Gestiona raton move mapa.
/// </summary>
private void OnMouseMoveMapa(RealTimeMap.ClicksMapArgs args)
{
_coordsMouse = (args.location.latitude, args.location.longitude);
StateHasChanged();
}
/// <summary>
/// Gestiona brillo.
/// </summary>
[JSInvokable]
public async Task AlternarBrillo()
{
_nivelBrilloMapa++;
if (_nivelBrilloMapa > 4) _nivelBrilloMapa = 0;
await PersistirConfiguracionAsync();
StateHasChanged();
}
/// <summary>
/// Dibuja paradas.
/// </summary>
private async Task DibujarParadasAsync()
{
if (_mapa is null) return;
await _mapa.Geometric.DisplayPointsFromArray.deleteAllPoints();
var simboloParada = new RealTimeMap.PointSymbol
{
color = "black",
fillColor = "yellow",
radius = 5,
opacity = 0.9,
fillOpacity = 0.8,
weight = 1
};
foreach (var parada in RedBusServicio.Paradas)
{
await _mapa.Geometric.DisplayPointsFromArray.addPoint(
new[] { parada.Latitud, parada.Longitud },
simboloParada);
}
await SincronizarMarcadoresOrigenDestinoJsAsync();
}
// =========================================================
// Walking caché (OSRM/Valhalla)
// Nota: el "dur" devuelto NO se usa para tiempos finales.
// El tiempo a pie SIEMPRE sale de EstimarDuracionPieSegundos(dist)
// =========================================================
private async Task<(double[,]? line, double dist, double dur)> WalkOsrmCached(double lat1, double lon1, double lat2, double lon2)
{
var key = WalkKey("osrm", lat1, lon1, lat2, lon2);
if (_walkCache.TryGetValue(key, out var v))
return v;
try
{
var (line, dist, dur) = await EsrmCliente.ObtenerRutaAPieAsync(lat1, lon1, lat2, lon2);
// ✅ Solo cachear si es un resultado "bueno"
if (line is not null && dist > 0)
{
v = (line, dist, dur);
_walkCache[key] = v;
return v;
}
// ❌ si viene mal, NO lo cacheamos
return (null, 0, 0);
}
catch
{
// ❌ no cachear fallos
return (null, 0, 0);
}
}
private async Task<(double[,]? line, double dist, double dur)> WalkValhallaCached(double lat1, double lon1, double lat2, double lon2)
{
var key = WalkKey("valhalla", lat1, lon1, lat2, lon2);
if (_walkCache.TryGetValue(key, out var v))
return v;
try
{
var (line, dist, dur) = await ClienteValhalla.ObtenerRutaAsync(lat1, lon1, lat2, lon2, costing: "pedestrian");
// ✅ Solo cachear si es un resultado "bueno"
if (line is not null && dist > 0)
{
v = (line, dist, dur);
_walkCache[key] = v;
return v;
}
return (null, 0, 0);
}
catch
{
return (null, 0, 0);
}
}
/// <summary>
/// Gestiona linea recta origen destino.
/// </summary>
private double DistanciaLineaRectaOrigenDestino()
{
if (_coordenadasOrigen is null || _coordenadasDestino is null) return 0;
return CalcularDistanciaMetros(
_coordenadasOrigen[0], _coordenadasOrigen[1],
_coordenadasDestino[0], _coordenadasDestino[1]);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,299 @@
using System.Globalization;
using RutasDBUS.Modelos.Geocodificacion;
using RutasDBUS.Modelos.RedBus;
namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas
{
private enum CampoBusquedaMapa
{
Origen,
Destino
}
private sealed class SugerenciaBusquedaMapa
{
public string Id { get; init; } = "";
public string TextoEntrada { get; init; } = "";
public string Titulo { get; init; } = "";
public string Subtitulo { get; init; } = "";
public string Tipo { get; init; } = "";
public string Fuente { get; init; } = "";
public double Latitud { get; init; }
public double Longitud { get; init; }
}
private readonly List<SugerenciaBusquedaMapa> _sugerenciasBusquedaOrigen = new();
private readonly List<SugerenciaBusquedaMapa> _sugerenciasBusquedaDestino = new();
private CampoBusquedaMapa? _campoBusquedaActivo;
private SugerenciaBusquedaMapa? _ubicacionOrigenSeleccionada;
private SugerenciaBusquedaMapa? _ubicacionDestinoSeleccionada;
private int _versionSugerenciasBusqueda;
private CancellationTokenSource? _ctsSugerenciasBusqueda;
private IReadOnlyList<SugerenciaBusquedaMapa> SugerenciasOrigenVisibles =>
_campoBusquedaActivo == CampoBusquedaMapa.Origen
? _sugerenciasBusquedaOrigen
: Array.Empty<SugerenciaBusquedaMapa>();
private IReadOnlyList<SugerenciaBusquedaMapa> SugerenciasDestinoVisibles =>
_campoBusquedaActivo == CampoBusquedaMapa.Destino
? _sugerenciasBusquedaDestino
: Array.Empty<SugerenciaBusquedaMapa>();
private Task ActualizarSugerenciasOrigenAsync()
=> ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa.Origen);
private Task ActualizarSugerenciasDestinoAsync()
=> ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa.Destino);
private Task MostrarSugerenciasOrigenAsync()
=> ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa.Origen);
private Task MostrarSugerenciasDestinoAsync()
=> ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa.Destino);
private async Task ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa campo)
{
_campoBusquedaActivo = campo;
InvalidarUbicacionSeleccionadaSiEditada(campo);
var texto = ObtenerTextoBusqueda(campo);
var version = ++_versionSugerenciasBusqueda;
_ctsSugerenciasBusqueda?.Cancel();
_ctsSugerenciasBusqueda = new CancellationTokenSource();
var ct = _ctsSugerenciasBusqueda.Token;
if (string.IsNullOrWhiteSpace(texto) || texto.Trim().Length < 2)
{
AsignarSugerenciasBusqueda(campo, Array.Empty<SugerenciaBusquedaMapa>());
StateHasChanged();
return;
}
try
{
await Task.Delay(120, ct);
var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 10, ct);
if (ct.IsCancellationRequested || version != _versionSugerenciasBusqueda)
return;
AsignarSugerenciasBusqueda(campo, sugerencias);
StateHasChanged();
}
catch (OperationCanceledException)
{
}
catch
{
AsignarSugerenciasBusqueda(campo, Array.Empty<SugerenciaBusquedaMapa>());
StateHasChanged();
}
}
private async Task SeleccionarSugerenciaBusquedaAsync(
CampoBusquedaMapa campo,
SugerenciaBusquedaMapa sugerencia)
{
if (campo == CampoBusquedaMapa.Origen)
{
_textoBusquedaOrigen = sugerencia.TextoEntrada;
_coordenadasOrigen = new[] { sugerencia.Latitud, sugerencia.Longitud };
_ubicacionOrigenSeleccionada = sugerencia;
_sugerenciasBusquedaOrigen.Clear();
}
else
{
_textoBusquedaDestino = sugerencia.TextoEntrada;
_coordenadasDestino = new[] { sugerencia.Latitud, sugerencia.Longitud };
_ubicacionDestinoSeleccionada = sugerencia;
_sugerenciasBusquedaDestino.Clear();
}
_campoBusquedaActivo = null;
_mensajeRuta = string.Empty;
_hayRutaValida = false;
await SincronizarMarcadoresOrigenDestinoJsAsync();
StateHasChanged();
}
private async Task<SugerenciaBusquedaMapa?> ResolverUbicacionBusquedaAsync(
CampoBusquedaMapa campo,
CancellationToken cancellationToken = default)
{
var texto = ObtenerTextoBusqueda(campo);
if (string.IsNullOrWhiteSpace(texto))
return null;
var seleccion = ObtenerUbicacionSeleccionada(campo);
if (seleccion is not null &&
string.Equals(texto.Trim(), seleccion.TextoEntrada, StringComparison.OrdinalIgnoreCase))
{
return seleccion;
}
var parada = ResolverParadaDesdeTexto(texto);
if (parada is not null)
return CrearSugerenciaParada(parada);
var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 1, cancellationToken);
return sugerencias.FirstOrDefault();
}
private async Task<IReadOnlyList<SugerenciaBusquedaMapa>> CrearSugerenciasBusquedaAsync(
string texto,
int maxResultados,
CancellationToken cancellationToken)
{
var paradas = BuscarSugerenciasParadas(texto, 5);
var lugares = await GeocodificadorLocal.BuscarAsync(texto, Math.Max(6, maxResultados), cancellationToken);
return paradas
.Concat(lugares.Select(CrearSugerenciaLugar))
.GroupBy(sugerencia => NormalizarClaveSugerencia(sugerencia), StringComparer.Ordinal)
.Select(grupo => grupo.First())
.Take(Math.Clamp(maxResultados, 1, 20))
.ToList();
}
private IReadOnlyList<SugerenciaBusquedaMapa> BuscarSugerenciasParadas(string texto, int maxResultados)
{
var consulta = NormalizeForSearch(texto);
var tokens = consulta
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (tokens.Length == 0)
return Array.Empty<SugerenciaBusquedaMapa>();
return RedBusServicio.Paradas
.Select(parada => new
{
Parada = parada,
Puntuacion = PuntuarParada(parada, consulta, tokens)
})
.Where(x => x.Puntuacion < int.MaxValue)
.OrderBy(x => x.Puntuacion)
.ThenBy(x => x.Parada.Nombre, StringComparer.CurrentCultureIgnoreCase)
.Take(Math.Clamp(maxResultados, 1, 12))
.Select(x => CrearSugerenciaParada(x.Parada))
.ToList();
}
private static int PuntuarParada(Parada parada, string consulta, IReadOnlyList<string> tokens)
{
var codigo = NormalizeForSearch(CodigoParadaFormateado(parada.Codigo));
var codigoRaw = NormalizeForSearch(parada.Codigo);
var nombre = NormalizeForSearch(parada.Nombre);
var nombreCorto = NormalizeForSearch(parada.NombreCorto);
var abreviatura = NormalizeForSearch(parada.Abreviatura);
var texto = $"{codigo} {codigoRaw} {nombre} {nombreCorto} {abreviatura}";
foreach (var token in tokens)
{
if (!texto.Contains(token, StringComparison.Ordinal))
return int.MaxValue;
}
if (codigo.Equals(consulta, StringComparison.Ordinal) ||
codigoRaw.Equals(consulta, StringComparison.Ordinal))
{
return 0;
}
if (nombre.Equals(consulta, StringComparison.Ordinal))
return 8;
if (nombre.StartsWith(consulta, StringComparison.Ordinal) ||
nombreCorto.StartsWith(consulta, StringComparison.Ordinal) ||
abreviatura.StartsWith(consulta, StringComparison.Ordinal))
{
return 15;
}
return 45 + Math.Min(40, nombre.Length / 3);
}
private static SugerenciaBusquedaMapa CrearSugerenciaParada(Parada parada)
{
var codigo = CodigoParadaFormateado(parada.Codigo);
var texto = $"{parada.Nombre} ({codigo})";
return new SugerenciaBusquedaMapa
{
Id = "parada:" + parada.Codigo,
TextoEntrada = texto,
Titulo = texto,
Subtitulo = "Parada DBUS",
Tipo = "Parada",
Fuente = "DBUS",
Latitud = parada.Latitud,
Longitud = parada.Longitud
};
}
private static SugerenciaBusquedaMapa CrearSugerenciaLugar(LugarGeocodificado lugar)
{
return new SugerenciaBusquedaMapa
{
Id = lugar.Id,
TextoEntrada = lugar.Nombre,
Titulo = lugar.Nombre,
Subtitulo = string.IsNullOrWhiteSpace(lugar.Subtitulo)
? $"{lugar.Tipo} · {lugar.Fuente}"
: $"{lugar.Subtitulo} · {lugar.Fuente}",
Tipo = lugar.Tipo,
Fuente = lugar.Fuente,
Latitud = lugar.Latitud,
Longitud = lugar.Longitud
};
}
private string ObtenerTextoBusqueda(CampoBusquedaMapa campo)
=> campo == CampoBusquedaMapa.Origen
? _textoBusquedaOrigen
: _textoBusquedaDestino;
private SugerenciaBusquedaMapa? ObtenerUbicacionSeleccionada(CampoBusquedaMapa campo)
=> campo == CampoBusquedaMapa.Origen
? _ubicacionOrigenSeleccionada
: _ubicacionDestinoSeleccionada;
private void InvalidarUbicacionSeleccionadaSiEditada(CampoBusquedaMapa campo)
{
var texto = ObtenerTextoBusqueda(campo).Trim();
var seleccion = ObtenerUbicacionSeleccionada(campo);
if (seleccion is null ||
string.Equals(texto, seleccion.TextoEntrada, StringComparison.OrdinalIgnoreCase))
{
return;
}
if (campo == CampoBusquedaMapa.Origen)
_ubicacionOrigenSeleccionada = null;
else
_ubicacionDestinoSeleccionada = null;
}
private void AsignarSugerenciasBusqueda(
CampoBusquedaMapa campo,
IReadOnlyList<SugerenciaBusquedaMapa> sugerencias)
{
var lista = campo == CampoBusquedaMapa.Origen
? _sugerenciasBusquedaOrigen
: _sugerenciasBusquedaDestino;
lista.Clear();
lista.AddRange(sugerencias);
}
private static string NormalizarClaveSugerencia(SugerenciaBusquedaMapa sugerencia)
{
var lat = sugerencia.Latitud.ToString("0.000000", CultureInfo.InvariantCulture);
var lon = sugerencia.Longitud.ToString("0.000000", CultureInfo.InvariantCulture);
return $"{NormalizeForSearch(sugerencia.Titulo)}|{lat}|{lon}";
}
}

View File

@@ -0,0 +1,593 @@
using System.Globalization;
using System.Text.Json;
using Microsoft.JSInterop;
using RutasDBUS.Modelos.Historial;
namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas
{
private const string ClaveHistorialLocal = "rutasdbus.planificador.routeHistory.v1";
private const int MaxItemsHistorialLocal = 100;
private const int IntervaloRefrescoHistorialCompartidoSegundos = 3;
private readonly List<RegistroHistorialRuta> _historialLocal = new();
private readonly List<RegistroHistorialRuta> _historialCompartido = new();
private PeriodicTimer? _historialCompartidoTimer;
private CancellationTokenSource? _historialCompartidoCts;
private string _historialLocalSeleccionadoId = string.Empty;
private string _historialCompartidoSeleccionadoId = string.Empty;
private string _mensajeHistorial = string.Empty;
private bool _mensajeHistorialEsError = false;
private bool _mostrarHistorialRutas = false;
private bool _refrescandoHistorialCompartido = false;
private bool _omitirRegistroHistorialEnSiguienteCalculo = false;
private string? _historialCompartidoPendienteEliminarId;
private ConfiguracionLocalPlanificador? _configuracionTemporalPreviaHistorial;
private async Task ToggleHistorialPanel()
{
if (_mostrarHistorialRutas)
{
_mostrarHistorialRutas = false;
StopRefrescoHistorialCompartido();
await RestaurarConfiguracionTemporalHistorialAsync();
return;
}
_mostrarHistorialRutas = true;
await CargarHistorialCompartidoAsync();
StartRefrescoHistorialCompartido();
}
private async Task CargarHistorialLocalAsync()
{
try
{
var json = await JS.InvokeAsync<string?>("rt.loadLocalValue", ClaveHistorialLocal);
if (string.IsNullOrWhiteSpace(json))
return;
var items = JsonSerializer.Deserialize<List<RegistroHistorialRuta>>(json);
if (items is null)
return;
_historialLocal.Clear();
_historialLocal.AddRange(items
.Where(x => !string.IsNullOrWhiteSpace(x.Id))
.OrderByDescending(x => x.FechaActualizacionUtc)
.Take(MaxItemsHistorialLocal));
_historialLocalSeleccionadoId = _historialLocal.FirstOrDefault()?.Id ?? string.Empty;
}
catch (JSException)
{
}
catch (JsonException)
{
}
}
private async Task PersistirHistorialLocalAsync()
{
try
{
var json = JsonSerializer.Serialize(_historialLocal);
await JS.InvokeVoidAsync("rt.saveLocalValue", ClaveHistorialLocal, json);
}
catch (JSException)
{
}
}
private async Task CargarHistorialCompartidoAsync(bool mostrarError = true)
{
if (_refrescandoHistorialCompartido)
return;
_refrescandoHistorialCompartido = true;
try
{
var items = await HistorialRutasCompartidoService.ObtenerAsync();
_historialCompartido.Clear();
_historialCompartido.AddRange(items);
if (_historialCompartido.Count == 0)
{
_historialCompartidoSeleccionadoId = string.Empty;
return;
}
if (!_historialCompartido.Any(x => x.Id == _historialCompartidoSeleccionadoId))
_historialCompartidoSeleccionadoId = _historialCompartido[0].Id;
}
catch (Exception)
{
if (mostrarError)
MostrarMensajeHistorial("No he podido cargar el historial compartido.", esError: true);
}
finally
{
_refrescandoHistorialCompartido = false;
}
}
private void StartRefrescoHistorialCompartido()
{
if (_historialCompartidoTimer is not null)
return;
var cts = new CancellationTokenSource();
var timer = new PeriodicTimer(TimeSpan.FromSeconds(IntervaloRefrescoHistorialCompartidoSegundos));
_historialCompartidoCts = cts;
_historialCompartidoTimer = timer;
_ = Task.Run(async () =>
{
try
{
while (await timer.WaitForNextTickAsync(cts.Token))
{
if (!_mostrarHistorialRutas)
continue;
await InvokeAsync(async () =>
{
await CargarHistorialCompartidoAsync(mostrarError: false);
StateHasChanged();
});
}
}
catch
{
// Timer cancelled when the popup or component closes.
}
});
}
private void StopRefrescoHistorialCompartido()
{
_historialCompartidoCts?.Cancel();
_historialCompartidoTimer?.Dispose();
_historialCompartidoCts?.Dispose();
_historialCompartidoTimer = null;
_historialCompartidoCts = null;
}
private async Task RegistrarEscenarioActualEnHistorialLocalAsync()
{
var actual = ConstruirRegistroHistorialActual();
if (actual is null)
return;
var clave = CalcularClaveEscenario(actual);
var existente = _historialLocal.FirstOrDefault(x =>
string.Equals(CalcularClaveEscenario(x), clave, StringComparison.OrdinalIgnoreCase));
if (existente is null)
{
existente = actual;
_historialLocal.Insert(0, existente);
}
else
{
existente.OrigenTexto = actual.OrigenTexto;
existente.DestinoTexto = actual.DestinoTexto;
existente.OrigenLatitud = actual.OrigenLatitud;
existente.OrigenLongitud = actual.OrigenLongitud;
existente.DestinoLatitud = actual.DestinoLatitud;
existente.DestinoLongitud = actual.DestinoLongitud;
existente.UsarHorariosTeoricos = actual.UsarHorariosTeoricos;
existente.UsarHoraSimulada = actual.UsarHoraSimulada;
existente.FechaSimulada = actual.FechaSimulada;
existente.HoraSimulada = actual.HoraSimulada;
existente.CriterioPrioritarioRuta = actual.CriterioPrioritarioRuta;
existente.PreferenciaEtiquetaRuta = actual.PreferenciaEtiquetaRuta;
existente.ActivarFiltroLineas = actual.ActivarFiltroLineas;
existente.LineasPermitidas = actual.LineasPermitidas.ToList();
existente.FechaActualizacionUtc = DateTime.UtcNow;
_historialLocal.Remove(existente);
_historialLocal.Insert(0, existente);
}
while (_historialLocal.Count > MaxItemsHistorialLocal)
_historialLocal.RemoveAt(_historialLocal.Count - 1);
_historialLocalSeleccionadoId = existente.Id;
await PersistirHistorialLocalAsync();
}
private async Task CargarHistorialLocalSeleccionadoAsync()
{
var item = _historialLocal.FirstOrDefault(x => x.Id == _historialLocalSeleccionadoId);
if (item is null)
{
MostrarMensajeHistorial("No encuentro esa combinacion en tu historial personal.", esError: true);
return;
}
await AplicarRegistroHistorialAsync(item);
MostrarMensajeHistorial("Combinacion personal cargada.");
}
private async Task CargarHistorialLocalAsync(string id)
{
_historialLocalSeleccionadoId = id;
await CargarHistorialLocalSeleccionadoAsync();
}
private async Task CargarHistorialCompartidoSeleccionadoAsync()
{
var item = _historialCompartido.FirstOrDefault(x => x.Id == _historialCompartidoSeleccionadoId);
if (item is null)
{
MostrarMensajeHistorial("No encuentro esa combinacion en el historial compartido.", esError: true);
return;
}
await AplicarRegistroHistorialAsync(item);
MostrarMensajeHistorial("Combinación compartida cargada.");
}
private async Task CargarHistorialCompartidoAsync(string id)
{
_historialCompartidoSeleccionadoId = id;
await CargarHistorialCompartidoSeleccionadoAsync();
}
private async Task CompartirEscenarioActualAsync()
{
var actual = ConstruirRegistroHistorialActual()
?? _historialLocal.FirstOrDefault(x => x.Id == _historialLocalSeleccionadoId);
if (actual is null)
{
MostrarMensajeHistorial("Primero necesitamos una combinacion valida para poder compartirla.", esError: true);
return;
}
try
{
var guardado = await HistorialRutasCompartidoService.GuardarAsync(actual);
await CargarHistorialCompartidoAsync();
_historialCompartidoSeleccionadoId = guardado.Id;
MostrarMensajeHistorial("Combinacion enviada al historial compartido.");
}
catch (Exception)
{
MostrarMensajeHistorial("No he podido guardar la combinacion en el historial compartido.", esError: true);
}
}
private async Task CompartirHistorialLocalAsync(string id)
{
_historialLocalSeleccionadoId = id;
var item = _historialLocal.FirstOrDefault(x => x.Id == id);
if (item is null)
{
MostrarMensajeHistorial("No encuentro esa combinacion en tu historial personal.", esError: true);
return;
}
try
{
var guardado = await HistorialRutasCompartidoService.GuardarAsync(item);
await CargarHistorialCompartidoAsync();
_historialCompartidoSeleccionadoId = guardado.Id;
MostrarMensajeHistorial("Combinacion enviada al historial compartido.");
}
catch (Exception)
{
MostrarMensajeHistorial("No he podido guardar la combinacion en el historial compartido.", esError: true);
}
}
private async Task EliminarHistorialLocalAsync(string id)
{
var item = _historialLocal.FirstOrDefault(x => x.Id == id);
if (item is null)
return;
_historialLocal.Remove(item);
if (_historialLocalSeleccionadoId == id)
_historialLocalSeleccionadoId = _historialLocal.FirstOrDefault()?.Id ?? string.Empty;
await PersistirHistorialLocalAsync();
MostrarMensajeHistorial("Combinacion eliminada del historial personal.");
}
private void PedirConfirmacionEliminarHistorialCompartido(string id)
{
if (string.IsNullOrWhiteSpace(id))
return;
_historialCompartidoPendienteEliminarId = id;
}
private void CancelarConfirmacionEliminarHistorialCompartido()
{
_historialCompartidoPendienteEliminarId = null;
}
private async Task ConfirmarEliminarHistorialCompartidoAsync()
{
var id = _historialCompartidoPendienteEliminarId;
_historialCompartidoPendienteEliminarId = null;
if (string.IsNullOrWhiteSpace(id))
return;
await EliminarHistorialCompartidoAsync(id);
}
private async Task EliminarHistorialCompartidoAsync(string id)
{
try
{
var ok = await HistorialRutasCompartidoService.EliminarAsync(id);
if (!ok)
{
MostrarMensajeHistorial("No he podido eliminar esa combinacion compartida.", esError: true);
return;
}
await CargarHistorialCompartidoAsync();
if (_historialCompartidoSeleccionadoId == id)
_historialCompartidoSeleccionadoId = _historialCompartido.FirstOrDefault()?.Id ?? string.Empty;
MostrarMensajeHistorial("Combinacion eliminada del historial compartido.");
}
catch (Exception)
{
MostrarMensajeHistorial("No he podido eliminar esa combinacion compartida.", esError: true);
}
}
private async Task AplicarRegistroHistorialAsync(RegistroHistorialRuta item)
{
CapturarConfiguracionTemporalAntesDeHistorial();
_omitirRegistroHistorialEnSiguienteCalculo = true;
_textoBusquedaOrigen = item.OrigenTexto;
_textoBusquedaDestino = item.DestinoTexto;
_coordenadasOrigen = new[] { item.OrigenLatitud, item.OrigenLongitud };
_coordenadasDestino = new[] { item.DestinoLatitud, item.DestinoLongitud };
_usarHorariosTeoricos = item.UsarHorariosTeoricos;
_usarHoraSimulada = item.UsarHoraSimulada;
if (DateOnly.TryParse(item.FechaSimulada, out var fecha))
_fechaSimulada = fecha;
if (TimeOnly.TryParse(item.HoraSimulada, out var hora))
_horaSimulada = hora;
_criterioPrioritarioRuta = NormalizarCriterioRuta(item.CriterioPrioritarioRuta, item.PreferenciaEtiquetaRuta);
SincronizarPreferenciaDesdeCriterioRuta();
_activarRangoLineas = item.ActivarFiltroLineas;
_lineasPermitidas.Clear();
foreach (var linea in NormalizarLineasHistorial(item.LineasPermitidas))
_lineasPermitidas.Add(linea);
_busFilteredCache.Clear();
SincronizarSegundosPorParadaConHorarios();
StopClock();
if (_usarHoraSimulada)
await RefrescarHoraUIAsync();
else
StartClock();
await PersistirConfiguracionAsync();
await CerrarTodasLasPreviewsAsync();
await CerrarPopupsAsync();
if (_mapa is not null)
{
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
await ClearRoutesJsAsync();
}
await SincronizarMarcadoresOrigenDestinoJsAsync();
await CalcularRutaCompletaAsync();
}
private void CapturarConfiguracionTemporalAntesDeHistorial()
{
if (_configuracionTemporalPreviaHistorial is not null)
return;
_configuracionTemporalPreviaHistorial = CrearConfiguracionLocal();
}
private async Task RestaurarConfiguracionTemporalHistorialAsync()
{
var configuracionPrevia = _configuracionTemporalPreviaHistorial;
if (configuracionPrevia is null)
return;
_configuracionTemporalPreviaHistorial = null;
AplicarConfiguracionLocal(configuracionPrevia);
StopClock();
if (_usarHoraSimulada)
await RefrescarHoraUIAsync();
else
StartClock();
await PersistirConfiguracionAsync();
}
private RegistroHistorialRuta? ConstruirRegistroHistorialActual()
{
if (_coordenadasOrigen is null || _coordenadasDestino is null)
return null;
var ahora = _ultimaHoraReferenciaRuta ?? AhoraLocal();
return new RegistroHistorialRuta
{
Id = Guid.NewGuid().ToString("N"),
OrigenTexto = ResolverTextoPuntoHistorial(_coordenadasOrigen),
DestinoTexto = ResolverTextoPuntoHistorial(_coordenadasDestino),
OrigenLatitud = _coordenadasOrigen[0],
OrigenLongitud = _coordenadasOrigen[1],
DestinoLatitud = _coordenadasDestino[0],
DestinoLongitud = _coordenadasDestino[1],
UsarHorariosTeoricos = _usarHorariosTeoricos,
UsarHoraSimulada = _usarHoraSimulada,
FechaSimulada = DateOnly.FromDateTime(ahora).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
HoraSimulada = TimeOnly.FromDateTime(ahora).ToString("HH:mm:ss", CultureInfo.InvariantCulture),
CriterioPrioritarioRuta = _criterioPrioritarioRuta,
PreferenciaEtiquetaRuta = _preferenciaEtiquetaRuta,
ActivarFiltroLineas = _activarRangoLineas,
LineasPermitidas = ObtenerLineasPermitidasSeleccionadas().ToList(),
FechaCreacionUtc = DateTime.UtcNow,
FechaActualizacionUtc = DateTime.UtcNow
};
}
private static string? FormatearParadaHistorial(RutasDBUS.Modelos.RedBus.Parada? parada)
{
if (parada is null || string.IsNullOrWhiteSpace(parada.Codigo))
return null;
var nombre = string.IsNullOrWhiteSpace(parada.Nombre)
? parada.Codigo
: parada.Nombre.Trim();
return $"{nombre} ({CodigoParadaFormateado(parada.Codigo)})";
}
private string ResolverTextoPuntoHistorial(double[] coords)
{
var lat = coords[0];
var lon = coords[1];
var parada = RedBusServicio.BuscarParadaMasCercana(lat, lon);
var textoParada = FormatearParadaHistorial(parada);
if (!string.IsNullOrWhiteSpace(textoParada))
return textoParada;
return $"{lat.ToString("0.000000", CultureInfo.InvariantCulture)}, {lon.ToString("0.000000", CultureInfo.InvariantCulture)}";
}
private string TextoOpcionHistorial(RegistroHistorialRuta item)
{
var sello = item.FechaActualizacionUtc == default
? string.Empty
: item.FechaActualizacionUtc.ToLocalTime().ToString("dd/MM HH:mm", CultureInfo.CurrentCulture);
var horaReferencia = FormatearHoraReferenciaHistorial(item);
var modo = FormatearModoHistorial(item);
var textoBase = string.IsNullOrWhiteSpace(sello)
? $"{item.OrigenTexto} -> {item.DestinoTexto}"
: $"{sello} - {item.OrigenTexto} -> {item.DestinoTexto}";
var partes = new List<string> { textoBase };
if (!string.IsNullOrWhiteSpace(horaReferencia))
partes.Add($"HR: {horaReferencia}");
if (!string.IsNullOrWhiteSpace(modo))
partes.Add($"Modo: {modo}");
return string.Join(" \u00b7 ", partes);
}
private static string FormatearModoHistorial(RegistroHistorialRuta item)
{
var criterio = item.CriterioPrioritarioRuta?.Trim();
if (string.IsNullOrWhiteSpace(criterio))
{
var preferencia = item.PreferenciaEtiquetaRuta?.Trim() ?? string.Empty;
return string.Equals(preferencia, PreferenciaRutaAndar, StringComparison.OrdinalIgnoreCase)
? "Andar"
: preferencia;
}
if (string.Equals(criterio, CriterioRutaAndar, StringComparison.OrdinalIgnoreCase))
return "Andar";
if (string.Equals(criterio, CriterioRutaTiempo, StringComparison.OrdinalIgnoreCase))
return "Tiempo";
if (string.Equals(criterio, CriterioRutaEquilibrado, StringComparison.OrdinalIgnoreCase))
return "Equilibrado";
return criterio;
}
private static IReadOnlyList<int> NormalizarLineasHistorial(IEnumerable<int>? lineas)
=> (lineas ?? Array.Empty<int>())
.Where(linea => linea > 0)
.Distinct()
.OrderBy(linea => linea)
.ToList();
private static string FormatearHoraReferenciaHistorial(RegistroHistorialRuta item)
{
if (string.IsNullOrWhiteSpace(item.HoraSimulada))
return string.Empty;
var formatosHora = new[] { "HH:mm:ss", "H:mm:ss", "HH:mm", "H:mm" };
if (!TimeOnly.TryParseExact(
item.HoraSimulada.Trim(),
formatosHora,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var hora))
{
return string.Empty;
}
var horaTexto = hora.ToString("HH:mm", CultureInfo.InvariantCulture);
if (string.IsNullOrWhiteSpace(item.FechaSimulada))
return horaTexto;
var formatosFecha = new[] { "yyyy-MM-dd", "dd/MM/yyyy", "d/M/yyyy", "dd/MM/yy", "d/M/yy" };
return DateOnly.TryParseExact(
item.FechaSimulada.Trim(),
formatosFecha,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var fecha)
? $"{fecha.ToString("dd/MM", CultureInfo.InvariantCulture)} {horaTexto}"
: $"{item.FechaSimulada.Trim()} {horaTexto}";
}
private void MostrarMensajeHistorial(string mensaje, bool esError = false)
{
_mensajeHistorial = mensaje;
_mensajeHistorialEsError = esError;
}
private static string CalcularClaveEscenario(RegistroHistorialRuta item)
{
var origenLat = Math.Round(item.OrigenLatitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
var origenLon = Math.Round(item.OrigenLongitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
var destinoLat = Math.Round(item.DestinoLatitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
var destinoLon = Math.Round(item.DestinoLongitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
var fecha = item.UsarHoraSimulada ? item.FechaSimulada?.Trim() ?? string.Empty : string.Empty;
var hora = item.UsarHoraSimulada ? item.HoraSimulada?.Trim() ?? string.Empty : string.Empty;
var criterio = NormalizarCriterioRuta(item.CriterioPrioritarioRuta, item.PreferenciaEtiquetaRuta);
var lineas = item.ActivarFiltroLineas
? string.Join(",", NormalizarLineasHistorial(item.LineasPermitidas))
: string.Empty;
return string.Join("|",
origenLat,
origenLon,
destinoLat,
destinoLon,
item.UsarHorariosTeoricos ? "1" : "0",
item.UsarHoraSimulada ? "1" : "0",
fecha,
hora,
criterio,
item.ActivarFiltroLineas ? "1" : "0",
lineas);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,245 @@
using System.Globalization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
using RutasDBUS.Modelos.Importacion;
namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas
{
private const long MaxModeloHorarioUploadBytes = 250L * 1024 * 1024;
private IReadOnlyList<ModeloHorarioDbusInfo> _modelosHorarios = Array.Empty<ModeloHorarioDbusInfo>();
private string _modeloHorarioSeleccionadoId = "";
private string _mensajeModelosHorarios = "";
private bool _modelosHorariosCargando = false;
private bool _mostrarModelosHorariosPanel = false;
private ModeloHorarioDbusInfo? ModeloHorarioActivo =>
_modelosHorarios.FirstOrDefault(modelo => modelo.Activo);
private void AbrirModelosHorariosPanel()
{
_mostrarModelosHorariosPanel = true;
}
private void CerrarModelosHorariosPanel()
{
_mostrarModelosHorariosPanel = false;
}
private async Task CargarModelosHorariosAsync()
{
_modelosHorariosCargando = true;
try
{
_modelosHorarios = await ArchivoImportacionDbus.ListarModelosAsync();
var activo = ModeloHorarioActivo ?? _modelosHorarios.FirstOrDefault();
_modeloHorarioSeleccionadoId = activo?.Id ?? "";
}
catch (Exception ex)
{
_modelosHorarios = Array.Empty<ModeloHorarioDbusInfo>();
_modeloHorarioSeleccionadoId = "";
_mensajeModelosHorarios = "No se han podido leer los modelos: " + ex.Message;
}
finally
{
_modelosHorariosCargando = false;
}
}
private async Task ActualizarModelosHorariosAsync()
{
_mensajeModelosHorarios = "";
await CargarModelosHorariosAsync();
if (string.IsNullOrWhiteSpace(_mensajeModelosHorarios))
_mensajeModelosHorarios = _modelosHorarios.Count == 0
? "No hay ZIPs disponibles."
: "Lista actualizada.";
}
private async Task SeleccionarModeloHorarioAsync()
{
if (string.IsNullOrWhiteSpace(_modeloHorarioSeleccionadoId))
{
_mensajeModelosHorarios = "Selecciona un ZIP.";
return;
}
var seleccionado = _modelosHorarios
.FirstOrDefault(modelo => string.Equals(modelo.Id, _modeloHorarioSeleccionadoId, StringComparison.OrdinalIgnoreCase));
if (seleccionado?.Activo == true)
{
_mensajeModelosHorarios = "Ese modelo ya esta cargado.";
return;
}
_modelosHorariosCargando = true;
_mensajeModelosHorarios = "Cargando modelo...";
try
{
await ArchivoImportacionDbus.SeleccionarModeloAsync(_modeloHorarioSeleccionadoId);
await RecargarModeloHorarioActivoAsync();
await CargarModelosHorariosAsync();
var activo = ModeloHorarioActivo;
_mensajeModelosHorarios = activo is null
? "Modelo cargado."
: "Cargado: " + activo.NombreArchivo;
}
catch (Exception ex)
{
_mensajeModelosHorarios = "No se ha podido cargar el modelo: " + ex.Message;
}
finally
{
_modelosHorariosCargando = false;
}
}
private async Task SubirModeloHorarioAsync(InputFileChangeEventArgs e)
{
var archivo = e.File;
if (archivo is null)
return;
if (!archivo.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
_mensajeModelosHorarios = "Solo se pueden subir archivos .zip.";
return;
}
_modelosHorariosCargando = true;
_mensajeModelosHorarios = "Subiendo ZIP...";
try
{
await using var stream = archivo.OpenReadStream(MaxModeloHorarioUploadBytes);
var modelo = await ArchivoImportacionDbus.GuardarModeloSubidoAsync(archivo.Name, stream);
_modeloHorarioSeleccionadoId = modelo.Id;
await RecargarModeloHorarioActivoAsync();
await CargarModelosHorariosAsync();
_mensajeModelosHorarios = "Subido y cargado: " + modelo.NombreArchivo;
}
catch (Exception ex)
{
_mensajeModelosHorarios = "No se ha podido subir el ZIP: " + ex.Message;
}
finally
{
_modelosHorariosCargando = false;
}
}
private async Task LimpiarDatosLocalesAsync()
{
bool confirmado;
try
{
confirmado = await JS.InvokeAsync<bool>(
"confirm",
"¿Seguro que quieres limpiar los datos locales de este navegador? Se borrarán historial, configuración guardada, cachés y datos locales de este sitio.");
}
catch (JSException)
{
return;
}
if (!confirmado)
return;
_modelosHorariosCargando = true;
_mensajeModelosHorarios = "Limpiando datos locales...";
try
{
await JS.InvokeVoidAsync("rt.clearLocalBrowserData");
_mensajeModelosHorarios = "Datos locales borrados. Recargando...";
await InvokeAsync(StateHasChanged);
await Task.Delay(250);
await JS.InvokeVoidAsync("rt.reloadPage");
}
catch (Exception ex)
{
_mensajeModelosHorarios = "No se han podido limpiar los datos locales: " + ex.Message;
}
finally
{
_modelosHorariosCargando = false;
}
}
private async Task RecargarModeloHorarioActivoAsync()
{
_ctsCalculo?.Cancel();
IniciarNuevaVersionPintadoRuta();
await CerrarTodasLasPreviewsAsync();
await CerrarPopupsAsync();
if (_mapa is not null)
{
await ClearRoutesJsAsync();
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
}
ResetEstadoRuta(limpiarOpciones: true);
PlanificadorRutasServicio.InvalidarCachesModelo();
_busFilteredCache.Clear();
InvalidarBusIndex();
_itItems.Clear();
_itSeleccionadoId = null;
_itFiltro = "";
_selectItKey++;
_nextDeps.Clear();
_horariosOk = false;
_horariosError = null;
_tareaCargaRed = RedBusServicio.CargarRedAsync();
await _tareaCargaRed;
if (_usarHorariosTeoricos)
await EnsureHorariosLoadedAsync();
if (RedBusServicio.Paradas.Count > 0)
{
EnsureBusIndexBuilt();
BuildItinerariosDropdown();
SincronizarSeleccionItinerarioConFiltro();
}
_paradasPintadas = false;
if (_mapa is not null)
{
if (_mostrarParadas)
await DibujarParadasAsync();
else
await OcultarParadasAsync();
await SincronizarMarcadoresOrigenDestinoJsAsync();
_paradasPintadas = _mostrarParadas;
}
StateHasChanged();
}
private static string FormatearTamanoModelo(long bytes)
{
if (bytes >= 1024L * 1024L)
return (bytes / 1024d / 1024d).ToString("0.0", CultureInfo.InvariantCulture) + " MB";
if (bytes >= 1024L)
return (bytes / 1024d).ToString("0", CultureInfo.InvariantCulture) + " KB";
return bytes.ToString(CultureInfo.InvariantCulture) + " B";
}
private static string FormatearFechaModelo(DateTime fechaUtc)
=> fechaUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm", CultureInfo.InvariantCulture);
}

View File

@@ -0,0 +1,360 @@
using System.Globalization;
using System.Text.Json;
using Microsoft.JSInterop;
namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas
{
private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1";
private bool _configuracionLocalCargada = false;
private bool _mostrarConfirmacionRestablecer = false;
/// <summary>
/// Instantánea persistida de la configuración del planificador.
/// </summary>
private sealed class ConfiguracionLocalPlanificador
{
public bool? MostrarParadas { get; set; }
public bool UsarHorariosTeoricos { get; set; }
public int SegundosPorParadaBusManual { get; set; }
public bool ActivarRangoLineas { get; set; }
public int LineaMin { get; set; }
public int LineaMax { get; set; }
public List<int>? LineasPermitidas { get; set; }
public bool ActivarRangoItinerarios { get; set; }
public int ItinerarioMin { get; set; }
public int ItinerarioMax { get; set; }
public bool ActivarRangoTransbordos { get; set; }
public int MinTransbordosPermitidos { get; set; }
public int MaxTransbordosPermitidos { get; set; }
public bool UsarHoraSimulada { get; set; }
public string? FechaSimulada { get; set; }
public string? HoraSimulada { get; set; }
public double? FactorPenalizacionDistanciaPie { get; set; }
public double? ParametroPenalizacionDistanciaPieMetros { get; set; }
public double? CoeficienteEsfuerzoAltura { get; set; }
public double? PesoTransbordoRankingMinutos { get; set; }
public double VelocidadPieKmH { get; set; }
public double VelocidadBusKmH { get; set; }
public int ElevMaxPuntosPorTramo { get; set; }
public int RadioCandidatasMetros { get; set; }
public int MargenTransbordoSegundos { get; set; }
public string? WalkProvider { get; set; }
public int NivelBrilloMapa { get; set; }
public bool UsarColorRealLineaEnItinerarios { get; set; }
public bool UsarOsrmParaRanking { get; set; }
public string? CriterioPrioritarioRuta { get; set; }
public string? PreferenciaEtiquetaRuta { get; set; }
}
/// <summary>
/// Carga la configuración persistida del navegador y la aplica al estado actual.
/// </summary>
private async Task CargarConfiguracionLocalAsync()
{
try
{
var json = await JS.InvokeAsync<string?>("rt.loadLocalValue", ClaveConfiguracionLocal);
if (string.IsNullOrWhiteSpace(json))
return;
var config = JsonSerializer.Deserialize<ConfiguracionLocalPlanificador>(json);
if (config is null)
return;
AplicarConfiguracionLocal(config);
}
catch (JSException)
{
}
catch (JsonException)
{
}
finally
{
_configuracionLocalCargada = true;
}
}
/// <summary>
/// Guarda la configuración actual del planificador en el navegador.
/// </summary>
private async Task PersistirConfiguracionAsync()
{
if (!_configuracionLocalCargada)
return;
try
{
if (!_usarHorariosTeoricos)
_segundosPorParadaBusManual = Math.Max(0, _segundosPorParadaBus);
var json = JsonSerializer.Serialize(CrearConfiguracionLocal());
await JS.InvokeVoidAsync("rt.saveLocalValue", ClaveConfiguracionLocal, json);
}
catch (JSException)
{
}
}
/// <summary>
/// Sincroniza el valor manual de segundos por parada y persiste la configuración.
/// </summary>
private async Task OnSegundosPorParadaBusChanged()
{
_segundosPorParadaBus = Math.Max(0, _segundosPorParadaBus);
_segundosPorParadaBusManual = _segundosPorParadaBus;
await PersistirConfiguracionAsync();
}
/// <summary>
/// Construye la instantánea persistible a partir del estado actual.
/// </summary>
private ConfiguracionLocalPlanificador CrearConfiguracionLocal()
=> new()
{
MostrarParadas = _mostrarParadas,
UsarHorariosTeoricos = _usarHorariosTeoricos,
SegundosPorParadaBusManual = Math.Max(0, _segundosPorParadaBusManual),
ActivarRangoLineas = _activarRangoLineas,
LineaMin = _lineaMin,
LineaMax = _lineaMax,
LineasPermitidas = ObtenerLineasPermitidasSeleccionadas().ToList(),
ActivarRangoItinerarios = _activarRangoItinerarios,
ItinerarioMin = _itinerarioMin,
ItinerarioMax = _itinerarioMax,
ActivarRangoTransbordos = _activarRangoTransbordos,
MinTransbordosPermitidos = _minTransbordosPermitidos,
MaxTransbordosPermitidos = _maxTransbordosPermitidos,
UsarHoraSimulada = _usarHoraSimulada,
FechaSimulada = _fechaSimulada.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
HoraSimulada = _horaSimulada.ToString("HH:mm:ss", CultureInfo.InvariantCulture),
FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie,
ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros,
CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura,
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
VelocidadPieKmH = _velocidadPieKmH,
VelocidadBusKmH = _velocidadBusKmH,
ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo,
RadioCandidatasMetros = _radioCandidatasMetros,
MargenTransbordoSegundos = _margenTransbordoSegundos,
WalkProvider = _walkProvider.ToString(),
NivelBrilloMapa = _nivelBrilloMapa,
UsarColorRealLineaEnItinerarios = _usarColorRealLineaEnItinerarios,
UsarOsrmParaRanking = _usarOsrmParaRanking,
CriterioPrioritarioRuta = _criterioPrioritarioRuta,
PreferenciaEtiquetaRuta = _preferenciaEtiquetaRuta
};
/// <summary>
/// Aplica al componente una configuración previamente persistida.
/// </summary>
private void AplicarConfiguracionLocal(ConfiguracionLocalPlanificador config)
{
_mostrarParadas = config.MostrarParadas ?? true;
_usarHorariosTeoricos = config.UsarHorariosTeoricos;
_segundosPorParadaBusManual = Math.Clamp(config.SegundosPorParadaBusManual, 0, 3600);
_segundosPorParadaBus = _segundosPorParadaBusManual;
_lineaMin = Math.Clamp(config.LineaMin, 1, 200);
_lineaMax = Math.Clamp(config.LineaMax, 1, 200);
_lineasPermitidas.Clear();
foreach (var linea in NormalizarLineasPermitidas(config.LineasPermitidas, config.LineaMin, config.LineaMax))
_lineasPermitidas.Add(linea);
_activarRangoLineas = config.ActivarRangoLineas;
_activarRangoItinerarios = config.ActivarRangoItinerarios;
_itinerarioMin = Math.Clamp(config.ItinerarioMin, 1, 99);
_itinerarioMax = Math.Clamp(config.ItinerarioMax, 1, 99);
_activarRangoTransbordos = config.ActivarRangoTransbordos;
_minTransbordosPermitidos = Math.Clamp(config.MinTransbordosPermitidos, 0, 10);
_maxTransbordosPermitidos = Math.Clamp(config.MaxTransbordosPermitidos, 0, 10);
_usarHoraSimulada = config.UsarHoraSimulada;
if (DateOnly.TryParse(config.FechaSimulada, out var fechaSimulada))
_fechaSimulada = fechaSimulada;
if (TimeOnly.TryParse(config.HoraSimulada, out var horaSimulada))
_horaSimulada = horaSimulada;
if (config.FactorPenalizacionDistanciaPie is { } factor && double.IsFinite(factor))
_factorPenalizacionDistanciaPie = Math.Abs(factor - 1.3) < 0.0001 ? 1.6 : factor;
if (config.ParametroPenalizacionDistanciaPieMetros is { } parametro && double.IsFinite(parametro))
_parametroPenalizacionDistanciaPieMetros = parametro;
if (config.CoeficienteEsfuerzoAltura is { } coeficienteAltura && double.IsFinite(coeficienteAltura))
_coeficienteEsfuerzoAltura = Math.Abs(coeficienteAltura - 1.6) < 0.0001
? CoeficienteEsfuerzoAlturaPorDefecto
: coeficienteAltura;
if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo))
_pesoTransbordoRankingMinutos = Math.Abs(pesoTransbordo - 8.0) < 0.0001 ? 4.0 : Math.Max(0, pesoTransbordo);
_velocidadPieKmH = Math.Clamp(config.VelocidadPieKmH, 0.5, 25.0);
_velocidadBusKmH = Math.Clamp(
Math.Abs(config.VelocidadBusKmH - 15.0) < 0.0001 ||
Math.Abs(config.VelocidadBusKmH - 16.6) < 0.0001
? 16.0
: config.VelocidadBusKmH,
1.0,
120.0);
_elevMaxPuntosPorTramo = Math.Clamp(config.ElevMaxPuntosPorTramo, 2, 999);
_radioCandidatasMetros = Math.Clamp(config.RadioCandidatasMetros, 200, 3500);
_margenTransbordoSegundos = Math.Clamp(config.MargenTransbordoSegundos, 0, 1800);
_nivelBrilloMapa = Math.Clamp(config.NivelBrilloMapa, 0, 4);
_usarColorRealLineaEnItinerarios = config.UsarColorRealLineaEnItinerarios;
_usarOsrmParaRanking = config.UsarOsrmParaRanking;
_criterioPrioritarioRuta = NormalizarCriterioRuta(config.CriterioPrioritarioRuta, config.PreferenciaEtiquetaRuta);
SincronizarPreferenciaDesdeCriterioRuta();
if (Enum.TryParse<ProveedorCaminata>(config.WalkProvider, ignoreCase: true, out var walkProvider))
_walkProvider = walkProvider;
SincronizarTextosPenalizacionPie();
SincronizarSegundosPorParadaConHorarios();
}
private static IReadOnlyList<int> NormalizarLineasPermitidas(
IReadOnlyList<int>? lineas,
int lineaMinLegacy,
int lineaMaxLegacy)
{
if (lineas is { Count: > 0 })
{
return lineas
.Where(linea => linea > 0 && linea <= 999)
.Distinct()
.OrderBy(linea => linea)
.ToList();
}
if (lineaMinLegacy <= 0 && lineaMaxLegacy <= 0)
return Array.Empty<int>();
var min = Math.Clamp(Math.Min(lineaMinLegacy, lineaMaxLegacy), 1, 999);
var max = Math.Clamp(Math.Max(lineaMinLegacy, lineaMaxLegacy), 1, 999);
var esRangoPorDefecto = min <= 1 && max >= 200;
if (esRangoPorDefecto)
return Array.Empty<int>();
return Enumerable.Range(min, max - min + 1).ToList();
}
/// <summary>
/// Abre el cuadro de confirmación para restablecer valores.
/// </summary>
private void PedirConfirmacionRestablecer()
{
_mostrarConfirmacionRestablecer = true;
}
/// <summary>
/// Cierra el cuadro de confirmación de restablecer.
/// </summary>
private void CancelarConfirmacionRestablecer()
{
_mostrarConfirmacionRestablecer = false;
}
/// <summary>
/// Restablece la configuracion avanzada a los valores por defecto de la aplicacion.
/// </summary>
private async Task RestablecerConfiguracionPorDefectoAsync()
{
_mostrarConfirmacionRestablecer = false;
bool confirmado = true;
try
{
confirmado = await JS.InvokeAsync<bool>("confirm", "¿Seguro que quieres restablecer la configuración avanzada a sus valores por defecto?");
}
catch (JSException)
{
confirmado = false;
}
if (!confirmado)
return;
_mostrarParadas = true;
_usarHorariosTeoricos = true;
_segundosPorParadaBusManual = 30;
_segundosPorParadaBus = 30;
_activarRangoItinerarios = true;
_itinerarioMin = 1;
_itinerarioMax = 99;
_activarRangoTransbordos = true;
_minTransbordosPermitidos = 0;
_maxTransbordosPermitidos = 3;
_factorPenalizacionDistanciaPie = 1.6;
_parametroPenalizacionDistanciaPieMetros = 100.0;
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
_pesoTransbordoRankingMinutos = 4.0;
_velocidadPieKmH = 3.6;
_velocidadBusKmH = 16.0;
_minutosTransbordo = 5;
_margenTransbordoSegundos = 120;
_elevMaxPuntosPorTramo = 999;
_radioCandidatasMetros = 1200;
_walkProvider = ProveedorCaminata.Valhalla;
_nivelBrilloMapa = 0;
_usarColorRealLineaEnItinerarios = false;
_usarOsrmParaRanking = false;
if (_usarHoraSimulada)
StopClock();
else
StartClock();
SincronizarTextosPenalizacionPie();
SincronizarSegundosPorParadaConHorarios();
await RefrescarHoraUIAsync();
await PersistirConfiguracionAsync();
StateHasChanged();
}
/// <summary>
/// Restablece la configuración desde el modal personalizado centrado.
/// </summary>
private async Task ConfirmarRestablecerConfiguracionAsync()
{
_mostrarConfirmacionRestablecer = false;
_mostrarParadas = true;
_usarHorariosTeoricos = true;
_segundosPorParadaBusManual = 30;
_segundosPorParadaBus = 30;
_activarRangoItinerarios = true;
_itinerarioMin = 1;
_itinerarioMax = 99;
_activarRangoTransbordos = true;
_minTransbordosPermitidos = 0;
_maxTransbordosPermitidos = 3;
_factorPenalizacionDistanciaPie = 1.6;
_parametroPenalizacionDistanciaPieMetros = 100.0;
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
_pesoTransbordoRankingMinutos = 4.0;
_velocidadPieKmH = 3.6;
_velocidadBusKmH = 16.0;
_minutosTransbordo = 5;
_margenTransbordoSegundos = 120;
_elevMaxPuntosPorTramo = 999;
_radioCandidatasMetros = 1200;
_walkProvider = ProveedorCaminata.Valhalla;
_nivelBrilloMapa = 0;
_usarColorRealLineaEnItinerarios = false;
_usarOsrmParaRanking = false;
if (_usarHoraSimulada)
StopClock();
else
StartClock();
SincronizarTextosPenalizacionPie();
SincronizarSegundosPorParadaConHorarios();
await RefrescarHoraUIAsync();
await PersistirConfiguracionAsync();
StateHasChanged();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,844 @@
using System.Collections.Concurrent;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using LeafletForBlazor;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus;
using RutasDBUS.Servicios;
using RutasDBUS.Servicios.Caminata;
using RutasDBUS.Servicios.Horarios;
using RutasDBUS.Servicios.Planificacion;
using RutasDBUS.Servicios.RedBus;
namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas : ComponentBase
{
/// <summary>
/// Asegura que elevacion tramo este disponible.
/// </summary>
private async Task EnsureElevacionTramoAsync(DetalleTramoRuta tramo)
{
if (tramo is null) return;
// Si ya está calculado, nada
if (tramo.MuestrasElevacion is not null && tramo.MuestrasElevacion.Count > 0) return;
// Necesitamos la geometría
if (tramo.Linea is null || tramo.Linea.GetLength(0) <= 0) return;
if (tramo.ElevacionCargando) return;
tramo.ElevacionCargando = true;
StateHasChanged();
try
{
var ctNone = CancellationToken.None;
// 1) Samples bajo demanda, priorizando G y dejando O como best-effort
var samples = await ConstruirMuestrasElevacionAsync(tramo.Linea, ctNone);
tramo.MuestrasElevacion = samples;
// 2) Resumen O solo con lo que ya haya llegado, sin bloquear más la UI
var muestrasOpen = samples
.Where(x => x.Elevacion is not null)
.Select(x => x.Elevacion!.Value)
.ToList();
tramo.ElevInicio = samples.FirstOrDefault()?.Elevacion;
tramo.ElevFin = samples.LastOrDefault()?.Elevacion;
tramo.ElevMin = muestrasOpen.Count > 0 ? muestrasOpen.Min() : null;
tramo.ElevMax = muestrasOpen.Count > 0 ? muestrasOpen.Max() : null;
}
catch
{
// silencioso: la elevación es informativa
tramo.MuestrasElevacion ??= new List<MuestraElevacion>();
}
finally
{
tramo.ElevacionCargando = false;
StateHasChanged();
}
}
/// <summary>
/// Pinta opcion ruta completa.
/// </summary>
private async Task PintarOpcionRutaCompleta(AlternativaRuta opcion)
{
var versionPintado = IniciarNuevaVersionPintadoRuta();
var mensajeAnterior = _mensajeRuta;
try
{
await ClearRoutesJsAsync();
ThrowIfPintadoObsoleto(versionPintado);
if (opcion.EsSoloAPie)
{
await PintarRutaSoloAPie(opcion.LineaSoloAPie, opcion.DistanciaSoloAPie, versionPintado: versionPintado);
}
else
{
await PintarRutaDesdeAlternativa(opcion.OrigenParada, opcion.DestinoParada, opcion.AlternativaBus!, versionPintado);
ThrowIfPintadoObsoleto(versionPintado);
await PintarParadasSolucionSeleccionadaAsync(opcion, versionPintado);
}
ThrowIfPintadoObsoleto(versionPintado);
await CerrarPopupsAsync();
ThrowIfPintadoObsoleto(versionPintado);
await FitRoutesJsAsync();
ThrowIfPintadoObsoleto(versionPintado);
StateHasChanged();
}
catch (OperationCanceledException)
{
if (string.IsNullOrWhiteSpace(_mensajeRuta))
_mensajeRuta = mensajeAnterior;
StateHasChanged();
}
catch (Exception ex)
{
_mensajeRuta = $"No se ha podido representar la ruta seleccionada: {ex.Message}";
_hayRutaValida = false;
StateHasChanged();
}
}
/// <summary>
/// Pinta las paradas recorridas por la solución seleccionada con el color de cada tramo de bus.
/// </summary>
private async Task PintarParadasSolucionSeleccionadaAsync(AlternativaRuta opcion, int versionPintado)
{
await ClearRouteStopsJsAsync();
LimpiarParadasRutaVisibles();
ThrowIfPintadoObsoleto(versionPintado);
var tramos = opcion.AlternativaBus?.Tramos;
if (opcion.EsSoloAPie || tramos is null || tramos.Count == 0)
return;
for (var indice = 0; indice < tramos.Count; indice++)
{
var tramo = tramos[indice];
if (tramo?.Variante is null)
continue;
var paradasTramo = BuildParadasForTramoBus(tramo);
if (paradasTramo.Count == 0)
continue;
var stops = BuildStopsDesdeParadas(paradasTramo);
var id = $"route_stops_{indice}_{tramo.Variante.Linea.Codigo}_{tramo.Variante.CodigoItinerario}_{NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo)}";
var color = ColorCssDesdeVariante(tramo.Variante);
await DrawRouteStopsJsAsync(id, stops, color, radius: 8, weight: 2, opacity: 1.0, fillOpacity: 1.0);
RegistrarParadasRutaVisibles(id, paradasTramo);
ThrowIfPintadoObsoleto(versionPintado);
}
}
/// <summary>
/// Pinta ruta solo a pie.
/// </summary>
private async Task PintarRutaSoloAPie(double[,]? lineaPre = null, double distPre = 0, int versionPintado = 0)
{
if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null) return;
ResetEstadoRuta(limpiarMensaje: false);
ThrowIfPintadoObsoleto(versionPintado);
double[,]? linea = lineaPre;
double distancia = distPre;
if (linea is null)
{
var w = await WalkCached(_coordenadasOrigen[0], _coordenadasOrigen[1], _coordenadasDestino[0], _coordenadasDestino[1]);
linea = w.line;
distancia = w.dist;
ThrowIfPintadoObsoleto(versionPintado);
}
if (linea is null)
{
_mensajeRuta = "No he podido calcular una ruta a pie entre los puntos seleccionados.";
_hayRutaValida = false;
StateHasChanged();
return;
}
// ✅ duración por velocidad configurada
double duracion = (distancia > 0) ? EstimarDuracionPieSegundos(distancia) : 0;
_distanciaAPieInicio = distancia;
_duracionTotalSegundos = duracion;
// ✅ Guardamos el tramo andando SIN calcular elevación
_tramosRuta.Add(new DetalleTramoRuta
{
Tipo = "A pie",
Descripcion = CapitalizarPalabrasClave("Camina desde tu punto de origen hasta tu destino final."),
DistanciaMetros = distancia,
DuracionSegundos = duracion,
// clave para lazy elevation
Linea = linea,
// elevación aún no
ElevInicio = null,
ElevFin = null,
ElevMin = null,
ElevMax = null,
MuestrasElevacion = null
});
ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync("walk_full", linea, "#dc2626", 5, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
ThrowIfPintadoObsoleto(versionPintado);
_mensajeRuta = CapitalizarPalabrasClave(
$"Puedes realizar todo el recorrido andando: {FormatearDistancia(distancia)} aproximadamente."
);
_hayRutaValida = true;
ThrowIfPintadoObsoleto(versionPintado);
StateHasChanged();
}
/// <summary>
/// Pinta ruta desde alternativa.
/// </summary>
private async Task PintarRutaDesdeAlternativa(
Parada paradaOrigenCandidata,
Parada paradaDestinoCandidata,
RutaBusAlternativa alternativa,
int versionPintado)
{
if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null) return;
ResetEstadoRuta(limpiarMensaje: false);
ThrowIfPintadoObsoleto(versionPintado);
var tramosBus = alternativa.Tramos;
var lineasBus = RedBusServicio.ConstruirLineasBus(tramosBus, out var distanciaBusTotal);
Parada paradaInicioBus = paradaOrigenCandidata;
Parada paradaFinBus = paradaDestinoCandidata;
if (tramosBus.Count > 0 && lineasBus.Count > 0)
{
var primerTramo = tramosBus.First();
var ultimoTramo = tramosBus.Last();
paradaInicioBus = ResolverParadaFisica(
primerTramo.CodigoParadaInicio,
ObtenerPrimerPunto(lineasBus[0]),
paradaOrigenCandidata);
paradaFinBus = ResolverParadaFisica(
ultimoTramo.CodigoParadaFin,
ObtenerUltimoPunto(lineasBus[^1]),
paradaDestinoCandidata);
}
_paradaOrigenSeleccionada = paradaInicioBus;
_paradaDestinoSeleccionada = paradaFinBus;
_numeroTransbordos = CalcularTransbordosReales(alternativa);
_primerTransbordoseleccionado = null;
if (alternativa.CodigosParadasTransbordo is not null && alternativa.CodigosParadasTransbordo.Count > 0)
{
var codigoPrimera = alternativa.CodigosParadasTransbordo[0];
_primerTransbordoseleccionado = ResolverParadaFisica(codigoPrimera);
}
_distanciaAPieInicio = 0;
_distanciaAPieFin = 0;
_distanciaEnBus = 0;
_duracionTotalSegundos = 0;
var now = AhoraLocal();
var tCursor = now;
if (_usarHorariosTeoricos)
{
await EnsureHorariosLoadedAsync();
ThrowIfPintadoObsoleto(versionPintado);
}
// -----------------------
// Pie 1
// -----------------------
var w1 = await WalkCached(_coordenadasOrigen[0], _coordenadasOrigen[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
ThrowIfPintadoObsoleto(versionPintado);
if (w1.line is not null)
{
_distanciaAPieInicio = w1.dist;
var durPie1 = (w1.dist > 0) ? EstimarDuracionPieSegundos(w1.dist) : 0;
var tIni = tCursor;
tCursor = AddSecondsSafe(tCursor, durPie1);
_duracionTotalSegundos += durPie1;
_tramosRuta.Add(new DetalleTramoRuta
{
Tipo = "A pie",
Descripcion = CapitalizarPalabrasClave(
$"{HHmm(tIni)} -> {HHmm(tCursor)} - Desde tu punto de origen hasta la Parada \"{paradaInicioBus.Nombre}\" ({CodigoParadaFormateado(paradaInicioBus.Codigo)})."
),
DistanciaMetros = w1.dist,
DuracionSegundos = durPie1,
Linea = w1.line
});
ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync("walk_1", w1.line, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
ThrowIfPintadoObsoleto(versionPintado);
}
// -----------------------
// Bus
// -----------------------
_distanciaEnBus = distanciaBusTotal;
var lineasUsadasVisuales = new List<string>(Math.Max(1, tramosBus.Count));
Parada paradaUltimaBus = paradaInicioBus;
var ultimoPuntoDibujado = w1.line is not null
? ObtenerUltimoPunto(w1.line)
: new[] { paradaInicioBus.Latitud, paradaInicioBus.Longitud };
for (int i = 0; i < tramosBus.Count; i++)
{
var tramo = tramosBus[i];
var esTransbordoReal = i > 0 && UtilidadesTransitoDbus.EsTransbordoReal(tramosBus[i - 1], tramo);
var infoVariante = tramo.Variante;
var lineaGeom = lineasBus[i];
string codParadaIni = infoVariante.CodigosParadas[tramo.IndiceInicio];
string codParadaFin = infoVariante.CodigosParadas[tramo.IndiceFin];
var puntoInicioTramo = ObtenerPrimerPunto(lineaGeom);
var puntoFinTramo = ObtenerUltimoPunto(lineaGeom);
var pIniTramo = ResolverParadaFisica(codParadaIni, puntoInicioTramo, i == 0 ? paradaInicioBus : paradaUltimaBus);
var pFinTramo = ResolverParadaFisica(codParadaFin, puntoFinTramo, i == tramosBus.Count - 1 ? paradaFinBus : paradaUltimaBus);
var distanciaUnion = CalcularDistanciaEntrePuntos(ultimoPuntoDibujado, puntoInicioTramo);
if (ultimoPuntoDibujado is not null && puntoInicioTramo is not null && distanciaUnion > 0)
{
if (distanciaUnion <= UmbralCosturaVisualMetros)
{
EstablecerPrimerPunto(lineaGeom, ultimoPuntoDibujado);
puntoInicioTramo = ultimoPuntoDibujado;
}
else if (!esTransbordoReal && i > 0 && distanciaUnion <= UmbralUnionPeatonalAutomaticaMetros)
{
EstablecerPrimerPunto(lineaGeom, ultimoPuntoDibujado);
puntoInicioTramo = ultimoPuntoDibujado;
}
else if ((i == 0 || esTransbordoReal) && distanciaUnion <= UmbralUnionPeatonalAutomaticaMetros)
{
var union = await WalkCached(
ultimoPuntoDibujado[0],
ultimoPuntoDibujado[1],
puntoInicioTramo[0],
puntoInicioTramo[1]);
ThrowIfPintadoObsoleto(versionPintado);
var lineaUnion = union.line ?? ConstruirLineaDirecta(ultimoPuntoDibujado, puntoInicioTramo);
var distanciaUnionReal = union.dist > 0 ? union.dist : distanciaUnion;
var duracionUnion = EstimarDuracionPieSegundos(distanciaUnionReal);
var tIniWalkUnion = tCursor;
tCursor = AddSecondsSafe(tCursor, duracionUnion);
_duracionTotalSegundos += duracionUnion;
if (i == 0)
_distanciaAPieInicio += distanciaUnionReal;
_tramosRuta.Add(new DetalleTramoRuta
{
Tipo = i == 0 ? "A pie (Ajuste acceso)" : "A pie (Transbordo)",
Descripcion = CapitalizarPalabrasClave(
i == 0
? $"{HHmm(tIniWalkUnion)} -> {HHmm(tCursor)} - Camina desde el final del acceso peatonal hasta la Parada \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
: $"{HHmm(tIniWalkUnion)} -> {HHmm(tCursor)} - Camina desde la Parada \"{paradaUltimaBus.Nombre}\" ({CodigoParadaFormateado(paradaUltimaBus.Codigo)}) hasta \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
),
DistanciaMetros = distanciaUnionReal,
DuracionSegundos = duracionUnion,
Linea = lineaUnion
});
ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync($"walk_union_{i}", lineaUnion, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
ThrowIfPintadoObsoleto(versionPintado);
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaUnion);
}
}
var distanciaTransferenciaPendiente = CalcularDistanciaEntrePuntos(ultimoPuntoDibujado, puntoInicioTramo);
if (i > 0 &&
esTransbordoReal &&
ultimoPuntoDibujado is not null &&
puntoInicioTramo is not null &&
distanciaTransferenciaPendiente > UmbralUnionPeatonalAutomaticaMetros)
{
_mensajeRuta = "La alternativa seleccionada requiere un transbordo a pie demasiado largo. Recalcula la ruta para elegir otra combinacion.";
StateHasChanged();
return;
}
if (i > 0 &&
esTransbordoReal &&
ultimoPuntoDibujado is not null &&
puntoInicioTramo is not null &&
distanciaTransferenciaPendiente > UmbralCosturaVisualMetros)
{
var wt = await WalkCached(
ultimoPuntoDibujado[0],
ultimoPuntoDibujado[1],
puntoInicioTramo[0],
puntoInicioTramo[1]);
ThrowIfPintadoObsoleto(versionPintado);
var lineaTransferencia = wt.line ?? ConstruirLineaDirecta(ultimoPuntoDibujado, puntoInicioTramo);
var distanciaTransferenciaReal = wt.dist > 0 ? wt.dist : distanciaTransferenciaPendiente;
var duracionTransferencia = EstimarDuracionPieSegundos(distanciaTransferenciaReal);
var tIniWalkTransfer = tCursor;
tCursor = AddSecondsSafe(tCursor, duracionTransferencia);
_duracionTotalSegundos += duracionTransferencia;
_tramosRuta.Add(new DetalleTramoRuta
{
Tipo = "A pie (Transbordo)",
Descripcion = CapitalizarPalabrasClave(
$"{HHmm(tIniWalkTransfer)} -> {HHmm(tCursor)} - Camina desde la Parada \"{paradaUltimaBus.Nombre}\" ({CodigoParadaFormateado(paradaUltimaBus.Codigo)}) hasta \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
),
DistanciaMetros = distanciaTransferenciaReal,
DuracionSegundos = duracionTransferencia,
Linea = lineaTransferencia
});
ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync($"walk_transfer_real_{i}", lineaTransferencia, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
ThrowIfPintadoObsoleto(versionPintado);
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaTransferencia);
if (ultimoPuntoDibujado is not null)
{
EstablecerPrimerPunto(lineaGeom, ultimoPuntoDibujado);
puntoInicioTramo = ultimoPuntoDibujado;
}
}
// -----------------------
// Transbordo andando
// -----------------------
if (false && i > 0 && ultimoPuntoDibujado is null)
{
if (!paradaUltimaBus.Codigo.Equals(codParadaIni, StringComparison.OrdinalIgnoreCase))
{
var wt = await WalkCached(
paradaUltimaBus.Latitud, paradaUltimaBus.Longitud,
pIniTramo.Latitud, pIniTramo.Longitud);
ThrowIfPintadoObsoleto(versionPintado);
if (wt.line is not null)
{
var durTrans = (wt.dist > 0) ? EstimarDuracionPieSegundos(wt.dist) : 0;
var tIniWalk = tCursor;
tCursor = AddSecondsSafe(tCursor, durTrans);
_duracionTotalSegundos += durTrans;
_tramosRuta.Add(new DetalleTramoRuta
{
Tipo = "A pie (Transbordo)",
Descripcion = CapitalizarPalabrasClave(
$"{HHmm(tIniWalk)} -> {HHmm(tCursor)} - Camina desde la Parada \"{paradaUltimaBus.Nombre}\" ({CodigoParadaFormateado(paradaUltimaBus.Codigo)}) hasta \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
),
DistanciaMetros = wt.dist,
DuracionSegundos = durTrans,
Linea = wt.line
});
ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync($"walk_transfer_{i}", wt.line, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
ThrowIfPintadoObsoleto(versionPintado);
}
}
}
// -----------------------
// Intentar horario real
// -----------------------
string codLineaItinSentido = CodigoLineaItinerarioSentido(tramo);
string codLineaItinSentidoMostrado = codLineaItinSentido;
string lineaCod = LineaCodigoStr(infoVariante);
int itin = infoVariante.CodigoItinerario;
string sentido = NormalizarSentidoCodigo(infoVariante.Sentido?.Codigo);
DateTime? depT = null;
DateTime? arrT = null;
if (_usarHorariosTeoricos && _horariosOk)
{
var primeraSalidaAceptable = esTransbordoReal
? tCursor.AddSeconds(Math.Clamp(_margenTransbordoSegundos, 0, 1800))
: tCursor;
var fecha = DateOnly.FromDateTime(primeraSalidaAceptable);
var horaMin = TimeOnly.FromDateTime(primeraSalidaAceptable);
if (Horarios.TryGetNextBusSegment(
codParadaIni,
codParadaFin,
lineaCod,
itin,
sentido,
fecha,
horaMin,
out var dep,
out var arr,
out var expIdReal))
{
depT = dep;
arrT = arr;
if (!string.IsNullOrWhiteSpace(expIdReal))
{
var codigoReal = Horarios.GetLineaItinerarioSentidoText(expIdReal, lineaCod);
if (!string.IsNullOrWhiteSpace(codigoReal) && !codigoReal.Contains('?'))
codLineaItinSentidoMostrado = codigoReal;
}
}
}
// -----------------------
// Si hay horario real: usarlo
// Si no: NO inventar horas
// -----------------------
if (depT is not null && arrT is not null)
{
void AgregarContinuidadCircularEnBus(DateTime inicio, DateTime fin, double duracionSegundos)
{
_tramosRuta.Add(new DetalleTramoRuta
{
Tipo = duracionSegundos > 1 ? "Espera en bus" : "Continuidad en bus",
Descripcion = CapitalizarPalabrasClave(
$"{HHmm(inicio)} -> {HHmm(fin)} - continuidad circular de la linea {codLineaItinSentidoMostrado}; Parada \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
),
DistanciaMetros = 0,
DuracionSegundos = Math.Max(0, duracionSegundos)
});
}
if (depT.Value > tCursor)
{
var wait = (depT.Value - tCursor).TotalSeconds;
if (wait > 1)
{
var tIniWait = tCursor;
tCursor = depT.Value;
var esperaEnBus = i > 0 && !esTransbordoReal;
_duracionTotalSegundos += wait;
if (esperaEnBus)
{
AgregarContinuidadCircularEnBus(tIniWait, tCursor, wait);
}
else
{
_tramosRuta.Add(new DetalleTramoRuta
{
Tipo = "Espera",
Descripcion = CapitalizarPalabrasClave(
$"{HHmm(tIniWait)} -> {HHmm(tCursor)} - En \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
),
DistanciaMetros = 0,
DuracionSegundos = wait
});
}
}
}
else if (i > 0 && !esTransbordoReal && Math.Abs((depT.Value - tCursor).TotalSeconds) <= 1)
{
AgregarContinuidadCircularEnBus(tCursor, tCursor, 0);
}
var durBus = (arrT.Value - depT.Value).TotalSeconds;
if (durBus < 0) durBus = 0;
_duracionTotalSegundos += durBus;
tCursor = arrT.Value;
_tramosRuta.Add(new DetalleTramoRuta
{
Tipo = "Bus",
Descripcion = CapitalizarPalabrasClave(
$"{HHmm(depT.Value)} -> {HHmm(arrT.Value)} - Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre})."
),
DistanciaMetros = 0,
DuracionSegundos = durBus,
VarianteBus = infoVariante
});
}
else
{
double distanciaTramo = 0;
for (int p = 1; p < lineaGeom.GetLength(0); p++)
{
distanciaTramo += CalcularDistanciaMetros(
lineaGeom[p - 1, 0], lineaGeom[p - 1, 1],
lineaGeom[p, 0], lineaGeom[p, 1]);
}
int paradasTramo = ContarParadasRecorridas(tramo);
double extraParadasSeg = paradasTramo * Math.Max(0, _segundosPorParadaBus);
double duracionBusSegundos = EstimarDuracionBusSegundos(distanciaTramo) + extraParadasSeg;
_duracionTotalSegundos += duracionBusSegundos;
tCursor = AddSecondsSafe(tCursor, duracionBusSegundos);
_tramosRuta.Add(new DetalleTramoRuta
{
Tipo = "Bus",
Descripcion = CapitalizarPalabrasClave(
$"Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre})."
),
DistanciaMetros = distanciaTramo,
DuracionSegundos = duracionBusSegundos,
VarianteBus = infoVariante
});
}
lineasUsadasVisuales.Add(codLineaItinSentidoMostrado);
string colorCss = RedBusServicio.ConvertirColorArgbACss(infoVariante.Linea.ColorArgb);
var fullId = $"full_{infoVariante.Linea.Codigo}_{infoVariante.CodigoItinerario}_{NormalizarSentidoCodigo(infoVariante.Sentido?.Codigo)}_{infoVariante.Variante.Codigo}_{i}_{tramo.IndiceInicio}_{tramo.IndiceFin}";
var dashBus = string.Equals(NormalizarSentidoCodigo(infoVariante.Sentido?.Codigo), "V", StringComparison.OrdinalIgnoreCase)
? DashArrayBusVuelta
: null;
ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync(fullId, lineaGeom, colorCss, 5, 0.9, dashArray: dashBus, arrowEnd: true);
ThrowIfPintadoObsoleto(versionPintado);
paradaUltimaBus = pFinTramo;
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaGeom) ?? new[] { pFinTramo.Latitud, pFinTramo.Longitud };
}
// -----------------------
// Pie 2
// -----------------------
var w2 = await WalkCached(paradaFinBus.Latitud, paradaFinBus.Longitud, _coordenadasDestino[0], _coordenadasDestino[1]);
ThrowIfPintadoObsoleto(versionPintado);
if (w2.line is not null)
{
var inicioPieFinal = ObtenerPrimerPunto(w2.line);
var distanciaUnionFinal = CalcularDistanciaEntrePuntos(ultimoPuntoDibujado, inicioPieFinal);
if (ultimoPuntoDibujado is not null && inicioPieFinal is not null && distanciaUnionFinal > 0)
{
if (distanciaUnionFinal <= UmbralCosturaVisualMetros)
{
EstablecerPrimerPunto(w2.line, ultimoPuntoDibujado);
}
else if (distanciaUnionFinal <= UmbralUnionPeatonalAutomaticaMetros)
{
var unionFinal = await WalkCached(
ultimoPuntoDibujado[0],
ultimoPuntoDibujado[1],
inicioPieFinal[0],
inicioPieFinal[1]);
ThrowIfPintadoObsoleto(versionPintado);
var lineaUnionFinal = unionFinal.line ?? ConstruirLineaDirecta(ultimoPuntoDibujado, inicioPieFinal);
var distanciaUnionFinalReal = unionFinal.dist > 0 ? unionFinal.dist : distanciaUnionFinal;
var duracionUnionFinal = EstimarDuracionPieSegundos(distanciaUnionFinalReal);
var tIniUnionFinal = tCursor;
tCursor = AddSecondsSafe(tCursor, duracionUnionFinal);
_duracionTotalSegundos += duracionUnionFinal;
_distanciaAPieFin += distanciaUnionFinalReal;
_tramosRuta.Add(new DetalleTramoRuta
{
Tipo = "A pie (Ajuste salida)",
Descripcion = CapitalizarPalabrasClave(
$"{HHmm(tIniUnionFinal)} -> {HHmm(tCursor)} - Camina desde el punto real de bajada hasta la Parada \"{paradaFinBus.Nombre}\" ({CodigoParadaFormateado(paradaFinBus.Codigo)})."
),
DistanciaMetros = distanciaUnionFinalReal,
DuracionSegundos = duracionUnionFinal,
Linea = lineaUnionFinal
});
ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync("walk_union_final", lineaUnionFinal, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
ThrowIfPintadoObsoleto(versionPintado);
EstablecerPrimerPunto(w2.line, ObtenerUltimoPunto(lineaUnionFinal));
}
}
if (w2.dist > 0)
_distanciaAPieFin += w2.dist;
var durPie2 = (w2.dist > 0) ? EstimarDuracionPieSegundos(w2.dist) : 0;
var tIni2 = tCursor;
tCursor = AddSecondsSafe(tCursor, durPie2);
_duracionTotalSegundos += durPie2;
_tramosRuta.Add(new DetalleTramoRuta
{
Tipo = "A pie",
Descripcion = CapitalizarPalabrasClave(
$"{HHmm(tIni2)} -> {HHmm(tCursor)} - Desde la Parada \"{paradaFinBus.Nombre}\" ({CodigoParadaFormateado(paradaFinBus.Codigo)}) hasta tu destino."
),
DistanciaMetros = w2.dist,
DuracionSegundos = durPie2,
Linea = w2.line
});
ThrowIfPintadoObsoleto(versionPintado);
await DrawRouteJsAsync("walk_2", w2.line, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
ThrowIfPintadoObsoleto(versionPintado);
}
var lineasUsadas = lineasUsadasVisuales.Count > 0
? CompactarCodigosLineaConsecutivos(lineasUsadasVisuales)
: CompactarCodigosLineaConsecutivos(tramosBus.Select(t => CodigoLineaItinerarioSentido(t)));
string txtLineas = (lineasUsadas.Count == 0)
? ""
: $" Lineas: {string.Join(", ", lineasUsadas)}.";
int tramosCount = tramosBus?.Count ?? 0;
int trans = UtilidadesTransitoDbus.ContarTransbordosReales(tramosBus);
_mensajeRuta = CapitalizarPalabrasClave(
$"Inicio {HHmm(now)}. Camina {FormatearDistanciaDetalleRuta(_distanciaAPieInicio)} hasta \"{_paradaOrigenSeleccionada?.Nombre}\" ({CodigoParadaFormateado(_paradaOrigenSeleccionada?.Codigo)}). " +
$"Luego viaja en bus {FormatearDistanciaDetalleRuta(_distanciaEnBus)}" +
(tramosCount > 0 ? $" en {tramosCount} tramo(s)" : "") +
(trans == 0 ? "." : $" con {trans} transbordo(s).") +
txtLineas + " " +
$"Finalmente camina {FormatearDistanciaDetalleRuta(_distanciaAPieFin)} hasta tu destino."
);
_hayRutaValida = true;
ThrowIfPintadoObsoleto(versionPintado);
StateHasChanged();
}
/// <summary>
/// Recalcula con parametros.
/// </summary>
private async Task RecalcularConParametros()
{
if (_coordenadasOrigen is null || _coordenadasDestino is null) return;
await PersistirConfiguracionAsync();
if (_mapa is not null)
{
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
await ClearRoutesJsAsync();
}
await CerrarPopupsAsync();
await CalcularRutaCompletaAsync();
StateHasChanged();
}
/// <summary>
/// Reinicia seleccion.
/// </summary>
private async Task ReiniciarSeleccion()
{
await CerrarTodasLasPreviewsAsync();
await CerrarPopupsAsync();
_ctsCalculo?.Cancel();
IniciarNuevaVersionPintadoRuta();
_coordenadasOrigen = null;
_coordenadasDestino = null;
_textoBusquedaOrigen = string.Empty;
_textoBusquedaDestino = string.Empty;
_ubicacionOrigenSeleccionada = null;
_ubicacionDestinoSeleccionada = null;
_sugerenciasBusquedaOrigen.Clear();
_sugerenciasBusquedaDestino.Clear();
_campoBusquedaActivo = null;
_mensajeRuta = string.Empty;
_hayRutaValida = false;
_paradaOrigenSeleccionada = null;
_paradaDestinoSeleccionada = null;
_primerTransbordoseleccionado = null;
_numeroTransbordos = 0;
_distanciaAPieInicio = _distanciaAPieFin = _distanciaEnBus = 0;
_paradaClicada = null;
_tramosRuta.Clear();
_duracionTotalSegundos = 0;
_opcionesRuta.Clear();
_indiceAlternativaSeleccionada = 0;
if (_mapa != null)
{
await ClearRoutesJsAsync();
await ClearEndpointMarkersJsAsync();
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
await _mapa.Geometric.DisplayPointsFromArray.deleteAllPoints();
if (_mostrarParadas)
await DibujarParadasAsync();
else
await OcultarParadasAsync(); // (repone origen/destino si hubiera, aquí ya no hay, pero ok)
}
StateHasChanged();
}
// =========================================================
// Distancia rápida
// =========================================================
/// <summary>
/// Calcula distancia metros.
/// </summary>
private static double CalcularDistanciaMetros(double lat1, double lon1, double lat2, double lon2)
{
const double R = 6371000.0; // metros
static double ToRad(double x) => x * Math.PI / 180.0;
var dLat = ToRad(lat2 - lat1);
var dLon = ToRad(lon2 - lon1);
var a =
Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
Math.Cos(ToRad(lat1)) * Math.Cos(ToRad(lat2)) *
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
return R * c;
}
}

View File

@@ -0,0 +1,470 @@
using System.Collections.Concurrent;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using LeafletForBlazor;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus;
using RutasDBUS.Servicios;
using RutasDBUS.Servicios.Caminata;
using RutasDBUS.Servicios.Horarios;
using RutasDBUS.Servicios.Planificacion;
using RutasDBUS.Servicios.RedBus;
namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas : ComponentBase
{
/// <summary>
/// Gestiona hmm.
/// </summary>
private static string HHmm(DateTime dt) => dt.ToString("HH:mm", CultureInfo.InvariantCulture);
/// <summary>
///
/// </summary>
private static DateTime Compose(DateOnly d, TimeOnly t)
=> d.ToDateTime(t);
/// <summary>
/// Añade seconds safe.
/// </summary>
private static DateTime AddSecondsSafe(DateTime dt, double seconds)
{
if (seconds <= 0) return dt;
return dt.AddSeconds(seconds);
}
// Para sacar de InfoVariante lo que necesitamos para horarios
/// <summary>
/// Gestiona codigo str.
/// </summary>
private static string LineaCodigoStr(InfoVariante v) => v?.Linea?.Codigo?.ToString() ?? "?";
// =======================
// HORARIOS (ZIP)
// =======================
private bool _usarHorariosTeoricos = true;
private bool _horariosOk = false;
private string? _horariosError = null;
/// <summary>
/// Sincroniza la penalizacion manual por parada con el modo horario activo.
/// </summary>
private void SincronizarSegundosPorParadaConHorarios()
{
if (_usarHorariosTeoricos)
{
_segundosPorParadaBus = 0;
return;
}
if (_segundosPorParadaBus == 0 && _segundosPorParadaBusManual >= 0)
_segundosPorParadaBus = _segundosPorParadaBusManual;
}
/// <summary>
/// Gestiona lista horarios con actual.
/// </summary>
private List<ProximaSalidaVm> ConstruirListaHorariosConActual(
List<(TimeOnly dep, string expId, string rutaId)> deps,
TimeOnly now)
{
var ordenados = deps
.OrderBy(x => x.dep)
.ToList();
int idxProximo = ordenados.FindIndex(x => x.dep >= now);
var lista = new List<ProximaSalidaVm>(ordenados.Count);
for (int i = 0; i < ordenados.Count; i++)
{
var d = ordenados[i];
var itiClave = Horarios.GetLineaItinerarioSentidoText(d.expId, d.rutaId);
lista.Add(new ProximaSalidaVm(
Hora: d.dep.ToString("HH:mm"),
LineaTxt: Horarios.GetLineaItinerarioSentidoNombreText(d.expId, d.rutaId),
RutaId: d.rutaId,
ExpId: d.expId,
EsProximo: i == idxProximo,
AnchorId: i == idxProximo ? "horario-proximo-anchor" : "",
ItinerarioClave: itiClave));
}
return lista;
}
/// <summary>
/// Calcula el orden lógico de una línea visible del popup, respetando TB y enviando las líneas B al bloque 100.
/// </summary>
private static int ResolverOrdenLineaHorario(string? lineaVisible, string? rutaId)
{
var texto = (lineaVisible ?? "").Trim().ToUpperInvariant();
var matchTb = Regex.Match(texto, @"^TB(\d+)$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (matchTb.Success &&
int.TryParse(matchTb.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var tb))
{
return tb;
}
var matchBuho = Regex.Match(texto, @"^B(\d+)$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (matchBuho.Success &&
int.TryParse(matchBuho.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var buho))
{
return 100 + buho;
}
if (int.TryParse(texto, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeroDirecto))
return numeroDirecto;
if (int.TryParse(rutaId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var idRuta))
return idRuta;
var numeroExtraido = UtilidadesTransitoDbus.ExtraerNumeroLinea(texto);
return numeroExtraido ?? int.MaxValue;
}
/// <summary>
/// Extrae el orden lógico visible de línea, itinerario y sentido para los horarios del popup de parada.
/// </summary>
private static (int lineaOrden, string lineaVisible, int itinerarioOrden, string sentidoOrden) ParseClaveOrdenHorario(string? rutaId, string? lineaTxt)
{
if (string.IsNullOrWhiteSpace(lineaTxt))
return (ResolverOrdenLineaHorario(null, rutaId), "", int.MaxValue, "?");
var txt = lineaTxt.Trim();
var separadorDestino = txt.IndexOf(" - ", StringComparison.Ordinal);
if (separadorDestino > 0)
txt = txt[..separadorDestino].Trim();
var match = Regex.Match(
txt,
@"^(?<linea>[^.\s]+)\.(?<itin>\d+)-(?<sentido>[A-Za-z?]+)$",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (!match.Success)
return (ResolverOrdenLineaHorario(null, rutaId), "", int.MaxValue, "?");
var lineaVisible = match.Groups["linea"].Value.Trim().ToUpperInvariant();
var sentido = match.Groups["sentido"].Value.Trim().ToUpperInvariant();
int itinerario = int.MaxValue;
int.TryParse(match.Groups["itin"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out itinerario);
return (ResolverOrdenLineaHorario(lineaVisible, rutaId), lineaVisible, itinerario, sentido);
}
private IEnumerable<ProximaSalidaVm> NextDepsOrdenados
{
get
{
var ordenadosPorClave = _nextDeps
.Select(x => new
{
Item = x,
Orden = ParseClaveOrdenHorario(x.RutaId, x.LineaTxt)
});
if (_horarioOrden == HorarioOrden.PorHora)
{
return ordenadosPorClave
.OrderBy(x => x.Item.Hora, StringComparer.Ordinal)
.ThenBy(x => x.Orden.lineaOrden)
.ThenBy(x => x.Orden.lineaVisible, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Orden.itinerarioOrden)
.ThenBy(x => x.Orden.sentidoOrden, StringComparer.OrdinalIgnoreCase)
.Select(x => x.Item);
}
return ordenadosPorClave
.OrderBy(x => x.Orden.lineaOrden)
.ThenBy(x => x.Orden.lineaVisible, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Orden.itinerarioOrden)
.ThenBy(x => x.Orden.sentidoOrden, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.Item.Hora, StringComparer.Ordinal)
.Select(x => x.Item);
}
}
/// <summary>
/// Alterna orden horarios.
/// </summary>
private void ToggleOrdenHorarios()
{
_horarioOrden = _horarioOrden == HorarioOrden.PorHora
? HorarioOrden.PorItinerario
: HorarioOrden.PorHora;
if (_horarioOrden == HorarioOrden.PorHora)
_scrollHorariosPendiente = true;
}
// Próximas salidas para el popup de parada
/// <summary>
/// Inicializa una nueva instancia de la clase ProximaSalidaVm.
/// </summary>
private sealed record ProximaSalidaVm(
string Hora,
string LineaTxt,
string RutaId,
string ExpId,
bool EsProximo,
string AnchorId,
string ItinerarioClave);
private List<ProximaSalidaVm> _nextDeps = new();
/// <summary>
/// Gestiona txt desde ruta id.
/// </summary>
private string LineaTxtDesdeRutaId(string? rutaId)
{
var rid = CodigoSinDecimal(rutaId);
if (string.IsNullOrWhiteSpace(rid))
return $"L\u00ednea ?";
// Intenta mapear contra tu JSON de RedBus (Lineas)
var linea = RedBusServicio.Lineas
.FirstOrDefault(l => CodigoSinDecimal(l.Codigo?.ToString()) == rid);
if (linea is not null)
return $"L\u00ednea {CodigoSinDecimal(linea.Codigo?.ToString())} - {linea.Nombre}";
// Fallback si no encontramos la línea en el JSON
return $"L\u00ednea {rid}";
}
/// <summary>
/// Gestiona toggle hora simulada.
/// </summary>
private async Task OnToggleHoraSimulada(ChangeEventArgs e)
{
_usarHoraSimulada = e?.Value is bool b && b;
if (_usarHoraSimulada)
{
var ahora = DateTime.Now;
_fechaSimulada = DateOnly.FromDateTime(ahora);
_horaSimulada = TimeOnly.FromDateTime(ahora);
StopClock();
}
else
{
StartClock();
}
await RefrescarHoraUIAsync();
await PersistirConfiguracionAsync();
}
// ====== HORA (real / simulada) ======
private bool _usarHoraSimulada = true;
private TimeOnly _horaSimulada = TimeOnly.FromDateTime(DateTime.Now);
private DateOnly _fechaSimulada = DateOnly.FromDateTime(DateTime.Now);
/// <summary>
/// Gestiona local.
/// </summary>
private DateTime AhoraLocal()
{
if (_usarHoraSimulada)
return _fechaSimulada.ToDateTime(_horaSimulada);
return DateTime.Now;
}
/// <summary>
/// Inicializa la hora simulada con la hora real actual y la deja parada.
/// </summary>
protected override void OnInitialized()
{
SincronizarSegundosPorParadaConHorarios();
if (!_usarHoraSimulada)
return;
var ahora = DateTime.Now;
_fechaSimulada = DateOnly.FromDateTime(ahora);
_horaSimulada = TimeOnly.FromDateTime(ahora);
var es = new CultureInfo("es-ES");
_horaAhoraTxt = es.TextInfo.ToTitleCase(ahora.ToString("ddd dd/MM HH:mm:ss", es));
}
/// <summary>
/// Gestiona change fecha simulada.
/// </summary>
private async Task OnChangeFechaSimulada(ChangeEventArgs e)
{
var s = e?.Value?.ToString();
if (!string.IsNullOrWhiteSpace(s) && DateOnly.TryParse(s, out var d))
_fechaSimulada = d;
if (_usarHoraSimulada)
await RefrescarHoraUIAsync();
await PersistirConfiguracionAsync();
}
/// <summary>
/// Gestiona change hora simulada.
/// </summary>
private async Task OnChangeHoraSimulada(ChangeEventArgs e)
{
var s = e?.Value?.ToString();
if (!string.IsNullOrWhiteSpace(s) && TimeOnly.TryParse(s, out var t))
_horaSimulada = t;
if (_usarHoraSimulada)
await RefrescarHoraUIAsync();
await PersistirConfiguracionAsync();
}
/// <summary>
/// Ajusta la fecha y la hora simuladas al momento actual.
/// </summary>
private async Task IrAAhoraSimuladoAsync()
{
var ahora = DateTime.Now;
_fechaSimulada = DateOnly.FromDateTime(ahora);
_horaSimulada = TimeOnly.FromDateTime(ahora);
if (_usarHoraSimulada)
await RefrescarHoraUIAsync();
await PersistirConfiguracionAsync();
StateHasChanged();
}
/// <summary>
/// Restablece hora pruebas.
/// </summary>
private async Task ResetHoraPruebas()
{
_usarHoraSimulada = false;
_fechaSimulada = DateOnly.FromDateTime(DateTime.Now);
_horaSimulada = TimeOnly.FromDateTime(DateTime.Now);
StartClock();
await RefrescarHoraUIAsync();
}
private string _horaAhoraTxt = "";
private PeriodicTimer? _clockTimer;
private CancellationTokenSource? _clockCts;
/// <summary>
/// Gestiona clock.
/// </summary>
private void StartClock()
{
if (_clockTimer is not null) return;
_clockCts = new CancellationTokenSource();
_clockTimer = new PeriodicTimer(TimeSpan.FromSeconds(1));
_ = Task.Run(async () =>
{
try
{
while (await _clockTimer.WaitForNextTickAsync(_clockCts.Token))
{
// ✅ Solo actualizamos si NO estamos en simulada
if (_usarHoraSimulada) continue;
var now = DateTime.Now;
var es = new CultureInfo("es-ES");
_horaAhoraTxt = es.TextInfo.ToTitleCase(now.ToString("ddd dd/MM HH:mm:ss", es));
await InvokeAsync(StateHasChanged); // ✅ como antes
}
}
catch { /* cancel */ }
});
}
/// <summary>
/// Gestiona clock.
/// </summary>
private void StopClock()
{
_clockCts?.Cancel();
_clockTimer?.Dispose();
_clockCts?.Dispose();
_clockTimer = null;
_clockCts = null;
}
/// <summary>
/// Actualiza hora ui.
/// </summary>
private async Task RefrescarHoraUIAsync()
{
// pinta la hora (real o simulada) y refresca UI
var es = new CultureInfo("es-ES");
DateTime now;
if (_usarHoraSimulada)
now = _fechaSimulada.ToDateTime(_horaSimulada);
else
now = DateTime.Now;
_horaAhoraTxt = es.TextInfo.ToTitleCase(now.ToString("ddd dd/MM HH:mm:ss", es));
await InvokeAsync(StateHasChanged);
}
/// <summary>
///
/// </summary>
public void Dispose()
{
_clockCts?.Cancel();
_clockTimer?.Dispose();
_clockCts?.Dispose();
StopRefrescoHistorialCompartido();
_dotRef?.Dispose();
}
/// <summary>
///
/// </summary>
private static IEnumerable<int> Range(int from, int to, int step = 1)
{
if (step <= 0) step = 1;
if (from <= to)
{
for (int i = from; i <= to; i += step) yield return i;
}
else
{
for (int i = from; i >= to; i -= step) yield return i;
}
}
private Task<(double[,]? line, double dist, double dur)> WalkCached(double lat1, double lon1, double lat2, double lon2)
{
return _walkProvider == ProveedorCaminata.Valhalla
? WalkValhallaCached(lat1, lon1, lat2, lon2)
: WalkOsrmCached(lat1, lon1, lat2, lon2);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using RutasDBUS.Servicios.Caminata;
using RutasDBUS.Servicios.Geocodificacion;
using RutasDBUS.Servicios.Historial;
using RutasDBUS.Servicios.Horarios;
using RutasDBUS.Servicios.Importacion;
using RutasDBUS.Servicios.Planificacion;
using RutasDBUS.Servicios.RedBus;
namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas : ComponentBase
{
[Inject] protected ServicioHorariosDbus Horarios { get; set; } = default!;
[Inject] protected IServicioArchivoImportacionDbus ArchivoImportacionDbus { get; set; } = default!;
[Inject] protected IWebHostEnvironment Env { get; set; } = default!;
[Inject] protected IRedBusServicio RedBusServicio { get; set; } = default!;
[Inject] protected IPlanificadorRutas PlanificadorRutasServicio { get; set; } = default!;
[Inject] protected IHistorialRutasCompartidoService HistorialRutasCompartidoService { get; set; } = default!;
[Inject] protected IGeocodificadorLocalServicio GeocodificadorLocal { get; set; } = default!;
[Inject] protected IClienteOsrm EsrmCliente { get; set; } = default!;
[Inject] protected IJSRuntime JS { get; set; } = default!;
[Inject] protected IClienteValhalla ClienteValhalla { get; set; } = default!;
[Inject] protected HttpClient Http { get; set; } = default!;
}

View File

@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -0,0 +1,12 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using RutasDBUS
@using RutasDBUS.Components
@using LeafletForBlazor