adaptacion a movil de funcionalidad y vista

This commit is contained in:
2026-07-17 12:00:30 +02:00
parent c3cbec3523
commit 7dc251f44e
10 changed files with 1123 additions and 51 deletions

View File

@@ -7,14 +7,14 @@
<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="stylesheet" href="app.css?v=20260716m3" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="app.js?v=20260626b" defer></script>
<script src="app.js?v=20260716m3" defer></script>
<script src="_framework/blazor.web.js"></script>
</body>

View File

@@ -50,6 +50,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
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.initMobileMapControls"); } catch (Exception ex) { Console.WriteLine("rt.initMobileMapControls: " + ex); }
try { await JS.InvokeVoidAsync("rt.initDraggablePopups"); } catch (Exception ex) { Console.WriteLine("rt.initDraggablePopups: " + ex); }
await CargarConfiguracionLocalAsync();
await AsegurarHistorialLocalCargadoAsync();
@@ -253,6 +254,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
private double _inspectorLat, _inspectorLon;
private double? _inspectorElev;
private double? _inspectorElevIdee;
private string? _inspectorCallejero;
/// <summary>

View File

@@ -351,6 +351,7 @@ public partial class PlanificadorRutas : ComponentBase
private TimeSpan? _tiempoUltimoCalculoRuta = null;
private bool _mostrarElevacionesRuta = false;
private bool _pintandoElevacionesRuta = false;
private bool _panelRutasMovilContraido = false;
private double _umbralPendienteSubidaRuta = 3.0;
private double _umbralPendienteBajadaRuta = 3.0;
private readonly Dictionary<string, List<Parada>> _paradasItinerarioVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
@@ -1111,9 +1112,7 @@ public partial class PlanificadorRutas : ComponentBase
if (string.IsNullOrWhiteSpace(distancia))
return nombre;
return cercano.DistanciaMetros <= 20
? $"{nombre} ({distancia})"
: $"mas cercano: {nombre} ({distancia})";
return $"{nombre} ({distancia})";
}
private static string LimpiarTextoCallejeroResumen(string texto)

View File

