1027 lines
42 KiB
C#
1027 lines
42 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using LeafletForBlazor;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.JSInterop;
|
|
using RutasDBUS.Modelos.Planificacion;
|
|
using RutasDBUS.Modelos.RedBus;
|
|
using RutasDBUS.Servicios;
|
|
using RutasDBUS.Servicios.Caminata;
|
|
using RutasDBUS.Servicios.Horarios;
|
|
using RutasDBUS.Servicios.Planificacion;
|
|
using RutasDBUS.Servicios.RedBus;
|
|
|
|
namespace RutasDBUS.Components.Pages;
|
|
|
|
public partial class PlanificadorRutas : ComponentBase
|
|
{
|
|
/// <summary>
|
|
/// Asegura que elevacion tramo este disponible.
|
|
/// </summary>
|
|
private async Task EnsureElevacionTramoAsync(DetalleTramoRuta tramo)
|
|
{
|
|
if (tramo is null) return;
|
|
|
|
// Si ya está calculado, nada
|
|
if (tramo.MuestrasElevacion is not null && tramo.MuestrasElevacion.Count > 0) return;
|
|
|
|
// Necesitamos la geometría
|
|
if (tramo.Linea is null || tramo.Linea.GetLength(0) <= 0) return;
|
|
|
|
if (tramo.ElevacionCargando) return;
|
|
|
|
tramo.ElevacionCargando = true;
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
var ctNone = CancellationToken.None;
|
|
|
|
// 1) Samples bajo demanda, priorizando G y dejando O como best-effort
|
|
var samples = await ConstruirMuestrasElevacionAsync(tramo.Linea, ctNone);
|
|
tramo.MuestrasElevacion = samples;
|
|
|
|
// 2) Resumen O solo con lo que ya haya llegado, sin bloquear más la UI
|
|
var muestrasOpen = samples
|
|
.Where(x => x.Elevacion is not null)
|
|
.Select(x => x.Elevacion!.Value)
|
|
.ToList();
|
|
|
|
tramo.ElevInicio = samples.FirstOrDefault()?.Elevacion;
|
|
tramo.ElevFin = samples.LastOrDefault()?.Elevacion;
|
|
tramo.ElevMin = muestrasOpen.Count > 0 ? muestrasOpen.Min() : null;
|
|
tramo.ElevMax = muestrasOpen.Count > 0 ? muestrasOpen.Max() : null;
|
|
}
|
|
catch
|
|
{
|
|
// silencioso: la elevación es informativa
|
|
tramo.MuestrasElevacion ??= new List<MuestraElevacion>();
|
|
}
|
|
finally
|
|
{
|
|
tramo.ElevacionCargando = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task ToggleElevacionesRutaAsync(ChangeEventArgs e)
|
|
{
|
|
_mostrarElevacionesRuta = e.Value switch
|
|
{
|
|
bool valor => valor,
|
|
string texto when bool.TryParse(texto, out var valor) => valor,
|
|
_ => !_mostrarElevacionesRuta
|
|
};
|
|
|
|
await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta
|
|
? "Aplicando elevaciones..."
|
|
: "Representando ruta seleccionada...");
|
|
}
|
|
|
|
private async Task CambiarUmbralElevacionRutaAsync(ChangeEventArgs e, bool esSubida)
|
|
{
|
|
var actual = esSubida
|
|
? _umbralPendienteSubidaRuta
|
|
: _umbralPendienteBajadaRuta;
|
|
var nuevo = ParseUmbralPendienteRuta(e.Value, actual);
|
|
|
|
if (esSubida)
|
|
_umbralPendienteSubidaRuta = nuevo;
|
|
else
|
|
_umbralPendienteBajadaRuta = nuevo;
|
|
|
|
await PersistirConfiguracionAsync();
|
|
|
|
if (_mostrarElevacionesRuta)
|
|
await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando elevaciones...");
|
|
else
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task RepintarRutaSeleccionadaPorElevacionesAsync(string mensaje)
|
|
{
|
|
if (_opcionesRuta.Count == 0 ||
|
|
_indiceAlternativaSeleccionada < 0 ||
|
|
_indiceAlternativaSeleccionada >= _opcionesRuta.Count)
|
|
{
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
_pintandoElevacionesRuta = true;
|
|
_mensajeRuta = mensaje;
|
|
StateHasChanged();
|
|
await Task.Yield();
|
|
|
|
try
|
|
{
|
|
await CerrarPopupsAsync();
|
|
await PintarOpcionRutaCompleta(_opcionesRuta[_indiceAlternativaSeleccionada]);
|
|
}
|
|
finally
|
|
{
|
|
_pintandoElevacionesRuta = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private static double ParseUmbralPendienteRuta(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 NormalizarUmbralPendienteRuta(actual);
|
|
}
|
|
|
|
return NormalizarUmbralPendienteRuta(nuevo);
|
|
}
|
|
|
|
private static double NormalizarUmbralPendienteRuta(double valor)
|
|
=> double.IsFinite(valor)
|
|
? Math.Clamp(Math.Abs(valor), 0.0, 50.0)
|
|
: 3.0;
|
|
|
|
private async Task DrawWalkingRouteJsAsync(
|
|
string id,
|
|
double[,] linea,
|
|
int weight,
|
|
int versionPintado,
|
|
bool arrowEnd = true)
|
|
{
|
|
var segmentosElevacion = await ConstruirSegmentosElevacionMapaAsync(linea, CancellationToken.None);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
await DrawRouteJsAsync(
|
|
id,
|
|
linea,
|
|
"#dc2626",
|
|
weight,
|
|
0.9,
|
|
dashArray: DashArrayPiePunteado,
|
|
arrowEnd: arrowEnd,
|
|
lineCap: "round",
|
|
elevationSegments: segmentosElevacion);
|
|
}
|
|
|
|
private async Task<List<object>?> ConstruirSegmentosElevacionMapaAsync(double[,] linea, CancellationToken ct)
|
|
{
|
|
if (!_mostrarElevacionesRuta || linea.GetLength(0) < 2)
|
|
return null;
|
|
|
|
var muestras = await ConstruirMuestrasElevacionAsync(linea, ct);
|
|
if (muestras.Count < 2)
|
|
return null;
|
|
|
|
var segmentos = new List<object>();
|
|
var hayPendienteCalculada = false;
|
|
|
|
for (var i = 1; i < muestras.Count; i++)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
var anterior = muestras[i - 1];
|
|
var actual = muestras[i];
|
|
var distancia = CalcularDistanciaMetros(
|
|
anterior.Latitud,
|
|
anterior.Longitud,
|
|
actual.Latitud,
|
|
actual.Longitud);
|
|
|
|
if (distancia <= 0.5)
|
|
continue;
|
|
|
|
var elevAnterior = ElevacionPreferidaMapa(anterior);
|
|
var elevActual = ElevacionPreferidaMapa(actual);
|
|
double? pendiente = null;
|
|
|
|
if (elevAnterior is not null && elevActual is not null)
|
|
{
|
|
pendiente = (elevActual.Value - elevAnterior.Value) / distancia * 100.0;
|
|
hayPendienteCalculada = true;
|
|
}
|
|
|
|
var (categoria, color) = ClasificarPendienteMapa(pendiente);
|
|
segmentos.Add(new
|
|
{
|
|
coords = new[]
|
|
{
|
|
new[] { anterior.Latitud, anterior.Longitud },
|
|
new[] { actual.Latitud, actual.Longitud }
|
|
},
|
|
color,
|
|
category = categoria,
|
|
slope = pendiente,
|
|
distance = distancia,
|
|
elevation = elevActual,
|
|
label = pendiente is null
|
|
? "-"
|
|
: pendiente.Value.ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture) + " %"
|
|
});
|
|
}
|
|
|
|
return hayPendienteCalculada && segmentos.Count > 0
|
|
? segmentos
|
|
: null;
|
|
}
|
|
|
|
private static double? ElevacionPreferidaMapa(MuestraElevacion muestra)
|
|
=> muestra.ElevacionIdee ?? muestra.Elevacion;
|
|
|
|
private (string Categoria, string Color) ClasificarPendienteMapa(double? pendientePorcentaje)
|
|
{
|
|
if (pendientePorcentaje is null || !double.IsFinite(pendientePorcentaje.Value))
|
|
return ("sin-datos", "#94a3b8");
|
|
|
|
var umbralSubida = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta);
|
|
var umbralBajada = NormalizarUmbralPendienteRuta(_umbralPendienteBajadaRuta);
|
|
if (pendientePorcentaje.Value >= umbralSubida)
|
|
return ("subida", "#000000");
|
|
|
|
if (pendientePorcentaje.Value <= -umbralBajada)
|
|
return ("bajada", "#2563eb");
|
|
|
|
return ("llano", "#dc2626");
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Pinta opcion ruta completa.
|
|
/// </summary>
|
|
private async Task PintarOpcionRutaCompleta(AlternativaRuta opcion)
|
|
{
|
|
var versionPintado = IniciarNuevaVersionPintadoRuta();
|
|
var mensajeAnterior = _mensajeRuta;
|
|
|
|
try
|
|
{
|
|
await ClearRoutesJsAsync();
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
if (opcion.EsSoloAPie)
|
|
{
|
|
await PintarRutaSoloAPie(opcion.LineaSoloAPie, opcion.DistanciaSoloAPie, versionPintado: versionPintado);
|
|
}
|
|
else
|
|
{
|
|
await PintarRutaDesdeAlternativa(opcion.OrigenParada, opcion.DestinoParada, opcion.AlternativaBus!, versionPintado);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
await PintarParadasSolucionSeleccionadaAsync(opcion, versionPintado);
|
|
}
|
|
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
await ActualizarResumenTecnicoRutaAsync(CancellationToken.None);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
await CerrarPopupsAsync();
|
|
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
await FitRoutesJsAsync();
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
StateHasChanged();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_mensajeRuta))
|
|
_mensajeRuta = mensajeAnterior;
|
|
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_mensajeRuta = $"No se ha podido representar la ruta seleccionada: {ex.Message}";
|
|
_hayRutaValida = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pinta las paradas recorridas por la solución seleccionada con el color de cada tramo de bus.
|
|
/// </summary>
|
|
private async Task PintarParadasSolucionSeleccionadaAsync(AlternativaRuta opcion, int versionPintado)
|
|
{
|
|
await ClearRouteStopsJsAsync();
|
|
LimpiarParadasRutaVisibles();
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
var tramos = opcion.AlternativaBus?.Tramos;
|
|
if (opcion.EsSoloAPie || tramos is null || tramos.Count == 0)
|
|
return;
|
|
|
|
for (var indice = 0; indice < tramos.Count; indice++)
|
|
{
|
|
var tramo = tramos[indice];
|
|
if (tramo?.Variante is null)
|
|
continue;
|
|
|
|
var paradasTramo = BuildParadasForTramoBus(tramo);
|
|
if (paradasTramo.Count == 0)
|
|
continue;
|
|
|
|
var stops = BuildStopsDesdeParadas(paradasTramo);
|
|
|
|
var id = $"route_stops_{indice}_{tramo.Variante.Linea.Codigo}_{tramo.Variante.CodigoItinerario}_{NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo)}";
|
|
var color = ColorCssDesdeVariante(tramo.Variante);
|
|
|
|
await DrawRouteStopsJsAsync(id, stops, color, radius: 8, weight: 2, opacity: 1.0, fillOpacity: 1.0);
|
|
RegistrarParadasRutaVisibles(id, paradasTramo);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pinta ruta solo a pie.
|
|
/// </summary>
|
|
private async Task PintarRutaSoloAPie(double[,]? lineaPre = null, double distPre = 0, int versionPintado = 0)
|
|
{
|
|
if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null) return;
|
|
|
|
ResetEstadoRuta(limpiarMensaje: false);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
double[,]? linea = lineaPre;
|
|
double distancia = distPre;
|
|
|
|
if (linea is null)
|
|
{
|
|
var w = await WalkCached(_coordenadasOrigen[0], _coordenadasOrigen[1], _coordenadasDestino[0], _coordenadasDestino[1]);
|
|
linea = w.line;
|
|
distancia = w.dist;
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
}
|
|
|
|
if (linea is null)
|
|
{
|
|
_mensajeRuta = "No he podido calcular una ruta a pie entre los puntos seleccionados.";
|
|
_hayRutaValida = false;
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
// ✅ duración por velocidad configurada
|
|
double duracion = (distancia > 0) ? EstimarDuracionPieSegundos(distancia) : 0;
|
|
|
|
_distanciaAPieInicio = distancia;
|
|
_duracionTotalSegundos = duracion;
|
|
|
|
// ✅ Guardamos el tramo andando SIN calcular elevación
|
|
_tramosRuta.Add(new DetalleTramoRuta
|
|
{
|
|
Tipo = "A pie",
|
|
Descripcion = CapitalizarPalabrasClave("Camina desde tu punto de origen hasta tu destino final."),
|
|
DistanciaMetros = distancia,
|
|
DuracionSegundos = duracion,
|
|
|
|
// clave para lazy elevation
|
|
Linea = linea,
|
|
|
|
// elevación aún no
|
|
ElevInicio = null,
|
|
ElevFin = null,
|
|
ElevMin = null,
|
|
ElevMax = null,
|
|
MuestrasElevacion = null
|
|
});
|
|
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
await DrawWalkingRouteJsAsync("walk_full", linea, 5, versionPintado);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
_mensajeRuta = CapitalizarPalabrasClave(
|
|
$"Puedes realizar todo el recorrido andando: {FormatearDistancia(distancia)} aproximadamente."
|
|
);
|
|
|
|
_hayRutaValida = true;
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
StateHasChanged();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Pinta ruta desde alternativa.
|
|
/// </summary>
|
|
private async Task PintarRutaDesdeAlternativa(
|
|
Parada paradaOrigenCandidata,
|
|
Parada paradaDestinoCandidata,
|
|
RutaBusAlternativa alternativa,
|
|
int versionPintado)
|
|
{
|
|
if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null) return;
|
|
|
|
ResetEstadoRuta(limpiarMensaje: false);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
var tramosBus = alternativa.Tramos;
|
|
var lineasBus = RedBusServicio.ConstruirLineasBus(tramosBus, out var distanciaBusTotal);
|
|
|
|
Parada paradaInicioBus = paradaOrigenCandidata;
|
|
Parada paradaFinBus = paradaDestinoCandidata;
|
|
|
|
if (tramosBus.Count > 0 && lineasBus.Count > 0)
|
|
{
|
|
var primerTramo = tramosBus.First();
|
|
var ultimoTramo = tramosBus.Last();
|
|
paradaInicioBus = ResolverParadaFisica(
|
|
primerTramo.CodigoParadaInicio,
|
|
ObtenerPrimerPunto(lineasBus[0]),
|
|
paradaOrigenCandidata);
|
|
paradaFinBus = ResolverParadaFisica(
|
|
ultimoTramo.CodigoParadaFin,
|
|
ObtenerUltimoPunto(lineasBus[^1]),
|
|
paradaDestinoCandidata);
|
|
}
|
|
|
|
_paradaOrigenSeleccionada = paradaInicioBus;
|
|
_paradaDestinoSeleccionada = paradaFinBus;
|
|
|
|
_numeroTransbordos = CalcularTransbordosReales(alternativa);
|
|
|
|
_primerTransbordoseleccionado = null;
|
|
if (alternativa.CodigosParadasTransbordo is not null && alternativa.CodigosParadasTransbordo.Count > 0)
|
|
{
|
|
var codigoPrimera = alternativa.CodigosParadasTransbordo[0];
|
|
_primerTransbordoseleccionado = ResolverParadaFisica(codigoPrimera);
|
|
}
|
|
|
|
_distanciaAPieInicio = 0;
|
|
_distanciaAPieFin = 0;
|
|
_distanciaEnBus = 0;
|
|
_duracionTotalSegundos = 0;
|
|
|
|
var now = AhoraLocal();
|
|
var tCursor = now;
|
|
|
|
if (_usarHorariosTeoricos)
|
|
{
|
|
await EnsureHorariosLoadedAsync();
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
}
|
|
|
|
// -----------------------
|
|
// Pie 1
|
|
// -----------------------
|
|
var w1 = await WalkCached(_coordenadasOrigen[0], _coordenadasOrigen[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
if (w1.line is not null)
|
|
{
|
|
_distanciaAPieInicio = w1.dist;
|
|
|
|
var durPie1 = (w1.dist > 0) ? EstimarDuracionPieSegundos(w1.dist) : 0;
|
|
var tIni = tCursor;
|
|
tCursor = AddSecondsSafe(tCursor, durPie1);
|
|
|
|
_duracionTotalSegundos += durPie1;
|
|
|
|
_tramosRuta.Add(new DetalleTramoRuta
|
|
{
|
|
Tipo = "A pie",
|
|
Descripcion = CapitalizarPalabrasClave(
|
|
$"{HHmm(tIni)} -> {HHmm(tCursor)} - Desde tu punto de origen hasta la Parada \"{paradaInicioBus.Nombre}\" ({CodigoParadaFormateado(paradaInicioBus.Codigo)})."
|
|
),
|
|
DistanciaMetros = w1.dist,
|
|
DuracionSegundos = durPie1,
|
|
Linea = w1.line
|
|
});
|
|
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
await DrawWalkingRouteJsAsync("walk_1", w1.line, 4, versionPintado);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
}
|
|
|
|
// -----------------------
|
|
// Bus
|
|
// -----------------------
|
|
_distanciaEnBus = distanciaBusTotal;
|
|
var lineasUsadasVisuales = new List<string>(Math.Max(1, tramosBus.Count));
|
|
|
|
Parada paradaUltimaBus = paradaInicioBus;
|
|
var ultimoPuntoDibujado = w1.line is not null
|
|
? ObtenerUltimoPunto(w1.line)
|
|
: new[] { paradaInicioBus.Latitud, paradaInicioBus.Longitud };
|
|
|
|
for (int i = 0; i < tramosBus.Count; i++)
|
|
{
|
|
var tramo = tramosBus[i];
|
|
var esTransbordoReal = i > 0 && UtilidadesTransitoDbus.EsTransbordoReal(tramosBus[i - 1], tramo);
|
|
var infoVariante = tramo.Variante;
|
|
var lineaGeom = lineasBus[i];
|
|
|
|
string codParadaIni = infoVariante.CodigosParadas[tramo.IndiceInicio];
|
|
string codParadaFin = infoVariante.CodigosParadas[tramo.IndiceFin];
|
|
|
|
var puntoInicioTramo = ObtenerPrimerPunto(lineaGeom);
|
|
var puntoFinTramo = ObtenerUltimoPunto(lineaGeom);
|
|
|
|
var pIniTramo = ResolverParadaFisica(codParadaIni, puntoInicioTramo, i == 0 ? paradaInicioBus : paradaUltimaBus);
|
|
var pFinTramo = ResolverParadaFisica(codParadaFin, puntoFinTramo, i == tramosBus.Count - 1 ? paradaFinBus : paradaUltimaBus);
|
|
var distanciaUnion = CalcularDistanciaEntrePuntos(ultimoPuntoDibujado, puntoInicioTramo);
|
|
|
|
if (ultimoPuntoDibujado is not null && puntoInicioTramo is not null && distanciaUnion > 0)
|
|
{
|
|
if (distanciaUnion <= UmbralCosturaVisualMetros)
|
|
{
|
|
EstablecerPrimerPunto(lineaGeom, ultimoPuntoDibujado);
|
|
puntoInicioTramo = ultimoPuntoDibujado;
|
|
}
|
|
else if (!esTransbordoReal && i > 0 && distanciaUnion <= UmbralUnionPeatonalAutomaticaMetros)
|
|
{
|
|
EstablecerPrimerPunto(lineaGeom, ultimoPuntoDibujado);
|
|
puntoInicioTramo = ultimoPuntoDibujado;
|
|
}
|
|
else if ((i == 0 || esTransbordoReal) && distanciaUnion <= UmbralUnionPeatonalAutomaticaMetros)
|
|
{
|
|
var union = await WalkCached(
|
|
ultimoPuntoDibujado[0],
|
|
ultimoPuntoDibujado[1],
|
|
puntoInicioTramo[0],
|
|
puntoInicioTramo[1]);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
var lineaUnion = union.line ?? ConstruirLineaDirecta(ultimoPuntoDibujado, puntoInicioTramo);
|
|
var distanciaUnionReal = union.dist > 0 ? union.dist : distanciaUnion;
|
|
var duracionUnion = EstimarDuracionPieSegundos(distanciaUnionReal);
|
|
var tIniWalkUnion = tCursor;
|
|
tCursor = AddSecondsSafe(tCursor, duracionUnion);
|
|
|
|
_duracionTotalSegundos += duracionUnion;
|
|
if (i == 0)
|
|
_distanciaAPieInicio += distanciaUnionReal;
|
|
|
|
_tramosRuta.Add(new DetalleTramoRuta
|
|
{
|
|
Tipo = i == 0 ? "A pie (Ajuste acceso)" : "A pie (Transbordo)",
|
|
Descripcion = CapitalizarPalabrasClave(
|
|
i == 0
|
|
? $"{HHmm(tIniWalkUnion)} -> {HHmm(tCursor)} - Camina desde el final del acceso peatonal hasta la Parada \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
|
|
: $"{HHmm(tIniWalkUnion)} -> {HHmm(tCursor)} - Camina desde la Parada \"{paradaUltimaBus.Nombre}\" ({CodigoParadaFormateado(paradaUltimaBus.Codigo)}) hasta \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
|
|
),
|
|
DistanciaMetros = distanciaUnionReal,
|
|
DuracionSegundos = duracionUnion,
|
|
Linea = lineaUnion
|
|
});
|
|
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
await DrawWalkingRouteJsAsync($"walk_union_{i}", lineaUnion, 4, versionPintado);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaUnion);
|
|
}
|
|
}
|
|
|
|
var distanciaTransferenciaPendiente = CalcularDistanciaEntrePuntos(ultimoPuntoDibujado, puntoInicioTramo);
|
|
if (i > 0 &&
|
|
esTransbordoReal &&
|
|
ultimoPuntoDibujado is not null &&
|
|
puntoInicioTramo is not null &&
|
|
distanciaTransferenciaPendiente > UmbralUnionPeatonalAutomaticaMetros)
|
|
{
|
|
_mensajeRuta = "La alternativa seleccionada requiere un transbordo a pie demasiado largo. Recalcula la ruta para elegir otra combinacion.";
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
if (i > 0 &&
|
|
esTransbordoReal &&
|
|
ultimoPuntoDibujado is not null &&
|
|
puntoInicioTramo is not null &&
|
|
distanciaTransferenciaPendiente > UmbralCosturaVisualMetros)
|
|
{
|
|
var wt = await WalkCached(
|
|
ultimoPuntoDibujado[0],
|
|
ultimoPuntoDibujado[1],
|
|
puntoInicioTramo[0],
|
|
puntoInicioTramo[1]);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
var lineaTransferencia = wt.line ?? ConstruirLineaDirecta(ultimoPuntoDibujado, puntoInicioTramo);
|
|
var distanciaTransferenciaReal = wt.dist > 0 ? wt.dist : distanciaTransferenciaPendiente;
|
|
var duracionTransferencia = EstimarDuracionPieSegundos(distanciaTransferenciaReal);
|
|
var tIniWalkTransfer = tCursor;
|
|
tCursor = AddSecondsSafe(tCursor, duracionTransferencia);
|
|
|
|
_duracionTotalSegundos += duracionTransferencia;
|
|
|
|
_tramosRuta.Add(new DetalleTramoRuta
|
|
{
|
|
Tipo = "A pie (Transbordo)",
|
|
Descripcion = CapitalizarPalabrasClave(
|
|
$"{HHmm(tIniWalkTransfer)} -> {HHmm(tCursor)} - Camina desde la Parada \"{paradaUltimaBus.Nombre}\" ({CodigoParadaFormateado(paradaUltimaBus.Codigo)}) hasta \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
|
|
),
|
|
DistanciaMetros = distanciaTransferenciaReal,
|
|
DuracionSegundos = duracionTransferencia,
|
|
Linea = lineaTransferencia
|
|
});
|
|
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
await DrawWalkingRouteJsAsync($"walk_transfer_real_{i}", lineaTransferencia, 4, versionPintado);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaTransferencia);
|
|
if (ultimoPuntoDibujado is not null)
|
|
{
|
|
EstablecerPrimerPunto(lineaGeom, ultimoPuntoDibujado);
|
|
puntoInicioTramo = ultimoPuntoDibujado;
|
|
}
|
|
}
|
|
|
|
// -----------------------
|
|
// Transbordo andando
|
|
// -----------------------
|
|
if (false && i > 0 && ultimoPuntoDibujado is null)
|
|
{
|
|
if (!paradaUltimaBus.Codigo.Equals(codParadaIni, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var wt = await WalkCached(
|
|
paradaUltimaBus.Latitud, paradaUltimaBus.Longitud,
|
|
pIniTramo.Latitud, pIniTramo.Longitud);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
if (wt.line is not null)
|
|
{
|
|
var durTrans = (wt.dist > 0) ? EstimarDuracionPieSegundos(wt.dist) : 0;
|
|
var tIniWalk = tCursor;
|
|
tCursor = AddSecondsSafe(tCursor, durTrans);
|
|
|
|
_duracionTotalSegundos += durTrans;
|
|
|
|
_tramosRuta.Add(new DetalleTramoRuta
|
|
{
|
|
Tipo = "A pie (Transbordo)",
|
|
Descripcion = CapitalizarPalabrasClave(
|
|
$"{HHmm(tIniWalk)} -> {HHmm(tCursor)} - Camina desde la Parada \"{paradaUltimaBus.Nombre}\" ({CodigoParadaFormateado(paradaUltimaBus.Codigo)}) hasta \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
|
|
),
|
|
DistanciaMetros = wt.dist,
|
|
DuracionSegundos = durTrans,
|
|
Linea = wt.line
|
|
});
|
|
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
await DrawWalkingRouteJsAsync($"walk_transfer_{i}", wt.line, 4, versionPintado);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
}
|
|
}
|
|
}
|
|
|
|
// -----------------------
|
|
// Intentar horario real
|
|
// -----------------------
|
|
string codLineaItinSentido = CodigoLineaItinerarioSentido(tramo);
|
|
string codLineaItinSentidoMostrado = codLineaItinSentido;
|
|
string lineaCod = LineaCodigoStr(infoVariante);
|
|
int itin = infoVariante.CodigoItinerario;
|
|
string sentido = NormalizarSentidoCodigo(infoVariante.Sentido?.Codigo);
|
|
|
|
DateTime? depT = null;
|
|
DateTime? arrT = null;
|
|
|
|
if (_usarHorariosTeoricos && _horariosOk)
|
|
{
|
|
var primeraSalidaAceptable = esTransbordoReal
|
|
? tCursor.AddSeconds(Math.Clamp(_margenTransbordoSegundos, 0, 1800))
|
|
: tCursor;
|
|
var fecha = DateOnly.FromDateTime(primeraSalidaAceptable);
|
|
var horaMin = TimeOnly.FromDateTime(primeraSalidaAceptable);
|
|
|
|
if (Horarios.TryGetNextBusSegment(
|
|
codParadaIni,
|
|
codParadaFin,
|
|
lineaCod,
|
|
itin,
|
|
sentido,
|
|
fecha,
|
|
horaMin,
|
|
out var dep,
|
|
out var arr,
|
|
out var expIdReal))
|
|
{
|
|
depT = dep;
|
|
arrT = arr;
|
|
|
|
if (!string.IsNullOrWhiteSpace(expIdReal))
|
|
{
|
|
var codigoReal = Horarios.GetLineaItinerarioSentidoText(expIdReal, lineaCod);
|
|
if (!string.IsNullOrWhiteSpace(codigoReal) && !codigoReal.Contains('?'))
|
|
codLineaItinSentidoMostrado = codigoReal;
|
|
}
|
|
}
|
|
}
|
|
|
|
// -----------------------
|
|
// Si hay horario real: usarlo
|
|
// Si no: NO inventar horas
|
|
// -----------------------
|
|
if (depT is not null && arrT is not null)
|
|
{
|
|
void AgregarContinuidadCircularEnBus(DateTime inicio, DateTime fin, double duracionSegundos)
|
|
{
|
|
_tramosRuta.Add(new DetalleTramoRuta
|
|
{
|
|
Tipo = duracionSegundos > 1 ? "Espera en bus" : "Continuidad en bus",
|
|
Descripcion = CapitalizarPalabrasClave(
|
|
$"{HHmm(inicio)} -> {HHmm(fin)} - continuidad circular de la linea {codLineaItinSentidoMostrado}; Parada \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
|
|
),
|
|
DistanciaMetros = 0,
|
|
DuracionSegundos = Math.Max(0, duracionSegundos)
|
|
});
|
|
}
|
|
|
|
if (depT.Value > tCursor)
|
|
{
|
|
var wait = (depT.Value - tCursor).TotalSeconds;
|
|
if (wait > 1)
|
|
{
|
|
var tIniWait = tCursor;
|
|
tCursor = depT.Value;
|
|
var esperaEnBus = i > 0 && !esTransbordoReal;
|
|
|
|
_duracionTotalSegundos += wait;
|
|
|
|
if (esperaEnBus)
|
|
{
|
|
AgregarContinuidadCircularEnBus(tIniWait, tCursor, wait);
|
|
}
|
|
else
|
|
{
|
|
_tramosRuta.Add(new DetalleTramoRuta
|
|
{
|
|
Tipo = "Espera",
|
|
Descripcion = CapitalizarPalabrasClave(
|
|
$"{HHmm(tIniWait)} -> {HHmm(tCursor)} - En \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
|
|
),
|
|
DistanciaMetros = 0,
|
|
DuracionSegundos = wait
|
|
});
|
|
}
|
|
}
|
|
}
|
|
else if (i > 0 && !esTransbordoReal && Math.Abs((depT.Value - tCursor).TotalSeconds) <= 1)
|
|
{
|
|
AgregarContinuidadCircularEnBus(tCursor, tCursor, 0);
|
|
}
|
|
|
|
var durBus = (arrT.Value - depT.Value).TotalSeconds;
|
|
if (durBus < 0) durBus = 0;
|
|
|
|
_duracionTotalSegundos += durBus;
|
|
tCursor = arrT.Value;
|
|
|
|
_tramosRuta.Add(new DetalleTramoRuta
|
|
{
|
|
Tipo = "Bus",
|
|
Descripcion = CapitalizarPalabrasClave(
|
|
$"{HHmm(depT.Value)} -> {HHmm(arrT.Value)} - Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre})."
|
|
),
|
|
DistanciaMetros = 0,
|
|
DuracionSegundos = durBus,
|
|
VarianteBus = infoVariante
|
|
});
|
|
}
|
|
else
|
|
{
|
|
double distanciaTramo = 0;
|
|
for (int p = 1; p < lineaGeom.GetLength(0); p++)
|
|
{
|
|
distanciaTramo += CalcularDistanciaMetros(
|
|
lineaGeom[p - 1, 0], lineaGeom[p - 1, 1],
|
|
lineaGeom[p, 0], lineaGeom[p, 1]);
|
|
}
|
|
|
|
int paradasTramo = ContarParadasRecorridas(tramo);
|
|
double extraParadasSeg = paradasTramo * Math.Max(0, _segundosPorParadaBus);
|
|
double duracionBusSegundos = EstimarDuracionBusSegundos(distanciaTramo) + extraParadasSeg;
|
|
|
|
_duracionTotalSegundos += duracionBusSegundos;
|
|
tCursor = AddSecondsSafe(tCursor, duracionBusSegundos);
|
|
|
|
_tramosRuta.Add(new DetalleTramoRuta
|
|
{
|
|
Tipo = "Bus",
|
|
Descripcion = CapitalizarPalabrasClave(
|
|
$"Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre})."
|
|
),
|
|
DistanciaMetros = distanciaTramo,
|
|
DuracionSegundos = duracionBusSegundos,
|
|
VarianteBus = infoVariante
|
|
});
|
|
}
|
|
|
|
lineasUsadasVisuales.Add(codLineaItinSentidoMostrado);
|
|
|
|
string colorCss = RedBusServicio.ConvertirColorArgbACss(infoVariante.Linea.ColorArgb);
|
|
var fullId = $"full_{infoVariante.Linea.Codigo}_{infoVariante.CodigoItinerario}_{NormalizarSentidoCodigo(infoVariante.Sentido?.Codigo)}_{infoVariante.Variante.Codigo}_{i}_{tramo.IndiceInicio}_{tramo.IndiceFin}";
|
|
var dashBus = string.Equals(NormalizarSentidoCodigo(infoVariante.Sentido?.Codigo), "V", StringComparison.OrdinalIgnoreCase)
|
|
? DashArrayBusVuelta
|
|
: null;
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
await DrawRouteJsAsync(fullId, lineaGeom, colorCss, 5, 0.9, dashArray: dashBus, arrowEnd: true);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
paradaUltimaBus = pFinTramo;
|
|
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaGeom) ?? new[] { pFinTramo.Latitud, pFinTramo.Longitud };
|
|
}
|
|
|
|
// -----------------------
|
|
// Pie 2
|
|
// -----------------------
|
|
var w2 = await WalkCached(paradaFinBus.Latitud, paradaFinBus.Longitud, _coordenadasDestino[0], _coordenadasDestino[1]);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
if (w2.line is not null)
|
|
{
|
|
var inicioPieFinal = ObtenerPrimerPunto(w2.line);
|
|
var distanciaUnionFinal = CalcularDistanciaEntrePuntos(ultimoPuntoDibujado, inicioPieFinal);
|
|
if (ultimoPuntoDibujado is not null && inicioPieFinal is not null && distanciaUnionFinal > 0)
|
|
{
|
|
if (distanciaUnionFinal <= UmbralCosturaVisualMetros)
|
|
{
|
|
EstablecerPrimerPunto(w2.line, ultimoPuntoDibujado);
|
|
}
|
|
else if (distanciaUnionFinal <= UmbralUnionPeatonalAutomaticaMetros)
|
|
{
|
|
var unionFinal = await WalkCached(
|
|
ultimoPuntoDibujado[0],
|
|
ultimoPuntoDibujado[1],
|
|
inicioPieFinal[0],
|
|
inicioPieFinal[1]);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
|
|
var lineaUnionFinal = unionFinal.line ?? ConstruirLineaDirecta(ultimoPuntoDibujado, inicioPieFinal);
|
|
var distanciaUnionFinalReal = unionFinal.dist > 0 ? unionFinal.dist : distanciaUnionFinal;
|
|
var duracionUnionFinal = EstimarDuracionPieSegundos(distanciaUnionFinalReal);
|
|
var tIniUnionFinal = tCursor;
|
|
tCursor = AddSecondsSafe(tCursor, duracionUnionFinal);
|
|
|
|
_duracionTotalSegundos += duracionUnionFinal;
|
|
_distanciaAPieFin += distanciaUnionFinalReal;
|
|
|
|
_tramosRuta.Add(new DetalleTramoRuta
|
|
{
|
|
Tipo = "A pie (Ajuste salida)",
|
|
Descripcion = CapitalizarPalabrasClave(
|
|
$"{HHmm(tIniUnionFinal)} -> {HHmm(tCursor)} - Camina desde el punto real de bajada hasta la Parada \"{paradaFinBus.Nombre}\" ({CodigoParadaFormateado(paradaFinBus.Codigo)})."
|
|
),
|
|
DistanciaMetros = distanciaUnionFinalReal,
|
|
DuracionSegundos = duracionUnionFinal,
|
|
Linea = lineaUnionFinal
|
|
});
|
|
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
await DrawWalkingRouteJsAsync("walk_union_final", lineaUnionFinal, 4, versionPintado);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
EstablecerPrimerPunto(w2.line, ObtenerUltimoPunto(lineaUnionFinal));
|
|
}
|
|
}
|
|
|
|
if (w2.dist > 0)
|
|
_distanciaAPieFin += w2.dist;
|
|
|
|
var durPie2 = (w2.dist > 0) ? EstimarDuracionPieSegundos(w2.dist) : 0;
|
|
var tIni2 = tCursor;
|
|
tCursor = AddSecondsSafe(tCursor, durPie2);
|
|
|
|
_duracionTotalSegundos += durPie2;
|
|
|
|
_tramosRuta.Add(new DetalleTramoRuta
|
|
{
|
|
Tipo = "A pie",
|
|
Descripcion = CapitalizarPalabrasClave(
|
|
$"{HHmm(tIni2)} -> {HHmm(tCursor)} - Desde la Parada \"{paradaFinBus.Nombre}\" ({CodigoParadaFormateado(paradaFinBus.Codigo)}) hasta tu destino."
|
|
),
|
|
DistanciaMetros = w2.dist,
|
|
DuracionSegundos = durPie2,
|
|
Linea = w2.line
|
|
});
|
|
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
await DrawWalkingRouteJsAsync("walk_2", w2.line, 4, versionPintado);
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
}
|
|
|
|
var lineasUsadas = lineasUsadasVisuales.Count > 0
|
|
? CompactarCodigosLineaConsecutivos(lineasUsadasVisuales)
|
|
: CompactarCodigosLineaConsecutivos(tramosBus.Select(t => CodigoLineaItinerarioSentido(t)));
|
|
|
|
string txtLineas = (lineasUsadas.Count == 0)
|
|
? ""
|
|
: $" Lineas: {string.Join(", ", lineasUsadas)}.";
|
|
|
|
int tramosCount = tramosBus?.Count ?? 0;
|
|
int trans = UtilidadesTransitoDbus.ContarTransbordosReales(tramosBus);
|
|
|
|
_mensajeRuta = CapitalizarPalabrasClave(
|
|
$"Inicio {HHmm(now)}. Camina {FormatearDistanciaDetalleRuta(_distanciaAPieInicio)} hasta \"{_paradaOrigenSeleccionada?.Nombre}\" ({CodigoParadaFormateado(_paradaOrigenSeleccionada?.Codigo)}). " +
|
|
$"Luego viaja en bus {FormatearDistanciaDetalleRuta(_distanciaEnBus)}" +
|
|
(tramosCount > 0 ? $" en {tramosCount} tramo(s)" : "") +
|
|
(trans == 0 ? "." : $" con {trans} transbordo(s).") +
|
|
txtLineas + " " +
|
|
$"Finalmente camina {FormatearDistanciaDetalleRuta(_distanciaAPieFin)} hasta tu destino."
|
|
);
|
|
|
|
_hayRutaValida = true;
|
|
ThrowIfPintadoObsoleto(versionPintado);
|
|
StateHasChanged();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Recalcula con parametros.
|
|
/// </summary>
|
|
private async Task RecalcularConParametros()
|
|
{
|
|
if (_coordenadasOrigen is null || _coordenadasDestino is null) return;
|
|
await PersistirConfiguracionAsync();
|
|
|
|
if (_mapa is not null)
|
|
{
|
|
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
|
|
await ClearRoutesJsAsync();
|
|
}
|
|
await CerrarPopupsAsync();
|
|
|
|
await CalcularRutaCompletaAsync();
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reinicia seleccion.
|
|
/// </summary>
|
|
private async Task ReiniciarSeleccion()
|
|
{
|
|
await CerrarTodasLasPreviewsAsync();
|
|
|
|
await CerrarPopupsAsync();
|
|
|
|
_ctsCalculo?.Cancel();
|
|
IniciarNuevaVersionPintadoRuta();
|
|
|
|
_coordenadasOrigen = null;
|
|
_coordenadasDestino = null;
|
|
_textoBusquedaOrigen = string.Empty;
|
|
_textoBusquedaDestino = string.Empty;
|
|
_ubicacionOrigenSeleccionada = null;
|
|
_ubicacionDestinoSeleccionada = null;
|
|
_sugerenciasBusquedaOrigen.Clear();
|
|
_sugerenciasBusquedaDestino.Clear();
|
|
_campoBusquedaActivo = null;
|
|
|
|
_mensajeRuta = string.Empty;
|
|
_hayRutaValida = false;
|
|
_paradaOrigenSeleccionada = null;
|
|
_paradaDestinoSeleccionada = null;
|
|
_primerTransbordoseleccionado = null;
|
|
_numeroTransbordos = 0;
|
|
_distanciaAPieInicio = _distanciaAPieFin = _distanciaEnBus = 0;
|
|
_paradaClicada = null;
|
|
_tramosRuta.Clear();
|
|
_duracionTotalSegundos = 0;
|
|
_opcionesRuta.Clear();
|
|
_indiceAlternativaSeleccionada = 0;
|
|
|
|
if (_mapa != null)
|
|
{
|
|
await ClearRoutesJsAsync();
|
|
await ClearEndpointMarkersJsAsync();
|
|
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
|
|
|
|
await _mapa.Geometric.DisplayPointsFromArray.deleteAllPoints();
|
|
if (_mostrarParadas)
|
|
await DibujarParadasAsync();
|
|
else
|
|
await OcultarParadasAsync(); // (repone origen/destino si hubiera, aquí ya no hay, pero ok)
|
|
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
// =========================================================
|
|
// Distancia rápida
|
|
// =========================================================
|
|
/// <summary>
|
|
/// Calcula distancia metros.
|
|
/// </summary>
|
|
private static double CalcularDistanciaMetros(double lat1, double lon1, double lat2, double lon2)
|
|
{
|
|
const double R = 6371000.0; // metros
|
|
static double ToRad(double x) => x * Math.PI / 180.0;
|
|
|
|
var dLat = ToRad(lat2 - lat1);
|
|
var dLon = ToRad(lon2 - lon1);
|
|
|
|
var a =
|
|
Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
|
|
Math.Cos(ToRad(lat1)) * Math.Cos(ToRad(lat2)) *
|
|
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
|
|
|
|
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
|
|
return R * c;
|
|
}
|
|
}
|