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="/" /> <base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" /> <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="RutasDBUS.styles.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" /> <link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet /> <HeadOutlet />
</head> </head>
<body> <body>
<Routes /> <Routes />
<script src="app.js?v=20260626b" defer></script> <script src="app.js?v=20260716m3" defer></script>
<script src="_framework/blazor.web.js"></script> <script src="_framework/blazor.web.js"></script>
</body> </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("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.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.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); } try { await JS.InvokeVoidAsync("rt.initDraggablePopups"); } catch (Exception ex) { Console.WriteLine("rt.initDraggablePopups: " + ex); }
await CargarConfiguracionLocalAsync(); await CargarConfiguracionLocalAsync();
await AsegurarHistorialLocalCargadoAsync(); await AsegurarHistorialLocalCargadoAsync();
@@ -253,6 +254,7 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
private double _inspectorLat, _inspectorLon; private double _inspectorLat, _inspectorLon;
private double? _inspectorElev; private double? _inspectorElev;
private double? _inspectorElevIdee; private double? _inspectorElevIdee;
private string? _inspectorCallejero;
/// <summary> /// <summary>

View File

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

View File

@@ -1,5 +1,6 @@
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using RutasDBUS.Modelos.Historial; using RutasDBUS.Modelos.Historial;
@@ -8,8 +9,14 @@ namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas public partial class PlanificadorRutas
{ {
private const string ClaveHistorialLocal = "rutasdbus.planificador.routeHistory.v1"; 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 MaxItemsHistorialLocal = 100;
private const int IntervaloRefrescoHistorialCompartidoSegundos = 3; 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> _historialLocal = new();
private readonly List<RegistroHistorialRuta> _historialCompartido = new(); private readonly List<RegistroHistorialRuta> _historialCompartido = new();
@@ -26,25 +33,38 @@ public partial class PlanificadorRutas
private ConfiguracionLocalPlanificador? _configuracionTemporalPreviaHistorial; private ConfiguracionLocalPlanificador? _configuracionTemporalPreviaHistorial;
private Task<bool>? _tareaCargaHistorialLocal; private Task<bool>? _tareaCargaHistorialLocal;
private bool _historialLocalCargado = false; private bool _historialLocalCargado = false;
private bool _historialLocalCargando = false;
private int _versionAperturaHistorial;
private async Task ToggleHistorialPanel() private async Task ToggleHistorialPanel()
{ {
if (_mostrarHistorialRutas) if (_mostrarHistorialRutas)
{ {
_versionAperturaHistorial++;
_mostrarHistorialRutas = false; _mostrarHistorialRutas = false;
_historialLocalCargando = false;
StopRefrescoHistorialCompartido(); StopRefrescoHistorialCompartido();
await RestaurarConfiguracionTemporalHistorialAsync(); await RestaurarConfiguracionTemporalHistorialAsync();
return; return;
} }
if (!await AsegurarHistorialLocalCargadoAsync()) var versionApertura = ++_versionAperturaHistorial;
{
MostrarMensajeHistorial("No he podido cargar el historial personal. Intentalo de nuevo.", esError: true);
return;
}
_mostrarHistorialRutas = true; _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(); await CargarHistorialCompartidoAsync();
if (!_mostrarHistorialRutas || versionApertura != _versionAperturaHistorial)
return;
StartRefrescoHistorialCompartido(); StartRefrescoHistorialCompartido();
} }
@@ -55,44 +75,127 @@ public partial class PlanificadorRutas
_tareaCargaHistorialLocal ??= CargarHistorialLocalDesdeNavegadorAsync(); _tareaCargaHistorialLocal ??= CargarHistorialLocalDesdeNavegadorAsync();
var tareaCarga = _tareaCargaHistorialLocal; var tareaCarga = _tareaCargaHistorialLocal;
var cargado = await tareaCarga; try
{
if (!cargado && ReferenceEquals(_tareaCargaHistorialLocal, tareaCarga)) return await tareaCarga;
_tareaCargaHistorialLocal = null; }
catch
return cargado; {
return false;
}
finally
{
if (!_historialLocalCargado && ReferenceEquals(_tareaCargaHistorialLocal, tareaCarga))
_tareaCargaHistorialLocal = null;
}
} }
private async Task<bool> CargarHistorialLocalDesdeNavegadorAsync() private async Task<bool> CargarHistorialLocalDesdeNavegadorAsync()
{ {
string? json;
try try
{ {
var json = await JS.InvokeAsync<string?>("rt.loadLocalValue", ClaveHistorialLocal); 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;
} }
catch (JSException) catch (JSException)
{ {
return false; return false;
} }
catch (JsonException) catch (Exception)
{ {
return false; 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() private async Task PersistirHistorialLocalAsync()

View File

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

View File

@@ -1269,6 +1269,7 @@ public partial class PlanificadorRutas : ComponentBase
{ {
_inspectorLat = lat; _inspectorLat = lat;
_inspectorLon = lon; _inspectorLon = lon;
_inspectorCallejero = null;
try try
{ {
@@ -1316,6 +1317,19 @@ public partial class PlanificadorRutas : ComponentBase
_inspectorElevIdee = null; _inspectorElevIdee = null;
} }
try
{
var lugar = await GeocodificadorLocal.BuscarMasCercanoAsync(
lat,
lon,
CancellationToken.None);
_inspectorCallejero = FormatearLugarCallejeroResumen(lugar);
}
catch
{
_inspectorCallejero = null;
}
_popupInspectorVisible = true; _popupInspectorVisible = true;
StateHasChanged(); StateHasChanged();
} }
@@ -1412,6 +1426,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> /// <summary>
/// Gestiona seleccion origen destino. /// Gestiona seleccion origen destino.
/// </summary> /// </summary>
@@ -1515,6 +1557,7 @@ public partial class PlanificadorRutas : ComponentBase
return; return;
var cronometroCalculo = System.Diagnostics.Stopwatch.StartNew(); var cronometroCalculo = System.Diagnostics.Stopwatch.StartNew();
_panelRutasMovilContraido = _mostrarBuscadorMapa;
await CerrarTodasLasPreviewsAsync(); await CerrarTodasLasPreviewsAsync();

View File

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

View File

@@ -26,11 +26,13 @@
<!-- IZQUIERDA --> <!-- IZQUIERDA -->
<div class="header-left"> <div class="header-left">
<h3 class="titulo-app"> <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> <span class="app-version-inline">@_versionApp</span>
</h3> </h3>
<small> <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> </small>
</div> </div>
@@ -77,10 +79,11 @@
🕒 @_horaAhoraTxt 🕒 @_horaAhoraTxt
</div> </div>
<button type="button" <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")" title="@(_mostrarBuscadorMapa ? "Ocultar buscador" : "Mostrar buscador")"
@onclick="ToggleBuscadorMapa"> @onclick="ToggleBuscadorMapa">
<span class="search-icon @(_mostrarBuscadorMapa ? "off" : "")">🔎</span> <span class="search-icon @(_mostrarBuscadorMapa ? "off" : "")">🔎</span>
<span class="mobile-action-label">Buscar</span>
</button> </button>
@if (_inspectorActivo) @if (_inspectorActivo)
{ {
@@ -90,24 +93,29 @@
</div> </div>
} }
<button type="button" <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")" title="@(_mostrarParadas ? "Ocultar paradas" : "Mostrar paradas")"
@onclick="ToggleParadasAsync"> @onclick="ToggleParadasAsync">
<span class="pill-dot"></span> <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>
<button class="btn-ghost" title="limpiar rutas en pantalla" @onclick="ReiniciarSeleccion"> <button class="btn-ghost mobile-nav-action" title="Limpiar rutas en pantalla" @onclick="ReiniciarSeleccion">
Reiniciar <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>
<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="history-open-icon" aria-hidden="true">H</span>
<span class="mobile-action-label">Historial</span>
</button> </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> </button>
</div> </div>
@@ -336,6 +344,10 @@
<div class="popup-parada-body"> <div class="popup-parada-body">
<div><b>Latitud:</b> @_inspectorLat.ToString("0.000000")</div> <div><b>Latitud:</b> @_inspectorLat.ToString("0.000000")</div>
<div><b>Longitud:</b> @_inspectorLon.ToString("0.000000")</div> <div><b>Longitud:</b> @_inspectorLon.ToString("0.000000")</div>
@if (!string.IsNullOrWhiteSpace(_inspectorCallejero))
{
<div><b>Callejero:</b> @_inspectorCallejero</div>
}
<div> <div>
<b>Altitud (O):</b> <b>Altitud (O):</b>
@@ -611,12 +623,20 @@
<!-- PANEL DERECHO: RUTAS --> <!-- 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) @if (!string.IsNullOrEmpty(_mensajeRuta) || _opcionesRuta.Count > 0)
{ {
<div class="route-panel"> <div class="route-panel">
<div class="route-panel-header"> <div class="route-panel-header">
<h5 class="route-panel-title draggable-popup-handle">Rutas sugeridas</h5> <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"> <div class="route-priority-switch" role="group" aria-label="Criterio prioritario de rutas">
<button type="button" <button type="button"
class="route-priority-option @(CriterioPrioritarioAndarActivo ? "route-priority-option-active" : "")" class="route-priority-option @(CriterioPrioritarioAndarActivo ? "route-priority-option-active" : "")"
@@ -1042,7 +1062,11 @@
<div class="history-panel-body"> <div class="history-panel-body">
<div class="history-section"> <div class="history-section">
<label class="history-section-label">Personal:</label> <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> <div class="history-empty">Sin historial.</div>
} }

View File

@@ -2416,3 +2416,785 @@ html, body {
line-height: 1; line-height: 1;
white-space: nowrap; white-space: nowrap;
} }
/* Elementos alternativos para la experiencia movil. */
.mobile-only,
.mobile-action-label,
.mobile-action-icon {
display: none !important;
}
@media (max-width: 780px) {
:root {
--mobile-header-height: 58px;
--mobile-nav-height: 64px;
}
html,
body {
width: 100%;
max-width: 100%;
overscroll-behavior: none;
}
.full-page {
width: 100%;
height: 100dvh;
min-height: 100dvh;
}
.desktop-only,
.desktop-action-label {
display: none !important;
}
.mobile-only {
display: initial !important;
}
.rutas-header-linea {
box-sizing: border-box;
flex: 0 0 var(--mobile-header-height);
width: 100%;
height: var(--mobile-header-height);
grid-template-columns: minmax(0, 1fr);
gap: 0;
padding: 7px 12px;
border-color: rgba(212, 212, 216, .16);
background: rgba(24, 24, 27, .98);
}
.header-left {
width: 100%;
overflow: hidden;
}
.header-left .titulo-app,
.header-left small {
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rutas-header h3 {
font-size: .98rem;
line-height: 1.2;
}
.rutas-header small {
margin-top: 2px;
font-size: .68rem;
line-height: 1.2;
}
.titulo-app {
gap: .35rem;
}
.app-version-inline {
font-size: .6rem;
}
.header-center {
display: none !important;
}
.header-right {
position: fixed;
z-index: 2700;
right: 0;
bottom: 0;
left: 0;
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
align-items: start;
gap: 2px;
width: 100%;
height: calc(var(--mobile-nav-height) + env(safe-area-inset-bottom));
padding: 5px 5px env(safe-area-inset-bottom);
border-top: 1px solid rgba(212, 212, 216, .2);
background: rgba(24, 24, 27, .98);
box-shadow: 0 -8px 24px rgba(0, 0, 0, .28);
white-space: normal;
}
.header-right > .pill-live:not(.pill-clickable) {
display: none !important;
}
.header-right .mobile-nav-action,
.header-right .btn-ghost.mobile-nav-action,
.header-right .btn-icon.mobile-nav-action {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
box-sizing: border-box;
width: 100%;
min-width: 0;
height: 54px;
min-height: 54px;
margin: 0;
padding: 3px 2px;
border: 0;
border-radius: 6px;
background: transparent;
color: #e4e4e7;
line-height: 1;
}
.header-right .mobile-nav-action:hover,
.header-right .mobile-nav-action:focus-visible {
outline: none;
background: rgba(63, 63, 70, .82);
color: #fff;
}
.mobile-action-label {
display: block !important;
max-width: 100%;
overflow: hidden;
color: #d4d4d8;
font-size: .62rem;
font-weight: 650;
line-height: 1.05;
text-overflow: ellipsis;
white-space: nowrap;
}
.mobile-action-icon {
display: block !important;
}
.header-right .search-icon,
.header-right .history-open-icon,
.header-right .nav-action-icon,
.header-right .mobile-action-icon,
.header-right .pill-dot {
flex: 0 0 auto;
font-size: 1.08rem;
line-height: 1;
}
.header-right .pill-dot {
width: 10px;
height: 10px;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .08);
}
.header-right .pill-ok .mobile-action-label {
color: #86efac;
}
.header-right .pill-bad .mobile-action-label {
color: #fca5a5;
}
.map-container {
min-height: 0;
}
.coords-bar,
.map-container .leaflet-control-zoom {
display: none !important;
}
.map-container .leaflet-bottom {
bottom: calc(var(--mobile-nav-height) + env(safe-area-inset-bottom) + 2px);
}
.map-container .leaflet-control-attribution {
max-width: calc(100vw - 10px);
overflow: hidden;
font-size: 9px;
text-overflow: ellipsis;
white-space: nowrap;
}
.gm-search-panel {
top: 8px;
left: 8px;
box-sizing: border-box;
width: calc(100% - 16px);
max-width: none;
padding: 9px;
transform: none;
border: 1px solid rgba(212, 212, 216, .22);
border-radius: 8px;
background: rgba(24, 24, 27, .97);
}
.gm-search-row {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 8px;
}
.gm-search-inputs {
display: grid;
grid-template-areas:
"origin swap"
"destination swap";
grid-template-columns: minmax(0, 1fr) 44px;
gap: 6px;
width: 100%;
}
.gm-input-group:first-child {
grid-area: origin;
}
.gm-input-group:last-child {
grid-area: destination;
}
.gm-input-group label {
margin: 0 0 2px 3px;
font-size: .68rem;
}
.gm-input-group input {
box-sizing: border-box;
min-height: 44px;
padding: 8px 10px;
border-radius: 6px;
font-size: 16px;
}
.gm-swap-btn {
grid-area: swap;
align-self: end;
width: 44px;
height: 44px;
padding: 0;
border-radius: 6px;
transform: none;
font-size: 1rem;
}
.gm-search-actions {
display: grid;
grid-template-columns: minmax(0, 1fr) 44px;
gap: 6px;
}
.gm-search-button,
.gm-search-hide-btn {
box-sizing: border-box;
min-height: 44px;
border-radius: 6px;
font-size: .82rem;
}
.gm-search-hide-btn {
width: 44px;
min-width: 44px;
padding: 0;
}
.gm-suggestions {
max-height: 38dvh;
border-radius: 6px;
}
.gm-suggestion {
min-height: 44px;
}
.draggable-popup-handle {
cursor: default;
}
.popup-stack-left {
position: fixed;
z-index: 2500;
top: calc(var(--mobile-header-height) + 8px);
right: 8px;
left: 8px;
width: auto;
max-width: none;
max-height: 42dvh;
overflow-y: auto;
gap: 6px;
}
.full-page:has(.gm-search-panel) .popup-stack-left {
top: calc(var(--mobile-header-height) + 168px);
max-height: 28dvh;
}
.full-page:has(.route-panel-overlay:not(.route-panel-overlay--mobile-collapsed)) .popup-stack-left {
max-height: 18dvh;
}
.popup-stack-left .popup-parada,
.popup-stack-left .popup-itinerario,
.popup-stack-left .popup-inspector,
.popup-stack-left .popup-parada.draggable-popup--free {
position: relative !important;
top: auto !important;
right: auto !important;
bottom: auto !important;
left: auto !important;
box-sizing: border-box;
width: 100%;
max-width: none;
margin: 0 !important;
padding: 12px 48px 10px 12px;
transform: none !important;
border-radius: 8px;
}
.popup-parada-cerrar,
.config-close-btn {
display: grid;
place-items: center;
width: 44px;
height: 44px;
padding: 0;
font-size: 1.25rem;
}
.popup-parada-cerrar {
top: 0;
right: 0;
}
.route-panel-overlay,
.route-panel-overlay.draggable-popup--free {
position: fixed !important;
z-index: 2600;
top: auto !important;
right: 0 !important;
bottom: calc(var(--mobile-nav-height) + env(safe-area-inset-bottom)) !important;
left: 0 !important;
box-sizing: border-box;
width: 100% !important;
max-width: none;
height: auto;
max-height: min(62dvh, 560px);
margin: 0 !important;
transform: none !important;
}
.route-panel {
box-sizing: border-box;
width: 100%;
height: auto;
max-height: min(62dvh, 560px);
padding: 10px 10px 8px;
border: 1px solid rgba(212, 212, 216, .22);
border-bottom: 0;
border-radius: 8px 8px 0 0;
background: rgba(24, 24, 27, .98);
}
.route-panel-header {
display: grid;
grid-template-columns: minmax(0, 1fr) 44px;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
.route-panel-title {
grid-column: 1;
grid-row: 1;
font-size: .9rem;
}
.route-mobile-collapse {
grid-column: 2;
grid-row: 1;
display: grid !important;
place-items: center;
width: 44px;
height: 44px;
padding: 0;
border: 1px solid rgba(161, 161, 170, .45);
border-radius: 6px;
background: rgba(39, 39, 42, .92);
color: #f4f4f5;
font-size: 1.15rem;
}
.route-priority-switch {
grid-column: 1 / -1;
grid-row: 2;
width: 100%;
box-sizing: border-box;
border-radius: 7px;
}
.route-priority-option {
min-width: 0;
height: 40px;
font-size: .73rem;
}
.route-visual-options {
justify-content: flex-start;
gap: 6px 10px;
margin: 0 0 8px;
}
.route-elevation-switch {
min-height: 44px;
font-size: .72rem;
}
.route-elevation-threshold {
min-height: 44px;
font-size: .68rem;
}
.route-elevation-threshold input {
width: 48px;
height: 40px;
font-size: .78rem;
}
.route-alt-list {
gap: 6px;
margin-bottom: 6px;
}
.route-alt-card {
min-height: 56px;
padding: 8px 9px;
border-radius: 8px;
font-size: .8rem;
}
.route-alt-main {
flex-wrap: wrap;
row-gap: 2px;
}
.route-alt-sub,
.route-alt-walk {
white-space: normal;
}
.route-panel-body {
min-height: 0;
padding-right: 3px;
overscroll-behavior: contain;
}
.route-main-text {
font-size: .84rem;
line-height: 1.45;
}
.chips-row {
gap: 5px;
}
.chip {
min-height: 32px;
border-radius: 6px;
}
.lista-tramos-ruta {
font-size: .79rem;
}
.lista-tramos-ruta li {
gap: 7px;
margin-bottom: 8px;
}
.lista-tramos-ruta li .detalle-tramo-ruta-contenido {
display: grid !important;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start !important;
gap: 6px !important;
}
.detalle-tramo-ruta-contenido .btn-ghost {
min-width: 58px;
min-height: 42px;
padding: 6px 8px !important;
border-radius: 6px;
}
.route-panel-overlay--mobile-collapsed,
.route-panel-overlay--mobile-collapsed .route-panel {
max-height: none;
}
.route-panel-overlay--mobile-collapsed .route-panel-header {
margin-bottom: 0;
}
.route-panel-overlay--mobile-collapsed .route-priority-switch,
.route-panel-overlay--mobile-collapsed .route-visual-options,
.route-panel-overlay--mobile-collapsed .route-alt-list,
.route-panel-overlay--mobile-collapsed .route-panel-divider,
.route-panel-overlay--mobile-collapsed .route-panel-body {
display: none !important;
}
.history-panel,
.history-panel.draggable-popup--free {
position: fixed !important;
z-index: 2800;
top: 8px !important;
right: 8px !important;
bottom: calc(var(--mobile-nav-height) + env(safe-area-inset-bottom) + 8px) !important;
left: 8px !important;
box-sizing: border-box;
display: flex;
flex-direction: column;
width: auto;
max-width: none;
height: auto;
max-height: none;
margin: 0 !important;
padding: 12px;
transform: none !important;
border-radius: 8px;
background: rgba(24, 24, 27, .98);
}
.history-panel-header {
flex: 0 0 auto;
min-height: 44px;
margin-bottom: 8px;
}
.history-panel-header h5 {
font-size: .88rem;
}
.history-panel-body {
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
}
.history-list {
max-height: 29dvh;
gap: 6px;
}
.history-item-main {
min-height: 48px;
padding: 8px 9px;
border-radius: 7px;
font-size: .76rem;
}
.history-item-action,
.history-item-delete {
flex-basis: 44px;
width: 44px;
min-height: 44px;
border-radius: 7px;
}
.config-panel,
.config-panel.draggable-popup--free {
position: fixed !important;
z-index: 2800;
top: 8px !important;
right: 8px !important;
bottom: calc(var(--mobile-nav-height) + env(safe-area-inset-bottom) + 8px) !important;
left: 8px !important;
box-sizing: border-box;
width: auto;
max-width: none;
height: auto;
max-height: none;
margin: 0 !important;
padding: 12px;
transform: none !important;
border-radius: 8px;
background: rgba(24, 24, 27, .98);
overscroll-behavior: contain;
}
.config-panel-header {
position: sticky;
z-index: 4;
top: -12px;
min-height: 48px;
margin: -12px -12px 10px;
padding: 8px 8px 5px 12px;
border-bottom: 1px solid rgba(212, 212, 216, .16);
background: rgba(24, 24, 27, .99);
}
.config-panel h5 {
margin: 0;
font-size: .86rem;
}
.config-row {
grid-template-columns: minmax(0, 1fr) minmax(132px, 44%);
gap: 8px;
margin-bottom: 10px;
}
.config-row label {
line-height: 1.25;
}
.config-panel .config-input {
min-height: 42px;
font-size: 16px;
}
.config-row > .config-input,
.config-row > .num-inline,
.config-row > .config-dual-inline,
.config-row > .config-dual-labeled {
width: 100% !important;
}
.config-dual-labeled__field span,
.config-equilibrado-help {
font-size: .64rem;
}
.line-filter__toggle {
min-height: 36px;
}
.line-filter__toggle input[type="checkbox"] {
flex: 0 0 20px;
width: 20px;
height: 20px;
}
.line-filter__range {
grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);
gap: 6px;
}
.line-filter__range--fecha {
grid-template-columns: minmax(0, 1.15fr) minmax(0, .8fr) auto !important;
}
.line-filter__dropdown-panel {
position: static;
margin-top: 6px;
box-shadow: none;
}
.line-filter__list {
max-height: 28dvh;
}
.line-option {
min-height: 40px;
}
.modelos-horarios__toggle,
.modelos-horarios__button,
.config-mini-button,
.config-actions-row .btn-ghost,
.btn-ghost--tiny {
min-height: 44px;
border-radius: 6px;
}
.custom-modal-backdrop {
padding: 8px;
}
.custom-modal-card,
.custom-modal-card.modelos-horarios-modal {
box-sizing: border-box;
width: 100%;
max-height: calc(100dvh - 16px);
padding: 12px;
overflow-y: auto;
border-radius: 8px;
}
.custom-modal-actions {
display: grid;
grid-template-columns: 1fr 1fr;
}
.custom-modal-actions .btn-ghost {
min-width: 0;
min-height: 44px;
border-radius: 6px;
}
.modelos-horarios-modal__header {
min-height: 44px;
}
.modelos-horarios__actions {
gap: 6px;
}
.popup-elev-topright,
.popup-elev-topright.draggable-popup--free {
position: fixed !important;
z-index: 2850;
top: auto !important;
right: 8px !important;
bottom: calc(var(--mobile-nav-height) + env(safe-area-inset-bottom) + 8px) !important;
left: 8px !important;
box-sizing: border-box;
width: auto;
max-width: none;
margin: 0 !important;
padding: 10px;
transform: none !important;
border-radius: 8px;
}
.popup-elev-topright .popup-elev-title {
max-width: calc(100vw - 150px);
}
.popup-elev-actions .btn-ghost {
display: grid;
place-items: center;
width: 44px;
height: 44px;
padding: 0;
border-radius: 6px;
}
.popup-elev-table-wrap {
width: 100%;
max-width: 100%;
max-height: min(56dvh, 420px);
}
.popup-elev-table {
font-size: .62rem !important;
}
.popup-elev-table th,
.popup-elev-table td {
padding-right: 2px !important;
padding-left: 2px !important;
}
.popup-elev-table col:nth-child(1) { width: 26px !important; }
.popup-elev-table col:nth-child(2) { width: 44px !important; }
.popup-elev-table col:nth-child(3) { width: 39px !important; }
.popup-elev-table col:nth-child(4) { width: 52px !important; }
.popup-elev-table col:nth-child(5) { width: 56px !important; }
.popup-elev-table col:nth-child(6) { width: 46px !important; }
.elev-bubble {
right: 8px;
bottom: calc(var(--mobile-nav-height) + env(safe-area-inset-bottom) + 8px);
left: 8px;
justify-content: center;
min-height: 44px;
border-radius: 7px;
}
}

View File

@@ -225,7 +225,8 @@ window.rt.drawRoute = function (id, coords, options) {
color: segmentColor, color: segmentColor,
weight, weight,
opacity, opacity,
lineCap: lineCap || "round" lineCap: lineCap || "round",
bubblingMouseEvents: false
}; };
if (dashArray) segmentOpts.dashArray = dashArray; if (dashArray) segmentOpts.dashArray = dashArray;
@@ -245,6 +246,11 @@ window.rt.drawRoute = function (id, coords, options) {
}; };
segmentLine.on("mouseover", updateTooltipAtCursor); segmentLine.on("mouseover", updateTooltipAtCursor);
segmentLine.on("mousemove", updateTooltipAtCursor); segmentLine.on("mousemove", updateTooltipAtCursor);
segmentLine.on("click", function (e) {
if (!window.rt.isMobileLayout || !window.rt.isMobileLayout()) return;
updateTooltipAtCursor(e);
try { segmentLine.openTooltip(e && e.latlng); } catch { }
});
} }
segmentLine.addTo(group); segmentLine.addTo(group);
@@ -425,7 +431,8 @@ window.rt.drawElevPoints = function (id, points, options) {
fillOpacity: fillOpacity, fillOpacity: fillOpacity,
color: pointColor, color: pointColor,
fillColor: pointColor, fillColor: pointColor,
className: className className: className,
bubblingMouseEvents: false
}); });
if (showLabels && every > 0 && (i % every === 0)) { if (showLabels && every > 0 && (i % every === 0)) {
@@ -470,6 +477,10 @@ window.rt.drawElevPoints = function (id, points, options) {
opacity: 0.92, opacity: 0.92,
className: "rt-elev-tooltip" className: "rt-elev-tooltip"
}); });
m.on("click", function (e) {
if (!window.rt.isMobileLayout || !window.rt.isMobileLayout()) return;
try { m.openTooltip(e && e.latlng); } catch { }
});
} }
m.addTo(group); m.addTo(group);
@@ -541,6 +552,107 @@ window.rt.blockLeafletDoubleClick = function () {
}, true); // CAPTURING: lo pillamos antes que Leaflet }, true); // CAPTURING: lo pillamos antes que Leaflet
}; };
window.rt.isMobileLayout = function () {
return !!(window.matchMedia && window.matchMedia("(max-width: 780px)").matches);
};
window.rt.initMobileMapControls = function () {
if (window.rt._mobileMapControlsReady) return;
window.rt._mobileMapControlsReady = true;
const holdMilliseconds = 620;
const movementTolerance = 12;
let holdTimer = null;
let press = null;
let suppressClickUntil = 0;
const cancelHold = function () {
if (holdTimer !== null) {
window.clearTimeout(holdTimer);
holdTimer = null;
}
press = null;
};
document.addEventListener("pointerdown", function (event) {
if (!window.rt.isMobileLayout())
return;
if (event.isPrimary === false) {
cancelHold();
return;
}
const mapContainer = event.target && event.target.closest
? event.target.closest(".leaflet-container")
: null;
if (!mapContainer) return;
if (event.target.closest(".leaflet-control, .leaflet-marker-icon, .leaflet-popup"))
return;
cancelHold();
press = {
pointerId: event.pointerId,
clientX: event.clientX,
clientY: event.clientY,
mapContainer
};
holdTimer = window.setTimeout(function () {
const currentPress = press;
holdTimer = null;
if (!currentPress || !currentPress.mapContainer.isConnected)
return;
const map = window.__rtLeafletMap;
if (!map || !window.L || !window.rt._dotNetHelper)
return;
const rect = currentPress.mapContainer.getBoundingClientRect();
const point = window.L.point(
currentPress.clientX - rect.left,
currentPress.clientY - rect.top);
const latLng = map.containerPointToLatLng(point);
suppressClickUntil = Date.now() + 900;
try { navigator.vibrate?.(18); } catch { }
window.rt._dotNetHelper.invokeMethodAsync("OnMobileMapLongPress", latLng.lat, latLng.lng);
press = null;
}, holdMilliseconds);
}, true);
document.addEventListener("pointermove", function (event) {
if (!press || event.pointerId !== press.pointerId) return;
if (Math.abs(event.clientX - press.clientX) > movementTolerance
|| Math.abs(event.clientY - press.clientY) > movementTolerance) {
cancelHold();
}
}, true);
document.addEventListener("pointerup", cancelHold, true);
document.addEventListener("pointercancel", cancelHold, true);
document.addEventListener("click", function (event) {
if (Date.now() >= suppressClickUntil) return;
const isOnMap = event.target && event.target.closest
? event.target.closest(".leaflet-container")
: null;
if (!isOnMap) return;
event.preventDefault();
event.stopPropagation();
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
}, true);
document.addEventListener("contextmenu", function (event) {
if (!window.rt.isMobileLayout()) return;
const isOnMap = event.target && event.target.closest
? event.target.closest(".leaflet-container")
: null;
if (isOnMap) event.preventDefault();
}, true);
};
@@ -912,7 +1024,10 @@ window.rt.setEndpointMarker = function (kind, lat, lon, options) {
const color = (options && options.color) || "#22c55e"; const color = (options && options.color) || "#22c55e";
const title = (options && options.title) || ""; const title = (options && options.title) || "";
const cssKind = kind === "destination" ? "destination" : "origin"; const cssKind = kind === "destination" ? "destination" : "origin";
const size = (options && options.size) || 18; const requestedSize = (options && options.size) || 18;
const size = window.rt.isMobileLayout && window.rt.isMobileLayout()
? Math.max(24, requestedSize)
: requestedSize;
const icon = window.L.divIcon({ const icon = window.L.divIcon({
className: "rt-endpoint-icon", className: "rt-endpoint-icon",
@@ -1118,6 +1233,8 @@ window.rt.initDraggablePopups = function () {
bringPopupToFront(popup); bringPopupToFront(popup);
if (window.rt.isMobileLayout && window.rt.isMobileLayout()) return;
if (event.button !== 0) return; if (event.button !== 0) return;
if (event.target.closest("button,a,input,textarea,select,label")) return; if (event.target.closest("button,a,input,textarea,select,label")) return;