- historial arreglado

- solucion 1/2
- quitar segundo score y colocar el score bien calculado
- score equilibrado a
- no usar pendientes
- flechas arregaldas con el zoom
- intinerario permitidos hasta el 99
- añadir peso a los transbordos
factor equilibrado a 6
visualizar pendientes en la confi avanzada
cambiar pisos por metros
This commit is contained in:
2026-08-03 12:58:55 +02:00
parent 6145fb635a
commit e9d77ebb38
13 changed files with 1036 additions and 543 deletions

View File

@@ -50,9 +50,10 @@ public partial class PlanificadorRutas : ComponentBase
private const double FactorPenalizacionDistanciaPiePorDefecto = 1.35;
private const double ParametroPenalizacionDistanciaPieMetrosPorDefecto = 200.0;
private const double CoeficienteEsfuerzoAlturaPorDefecto = 8.2;
private const double FactorEquilibradoAndarBusPorDefecto = 6.0;
private const double UmbralPendienteSubidaRutaPorDefecto = 2.0;
private const double UmbralPendienteBajadaRutaPorDefecto = 5.0;
private const double UmbralPisosPendienteRutaPorDefecto = 2.0;
private const double UmbralAlturaAlertaPendienteRutaMetrosPorDefecto = 6.0;
private const double MetrosPorPiso = 3.0;
private double _velocidadPieKmH = 3.6;
@@ -60,12 +61,12 @@ public partial class PlanificadorRutas : ComponentBase
private double _parametroPenalizacionDistanciaPieMetros = ParametroPenalizacionDistanciaPieMetrosPorDefecto;
private double _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
private double _pesoTransbordoRankingMinutos = 4.0;
private double _factorEquilibradoAndarBus = 12.0;
private double _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
private string _factorPenalizacionDistanciaPieTextoEdicion = "1.35";
private string _parametroPenalizacionDistanciaPieTextoEdicion = "200";
private string _coeficienteEsfuerzoAlturaTextoEdicion = "8.20";
private string _pesoTransbordoRankingTextoEdicion = "4,0";
private string _factorEquilibradoAndarBusTextoEdicion = "12";
private string _factorEquilibradoAndarBusTextoEdicion = "6";
private double _velocidadBusKmH = 16.0;
private int _minutosTransbordo = 5;
private int _margenTransbordoSegundos = 120;
@@ -275,7 +276,11 @@ public partial class PlanificadorRutas : ComponentBase
{
SincronizarTextosPenalizacionPie();
await PersistirConfiguracionAsync();
await RecalcularRankingTrasCambioEsfuerzoPendienteAsync();
if (_mostrarElevacionesRuta)
await RecalcularRankingTrasCambioEsfuerzoPendienteAsync();
else
StateHasChanged();
}
/// <summary>
@@ -285,6 +290,7 @@ public partial class PlanificadorRutas : ComponentBase
{
SincronizarTextosPenalizacionPie();
await PersistirConfiguracionAsync();
await RecalcularRutaEquilibradaTrasCambioFactorAsync();
}
/// <summary>
@@ -364,14 +370,14 @@ public partial class PlanificadorRutas : ComponentBase
private bool _panelRutasMovilContraido = false;
private double _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
private double _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
private double _umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto;
private double _umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
private double _alturaPendientePositivaSignificativaRutaMetros = 0.0;
private double _distanciaPendientePositivaSignificativaRutaMetros = 0.0;
private bool MostrarAvisoPendientesPositivasSignificativas =>
private bool MostrarAlertaPendientesPositivasSignificativas =>
_mostrarElevacionesRuta &&
_alturaPendientePositivaSignificativaRutaMetros >
(NormalizarUmbralPisosPendienteRuta(_umbralPisosPendienteRuta) * MetrosPorPiso);
NormalizarUmbralAlturaAlertaPendienteRutaMetros(_umbralAlturaAlertaPendienteRutaMetros);
private readonly Dictionary<string, List<Parada>> _paradasItinerarioVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, List<Parada>> _paradasRutaVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
@@ -1351,15 +1357,18 @@ public partial class PlanificadorRutas : ComponentBase
}
/// <summary>
/// Construye los parametros actuales que afectan a la penalizacion peatonal.
/// Construye los parametros actuales que afectan al ranking de rutas.
/// </summary>
private ParametrosPlanificadorRuta CrearParametrosPenalizacionActuales()
{
return new ParametrosPlanificadorRuta
{
VelocidadPieKmH = _velocidadPieKmH,
VelocidadBusKmH = _velocidadBusKmH,
FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie,
ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros
ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros,
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus
};
}
@@ -1386,7 +1395,8 @@ public partial class PlanificadorRutas : ComponentBase
var distanciaCorregidaBase = UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaTotalMetros(
distanciasPiePorTramo,
parametros);
var esfuerzoExtra = double.IsFinite(alternativa.EsfuerzoExtraPendienteMetros)
var esfuerzoExtra = _mostrarElevacionesRuta &&
double.IsFinite(alternativa.EsfuerzoExtraPendienteMetros)
? Math.Max(0, alternativa.EsfuerzoExtraPendienteMetros)
: 0;
@@ -1416,7 +1426,7 @@ public partial class PlanificadorRutas : ComponentBase
var distanciaAndando = CalcularDistanciaCorregidaAlternativa(alternativa);
var factor = double.IsFinite(_factorEquilibradoAndarBus)
? Math.Max(0, _factorEquilibradoAndarBus)
: 12.0;
: FactorEquilibradoAndarBusPorDefecto;
var distanciaEquilibrada = Math.Max(0, distanciaAndando) * factor;
return double.IsFinite(distanciaEquilibrada)
@@ -1431,7 +1441,10 @@ public partial class PlanificadorRutas : ComponentBase
var distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus);
var distanciaEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(alternativa);
var score = distanciaBus + distanciaEquilibradaAndando;
var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros(
alternativa.NumeroTransbordosReales,
CrearParametrosPenalizacionActuales());
var score = distanciaBus + distanciaEquilibradaAndando + penalizacionTransbordos;
return double.IsFinite(score) && score >= 0
? score
: double.PositiveInfinity;
@@ -1443,7 +1456,7 @@ public partial class PlanificadorRutas : ComponentBase
if (!double.IsFinite(score))
return string.Empty;
return $"Score {FormatearDistanciaTarjeta(score)}";
return FormatearDistanciaTarjeta(score);
}
private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync()

View File

