- Separado el control de poligonos excluyentes
- 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
This commit is contained in:
2
.serena/.gitignore
vendored
Normal file
2
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/cache
|
||||
/project.local.yml
|
||||
169
.serena/project.yml
Normal file
169
.serena/project.yml
Normal file
@@ -0,0 +1,169 @@
|
||||
# the name by which the project can be referenced within Serena/when chatting with the LLM.
|
||||
project_name: "DBUS.net"
|
||||
|
||||
# list of language servers to start when using the LSP backend; choose from:
|
||||
# ada al angular ansible bash
|
||||
# bsl clojure cpp cpp_ccls crystal
|
||||
# csharp csharp_omnisharp cue dart deno
|
||||
# elixir elm erlang fortran fsharp
|
||||
# gdscript gleam go groovy haskell
|
||||
# haxe hlsl html java json
|
||||
# julia kotlin latex lean4 lua
|
||||
# luau markdown matlab msl nextflow
|
||||
# nix ocaml pascal perl php
|
||||
# php_phpactor php_phpantom powershell python python_basedpyright
|
||||
# python_jedi python_pyrefly python_ty qml r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# scss solidity svelte swift systemverilog
|
||||
# terraform toml typescript typescript_vts vue
|
||||
# wolfram yaml zig
|
||||
# (This list may be outdated; generated with scripts/print_language_list.py;
|
||||
# For the current list, see values of the LanguageServerId enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
|
||||
# For some languages, there are several alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
|
||||
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
|
||||
# - For Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH)
|
||||
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some language servers require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple language servers, the first language server that supports a given file will be used for that file.
|
||||
# The first language server is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
language_servers:
|
||||
- csharp
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
|
||||
# the command runs in the project root directory and is only executed if the project is trusted
|
||||
# (see trusted_project_path_patterns in the global configuration).
|
||||
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
|
||||
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
|
||||
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
|
||||
# example: activation_command: "npx nx run-many -t build"
|
||||
activation_command:
|
||||
|
||||
# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
|
||||
# must be a positive number.
|
||||
activation_command_timeout: 180.0
|
||||
|
||||
# line ending convention to use when writing source files.
|
||||
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||
line_ending:
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# advanced configuration option allowing to configure language server-specific options.
|
||||
# Maps the language key to the options.
|
||||
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
|
||||
ls_specific_settings: {}
|
||||
|
||||
# list of workspace folder paths (LSP backend only).
|
||||
# These folders will be used to build up Serena's symbol index.
|
||||
# Paths must be within the project root and should thus be relative to the project root.
|
||||
# Furthermore, the paths should not be filtered by ignore settings.
|
||||
# Default setting: The entire project root folder (".") is considered.
|
||||
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
|
||||
# ls_workspace_folders:
|
||||
# - "./subproject1"
|
||||
# - "./subproject2"
|
||||
ls_workspace_folders:
|
||||
- "."
|
||||
|
||||
# list of additional workspace folder paths for cross-package reference support.
|
||||
# Paths can be absolute or relative to the project root.
|
||||
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
|
||||
# symbols and references across package boundaries, but these folders are not indexed by Serena,
|
||||
# i.e. the respective symbols will not be found using Serena's symbol search tools.
|
||||
# Example:
|
||||
# additional_workspace_folders:
|
||||
# - ../sibling-package
|
||||
# - ../shared-lib
|
||||
ls_additional_workspace_folders: []
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
|
||||
# Example:
|
||||
# ignored_paths:
|
||||
# - "examples/**"
|
||||
# - ".worktrees/**"
|
||||
# - "**/bin/**"
|
||||
# - "**/obj/**"
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude.
|
||||
# This extends the existing exclusions (e.g. from the global configuration)
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
|
||||
# This extends the existing inclusions (e.g. from the global configuration).
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
fixed_tools: []
|
||||
|
||||
# list of mode names that are to be activated by default, overriding the setting in the global configuration.
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
|
||||
# for this project.
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
default_modes:
|
||||
|
||||
# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
added_modes:
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
# such as docstrings or parameter information.
|
||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||
# If null or missing, use the setting from the global configuration.
|
||||
symbol_info_budget:
|
||||
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
|
||||
# list of regex patterns for memories to completely ignore.
|
||||
# Matching memories will not appear in list_memories or activate_project output
|
||||
# and cannot be accessed via read_memory or write_memory.
|
||||
# To access ignored memory files, use the read_file tool on the raw file path.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
# Example: ["_archive/.*", "_episodes/.*"]
|
||||
ignored_memory_patterns: []
|
||||
@@ -506,13 +506,16 @@ public partial class PlanificadorRutas : ComponentBase, IDisposable
|
||||
|
||||
private async Task<(double[,]? line, double dist, double dur)> WalkValhallaCached(double lat1, double lon1, double lat2, double lon2)
|
||||
{
|
||||
var poligonos = ObtenerPoligonosExclusionCaminata();
|
||||
var key = WalkKey("valhalla", lat1, lon1, lat2, lon2);
|
||||
if (poligonos is not null) key += "|" + JsonSerializer.Serialize(poligonos);
|
||||
if (_walkCache.TryGetValue(key, out var v))
|
||||
return v;
|
||||
|
||||
try
|
||||
{
|
||||
var (line, dist, dur) = await ClienteValhalla.ObtenerRutaAsync(lat1, lon1, lat2, lon2, costing: "pedestrian");
|
||||
var (line, dist, dur) = await ClienteValhalla.ObtenerRutaAsync(lat1, lon1, lat2, lon2,
|
||||
costing: "pedestrian", excludePolygons: poligonos);
|
||||
|
||||
// ✅ Solo cachear si es un resultado "bueno"
|
||||
if (line is not null && dist > 0)
|
||||
|
||||
@@ -68,12 +68,12 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
private double _factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
|
||||
private double _parametroPenalizacionDistanciaPieMetros = ParametroPenalizacionDistanciaPieMetrosPorDefecto;
|
||||
private double _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
||||
private double _pesoTransbordoRankingMinutos = 4.0;
|
||||
private double _pesoTransbordoRankingMinutos = 6.0;
|
||||
private double _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
|
||||
private string _factorPenalizacionDistanciaPieTextoEdicion = "1.8";
|
||||
private string _parametroPenalizacionDistanciaPieTextoEdicion = "200";
|
||||
private string _coeficienteEsfuerzoAlturaTextoEdicion = "5.20";
|
||||
private string _pesoTransbordoRankingTextoEdicion = "4,0";
|
||||
private string _pesoTransbordoRankingTextoEdicion = "6,0";
|
||||
private string _factorEquilibradoAndarBusTextoEdicion = "6";
|
||||
private double _velocidadBusKmH = 16.0;
|
||||
private int _minutosTransbordo = 5;
|
||||
|
||||
@@ -1522,6 +1522,8 @@ public partial class PlanificadorRutas
|
||||
_coordenadasOrigen = new[] { item.OrigenLatitud, item.OrigenLongitud };
|
||||
_coordenadasDestino = new[] { item.DestinoLatitud, item.DestinoLongitud };
|
||||
|
||||
if (_modoConfiguracionHistorial == ModoConfiguracionHistorial.Predeterminada)
|
||||
_configuracionFabrica = await ValoresFabrica.CargarAsync();
|
||||
var configuracionHistorial = _modoConfiguracionHistorial switch
|
||||
{
|
||||
ModoConfiguracionHistorial.Guardada => CrearConfiguracionGuardadaHistorial(item),
|
||||
|
||||
@@ -1400,7 +1400,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
private bool _elevPopupMinimizado = false;
|
||||
|
||||
|
||||
private const string _versionApp = "(v-20260907a)";
|
||||
private const string _versionApp = "(v-20260908a)";
|
||||
|
||||
|
||||
private double? _paradaElevIdee;
|
||||
|
||||
@@ -23,6 +23,8 @@ public partial class PlanificadorRutas
|
||||
/// </summary>
|
||||
private async Task CargarConfiguracionLocalAsync()
|
||||
{
|
||||
_configuracionFabrica = await ValoresFabrica.CargarAsync();
|
||||
AplicarConfiguracionLocal(CrearConfiguracionPredeterminadaHistorial());
|
||||
try
|
||||
{
|
||||
var json = await JS.InvokeAsync<string?>("rt.loadLocalValue", ClaveConfiguracionLocal);
|
||||
@@ -146,7 +148,7 @@ public partial class PlanificadorRutas
|
||||
PreferenciaEtiquetaRuta = _preferenciaEtiquetaRuta
|
||||
};
|
||||
|
||||
private static ConfiguracionHistorialRuta CrearConfiguracionPredeterminadaHistorial()
|
||||
private static ConfiguracionHistorialRuta CrearConfiguracionBaseHistorial()
|
||||
{
|
||||
var ahora = DateTime.Now;
|
||||
return new ConfiguracionHistorialRuta
|
||||
@@ -174,7 +176,7 @@ public partial class PlanificadorRutas
|
||||
ParametroPenalizacionDistanciaPieMetros = ParametroPenalizacionDistanciaPieMetrosPorDefecto,
|
||||
CoeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto,
|
||||
VersionCoeficienteEsfuerzo = VersionPersistenciaCoeficienteEsfuerzo,
|
||||
PesoTransbordoRankingMinutos = 4.0,
|
||||
PesoTransbordoRankingMinutos = 6.0,
|
||||
FactorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto,
|
||||
VersionFactorEquilibrado = VersionPersistenciaFactorEquilibrado,
|
||||
MostrarElevacionesRuta = true,
|
||||
@@ -267,7 +269,7 @@ public partial class PlanificadorRutas
|
||||
? CoeficienteEsfuerzoAlturaPorDefecto
|
||||
: coeficienteAltura;
|
||||
if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo))
|
||||
_pesoTransbordoRankingMinutos = Math.Abs(pesoTransbordo - 8.0) < 0.0001 ? 4.0 : Math.Max(0, pesoTransbordo);
|
||||
_pesoTransbordoRankingMinutos = Math.Max(0, pesoTransbordo);
|
||||
if (config.FactorEquilibradoAndarBus is { } factorEquilibrado &&
|
||||
double.IsFinite(factorEquilibrado) &&
|
||||
factorEquilibrado > 0)
|
||||
@@ -442,48 +444,8 @@ public partial class PlanificadorRutas
|
||||
if (!confirmado)
|
||||
return;
|
||||
|
||||
_mostrarParadas = true;
|
||||
_usarHorariosTeoricos = true;
|
||||
_segundosPorParadaBusManual = 30;
|
||||
_segundosPorParadaBus = 30;
|
||||
|
||||
_activarRangoItinerarios = true;
|
||||
_itinerarioMin = 1;
|
||||
_itinerarioMax = 99;
|
||||
_filtroSentidoItinerario = FiltroSentidoItinerario.Ambos;
|
||||
|
||||
_activarRangoTransbordos = true;
|
||||
_minTransbordosPermitidos = 0;
|
||||
_maxTransbordosPermitidos = 3;
|
||||
|
||||
_factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
|
||||
_parametroPenalizacionDistanciaPieMetros = ParametroPenalizacionDistanciaPieMetrosPorDefecto;
|
||||
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
||||
_pesoTransbordoRankingMinutos = 4.0;
|
||||
_factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
|
||||
_mostrarElevacionesRutaConfiguracion = true;
|
||||
_fuenteElevacionCalculo = FuenteElevacionLocal5;
|
||||
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
|
||||
_umbralPendienteSubidaMediaRuta = UmbralPendienteSubidaMediaRutaPorDefecto;
|
||||
_umbralPendienteSubidaFuerteRuta = UmbralPendienteSubidaFuerteRutaPorDefecto;
|
||||
_colorPendienteBajadaRuta = ColorPendienteBajadaRutaPorDefecto;
|
||||
_colorPendienteLlanoRuta = ColorPendienteLlanoRutaPorDefecto;
|
||||
_colorPendienteSubidaSuaveRuta = ColorPendienteSubidaSuaveRutaPorDefecto;
|
||||
_colorPendienteSubidaMediaRuta = ColorPendienteSubidaMediaRutaPorDefecto;
|
||||
_colorPendienteSubidaFuerteRuta = ColorPendienteSubidaFuerteRutaPorDefecto;
|
||||
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
|
||||
_umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
|
||||
_velocidadPieKmH = 3.6;
|
||||
_velocidadBusKmH = 16.0;
|
||||
_minutosTransbordo = 5;
|
||||
_margenTransbordoSegundos = 120;
|
||||
_elevMaxPuntosPorTramo = 999;
|
||||
_radioCandidatasMetros = 1200;
|
||||
_walkProvider = ProveedorCaminata.Valhalla;
|
||||
_nivelBrilloMapa = 0;
|
||||
_usarColorRealLineaEnItinerarios = false;
|
||||
_usarOsrmParaRanking = false;
|
||||
_modoConfiguracionHistorial = ModoConfiguracionHistorial.Actual;
|
||||
_configuracionFabrica = await ValoresFabrica.CargarAsync();
|
||||
AplicarValoresFabricaConservandoSeleccion();
|
||||
|
||||
if (_usarHoraSimulada)
|
||||
StopClock();
|
||||
@@ -504,48 +466,8 @@ public partial class PlanificadorRutas
|
||||
{
|
||||
_mostrarConfirmacionRestablecer = false;
|
||||
|
||||
_mostrarParadas = true;
|
||||
_usarHorariosTeoricos = true;
|
||||
_segundosPorParadaBusManual = 30;
|
||||
_segundosPorParadaBus = 30;
|
||||
|
||||
_activarRangoItinerarios = true;
|
||||
_itinerarioMin = 1;
|
||||
_itinerarioMax = 99;
|
||||
_filtroSentidoItinerario = FiltroSentidoItinerario.Ambos;
|
||||
|
||||
_activarRangoTransbordos = true;
|
||||
_minTransbordosPermitidos = 0;
|
||||
_maxTransbordosPermitidos = 3;
|
||||
|
||||
_factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
|
||||
_parametroPenalizacionDistanciaPieMetros = ParametroPenalizacionDistanciaPieMetrosPorDefecto;
|
||||
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
||||
_pesoTransbordoRankingMinutos = 4.0;
|
||||
_factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
|
||||
_mostrarElevacionesRutaConfiguracion = true;
|
||||
_fuenteElevacionCalculo = FuenteElevacionLocal5;
|
||||
_umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
|
||||
_umbralPendienteSubidaMediaRuta = UmbralPendienteSubidaMediaRutaPorDefecto;
|
||||
_umbralPendienteSubidaFuerteRuta = UmbralPendienteSubidaFuerteRutaPorDefecto;
|
||||
_colorPendienteBajadaRuta = ColorPendienteBajadaRutaPorDefecto;
|
||||
_colorPendienteLlanoRuta = ColorPendienteLlanoRutaPorDefecto;
|
||||
_colorPendienteSubidaSuaveRuta = ColorPendienteSubidaSuaveRutaPorDefecto;
|
||||
_colorPendienteSubidaMediaRuta = ColorPendienteSubidaMediaRutaPorDefecto;
|
||||
_colorPendienteSubidaFuerteRuta = ColorPendienteSubidaFuerteRutaPorDefecto;
|
||||
_umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
|
||||
_umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
|
||||
_velocidadPieKmH = 3.6;
|
||||
_velocidadBusKmH = 16.0;
|
||||
_minutosTransbordo = 5;
|
||||
_margenTransbordoSegundos = 120;
|
||||
_elevMaxPuntosPorTramo = 999;
|
||||
_radioCandidatasMetros = 1200;
|
||||
_walkProvider = ProveedorCaminata.Valhalla;
|
||||
_nivelBrilloMapa = 0;
|
||||
_usarColorRealLineaEnItinerarios = false;
|
||||
_usarOsrmParaRanking = false;
|
||||
_modoConfiguracionHistorial = ModoConfiguracionHistorial.Actual;
|
||||
_configuracionFabrica = await ValoresFabrica.CargarAsync();
|
||||
AplicarValoresFabricaConservandoSeleccion();
|
||||
|
||||
if (_usarHoraSimulada)
|
||||
StopClock();
|
||||
|
||||
@@ -1146,6 +1146,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
if (indice < 0 || indice >= _opcionesRuta.Count) return;
|
||||
|
||||
_indiceAlternativaSeleccionada = indice;
|
||||
_proveedorSoloAPieRecalculo = _opcionesRuta[indice].EsSoloAPie ? _opcionesRuta[indice].ProveedorRuta : null;
|
||||
await CerrarPopupsAsync();
|
||||
|
||||
ResetEstadoRuta(limpiarOpciones: false, limpiarMensaje: false);
|
||||
@@ -1755,6 +1756,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
[JSInvokable]
|
||||
public async Task OnMobileMapLongPress(double lat, double lon)
|
||||
{
|
||||
if (_modoDibujoExclusionCaminata) return;
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
if (_inspectorActivo)
|
||||
@@ -1874,15 +1876,27 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
/// <summary>
|
||||
/// Calcula ruta completa.
|
||||
/// </summary>
|
||||
private string? _proveedorSoloAPieRecalculo;
|
||||
|
||||
private string? ObtenerProveedorSoloAPieParaRecalculo(bool conservarSeleccion)
|
||||
{
|
||||
if (!conservarSeleccion)
|
||||
_proveedorSoloAPieRecalculo = null;
|
||||
else if (_indiceAlternativaSeleccionada >= 0 && _indiceAlternativaSeleccionada < _opcionesRuta.Count)
|
||||
{
|
||||
var seleccionada = _opcionesRuta[_indiceAlternativaSeleccionada];
|
||||
_proveedorSoloAPieRecalculo = seleccionada.EsSoloAPie ? seleccionada.ProveedorRuta : null;
|
||||
}
|
||||
// Durante un calculo las opciones se vacian; otro cambio de punto conserva la preferencia.
|
||||
return _proveedorSoloAPieRecalculo;
|
||||
}
|
||||
|
||||
private async Task CalcularRutaCompletaAsync(bool conservarSeleccionSoloAPie = false)
|
||||
{
|
||||
if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null)
|
||||
return;
|
||||
|
||||
var restaurarSeleccionSoloAPie = conservarSeleccionSoloAPie &&
|
||||
_indiceAlternativaSeleccionada >= 0 &&
|
||||
_indiceAlternativaSeleccionada < _opcionesRuta.Count &&
|
||||
_opcionesRuta[_indiceAlternativaSeleccionada].EsSoloAPie;
|
||||
var proveedorSoloAPiePreferido = ObtenerProveedorSoloAPieParaRecalculo(conservarSeleccionSoloAPie);
|
||||
|
||||
_mostrarElevacionesRuta = _mostrarElevacionesRutaConfiguracion;
|
||||
|
||||
@@ -2002,8 +2016,9 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
return;
|
||||
}
|
||||
|
||||
var idxPreferida = restaurarSeleccionSoloAPie
|
||||
? _opcionesRuta.FindIndex(opcion => opcion.EsSoloAPie)
|
||||
var idxPreferida = proveedorSoloAPiePreferido is not null
|
||||
? _opcionesRuta.FindIndex(opcion => opcion.EsSoloAPie &&
|
||||
string.Equals(opcion.ProveedorRuta, proveedorSoloAPiePreferido, StringComparison.OrdinalIgnoreCase))
|
||||
: BuscarIndicePreferidoPorCriterio(_criterioPrioritarioRuta);
|
||||
if (idxPreferida < 0 || idxPreferida >= _opcionesRuta.Count)
|
||||
{
|
||||
|
||||
@@ -1312,7 +1312,7 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
}
|
||||
await CerrarPopupsAsync();
|
||||
|
||||
await CalcularRutaCompletaAsync();
|
||||
await CalcularRutaCompletaAsync(conservarSeleccionSoloAPie: true);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
@@ -1321,6 +1321,9 @@ public partial class PlanificadorRutas : ComponentBase
|
||||
/// </summary>
|
||||
private async Task ReiniciarSeleccion()
|
||||
{
|
||||
_proveedorSoloAPieRecalculo = null;
|
||||
await LimpiarZonasExclusionAsync();
|
||||
await CerrarHerramientaZonasAsync();
|
||||
await CerrarTodasLasPreviewsAsync();
|
||||
|
||||
await CerrarPopupsAsync();
|
||||
|
||||
@@ -4,112 +4,159 @@ namespace RutasDBUS.Components.Pages;
|
||||
|
||||
public partial class PlanificadorRutas
|
||||
{
|
||||
private bool _modoDibujoExclusionCaminata;
|
||||
private bool _poligonoExclusionCaminataCerrado;
|
||||
private readonly List<double[]> _verticesExclusionCaminata = new();
|
||||
private sealed class ZonaExclusionCaminata
|
||||
{
|
||||
public string Id { get; } = Guid.NewGuid().ToString("N");
|
||||
public List<double[]> Vertices { get; } = new();
|
||||
public bool Cerrada { get; set; }
|
||||
}
|
||||
|
||||
private bool PuedeProbarExclusionCaminata =>
|
||||
_walkProvider == ProveedorCaminata.Valhalla &&
|
||||
_hayRutaValida &&
|
||||
_opcionesRuta.Count > 0 &&
|
||||
_indiceAlternativaSeleccionada >= 0 &&
|
||||
_indiceAlternativaSeleccionada < _opcionesRuta.Count &&
|
||||
(_opcionesRuta[_indiceAlternativaSeleccionada].EsSoloAPie ||
|
||||
_tramosRuta.Any(tramo => tramo.Tipo.StartsWith("A pie", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
private bool PoligonoExclusionCaminataValido =>
|
||||
_verticesExclusionCaminata.Count >= 3 && _poligonoExclusionCaminataCerrado;
|
||||
private readonly List<ZonaExclusionCaminata> _zonasExclusionCaminata = new();
|
||||
private string? _zonaExclusionSeleccionada;
|
||||
private bool _mostrarHerramientaZonas;
|
||||
private bool _recalculandoZonas;
|
||||
private string? _errorZonas;
|
||||
private ZonaExclusionCaminata? ZonaExclusionSeleccionada =>
|
||||
_zonasExclusionCaminata.Find(zona => zona.Id == _zonaExclusionSeleccionada);
|
||||
private bool _modoDibujoExclusionCaminata =>
|
||||
_mostrarHerramientaZonas && ZonaExclusionSeleccionada is { Cerrada: false };
|
||||
private int NumeroZonasExclusion => _zonasExclusionCaminata.Count(zona => zona.Cerrada);
|
||||
|
||||
private List<List<double[]>>? ObtenerPoligonosExclusionCaminata()
|
||||
{
|
||||
if (!PoligonoExclusionCaminataValido)
|
||||
return null;
|
||||
var poligonos = _zonasExclusionCaminata.Where(zona => zona.Cerrada)
|
||||
.Select(zona => zona.Vertices.Select(p => new[] { p[1], p[0] })
|
||||
.Append(new[] { zona.Vertices[0][1], zona.Vertices[0][0] }).ToList()).ToList();
|
||||
return poligonos.Count == 0 ? null : poligonos;
|
||||
}
|
||||
|
||||
return new List<List<double[]>>
|
||||
{
|
||||
_verticesExclusionCaminata
|
||||
.Select(punto => new[] { punto[1], punto[0] })
|
||||
.Append(new[] { _verticesExclusionCaminata[0][1], _verticesExclusionCaminata[0][0] })
|
||||
.ToList()
|
||||
};
|
||||
private async Task AbrirHerramientaZonasAsync()
|
||||
{
|
||||
_mostrarConfigAvanzada = false;
|
||||
_mostrarHerramientaZonas = true;
|
||||
await ActualizarEditorExclusionCaminataAsync();
|
||||
}
|
||||
|
||||
private async Task CerrarHerramientaZonasAsync()
|
||||
{
|
||||
_zonasExclusionCaminata.RemoveAll(zona => !zona.Cerrada);
|
||||
_zonaExclusionSeleccionada = null;
|
||||
_mostrarHerramientaZonas = false;
|
||||
_errorZonas = null;
|
||||
await JS.InvokeVoidAsync("rt.clearExclusionPolygonEditor");
|
||||
}
|
||||
|
||||
private async Task IniciarDibujoExclusionCaminataAsync()
|
||||
{
|
||||
if (!PuedeProbarExclusionCaminata)
|
||||
return;
|
||||
|
||||
_modoDibujoExclusionCaminata = true;
|
||||
_poligonoExclusionCaminataCerrado = false;
|
||||
_verticesExclusionCaminata.Clear();
|
||||
await JS.InvokeVoidAsync("rt.beginExclusionPolygonEditor");
|
||||
StateHasChanged();
|
||||
if (_recalculandoZonas || _modoDibujoExclusionCaminata || _zonasExclusionCaminata.Count >= 10) return;
|
||||
var zona = new ZonaExclusionCaminata();
|
||||
_zonasExclusionCaminata.Add(zona);
|
||||
_zonaExclusionSeleccionada = zona.Id;
|
||||
_errorZonas = null;
|
||||
await ActualizarEditorExclusionCaminataAsync();
|
||||
}
|
||||
|
||||
private async Task CancelarDibujoExclusionCaminataAsync()
|
||||
private async Task SeleccionarZonaExclusionAsync(string id)
|
||||
{
|
||||
_modoDibujoExclusionCaminata = false;
|
||||
_poligonoExclusionCaminataCerrado = false;
|
||||
_verticesExclusionCaminata.Clear();
|
||||
if (_recalculandoZonas) return;
|
||||
_zonasExclusionCaminata.RemoveAll(zona => !zona.Cerrada);
|
||||
_zonaExclusionSeleccionada = id;
|
||||
_errorZonas = null;
|
||||
await ActualizarEditorExclusionCaminataAsync();
|
||||
}
|
||||
|
||||
private async Task EliminarZonaExclusionAsync(string id)
|
||||
{
|
||||
if (_recalculandoZonas) return;
|
||||
_zonasExclusionCaminata.RemoveAll(zona => zona.Id == id);
|
||||
if (_zonaExclusionSeleccionada == id) _zonaExclusionSeleccionada = null;
|
||||
_errorZonas = null;
|
||||
await ActualizarEditorExclusionCaminataAsync();
|
||||
}
|
||||
|
||||
private async Task LimpiarZonasExclusionAsync()
|
||||
{
|
||||
_zonasExclusionCaminata.Clear();
|
||||
_zonaExclusionSeleccionada = null;
|
||||
_errorZonas = null;
|
||||
await JS.InvokeVoidAsync("rt.clearExclusionPolygonEditor");
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task DeshacerVerticeExclusionCaminataAsync()
|
||||
{
|
||||
if (_verticesExclusionCaminata.Count == 0 || _poligonoExclusionCaminataCerrado)
|
||||
return;
|
||||
|
||||
_verticesExclusionCaminata.RemoveAt(_verticesExclusionCaminata.Count - 1);
|
||||
if (ZonaExclusionSeleccionada is not { Cerrada: false } zona || zona.Vertices.Count == 0) return;
|
||||
zona.Vertices.RemoveAt(zona.Vertices.Count - 1);
|
||||
await ActualizarEditorExclusionCaminataAsync();
|
||||
}
|
||||
|
||||
private async Task CerrarPoligonoExclusionCaminataAsync()
|
||||
{
|
||||
if (_verticesExclusionCaminata.Count < 3)
|
||||
if (ZonaExclusionSeleccionada is not { Cerrada: false } zona || zona.Vertices.Count < 3) return;
|
||||
if (!await JS.InvokeAsync<bool>("rt.isExclusionPolygonValid", zona.Vertices))
|
||||
{
|
||||
_errorZonas = "Revisa la zona: los lados no pueden cruzarse ni quedar alineados.";
|
||||
return;
|
||||
|
||||
_poligonoExclusionCaminataCerrado = true;
|
||||
await JS.InvokeVoidAsync("rt.closeExclusionPolygonEditor");
|
||||
StateHasChanged();
|
||||
}
|
||||
zona.Cerrada = true;
|
||||
_errorZonas = null;
|
||||
await ActualizarEditorExclusionCaminataAsync();
|
||||
}
|
||||
|
||||
private async Task CalcularRutaEvitandoPoligonoCaminataAsync()
|
||||
{
|
||||
if (!PoligonoExclusionCaminataValido)
|
||||
return;
|
||||
|
||||
_modoDibujoExclusionCaminata = false;
|
||||
await JS.InvokeVoidAsync("rt.finishExclusionPolygonEditor");
|
||||
_omitirRegistroHistorialEnSiguienteCalculo = true;
|
||||
await CalcularRutaCompletaAsync();
|
||||
if (_recalculandoZonas || _modoDibujoExclusionCaminata || _coordenadasOrigen is null || _coordenadasDestino is null) return;
|
||||
foreach (var zona in _zonasExclusionCaminata)
|
||||
{
|
||||
if (!await JS.InvokeAsync<bool>("rt.isExclusionPolygonValid", zona.Vertices))
|
||||
{
|
||||
_errorZonas = "Una zona tiene lados cruzados o no tiene superficie. Ajusta sus puntos.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
_recalculandoZonas = true;
|
||||
_errorZonas = null;
|
||||
await ActualizarEditorExclusionCaminataAsync();
|
||||
try
|
||||
{
|
||||
_omitirRegistroHistorialEnSiguienteCalculo = true;
|
||||
await CalcularRutaCompletaAsync(conservarSeleccionSoloAPie: true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_recalculandoZonas = false;
|
||||
await ActualizarEditorExclusionCaminataAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task OnExclusionPolygonMapClick(double latitud, double longitud)
|
||||
{
|
||||
if (!_modoDibujoExclusionCaminata || _poligonoExclusionCaminataCerrado)
|
||||
return;
|
||||
|
||||
_verticesExclusionCaminata.Add(new[] { latitud, longitud });
|
||||
if (!_modoDibujoExclusionCaminata || _recalculandoZonas || !CoordenadaExclusionValida(latitud, longitud)) return;
|
||||
var zona = ZonaExclusionSeleccionada!;
|
||||
if (zona.Vertices.Count >= 50) return;
|
||||
zona.Vertices.Add(new[] { latitud, longitud });
|
||||
await ActualizarEditorExclusionCaminataAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public Task OnExclusionPolygonVertexDragged(int indice, double latitud, double longitud)
|
||||
public Task OnExclusionPolygonVertexDragged(string id, int indice, double latitud, double longitud)
|
||||
{
|
||||
if (indice >= 0 && indice < _verticesExclusionCaminata.Count)
|
||||
{
|
||||
_verticesExclusionCaminata[indice][0] = latitud;
|
||||
_verticesExclusionCaminata[indice][1] = longitud;
|
||||
}
|
||||
|
||||
var zona = _zonasExclusionCaminata.Find(zona => zona.Id == id);
|
||||
if (_mostrarHerramientaZonas && !_recalculandoZonas && zona is not null &&
|
||||
indice >= 0 && indice < zona.Vertices.Count && CoordenadaExclusionValida(latitud, longitud))
|
||||
zona.Vertices[indice] = new[] { latitud, longitud };
|
||||
return InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private static bool CoordenadaExclusionValida(double latitud, double longitud) =>
|
||||
double.IsFinite(latitud) && double.IsFinite(longitud) && Math.Abs(latitud) <= 90 && Math.Abs(longitud) <= 180;
|
||||
|
||||
private async Task ActualizarEditorExclusionCaminataAsync()
|
||||
{
|
||||
await JS.InvokeVoidAsync("rt.updateExclusionPolygonEditor", _verticesExclusionCaminata);
|
||||
StateHasChanged();
|
||||
if (!_mostrarHerramientaZonas) return;
|
||||
await JS.InvokeVoidAsync("rt.updateExclusionPolygonEditor",
|
||||
_zonasExclusionCaminata.Select(zona => new { id = zona.Id, points = zona.Vertices, closed = zona.Cerrada }),
|
||||
_zonaExclusionSeleccionada, _recalculandoZonas);
|
||||
}
|
||||
}
|
||||
|
||||
86
RutasDBUS/Components/Pages/PlanificadorRutas.Superficie.cs
Normal file
86
RutasDBUS/Components/Pages/PlanificadorRutas.Superficie.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.JSInterop;
|
||||
using RutasDBUS.Modelos.Planificacion;
|
||||
|
||||
namespace RutasDBUS.Components.Pages;
|
||||
|
||||
public partial class PlanificadorRutas
|
||||
{
|
||||
private int _versionConsultaSuperficieSegmento;
|
||||
|
||||
/// <summary>
|
||||
/// Consulta la superficie únicamente del segmento de elevación que se acaba de seleccionar.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task OnElevationSegmentSelected(
|
||||
string routeId,
|
||||
int segmentIndex,
|
||||
double[][] coordenadas)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(routeId)
|
||||
|| segmentIndex < 0
|
||||
|| coordenadas is null
|
||||
|| coordenadas.Length < 2)
|
||||
return;
|
||||
|
||||
var linea = new double[coordenadas.Length, 2];
|
||||
for (var i = 0; i < coordenadas.Length; i++)
|
||||
{
|
||||
if (coordenadas[i] is null || coordenadas[i].Length < 2)
|
||||
return;
|
||||
|
||||
linea[i, 0] = coordenadas[i][0];
|
||||
linea[i, 1] = coordenadas[i][1];
|
||||
if (!double.IsFinite(linea[i, 0]) || !double.IsFinite(linea[i, 1]))
|
||||
return;
|
||||
}
|
||||
|
||||
var version = ++_versionConsultaSuperficieSegmento;
|
||||
string superficie;
|
||||
|
||||
try
|
||||
{
|
||||
var informacion = await ClienteValhalla.ObtenerInformacionSueloAsync(linea);
|
||||
superficie = FormatearSuperficieSegmento(informacion);
|
||||
}
|
||||
catch
|
||||
{
|
||||
superficie = "No disponible";
|
||||
}
|
||||
|
||||
if (version != _versionConsultaSuperficieSegmento)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync(
|
||||
"rt.setElevationSegmentSurface",
|
||||
routeId,
|
||||
segmentIndex,
|
||||
superficie);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// El mapa puede haber sido redibujado mientras respondía la consulta.
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatearSuperficieSegmento(InformacionSueloTramo? informacion)
|
||||
{
|
||||
if (informacion is null || !informacion.Disponible || informacion.Segmentos.Count == 0)
|
||||
return "No disponible";
|
||||
|
||||
return string.Join(
|
||||
"<br>",
|
||||
informacion.Segmentos.Select(segmento =>
|
||||
{
|
||||
var uso = string.IsNullOrWhiteSpace(segmento.Uso)
|
||||
? ""
|
||||
: $" ({segmento.Uso})";
|
||||
var distancia = segmento.DistanciaMetros < 1000
|
||||
? segmento.DistanciaMetros.ToString("0.##", CultureInfo.InvariantCulture) + " m"
|
||||
: (segmento.DistanciaMetros / 1000.0).ToString("0.###", CultureInfo.InvariantCulture) + " km";
|
||||
return $"{segmento.Superficie}{uso}, {distancia}";
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using RutasDBUS.Modelos.Historial;
|
||||
using RutasDBUS.Servicios.Configuracion;
|
||||
|
||||
namespace RutasDBUS.Components.Pages;
|
||||
|
||||
public partial class PlanificadorRutas
|
||||
{
|
||||
[Inject] private ServicioValoresFabrica ValoresFabrica { get; set; } = default!;
|
||||
private ConfiguracionHistorialRuta? _configuracionFabrica;
|
||||
|
||||
private ConfiguracionHistorialRuta CrearConfiguracionPredeterminadaHistorial()
|
||||
{
|
||||
var config = _configuracionFabrica?.Clonar() ?? CrearConfiguracionBaseHistorial();
|
||||
var ahora = DateTime.Now;
|
||||
config.FechaSimulada = ahora.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
|
||||
config.HoraSimulada = ahora.ToString("HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture);
|
||||
return config;
|
||||
}
|
||||
|
||||
private void AplicarValoresFabricaConservandoSeleccion()
|
||||
{
|
||||
var actual = CrearConfiguracionLocal();
|
||||
var predeterminada = CrearConfiguracionPredeterminadaHistorial();
|
||||
predeterminada.ActivarRangoLineas = actual.ActivarRangoLineas;
|
||||
predeterminada.LineaMin = actual.LineaMin;
|
||||
predeterminada.LineaMax = actual.LineaMax;
|
||||
predeterminada.LineasPermitidas = actual.LineasPermitidas;
|
||||
predeterminada.UsarHoraSimulada = actual.UsarHoraSimulada;
|
||||
predeterminada.FechaSimulada = actual.FechaSimulada;
|
||||
predeterminada.HoraSimulada = actual.HoraSimulada;
|
||||
predeterminada.CriterioPrioritarioRuta = actual.CriterioPrioritarioRuta;
|
||||
predeterminada.PreferenciaEtiquetaRuta = actual.PreferenciaEtiquetaRuta;
|
||||
AplicarConfiguracionLocal(predeterminada);
|
||||
_modoConfiguracionHistorial = ModoConfiguracionHistorial.Actual;
|
||||
}
|
||||
}
|
||||
@@ -227,6 +227,16 @@
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
<button type="button"
|
||||
class="map-zones-toggle @(_mostrarHerramientaZonas ? "is-active" : "")"
|
||||
title="Zonas a evitar: @NumeroZonasExclusion"
|
||||
aria-label="Abrir zonas a evitar"
|
||||
aria-pressed="@_mostrarHerramientaZonas"
|
||||
@onclick="AbrirHerramientaZonasAsync">
|
||||
<span aria-hidden="true">⬡</span>
|
||||
<span>Zonas</span>
|
||||
</button>
|
||||
</div>
|
||||
@* STACK DE POPUPS (izquierda) *@
|
||||
@if (_paradaClicada is not null || _itinerarioClicado is not null || _popupInspectorVisible)
|
||||
@@ -847,53 +857,6 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (PuedeProbarExclusionCaminata)
|
||||
{
|
||||
<div class="route-exclusion-tool" role="region" aria-label="Evitar zona andando">
|
||||
<div class="route-exclusion-tool__header">
|
||||
<strong>Probar evitando una zona andando</strong>
|
||||
<span class="muted">v2</span>
|
||||
</div>
|
||||
|
||||
@if (!_modoDibujoExclusionCaminata && !PoligonoExclusionCaminataValido)
|
||||
{
|
||||
<button type="button" class="btn-ghost route-exclusion-tool__button"
|
||||
title="Dibujar una zona para evitarla en la siguiente búsqueda"
|
||||
@onclick="IniciarDibujoExclusionCaminataAsync">
|
||||
Dibujar zona a evitar
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="route-exclusion-tool__status">
|
||||
@if (_poligonoExclusionCaminataCerrado)
|
||||
{
|
||||
<span>Zona cerrada con @_verticesExclusionCaminata.Count vértices. Puedes arrastrarlos.</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Pulsa en el mapa para añadir vértices: @_verticesExclusionCaminata.Count.</span>
|
||||
}
|
||||
</div>
|
||||
<div class="route-exclusion-tool__actions">
|
||||
@if (!_poligonoExclusionCaminataCerrado)
|
||||
{
|
||||
<button type="button" class="btn-ghost" @onclick="DeshacerVerticeExclusionCaminataAsync"
|
||||
disabled="@(_verticesExclusionCaminata.Count == 0)">Deshacer punto</button>
|
||||
<button type="button" class="btn-ghost route-exclusion-tool__close" @onclick="CerrarPoligonoExclusionCaminataAsync"
|
||||
disabled="@(_verticesExclusionCaminata.Count < 3)">Cerrar zona</button>
|
||||
}
|
||||
@if (PoligonoExclusionCaminataValido)
|
||||
{
|
||||
<button type="button" class="btn-primary route-exclusion-tool__calculate" @onclick="CalcularRutaEvitandoPoligonoCaminataAsync">
|
||||
Calcular alternativa
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="btn-ghost" @onclick="CancelarDibujoExclusionCaminataAsync">Cancelar</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ========= RESUMEN BONITO ARRIBA (único) ========= *@
|
||||
<p class="route-main-text">
|
||||
@@ -1503,6 +1466,53 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_mostrarHerramientaZonas)
|
||||
{
|
||||
<section class="zones-panel draggable-popup" data-drag-key="zonas-exclusion" aria-label="Zonas a evitar">
|
||||
<div class="config-panel-header draggable-popup-handle">
|
||||
<h5>Zonas a evitar <small>v2</small></h5>
|
||||
<button type="button" class="config-close-btn" title="Cerrar herramienta" aria-label="Cerrar herramienta" @onclick="CerrarHerramientaZonasAsync">×</button>
|
||||
</div>
|
||||
<div class="zones-panel__list">
|
||||
@for (var i = 0; i < _zonasExclusionCaminata.Count; i++)
|
||||
{
|
||||
var zona = _zonasExclusionCaminata[i];
|
||||
<div class="zones-panel__item" @key="zona.Id">
|
||||
<button type="button" class="btn-ghost @(zona.Id == _zonaExclusionSeleccionada ? "zones-panel__selected" : "")"
|
||||
disabled="@(_recalculandoZonas || !zona.Cerrada)" @onclick="() => SeleccionarZonaExclusionAsync(zona.Id)">
|
||||
Zona @(i + 1) · @zona.Vertices.Count puntos @(!zona.Cerrada ? "(abierta)" : "")
|
||||
</button>
|
||||
<button type="button" class="btn-ghost zones-panel__icon" title="Eliminar zona" aria-label="Eliminar zona"
|
||||
disabled="@_recalculandoZonas" @onclick="() => EliminarZonaExclusionAsync(zona.Id)">×</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="zones-panel__actions">
|
||||
@if (_modoDibujoExclusionCaminata)
|
||||
{
|
||||
<button type="button" class="btn-ghost" title="Deshacer último punto" aria-label="Deshacer último punto"
|
||||
disabled="@(ZonaExclusionSeleccionada!.Vertices.Count == 0)" @onclick="DeshacerVerticeExclusionCaminataAsync">↶</button>
|
||||
<button type="button" class="btn-ghost" disabled="@(ZonaExclusionSeleccionada!.Vertices.Count < 3)"
|
||||
@onclick="CerrarPoligonoExclusionCaminataAsync">Cerrar zona</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="btn-ghost" disabled="@(_recalculandoZonas || _zonasExclusionCaminata.Count >= 10)"
|
||||
@onclick="IniciarDibujoExclusionCaminataAsync">Añadir zona</button>
|
||||
}
|
||||
<button type="button" class="btn-ghost" disabled="@(_recalculandoZonas || _zonasExclusionCaminata.Count == 0)"
|
||||
@onclick="LimpiarZonasExclusionAsync">Borrar todas</button>
|
||||
<button type="button" class="btn-ghost zones-panel__primary"
|
||||
disabled="@(_recalculandoZonas || _modoDibujoExclusionCaminata || _coordenadasOrigen is null || _coordenadasDestino is null)"
|
||||
@onclick="CalcularRutaEvitandoPoligonoCaminataAsync">@(_recalculandoZonas ? "Calculando..." : "Recalcular")</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(_errorZonas))
|
||||
{
|
||||
<p class="zones-panel__error" role="alert">@_errorZonas</p>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- CONFIG -->
|
||||
@* <button class="config-toggle-btn" @onclick="ToggleConfigPanel">⚙</button>
|
||||
*@
|
||||
|
||||
19
RutasDBUS/Modelos/Planificacion/InformacionSueloTramo.cs
Normal file
19
RutasDBUS/Modelos/Planificacion/InformacionSueloTramo.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace RutasDBUS.Modelos.Planificacion;
|
||||
|
||||
public sealed class InformacionSueloTramo
|
||||
{
|
||||
public bool Disponible { get; init; }
|
||||
public string? Mensaje { get; init; }
|
||||
public List<SegmentoSueloTramo> Segmentos { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class SegmentoSueloTramo
|
||||
{
|
||||
public double DistanciaMetros { get; init; }
|
||||
public string Superficie { get; init; } = "Sin datos";
|
||||
public string? Uso { get; init; }
|
||||
public bool? SinPavimentar { get; init; }
|
||||
public long? IdCamino { get; init; }
|
||||
public int? IndiceInicioForma { get; init; }
|
||||
public int? IndiceFinForma { get; init; }
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public sealed class ParametrosPlanificadorRuta
|
||||
public int SegundosPorParadaBus { get; set; } = 30;
|
||||
public double FactorPenalizacionDistanciaPie { get; set; } = 1.8;
|
||||
public double ParametroPenalizacionDistanciaPieMetros { get; set; } = 200.0;
|
||||
public double PesoTransbordoRankingMinutos { get; set; } = 4.0;
|
||||
public double PesoTransbordoRankingMinutos { get; set; } = 6.0;
|
||||
// Divisor aplicado a la distancia en bus dentro del score equilibrado.
|
||||
public double FactorEquilibradoAndarBus { get; set; } = 6.0;
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ builder.Services.AddSingleton<CreadorIndiceTransitoDbus>();
|
||||
builder.Services.AddSingleton<ServicioIndiceTransitoDbus>();
|
||||
builder.Services.AddSingleton<BuscadorRutasTemporalesDbus>();
|
||||
builder.Services.AddSingleton<IPlanificadorRutas, PlanificadorRutasServicio>();
|
||||
builder.Services.AddSingleton<RutasDBUS.Servicios.Configuracion.ServicioValoresFabrica>();
|
||||
|
||||
// HttpClient genérico (lo usas en el Razor: @inject HttpClient Http)
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
|
||||
@@ -112,6 +113,199 @@ public class ClienteValhalla : IClienteValhalla
|
||||
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
|
||||
// =======================
|
||||
@@ -134,6 +328,75 @@ public class ClienteValhalla : IClienteValhalla
|
||||
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")]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using RutasDBUS.Modelos.Planificacion;
|
||||
|
||||
namespace RutasDBUS.Servicios.Caminata;
|
||||
|
||||
@@ -15,4 +16,12 @@ public interface IClienteValhalla
|
||||
double longitudHasta,
|
||||
string costing = "pedestrian",
|
||||
List<List<double[]>>? excludePolygons = null); // "pedestrian", "bicycle", etc.
|
||||
|
||||
/// <summary>
|
||||
/// Obtiene la superficie y el tipo de uso de los segmentos de una geometría peatonal.
|
||||
/// La respuesta es informativa y no interviene en el cálculo de rutas.
|
||||
/// </summary>
|
||||
Task<InformacionSueloTramo?> ObtenerInformacionSueloAsync(
|
||||
double[,] linea,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
57
RutasDBUS/Servicios/Configuracion/ServicioValoresFabrica.cs
Normal file
57
RutasDBUS/Servicios/Configuracion/ServicioValoresFabrica.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.Text.Json;
|
||||
using RutasDBUS.Modelos.Historial;
|
||||
|
||||
namespace RutasDBUS.Servicios.Configuracion;
|
||||
|
||||
public sealed class ServicioValoresFabrica
|
||||
{
|
||||
private readonly IConfiguration _configuracion;
|
||||
private readonly ILogger<ServicioValoresFabrica> _logger;
|
||||
private readonly SemaphoreSlim _bloqueo = new(1, 1);
|
||||
private readonly string _ruta;
|
||||
|
||||
public ServicioValoresFabrica(IConfiguration configuracion, ILogger<ServicioValoresFabrica> logger)
|
||||
{
|
||||
_configuracion = configuracion;
|
||||
_logger = logger;
|
||||
var directorio = configuracion["ValoresFabrica:Directorio"] ??
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "RutasDBUS");
|
||||
_ruta = Path.Combine(directorio, "configuracion-fabrica.json");
|
||||
}
|
||||
|
||||
public async Task<ConfiguracionHistorialRuta?> CargarAsync()
|
||||
{
|
||||
await _bloqueo.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_ruta)) return null;
|
||||
using var archivo = File.OpenRead(_ruta);
|
||||
var config = await JsonSerializer.DeserializeAsync<ConfiguracionHistorialRuta>(archivo);
|
||||
return config is not null && EsValida(config) ? config : null;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException)
|
||||
{
|
||||
_logger.LogWarning("No se pudieron leer los valores de fabrica: {Tipo}", ex.GetType().Name);
|
||||
return null;
|
||||
}
|
||||
finally { _bloqueo.Release(); }
|
||||
}
|
||||
|
||||
private static bool EsValida(ConfiguracionHistorialRuta config)
|
||||
{
|
||||
static bool Positivo(double? valor) => valor is { } v && double.IsFinite(v) && v > 0;
|
||||
static bool NoNegativo(double? valor) => valor is { } v && double.IsFinite(v) && v >= 0;
|
||||
return config.Version == 1 &&
|
||||
Positivo(config.VelocidadPieKmH) && Positivo(config.VelocidadBusKmH) &&
|
||||
Positivo(config.FactorEquilibradoAndarBus) && Positivo(config.FactorPenalizacionDistanciaPie) &&
|
||||
Positivo(config.ParametroPenalizacionDistanciaPieMetros) && Positivo(config.CoeficienteEsfuerzoAltura) &&
|
||||
NoNegativo(config.PesoTransbordoRankingMinutos) &&
|
||||
NoNegativo(config.UmbralPendienteSubidaRuta) && NoNegativo(config.UmbralPendienteBajadaRuta) &&
|
||||
NoNegativo(config.UmbralAlturaAlertaPendienteRutaMetros) &&
|
||||
config.ItinerarioMin is >= 1 and <= 99 && config.ItinerarioMax >= config.ItinerarioMin &&
|
||||
config.ItinerarioMax <= 99 && config.MinTransbordosPermitidos >= 0 &&
|
||||
config.MaxTransbordosPermitidos >= config.MinTransbordosPermitidos &&
|
||||
config.MaxTransbordosPermitidos <= 10 && config.ElevMaxPuntosPorTramo is >= 2 and <= 999 &&
|
||||
config.RadioCandidatasMetros > 0 && config.MargenTransbordoSegundos is >= 0 and <= 1800;
|
||||
}
|
||||
}
|
||||
@@ -3884,13 +3884,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
{
|
||||
if (excludePolygons is { Count: > 0 })
|
||||
{
|
||||
// Una zona claramente alejada no puede cambiar razonablemente este trayecto.
|
||||
// Evitamos enviar a Valhalla una busqueda restringida innecesaria.
|
||||
if (!ExclusionPuedeAfectarTrayecto(
|
||||
latitudOrigen, longitudOrigen, latitudDestino, longitudDestino, excludePolygons))
|
||||
// Solo reutilizar una geometria conocida y alejada de todas las zonas.
|
||||
var claveNormal = BuildWalkKey("valhalla", latitudOrigen, longitudOrigen, latitudDestino, longitudDestino);
|
||||
if (_cacheCaminatas.TryGetValue(claveNormal, out var normal) && normal.linea is not null &&
|
||||
!ExclusionPuedeAfectarTrayecto(normal.linea, excludePolygons))
|
||||
{
|
||||
return await WalkValhallaCached(
|
||||
latitudOrigen, longitudOrigen, latitudDestino, longitudDestino);
|
||||
return normal;
|
||||
}
|
||||
|
||||
var claveExclusion = BuildWalkKey("valhalla-exclusion", latitudOrigen, longitudOrigen, latitudDestino, longitudDestino)
|
||||
@@ -3909,13 +3908,16 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
excludePolygons: excludePolygons);
|
||||
|
||||
var valor = (linea, distancia, duracion);
|
||||
_cacheCaminatasConExclusion[claveExclusion] = valor;
|
||||
if (linea is not null && distancia > 0)
|
||||
{
|
||||
if (_cacheCaminatasConExclusion.Count >= 2048) _cacheCaminatasConExclusion.Clear();
|
||||
_cacheCaminatasConExclusion[claveExclusion] = valor;
|
||||
}
|
||||
return valor;
|
||||
}
|
||||
catch
|
||||
{
|
||||
var vacio = (linea: (double[,]?)null, distancia: 0.0, duracion: 0.0);
|
||||
_cacheCaminatasConExclusion[claveExclusion] = vacio;
|
||||
return vacio;
|
||||
}
|
||||
}
|
||||
@@ -3945,38 +3947,35 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ExclusionPuedeAfectarTrayecto(
|
||||
double latitudOrigen,
|
||||
double longitudOrigen,
|
||||
double latitudDestino,
|
||||
double longitudDestino,
|
||||
List<List<double[]>> poligonos)
|
||||
private static bool ExclusionPuedeAfectarTrayecto(double[,] linea, List<List<double[]>> poligonos)
|
||||
{
|
||||
const double umbralMetros = 2500.0;
|
||||
var escalaLat = 111_320.0;
|
||||
var escalaLon = Math.Max(1.0, escalaLat * Math.Cos(latitudOrigen * Math.PI / 180.0));
|
||||
var ax = 0.0;
|
||||
var ay = 0.0;
|
||||
var bx = (longitudDestino - longitudOrigen) * escalaLon;
|
||||
var by = (latitudDestino - latitudOrigen) * escalaLat;
|
||||
var ab2 = bx * bx + by * by;
|
||||
|
||||
if (linea.GetLength(0) < 2) return true;
|
||||
var minLat = double.PositiveInfinity;
|
||||
var maxLat = double.NegativeInfinity;
|
||||
var minLon = double.PositiveInfinity;
|
||||
var maxLon = double.NegativeInfinity;
|
||||
for (var i = 0; i < linea.GetLength(0); i++)
|
||||
{
|
||||
if (!double.IsFinite(linea[i, 0]) || !double.IsFinite(linea[i, 1])) return true;
|
||||
minLat = Math.Min(minLat, linea[i, 0]);
|
||||
maxLat = Math.Max(maxLat, linea[i, 0]);
|
||||
minLon = Math.Min(minLon, linea[i, 1]);
|
||||
maxLon = Math.Max(maxLon, linea[i, 1]);
|
||||
}
|
||||
// Margen conservador para las vias y sus extremos, no solo los vertices dibujados.
|
||||
const double margenLat = 2500.0 / 111320.0;
|
||||
var margenLon = margenLat / Math.Max(0.01, Math.Cos((minLat + maxLat) * Math.PI / 360.0));
|
||||
foreach (var poligono in poligonos)
|
||||
{
|
||||
foreach (var punto in poligono)
|
||||
{
|
||||
if (punto is null || punto.Length < 2) continue;
|
||||
// Valhalla recibe los vertices como [longitud, latitud].
|
||||
var px = (punto[0] - longitudOrigen) * escalaLon;
|
||||
var py = (punto[1] - latitudOrigen) * escalaLat;
|
||||
var t = ab2 > 0 ? Math.Clamp((px * bx + py * by) / ab2, 0.0, 1.0) : 0.0;
|
||||
var dx = px - (ax + t * bx);
|
||||
var dy = py - (ay + t * by);
|
||||
if (Math.Sqrt(dx * dx + dy * dy) <= umbralMetros)
|
||||
return true;
|
||||
}
|
||||
if (poligono.Count < 3 || poligono.Any(p => p.Length < 2 || !double.IsFinite(p[0]) || !double.IsFinite(p[1])))
|
||||
return true;
|
||||
// Los poligonos usan [lon, lat]; sus limites incluyen lados y area interior.
|
||||
if (poligono.Max(p => p[1]) >= minLat - margenLat &&
|
||||
poligono.Min(p => p[1]) <= maxLat + margenLat &&
|
||||
poligono.Max(p => p[0]) >= minLon - margenLon &&
|
||||
poligono.Min(p => p[0]) <= maxLon + margenLon)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1132,65 +1132,69 @@ html, body {
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.route-exclusion-tool {
|
||||
margin: 0 0 .55rem;
|
||||
padding: .48rem .6rem;
|
||||
border: 1px solid rgba(248, 113, 113, .42);
|
||||
border-left: 3px solid #ef4444;
|
||||
border-radius: 6px;
|
||||
background: rgba(127, 29, 29, .16);
|
||||
.zones-panel {
|
||||
position: fixed;
|
||||
top: 90px;
|
||||
left: 16px;
|
||||
width: min(340px, calc(100vw - 32px));
|
||||
max-height: calc(100dvh - 110px);
|
||||
overflow: auto;
|
||||
z-index: 1600;
|
||||
padding: .75rem .85rem;
|
||||
border: 1px solid rgba(148, 163, 184, .4);
|
||||
border-radius: .75rem;
|
||||
background: rgba(15, 23, 42, .97);
|
||||
color: #e5e7eb;
|
||||
box-shadow: 0 16px 30px rgba(0, 0, 0, .35);
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #64748b transparent;
|
||||
}
|
||||
|
||||
.route-exclusion-tool__header {
|
||||
display: flex;
|
||||
.zones-panel .config-panel-header h5 { font-size: .94rem; }
|
||||
.zones-panel h5 small { font-size: .74rem; color: #94a3b8; }
|
||||
.zones-panel__list { display: grid; gap: .35rem; margin-block: .5rem; }
|
||||
.zones-panel__item { display: flex; gap: .35rem; min-width: 0; }
|
||||
.zones-panel__item > :first-child { flex: 1; text-align: left; }
|
||||
.zones-panel__actions { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin-top: .5rem; }
|
||||
.zones-panel__actions .btn-ghost { white-space: nowrap; }
|
||||
.zones-panel__icon { padding-inline: .6rem; }
|
||||
.btn-ghost.zones-panel__primary { background: #1d4ed8; color: #fff; }
|
||||
.btn-ghost.zones-panel__primary:disabled { background: #111827; color: #94a3b8; }
|
||||
.btn-ghost.zones-panel__selected { border-color: #e879f9; background: #35203d; }
|
||||
.zones-panel__error { margin: .5rem 0 0; color: #fda4af; font-size: .8rem; }
|
||||
.map-zones-toggle {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
bottom: 42px;
|
||||
z-index: 1150;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: .6rem;
|
||||
margin-bottom: .28rem;
|
||||
}
|
||||
|
||||
.route-exclusion-tool__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: .35rem;
|
||||
margin-top: .35rem;
|
||||
}
|
||||
|
||||
.route-exclusion-tool__close {
|
||||
border-color: rgba(248, 113, 113, .6);
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.route-exclusion-tool__close:not(:disabled):hover {
|
||||
border-color: #f87171;
|
||||
background: rgba(185, 28, 28, .25);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.route-exclusion-tool__close:disabled {
|
||||
opacity: .45;
|
||||
}
|
||||
|
||||
.route-exclusion-tool__calculate {
|
||||
border: 1px solid rgba(125, 211, 252, .7);
|
||||
gap: .38rem;
|
||||
min-height: 34px;
|
||||
padding: .35rem .7rem;
|
||||
border: 1px solid rgba(148, 163, 184, .65);
|
||||
border-radius: 999px;
|
||||
background: #1d4ed8;
|
||||
padding: .3rem .9rem;
|
||||
font-size: .78rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: rgba(15, 23, 42, .94);
|
||||
color: #e5e7eb;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, .45);
|
||||
cursor: pointer;
|
||||
font-size: .76rem;
|
||||
font-weight: 650;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.route-exclusion-tool__calculate:hover {
|
||||
background: #2563eb;
|
||||
border-color: #bae6fd;
|
||||
.map-zones-toggle:hover,
|
||||
.map-zones-toggle:focus-visible,
|
||||
.map-zones-toggle.is-active {
|
||||
border-color: #c084fc;
|
||||
background: #312e81;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.route-exclusion-tool__hint {
|
||||
color: var(--muted-text, #a1a1aa);
|
||||
font-size: .82rem;
|
||||
.map-zones-toggle > span:first-child {
|
||||
color: #e879f9;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.rt-exclusion-vertex {
|
||||
@@ -1198,8 +1202,8 @@ html, body {
|
||||
height: 18px !important;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
background: #dc2626;
|
||||
box-shadow: 0 0 0 2px #7f1d1d, 0 1px 4px rgba(0, 0, 0, .55);
|
||||
background: #a21caf;
|
||||
box-shadow: 0 0 0 2px #581c87, 0 1px 4px rgba(0, 0, 0, .55);
|
||||
}
|
||||
|
||||
.taxibus-notice {
|
||||
@@ -3254,6 +3258,7 @@ html, body {
|
||||
font-size: .74rem;
|
||||
}
|
||||
|
||||
|
||||
/* ===== SLIDER (range) limpio para el panel ===== */
|
||||
.config-range {
|
||||
width: 100%;
|
||||
@@ -3588,6 +3593,14 @@ html, body {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.map-zones-toggle {
|
||||
left: 10px;
|
||||
bottom: calc(var(--mobile-nav-height) + env(safe-area-inset-bottom) + 10px);
|
||||
min-height: 44px;
|
||||
padding-inline: .8rem;
|
||||
font-size: .76rem;
|
||||
}
|
||||
|
||||
.coords-bar,
|
||||
.map-container .leaflet-control-zoom {
|
||||
display: none !important;
|
||||
|
||||
@@ -268,12 +268,31 @@ window.rt.selectElevationSegment = function (
|
||||
};
|
||||
};
|
||||
|
||||
window.rt.setElevationSegmentSurface = function (routeId, segmentIndex, surfaceText) {
|
||||
const linesByRoute = window.rt._elevationSegmentLines || {};
|
||||
const line = linesByRoute[routeId] && linesByRoute[routeId][segmentIndex];
|
||||
if (!line || !line._rtElevationSegment || typeof line._rtElevationTooltip !== "function") return;
|
||||
|
||||
line._rtElevationSegment.surfaceText = surfaceText || "No disponible";
|
||||
const progress = Number.isFinite(line._rtElevationProgress)
|
||||
? line._rtElevationProgress
|
||||
: 0.5;
|
||||
try {
|
||||
line.setTooltipContent(line._rtElevationTooltip(line._rtElevationSegment, progress));
|
||||
if (window.rt._selectedElevationSegment &&
|
||||
window.rt._selectedElevationSegment.line === line) {
|
||||
line.openTooltip();
|
||||
}
|
||||
} catch { }
|
||||
};
|
||||
|
||||
window.rt.clearRoutes = function () {
|
||||
if (!window.rt.ensureRouteLayer()) return;
|
||||
window.rt.clearElevationSegmentSelection();
|
||||
window.rt._routeLayer.clearLayers();
|
||||
window.rt._routeLines = {};
|
||||
window.rt._routeCoords = {};
|
||||
window.rt._elevationSegmentLines = {};
|
||||
|
||||
if (window.rt._routeStopsLayer) {
|
||||
window.rt._routeStopsLayer.clearLayers();
|
||||
@@ -292,6 +311,8 @@ window.rt.drawRoute = function (id, coords, options) {
|
||||
window.rt._routeLayer.removeLayer(window.rt._routeLines[id]);
|
||||
delete window.rt._routeLines[id];
|
||||
}
|
||||
window.rt._elevationSegmentLines = window.rt._elevationSegmentLines || {};
|
||||
delete window.rt._elevationSegmentLines[id];
|
||||
|
||||
const arrowId = id + "__arrow";
|
||||
if (window.rt._routeLines[arrowId]) {
|
||||
@@ -357,6 +378,8 @@ window.rt.drawRoute = function (id, coords, options) {
|
||||
|
||||
if (elevationSegments.length > 0 && coords && coords.length >= 2) {
|
||||
const group = window.L.layerGroup();
|
||||
window.rt._elevationSegmentLines = window.rt._elevationSegmentLines || {};
|
||||
window.rt._elevationSegmentLines[id] = {};
|
||||
|
||||
const progressAlongSegment = function (segmentCoords, latlng) {
|
||||
if (!latlng || !Array.isArray(segmentCoords) || segmentCoords.length < 2) return 0.5;
|
||||
@@ -404,14 +427,18 @@ window.rt.drawRoute = function (id, coords, options) {
|
||||
? estimatedElevation.toFixed(2) + " m"
|
||||
: "-";
|
||||
|
||||
return [
|
||||
const tooltipLines = [
|
||||
"Pendiente: " + slopeText,
|
||||
"Dist. anterior: " + distanceText,
|
||||
"Altura: " + elevationText
|
||||
].join("<br>");
|
||||
];
|
||||
if (segment && segment.surfaceText) {
|
||||
tooltipLines.push("Superficie: " + String(segment.surfaceText));
|
||||
}
|
||||
return tooltipLines.join("<br>");
|
||||
};
|
||||
|
||||
elevationSegments.forEach(function (segment) {
|
||||
elevationSegments.forEach(function (segment, segmentIndex) {
|
||||
const segmentCoords = segment && segment.coords;
|
||||
if (!Array.isArray(segmentCoords) || segmentCoords.length < 2) return;
|
||||
|
||||
@@ -442,6 +469,10 @@ window.rt.drawRoute = function (id, coords, options) {
|
||||
}
|
||||
|
||||
const segmentLine = buildLine(segmentCoords, segmentOpts);
|
||||
segmentLine._rtElevationSegment = segment;
|
||||
segmentLine._rtElevationTooltip = elevationTooltip;
|
||||
segmentLine._rtElevationProgress = 0.5;
|
||||
window.rt._elevationSegmentLines[id][segmentIndex] = segmentLine;
|
||||
const originalSegmentStyle = {
|
||||
color: segmentColor,
|
||||
weight,
|
||||
@@ -460,6 +491,7 @@ window.rt.drawRoute = function (id, coords, options) {
|
||||
});
|
||||
const updateTooltipAtCursor = function (e) {
|
||||
const progress = progressAlongSegment(segmentCoords, e && e.latlng);
|
||||
segmentLine._rtElevationProgress = progress;
|
||||
segmentLine.setTooltipContent(elevationTooltip(segment, progress));
|
||||
};
|
||||
segmentLine.on("mouseover", updateTooltipAtCursor);
|
||||
@@ -475,6 +507,19 @@ window.rt.drawRoute = function (id, coords, options) {
|
||||
group,
|
||||
originalSegmentStyle,
|
||||
weight);
|
||||
segment.surfaceText = "Consultando…";
|
||||
updateTooltipAtCursor(e);
|
||||
try {
|
||||
segmentLine.setTooltipContent(
|
||||
elevationTooltip(segment, segmentLine._rtElevationProgress));
|
||||
} catch { }
|
||||
if (window.rt._dotNetHelper) {
|
||||
window.rt._dotNetHelper.invokeMethodAsync(
|
||||
"OnElevationSegmentSelected",
|
||||
id,
|
||||
segmentIndex,
|
||||
segmentCoords).catch(function () { });
|
||||
}
|
||||
window.requestAnimationFrame(function () {
|
||||
try {
|
||||
if (segmentLine._path && typeof segmentLine._path.blur === "function") {
|
||||
@@ -657,91 +702,100 @@ window.rt.setDotNetHelper = function (dotNetHelper) {
|
||||
window.rt._dotNetHelper = dotNetHelper;
|
||||
};
|
||||
|
||||
// Editor temporal de zonas que se excluiran de las caminatas de Valhalla.
|
||||
window.rt._exclusionPolygonLayer = null;
|
||||
window.rt._exclusionPolygon = null;
|
||||
window.rt._exclusionPolygonMarkers = [];
|
||||
// Las zonas son independientes de las capas de rutas; cerrar el editor retira sus eventos.
|
||||
window.rt._exclusionEditMode = false;
|
||||
window.rt._exclusionPolygonLayer = null;
|
||||
window.rt._exclusionClickHandler = null;
|
||||
window.rt._exclusionMapContainer = null;
|
||||
|
||||
window.rt.beginExclusionPolygonEditor = function () {
|
||||
const map = window.__rtLeafletMap;
|
||||
if (!map || !window.L) return;
|
||||
window.rt.clearExclusionPolygonEditor = function () {
|
||||
if (window.rt._exclusionMapContainer && window.rt._exclusionClickHandler)
|
||||
window.rt._exclusionMapContainer.removeEventListener("click", window.rt._exclusionClickHandler, true);
|
||||
window.rt._exclusionPolygonLayer?.remove();
|
||||
window.rt._exclusionPolygonLayer = null;
|
||||
window.rt._exclusionClickHandler = null;
|
||||
window.rt._exclusionMapContainer = null;
|
||||
window.rt._exclusionEditMode = false;
|
||||
};
|
||||
|
||||
window.rt.updateExclusionPolygonEditor = function (zones, selectedId, busy) {
|
||||
window.rt.clearExclusionPolygonEditor();
|
||||
window.rt._exclusionEditMode = true;
|
||||
window.rt._exclusionPolygonLayer = window.L.layerGroup().addTo(map);
|
||||
window.rt._exclusionPolygon = window.L.polygon([], {
|
||||
color: "#dc2626",
|
||||
weight: 3,
|
||||
opacity: 0.95,
|
||||
fillColor: "#ef4444",
|
||||
fillOpacity: 0.2,
|
||||
interactive: false
|
||||
}).addTo(window.rt._exclusionPolygonLayer);
|
||||
};
|
||||
|
||||
window.rt.updateExclusionPolygonEditor = function (points) {
|
||||
const map = window.__rtLeafletMap;
|
||||
if (!map || !window.L) return;
|
||||
if (!window.rt._exclusionPolygonLayer) window.rt.beginExclusionPolygonEditor();
|
||||
const L = window.L;
|
||||
if (!map || !L) return;
|
||||
|
||||
const polygonPoints = Array.isArray(points)
|
||||
? points.filter(p => Array.isArray(p) && Number.isFinite(Number(p[0])) && Number.isFinite(Number(p[1])))
|
||||
.map(p => [Number(p[0]), Number(p[1])])
|
||||
: [];
|
||||
const layer = L.layerGroup().addTo(map);
|
||||
window.rt._exclusionPolygonLayer = layer;
|
||||
const drawing = !busy && zones.some(zone => zone.id === selectedId && !zone.closed);
|
||||
window.rt._exclusionEditMode = drawing;
|
||||
|
||||
window.rt._exclusionPolygon.setLatLngs(polygonPoints);
|
||||
window.rt._exclusionPolygonMarkers.forEach(marker => marker.remove());
|
||||
window.rt._exclusionPolygonMarkers = polygonPoints.map((point, index) => {
|
||||
const marker = window.L.marker(point, {
|
||||
draggable: true,
|
||||
autoPan: true,
|
||||
bubblingMouseEvents: false,
|
||||
icon: window.L.divIcon({
|
||||
className: "rt-exclusion-vertex",
|
||||
html: "",
|
||||
iconSize: [18, 18],
|
||||
iconAnchor: [9, 9]
|
||||
})
|
||||
}).addTo(window.rt._exclusionPolygonLayer);
|
||||
|
||||
marker.on("click", event => {
|
||||
if (event && event.originalEvent) event.originalEvent.stopPropagation();
|
||||
for (const zone of zones) {
|
||||
const selected = zone.id === selectedId;
|
||||
const points = zone.points.map(p => [p[0], p[1]]);
|
||||
const options = {
|
||||
color: selected ? "#e879f9" : "#f97316", weight: selected ? 3 : 2,
|
||||
fillOpacity: zone.closed ? 0.18 : 0.08, interactive: false
|
||||
};
|
||||
const polygon = (zone.closed ? L.polygon(points, options) : L.polyline(points, options)).addTo(layer);
|
||||
if (!selected || busy) continue;
|
||||
points.forEach((point, index) => {
|
||||
const marker = L.marker(point, {
|
||||
draggable: true, autoPan: true, bubblingMouseEvents: false, zIndexOffset: 3000,
|
||||
icon: L.divIcon({ className: "rt-exclusion-vertex", html: "",
|
||||
iconSize: [18, 18], iconAnchor: [9, 9] })
|
||||
}).addTo(layer);
|
||||
const update = () => {
|
||||
const pos = marker.getLatLng();
|
||||
points[index] = [pos.lat, pos.lng];
|
||||
polygon.setLatLngs(points);
|
||||
return pos;
|
||||
};
|
||||
marker.on("drag", update);
|
||||
marker.on("dragend", () => {
|
||||
const pos = update();
|
||||
window.rt._dotNetHelper?.invokeMethodAsync(
|
||||
"OnExclusionPolygonVertexDragged", zone.id, index, pos.lat, pos.lng);
|
||||
});
|
||||
});
|
||||
marker.on("drag", event => {
|
||||
const latLng = event.target.getLatLng();
|
||||
const current = window.rt._exclusionPolygon.getLatLngs()[0] || [];
|
||||
if (current[index]) {
|
||||
current[index] = latLng;
|
||||
window.rt._exclusionPolygon.setLatLngs([current]);
|
||||
}
|
||||
});
|
||||
marker.on("dragend", event => {
|
||||
const latLng = event.target.getLatLng();
|
||||
window.rt._dotNetHelper?.invokeMethodAsync(
|
||||
"OnExclusionPolygonVertexDragged", index, latLng.lat, latLng.lng);
|
||||
});
|
||||
return marker;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
window.rt.closeExclusionPolygonEditor = function () {
|
||||
if (window.rt._exclusionPolygon) {
|
||||
window.rt._exclusionPolygon.setStyle({ fillOpacity: 0.28 });
|
||||
if (drawing) {
|
||||
const container = map.getContainer();
|
||||
const handler = event => {
|
||||
if (event.target.closest(".rt-exclusion-vertex, .leaflet-control, .leaflet-popup")) return;
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
const pos = map.mouseEventToLatLng(event);
|
||||
window.rt._dotNetHelper?.invokeMethodAsync("OnExclusionPolygonMapClick", pos.lat, pos.lng);
|
||||
};
|
||||
container.addEventListener("click", handler, true);
|
||||
window.rt._exclusionMapContainer = container;
|
||||
window.rt._exclusionClickHandler = handler;
|
||||
}
|
||||
};
|
||||
|
||||
window.rt.finishExclusionPolygonEditor = function () {
|
||||
window.rt._exclusionEditMode = false;
|
||||
};
|
||||
|
||||
window.rt.clearExclusionPolygonEditor = function () {
|
||||
window.rt._exclusionPolygonMarkers.forEach(marker => marker.remove());
|
||||
window.rt._exclusionPolygonMarkers = [];
|
||||
window.rt._exclusionPolygonLayer?.remove();
|
||||
window.rt._exclusionPolygonLayer = null;
|
||||
window.rt._exclusionPolygon = null;
|
||||
window.rt._exclusionEditMode = false;
|
||||
window.rt.isExclusionPolygonValid = function (points) {
|
||||
if (!Array.isArray(points) || points.length < 3) return false;
|
||||
const eps = 1e-12;
|
||||
const cross = (a, b, c) => (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
||||
const on = (a, b, p) => Math.abs(cross(a, b, p)) < eps &&
|
||||
p[0] >= Math.min(a[0], b[0]) - eps && p[0] <= Math.max(a[0], b[0]) + eps &&
|
||||
p[1] >= Math.min(a[1], b[1]) - eps && p[1] <= Math.max(a[1], b[1]) + eps;
|
||||
const n = points.length;
|
||||
let area = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = points[i], b = points[(i + 1) % n];
|
||||
if (!a || a.length < 2 || !a.every(Number.isFinite)) return false;
|
||||
if (a[0] === b[0] && a[1] === b[1]) return false;
|
||||
area += cross(points[0], a, b);
|
||||
for (let j = i + 2; j < n; j++) {
|
||||
if (i === 0 && j === n - 1) continue;
|
||||
const c = points[j], d = points[(j + 1) % n];
|
||||
if ((cross(a, b, c) * cross(a, b, d) < 0 && cross(c, d, a) * cross(c, d, b) < 0) ||
|
||||
on(a, b, c) || on(a, b, d) || on(c, d, a) || on(c, d, b)) return false;
|
||||
}
|
||||
}
|
||||
return Math.abs(area) > eps;
|
||||
};
|
||||
|
||||
|
||||
|
||||
111
tests/RutasDBUS.RegressionTests/Program.cs
Normal file
111
tests/RutasDBUS.RegressionTests/Program.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using RutasDBUS.Components.Pages;
|
||||
using RutasDBUS.Modelos.Historial;
|
||||
using RutasDBUS.Modelos.Planificacion;
|
||||
using RutasDBUS.Servicios.Caminata;
|
||||
using RutasDBUS.Servicios.Configuracion;
|
||||
using RutasDBUS.Servicios.Planificacion;
|
||||
|
||||
static void Check(bool result, string message)
|
||||
{
|
||||
if (!result) throw new Exception(message);
|
||||
Console.WriteLine("OK " + message);
|
||||
}
|
||||
|
||||
var baseMethod = typeof(PlanificadorRutas).GetMethod("CrearConfiguracionBaseHistorial", BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
var defaults = (ConfiguracionHistorialRuta)baseMethod.Invoke(null, null)!;
|
||||
Check(defaults.PesoTransbordoRankingMinutos == 6, "Penalizacion inicial de 6 minutos");
|
||||
|
||||
var folder = Path.Combine(Path.GetTempPath(), "rutasdbus-regression-" + Guid.NewGuid().ToString("N"));
|
||||
IConfiguration Config() => new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ValoresFabrica:Directorio"] = folder
|
||||
}).Build();
|
||||
ServicioValoresFabrica Service() => new(Config(), NullLogger<ServicioValoresFabrica>.Instance);
|
||||
|
||||
try
|
||||
{
|
||||
defaults.PesoTransbordoRankingMinutos = 9;
|
||||
defaults.FechaSimulada = null;
|
||||
defaults.HoraSimulada = null;
|
||||
Directory.CreateDirectory(folder);
|
||||
var file = Path.Combine(folder, "configuracion-fabrica.json");
|
||||
await File.WriteAllTextAsync(file, JsonSerializer.Serialize(defaults));
|
||||
var loaded = await Service().CargarAsync();
|
||||
Check(loaded?.PesoTransbordoRankingMinutos == 9, "Valores compartidos entre instancias");
|
||||
Check(loaded is { FechaSimulada: null, HoraSimulada: null }, "Los valores de fabrica no fijan la fecha y hora");
|
||||
File.WriteAllText(file, "{incompleto");
|
||||
Check(await Service().CargarAsync() is null, "JSON danado permite usar valores base");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Solo datos temporales creados por esta prueba.
|
||||
if (Directory.Exists(folder)) Directory.Delete(folder, recursive: true);
|
||||
}
|
||||
|
||||
var fake = new FakeValhalla();
|
||||
var planner = new PlanificadorRutasServicio(null!, null!, fake, null!, null!, null!, null!);
|
||||
var walk = typeof(PlanificadorRutasServicio).GetMethod("WalkValhallaCached", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
async Task<(double[,]? linea, double distancia, double duracion)> Walk(List<List<double[]>>? zones = null) =>
|
||||
await (Task<(double[,]?, double, double)>)walk.Invoke(planner, new object?[] { 43.31, -1.99, 43.32, -1.98, zones })!;
|
||||
List<double[]> Ring(double lon, double lat, double size) =>
|
||||
new() { new[] { lon, lat }, new[] { lon + size, lat }, new[] { lon + size, lat + size },
|
||||
new[] { lon, lat + size }, new[] { lon, lat } };
|
||||
|
||||
await Walk();
|
||||
var far = new List<List<double[]>> { Ring(-2.3, 43.0, .01) };
|
||||
await Walk(far);
|
||||
Check(fake.Calls == 1, "Zona lejana reutiliza la geometria conocida");
|
||||
var near = new List<List<double[]>> { Ring(-1.985, 43.315, .001), Ring(-1.982, 43.317, .001) };
|
||||
await Walk(near);
|
||||
Check(fake.Zones == 2, "Se envian todos los poligonos");
|
||||
await Walk(near);
|
||||
Check(fake.Calls == 2, "Mismas zonas reutilizan cache restringida");
|
||||
near[0][0][0] += .00001;
|
||||
await Walk(near);
|
||||
Check(fake.Calls == 3, "Mover un vertice invalida esa entrada de cache");
|
||||
await Walk(new() { Ring(-2.2, 43.1, .5) });
|
||||
Check(fake.Calls == 4, "Una zona que rodea la ruta no se ignora aunque sus vertices esten lejos");
|
||||
fake.Fail = true;
|
||||
var retry = new List<List<double[]>> { Ring(-1.984, 43.312, .0002) };
|
||||
await Walk(retry);
|
||||
fake.Fail = false;
|
||||
await Walk(retry);
|
||||
Check(fake.Calls == 6, "Un fallo temporal no queda guardado en cache de zonas");
|
||||
var page = new PlanificadorRutas();
|
||||
var flags = BindingFlags.Instance | BindingFlags.NonPublic;
|
||||
var options = (List<AlternativaRuta>)typeof(PlanificadorRutas).GetField("_opcionesRuta", flags)!.GetValue(page)!;
|
||||
var selected = typeof(PlanificadorRutas).GetField("_indiceAlternativaSeleccionada", flags)!;
|
||||
var remember = typeof(PlanificadorRutas).GetMethod("ObtenerProveedorSoloAPieParaRecalculo", flags)!;
|
||||
string? Remember(bool keep) => (string?)remember.Invoke(page, new object[] { keep });
|
||||
options.Add(new() { EsSoloAPie = true, ProveedorRuta = "v1" });
|
||||
options.Add(new() { EsSoloAPie = true, ProveedorRuta = "v2" });
|
||||
selected.SetValue(page, 1);
|
||||
Check(Remember(true) == "v2", "Se conserva la version andando seleccionada");
|
||||
options.Clear();
|
||||
Check(Remember(true) == "v2", "Otro cambio de punto durante el calculo conserva v2");
|
||||
options.Add(new() { EsSoloAPie = false });
|
||||
selected.SetValue(page, 0);
|
||||
Check(Remember(true) is null, "Seleccionar bus libera la preferencia andando");
|
||||
options.Clear();
|
||||
Check(Remember(false) is null, "Nueva seleccion de criterio no hereda la preferencia andando");
|
||||
Console.WriteLine("Pruebas completadas.");
|
||||
|
||||
sealed class FakeValhalla : IClienteValhalla
|
||||
{
|
||||
public int Calls { get; private set; }
|
||||
public int Zones { get; private set; }
|
||||
public bool Fail { get; set; }
|
||||
public Task<(double[,]? Linea, double DistanciaMetros, double DuracionSegundos)> ObtenerRutaAsync(
|
||||
double latitudDesde, double longitudDesde, double latitudHasta, double longitudHasta,
|
||||
string costing = "pedestrian", List<List<double[]>>? excludePolygons = null)
|
||||
{
|
||||
Calls++;
|
||||
Zones = excludePolygons?.Count ?? 0;
|
||||
return Task.FromResult<(double[,]?, double, double)>(Fail ? (null, 0, 0) :
|
||||
(new[,] { { latitudDesde, longitudDesde }, { latitudHasta, longitudHasta } }, 100, 100));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../RutasDBUS/RutasDBUS.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
80
tests/verify-zones.cjs
Normal file
80
tests/verify-zones.cjs
Normal file
@@ -0,0 +1,80 @@
|
||||
const { chromium } = require(process.argv[2] || "playwright");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ channel: "msedge", headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
const errors = [];
|
||||
page.on("pageerror", error => errors.push(error.stack || error.message));
|
||||
const artifacts = path.join(__dirname, "../artifacts/zones-check");
|
||||
fs.mkdirSync(artifacts, { recursive: true });
|
||||
try {
|
||||
await page.goto("http://127.0.0.1:5199", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => !!window.rt?._dotNetHelper && !!window.__rtLeafletMap);
|
||||
const initialErrors = errors.slice();
|
||||
await page.getByRole("button", { name: "Abrir zonas a evitar", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Añadir zona", exact: true }).click();
|
||||
for (const [x, y] of [[600, 420], [850, 430], [760, 640]]) {
|
||||
await page.mouse.click(x, y);
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
await page.getByRole("button", { name: "Cerrar zona", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Añadir zona", exact: true }).waitFor();
|
||||
assert.equal(await page.locator(".zones-panel__item").count(), 1);
|
||||
assert.equal(await page.locator(".rt-exclusion-vertex").count(), 3);
|
||||
const polygon = page.locator('path[stroke="#e879f9"]');
|
||||
const previousShape = await polygon.getAttribute("d");
|
||||
const marker = await page.locator(".rt-exclusion-vertex").first().boundingBox();
|
||||
await page.mouse.move(marker.x + marker.width / 2, marker.y + marker.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(marker.x - 55, marker.y + 65, { steps: 10 });
|
||||
assert.notEqual(await polygon.getAttribute("d"), previousShape, "Poligono se mueve durante el arrastre");
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(250);
|
||||
await page.getByRole("button", { name: "Añadir zona", exact: true }).click();
|
||||
for (const [x, y] of [[920, 460], [1090, 460], [1020, 640]]) {
|
||||
await page.mouse.click(x, y);
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
await page.getByRole("button", { name: "Cerrar zona", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Añadir zona", exact: true }).waitFor();
|
||||
assert.equal(await page.locator(".zones-panel__item").count(), 2);
|
||||
await page.screenshot({ path: path.join(artifacts, "desktop.png") });
|
||||
const heights = await page.locator(".zones-panel__actions .btn-ghost").evaluateAll(
|
||||
buttons => buttons.map(button => button.getBoundingClientRect().height));
|
||||
assert.equal(new Set(heights).size, 1, "Botones de igual altura");
|
||||
await page.getByRole("button", { name: "Cerrar herramienta", exact: true }).click();
|
||||
await page.locator(".zones-panel").waitFor({ state: "detached" });
|
||||
assert.equal(await page.locator(".rt-exclusion-vertex").count(), 0);
|
||||
assert.equal(await page.locator('path[stroke="#e879f9"], path[stroke="#f97316"]').count(), 0);
|
||||
await page.getByRole("button", { name: "Abrir zonas a evitar", exact: true }).click();
|
||||
await page.locator(".zones-panel").waitFor();
|
||||
assert.equal(await page.locator(".zones-panel__item").count(), 2, "Reabrir conserva zonas editables");
|
||||
await page.getByRole("button", { name: "Eliminar zona", exact: true }).first().click();
|
||||
await page.waitForFunction(() => document.querySelectorAll(".zones-panel__item").length === 1);
|
||||
assert.equal(await page.locator(".zones-panel__item").count(), 1);
|
||||
await page.getByRole("button", { name: "Borrar todas", exact: true }).click();
|
||||
await page.waitForFunction(() => document.querySelectorAll(".zones-panel__item").length === 0);
|
||||
assert.equal(await page.locator(".zones-panel__item").count(), 0);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.screenshot({ path: path.join(artifacts, "mobile.png") });
|
||||
const box = await page.locator(".zones-panel").boundingBox();
|
||||
assert.ok(box.x >= 0 && box.x + box.width <= 390 && box.y + box.height <= 844, "Panel dentro del movil");
|
||||
await page.getByRole("button", { name: "Añadir zona", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Cerrar herramienta", exact: true }).click();
|
||||
await page.setViewportSize({ width: 1440, height: 1000 });
|
||||
await page.getByTitle("Configuración", { exact: true }).click();
|
||||
assert.equal(await page.locator(".config-more-menu").count(), 0, "No hay opcion para guardar valores de fabrica");
|
||||
const externalErrors = errors.filter(error =>
|
||||
error.includes("esri-leaflet-geocoder@3.1.4/dist/esri-leaflet-geocoder.js") &&
|
||||
error.includes("reading 'extend'"));
|
||||
const newErrors = errors.filter(error => !initialErrors.includes(error) && !externalErrors.includes(error));
|
||||
assert.equal(newErrors.length, 0, newErrors.join("\n"));
|
||||
if (externalErrors.length) console.log("Aviso del complemento externo de geocodificacion: " + externalErrors[0]);
|
||||
console.log("OK multiples zonas, arrastre en vivo, cierre, reapertura, borrado, alturas de botones y movil");
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
Reference in New Issue
Block a user