- Añadido interpreacion de tipologia de suelo en segmentos andando - Persistencia modo andando - Transbordo penalizado a 6 - Añadidos varias zonas de prohibicion de poligonos
544 lines
17 KiB
C#
544 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json.Serialization;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Configuration;
|
|
using RutasDBUS.Modelos.Planificacion;
|
|
|
|
namespace RutasDBUS.Servicios.Caminata;
|
|
|
|
public class ClienteValhalla : IClienteValhalla
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly string _baseUrl;
|
|
|
|
/// <summary>
|
|
/// Inicializa una nueva instancia de la clase ClienteValhalla.
|
|
/// </summary>
|
|
public ClienteValhalla(HttpClient httpClient, IConfiguration configuracion)
|
|
{
|
|
_httpClient = httpClient;
|
|
|
|
_baseUrl = configuracion["Caminata:Valhalla:BaseUrl"]
|
|
?? throw new InvalidOperationException(
|
|
"Falta la configuración 'Caminata:Valhalla:BaseUrl' en appsettings.json.");
|
|
|
|
_baseUrl = _baseUrl.Trim().TrimEnd('/');
|
|
}
|
|
|
|
public async Task<(double[,]? Linea, double DistanciaMetros, double DuracionSegundos)> ObtenerRutaAsync(
|
|
double latitudDesde,
|
|
double longitudDesde,
|
|
double latitudHasta,
|
|
double longitudHasta,
|
|
string costing = "pedestrian",
|
|
List<List<double[]>>? excludePolygons = null)
|
|
{
|
|
var cuerpo = new SolicitudValhalla
|
|
{
|
|
Ubicaciones = new List<LocalizacionValhalla>
|
|
{
|
|
new() { Latitud = latitudDesde, Longitud = longitudDesde },
|
|
new() { Latitud = latitudHasta, Longitud = longitudHasta }
|
|
},
|
|
PerfilCoste = string.IsNullOrWhiteSpace(costing) ? "pedestrian" : costing,
|
|
FormatoGeometria = "polyline6",
|
|
PoligonosExclusion = excludePolygons,
|
|
OpcionesIndicaciones = new OpcionesIndicacionesValhalla
|
|
{
|
|
Idioma = "es-ES",
|
|
Unidades = "kilometers"
|
|
}
|
|
};
|
|
|
|
HttpResponseMessage respuesta;
|
|
try
|
|
{
|
|
respuesta = await _httpClient.PostAsJsonAsync($"{_baseUrl}/route", cuerpo);
|
|
}
|
|
catch
|
|
{
|
|
return (null, 0, 0);
|
|
}
|
|
|
|
if (!respuesta.IsSuccessStatusCode)
|
|
return (null, 0, 0);
|
|
|
|
RespuestaRutaValhalla? data;
|
|
try
|
|
{
|
|
data = await respuesta.Content.ReadFromJsonAsync<RespuestaRutaValhalla>();
|
|
}
|
|
catch
|
|
{
|
|
return (null, 0, 0);
|
|
}
|
|
|
|
var viaje = data?.Viaje;
|
|
if (viaje is null || viaje.Tramos is null || viaje.Tramos.Count == 0)
|
|
return (null, 0, 0);
|
|
|
|
var forma = viaje.Tramos[0]?.Forma;
|
|
if (string.IsNullOrWhiteSpace(forma))
|
|
return (null, 0, 0);
|
|
|
|
var coords = DecodificarPolyline6(forma);
|
|
if (coords.Count == 0)
|
|
return (null, 0, 0);
|
|
|
|
var linea = new double[coords.Count, 2];
|
|
for (int i = 0; i < coords.Count; i++)
|
|
{
|
|
linea[i, 0] = coords[i].Latitud;
|
|
linea[i, 1] = coords[i].Longitud;
|
|
}
|
|
|
|
// summary.length depende de units
|
|
double longitud = viaje.Resumen?.Longitud ?? 0.0;
|
|
string unidades = (viaje.Unidades ?? "").Trim().ToLowerInvariant();
|
|
|
|
double distanciaMetros = unidades switch
|
|
{
|
|
"miles" => longitud * 1609.344,
|
|
_ => longitud * 1000.0 // "kilometers" o desconocido -> km
|
|
};
|
|
|
|
double duracionSegundos = viaje.Resumen?.Tiempo ?? 0.0;
|
|
|
|
return (
|
|
AnclarExtremosLinea(linea, latitudDesde, longitudDesde, latitudHasta, longitudHasta),
|
|
distanciaMetros,
|
|
duracionSegundos);
|
|
}
|
|
|
|
public async Task<InformacionSueloTramo?> ObtenerInformacionSueloAsync(
|
|
double[,] linea,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var forma = ConstruirFormaTraza(linea);
|
|
if (forma.Count < 2)
|
|
return new InformacionSueloTramo
|
|
{
|
|
Disponible = false,
|
|
Mensaje = "No hay geometría suficiente para consultar la superficie."
|
|
};
|
|
|
|
var cuerpo = new SolicitudAtributosSuperficieValhalla
|
|
{
|
|
Forma = forma,
|
|
PerfilCoste = "pedestrian",
|
|
AjusteForma = "walk_or_snap",
|
|
Filtros = new FiltrosAtributosValhalla
|
|
{
|
|
Atributos = new List<string>
|
|
{
|
|
"edge.length",
|
|
"edge.surface",
|
|
"edge.unpaved",
|
|
"edge.use",
|
|
"edge.way_id",
|
|
"edge.begin_shape_index",
|
|
"edge.end_shape_index"
|
|
}
|
|
},
|
|
OpcionesIndicaciones = new OpcionesIndicacionesValhalla
|
|
{
|
|
Idioma = "es-ES",
|
|
Unidades = "kilometers"
|
|
}
|
|
};
|
|
|
|
try
|
|
{
|
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
timeout.CancelAfter(TimeSpan.FromSeconds(8));
|
|
|
|
using var respuesta = await _httpClient.PostAsJsonAsync(
|
|
$"{_baseUrl}/trace_attributes",
|
|
cuerpo,
|
|
timeout.Token);
|
|
|
|
if (!respuesta.IsSuccessStatusCode)
|
|
return CrearInformacionSueloNoDisponible("El servicio de superficies no ha respondido correctamente.");
|
|
|
|
var data = await respuesta.Content.ReadFromJsonAsync<RespuestaAtributosSuperficieValhalla>(timeout.Token);
|
|
return ConvertirInformacionSuelo(data);
|
|
}
|
|
catch
|
|
{
|
|
return CrearInformacionSueloNoDisponible("No se ha podido consultar la superficie del tramo.");
|
|
}
|
|
}
|
|
|
|
private static List<PuntoFormaValhalla> ConstruirFormaTraza(double[,] linea)
|
|
{
|
|
var forma = new List<PuntoFormaValhalla>();
|
|
if (linea is null || linea.Rank != 2 || linea.GetLength(1) < 2)
|
|
return forma;
|
|
|
|
for (var i = 0; i < linea.GetLength(0); i++)
|
|
{
|
|
var latitud = linea[i, 0];
|
|
var longitud = linea[i, 1];
|
|
if (!double.IsFinite(latitud) || !double.IsFinite(longitud))
|
|
continue;
|
|
|
|
if (forma.Count > 0
|
|
&& Math.Abs(forma[^1].Latitud - latitud) < 0.00000001
|
|
&& Math.Abs(forma[^1].Longitud - longitud) < 0.00000001)
|
|
continue;
|
|
|
|
forma.Add(new PuntoFormaValhalla { Latitud = latitud, Longitud = longitud });
|
|
}
|
|
|
|
return forma;
|
|
}
|
|
|
|
private static InformacionSueloTramo ConvertirInformacionSuelo(RespuestaAtributosSuperficieValhalla? data)
|
|
{
|
|
if (data?.Aristas is null || data.Aristas.Count == 0)
|
|
return CrearInformacionSueloNoDisponible("No hay datos de superficie para este tramo.");
|
|
|
|
var segmentos = new List<SegmentoSueloTramo>();
|
|
foreach (var arista in data.Aristas)
|
|
{
|
|
var distanciaMetros = ConvertirLongitudMetros(arista.Longitud, data.Unidades);
|
|
if (distanciaMetros <= 0)
|
|
continue;
|
|
|
|
var superficie = EtiquetaSuperficie(arista.Superficie, arista.SinPavimentar);
|
|
var uso = EtiquetaUso(arista.Uso);
|
|
|
|
if (segmentos.Count > 0 && EsMismaSuperficie(segmentos[^1], superficie, uso, arista.SinPavimentar))
|
|
{
|
|
var anterior = segmentos[^1];
|
|
segmentos[^1] = new SegmentoSueloTramo
|
|
{
|
|
DistanciaMetros = anterior.DistanciaMetros + distanciaMetros,
|
|
Superficie = anterior.Superficie,
|
|
Uso = anterior.Uso,
|
|
SinPavimentar = anterior.SinPavimentar,
|
|
IdCamino = anterior.IdCamino == arista.IdCamino ? anterior.IdCamino : null,
|
|
IndiceInicioForma = anterior.IndiceInicioForma,
|
|
IndiceFinForma = arista.IndiceFinForma ?? anterior.IndiceFinForma
|
|
};
|
|
}
|
|
else
|
|
{
|
|
segmentos.Add(new SegmentoSueloTramo
|
|
{
|
|
DistanciaMetros = distanciaMetros,
|
|
Superficie = superficie,
|
|
Uso = uso,
|
|
SinPavimentar = arista.SinPavimentar,
|
|
IdCamino = arista.IdCamino,
|
|
IndiceInicioForma = arista.IndiceInicioForma,
|
|
IndiceFinForma = arista.IndiceFinForma
|
|
});
|
|
}
|
|
}
|
|
|
|
if (segmentos.Count == 0)
|
|
return CrearInformacionSueloNoDisponible("No hay datos de superficie para este tramo.");
|
|
|
|
return new InformacionSueloTramo
|
|
{
|
|
Disponible = true,
|
|
Segmentos = segmentos
|
|
};
|
|
}
|
|
|
|
private static bool EsMismaSuperficie(
|
|
SegmentoSueloTramo anterior,
|
|
string superficie,
|
|
string? uso,
|
|
bool? sinPavimentar)
|
|
=> string.Equals(anterior.Superficie, superficie, StringComparison.Ordinal)
|
|
&& string.Equals(anterior.Uso, uso, StringComparison.Ordinal)
|
|
&& anterior.SinPavimentar == sinPavimentar;
|
|
|
|
private static double ConvertirLongitudMetros(double? longitud, string? unidades)
|
|
{
|
|
if (longitud is not double valor || !double.IsFinite(valor) || valor <= 0)
|
|
return 0;
|
|
|
|
return string.Equals(unidades, "miles", StringComparison.OrdinalIgnoreCase)
|
|
? valor * 1609.344
|
|
: valor * 1000.0;
|
|
}
|
|
|
|
private static string EtiquetaSuperficie(string? superficie, bool? sinPavimentar)
|
|
=> (superficie ?? "").Trim().ToLowerInvariant() switch
|
|
{
|
|
"paved_smooth" => "Pavimento liso",
|
|
"paved" => "Pavimento",
|
|
"paved_rough" => "Pavimento rugoso",
|
|
"compacted" => "Tierra compactada",
|
|
"dirt" => "Tierra",
|
|
"gravel" => "Grava",
|
|
"path" => "Senda",
|
|
"impassable" => "Intransitable",
|
|
_ when sinPavimentar == true => "No pavimentado",
|
|
_ => "Sin datos"
|
|
};
|
|
|
|
private static string? EtiquetaUso(string? uso)
|
|
=> (uso ?? "").Trim().ToLowerInvariant() switch
|
|
{
|
|
"sidewalk" => "Acera",
|
|
"footway" => "Camino peatonal",
|
|
"steps" => "Escaleras",
|
|
"path" => "Senda",
|
|
"track" => "Pista",
|
|
"road" => "Calzada",
|
|
"service_other" => "Vía de servicio",
|
|
"other" => "Otro",
|
|
"" => null,
|
|
var valor => valor
|
|
};
|
|
|
|
private static InformacionSueloTramo CrearInformacionSueloNoDisponible(string mensaje)
|
|
=> new()
|
|
{
|
|
Disponible = false,
|
|
Mensaje = mensaje
|
|
};
|
|
|
|
// =======================
|
|
// Modelos request/response
|
|
// =======================
|
|
|
|
private sealed class SolicitudValhalla
|
|
{
|
|
[JsonPropertyName("locations")]
|
|
public List<LocalizacionValhalla> Ubicaciones { get; set; } = new();
|
|
|
|
[JsonPropertyName("costing")]
|
|
public string PerfilCoste { get; set; } = "pedestrian";
|
|
|
|
[JsonPropertyName("shape_format")]
|
|
public string FormatoGeometria { get; set; } = "polyline6";
|
|
|
|
[JsonPropertyName("exclude_polygons")]
|
|
public List<List<double[]>>? PoligonosExclusion { get; set; }
|
|
|
|
[JsonPropertyName("directions_options")]
|
|
public OpcionesIndicacionesValhalla OpcionesIndicaciones { get; set; } = new();
|
|
}
|
|
|
|
private sealed class SolicitudAtributosSuperficieValhalla
|
|
{
|
|
[JsonPropertyName("shape")]
|
|
public List<PuntoFormaValhalla> Forma { get; set; } = new();
|
|
|
|
[JsonPropertyName("costing")]
|
|
public string PerfilCoste { get; set; } = "pedestrian";
|
|
|
|
[JsonPropertyName("shape_match")]
|
|
public string AjusteForma { get; set; } = "walk_or_snap";
|
|
|
|
[JsonPropertyName("filters")]
|
|
public FiltrosAtributosValhalla Filtros { get; set; } = new();
|
|
|
|
[JsonPropertyName("directions_options")]
|
|
public OpcionesIndicacionesValhalla OpcionesIndicaciones { get; set; } = new();
|
|
}
|
|
|
|
private sealed class PuntoFormaValhalla
|
|
{
|
|
[JsonPropertyName("lat")]
|
|
public double Latitud { get; set; }
|
|
|
|
[JsonPropertyName("lon")]
|
|
public double Longitud { get; set; }
|
|
}
|
|
|
|
private sealed class FiltrosAtributosValhalla
|
|
{
|
|
[JsonPropertyName("attributes")]
|
|
public List<string> Atributos { get; set; } = new();
|
|
|
|
[JsonPropertyName("action")]
|
|
public string Accion { get; set; } = "include";
|
|
}
|
|
|
|
private sealed class RespuestaAtributosSuperficieValhalla
|
|
{
|
|
[JsonPropertyName("edges")]
|
|
public List<AristaAtributosValhalla> Aristas { get; set; } = new();
|
|
|
|
[JsonPropertyName("units")]
|
|
public string? Unidades { get; set; }
|
|
}
|
|
|
|
private sealed class AristaAtributosValhalla
|
|
{
|
|
[JsonPropertyName("length")]
|
|
public double? Longitud { get; set; }
|
|
|
|
[JsonPropertyName("surface")]
|
|
public string? Superficie { get; set; }
|
|
|
|
[JsonPropertyName("unpaved")]
|
|
public bool? SinPavimentar { get; set; }
|
|
|
|
[JsonPropertyName("use")]
|
|
public string? Uso { get; set; }
|
|
|
|
[JsonPropertyName("way_id")]
|
|
public long? IdCamino { get; set; }
|
|
|
|
[JsonPropertyName("begin_shape_index")]
|
|
public int? IndiceInicioForma { get; set; }
|
|
|
|
[JsonPropertyName("end_shape_index")]
|
|
public int? IndiceFinForma { get; set; }
|
|
}
|
|
|
|
private sealed class LocalizacionValhalla
|
|
{
|
|
[JsonPropertyName("lat")]
|
|
public double Latitud { get; set; }
|
|
|
|
[JsonPropertyName("lon")]
|
|
public double Longitud { get; set; }
|
|
}
|
|
|
|
private sealed class OpcionesIndicacionesValhalla
|
|
{
|
|
[JsonPropertyName("language")]
|
|
public string Idioma { get; set; } = "es-ES";
|
|
|
|
[JsonPropertyName("units")]
|
|
public string Unidades { get; set; } = "kilometers";
|
|
}
|
|
|
|
private sealed class RespuestaRutaValhalla
|
|
{
|
|
[JsonPropertyName("trip")]
|
|
public ViajeValhalla? Viaje { get; set; }
|
|
}
|
|
|
|
private sealed class ViajeValhalla
|
|
{
|
|
[JsonPropertyName("summary")]
|
|
public ResumenValhalla? Resumen { get; set; }
|
|
|
|
[JsonPropertyName("legs")]
|
|
public List<TramoValhalla?> Tramos { get; set; } = new();
|
|
|
|
[JsonPropertyName("units")]
|
|
public string? Unidades { get; set; } // "kilometers" / "miles"
|
|
}
|
|
|
|
private sealed class ResumenValhalla
|
|
{
|
|
[JsonPropertyName("length")]
|
|
public double Longitud { get; set; } // en km si units=kilometers
|
|
|
|
[JsonPropertyName("time")]
|
|
public double Tiempo { get; set; } // segundos
|
|
}
|
|
|
|
private sealed class TramoValhalla
|
|
{
|
|
[JsonPropertyName("shape")]
|
|
public string? Forma { get; set; } // polyline6
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
private readonly record struct Coordenada(double Latitud, double Longitud);
|
|
|
|
/// <summary>
|
|
/// Decodifica polyline6 (factor 1e6).
|
|
/// </summary>
|
|
private static List<Coordenada> DecodificarPolyline6(string encoded)
|
|
{
|
|
var coords = new List<Coordenada>();
|
|
if (string.IsNullOrEmpty(encoded)) return coords;
|
|
|
|
int index = 0;
|
|
int lat = 0, lon = 0;
|
|
|
|
try
|
|
{
|
|
while (index < encoded.Length)
|
|
{
|
|
lat += LeerSiguienteValor(encoded, ref index);
|
|
lon += LeerSiguienteValor(encoded, ref index);
|
|
|
|
coords.Add(new Coordenada(lat / 1e6, lon / 1e6));
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// si llega corrupto o incompleto, devolvemos lo que haya (o vacío)
|
|
if (coords.Count == 0) return new List<Coordenada>();
|
|
}
|
|
|
|
return coords;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lee siguiente valor.
|
|
/// </summary>
|
|
private static int LeerSiguienteValor(string encoded, ref int index)
|
|
{
|
|
int result = 0;
|
|
int shift = 0;
|
|
int b;
|
|
|
|
do
|
|
{
|
|
if (index >= encoded.Length) throw new InvalidOperationException("Polyline incompleta.");
|
|
b = encoded[index++] - 63;
|
|
result |= (b & 0x1f) << shift;
|
|
shift += 5;
|
|
}
|
|
while (b >= 0x20);
|
|
|
|
// zigzag decode
|
|
return (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fuerza que la geometria empiece y termine exactamente en los puntos solicitados.
|
|
/// </summary>
|
|
private static double[,] AnclarExtremosLinea(
|
|
double[,] linea,
|
|
double latitudDesde,
|
|
double longitudDesde,
|
|
double latitudHasta,
|
|
double longitudHasta)
|
|
{
|
|
var puntos = linea.GetLength(0);
|
|
if (puntos <= 0)
|
|
{
|
|
return new double[,]
|
|
{
|
|
{ latitudDesde, longitudDesde },
|
|
{ latitudHasta, longitudHasta }
|
|
};
|
|
}
|
|
|
|
if (puntos == 1)
|
|
{
|
|
return new double[,]
|
|
{
|
|
{ latitudDesde, longitudDesde },
|
|
{ latitudHasta, longitudHasta }
|
|
};
|
|
}
|
|
|
|
linea[0, 0] = latitudDesde;
|
|
linea[0, 1] = longitudDesde;
|
|
linea[puntos - 1, 0] = latitudHasta;
|
|
linea[puntos - 1, 1] = longitudHasta;
|
|
return linea;
|
|
}
|
|
}
|