@@ -12,6 +12,7 @@ public partial class PlanificadorRutas
private const string ClaveRespaldoHistorialLocal = "rutasdbus.planificador.routeHistory.recovery.v1";
private const int MaxItemsHistorialLocal = 100;
private const int IntervaloRefrescoHistorialCompartidoSegundos = 3;
private const int TiempoMaxOperacionHistorialLocalSegundos = 8;
private static readonly JsonSerializerOptions OpcionesLecturaHistorialLocal = new()
{
PropertyNameCaseInsensitive = true,
@@ -36,6 +37,27 @@ public partial class PlanificadorRutas
private bool _historialLocalCargado = false;
private bool _historialLocalCargando = false;
private int _versionAperturaHistorial;
private int _numeroIntentoCargaHistorialLocal;
private string _resumenErrorHistorialLocal = string.Empty;
private string _detalleErrorHistorialLocal = string.Empty;
private sealed class ResultadoStorageNavegador
{
[JsonPropertyName("ok")]
public bool Ok { get; set; }
[JsonPropertyName("found")]
public bool Found { get; set; }
[JsonPropertyName("value")]
public string? Value { get; set; }
[JsonPropertyName("errorName")]
public string? ErrorName { get; set; }
[JsonPropertyName("errorMessage")]
public string? ErrorMessage { get; set; }
}
private async Task ToggleHistorialPanel()
{
@@ -80,8 +102,9 @@ public partial class PlanificadorRutas
{
return await tareaCarga;
}
catch
catch (Exception ex)
{
RegistrarErrorHistorialLocal("coordinacion de la carga", ex);
return false;
}
finally
@@ -93,62 +116,159 @@ public partial class PlanificadorRutas
private async Task<bool> CargarHistorialLocalDesdeNavegadorAsync()
{
string? json;
var numeroIntento = ++_numeroIntentoCargaHistorialLocal;
ResultadoStorageNavegador? resultadoLectura;
try
{
json = await JS.InvokeAsync<string?>("rt.loadLocalValue", ClaveHistorialLocal);
using var cts = new CancellationTokenSource(
TimeSpan.FromSeconds(TiempoMaxOperacionHistorialLocalSegundos));
resultadoLectura = await JS.InvokeAsync<ResultadoStorageNavegador?>(
"rt.loadLocalValueResult",
cts.Token,
ClaveHistorialLocal);
}
catch (JSException)
catch (OperationCanceledException ex)
{
RegistrarErrorHistorialLocal(
"lectura de localStorage",
new TimeoutException(
$"La lectura no respondio en {TiempoMaxOperacionHistorialLocalSegundos} segundos.",
ex),
$"Intento de carga: {numeroIntento}.");
return false;
}
catch (Exception)
catch (Exception ex)
{
RegistrarErrorHistorialLocal(
"lectura de localStorage",
ex,
$"Intento de carga: {numeroIntento}.");
return false;
}
if (string.IsNullOrWhiteSpace(json))
if (resultadoLectura is null)
{
RegistrarErrorHistorialLocal(
"lectura de localStorage",
"RespuestaVacia",
"JavaScript no devolvio ningun resultado de lectura.",
$"Intento de carga: {numeroIntento}.");
return false;
}
if (!resultadoLectura.Ok)
{
RegistrarErrorHistorialLocal(
"lectura de localStorage",
resultadoLectura.ErrorName ?? "StorageError",
resultadoLectura.ErrorMessage ?? "El navegador no ha indicado el motivo del error.",
$"Intento de carga: {numeroIntento}.");
return false;
}
var json = resultadoLectura.Value;
if (!resultadoLectura.Found || string.IsNullOrWhiteSpace(json))
{
FinalizarCargaHistorialLocal(Array.Empty<RegistroHistorialRuta>());
LimpiarErrorHistorialLocal();
return true;
}
try
{
var items = DeserializarHistorialLocal(json, out var registrosOmitidos);
var items = DeserializarHistorialLocal(
json,
out var registrosOmitidos,
out var primerErrorRegistro);
FinalizarCargaHistorialLocal(items);
if (registrosOmitidos > 0)
{
await GuardarHistorialLocalRecuperadoAsync(json);
var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(json);
var detalleFalloRecuperacion = recuperacionGuardada
? null
: _detalleErrorHistorialLocal;
if (!recuperacionGuardada)
_historialLocalCargado = false;
RegistrarErrorHistorialLocal(
"validacion de registros",
"RegistrosDaniados",
$"Se omitieron {registrosOmitidos} registros que no se podian utilizar.",
$"Primer fallo: {primerErrorRegistro ?? "sin detalle"}. " +
$"Registros recuperados: {_historialLocal.Count}. " +
$"Tamano leido: {json.Length} caracteres. " +
$"Copia de recuperacion: {(recuperacionGuardada ? "guardada" : "no guardada")}." +
(string.IsNullOrWhiteSpace(detalleFalloRecuperacion)
? string.Empty
: $"{Environment.NewLine}Fallo al recuperar: {detalleFalloRecuperacion}"));
MostrarMensajeHistorial(
$"Se han recuperado {_historialLocal.Count} registros del historial; {registrosOmitidos} estaban dañados.",
esError: true);
}
else
{
LimpiarErrorHistorialLocal();
}
return true;
return _historialLocalCargado;
}
catch (Exception ex) when (ex is JsonException or NotSupportedException or InvalidOperationException)
catch (Exception ex) when (
ex is JsonException or
NotSupportedException or
InvalidOperationException or
ArgumentException)
{
FinalizarCargaHistorialLocal(Array.Empty<RegistroHistorialRuta>());
await GuardarHistorialLocalRecuperadoAsync(json);
var recuperacionGuardada = await GuardarHistorialLocalRecuperadoAsync(json);
var detalleFalloRecuperacion = recuperacionGuardada
? null
: _detalleErrorHistorialLocal;
if (!recuperacionGuardada)
_historialLocalCargado = false;
RegistrarErrorHistorialLocal(
"deserializacion del historial",
ex,
$"Tamano leido: {json.Length} caracteres. " +
$"Copia de recuperacion: {(recuperacionGuardada ? "guardada" : "no guardada")}." +
(string.IsNullOrWhiteSpace(detalleFalloRecuperacion)
? string.Empty
: $"{Environment.NewLine}Fallo al recuperar: {detalleFalloRecuperacion}"));
MostrarMensajeHistorial(
"El historial personal guardado estaba dañado. Se ha conservado una copia y se ha iniciado uno nuevo.",
recuperacionGuardada
? "El historial personal guardado estaba dañado. Se ha conservado una copia y se ha iniciado uno nuevo."
: "El historial personal esta dañado, pero no se ha modificado porque no se pudo guardar la copia. Reintenta la carga.",
esError: true);
return true;
return _historialLocalCargado;
}
catch (Exception ex)
{
_historialLocalCargado = false;
RegistrarErrorHistorialLocal(
"fallo inesperado al procesar el historial",
ex,
$"Tamano leido: {json.Length} caracteres. El contenido original no se ha modificado.");
MostrarMensajeHistorial(
"El codigo ha producido un error inesperado al cargar el historial. Consulta el diagnostico; los datos originales no se han modificado.",
esError: true);
return false;
}
}
private static IReadOnlyList<RegistroHistorialRuta> DeserializarHistorialLocal(
string json,
out int registrosOmitidos)
out int registrosOmitidos,
out string? primerErrorRegistro)
{
using var documento = JsonDocument.Parse(json);
if (documento.RootElement.ValueKind != JsonValueKind.Array)
throw new JsonException("El historial local no contiene una lista.");
registrosOmitidos = 0;
primerErrorRegistro = null;
var recuperados = new List<RegistroHistorialRuta>();
var idsVistos = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var elemento in documento.RootElement.EnumerateArray())
{
try
@@ -157,18 +277,27 @@ public partial class PlanificadorRutas
if (item is null)
{
registrosOmitidos++;
primerErrorRegistro ??= "El registro deserializado era nulo.";
continue;
}
if (string.IsNullOrWhiteSpace(item.Id))
if (string.IsNullOrWhiteSpace(item.Id) || !idsVistos.Add(item.Id))
{
item.Id = Guid.NewGuid().ToString("N");
idsVistos.Add(item.Id);
}
item.LineasPermitidas ??= new List<int>();
NormalizarRegistroHistorialLocal(item);
recuperados.Add(item);
}
catch (Exception ex) when (ex is JsonException or NotSupportedException or InvalidOperationException)
catch (Exception ex) when (
ex is JsonException or
NotSupportedException or
InvalidOperationException or
ArgumentException)
{
registrosOmitidos++;
primerErrorRegistro ??= $"{ex.GetType().Name}: {ex.Message}";
}
}
@@ -178,6 +307,33 @@ public partial class PlanificadorRutas
.ToList();
}
private static void NormalizarRegistroHistorialLocal(RegistroHistorialRuta item)
{
if (!CoordenadaHistorialValida(item.OrigenLatitud, item.OrigenLongitud))
throw new JsonException("Las coordenadas de origen no son validas.");
if (!CoordenadaHistorialValida(item.DestinoLatitud, item.DestinoLongitud))
throw new JsonException("Las coordenadas de destino no son validas.");
item.OrigenTexto = string.IsNullOrWhiteSpace(item.OrigenTexto)
? $"{item.OrigenLatitud.ToString("0.000000", CultureInfo.InvariantCulture)}, {item.OrigenLongitud.ToString("0.000000", CultureInfo.InvariantCulture)}"
: item.OrigenTexto.Trim();
item.DestinoTexto = string.IsNullOrWhiteSpace(item.DestinoTexto)
? $"{item.DestinoLatitud.ToString("0.000000", CultureInfo.InvariantCulture)}, {item.DestinoLongitud.ToString("0.000000", CultureInfo.InvariantCulture)}"
: item.DestinoTexto.Trim();
item.LineasPermitidas = NormalizarLineasHistorial(item.LineasPermitidas).ToList();
if (item.FechaCreacionUtc == default)
item.FechaCreacionUtc = item.FechaActualizacionUtc == default ? DateTime.UtcNow : item.FechaActualizacionUtc;
if (item.FechaActualizacionUtc == default)
item.FechaActualizacionUtc = item.FechaCreacionUtc;
}
private static bool CoordenadaHistorialValida(double latitud, double longitud)
=> double.IsFinite(latitud) &&
double.IsFinite(longitud) &&
latitud is >= -90 and <= 90 &&
longitud is >= -180 and <= 180;
private void FinalizarCargaHistorialLocal(IEnumerable<RegistroHistorialRuta> items)
{
_historialLocal.Clear();
@@ -187,17 +343,19 @@ public partial class PlanificadorRutas
_historialLocalCargado = true;
}
private async Task GuardarHistorialLocalRecuperadoAsync(string jsonOriginal)
private async Task<bool> 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)
{
}
if (!await GuardarValorHistorialLocalAsync(
ClaveRespaldoHistorialLocal,
jsonOriginal,
"guardado de la copia de recuperacion"))
return false;
var jsonRecuperado = JsonSerializer.Serialize(_historialLocal);
return await GuardarValorHistorialLocalAsync(
ClaveHistorialLocal,
jsonRecuperado,
"guardado del historial recuperado");
}
private async Task PersistirHistorialLocalAsync()
@@ -205,14 +363,143 @@ public partial class PlanificadorRutas
if (!_historialLocalCargado)
return;
var json = JsonSerializer.Serialize(_historialLocal);
if (!await GuardarValorHistorialLocalAsync(
ClaveHistorialLocal,
json,
"persistencia del historial"))
{
MostrarMensajeHistorial(
"No se ha podido guardar el historial personal. Consulta el diagnostico.",
esError: true);
}
}
private async Task<bool> GuardarValorHistorialLocalAsync(string clave, string valor, string fase)
{
try
{
var json = JsonSerializer.Serialize(_historialLocal);
await JS.InvokeVoidAsync("rt.saveLocalValue", ClaveHistorialLocal, json);
using var cts = new CancellationTokenSource(
TimeSpan.FromSeconds(TiempoMaxOperacionHistorialLocalSegundos));
var resultado = await JS.InvokeAsync<ResultadoStorageNavegador?>(
"rt.saveLocalValueResult",
cts.Token,
clave,
valor);
if (resultado?.Ok == true)
return true;
RegistrarErrorHistorialLocal(
fase,
resultado?.ErrorName ?? "RespuestaVacia",
resultado?.ErrorMessage ?? "JavaScript no devolvio un resultado valido al guardar.",
$"Tamano que se intentaba guardar: {valor.Length} caracteres.");
return false;
}
catch (JSException)
catch (OperationCanceledException ex)
{
RegistrarErrorHistorialLocal(
fase,
new TimeoutException(
$"La escritura no respondio en {TiempoMaxOperacionHistorialLocalSegundos} segundos.",
ex),
$"Tamano que se intentaba guardar: {valor.Length} caracteres.");
return false;
}
catch (Exception ex)
{
RegistrarErrorHistorialLocal(
fase,
ex,
$"Tamano que se intentaba guardar: {valor.Length} caracteres.");
return false;
}
}
private async Task ReintentarCargaHistorialLocalAsync()
{
if (_historialLocalCargando)
return;
_historialLocalCargado = false;
_tareaCargaHistorialLocal = null;
_historialLocalCargando = true;
StateHasChanged();
try
{
var cargado = await AsegurarHistorialLocalCargadoAsync();
MostrarMensajeHistorial(
cargado
? $"Historial personal cargado: {_historialLocal.Count} registro(s)."
: "El historial personal sigue sin poder cargarse. Consulta el diagnostico.",
esError: !cargado);
}
finally
{
_historialLocalCargando = false;
StateHasChanged();
}
}
private void RegistrarErrorHistorialLocal(
string fase,
Exception excepcion,
string? contexto = null)
{
var causa = excepcion.GetBaseException();
RegistrarErrorHistorialLocal(
fase,
causa.GetType().Name,
causa.Message,
CombinarDetalle(contexto, excepcion.ToString()));
}
private void RegistrarErrorHistorialLocal(
string fase,
string tipo,
string mensaje,
string? contexto = null)
{
var marcaTiempo = DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss", CultureInfo.CurrentCulture);
_resumenErrorHistorialLocal = $"{tipo}: {LimitarTextoDiagnostico(mensaje, 240)}";
_detalleErrorHistorialLocal = string.Join(
Environment.NewLine,
new[]
{
$"Fecha: {marcaTiempo}",
$"Fase: {fase}",
$"Tipo: {tipo}",
$"Mensaje: {mensaje}",
$"Estado: cargado={_historialLocalCargado}, cargando={_historialLocalCargando}, registrosEnMemoria={_historialLocal.Count}",
string.IsNullOrWhiteSpace(contexto) ? null : $"Contexto: {contexto}"
}.Where(linea => linea is not null));
Console.Error.WriteLine($"[Historial personal] {_detalleErrorHistorialLocal}");
}
private void LimpiarErrorHistorialLocal()
{
_resumenErrorHistorialLocal = string.Empty;
_detalleErrorHistorialLocal = string.Empty;
}
private static string? CombinarDetalle(string? contexto, string? detalle)
{
if (string.IsNullOrWhiteSpace(contexto))
return detalle;
if (string.IsNullOrWhiteSpace(detalle))
return contexto;
return $"{contexto}{Environment.NewLine}{detalle}";
}
private static string LimitarTextoDiagnostico(string texto, int maximo)
{
if (string.IsNullOrWhiteSpace(texto) || texto.Length <= maximo)
return texto;
return texto[..maximo] + "...";
}
private async Task CargarHistorialCompartidoAsync(bool mostrarError = true)
@@ -339,16 +626,29 @@ public partial class PlanificadorRutas
private async Task CargarHistorialLocalAsync(string id)
{
var item = _historialLocal.FirstOrDefault(x => x.Id == id);
if (item is null)
try
{
MostrarMensajeHistorial("No encuentro esa combinacion en tu historial personal.", esError: true);
return;
}
var item = _historialLocal.FirstOrDefault(x => x.Id == id);
if (item is null)
{
MostrarMensajeHistorial("No encuentro esa combinacion en tu historial personal.", esError: true);
return;
}
await AplicarRegistroHistorialAsync(item);
SeleccionarHistorialLocal(id);
MostrarMensajeHistorial("Combinacion personal cargada.");
await AplicarRegistroHistorialAsync(item);
SeleccionarHistorialLocal(id);
MostrarMensajeHistorial("Combinacion personal cargada.");
}
catch (Exception ex)
{
RegistrarErrorHistorialLocal(
"aplicacion de un registro personal",
ex,
$"Id del registro: {id}.");
MostrarMensajeHistorial(
"La combinacion personal ha producido un error. Consulta el diagnostico.",
esError: true);
}
}
private async Task CargarHistorialCompartidoAsync(string id)
@@ -429,33 +729,46 @@ public partial class PlanificadorRutas
private async Task EliminarHistorialLocalAsync(string id)
{
if (!await AsegurarHistorialLocalCargadoAsync())
return;
var indiceEliminado = _historialLocal.FindIndex(x => x.Id == id);
if (indiceEliminado < 0)
return;
var estabaSeleccionado = _historialLocalSeleccionadoId == id;
_historialLocal.RemoveAt(indiceEliminado);
await PersistirHistorialLocalAsync();
if (estabaSeleccionado)
try
{
if (_historialLocal.Count == 0)
{
_historialLocalSeleccionadoId = string.Empty;
}
else
{
var siguiente = _historialLocal[Math.Min(indiceEliminado, _historialLocal.Count - 1)];
await AplicarRegistroHistorialAsync(siguiente);
SeleccionarHistorialLocal(siguiente.Id);
}
}
if (!await AsegurarHistorialLocalCargadoAsync())
return;
MostrarMensajeHistorial("Combinacion eliminada del historial personal.");
var indiceEliminado = _historialLocal.FindIndex(x => x.Id == id);
if (indiceEliminado < 0)
return;
var estabaSeleccionado = _historialLocalSeleccionadoId == id;
_historialLocal.RemoveAt(indiceEliminado);
await PersistirHistorialLocalAsync();
if (estabaSeleccionado)
{
if (_historialLocal.Count == 0)
{
_historialLocalSeleccionadoId = string.Empty;
}
else
{
var siguiente = _historialLocal[Math.Min(indiceEliminado, _historialLocal.Count - 1)];
await AplicarRegistroHistorialAsync(siguiente);
SeleccionarHistorialLocal(siguiente.Id);
}
}
MostrarMensajeHistorial("Combinacion eliminada del historial personal.");
}
catch (Exception ex)
{
RegistrarErrorHistorialLocal(
"eliminacion de un registro personal",
ex,
$"Id del registro: {id}.");
MostrarMensajeHistorial(
"No se ha podido completar la eliminacion. Consulta el diagnostico.",
esError: true);
}
}
private void PedirConfirmacionEliminarHistorialCompartido(string id)
@@ -638,7 +951,7 @@ public partial class PlanificadorRutas
PreferenciaEtiquetaRuta = _preferenciaEtiquetaRuta,
FactorEquilibradoAndarBus = double.IsFinite(_factorEquilibradoAndarBus)
? Math.Max(0, _factorEquilibradoAndarBus)
: 12.0,
: FactorEquilibradoAndarBusPorDefecto,
ActivarFiltroLineas = _activarRangoLineas,
LineasPermitidas = ObtenerLineasPermitidasSeleccionadas().ToList(),
FechaCreacionUtc = DateTime.UtcNow,

