- 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:
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">⚙</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">×</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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user