Initial clean RutasDBUS app
This commit is contained in:
22
RutasDBUS/Components/App.razor
Normal file
22
RutasDBUS/Components/App.razor
Normal 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>
|
||||
23
RutasDBUS/Components/Layout/MainLayout.razor
Normal file
23
RutasDBUS/Components/Layout/MainLayout.razor
Normal 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>
|
||||
96
RutasDBUS/Components/Layout/MainLayout.razor.css
Normal file
96
RutasDBUS/Components/Layout/MainLayout.razor.css
Normal 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;
|
||||
}
|
||||
118
RutasDBUS/Components/Layout/NavMenu.razor
Normal file
118
RutasDBUS/Components/Layout/NavMenu.razor
Normal 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> *@
|
||||
105
RutasDBUS/Components/Layout/NavMenu.razor.css
Normal file
105
RutasDBUS/Components/Layout/NavMenu.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
36
RutasDBUS/Components/Pages/Error.razor
Normal file
36
RutasDBUS/Components/Pages/Error.razor
Normal 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;
|
||||
}
|
||||
496
RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs
Normal file
496
RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs
Normal 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]);
|
||||
}
|
||||
|
||||
}
|
||||
1633
RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs
Normal file
1633
RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs
Normal file
File diff suppressed because it is too large
Load Diff
299
RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs
Normal file
299
RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
593
RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs
Normal file
593
RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
1565
RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs
Normal file
1565
RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs
Normal file
File diff suppressed because it is too large
Load Diff
245
RutasDBUS/Components/Pages/PlanificadorRutas.ModelosHorarios.cs
Normal file
245
RutasDBUS/Components/Pages/PlanificadorRutas.ModelosHorarios.cs
Normal 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);
|
||||
}
|
||||
360
RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs
Normal file
360
RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
2285
RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs
Normal file
2285
RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs
Normal file
File diff suppressed because it is too large
Load Diff
844
RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs
Normal file
844
RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
470
RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs
Normal file
470
RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
1598
RutasDBUS/Components/Pages/PlanificadorRutas.razor
Normal file
1598
RutasDBUS/Components/Pages/PlanificadorRutas.razor
Normal file
File diff suppressed because it is too large
Load Diff
26
RutasDBUS/Components/Pages/PlanificadorRutas.razor.cs
Normal file
26
RutasDBUS/Components/Pages/PlanificadorRutas.razor.cs
Normal 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!;
|
||||
}
|
||||
6
RutasDBUS/Components/Routes.razor
Normal file
6
RutasDBUS/Components/Routes.razor
Normal 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>
|
||||
12
RutasDBUS/Components/_Imports.razor
Normal file
12
RutasDBUS/Components/_Imports.razor
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user