View File

@@ -1204,7 +1204,7 @@ public partial class PlanificadorRutas : ComponentBase
private bool _elevPopupMinimizado = false;
private const string _versionApp = "(v-20260728a)";
private const string _versionApp = "(v-20260731a)";
private double? _paradaElevG;

View File

@@ -7,6 +7,7 @@ namespace RutasDBUS.Components.Pages;
public partial class PlanificadorRutas
{
private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1";
private const int VersionPersistenciaFactorEquilibrado = 2;
private bool _configuracionLocalCargada = false;
private bool _mostrarConfirmacionRestablecer = false;
@@ -36,8 +37,12 @@ public partial class PlanificadorRutas
public double? CoeficienteEsfuerzoAltura { get; set; }
public double? PesoTransbordoRankingMinutos { get; set; }
public double? FactorEquilibradoAndarBus { get; set; }
public int? VersionFactorEquilibrado { get; set; }
public bool? MostrarElevacionesRuta { get; set; }
public double? UmbralPendienteSubidaRuta { get; set; }
public double? UmbralPendienteBajadaRuta { get; set; }
public double? UmbralAlturaAlertaPendienteRutaMetros { get; set; }
// Compatibilidad con configuraciones guardadas cuando el umbral se expresaba en pisos.
public double? UmbralPisosPendienteRuta { get; set; }
public double VelocidadPieKmH { get; set; }
public double VelocidadBusKmH { get; set; }
@@ -139,9 +144,11 @@ public partial class PlanificadorRutas
CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura,
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
FactorEquilibradoAndarBus = _factorEquilibradoAndarBus,
VersionFactorEquilibrado = VersionPersistenciaFactorEquilibrado,
MostrarElevacionesRuta = _mostrarElevacionesRuta,
UmbralPendienteSubidaRuta = _umbralPendienteSubidaRuta,
UmbralPendienteBajadaRuta = _umbralPendienteBajadaRuta,
UmbralPisosPendienteRuta = _umbralPisosPendienteRuta,
UmbralAlturaAlertaPendienteRutaMetros = _umbralAlturaAlertaPendienteRutaMetros,
VelocidadPieKmH = _velocidadPieKmH,
VelocidadBusKmH = _velocidadBusKmH,
ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo,
@@ -206,7 +213,11 @@ public partial class PlanificadorRutas
if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo))
_pesoTransbordoRankingMinutos = Math.Abs(pesoTransbordo - 8.0) < 0.0001 ? 4.0 : Math.Max(0, pesoTransbordo);
if (config.FactorEquilibradoAndarBus is { } factorEquilibrado && double.IsFinite(factorEquilibrado))
_factorEquilibradoAndarBus = Math.Max(0, factorEquilibrado);
_factorEquilibradoAndarBus = (config.VersionFactorEquilibrado ?? 0) < VersionPersistenciaFactorEquilibrado &&
Math.Abs(factorEquilibrado - 12.0) < 0.0001
? FactorEquilibradoAndarBusPorDefecto
: Math.Max(0, factorEquilibrado);
_mostrarElevacionesRuta = config.MostrarElevacionesRuta ?? false;
if (config.UmbralPendienteSubidaRuta is { } umbralSubida && double.IsFinite(umbralSubida))
_umbralPendienteSubidaRuta = Math.Abs(umbralSubida - 3.0) < 0.0001
? UmbralPendienteSubidaRutaPorDefecto
@@ -217,8 +228,16 @@ public partial class PlanificadorRutas
Math.Abs(umbralBajada - 6.0) < 0.0001
? UmbralPendienteBajadaRutaPorDefecto
: NormalizarUmbralPendienteRuta(umbralBajada);
if (config.UmbralPisosPendienteRuta is { } umbralPisos && double.IsFinite(umbralPisos))
_umbralPisosPendienteRuta = NormalizarUmbralPisosPendienteRuta(umbralPisos);
if (config.UmbralAlturaAlertaPendienteRutaMetros is { } umbralAltura && double.IsFinite(umbralAltura))
{
_umbralAlturaAlertaPendienteRutaMetros =
NormalizarUmbralAlturaAlertaPendienteRutaMetros(umbralAltura);
}
else if (config.UmbralPisosPendienteRuta is { } umbralPisos && double.IsFinite(umbralPisos))
{
_umbralAlturaAlertaPendienteRutaMetros =
NormalizarUmbralAlturaAlertaPendienteRutaMetros(umbralPisos * MetrosPorPiso);
}
_velocidadPieKmH = Math.Clamp(config.VelocidadPieKmH, 0.5, 25.0);
_velocidadBusKmH = Math.Clamp(
Math.Abs(config.VelocidadBusKmH - 15.0) < 0.0001 ||
@@ -321,10 +340,11 @@ public partial class PlanificadorRutas
_parametroPenalizacionDistanciaPieMetros = ParametroPenalizacionDistanciaPieMetrosPorDefecto;
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
_pesoTransbordoRankingMinutos = 4.0;
_factorEquilibradoAndarBus = 12.0;
_factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
_mostrarElevacionesRuta = false;
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
_umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto;
_umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
_velocidadPieKmH = 3.6;
_velocidadBusKmH = 16.0;
_minutosTransbordo = 5;
@@ -371,10 +391,11 @@ public partial class PlanificadorRutas
_parametroPenalizacionDistanciaPieMetros = ParametroPenalizacionDistanciaPieMetrosPorDefecto;
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
_pesoTransbordoRankingMinutos = 4.0;
_factorEquilibradoAndarBus = 12.0;
_factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
_mostrarElevacionesRuta = false;
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
_umbralPisosPendienteRuta = UmbralPisosPendienteRutaPorDefecto;
_umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
_velocidadPieKmH = 3.6;
_velocidadBusKmH = 16.0;
_minutosTransbordo = 5;

View File

@@ -384,6 +384,12 @@ public partial class PlanificadorRutas : ComponentBase
IReadOnlyList<AlternativaRuta> alternativas,
CancellationToken ct)
{
foreach (var alternativa in alternativas)
alternativa.EsfuerzoExtraPendienteMetros = 0;
if (!_mostrarElevacionesRuta || alternativas.Count == 0)
return;
var lineasPorAlternativa = alternativas
.Select(alternativa => new
{

View File

@@ -81,16 +81,40 @@ public partial class PlanificadorRutas : ComponentBase
private async Task ToggleElevacionesRutaAsync(ChangeEventArgs e)
{
_mostrarElevacionesRuta = e.Value switch
var mostrarElevaciones = e.Value switch
{
bool valor => valor,
string texto when bool.TryParse(texto, out var valor) => valor,
_ => !_mostrarElevacionesRuta
};
await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta
if (_mostrarElevacionesRuta == mostrarElevaciones)
return;
_mostrarElevacionesRuta = mostrarElevaciones;
await PersistirConfiguracionAsync();
if (_opcionesRuta.Count == 0)
{
StateHasChanged();
return;
}
_pintandoElevacionesRuta = true;
_mensajeRuta = _mostrarElevacionesRuta
? "Aplicando pendientes..."
: "Representando ruta seleccionada...");
: "Recalculando sin pendientes...";
StateHasChanged();
await Task.Yield();
try
{
await RecalcularRankingTrasCambioEsfuerzoPendienteAsync(forzarRepintado: true);
}
finally
{
_pintandoElevacionesRuta = false;
StateHasChanged();
}
}
private async Task CambiarUmbralElevacionRutaAsync(ChangeEventArgs e, bool esSubida)
@@ -107,7 +131,7 @@ public partial class PlanificadorRutas : ComponentBase
await PersistirConfiguracionAsync();
if (esSubida)
if (esSubida && _mostrarElevacionesRuta)
{
await RecalcularRankingTrasCambioEsfuerzoPendienteAsync();
}
@@ -117,7 +141,7 @@ public partial class PlanificadorRutas : ComponentBase
StateHasChanged();
}
private async Task RecalcularRankingTrasCambioEsfuerzoPendienteAsync()
private async Task RecalcularRankingTrasCambioEsfuerzoPendienteAsync(bool forzarRepintado = false)
{
if (_opcionesRuta.Count == 0)
{
@@ -143,7 +167,7 @@ public partial class PlanificadorRutas : ComponentBase
!ReferenceEquals(opcionSeleccionadaAnterior, _opcionesRuta[nuevoIndice]);
_indiceAlternativaSeleccionada = nuevoIndice;
if (_mostrarElevacionesRuta || cambioSeleccion)
if (forzarRepintado || _mostrarElevacionesRuta || cambioSeleccion)
{
await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta
? "Aplicando pendientes..."
@@ -155,11 +179,11 @@ public partial class PlanificadorRutas : ComponentBase
}
}
private async Task CambiarUmbralPisosPendienteRutaAsync(ChangeEventArgs e)
private async Task CambiarUmbralAlturaAlertaPendienteRutaAsync(ChangeEventArgs e)
{
_umbralPisosPendienteRuta = ParseUmbralPisosPendienteRuta(
_umbralAlturaAlertaPendienteRutaMetros = ParseUmbralAlturaAlertaPendienteRutaMetros(
e.Value,
_umbralPisosPendienteRuta);
_umbralAlturaAlertaPendienteRutaMetros);
await PersistirConfiguracionAsync();
StateHasChanged();
@@ -210,22 +234,22 @@ public partial class PlanificadorRutas : ComponentBase
? Math.Clamp(Math.Abs(valor), 0.0, 100.0)
: UmbralPendienteSubidaRutaPorDefecto;
private static double ParseUmbralPisosPendienteRuta(object? valor, double actual)
private static double ParseUmbralAlturaAlertaPendienteRutaMetros(object? valor, double actual)
{
var texto = Convert.ToString(valor, CultureInfo.InvariantCulture)?.Replace(',', '.');
if (!double.TryParse(texto, NumberStyles.Float, CultureInfo.InvariantCulture, out var nuevo) ||
!double.IsFinite(nuevo))
{
return NormalizarUmbralPisosPendienteRuta(actual);
return NormalizarUmbralAlturaAlertaPendienteRutaMetros(actual);
}
return NormalizarUmbralPisosPendienteRuta(nuevo);
return NormalizarUmbralAlturaAlertaPendienteRutaMetros(nuevo);
}
private static double NormalizarUmbralPisosPendienteRuta(double valor)
private static double NormalizarUmbralAlturaAlertaPendienteRutaMetros(double valor)
=> double.IsFinite(valor)
? Math.Clamp(Math.Abs(valor), 0.0, 100.0)
: UmbralPisosPendienteRutaPorDefecto;
? Math.Clamp(Math.Abs(valor), 0.0, 1000.0)
: UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
private async Task DrawWalkingRouteJsAsync(
string id,

View File

@@ -613,88 +613,6 @@
</div>
</div>
<div class="route-visual-options">
<label class="route-elevation-switch @(_mostrarElevacionesRuta ? "route-elevation-switch--active" : "")"
title="Colorear tramos a pie segun pendiente">
<input type="checkbox"
checked="@_mostrarElevacionesRuta"
disabled="@(_pintandoElevacionesRuta || _opcionesRuta.Count == 0)"
@onchange="ToggleElevacionesRutaAsync" />
<span class="route-elevation-switch-track">
<span class="route-elevation-switch-thumb"></span>
</span>
<span class="route-elevation-switch-text">
Visualizar Pendientes
</span>
</label>
<div class="route-elevation-settings">
<button type="button"
class="route-elevation-settings-trigger @(_mostrarConfiguracionPendientesRuta ? "route-elevation-settings-trigger--active" : "")"
title="Configurar pendientes"
aria-label="Configurar pendientes"
aria-haspopup="dialog"
aria-expanded="@_mostrarConfiguracionPendientesRuta"
disabled="@_pintandoElevacionesRuta"
@onclick="() => _mostrarConfiguracionPendientesRuta = !_mostrarConfiguracionPendientesRuta">
<span aria-hidden="true">&#9881;</span>
</button>
@if (_mostrarConfiguracionPendientesRuta)
{
<div class="route-elevation-settings-popover"
role="dialog"
aria-label="Umbrales de pendientes"
@onclick:stopPropagation="true">
<div class="route-elevation-settings-header">
<span>Configurar Pendientes</span>
<button type="button"
class="route-elevation-settings-close"
title="Cerrar"
aria-label="Cerrar configuración de pendientes"
@onclick="() => _mostrarConfiguracionPendientesRuta = false">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="route-elevation-settings-fields">
<label class="route-elevation-setting-row">
<span>Subida</span>
<input type="text"
inputmode="decimal"
aria-label="Porcentaje de subida"
value="@_umbralPendienteSubidaRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, true))" />
<span>%</span>
</label>
<label class="route-elevation-setting-row">
<span>Bajada</span>
<input type="text"
inputmode="decimal"
aria-label="Porcentaje de bajada"
value="@_umbralPendienteBajadaRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, false))" />
<span>%</span>
</label>
<label class="route-elevation-setting-row"
title="Pisos acumulados para mostrar el aviso de pendientes positivas">
<span>Aviso</span>
<input type="text"
inputmode="decimal"
aria-label="Pisos para mostrar el aviso"
value="@_umbralPisosPendienteRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="CambiarUmbralPisosPendienteRutaAsync" />
<span>pisos</span>
</label>
</div>
</div>
}
</div>
</div>
@if (_opcionesRuta.Count > 0)
{
<div class="route-alt-list">
@@ -763,16 +681,13 @@
{
var alt = _opcionesRuta[_indiceAlternativaSeleccionada];
@if (MostrarAvisoPendientesPositivasSignificativas)
@if (MostrarAlertaPendientesPositivasSignificativas)
{
<div class="route-slope-warning" role="status">
<div>Pendientes positivas significativas.</div>
<div style="margin-top:.2rem; padding-left:.55rem;">
<div>- Distancia con subidas: @FormatearDistanciaResumenElevacion(_distanciaPendientePositivaSignificativaRutaMetros)</div>
<div>
- Subidas acumuladas: @_alturaPendientePositivaSignificativaRutaMetros.ToString("0.00", CultureInfo.InvariantCulture) m
(@((_alturaPendientePositivaSignificativaRutaMetros / MetrosPorPiso).ToString("0.0", CultureInfo.InvariantCulture)) pisos)
</div>
<div>- Subidas acumuladas: @_alturaPendientePositivaSignificativaRutaMetros.ToString("0.00", CultureInfo.InvariantCulture) m</div>
</div>
</div>
}
@@ -975,7 +890,7 @@
}
@if (!string.IsNullOrWhiteSpace(scoreEquilibradoSeleccionado))
{
<div>Score equilibrado: @scoreEquilibradoSeleccionado</div>
<div>Score equilibrado A: @scoreEquilibradoSeleccionado</div>
}
@if (!string.IsNullOrWhiteSpace(xyzOrigen))
{
@@ -1113,6 +1028,27 @@
}
</div>
}
@if (!string.IsNullOrWhiteSpace(_resumenErrorHistorialLocal))
{
<div class="history-local-diagnostic" role="alert">
<div class="history-local-diagnostic__title">Diagnóstico del historial personal</div>
<div class="history-local-diagnostic__summary">@_resumenErrorHistorialLocal</div>
<details class="history-local-diagnostic__details">
<summary>Detalle técnico</summary>
<pre>@_detalleErrorHistorialLocal</pre>
</details>
@if (!_historialLocalCargado && !_historialLocalCargando)
{
<button type="button"
class="history-local-diagnostic__retry"
@onclick="ReintentarCargaHistorialLocalAsync">
Reintentar carga
</button>
}
</div>
}
</div>
<div class="history-section">
@@ -1262,10 +1198,10 @@
</div>
<div class="config-row">
<label>Bus/paradas:</label>
<label>Paradas:</label>
<div class="config-dual-labeled">
<label class="config-dual-labeled__field">
<span>Seg. parada</span>
<span>Seg./parada</span>
@if (_usarHorariosTeoricos)
{
<input type="number" class="config-input" step="1" min="0" value="0" disabled readonly />
@@ -1348,7 +1284,7 @@
@bind="_itinerarioMin"
@bind:event="onchange"
@bind:after="PersistirConfiguracionAsync">
@foreach (var n in Range(1, 150, 1))
@foreach (var n in Range(1, 99, 1))
{
<option value="@n">@n</option>
}
@@ -1361,7 +1297,7 @@
@bind="_itinerarioMax"
@bind:event="onchange"
@bind:after="PersistirConfiguracionAsync">
@foreach (var n in Range(1, 150, 1))
@foreach (var n in Range(1, 99, 1))
{
<option value="@n">@n</option>
}
@@ -1441,19 +1377,23 @@
</div>
</div>
<div class="config-row config-row--radio-legacy">
<label>Coef. / Param.(m) andando:</label>
<div class="config-dual-inline config-dual-inline--bare">
<input type="number" class="config-input" step="any"
placeholder="Coef."
title="Coeficiente de penalizacion"
@bind="FactorPenalizacionDistanciaPieTexto" @bind:event="oninput"
@onblur="OnBlurFactorPenalizacionDistanciaPie" />
<input type="number" class="config-input" step="any"
placeholder="Param."
title="Parametro base en metros"
@bind="ParametroPenalizacionDistanciaPieTexto" @bind:event="oninput"
@onblur="OnBlurParametroPenalizacionDistanciaPie" />
<div class="config-row">
<label>Andando:</label>
<div class="config-dual-labeled">
<label class="config-dual-labeled__field">
<span>Coef.</span>
<input type="number" class="config-input" step="any"
title="Coeficiente de penalización andando"
@bind="FactorPenalizacionDistanciaPieTexto" @bind:event="oninput"
@onblur="OnBlurFactorPenalizacionDistanciaPie" />
</label>
<label class="config-dual-labeled__field">
<span>Base (m)</span>
<input type="number" class="config-input" step="any"
title="Parámetro base en metros"
@bind="ParametroPenalizacionDistanciaPieTexto" @bind:event="oninput"
@onblur="OnBlurParametroPenalizacionDistanciaPie" />
</label>
</div>
</div>
@@ -1464,7 +1404,7 @@
<span>Peso rank.</span>
<input type="text" class="config-input"
placeholder="4,0"
title="Penalizacion interna por cada transbordo real en minutos decimales"
title="Penalización temporal y distancia equivalente por cada transbordo real"
@bind="PesoTransbordoRankingTexto" @bind:event="oninput"
@onblur="OnBlurPesoTransbordoRanking" />
</label>
@@ -1478,59 +1418,90 @@
</div>
<div class="config-row">
<label>Equilibrado:</label>
<div class="config-dual-labeled config-dual-labeled--equilibrado">
<label>Factor / Método:</label>
<div class="config-dual-labeled">
<label class="config-dual-labeled__field">
<span>Factor</span>
<span>Equilib. A</span>
<input type="number" class="config-input" step="0.1" min="0"
placeholder="12"
placeholder="6"
title="Factor del modo equilibrado"
@bind="FactorEquilibradoAndarBusTexto" @bind:event="oninput"
@onblur="OnBlurFactorEquilibradoAndarBus" />
</label>
<label class="config-dual-labeled__field">
<span>Andando</span>
<select class="config-input"
title="Método de cálculo andando"
@bind="_walkProvider" @bind:after="PersistirConfiguracionAsync">
<option value="@ProveedorCaminata.Osrm">v1</option>
<option value="@ProveedorCaminata.Valhalla">v2</option>
</select>
</label>
</div>
</div>
<div class="config-row">
<label>Vel. Andando/Bus (km/h):</label>
<div class="config-dual-inline config-dual-inline--bare">
<input type="number" class="config-input" step="0.1"
placeholder="Andar"
title="Velocidad andando (km/h)"
@bind="_velocidadPieKmH" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
<input type="number" class="config-input" step="0.1"
placeholder="Bus"
title="Velocidad bus (km/h)"
@bind="_velocidadBusKmH" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
<label>Velocidad:</label>
<div class="config-dual-labeled">
<label class="config-dual-labeled__field">
<span>Andar (km/h)</span>
<input type="number" class="config-input" step="0.1"
title="Velocidad andando (km/h)"
@bind="_velocidadPieKmH" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
</label>
<label class="config-dual-labeled__field">
<span>Bus (km/h)</span>
<input type="number" class="config-input" step="0.1"
title="Velocidad bus (km/h)"
@bind="_velocidadBusKmH" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
</label>
</div>
</div>
<div class="config-row">
<label>Altitud: Max Puntos / Coef</label>
<div class="config-dual-inline config-dual-inline--bare">
<input type="number" class="config-input" min="2" max="999" step="1"
placeholder="Puntos"
title="Máximo de puntos de altitud por tramo"
@bind="_elevMaxPuntosPorTramo" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
<input type="number" class="config-input" step="any"
placeholder="Coef."
title="Coeficiente de esfuerzo por altura"
@bind="CoeficienteEsfuerzoAlturaTexto" @bind:event="oninput"
@onblur="OnBlurCoeficienteEsfuerzoAltura" />
<label>Altitud:</label>
<div class="config-dual-labeled">
<label class="config-dual-labeled__field">
<span>Máx. puntos</span>
<input type="number" class="config-input" min="2" max="999" step="1"
title="Máximo de puntos de altitud por tramo"
@bind="_elevMaxPuntosPorTramo" @bind:event="oninput" @bind:after="PersistirConfiguracionAsync" />
</label>
<label class="config-dual-labeled__field">
<span>Coef.</span>
<input type="number" class="config-input" step="any"
title="Coeficiente de esfuerzo por altura"
@bind="CoeficienteEsfuerzoAlturaTexto" @bind:event="oninput"
@onblur="OnBlurCoeficienteEsfuerzoAltura" />
</label>
</div>
</div>
<div class="config-row">
<label>Método cálculo andando:</label>
<div class="num-inline">
<select class="config-input"
style="width:100%; min-width:0;"
@bind="_walkProvider" @bind:after="PersistirConfiguracionAsync">
<option value="@ProveedorCaminata.Osrm">v1</option>
<option value="@ProveedorCaminata.Valhalla">v2</option>
</select>
</div>
</div>
<div class="config-row config-row-pendientes">
<label>Pendientes:</label>
<div class="config-pendientes-inline">
<label class="route-elevation-switch @(_mostrarElevacionesRuta ? "route-elevation-switch--active" : "")"
title="Activar o desactivar las pendientes en el cálculo">
<input type="checkbox"
checked="@_mostrarElevacionesRuta"
disabled="@_pintandoElevacionesRuta"
@onchange="ToggleElevacionesRutaAsync" />
<span class="route-elevation-switch-track" aria-hidden="true">
<span class="route-elevation-switch-thumb"></span>
</span>
<span class="config-pendientes-inline__label">Visualizar</span>
</label>
<button type="button"
class="config-pendientes-settings-button"
title="Configurar Pendientes"
aria-label="Configurar Pendientes"
disabled="@_pintandoElevacionesRuta"
@onclick="() => _mostrarConfiguracionPendientesRuta = true">
<span aria-hidden="true">⚙</span>
</button>
</div>
</div>
<div class="config-actions-row">
<button class="btn-ghost" @onclick="RecalcularConParametros">
@@ -1562,6 +1533,57 @@
}
</div>
@if (_mostrarConfiguracionPendientesRuta)
{
<div class="custom-modal-backdrop pendientes-config-modal-backdrop"
@onclick="() => _mostrarConfiguracionPendientesRuta = false">
<div class="custom-modal-card pendientes-config-modal" @onclick:stopPropagation="true">
<div class="pendientes-config-modal__header">
<div class="custom-modal-title">Configurar Pendientes</div>
<button type="button"
class="config-close-btn pendientes-config-modal__close"
title="Cerrar"
aria-label="Cerrar"
@onclick="() => _mostrarConfiguracionPendientesRuta = false">
×
</button>
</div>
<div class="pendientes-config-modal__fields">
<label class="pendientes-config-modal__field">
<span>Subida (%)</span>
<input type="text"
class="config-input"
inputmode="decimal"
value="@_umbralPendienteSubidaRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, true))" />
</label>
<label class="pendientes-config-modal__field">
<span>Bajada (%)</span>
<input type="text"
class="config-input"
inputmode="decimal"
value="@_umbralPendienteBajadaRuta.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="@(e => CambiarUmbralElevacionRutaAsync(e, false))" />
</label>
<label class="pendientes-config-modal__field">
<span>Alerta (m)</span>
<input type="text"
class="config-input"
inputmode="decimal"
value="@_umbralAlturaAlertaPendienteRutaMetros.ToString("0.#", CultureInfo.InvariantCulture)"
disabled="@_pintandoElevacionesRuta"
@onchange="CambiarUmbralAlturaAlertaPendienteRutaAsync" />
</label>
</div>
</div>
</div>
}
@if (_mostrarModelosHorariosPanel)
{
<div class="custom-modal-backdrop modelos-horarios-modal-backdrop" @onclick="CerrarModelosHorariosPanel">

View File

@@ -26,7 +26,7 @@ public sealed class ParametrosPlanificadorRuta
public double FactorPenalizacionDistanciaPie { get; set; } = 1.35;
public double ParametroPenalizacionDistanciaPieMetros { get; set; } = 200.0;
public double PesoTransbordoRankingMinutos { get; set; } = 4.0;
public double FactorEquilibradoAndarBus { get; set; } = 12.0;
public double FactorEquilibradoAndarBus { get; set; } = 6.0;
// selección candidatas
public int RadioParadasCandidatasMetros { get; set; } = 1200;

View File

@@ -828,9 +828,12 @@ public sealed class BuscadorRutasTemporalesDbus
parametros);
var factor = double.IsFinite(parametros.FactorEquilibradoAndarBus)
? Math.Max(0, parametros.FactorEquilibradoAndarBus)
: 12.0;
: 6.0;
var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros(
UtilidadesTransitoDbus.ContarTransbordosReales(resultado.RutaBus.Tramos),
parametros);
var score = distanciaBus + (distanciaAndando * factor);
var score = distanciaBus + (distanciaAndando * factor) + penalizacionTransbordos;
return double.IsFinite(score) && score >= 0
? score
: double.PositiveInfinity;

View File

@@ -1449,6 +1449,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
var limiteCoste = Math.Max(1, maxPorCoste);
var limiteAndar = Math.Max(8, limiteCoste / 2);
var limiteEquilibrado = Math.Max(8, limiteCoste / 2);
var limiteDirectas = Math.Max(8, limiteCoste / 4);
var duracionMinima = lista
.Select(x => x.DuracionEstimadaSegundos)
.Where(double.IsFinite)
@@ -1479,6 +1480,15 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
.ThenBy(x => x.PuntuacionCoste)
.Take(limiteEquilibrado));
// Las directas no deben depender de que una reconstruccion circular las vuelva a inyectar.
Agregar(lista
.Where(x => x.RutaBus.Tramos.Count == 1)
.OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
.ThenBy(x => x.PuntuacionCoste)
.ThenBy(x => x.DuracionEstimadaSegundos)
.ThenBy(x => x.DistanciaPieTotal)
.Take(limiteDirectas));
Agregar(lista
.Where(x => x.DuracionEstimadaSegundos <= limiteDuracionAndarRazonable)
.OrderBy(x => x.DistanciaPieTotal)
@@ -1527,9 +1537,14 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
parametros);
var factor = parametros is not null && double.IsFinite(parametros.FactorEquilibradoAndarBus)
? Math.Max(0, parametros.FactorEquilibradoAndarBus)
: 12.0;
: 6.0;
var penalizacionTransbordos = parametros is null
? 0
: UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros(
UtilidadesTransitoDbus.ContarTransbordosReales(resultado.RutaBus.Tramos),
parametros);
var score = distanciaBus + (distanciaAndando * factor);
var score = distanciaBus + (distanciaAndando * factor) + penalizacionTransbordos;
return double.IsFinite(score) && score >= 0
? score
: double.PositiveInfinity;
@@ -3199,9 +3214,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
var distanciaAndando = CalcularDistanciaAndandoCorregidaMetros(alternativa, parametros);
var factor = double.IsFinite(parametros.FactorEquilibradoAndarBus)
? Math.Max(0, parametros.FactorEquilibradoAndarBus)
: 12.0;
: 6.0;
var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros(
alternativa.NumeroTransbordosReales,
parametros);
var score = distanciaBus + (distanciaAndando * factor);
var score = distanciaBus + (distanciaAndando * factor) + penalizacionTransbordos;
return double.IsFinite(score) && score >= 0
? score
: double.PositiveInfinity;
@@ -3458,7 +3476,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
}
/// <summary>
/// Firma orientada a UI: agrupa alternativas que usan la misma familia de lineas aunque cambie la parada exacta.
/// Firma orientada a UI: agrupa alternativas con la misma secuencia de lineas y sentidos
/// aunque cambie la parada exacta.
/// </summary>
private static string CrearFirmaFamiliaRutaBus(RutaBusAlternativa alternativa)
{
@@ -3466,7 +3485,8 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
foreach (var tramo in alternativa.Tramos)
{
var linea = UtilidadesTransitoDbus.LineaCodigoStr(tramo.Variante);
var parte = $"{linea}.{tramo.Variante.CodigoItinerario}";
var sentido = UtilidadesTransitoDbus.NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo);
var parte = $"{linea}.{tramo.Variante.CodigoItinerario}-{sentido}";
if (partes.Count == 0 || !string.Equals(partes[^1], parte, StringComparison.OrdinalIgnoreCase))
partes.Add(parte);
}

View File

@@ -384,6 +384,28 @@ internal static class UtilidadesTransitoDbus
return numeroTransbordosReales * pesoMinutos * 60.0;
}
/// <summary>
/// Convierte la penalizacion temporal de los transbordos en distancia equivalente de bus.
/// </summary>
public static double CalcularPenalizacionTransbordosRankingMetros(
int numeroTransbordosReales,
ParametrosPlanificadorRuta parametros)
{
var penalizacionSegundos = CalcularPenalizacionTransbordosRankingSegundos(
numeroTransbordosReales,
parametros);
if (penalizacionSegundos <= 0)
return 0;
var velocidadBusKmH = double.IsFinite(parametros.VelocidadBusKmH) && parametros.VelocidadBusKmH > 0
? parametros.VelocidadBusKmH
: 16.0;
var penalizacionMetros = penalizacionSegundos * (velocidadBusKmH * 1000.0 / 3600.0);
return double.IsFinite(penalizacionMetros) && penalizacionMetros > 0
? penalizacionMetros
: 0;
}
/// <summary>
/// Calcula el coste interno aplicando preferencias de caminata y transbordos reales.
/// </summary>

View File

@@ -872,24 +872,6 @@ html, body {
background: #2563eb;
}
.route-visual-options {
position: relative;
display: inline-flex;
align-self: flex-start;
align-items: center;
justify-content: flex-start;
flex-wrap: nowrap;
width: fit-content;
max-width: 100%;
box-sizing: border-box;
gap: .25rem;
margin: -.15rem 0 .4rem;
padding: 2px 3px 2px 5px;
border: 1px solid rgba(148, 163, 184, .28);
border-radius: 7px;
background: rgba(2, 6, 23, .3);
}
.route-elevation-switch {
display: inline-flex;
align-items: center;
@@ -952,130 +934,6 @@ html, body {
opacity: .55;
}
.route-elevation-settings {
display: inline-flex;
align-items: center;
margin-left: .05rem;
padding-left: .3rem;
border-left: 1px solid rgba(148, 163, 184, .35);
pointer-events: auto;
}
.route-elevation-settings-trigger {
display: grid;
place-items: center;
width: 26px;
height: 24px;
padding: 0;
border: 1px solid rgba(148, 163, 184, .55);
border-radius: 6px;
background: rgba(2, 6, 23, .78);
color: #cbd5e1;
cursor: pointer;
font-family: "Segoe UI Symbol", sans-serif;
font-size: 1rem;
line-height: 1;
}
.route-elevation-settings-trigger:hover,
.route-elevation-settings-trigger--active {
border-color: rgba(56, 189, 248, .9);
background: rgba(30, 64, 175, .55);
color: #f8fafc;
}
.route-elevation-settings-trigger:disabled {
cursor: default;
opacity: .55;
}
.route-elevation-settings-popover {
position: absolute;
z-index: 20;
top: calc(100% + 6px);
right: auto;
left: 0;
width: 238px;
max-width: calc(100vw - 24px);
box-sizing: border-box;
padding: .55rem;
border: 1px solid rgba(148, 163, 184, .55);
border-radius: 7px;
background: rgba(15, 23, 42, .99);
box-shadow: 0 14px 32px rgba(0, 0, 0, .5);
color: #e5e7eb;
}
.route-elevation-settings-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: .5rem;
margin-bottom: .45rem;
font-size: .72rem;
font-weight: 800;
}
.route-elevation-settings-close {
display: grid;
place-items: center;
width: 22px;
height: 22px;
padding: 0;
border: 0;
border-radius: 4px;
background: transparent;
color: #cbd5e1;
cursor: pointer;
font-size: 1rem;
line-height: 1;
}
.route-elevation-settings-close:hover {
background: rgba(71, 85, 105, .55);
color: #fff;
}
.route-elevation-settings-fields {
display: grid;
gap: .35rem;
}
.route-elevation-setting-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 44px 32px;
align-items: center;
gap: .35rem;
min-height: 26px;
color: #cbd5e1;
font-size: .68rem;
font-weight: 700;
}
.route-elevation-setting-row input {
width: 44px;
height: 22px;
box-sizing: border-box;
border: 1px solid rgba(148, 163, 184, .55);
border-radius: 6px;
background: rgba(2, 6, 23, .78);
color: #f8fafc;
font-size: .68rem;
font-weight: 800;
text-align: center;
padding: 0 .18rem;
outline: none;
}
.route-elevation-setting-row input:focus {
border-color: rgba(56, 189, 248, .95);
box-shadow: 0 0 0 2px rgba(56, 189, 248, .18);
}
.route-elevation-setting-row input:disabled {
opacity: .55;
}
.route-slope-warning {
margin: 0 0 .45rem;
padding: .36rem .5rem;
@@ -1414,6 +1272,70 @@ html, body {
color: #fca5a5;
}
.history-local-diagnostic {
margin-top: .35rem;
padding: .5rem .58rem;
border-left: 3px solid #ef4444;
background: rgba(127, 29, 29, .18);
color: #fecaca;
font-size: .72rem;
line-height: 1.35;
}
.history-local-diagnostic__title {
color: #fee2e2;
font-weight: 700;
}
.history-local-diagnostic__summary {
margin-top: .14rem;
overflow-wrap: anywhere;
}
.history-local-diagnostic__details {
margin-top: .3rem;
color: #fca5a5;
}
.history-local-diagnostic__details summary {
width: fit-content;
cursor: pointer;
color: #fda4af;
font-weight: 600;
}
.history-local-diagnostic__details pre {
max-height: 150px;
margin: .35rem 0 0;
padding: .38rem .42rem;
overflow: auto;
border: 1px solid rgba(248, 113, 113, .25);
background: rgba(15, 23, 42, .72);
color: #fecaca;
font-family: Consolas, monospace;
font-size: .66rem;
line-height: 1.35;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.history-local-diagnostic__retry {
margin-top: .42rem;
padding: .3rem .55rem;
border: 1px solid rgba(248, 113, 113, .6);
border-radius: .35rem;
background: rgba(127, 29, 29, .32);
color: #fee2e2;
font-size: .7rem;
font-weight: 600;
cursor: pointer;
}
.history-local-diagnostic__retry:hover {
border-color: #fca5a5;
background: rgba(153, 27, 27, .48);
}
.config-panel {
position: absolute;
bottom: 2.4rem;
@@ -1755,12 +1677,36 @@ html, body {
border: none;
}
.rt-stop-direction-icon {
.rt-highlighted-stop-icon {
border: 0;
background: transparent;
pointer-events: auto;
}
.rt-highlighted-stop-marker {
position: relative;
display: block;
width: 100%;
height: 100%;
}
.rt-highlighted-stop-marker__disc,
.rt-highlighted-stop-marker__ring {
position: absolute;
border-radius: 50%;
pointer-events: none;
}
.rt-highlighted-stop-marker__ring {
inset: 0;
box-sizing: border-box;
border-style: solid;
}
.rt-highlighted-stop-marker__disc {
z-index: 1;
}
.rt-stop-direction-arrow {
position: absolute;
top: 50%;
@@ -1770,6 +1716,8 @@ html, body {
transform: translate(-50%, -50%) rotate(var(--stop-bearing, 0deg));
transform-origin: 50% 50%;
filter: drop-shadow(0 0 1px #0f172a) drop-shadow(0 1px 0 #0f172a);
z-index: 2;
pointer-events: none;
}
.rt-stop-direction-arrow::before {
@@ -1863,53 +1811,6 @@ html, body {
-moz-appearance: textfield;
}
.num-inline {
display: grid;
grid-template-columns: 1fr 1.6rem 1.6rem; /* input | ? | ? */
gap: .25rem;
align-items: center;
min-width: 0;
}
.num-inline .config-input {
width: 100% !important; /* pisa width:90px anterior */
min-width: 0;
box-sizing: border-box;
}
.config-dual-inline {
display: grid;
grid-template-columns: 1fr 1fr;
gap: .5rem;
align-items: center;
min-width: 0;
}
.config-dual-inline .config-input {
width: 100% !important;
min-width: 0;
box-sizing: border-box;
text-align: center;
}
.config-dual-inline--bare {
grid-template-columns: repeat(2, minmax(0, 4.2rem));
justify-content: end;
gap: .35rem;
}
.config-dual-inline--bare .config-input {
max-width: none;
text-align: center;
padding-left: .35rem;
padding-right: .35rem;
}
.config-dual-inline--bare .config-input::placeholder {
color: #94a3b8;
opacity: 1;
font-size: .72rem;
}
.config-dual-labeled {
display: grid;
grid-template-columns: repeat(2, minmax(0, 4.2rem));
@@ -1941,19 +1842,113 @@ html, body {
text-align: center;
}
.config-dual-labeled--equilibrado {
grid-template-columns: minmax(0, 4rem);
align-items: end;
justify-content: start;
}
/* ✅ Solo esta fila: label arriba + control abajo, ancho completo */
.config-row.config-row-full {
grid-template-columns: 1fr !important;
}
.config-pendientes-inline {
display: flex;
align-items: center;
justify-content: flex-end;
gap: .28rem;
width: 100%;
min-width: 0;
}
.config-pendientes-inline .route-elevation-switch {
min-height: 30px;
font-size: .68rem;
font-weight: 600;
}
.config-pendientes-inline .route-elevation-switch--active .route-elevation-switch-track {
border-color: #3b82f6;
background: #2563eb;
}
.config-pendientes-settings-button {
display: grid;
place-items: center;
width: 28px;
height: 28px;
flex: 0 0 28px;
padding: 0;
border: 0;
border-radius: 6px;
background: transparent;
color: #94a3b8;
cursor: pointer;
font-family: "Segoe UI Symbol", sans-serif;
font-size: .92rem;
line-height: 1;
}
.config-pendientes-settings-button:hover,
.config-pendientes-settings-button:focus-visible {
background: rgba(51, 65, 85, .72);
color: #f8fafc;
outline: none;
}
.config-pendientes-settings-button:disabled {
cursor: default;
opacity: .5;
}
.pendientes-config-modal-backdrop {
z-index: 5200;
}
.pendientes-config-modal {
width: min(286px, calc(100vw - 24px));
padding: .75rem .8rem .8rem;
border-radius: 8px;
}
.pendientes-config-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: .5rem;
min-height: 30px;
margin-bottom: .65rem;
}
.pendientes-config-modal__header .custom-modal-title {
margin: 0;
font-size: .86rem;
}
.pendientes-config-modal__close {
width: 30px;
height: 30px;
flex: 0 0 30px;
}
.pendientes-config-modal__fields {
display: grid;
gap: .45rem;
min-width: 0;
}
.pendientes-config-modal__field {
display: grid;
grid-template-columns: minmax(0, 1fr) 86px;
align-items: center;
gap: .65rem;
min-width: 0;
color: #cbd5e1;
font-size: .76rem;
}
.pendientes-config-modal__field .config-input {
width: 100%;
min-width: 0;
box-sizing: border-box;
text-align: center;
}
.config-row-map-opacity {
margin-top: .7rem !important;
margin-bottom: 0;
@@ -3171,37 +3166,11 @@ html, body {
font-size: .73rem;
}
.route-visual-options {
justify-content: flex-start;
flex-wrap: nowrap;
gap: 4px;
margin: 0 0 8px;
padding: 2px 3px 2px 6px;
}
.route-elevation-switch {
min-height: 44px;
font-size: .72rem;
}
.route-elevation-settings-trigger {
width: 40px;
height: 40px;
}
.route-elevation-settings-popover {
right: auto;
left: 0;
width: 250px;
max-width: calc(100vw - 20px);
}
.route-elevation-setting-row {
grid-template-columns: minmax(0, 1fr) 48px 36px;
min-height: 40px;
font-size: .72rem;
}
.route-elevation-setting-row input {
width: 48px;
height: 36px;
@@ -3283,7 +3252,6 @@ html, body {
}
.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 {
@@ -3400,9 +3368,23 @@ html, body {
font-size: 16px;
}
.config-pendientes-inline .route-elevation-switch,
.config-pendientes-settings-button {
min-height: 44px;
}
.config-pendientes-settings-button {
width: 44px;
height: 44px;
flex-basis: 44px;
}
.pendientes-config-modal__field .config-input {
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;
}

View File

@@ -901,29 +901,89 @@ window.rt.ensureItStopsLayer = function () {
return true;
};
window.rt.addStopDirectionMarker = function (group, stop, pane, radius) {
if (!group || !stop || !window.L) return;
window.rt.createHighlightedStopMarker = function (stop, pane, options) {
if (!stop || !window.L) return null;
const bearing = Number(stop.bearing);
const lat = Number(stop.lat);
const lon = Number(stop.lon);
if (!Number.isFinite(bearing) || !Number.isFinite(lat) || !Number.isFinite(lon)) return;
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
const toPositiveNumber = function (value, fallback) {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
};
const toOpacity = function (value, fallback) {
const parsed = Number(value);
return Number.isFinite(parsed) ? Math.min(1, Math.max(0, parsed)) : fallback;
};
const radius = toPositiveNumber(options && options.radius, 7);
const weight = toPositiveNumber(options && options.weight, 2);
const color = String((options && options.color) || "#0f172a");
const fillColor = String((options && options.fillColor) || "#22c55e");
const opacity = toOpacity(options && options.opacity, 1);
const fillOpacity = toOpacity(options && options.fillOpacity, 1);
const size = Math.max(14, Math.ceil((radius * 2) + weight));
const markerElement = document.createElement("span");
markerElement.className = "rt-highlighted-stop-marker";
const disc = document.createElement("span");
disc.className = "rt-highlighted-stop-marker__disc";
disc.style.inset = `${weight}px`;
disc.style.backgroundColor = fillColor;
disc.style.opacity = String(fillOpacity);
markerElement.appendChild(disc);
const ring = document.createElement("span");
ring.className = "rt-highlighted-stop-marker__ring";
ring.style.borderColor = color;
ring.style.borderWidth = `${weight}px`;
ring.style.opacity = String(opacity);
markerElement.appendChild(ring);
const rawBearing = stop.bearing;
const bearing = rawBearing === null || rawBearing === undefined || rawBearing === ""
? null
: Number(rawBearing);
if (Number.isFinite(bearing)) {
const arrow = document.createElement("span");
arrow.className = "rt-stop-direction-arrow";
arrow.style.setProperty("--stop-bearing", `${bearing}deg`);
markerElement.appendChild(arrow);
}
const size = Math.max(14, Number(radius || 7) * 2);
const icon = window.L.divIcon({
className: "rt-stop-direction-icon",
html: `<span class="rt-stop-direction-arrow" style="--stop-bearing:${bearing}deg"></span>`,
className: "rt-highlighted-stop-icon",
html: markerElement,
iconSize: [size, size],
iconAnchor: [size / 2, size / 2]
});
window.L.marker([lat, lon], {
const marker = window.L.marker([lat, lon], {
pane,
icon,
interactive: false,
interactive: true,
keyboard: false,
bubblingMouseEvents: false,
zIndexOffset: 1000
}).addTo(group);
});
if (stop.code && window.rt._dotNetHelper) {
marker.on("click", function (event) {
try {
if (event && event.originalEvent) {
if (event.originalEvent.preventDefault) event.originalEvent.preventDefault();
if (event.originalEvent.stopPropagation) event.originalEvent.stopPropagation();
if (event.originalEvent.stopImmediatePropagation) event.originalEvent.stopImmediatePropagation();
}
window.rt._dotNetHelper.invokeMethodAsync("OnHighlightedStopClick", stop.code);
} catch (error) { }
});
}
return marker;
};
// stops: [{lat, lon, code, bearing}]
@@ -949,34 +1009,15 @@ window.rt.toggleItStops = function (id, stops, options) {
if (stops && stops.length) {
for (let i = 0; i < stops.length; i++) {
const s = stops[i];
const m = window.L.circleMarker([s.lat, s.lon], {
pane: "rtStopsPane",
const marker = window.rt.createHighlightedStopMarker(s, "rtStopsPane", {
radius,
color,
weight,
opacity,
fillColor,
fillOpacity,
interactive: true
fillOpacity
});
if (s.code && window.rt._dotNetHelper) {
m.on("click", function (e) {
try {
if (e && e.originalEvent) {
if (e.originalEvent.preventDefault) e.originalEvent.preventDefault();
if (e.originalEvent.stopPropagation) e.originalEvent.stopPropagation();
if (e.originalEvent.stopImmediatePropagation) e.originalEvent.stopImmediatePropagation();
}
window.rt._dotNetHelper.invokeMethodAsync("OnHighlightedStopClick", s.code);
} catch (err) { }
});
}
m.addTo(group);
window.rt.addStopDirectionMarker(group, s, "rtStopsPane", radius);
if (marker) marker.addTo(group);
}
}
@@ -1027,34 +1068,15 @@ window.rt.drawRouteStops = function (id, stops, options) {
for (let i = 0; i < stops.length; i++) {
const s = stops[i];
const m = window.L.circleMarker([s.lat, s.lon], {
pane: "rtRouteStopsPane",
const marker = window.rt.createHighlightedStopMarker(s, "rtRouteStopsPane", {
radius,
color,
weight,
opacity,
fillColor,
fillOpacity,
interactive: true
fillOpacity
});
if (s.code && window.rt._dotNetHelper) {
m.on("click", function (e) {
try {
if (e && e.originalEvent) {
if (e.originalEvent.preventDefault) e.originalEvent.preventDefault();
if (e.originalEvent.stopPropagation) e.originalEvent.stopPropagation();
if (e.originalEvent.stopImmediatePropagation) e.originalEvent.stopImmediatePropagation();
}
window.rt._dotNetHelper.invokeMethodAsync("OnHighlightedStopClick", s.code);
} catch (err) { }
});
}
m.addTo(group);
window.rt.addStopDirectionMarker(group, s, "rtRouteStopsPane", radius);
if (marker) marker.addTo(group);
}
group.addTo(window.rt._routeStopsLayer);
@@ -1537,20 +1559,65 @@ window.rt.initDraggablePopups = function () {
applySavedPositions();
};
window.rt.saveLocalValue = function (key, value) {
window.rt.saveLocalValueResult = function (key, value) {
try {
if (!window.localStorage) return;
window.localStorage.setItem(key, value ?? "");
} catch { }
const storage = window.localStorage;
if (!storage) {
return {
ok: false,
errorName: "StorageUnavailable",
errorMessage: "El navegador no expone localStorage."
};
}
storage.setItem(key, value ?? "");
return { ok: true };
} catch (error) {
return {
ok: false,
errorName: error?.name ? String(error.name) : "StorageError",
errorMessage: error?.message ? String(error.message) : String(error ?? "Error desconocido al guardar en localStorage.")
};
}
};
window.rt.loadLocalValueResult = function (key) {
try {
const storage = window.localStorage;
if (!storage) {
return {
ok: false,
found: false,
value: null,
errorName: "StorageUnavailable",
errorMessage: "El navegador no expone localStorage."
};
}
const value = storage.getItem(key);
return {
ok: true,
found: value !== null,
value
};
} catch (error) {
return {
ok: false,
found: false,
value: null,
errorName: error?.name ? String(error.name) : "StorageError",
errorMessage: error?.message ? String(error.message) : String(error ?? "Error desconocido al leer localStorage.")
};
}
};
window.rt.saveLocalValue = function (key, value) {
window.rt.saveLocalValueResult(key, value);
};
window.rt.loadLocalValue = function (key) {
try {
if (!window.localStorage) return null;
return window.localStorage.getItem(key);
} catch {
return null;
}
const result = window.rt.loadLocalValueResult(key);
return result.ok ? result.value : null;
};
window.rt.clearLocalBrowserData = async function () {