@@ -1,5 +1,6 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.JSInterop;
using RutasDBUS.Modelos.Historial;
@@ -8,8 +9,14 @@ namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas
{
private const string ClaveHistorialLocal = "rutasdbus.planificador.routeHistory.v1";
private const string ClaveRespaldoHistorialLocal = "rutasdbus.planificador.routeHistory.recovery.v1";
private const int MaxItemsHistorialLocal = 100;
private const int IntervaloRefrescoHistorialCompartidoSegundos = 3;
private static readonly JsonSerializerOptions OpcionesLecturaHistorialLocal = new()
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
private readonly List<RegistroHistorialRuta> _historialLocal = new();
private readonly List<RegistroHistorialRuta> _historialCompartido = new();
@@ -26,25 +33,38 @@ public partial class PlanificadorRutas
private ConfiguracionLocalPlanificador? _configuracionTemporalPreviaHistorial;
private Task<bool>? _tareaCargaHistorialLocal;
private bool _historialLocalCargado = false;
private bool _historialLocalCargando = false;
private int _versionAperturaHistorial;
private async Task ToggleHistorialPanel()
{
if (_mostrarHistorialRutas)
{
_versionAperturaHistorial++;
_mostrarHistorialRutas = false;
_historialLocalCargando = false;
StopRefrescoHistorialCompartido();
await RestaurarConfiguracionTemporalHistorialAsync();
return;
}
if (!await AsegurarHistorialLocalCargadoAsync())
{
MostrarMensajeHistorial("No he podido cargar el historial personal. Intentalo de nuevo.", esError: true);
return;
}
var versionApertura = ++_versionAperturaHistorial;
_mostrarHistorialRutas = true;
_historialLocalCargando = !_historialLocalCargado;
StateHasChanged();
var historialLocalCargado = await AsegurarHistorialLocalCargadoAsync();
if (!_mostrarHistorialRutas || versionApertura != _versionAperturaHistorial)
return;
_historialLocalCargando = false;
if (!historialLocalCargado)
MostrarMensajeHistorial("No he podido cargar el historial personal. Intentalo de nuevo.", esError: true);
await CargarHistorialCompartidoAsync();
if (!_mostrarHistorialRutas || versionApertura != _versionAperturaHistorial)
return;
StartRefrescoHistorialCompartido();
}
@@ -55,44 +75,127 @@ public partial class PlanificadorRutas
_tareaCargaHistorialLocal ??= CargarHistorialLocalDesdeNavegadorAsync();
var tareaCarga = _tareaCargaHistorialLocal;
var cargado = await tareaCarga;
if (!cargado && ReferenceEquals(_tareaCargaHistorialLocal, tareaCarga))
_tareaCargaHistorialLocal = null;
return cargado;
try
{
return await tareaCarga;
}
catch
{
return false;
}
finally
{
if (!_historialLocalCargado && ReferenceEquals(_tareaCargaHistorialLocal, tareaCarga))
_tareaCargaHistorialLocal = null;
}
}
private async Task<bool> CargarHistorialLocalDesdeNavegadorAsync()
{
string? json;
try
{
var json = await JS.InvokeAsync<string?>("rt.loadLocalValue", ClaveHistorialLocal);
if (!string.IsNullOrWhiteSpace(json))
{
var items = JsonSerializer.Deserialize<List<RegistroHistorialRuta>>(json);
if (items is null)
return false;
_historialLocal.Clear();
_historialLocal.AddRange(items
.Where(x => !string.IsNullOrWhiteSpace(x.Id))
.OrderByDescending(x => x.FechaActualizacionUtc)
.Take(MaxItemsHistorialLocal));
}
_historialLocalSeleccionadoId = _historialLocal.FirstOrDefault()?.Id ?? string.Empty;
_historialLocalCargado = true;
return true;
json = await JS.InvokeAsync<string?>("rt.loadLocalValue", ClaveHistorialLocal);
}
catch (JSException)
{
return false;
}
catch (JsonException)
catch (Exception)
{
return false;
}
if (string.IsNullOrWhiteSpace(json))
{
FinalizarCargaHistorialLocal(Array.Empty<RegistroHistorialRuta>());
return true;
}
try
{
var items = DeserializarHistorialLocal(json, out var registrosOmitidos);
FinalizarCargaHistorialLocal(items);
if (registrosOmitidos > 0)
{
await GuardarHistorialLocalRecuperadoAsync(json);
MostrarMensajeHistorial(
$"Se han recuperado {_historialLocal.Count} registros del historial; {registrosOmitidos} estaban dañados.",
esError: true);
}
return true;
}
catch (Exception ex) when (ex is JsonException or NotSupportedException or InvalidOperationException)
{
FinalizarCargaHistorialLocal(Array.Empty<RegistroHistorialRuta>());
await GuardarHistorialLocalRecuperadoAsync(json);
MostrarMensajeHistorial(
"El historial personal guardado estaba dañado. Se ha conservado una copia y se ha iniciado uno nuevo.",
esError: true);
return true;
}
}
private static IReadOnlyList<RegistroHistorialRuta> DeserializarHistorialLocal(
string json,
out int registrosOmitidos)
{
using var documento = JsonDocument.Parse(json);
if (documento.RootElement.ValueKind != JsonValueKind.Array)
throw new JsonException("El historial local no contiene una lista.");
registrosOmitidos = 0;
var recuperados = new List<RegistroHistorialRuta>();
foreach (var elemento in documento.RootElement.EnumerateArray())
{
try
{
var item = elemento.Deserialize<RegistroHistorialRuta>(OpcionesLecturaHistorialLocal);
if (item is null)
{
registrosOmitidos++;
continue;
}
if (string.IsNullOrWhiteSpace(item.Id))
item.Id = Guid.NewGuid().ToString("N");
item.LineasPermitidas ??= new List<int>();
recuperados.Add(item);
}
catch (Exception ex) when (ex is JsonException or NotSupportedException or InvalidOperationException)
{
registrosOmitidos++;
}
}
return recuperados
.OrderByDescending(x => x.FechaActualizacionUtc)
.Take(MaxItemsHistorialLocal)
.ToList();
}
private void FinalizarCargaHistorialLocal(IEnumerable<RegistroHistorialRuta> items)
{
_historialLocal.Clear();
_historialLocal.AddRange(items);
_historialLocalSeleccionadoId = _historialLocal.FirstOrDefault()?.Id ?? string.Empty;
_historialLocalCargado = true;
}
private async Task GuardarHistorialLocalRecuperadoAsync(string jsonOriginal)
{
try
{
await JS.InvokeVoidAsync("rt.saveLocalValue", ClaveRespaldoHistorialLocal, jsonOriginal);
var jsonRecuperado = JsonSerializer.Serialize(_historialLocal);
await JS.InvokeVoidAsync("rt.saveLocalValue", ClaveHistorialLocal, jsonRecuperado);
}
catch (JSException)
{
}
}
private async Task PersistirHistorialLocalAsync()

