Files
RutasDBUS/RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs

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;
}
}