View File

@@ -34,6 +34,7 @@ public partial class PlanificadorRutas : ComponentBase
private void ToggleBuscadorMapa()
{
_mostrarBuscadorMapa = !_mostrarBuscadorMapa;
_panelRutasMovilContraido = _mostrarBuscadorMapa;
}
/// <summary>
@@ -1181,7 +1182,7 @@ public partial class PlanificadorRutas : ComponentBase
private bool _elevPopupMinimizado = false;
private const string _versionApp = "(v-20260715a)";
private const string _versionApp = "(v-20260716a)";
private double? _paradaElevG;

View File

@@ -1269,6 +1269,7 @@ public partial class PlanificadorRutas : ComponentBase
{
_inspectorLat = lat;
_inspectorLon = lon;
_inspectorCallejero = null;
try
{
@@ -1316,6 +1317,19 @@ public partial class PlanificadorRutas : ComponentBase
_inspectorElevIdee = null;
}
try
{
var lugar = await GeocodificadorLocal.BuscarMasCercanoAsync(
lat,
lon,
CancellationToken.None);
_inspectorCallejero = FormatearLugarCallejeroResumen(lugar);
}
catch
{
_inspectorCallejero = null;
}
_popupInspectorVisible = true;
StateHasChanged();
}
@@ -1411,6 +1425,34 @@ public partial class PlanificadorRutas : ComponentBase
}
});
}
/// <summary>
/// Selecciona origen o destino mediante pulsacion larga en dispositivos tactiles.
/// </summary>
[JSInvokable]
public async Task OnMobileMapLongPress(double lat, double lon)
{
await InvokeAsync(async () =>
{
if (_inspectorActivo)
return;
_ctsDobleClick?.Cancel();
_ctsDobleClick?.Dispose();
_ctsDobleClick = null;
_primerClickPendiente = null;
_paradaPrimerClickPendiente = null;
var paradaHit = await DetectarParadaHitAsync(lat, lon);
if (paradaHit is null && !_mostrarParadas)
paradaHit = await DetectarParadaDestacadaHitAsync(lat, lon);
if (paradaHit is not null)
await EjecutarSeleccionOrigenDestinoAsync(paradaHit.Latitud, paradaHit.Longitud);
else
await EjecutarSeleccionOrigenDestinoAsync(lat, lon);
});
}
/// <summary>
/// Gestiona seleccion origen destino.
@@ -1515,6 +1557,7 @@ public partial class PlanificadorRutas : ComponentBase
return;
var cronometroCalculo = System.Diagnostics.Stopwatch.StartNew();
_panelRutasMovilContraido = _mostrarBuscadorMapa;
await CerrarTodasLasPreviewsAsync();

View File

@@ -984,6 +984,7 @@ public partial class PlanificadorRutas : ComponentBase
_duracionTotalSegundos = 0;
_opcionesRuta.Clear();
_indiceAlternativaSeleccionada = 0;
_panelRutasMovilContraido = false;
if (_mapa != null)
{

View File

@@ -26,11 +26,13 @@
<!-- IZQUIERDA -->
<div class="header-left">
<h3 class="titulo-app">
Planificador de rutas en Dbus
<span class="desktop-only">Planificador de rutas en Dbus</span>
<span class="mobile-only">Rutas Dbus</span>
<span class="app-version-inline">@_versionApp</span>
</h3>
<small>
Haz doble clic en el mapa para marcar origen y destino
<span class="desktop-only">Haz doble clic en el mapa para marcar origen y destino</span>
<span class="mobile-only">Mantén pulsado el mapa para marcar origen y destino</span>
</small>
</div>
@@ -77,10 +79,11 @@
🕒 @_horaAhoraTxt
</div>
<button type="button"
class="btn-ghost btn-icon search-toggle"
class="btn-ghost btn-icon search-toggle mobile-nav-action"
title="@(_mostrarBuscadorMapa ? "Ocultar buscador" : "Mostrar buscador")"
@onclick="ToggleBuscadorMapa">
<span class="search-icon @(_mostrarBuscadorMapa ? "off" : "")">🔎</span>
<span class="mobile-action-label">Buscar</span>
</button>
@if (_inspectorActivo)
{
@@ -90,24 +93,29 @@
</div>
}
<button type="button"
class="pill-live pill-clickable @( _mostrarParadas ? "pill-ok" : "pill-bad")"
class="pill-live pill-clickable mobile-nav-action @( _mostrarParadas ? "pill-ok" : "pill-bad")"
title="@(_mostrarParadas ? "Ocultar paradas" : "Mostrar paradas")"
@onclick="ToggleParadasAsync">
<span class="pill-dot"></span>
@(_mostrarParadas ? "Paradas visibles" : "Paradas ocultas")
<span class="desktop-action-label">@(_mostrarParadas ? "Paradas visibles" : "Paradas ocultas")</span>
<span class="mobile-action-label">Paradas</span>
</button>
<button class="btn-ghost" title="limpiar rutas en pantalla" @onclick="ReiniciarSeleccion">
Reiniciar
<button class="btn-ghost mobile-nav-action" title="Limpiar rutas en pantalla" @onclick="ReiniciarSeleccion">
<span class="desktop-action-label">Reiniciar</span>
<span class="mobile-action-icon" aria-hidden="true">↺</span>
<span class="mobile-action-label">Reiniciar</span>
</button>
<button type="button" class="btn-ghost btn-icon" title="Historial de rutas" @onclick="ToggleHistorialPanel">
<button type="button" class="btn-ghost btn-icon mobile-nav-action" title="Historial de rutas" @onclick="ToggleHistorialPanel">
<span class="history-open-icon" aria-hidden="true">H</span>
<span class="mobile-action-label">Historial</span>
</button>
<button class="btn-ghost" title="Configuración" @onclick="ToggleConfigPanel">
<button class="btn-ghost mobile-nav-action" title="Configuración" @onclick="ToggleConfigPanel">
<span class="nav-action-icon" aria-hidden="true">⚙</span>
<span class="mobile-action-label">Ajustes</span>
</button>
</div>
@@ -336,6 +344,10 @@
<div class="popup-parada-body">
<div><b>Latitud:</b> @_inspectorLat.ToString("0.000000")</div>
<div><b>Longitud:</b> @_inspectorLon.ToString("0.000000")</div>
@if (!string.IsNullOrWhiteSpace(_inspectorCallejero))
{
<div><b>Callejero:</b> @_inspectorCallejero</div>
}
<div>
<b>Altitud (O):</b>
@@ -611,12 +623,20 @@
<!-- PANEL DERECHO: RUTAS -->
<div class="route-panel-overlay draggable-popup" data-drag-key="panel-rutas">
<div class="route-panel-overlay draggable-popup @(_panelRutasMovilContraido ? "route-panel-overlay--mobile-collapsed" : "")" data-drag-key="panel-rutas">
@if (!string.IsNullOrEmpty(_mensajeRuta) || _opcionesRuta.Count > 0)
{
<div class="route-panel">
<div class="route-panel-header">
<h5 class="route-panel-title draggable-popup-handle">Rutas sugeridas</h5>
<button type="button"
class="route-mobile-collapse mobile-only"
title="@(_panelRutasMovilContraido ? "Desplegar resultados" : "Contraer resultados")"
aria-label="@(_panelRutasMovilContraido ? "Desplegar resultados" : "Contraer resultados")"
aria-expanded="@(!_panelRutasMovilContraido)"
@onclick="() => _panelRutasMovilContraido = !_panelRutasMovilContraido">
<span aria-hidden="true">@(_panelRutasMovilContraido ? "▴" : "▾")</span>
</button>
<div class="route-priority-switch" role="group" aria-label="Criterio prioritario de rutas">
<button type="button"
class="route-priority-option @(CriterioPrioritarioAndarActivo ? "route-priority-option-active" : "")"
@@ -1042,7 +1062,11 @@
<div class="history-panel-body">
<div class="history-section">
<label class="history-section-label">Personal:</label>
@if (_historialLocal.Count == 0)
@if (_historialLocalCargando)
{
<div class="history-empty">Cargando historial...</div>
}
else if (_historialLocal.Count == 0)
{
<div class="history-empty">Sin historial.</div>
}