Initial clean RutasDBUS app
This commit is contained in:
3
.github/copilot-instructions.md
vendored
Normal file
3
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
- @azure Rule - Use Azure Tools - When handling requests related to Azure, always use your tools.
|
||||||
|
- @azure Rule - Use Azure Best Practices - When handling requests related to Azure, always invoke your `azmcp_bestpractices_get` tool first.
|
||||||
|
- @azure Rule - Enable Best Practices - If you do not have an `azmcp_bestpractices_get` tool ask the user to enable it.
|
||||||
402
.gitignore
vendored
Normal file
402
.gitignore
vendored
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
# ---> VisualStudio
|
||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Mono auto generated files
|
||||||
|
mono_crash.*
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Ww][Ii][Nn]32/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
|
||||||
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# Visual Studio 2017 auto generated files
|
||||||
|
Generated\ Files/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUnit
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
nunit-*.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# ASP.NET Scaffolding
|
||||||
|
ScaffoldingReadMe.txt
|
||||||
|
|
||||||
|
# StyleCop
|
||||||
|
StyleCopReport.xml
|
||||||
|
|
||||||
|
# Files built by Visual Studio
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_h.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.iobj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.ipdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||||
|
!Directory.Build.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*_wpftmp.csproj
|
||||||
|
*.log
|
||||||
|
*.tlog
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Coverlet is a free, cross platform Code Coverage Tool
|
||||||
|
coverage*.json
|
||||||
|
coverage*.xml
|
||||||
|
coverage*.info
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# NuGet Symbol Packages
|
||||||
|
*.snupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
*.appxbundle
|
||||||
|
*.appxupload
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!?*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Including strong name files can present a security risk
|
||||||
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
|
#*.snk
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
ServiceFabricBackup/
|
||||||
|
*.rptproj.bak
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
*.rptproj.rsuser
|
||||||
|
*- [Bb]ackup.rdl
|
||||||
|
*- [Bb]ackup ([0-9]).rdl
|
||||||
|
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||||
|
*.vbp
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||||
|
*.dsw
|
||||||
|
*.dsp
|
||||||
|
|
||||||
|
# Visual Studio 6 technical files
|
||||||
|
*.ncb
|
||||||
|
*.aps
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# CodeRush personal settings
|
||||||
|
.cr/personal
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
|
||||||
|
# Azure Stream Analytics local run output
|
||||||
|
ASALocalRun/
|
||||||
|
|
||||||
|
# MSBuild Binary and Structured Log
|
||||||
|
*.binlog
|
||||||
|
|
||||||
|
# NVidia Nsight GPU debugger configuration file
|
||||||
|
*.nvuser
|
||||||
|
|
||||||
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
|
.mfractor/
|
||||||
|
|
||||||
|
# Local History for Visual Studio
|
||||||
|
.localhistory/
|
||||||
|
|
||||||
|
# Visual Studio History (VSHistory) files
|
||||||
|
.vshistory/
|
||||||
|
|
||||||
|
# BeatPulse healthcheck temp database
|
||||||
|
healthchecksdb
|
||||||
|
|
||||||
|
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||||
|
MigrationBackup/
|
||||||
|
|
||||||
|
# Ionide (cross platform F# VS Code tools) working folder
|
||||||
|
.ionide/
|
||||||
|
|
||||||
|
# Fody - auto-generated XML schema
|
||||||
|
FodyWeavers.xsd
|
||||||
|
|
||||||
|
# VS Code files for those working on multiple tools
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Windows Installer files from build outputs
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# JetBrains Rider
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
7
NuGet.Config
Normal file
7
NuGet.Config
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
34
RutasDBUS.sln
Normal file
34
RutasDBUS.sln
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RutasDBUS", "RutasDBUS\RutasDBUS.csproj", "{FABFF09E-333D-49F2-9A57-D24694E953C6}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{FABFF09E-333D-49F2-9A57-D24694E953C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{FABFF09E-333D-49F2-9A57-D24694E953C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{FABFF09E-333D-49F2-9A57-D24694E953C6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{FABFF09E-333D-49F2-9A57-D24694E953C6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{FABFF09E-333D-49F2-9A57-D24694E953C6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{FABFF09E-333D-49F2-9A57-D24694E953C6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{FABFF09E-333D-49F2-9A57-D24694E953C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{FABFF09E-333D-49F2-9A57-D24694E953C6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{FABFF09E-333D-49F2-9A57-D24694E953C6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{FABFF09E-333D-49F2-9A57-D24694E953C6}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{FABFF09E-333D-49F2-9A57-D24694E953C6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{FABFF09E-333D-49F2-9A57-D24694E953C6}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
22
RutasDBUS/Components/App.razor
Normal file
22
RutasDBUS/Components/App.razor
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<base href="/" />
|
||||||
|
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="RutasDBUS.styles.css" />
|
||||||
|
<link rel="stylesheet" href="app.css?v=20260626b" />
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
|
<HeadOutlet />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<Routes />
|
||||||
|
<script src="app.js?v=20260626b" defer></script>
|
||||||
|
<script src="_framework/blazor.web.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
23
RutasDBUS/Components/Layout/MainLayout.razor
Normal file
23
RutasDBUS/Components/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
@* <div class="sidebar">
|
||||||
|
<NavMenu />
|
||||||
|
</div> *@
|
||||||
|
|
||||||
|
<main>
|
||||||
|
@* <div class="top-row px-4">
|
||||||
|
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||||
|
</div>
|
||||||
|
*@
|
||||||
|
<article class="content">
|
||||||
|
@Body
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="blazor-error-ui">
|
||||||
|
An unhandled error has occurred.
|
||||||
|
<a href="" class="reload">Reload</a>
|
||||||
|
<a class="dismiss">🗙</a>
|
||||||
|
</div>
|
||||||
96
RutasDBUS/Components/Layout/MainLayout.razor.css
Normal file
96
RutasDBUS/Components/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #d6d5d5;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 3.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:first-child {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640.98px) {
|
||||||
|
.top-row {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.page {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
height: 100vh;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row.auth ::deep a:first-child {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row, article {
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
padding-right: 1.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui {
|
||||||
|
background: lightyellow;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui .dismiss {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
118
RutasDBUS/Components/Layout/NavMenu.razor
Normal file
118
RutasDBUS/Components/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<div class="top-row ps-3 navbar navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="">RutasDBUS</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
|
||||||
|
|
||||||
|
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||||
|
<nav class="flex-column">
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||||
|
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="counter">
|
||||||
|
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="weather">
|
||||||
|
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@* <div class="map-overlay-card">
|
||||||
|
<strong>Flujo de selección</strong>
|
||||||
|
<p>@TextoPasoActual</p>
|
||||||
|
|
||||||
|
<div class="mini">
|
||||||
|
<div>
|
||||||
|
<b>Origen:</b>
|
||||||
|
@if (_paradaOrigenSeleccionada is not null)
|
||||||
|
{
|
||||||
|
<span class="muted">
|
||||||
|
@_paradaOrigenSeleccionada.Nombre (@_paradaOrigenSeleccionada.Codigo)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else if (_coordenadasOrigen is not null)
|
||||||
|
{
|
||||||
|
<span class="muted">Seleccionado en el mapa</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="muted">Pendiente</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b>Destino:</b>
|
||||||
|
@if (_paradaDestinoSeleccionada is not null)
|
||||||
|
{
|
||||||
|
<span class="muted">
|
||||||
|
@_paradaDestinoSeleccionada.Nombre (@_paradaDestinoSeleccionada.Codigo)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else if (_coordenadasDestino is not null)
|
||||||
|
{
|
||||||
|
<span class="muted">Seleccionado en el mapa</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="muted">Pendiente</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (_primerTransbordoseleccionado is not null)
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<b>1er Transbordo:</b>
|
||||||
|
<span class="muted">
|
||||||
|
@_primerTransbordoseleccionado.Nombre (@_primerTransbordoseleccionado.Codigo)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@* Info de la última parada clicada *@
|
||||||
|
@* @if (_paradaClicada is not null)
|
||||||
|
{
|
||||||
|
<hr style="border-color:rgba(55,65,81,.6);margin:.35rem 0;" />
|
||||||
|
<div>
|
||||||
|
<b>Parada clicada:</b>
|
||||||
|
<span class="muted">
|
||||||
|
@_paradaClicada.Nombre (@_paradaClicada.Codigo)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mini">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_paradaClicada.Abreviatura))
|
||||||
|
{
|
||||||
|
<span>Abrev.: @_paradaClicada.Abreviatura</span>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_paradaClicada.NombreCorto))
|
||||||
|
{
|
||||||
|
<span> · Corto: @_paradaClicada.NombreCorto</span>
|
||||||
|
}
|
||||||
|
@if (_paradaClicada.Altura > 0)
|
||||||
|
{
|
||||||
|
<span> · Altura: @_paradaClicada.Altura.ToString("0") m</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<button class="btn-ghost btn-sm"
|
||||||
|
style="padding:.15rem .6rem;font-size:.72rem"
|
||||||
|
@onclick="ReiniciarSeleccion">
|
||||||
|
Limpiar selección
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div> *@
|
||||||
105
RutasDBUS/Components/Layout/NavMenu.razor.css
Normal file
105
RutasDBUS/Components/Layout/NavMenu.razor.css
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
.navbar-toggler {
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
color: white;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler:checked {
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
height: 3.5rem;
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
top: -1px;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-house-door-fill-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-plus-square-fill-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-list-nested-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:first-of-type {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:last-of-type {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep .nav-link {
|
||||||
|
color: #d7d7d7;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 3rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep a.active {
|
||||||
|
background-color: rgba(255,255,255,0.37);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep .nav-link:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-scrollable {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler:checked ~ .nav-scrollable {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.navbar-toggler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-scrollable {
|
||||||
|
/* Never collapse the sidebar for wide screens */
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
/* Allow sidebar to scroll for tall menus */
|
||||||
|
height: calc(100vh - 3.5rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
RutasDBUS/Components/Pages/Error.razor
Normal file
36
RutasDBUS/Components/Pages/Error.razor
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
@page "/Error"
|
||||||
|
@using System.Diagnostics
|
||||||
|
|
||||||
|
<PageTitle>Error</PageTitle>
|
||||||
|
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
|
||||||
|
@if (ShowRequestId)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Development Mode</h3>
|
||||||
|
<p>
|
||||||
|
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||||
|
It can result in displaying sensitive information from exceptions to end users.
|
||||||
|
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||||
|
and restarting the app.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@code{
|
||||||
|
[CascadingParameter]
|
||||||
|
private HttpContext? HttpContext { get; set; }
|
||||||
|
|
||||||
|
private string? RequestId { get; set; }
|
||||||
|
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
|
||||||
|
protected override void OnInitialized() =>
|
||||||
|
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||||
|
}
|
||||||
496
RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs
Normal file
496
RutasDBUS/Components/Pages/PlanificadorRutas.CicloVidaYMapa.cs
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using LeafletForBlazor;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using RutasDBUS.Modelos.Planificacion;
|
||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
using RutasDBUS.Servicios;
|
||||||
|
using RutasDBUS.Servicios.Caminata;
|
||||||
|
using RutasDBUS.Servicios.Horarios;
|
||||||
|
using RutasDBUS.Servicios.Planificacion;
|
||||||
|
using RutasDBUS.Servicios.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Components.Pages;
|
||||||
|
|
||||||
|
public partial class PlanificadorRutas : ComponentBase, IDisposable
|
||||||
|
{
|
||||||
|
private DotNetObjectReference<PlanificadorRutas>? _dotRef;
|
||||||
|
|
||||||
|
private enum HorarioOrden
|
||||||
|
{
|
||||||
|
PorHora = 0,
|
||||||
|
PorItinerario = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private HorarioOrden _horarioOrden = HorarioOrden.PorHora;
|
||||||
|
|
||||||
|
private string HorarioOrdenIcono =>
|
||||||
|
_horarioOrden == HorarioOrden.PorHora ? "\u23F0" : "\U0001F68C";
|
||||||
|
|
||||||
|
private string HorarioOrdenTooltip =>
|
||||||
|
_horarioOrden == HorarioOrden.PorHora
|
||||||
|
? "Orden actual: por hora. Pulsar para ordenar por itinerario"
|
||||||
|
: "Orden actual: por itinerario. Pulsar para ordenar por hora";
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona after render.
|
||||||
|
/// </summary>
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
_dotRef = DotNetObjectReference.Create(this);
|
||||||
|
|
||||||
|
try { await JS.InvokeVoidAsync("initBrilloMapa", _dotRef); } catch (Exception ex) { Console.WriteLine("initBrilloMapa: " + ex); }
|
||||||
|
try { await JS.InvokeVoidAsync("initAltInspector", _dotRef); } catch (Exception ex) { Console.WriteLine("initAltInspector: " + ex); }
|
||||||
|
try { await JS.InvokeVoidAsync("rt.setDotNetHelper", _dotRef); } catch (Exception ex) { Console.WriteLine("rt.setDotNetHelper: " + ex); }
|
||||||
|
try { await JS.InvokeVoidAsync("rt.blockLeafletDoubleClick"); } catch (Exception ex) { Console.WriteLine("rt.blockLeafletDoubleClick: " + ex); }
|
||||||
|
try { await JS.InvokeVoidAsync("rt.initDraggablePopups"); } catch (Exception ex) { Console.WriteLine("rt.initDraggablePopups: " + ex); }
|
||||||
|
await CargarConfiguracionLocalAsync();
|
||||||
|
await CargarHistorialLocalAsync();
|
||||||
|
await CargarHistorialCompartidoAsync();
|
||||||
|
await CargarModelosHorariosAsync();
|
||||||
|
|
||||||
|
if (_usarHoraSimulada)
|
||||||
|
await RefrescarHoraUIAsync();
|
||||||
|
else
|
||||||
|
StartClock();
|
||||||
|
|
||||||
|
if (_usarHorariosTeoricos)
|
||||||
|
await EnsureHorariosLoadedAsync();
|
||||||
|
|
||||||
|
_tareaCargaRed = RedBusServicio.CargarRedAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_tareaCargaRed ??= RedBusServicio.CargarRedAsync();
|
||||||
|
if (!_tareaCargaRed.IsCompleted) return;
|
||||||
|
|
||||||
|
await _tareaCargaRed;
|
||||||
|
|
||||||
|
if (RedBusServicio.Paradas.Count > 0)
|
||||||
|
EnsureBusIndexBuilt();
|
||||||
|
|
||||||
|
if (!_zoomLimitsAplicados && _mapa is not null)
|
||||||
|
{
|
||||||
|
_zoomLimitsAplicados = true;
|
||||||
|
await JS.InvokeVoidAsync("rt.setZoomLimits", 3, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
try { await JS.InvokeVoidAsync("rt.addZoomIndicator", new { fakeZoom19 = true }); } catch { }
|
||||||
|
|
||||||
|
if (!_paradasPintadas && !_pintandoParadas && _mapa is not null && RedBusServicio.Paradas.Count > 0)
|
||||||
|
{
|
||||||
|
_pintandoParadas = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_mostrarParadas)
|
||||||
|
await DibujarParadasAsync();
|
||||||
|
else
|
||||||
|
await OcultarParadasAsync();
|
||||||
|
|
||||||
|
_paradasPintadas = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_pintandoParadas = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
if (_itItems.Count == 0 && RedBusServicio.Paradas.Count > 0)
|
||||||
|
{
|
||||||
|
BuildItinerariosDropdown();
|
||||||
|
SincronizarSeleccionItinerarioConFiltro();
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ESTO VA FUERA
|
||||||
|
if (_scrollHorariosPendiente)
|
||||||
|
{
|
||||||
|
_scrollHorariosPendiente = false;
|
||||||
|
try { await JS.InvokeVoidAsync("rt.scrollToHorarioProximo"); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private bool _scrollHorariosPendiente = false;
|
||||||
|
// =============================
|
||||||
|
// PREVIEWS ITINERARIOS (por sentido real)
|
||||||
|
// =============================
|
||||||
|
|
||||||
|
// ids reales pintados (subIds): it_xxx_IV_I, it_xxx_IV_V, it_xxx_I_3, etc.
|
||||||
|
private readonly HashSet<string> _itPreviewActivos = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Mapa: clave lógica "26.12-I" -> ids reales pintados (pueden ser 1 o varios)
|
||||||
|
private readonly Dictionary<string, HashSet<string>> _itPreviewIdsPorClave =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona clave.
|
||||||
|
/// </summary>
|
||||||
|
private static string ItClave(InfoVariante v)
|
||||||
|
{
|
||||||
|
var linea = v.Linea?.Codigo?.ToString() ?? "?";
|
||||||
|
var itin = v.CodigoItinerario.ToString(CultureInfo.InvariantCulture);
|
||||||
|
var sentido = NormalizarSentidoCodigo(v.Sentido?.Codigo); // I / V
|
||||||
|
return $"{linea}.{itin}-{sentido}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona vista previa.
|
||||||
|
/// </summary>
|
||||||
|
private void RegistrarPreview(string clave, string subId)
|
||||||
|
{
|
||||||
|
_itPreviewActivos.Add(subId);
|
||||||
|
|
||||||
|
if (!_itPreviewIdsPorClave.TryGetValue(clave, out var set))
|
||||||
|
{
|
||||||
|
set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
_itPreviewIdsPorClave[clave] = set;
|
||||||
|
}
|
||||||
|
set.Add(subId);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene seleccionada it.
|
||||||
|
/// </summary>
|
||||||
|
private ElementoItinerario? GetSelectedIt()
|
||||||
|
=> _itItems.FirstOrDefault(x => x.Id.Equals(_itSeleccionadoId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pinta it seleccionado.
|
||||||
|
/// </summary>
|
||||||
|
private async Task PintarItSeleccionado()
|
||||||
|
{
|
||||||
|
var it = GetSelectedIt();
|
||||||
|
if (it is null) return;
|
||||||
|
|
||||||
|
await PintarItItemAsync(it);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Quita it seleccionado.
|
||||||
|
/// </summary>
|
||||||
|
private async Task QuitarItSeleccionado()
|
||||||
|
{
|
||||||
|
var it = GetSelectedIt();
|
||||||
|
if (it is null) return;
|
||||||
|
|
||||||
|
// claves a borrar:
|
||||||
|
// - si seleccionas IV => borra I y V
|
||||||
|
// - si seleccionas I/V => borra solo ese sentido
|
||||||
|
var claves = new List<string>();
|
||||||
|
|
||||||
|
if (it.Variantes.Count == 2)
|
||||||
|
{
|
||||||
|
claves.Add(ItClave(it.Variantes[0])); // I
|
||||||
|
claves.Add(ItClave(it.Variantes[1])); // V
|
||||||
|
}
|
||||||
|
else if (it.Variantes.Count == 1)
|
||||||
|
{
|
||||||
|
claves.Add(ItClave(it.Variantes[0])); // solo I o solo V
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var clave in claves)
|
||||||
|
{
|
||||||
|
if (!_itPreviewIdsPorClave.TryGetValue(clave, out var ids) || ids.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// borrar TODOS los subIds reales asociados a ese sentido (aunque se pintaran desde IV)
|
||||||
|
foreach (var id in ids.ToList())
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("rt.drawPreviewLine", id, new List<double[]>(), new { });
|
||||||
|
await JS.InvokeVoidAsync("rt.toggleItStops", id, new List<object>(), new { });
|
||||||
|
QuitarParadasItinerarioVisibles(id);
|
||||||
|
|
||||||
|
_itPreviewActivos.Remove(id);
|
||||||
|
ids.Remove(id);
|
||||||
|
|
||||||
|
// si el popup abierto corresponde a lo borrado, cerrarlo
|
||||||
|
if (!string.IsNullOrWhiteSpace(_itinerarioClicadoSubId) &&
|
||||||
|
string.Equals(_itinerarioClicadoSubId, id, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_itinerarioClicado = null;
|
||||||
|
_itinerarioClicadoSubId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ids.Count == 0)
|
||||||
|
_itPreviewIdsPorClave.Remove(clave);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona todas las vistas previas.
|
||||||
|
/// </summary>
|
||||||
|
private async Task CerrarTodasLasPreviewsAsync()
|
||||||
|
{
|
||||||
|
try { await JS.InvokeVoidAsync("rt.clearPreview"); } catch { }
|
||||||
|
|
||||||
|
_itPreviewActivos.Clear();
|
||||||
|
_itPreviewIdsPorClave.Clear();
|
||||||
|
LimpiarParadasItinerarioVisibles();
|
||||||
|
|
||||||
|
_previewLineasActivas.Clear(); // <- esto ya lo usas para los "full_..."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private bool _inspectorActivo = false;
|
||||||
|
|
||||||
|
private bool _popupInspectorVisible = false;
|
||||||
|
private double _inspectorLat, _inspectorLon;
|
||||||
|
private double? _inspectorElev;
|
||||||
|
private double? _inspectorElevIdee;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona vista previa itinerario clic.
|
||||||
|
/// </summary>
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task OnPreviewItinerarioClick(string previewId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(previewId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Cerrar popup de parada si está abierto
|
||||||
|
|
||||||
|
|
||||||
|
// Guardamos el subId real pulsado (ej: it_xxx_I o full_...)
|
||||||
|
_itinerarioClicadoSubId = previewId;
|
||||||
|
|
||||||
|
// 1) Si viene de previews del desplegable/filtro (it_...)
|
||||||
|
// Puede venir:
|
||||||
|
// - id directo: "it_26_12_I_3"
|
||||||
|
// - subId combinado: "it_26_12_IV_I" / "it_26_12_IV_V"
|
||||||
|
ElementoItinerario? encontrado = null;
|
||||||
|
|
||||||
|
// coincidencia exacta
|
||||||
|
encontrado = _itItems.FirstOrDefault(x =>
|
||||||
|
x.Id.Equals(previewId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
// si no, probar quitando sufijo _I / _V de subId combinado
|
||||||
|
if (encontrado is null)
|
||||||
|
{
|
||||||
|
if (previewId.EndsWith("_I", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
previewId.EndsWith("_V", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var baseId = previewId.Substring(0, previewId.Length - 2);
|
||||||
|
encontrado = _itItems.FirstOrDefault(x =>
|
||||||
|
x.Id.Equals(baseId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Si no se encuentra, puede ser preview azul de tramo de ruta: "full_..."
|
||||||
|
// En ese caso intentamos construir info simple desde el id.
|
||||||
|
if (encontrado is null && previewId.StartsWith("full_", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// formato: full_{linea}_{itin}_{sentido}_{variante}
|
||||||
|
var parts = previewId.Split('_', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
if (parts.Length >= 5)
|
||||||
|
{
|
||||||
|
var linea = parts[1];
|
||||||
|
var itinTxt = parts[2];
|
||||||
|
var sentidoRaw = parts[3]; // "Ida"/"Vuelta" o código
|
||||||
|
var varTxt = parts[4];
|
||||||
|
|
||||||
|
int.TryParse(itinTxt, out var itinNum);
|
||||||
|
|
||||||
|
var sentido = NormalizarSentidoCodigo(sentidoRaw);
|
||||||
|
|
||||||
|
var info = _infoVariantes.FirstOrDefault(v =>
|
||||||
|
string.Equals(v.Linea.Codigo?.ToString(), linea, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
v.CodigoItinerario == itinNum &&
|
||||||
|
string.Equals(NormalizarSentidoCodigo(v.Sentido?.Codigo), sentido, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.Equals(v.Variante.Codigo.ToString(), varTxt, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (info is not null)
|
||||||
|
{
|
||||||
|
encontrado = new ElementoItinerario
|
||||||
|
{
|
||||||
|
Id = previewId,
|
||||||
|
Text = $"{linea}.{itinNum}-{sentido} — {info.Linea.Nombre}",
|
||||||
|
Variantes = new List<InfoVariante> { info }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_itinerarioClicado = encontrado;
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// silencioso
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abre la información de una parada destacada pulsada directamente sobre la solución o el itinerario.
|
||||||
|
/// </summary>
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task OnHighlightedStopClick(string codigoParada)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(codigoParada))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_paradaByCode.TryGetValue(codigoParada, out var parada))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await MostrarPopupParadaAsync(parada);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona inspector activo.
|
||||||
|
/// </summary>
|
||||||
|
[JSInvokable]
|
||||||
|
public void SetInspectorActivo(bool activo)
|
||||||
|
{
|
||||||
|
_inspectorActivo = activo;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona raton move mapa.
|
||||||
|
/// </summary>
|
||||||
|
private void OnMouseMoveMapa(RealTimeMap.ClicksMapArgs args)
|
||||||
|
{
|
||||||
|
_coordsMouse = (args.location.latitude, args.location.longitude);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona brillo.
|
||||||
|
/// </summary>
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task AlternarBrillo()
|
||||||
|
{
|
||||||
|
_nivelBrilloMapa++;
|
||||||
|
if (_nivelBrilloMapa > 4) _nivelBrilloMapa = 0;
|
||||||
|
await PersistirConfiguracionAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dibuja paradas.
|
||||||
|
/// </summary>
|
||||||
|
private async Task DibujarParadasAsync()
|
||||||
|
{
|
||||||
|
if (_mapa is null) return;
|
||||||
|
|
||||||
|
await _mapa.Geometric.DisplayPointsFromArray.deleteAllPoints();
|
||||||
|
|
||||||
|
|
||||||
|
var simboloParada = new RealTimeMap.PointSymbol
|
||||||
|
{
|
||||||
|
color = "black",
|
||||||
|
fillColor = "yellow",
|
||||||
|
radius = 5,
|
||||||
|
opacity = 0.9,
|
||||||
|
fillOpacity = 0.8,
|
||||||
|
weight = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var parada in RedBusServicio.Paradas)
|
||||||
|
{
|
||||||
|
await _mapa.Geometric.DisplayPointsFromArray.addPoint(
|
||||||
|
new[] { parada.Latitud, parada.Longitud },
|
||||||
|
simboloParada);
|
||||||
|
}
|
||||||
|
|
||||||
|
await SincronizarMarcadoresOrigenDestinoJsAsync();
|
||||||
|
}
|
||||||
|
// =========================================================
|
||||||
|
// Walking caché (OSRM/Valhalla)
|
||||||
|
// Nota: el "dur" devuelto NO se usa para tiempos finales.
|
||||||
|
// El tiempo a pie SIEMPRE sale de EstimarDuracionPieSegundos(dist)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
private async Task<(double[,]? line, double dist, double dur)> WalkOsrmCached(double lat1, double lon1, double lat2, double lon2)
|
||||||
|
{
|
||||||
|
var key = WalkKey("osrm", lat1, lon1, lat2, lon2);
|
||||||
|
if (_walkCache.TryGetValue(key, out var v))
|
||||||
|
return v;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (line, dist, dur) = await EsrmCliente.ObtenerRutaAPieAsync(lat1, lon1, lat2, lon2);
|
||||||
|
|
||||||
|
// ✅ Solo cachear si es un resultado "bueno"
|
||||||
|
if (line is not null && dist > 0)
|
||||||
|
{
|
||||||
|
v = (line, dist, dur);
|
||||||
|
_walkCache[key] = v;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ si viene mal, NO lo cacheamos
|
||||||
|
return (null, 0, 0);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ❌ no cachear fallos
|
||||||
|
return (null, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(double[,]? line, double dist, double dur)> WalkValhallaCached(double lat1, double lon1, double lat2, double lon2)
|
||||||
|
{
|
||||||
|
var key = WalkKey("valhalla", lat1, lon1, lat2, lon2);
|
||||||
|
if (_walkCache.TryGetValue(key, out var v))
|
||||||
|
return v;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (line, dist, dur) = await ClienteValhalla.ObtenerRutaAsync(lat1, lon1, lat2, lon2, costing: "pedestrian");
|
||||||
|
|
||||||
|
// ✅ Solo cachear si es un resultado "bueno"
|
||||||
|
if (line is not null && dist > 0)
|
||||||
|
{
|
||||||
|
v = (line, dist, dur);
|
||||||
|
_walkCache[key] = v;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null, 0, 0);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (null, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona linea recta origen destino.
|
||||||
|
/// </summary>
|
||||||
|
private double DistanciaLineaRectaOrigenDestino()
|
||||||
|
{
|
||||||
|
if (_coordenadasOrigen is null || _coordenadasDestino is null) return 0;
|
||||||
|
|
||||||
|
return CalcularDistanciaMetros(
|
||||||
|
_coordenadasOrigen[0], _coordenadasOrigen[1],
|
||||||
|
_coordenadasDestino[0], _coordenadasDestino[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
1633
RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs
Normal file
1633
RutasDBUS/Components/Pages/PlanificadorRutas.Estado.cs
Normal file
File diff suppressed because it is too large
Load Diff
299
RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs
Normal file
299
RutasDBUS/Components/Pages/PlanificadorRutas.Geocodificacion.cs
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using RutasDBUS.Modelos.Geocodificacion;
|
||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Components.Pages;
|
||||||
|
|
||||||
|
public partial class PlanificadorRutas
|
||||||
|
{
|
||||||
|
private enum CampoBusquedaMapa
|
||||||
|
{
|
||||||
|
Origen,
|
||||||
|
Destino
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SugerenciaBusquedaMapa
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = "";
|
||||||
|
public string TextoEntrada { get; init; } = "";
|
||||||
|
public string Titulo { get; init; } = "";
|
||||||
|
public string Subtitulo { get; init; } = "";
|
||||||
|
public string Tipo { get; init; } = "";
|
||||||
|
public string Fuente { get; init; } = "";
|
||||||
|
public double Latitud { get; init; }
|
||||||
|
public double Longitud { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly List<SugerenciaBusquedaMapa> _sugerenciasBusquedaOrigen = new();
|
||||||
|
private readonly List<SugerenciaBusquedaMapa> _sugerenciasBusquedaDestino = new();
|
||||||
|
private CampoBusquedaMapa? _campoBusquedaActivo;
|
||||||
|
private SugerenciaBusquedaMapa? _ubicacionOrigenSeleccionada;
|
||||||
|
private SugerenciaBusquedaMapa? _ubicacionDestinoSeleccionada;
|
||||||
|
private int _versionSugerenciasBusqueda;
|
||||||
|
private CancellationTokenSource? _ctsSugerenciasBusqueda;
|
||||||
|
|
||||||
|
private IReadOnlyList<SugerenciaBusquedaMapa> SugerenciasOrigenVisibles =>
|
||||||
|
_campoBusquedaActivo == CampoBusquedaMapa.Origen
|
||||||
|
? _sugerenciasBusquedaOrigen
|
||||||
|
: Array.Empty<SugerenciaBusquedaMapa>();
|
||||||
|
|
||||||
|
private IReadOnlyList<SugerenciaBusquedaMapa> SugerenciasDestinoVisibles =>
|
||||||
|
_campoBusquedaActivo == CampoBusquedaMapa.Destino
|
||||||
|
? _sugerenciasBusquedaDestino
|
||||||
|
: Array.Empty<SugerenciaBusquedaMapa>();
|
||||||
|
|
||||||
|
private Task ActualizarSugerenciasOrigenAsync()
|
||||||
|
=> ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa.Origen);
|
||||||
|
|
||||||
|
private Task ActualizarSugerenciasDestinoAsync()
|
||||||
|
=> ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa.Destino);
|
||||||
|
|
||||||
|
private Task MostrarSugerenciasOrigenAsync()
|
||||||
|
=> ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa.Origen);
|
||||||
|
|
||||||
|
private Task MostrarSugerenciasDestinoAsync()
|
||||||
|
=> ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa.Destino);
|
||||||
|
|
||||||
|
private async Task ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa campo)
|
||||||
|
{
|
||||||
|
_campoBusquedaActivo = campo;
|
||||||
|
InvalidarUbicacionSeleccionadaSiEditada(campo);
|
||||||
|
|
||||||
|
var texto = ObtenerTextoBusqueda(campo);
|
||||||
|
var version = ++_versionSugerenciasBusqueda;
|
||||||
|
|
||||||
|
_ctsSugerenciasBusqueda?.Cancel();
|
||||||
|
_ctsSugerenciasBusqueda = new CancellationTokenSource();
|
||||||
|
var ct = _ctsSugerenciasBusqueda.Token;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(texto) || texto.Trim().Length < 2)
|
||||||
|
{
|
||||||
|
AsignarSugerenciasBusqueda(campo, Array.Empty<SugerenciaBusquedaMapa>());
|
||||||
|
StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(120, ct);
|
||||||
|
var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 10, ct);
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested || version != _versionSugerenciasBusqueda)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AsignarSugerenciasBusqueda(campo, sugerencias);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
AsignarSugerenciasBusqueda(campo, Array.Empty<SugerenciaBusquedaMapa>());
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeleccionarSugerenciaBusquedaAsync(
|
||||||
|
CampoBusquedaMapa campo,
|
||||||
|
SugerenciaBusquedaMapa sugerencia)
|
||||||
|
{
|
||||||
|
if (campo == CampoBusquedaMapa.Origen)
|
||||||
|
{
|
||||||
|
_textoBusquedaOrigen = sugerencia.TextoEntrada;
|
||||||
|
_coordenadasOrigen = new[] { sugerencia.Latitud, sugerencia.Longitud };
|
||||||
|
_ubicacionOrigenSeleccionada = sugerencia;
|
||||||
|
_sugerenciasBusquedaOrigen.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_textoBusquedaDestino = sugerencia.TextoEntrada;
|
||||||
|
_coordenadasDestino = new[] { sugerencia.Latitud, sugerencia.Longitud };
|
||||||
|
_ubicacionDestinoSeleccionada = sugerencia;
|
||||||
|
_sugerenciasBusquedaDestino.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
_campoBusquedaActivo = null;
|
||||||
|
_mensajeRuta = string.Empty;
|
||||||
|
_hayRutaValida = false;
|
||||||
|
|
||||||
|
await SincronizarMarcadoresOrigenDestinoJsAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<SugerenciaBusquedaMapa?> ResolverUbicacionBusquedaAsync(
|
||||||
|
CampoBusquedaMapa campo,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var texto = ObtenerTextoBusqueda(campo);
|
||||||
|
if (string.IsNullOrWhiteSpace(texto))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var seleccion = ObtenerUbicacionSeleccionada(campo);
|
||||||
|
if (seleccion is not null &&
|
||||||
|
string.Equals(texto.Trim(), seleccion.TextoEntrada, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return seleccion;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parada = ResolverParadaDesdeTexto(texto);
|
||||||
|
if (parada is not null)
|
||||||
|
return CrearSugerenciaParada(parada);
|
||||||
|
|
||||||
|
var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 1, cancellationToken);
|
||||||
|
return sugerencias.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<SugerenciaBusquedaMapa>> CrearSugerenciasBusquedaAsync(
|
||||||
|
string texto,
|
||||||
|
int maxResultados,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var paradas = BuscarSugerenciasParadas(texto, 5);
|
||||||
|
var lugares = await GeocodificadorLocal.BuscarAsync(texto, Math.Max(6, maxResultados), cancellationToken);
|
||||||
|
|
||||||
|
return paradas
|
||||||
|
.Concat(lugares.Select(CrearSugerenciaLugar))
|
||||||
|
.GroupBy(sugerencia => NormalizarClaveSugerencia(sugerencia), StringComparer.Ordinal)
|
||||||
|
.Select(grupo => grupo.First())
|
||||||
|
.Take(Math.Clamp(maxResultados, 1, 20))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<SugerenciaBusquedaMapa> BuscarSugerenciasParadas(string texto, int maxResultados)
|
||||||
|
{
|
||||||
|
var consulta = NormalizeForSearch(texto);
|
||||||
|
var tokens = consulta
|
||||||
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
|
if (tokens.Length == 0)
|
||||||
|
return Array.Empty<SugerenciaBusquedaMapa>();
|
||||||
|
|
||||||
|
return RedBusServicio.Paradas
|
||||||
|
.Select(parada => new
|
||||||
|
{
|
||||||
|
Parada = parada,
|
||||||
|
Puntuacion = PuntuarParada(parada, consulta, tokens)
|
||||||
|
})
|
||||||
|
.Where(x => x.Puntuacion < int.MaxValue)
|
||||||
|
.OrderBy(x => x.Puntuacion)
|
||||||
|
.ThenBy(x => x.Parada.Nombre, StringComparer.CurrentCultureIgnoreCase)
|
||||||
|
.Take(Math.Clamp(maxResultados, 1, 12))
|
||||||
|
.Select(x => CrearSugerenciaParada(x.Parada))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PuntuarParada(Parada parada, string consulta, IReadOnlyList<string> tokens)
|
||||||
|
{
|
||||||
|
var codigo = NormalizeForSearch(CodigoParadaFormateado(parada.Codigo));
|
||||||
|
var codigoRaw = NormalizeForSearch(parada.Codigo);
|
||||||
|
var nombre = NormalizeForSearch(parada.Nombre);
|
||||||
|
var nombreCorto = NormalizeForSearch(parada.NombreCorto);
|
||||||
|
var abreviatura = NormalizeForSearch(parada.Abreviatura);
|
||||||
|
var texto = $"{codigo} {codigoRaw} {nombre} {nombreCorto} {abreviatura}";
|
||||||
|
|
||||||
|
foreach (var token in tokens)
|
||||||
|
{
|
||||||
|
if (!texto.Contains(token, StringComparison.Ordinal))
|
||||||
|
return int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codigo.Equals(consulta, StringComparison.Ordinal) ||
|
||||||
|
codigoRaw.Equals(consulta, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nombre.Equals(consulta, StringComparison.Ordinal))
|
||||||
|
return 8;
|
||||||
|
|
||||||
|
if (nombre.StartsWith(consulta, StringComparison.Ordinal) ||
|
||||||
|
nombreCorto.StartsWith(consulta, StringComparison.Ordinal) ||
|
||||||
|
abreviatura.StartsWith(consulta, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 45 + Math.Min(40, nombre.Length / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SugerenciaBusquedaMapa CrearSugerenciaParada(Parada parada)
|
||||||
|
{
|
||||||
|
var codigo = CodigoParadaFormateado(parada.Codigo);
|
||||||
|
var texto = $"{parada.Nombre} ({codigo})";
|
||||||
|
|
||||||
|
return new SugerenciaBusquedaMapa
|
||||||
|
{
|
||||||
|
Id = "parada:" + parada.Codigo,
|
||||||
|
TextoEntrada = texto,
|
||||||
|
Titulo = texto,
|
||||||
|
Subtitulo = "Parada DBUS",
|
||||||
|
Tipo = "Parada",
|
||||||
|
Fuente = "DBUS",
|
||||||
|
Latitud = parada.Latitud,
|
||||||
|
Longitud = parada.Longitud
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SugerenciaBusquedaMapa CrearSugerenciaLugar(LugarGeocodificado lugar)
|
||||||
|
{
|
||||||
|
return new SugerenciaBusquedaMapa
|
||||||
|
{
|
||||||
|
Id = lugar.Id,
|
||||||
|
TextoEntrada = lugar.Nombre,
|
||||||
|
Titulo = lugar.Nombre,
|
||||||
|
Subtitulo = string.IsNullOrWhiteSpace(lugar.Subtitulo)
|
||||||
|
? $"{lugar.Tipo} · {lugar.Fuente}"
|
||||||
|
: $"{lugar.Subtitulo} · {lugar.Fuente}",
|
||||||
|
Tipo = lugar.Tipo,
|
||||||
|
Fuente = lugar.Fuente,
|
||||||
|
Latitud = lugar.Latitud,
|
||||||
|
Longitud = lugar.Longitud
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ObtenerTextoBusqueda(CampoBusquedaMapa campo)
|
||||||
|
=> campo == CampoBusquedaMapa.Origen
|
||||||
|
? _textoBusquedaOrigen
|
||||||
|
: _textoBusquedaDestino;
|
||||||
|
|
||||||
|
private SugerenciaBusquedaMapa? ObtenerUbicacionSeleccionada(CampoBusquedaMapa campo)
|
||||||
|
=> campo == CampoBusquedaMapa.Origen
|
||||||
|
? _ubicacionOrigenSeleccionada
|
||||||
|
: _ubicacionDestinoSeleccionada;
|
||||||
|
|
||||||
|
private void InvalidarUbicacionSeleccionadaSiEditada(CampoBusquedaMapa campo)
|
||||||
|
{
|
||||||
|
var texto = ObtenerTextoBusqueda(campo).Trim();
|
||||||
|
var seleccion = ObtenerUbicacionSeleccionada(campo);
|
||||||
|
if (seleccion is null ||
|
||||||
|
string.Equals(texto, seleccion.TextoEntrada, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (campo == CampoBusquedaMapa.Origen)
|
||||||
|
_ubicacionOrigenSeleccionada = null;
|
||||||
|
else
|
||||||
|
_ubicacionDestinoSeleccionada = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AsignarSugerenciasBusqueda(
|
||||||
|
CampoBusquedaMapa campo,
|
||||||
|
IReadOnlyList<SugerenciaBusquedaMapa> sugerencias)
|
||||||
|
{
|
||||||
|
var lista = campo == CampoBusquedaMapa.Origen
|
||||||
|
? _sugerenciasBusquedaOrigen
|
||||||
|
: _sugerenciasBusquedaDestino;
|
||||||
|
|
||||||
|
lista.Clear();
|
||||||
|
lista.AddRange(sugerencias);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizarClaveSugerencia(SugerenciaBusquedaMapa sugerencia)
|
||||||
|
{
|
||||||
|
var lat = sugerencia.Latitud.ToString("0.000000", CultureInfo.InvariantCulture);
|
||||||
|
var lon = sugerencia.Longitud.ToString("0.000000", CultureInfo.InvariantCulture);
|
||||||
|
return $"{NormalizeForSearch(sugerencia.Titulo)}|{lat}|{lon}";
|
||||||
|
}
|
||||||
|
}
|
||||||
593
RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs
Normal file
593
RutasDBUS/Components/Pages/PlanificadorRutas.Historial.cs
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using RutasDBUS.Modelos.Historial;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Components.Pages;
|
||||||
|
|
||||||
|
public partial class PlanificadorRutas
|
||||||
|
{
|
||||||
|
private const string ClaveHistorialLocal = "rutasdbus.planificador.routeHistory.v1";
|
||||||
|
private const int MaxItemsHistorialLocal = 100;
|
||||||
|
private const int IntervaloRefrescoHistorialCompartidoSegundos = 3;
|
||||||
|
|
||||||
|
private readonly List<RegistroHistorialRuta> _historialLocal = new();
|
||||||
|
private readonly List<RegistroHistorialRuta> _historialCompartido = new();
|
||||||
|
private PeriodicTimer? _historialCompartidoTimer;
|
||||||
|
private CancellationTokenSource? _historialCompartidoCts;
|
||||||
|
private string _historialLocalSeleccionadoId = string.Empty;
|
||||||
|
private string _historialCompartidoSeleccionadoId = string.Empty;
|
||||||
|
private string _mensajeHistorial = string.Empty;
|
||||||
|
private bool _mensajeHistorialEsError = false;
|
||||||
|
private bool _mostrarHistorialRutas = false;
|
||||||
|
private bool _refrescandoHistorialCompartido = false;
|
||||||
|
private bool _omitirRegistroHistorialEnSiguienteCalculo = false;
|
||||||
|
private string? _historialCompartidoPendienteEliminarId;
|
||||||
|
private ConfiguracionLocalPlanificador? _configuracionTemporalPreviaHistorial;
|
||||||
|
|
||||||
|
private async Task ToggleHistorialPanel()
|
||||||
|
{
|
||||||
|
if (_mostrarHistorialRutas)
|
||||||
|
{
|
||||||
|
_mostrarHistorialRutas = false;
|
||||||
|
StopRefrescoHistorialCompartido();
|
||||||
|
await RestaurarConfiguracionTemporalHistorialAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mostrarHistorialRutas = true;
|
||||||
|
await CargarHistorialCompartidoAsync();
|
||||||
|
StartRefrescoHistorialCompartido();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CargarHistorialLocalAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = await JS.InvokeAsync<string?>("rt.loadLocalValue", ClaveHistorialLocal);
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var items = JsonSerializer.Deserialize<List<RegistroHistorialRuta>>(json);
|
||||||
|
if (items is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_historialLocal.Clear();
|
||||||
|
_historialLocal.AddRange(items
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.Id))
|
||||||
|
.OrderByDescending(x => x.FechaActualizacionUtc)
|
||||||
|
.Take(MaxItemsHistorialLocal));
|
||||||
|
|
||||||
|
_historialLocalSeleccionadoId = _historialLocal.FirstOrDefault()?.Id ?? string.Empty;
|
||||||
|
}
|
||||||
|
catch (JSException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PersistirHistorialLocalAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(_historialLocal);
|
||||||
|
await JS.InvokeVoidAsync("rt.saveLocalValue", ClaveHistorialLocal, json);
|
||||||
|
}
|
||||||
|
catch (JSException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CargarHistorialCompartidoAsync(bool mostrarError = true)
|
||||||
|
{
|
||||||
|
if (_refrescandoHistorialCompartido)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_refrescandoHistorialCompartido = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var items = await HistorialRutasCompartidoService.ObtenerAsync();
|
||||||
|
_historialCompartido.Clear();
|
||||||
|
_historialCompartido.AddRange(items);
|
||||||
|
|
||||||
|
if (_historialCompartido.Count == 0)
|
||||||
|
{
|
||||||
|
_historialCompartidoSeleccionadoId = string.Empty;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_historialCompartido.Any(x => x.Id == _historialCompartidoSeleccionadoId))
|
||||||
|
_historialCompartidoSeleccionadoId = _historialCompartido[0].Id;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
if (mostrarError)
|
||||||
|
MostrarMensajeHistorial("No he podido cargar el historial compartido.", esError: true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_refrescandoHistorialCompartido = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartRefrescoHistorialCompartido()
|
||||||
|
{
|
||||||
|
if (_historialCompartidoTimer is not null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
var timer = new PeriodicTimer(TimeSpan.FromSeconds(IntervaloRefrescoHistorialCompartidoSegundos));
|
||||||
|
_historialCompartidoCts = cts;
|
||||||
|
_historialCompartidoTimer = timer;
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (await timer.WaitForNextTickAsync(cts.Token))
|
||||||
|
{
|
||||||
|
if (!_mostrarHistorialRutas)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
await CargarHistorialCompartidoAsync(mostrarError: false);
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Timer cancelled when the popup or component closes.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopRefrescoHistorialCompartido()
|
||||||
|
{
|
||||||
|
_historialCompartidoCts?.Cancel();
|
||||||
|
_historialCompartidoTimer?.Dispose();
|
||||||
|
_historialCompartidoCts?.Dispose();
|
||||||
|
|
||||||
|
_historialCompartidoTimer = null;
|
||||||
|
_historialCompartidoCts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RegistrarEscenarioActualEnHistorialLocalAsync()
|
||||||
|
{
|
||||||
|
var actual = ConstruirRegistroHistorialActual();
|
||||||
|
if (actual is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var clave = CalcularClaveEscenario(actual);
|
||||||
|
var existente = _historialLocal.FirstOrDefault(x =>
|
||||||
|
string.Equals(CalcularClaveEscenario(x), clave, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (existente is null)
|
||||||
|
{
|
||||||
|
existente = actual;
|
||||||
|
_historialLocal.Insert(0, existente);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existente.OrigenTexto = actual.OrigenTexto;
|
||||||
|
existente.DestinoTexto = actual.DestinoTexto;
|
||||||
|
existente.OrigenLatitud = actual.OrigenLatitud;
|
||||||
|
existente.OrigenLongitud = actual.OrigenLongitud;
|
||||||
|
existente.DestinoLatitud = actual.DestinoLatitud;
|
||||||
|
existente.DestinoLongitud = actual.DestinoLongitud;
|
||||||
|
existente.UsarHorariosTeoricos = actual.UsarHorariosTeoricos;
|
||||||
|
existente.UsarHoraSimulada = actual.UsarHoraSimulada;
|
||||||
|
existente.FechaSimulada = actual.FechaSimulada;
|
||||||
|
existente.HoraSimulada = actual.HoraSimulada;
|
||||||
|
existente.CriterioPrioritarioRuta = actual.CriterioPrioritarioRuta;
|
||||||
|
existente.PreferenciaEtiquetaRuta = actual.PreferenciaEtiquetaRuta;
|
||||||
|
existente.ActivarFiltroLineas = actual.ActivarFiltroLineas;
|
||||||
|
existente.LineasPermitidas = actual.LineasPermitidas.ToList();
|
||||||
|
existente.FechaActualizacionUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
_historialLocal.Remove(existente);
|
||||||
|
_historialLocal.Insert(0, existente);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_historialLocal.Count > MaxItemsHistorialLocal)
|
||||||
|
_historialLocal.RemoveAt(_historialLocal.Count - 1);
|
||||||
|
|
||||||
|
_historialLocalSeleccionadoId = existente.Id;
|
||||||
|
await PersistirHistorialLocalAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CargarHistorialLocalSeleccionadoAsync()
|
||||||
|
{
|
||||||
|
var item = _historialLocal.FirstOrDefault(x => x.Id == _historialLocalSeleccionadoId);
|
||||||
|
if (item is null)
|
||||||
|
{
|
||||||
|
MostrarMensajeHistorial("No encuentro esa combinacion en tu historial personal.", esError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AplicarRegistroHistorialAsync(item);
|
||||||
|
MostrarMensajeHistorial("Combinacion personal cargada.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CargarHistorialLocalAsync(string id)
|
||||||
|
{
|
||||||
|
_historialLocalSeleccionadoId = id;
|
||||||
|
await CargarHistorialLocalSeleccionadoAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CargarHistorialCompartidoSeleccionadoAsync()
|
||||||
|
{
|
||||||
|
var item = _historialCompartido.FirstOrDefault(x => x.Id == _historialCompartidoSeleccionadoId);
|
||||||
|
if (item is null)
|
||||||
|
{
|
||||||
|
MostrarMensajeHistorial("No encuentro esa combinacion en el historial compartido.", esError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AplicarRegistroHistorialAsync(item);
|
||||||
|
MostrarMensajeHistorial("Combinación compartida cargada.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CargarHistorialCompartidoAsync(string id)
|
||||||
|
{
|
||||||
|
_historialCompartidoSeleccionadoId = id;
|
||||||
|
await CargarHistorialCompartidoSeleccionadoAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CompartirEscenarioActualAsync()
|
||||||
|
{
|
||||||
|
var actual = ConstruirRegistroHistorialActual()
|
||||||
|
?? _historialLocal.FirstOrDefault(x => x.Id == _historialLocalSeleccionadoId);
|
||||||
|
|
||||||
|
if (actual is null)
|
||||||
|
{
|
||||||
|
MostrarMensajeHistorial("Primero necesitamos una combinacion valida para poder compartirla.", esError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var guardado = await HistorialRutasCompartidoService.GuardarAsync(actual);
|
||||||
|
await CargarHistorialCompartidoAsync();
|
||||||
|
_historialCompartidoSeleccionadoId = guardado.Id;
|
||||||
|
MostrarMensajeHistorial("Combinacion enviada al historial compartido.");
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
MostrarMensajeHistorial("No he podido guardar la combinacion en el historial compartido.", esError: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CompartirHistorialLocalAsync(string id)
|
||||||
|
{
|
||||||
|
_historialLocalSeleccionadoId = id;
|
||||||
|
var item = _historialLocal.FirstOrDefault(x => x.Id == id);
|
||||||
|
if (item is null)
|
||||||
|
{
|
||||||
|
MostrarMensajeHistorial("No encuentro esa combinacion en tu historial personal.", esError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var guardado = await HistorialRutasCompartidoService.GuardarAsync(item);
|
||||||
|
await CargarHistorialCompartidoAsync();
|
||||||
|
_historialCompartidoSeleccionadoId = guardado.Id;
|
||||||
|
MostrarMensajeHistorial("Combinacion enviada al historial compartido.");
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
MostrarMensajeHistorial("No he podido guardar la combinacion en el historial compartido.", esError: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EliminarHistorialLocalAsync(string id)
|
||||||
|
{
|
||||||
|
var item = _historialLocal.FirstOrDefault(x => x.Id == id);
|
||||||
|
if (item is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_historialLocal.Remove(item);
|
||||||
|
|
||||||
|
if (_historialLocalSeleccionadoId == id)
|
||||||
|
_historialLocalSeleccionadoId = _historialLocal.FirstOrDefault()?.Id ?? string.Empty;
|
||||||
|
|
||||||
|
await PersistirHistorialLocalAsync();
|
||||||
|
MostrarMensajeHistorial("Combinacion eliminada del historial personal.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PedirConfirmacionEliminarHistorialCompartido(string id)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_historialCompartidoPendienteEliminarId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelarConfirmacionEliminarHistorialCompartido()
|
||||||
|
{
|
||||||
|
_historialCompartidoPendienteEliminarId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ConfirmarEliminarHistorialCompartidoAsync()
|
||||||
|
{
|
||||||
|
var id = _historialCompartidoPendienteEliminarId;
|
||||||
|
_historialCompartidoPendienteEliminarId = null;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await EliminarHistorialCompartidoAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EliminarHistorialCompartidoAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ok = await HistorialRutasCompartidoService.EliminarAsync(id);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
MostrarMensajeHistorial("No he podido eliminar esa combinacion compartida.", esError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CargarHistorialCompartidoAsync();
|
||||||
|
|
||||||
|
if (_historialCompartidoSeleccionadoId == id)
|
||||||
|
_historialCompartidoSeleccionadoId = _historialCompartido.FirstOrDefault()?.Id ?? string.Empty;
|
||||||
|
|
||||||
|
MostrarMensajeHistorial("Combinacion eliminada del historial compartido.");
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
MostrarMensajeHistorial("No he podido eliminar esa combinacion compartida.", esError: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AplicarRegistroHistorialAsync(RegistroHistorialRuta item)
|
||||||
|
{
|
||||||
|
CapturarConfiguracionTemporalAntesDeHistorial();
|
||||||
|
|
||||||
|
_omitirRegistroHistorialEnSiguienteCalculo = true;
|
||||||
|
_textoBusquedaOrigen = item.OrigenTexto;
|
||||||
|
_textoBusquedaDestino = item.DestinoTexto;
|
||||||
|
_coordenadasOrigen = new[] { item.OrigenLatitud, item.OrigenLongitud };
|
||||||
|
_coordenadasDestino = new[] { item.DestinoLatitud, item.DestinoLongitud };
|
||||||
|
|
||||||
|
_usarHorariosTeoricos = item.UsarHorariosTeoricos;
|
||||||
|
_usarHoraSimulada = item.UsarHoraSimulada;
|
||||||
|
|
||||||
|
if (DateOnly.TryParse(item.FechaSimulada, out var fecha))
|
||||||
|
_fechaSimulada = fecha;
|
||||||
|
|
||||||
|
if (TimeOnly.TryParse(item.HoraSimulada, out var hora))
|
||||||
|
_horaSimulada = hora;
|
||||||
|
|
||||||
|
_criterioPrioritarioRuta = NormalizarCriterioRuta(item.CriterioPrioritarioRuta, item.PreferenciaEtiquetaRuta);
|
||||||
|
SincronizarPreferenciaDesdeCriterioRuta();
|
||||||
|
|
||||||
|
_activarRangoLineas = item.ActivarFiltroLineas;
|
||||||
|
_lineasPermitidas.Clear();
|
||||||
|
foreach (var linea in NormalizarLineasHistorial(item.LineasPermitidas))
|
||||||
|
_lineasPermitidas.Add(linea);
|
||||||
|
_busFilteredCache.Clear();
|
||||||
|
|
||||||
|
SincronizarSegundosPorParadaConHorarios();
|
||||||
|
|
||||||
|
StopClock();
|
||||||
|
if (_usarHoraSimulada)
|
||||||
|
await RefrescarHoraUIAsync();
|
||||||
|
else
|
||||||
|
StartClock();
|
||||||
|
|
||||||
|
await PersistirConfiguracionAsync();
|
||||||
|
await CerrarTodasLasPreviewsAsync();
|
||||||
|
await CerrarPopupsAsync();
|
||||||
|
|
||||||
|
if (_mapa is not null)
|
||||||
|
{
|
||||||
|
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
|
||||||
|
await ClearRoutesJsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await SincronizarMarcadoresOrigenDestinoJsAsync();
|
||||||
|
await CalcularRutaCompletaAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CapturarConfiguracionTemporalAntesDeHistorial()
|
||||||
|
{
|
||||||
|
if (_configuracionTemporalPreviaHistorial is not null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_configuracionTemporalPreviaHistorial = CrearConfiguracionLocal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RestaurarConfiguracionTemporalHistorialAsync()
|
||||||
|
{
|
||||||
|
var configuracionPrevia = _configuracionTemporalPreviaHistorial;
|
||||||
|
if (configuracionPrevia is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_configuracionTemporalPreviaHistorial = null;
|
||||||
|
AplicarConfiguracionLocal(configuracionPrevia);
|
||||||
|
|
||||||
|
StopClock();
|
||||||
|
if (_usarHoraSimulada)
|
||||||
|
await RefrescarHoraUIAsync();
|
||||||
|
else
|
||||||
|
StartClock();
|
||||||
|
|
||||||
|
await PersistirConfiguracionAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private RegistroHistorialRuta? ConstruirRegistroHistorialActual()
|
||||||
|
{
|
||||||
|
if (_coordenadasOrigen is null || _coordenadasDestino is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var ahora = _ultimaHoraReferenciaRuta ?? AhoraLocal();
|
||||||
|
|
||||||
|
return new RegistroHistorialRuta
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString("N"),
|
||||||
|
OrigenTexto = ResolverTextoPuntoHistorial(_coordenadasOrigen),
|
||||||
|
DestinoTexto = ResolverTextoPuntoHistorial(_coordenadasDestino),
|
||||||
|
OrigenLatitud = _coordenadasOrigen[0],
|
||||||
|
OrigenLongitud = _coordenadasOrigen[1],
|
||||||
|
DestinoLatitud = _coordenadasDestino[0],
|
||||||
|
DestinoLongitud = _coordenadasDestino[1],
|
||||||
|
UsarHorariosTeoricos = _usarHorariosTeoricos,
|
||||||
|
UsarHoraSimulada = _usarHoraSimulada,
|
||||||
|
FechaSimulada = DateOnly.FromDateTime(ahora).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||||
|
HoraSimulada = TimeOnly.FromDateTime(ahora).ToString("HH:mm:ss", CultureInfo.InvariantCulture),
|
||||||
|
CriterioPrioritarioRuta = _criterioPrioritarioRuta,
|
||||||
|
PreferenciaEtiquetaRuta = _preferenciaEtiquetaRuta,
|
||||||
|
ActivarFiltroLineas = _activarRangoLineas,
|
||||||
|
LineasPermitidas = ObtenerLineasPermitidasSeleccionadas().ToList(),
|
||||||
|
FechaCreacionUtc = DateTime.UtcNow,
|
||||||
|
FechaActualizacionUtc = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FormatearParadaHistorial(RutasDBUS.Modelos.RedBus.Parada? parada)
|
||||||
|
{
|
||||||
|
if (parada is null || string.IsNullOrWhiteSpace(parada.Codigo))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var nombre = string.IsNullOrWhiteSpace(parada.Nombre)
|
||||||
|
? parada.Codigo
|
||||||
|
: parada.Nombre.Trim();
|
||||||
|
|
||||||
|
return $"{nombre} ({CodigoParadaFormateado(parada.Codigo)})";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolverTextoPuntoHistorial(double[] coords)
|
||||||
|
{
|
||||||
|
var lat = coords[0];
|
||||||
|
var lon = coords[1];
|
||||||
|
|
||||||
|
var parada = RedBusServicio.BuscarParadaMasCercana(lat, lon);
|
||||||
|
|
||||||
|
var textoParada = FormatearParadaHistorial(parada);
|
||||||
|
if (!string.IsNullOrWhiteSpace(textoParada))
|
||||||
|
return textoParada;
|
||||||
|
|
||||||
|
return $"{lat.ToString("0.000000", CultureInfo.InvariantCulture)}, {lon.ToString("0.000000", CultureInfo.InvariantCulture)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string TextoOpcionHistorial(RegistroHistorialRuta item)
|
||||||
|
{
|
||||||
|
var sello = item.FechaActualizacionUtc == default
|
||||||
|
? string.Empty
|
||||||
|
: item.FechaActualizacionUtc.ToLocalTime().ToString("dd/MM HH:mm", CultureInfo.CurrentCulture);
|
||||||
|
var horaReferencia = FormatearHoraReferenciaHistorial(item);
|
||||||
|
var modo = FormatearModoHistorial(item);
|
||||||
|
|
||||||
|
var textoBase = string.IsNullOrWhiteSpace(sello)
|
||||||
|
? $"{item.OrigenTexto} -> {item.DestinoTexto}"
|
||||||
|
: $"{sello} - {item.OrigenTexto} -> {item.DestinoTexto}";
|
||||||
|
|
||||||
|
var partes = new List<string> { textoBase };
|
||||||
|
if (!string.IsNullOrWhiteSpace(horaReferencia))
|
||||||
|
partes.Add($"HR: {horaReferencia}");
|
||||||
|
if (!string.IsNullOrWhiteSpace(modo))
|
||||||
|
partes.Add($"Modo: {modo}");
|
||||||
|
return string.Join(" \u00b7 ", partes);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatearModoHistorial(RegistroHistorialRuta item)
|
||||||
|
{
|
||||||
|
var criterio = item.CriterioPrioritarioRuta?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(criterio))
|
||||||
|
{
|
||||||
|
var preferencia = item.PreferenciaEtiquetaRuta?.Trim() ?? string.Empty;
|
||||||
|
return string.Equals(preferencia, PreferenciaRutaAndar, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "Andar"
|
||||||
|
: preferencia;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(criterio, CriterioRutaAndar, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "Andar";
|
||||||
|
if (string.Equals(criterio, CriterioRutaTiempo, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "Tiempo";
|
||||||
|
if (string.Equals(criterio, CriterioRutaEquilibrado, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "Equilibrado";
|
||||||
|
|
||||||
|
return criterio;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<int> NormalizarLineasHistorial(IEnumerable<int>? lineas)
|
||||||
|
=> (lineas ?? Array.Empty<int>())
|
||||||
|
.Where(linea => linea > 0)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(linea => linea)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private static string FormatearHoraReferenciaHistorial(RegistroHistorialRuta item)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(item.HoraSimulada))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var formatosHora = new[] { "HH:mm:ss", "H:mm:ss", "HH:mm", "H:mm" };
|
||||||
|
if (!TimeOnly.TryParseExact(
|
||||||
|
item.HoraSimulada.Trim(),
|
||||||
|
formatosHora,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.None,
|
||||||
|
out var hora))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var horaTexto = hora.ToString("HH:mm", CultureInfo.InvariantCulture);
|
||||||
|
if (string.IsNullOrWhiteSpace(item.FechaSimulada))
|
||||||
|
return horaTexto;
|
||||||
|
|
||||||
|
var formatosFecha = new[] { "yyyy-MM-dd", "dd/MM/yyyy", "d/M/yyyy", "dd/MM/yy", "d/M/yy" };
|
||||||
|
return DateOnly.TryParseExact(
|
||||||
|
item.FechaSimulada.Trim(),
|
||||||
|
formatosFecha,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.None,
|
||||||
|
out var fecha)
|
||||||
|
? $"{fecha.ToString("dd/MM", CultureInfo.InvariantCulture)} {horaTexto}"
|
||||||
|
: $"{item.FechaSimulada.Trim()} {horaTexto}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MostrarMensajeHistorial(string mensaje, bool esError = false)
|
||||||
|
{
|
||||||
|
_mensajeHistorial = mensaje;
|
||||||
|
_mensajeHistorialEsError = esError;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CalcularClaveEscenario(RegistroHistorialRuta item)
|
||||||
|
{
|
||||||
|
var origenLat = Math.Round(item.OrigenLatitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
|
||||||
|
var origenLon = Math.Round(item.OrigenLongitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
|
||||||
|
var destinoLat = Math.Round(item.DestinoLatitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
|
||||||
|
var destinoLon = Math.Round(item.DestinoLongitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
|
||||||
|
var fecha = item.UsarHoraSimulada ? item.FechaSimulada?.Trim() ?? string.Empty : string.Empty;
|
||||||
|
var hora = item.UsarHoraSimulada ? item.HoraSimulada?.Trim() ?? string.Empty : string.Empty;
|
||||||
|
var criterio = NormalizarCriterioRuta(item.CriterioPrioritarioRuta, item.PreferenciaEtiquetaRuta);
|
||||||
|
var lineas = item.ActivarFiltroLineas
|
||||||
|
? string.Join(",", NormalizarLineasHistorial(item.LineasPermitidas))
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
return string.Join("|",
|
||||||
|
origenLat,
|
||||||
|
origenLon,
|
||||||
|
destinoLat,
|
||||||
|
destinoLon,
|
||||||
|
item.UsarHorariosTeoricos ? "1" : "0",
|
||||||
|
item.UsarHoraSimulada ? "1" : "0",
|
||||||
|
fecha,
|
||||||
|
hora,
|
||||||
|
criterio,
|
||||||
|
item.ActivarFiltroLineas ? "1" : "0",
|
||||||
|
lineas);
|
||||||
|
}
|
||||||
|
}
|
||||||
1565
RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs
Normal file
1565
RutasDBUS/Components/Pages/PlanificadorRutas.Itinerarios.cs
Normal file
File diff suppressed because it is too large
Load Diff
245
RutasDBUS/Components/Pages/PlanificadorRutas.ModelosHorarios.cs
Normal file
245
RutasDBUS/Components/Pages/PlanificadorRutas.ModelosHorarios.cs
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using RutasDBUS.Modelos.Importacion;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Components.Pages;
|
||||||
|
|
||||||
|
public partial class PlanificadorRutas
|
||||||
|
{
|
||||||
|
private const long MaxModeloHorarioUploadBytes = 250L * 1024 * 1024;
|
||||||
|
|
||||||
|
private IReadOnlyList<ModeloHorarioDbusInfo> _modelosHorarios = Array.Empty<ModeloHorarioDbusInfo>();
|
||||||
|
private string _modeloHorarioSeleccionadoId = "";
|
||||||
|
private string _mensajeModelosHorarios = "";
|
||||||
|
private bool _modelosHorariosCargando = false;
|
||||||
|
private bool _mostrarModelosHorariosPanel = false;
|
||||||
|
|
||||||
|
private ModeloHorarioDbusInfo? ModeloHorarioActivo =>
|
||||||
|
_modelosHorarios.FirstOrDefault(modelo => modelo.Activo);
|
||||||
|
|
||||||
|
private void AbrirModelosHorariosPanel()
|
||||||
|
{
|
||||||
|
_mostrarModelosHorariosPanel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CerrarModelosHorariosPanel()
|
||||||
|
{
|
||||||
|
_mostrarModelosHorariosPanel = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CargarModelosHorariosAsync()
|
||||||
|
{
|
||||||
|
_modelosHorariosCargando = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_modelosHorarios = await ArchivoImportacionDbus.ListarModelosAsync();
|
||||||
|
var activo = ModeloHorarioActivo ?? _modelosHorarios.FirstOrDefault();
|
||||||
|
_modeloHorarioSeleccionadoId = activo?.Id ?? "";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_modelosHorarios = Array.Empty<ModeloHorarioDbusInfo>();
|
||||||
|
_modeloHorarioSeleccionadoId = "";
|
||||||
|
_mensajeModelosHorarios = "No se han podido leer los modelos: " + ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_modelosHorariosCargando = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ActualizarModelosHorariosAsync()
|
||||||
|
{
|
||||||
|
_mensajeModelosHorarios = "";
|
||||||
|
await CargarModelosHorariosAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(_mensajeModelosHorarios))
|
||||||
|
_mensajeModelosHorarios = _modelosHorarios.Count == 0
|
||||||
|
? "No hay ZIPs disponibles."
|
||||||
|
: "Lista actualizada.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeleccionarModeloHorarioAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_modeloHorarioSeleccionadoId))
|
||||||
|
{
|
||||||
|
_mensajeModelosHorarios = "Selecciona un ZIP.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var seleccionado = _modelosHorarios
|
||||||
|
.FirstOrDefault(modelo => string.Equals(modelo.Id, _modeloHorarioSeleccionadoId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (seleccionado?.Activo == true)
|
||||||
|
{
|
||||||
|
_mensajeModelosHorarios = "Ese modelo ya esta cargado.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_modelosHorariosCargando = true;
|
||||||
|
_mensajeModelosHorarios = "Cargando modelo...";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ArchivoImportacionDbus.SeleccionarModeloAsync(_modeloHorarioSeleccionadoId);
|
||||||
|
await RecargarModeloHorarioActivoAsync();
|
||||||
|
await CargarModelosHorariosAsync();
|
||||||
|
|
||||||
|
var activo = ModeloHorarioActivo;
|
||||||
|
_mensajeModelosHorarios = activo is null
|
||||||
|
? "Modelo cargado."
|
||||||
|
: "Cargado: " + activo.NombreArchivo;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_mensajeModelosHorarios = "No se ha podido cargar el modelo: " + ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_modelosHorariosCargando = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SubirModeloHorarioAsync(InputFileChangeEventArgs e)
|
||||||
|
{
|
||||||
|
var archivo = e.File;
|
||||||
|
if (archivo is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!archivo.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_mensajeModelosHorarios = "Solo se pueden subir archivos .zip.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_modelosHorariosCargando = true;
|
||||||
|
_mensajeModelosHorarios = "Subiendo ZIP...";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var stream = archivo.OpenReadStream(MaxModeloHorarioUploadBytes);
|
||||||
|
var modelo = await ArchivoImportacionDbus.GuardarModeloSubidoAsync(archivo.Name, stream);
|
||||||
|
|
||||||
|
_modeloHorarioSeleccionadoId = modelo.Id;
|
||||||
|
await RecargarModeloHorarioActivoAsync();
|
||||||
|
await CargarModelosHorariosAsync();
|
||||||
|
_mensajeModelosHorarios = "Subido y cargado: " + modelo.NombreArchivo;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_mensajeModelosHorarios = "No se ha podido subir el ZIP: " + ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_modelosHorariosCargando = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LimpiarDatosLocalesAsync()
|
||||||
|
{
|
||||||
|
bool confirmado;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
confirmado = await JS.InvokeAsync<bool>(
|
||||||
|
"confirm",
|
||||||
|
"¿Seguro que quieres limpiar los datos locales de este navegador? Se borrarán historial, configuración guardada, cachés y datos locales de este sitio.");
|
||||||
|
}
|
||||||
|
catch (JSException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmado)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_modelosHorariosCargando = true;
|
||||||
|
_mensajeModelosHorarios = "Limpiando datos locales...";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("rt.clearLocalBrowserData");
|
||||||
|
_mensajeModelosHorarios = "Datos locales borrados. Recargando...";
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await Task.Delay(250);
|
||||||
|
await JS.InvokeVoidAsync("rt.reloadPage");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_mensajeModelosHorarios = "No se han podido limpiar los datos locales: " + ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_modelosHorariosCargando = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RecargarModeloHorarioActivoAsync()
|
||||||
|
{
|
||||||
|
_ctsCalculo?.Cancel();
|
||||||
|
IniciarNuevaVersionPintadoRuta();
|
||||||
|
|
||||||
|
await CerrarTodasLasPreviewsAsync();
|
||||||
|
await CerrarPopupsAsync();
|
||||||
|
|
||||||
|
if (_mapa is not null)
|
||||||
|
{
|
||||||
|
await ClearRoutesJsAsync();
|
||||||
|
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetEstadoRuta(limpiarOpciones: true);
|
||||||
|
PlanificadorRutasServicio.InvalidarCachesModelo();
|
||||||
|
_busFilteredCache.Clear();
|
||||||
|
InvalidarBusIndex();
|
||||||
|
|
||||||
|
_itItems.Clear();
|
||||||
|
_itSeleccionadoId = null;
|
||||||
|
_itFiltro = "";
|
||||||
|
_selectItKey++;
|
||||||
|
_nextDeps.Clear();
|
||||||
|
_horariosOk = false;
|
||||||
|
_horariosError = null;
|
||||||
|
|
||||||
|
_tareaCargaRed = RedBusServicio.CargarRedAsync();
|
||||||
|
await _tareaCargaRed;
|
||||||
|
|
||||||
|
if (_usarHorariosTeoricos)
|
||||||
|
await EnsureHorariosLoadedAsync();
|
||||||
|
|
||||||
|
if (RedBusServicio.Paradas.Count > 0)
|
||||||
|
{
|
||||||
|
EnsureBusIndexBuilt();
|
||||||
|
BuildItinerariosDropdown();
|
||||||
|
SincronizarSeleccionItinerarioConFiltro();
|
||||||
|
}
|
||||||
|
|
||||||
|
_paradasPintadas = false;
|
||||||
|
if (_mapa is not null)
|
||||||
|
{
|
||||||
|
if (_mostrarParadas)
|
||||||
|
await DibujarParadasAsync();
|
||||||
|
else
|
||||||
|
await OcultarParadasAsync();
|
||||||
|
|
||||||
|
await SincronizarMarcadoresOrigenDestinoJsAsync();
|
||||||
|
_paradasPintadas = _mostrarParadas;
|
||||||
|
}
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatearTamanoModelo(long bytes)
|
||||||
|
{
|
||||||
|
if (bytes >= 1024L * 1024L)
|
||||||
|
return (bytes / 1024d / 1024d).ToString("0.0", CultureInfo.InvariantCulture) + " MB";
|
||||||
|
|
||||||
|
if (bytes >= 1024L)
|
||||||
|
return (bytes / 1024d).ToString("0", CultureInfo.InvariantCulture) + " KB";
|
||||||
|
|
||||||
|
return bytes.ToString(CultureInfo.InvariantCulture) + " B";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatearFechaModelo(DateTime fechaUtc)
|
||||||
|
=> fechaUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
360
RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs
Normal file
360
RutasDBUS/Components/Pages/PlanificadorRutas.Persistencia.cs
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Components.Pages;
|
||||||
|
|
||||||
|
public partial class PlanificadorRutas
|
||||||
|
{
|
||||||
|
private const string ClaveConfiguracionLocal = "rutasdbus.planificador.config.v1";
|
||||||
|
private bool _configuracionLocalCargada = false;
|
||||||
|
private bool _mostrarConfirmacionRestablecer = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantánea persistida de la configuración del planificador.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ConfiguracionLocalPlanificador
|
||||||
|
{
|
||||||
|
public bool? MostrarParadas { get; set; }
|
||||||
|
public bool UsarHorariosTeoricos { get; set; }
|
||||||
|
public int SegundosPorParadaBusManual { get; set; }
|
||||||
|
public bool ActivarRangoLineas { get; set; }
|
||||||
|
public int LineaMin { get; set; }
|
||||||
|
public int LineaMax { get; set; }
|
||||||
|
public List<int>? LineasPermitidas { get; set; }
|
||||||
|
public bool ActivarRangoItinerarios { get; set; }
|
||||||
|
public int ItinerarioMin { get; set; }
|
||||||
|
public int ItinerarioMax { get; set; }
|
||||||
|
public bool ActivarRangoTransbordos { get; set; }
|
||||||
|
public int MinTransbordosPermitidos { get; set; }
|
||||||
|
public int MaxTransbordosPermitidos { get; set; }
|
||||||
|
public bool UsarHoraSimulada { get; set; }
|
||||||
|
public string? FechaSimulada { get; set; }
|
||||||
|
public string? HoraSimulada { get; set; }
|
||||||
|
public double? FactorPenalizacionDistanciaPie { get; set; }
|
||||||
|
public double? ParametroPenalizacionDistanciaPieMetros { get; set; }
|
||||||
|
public double? CoeficienteEsfuerzoAltura { get; set; }
|
||||||
|
public double? PesoTransbordoRankingMinutos { get; set; }
|
||||||
|
public double VelocidadPieKmH { get; set; }
|
||||||
|
public double VelocidadBusKmH { get; set; }
|
||||||
|
public int ElevMaxPuntosPorTramo { get; set; }
|
||||||
|
public int RadioCandidatasMetros { get; set; }
|
||||||
|
public int MargenTransbordoSegundos { get; set; }
|
||||||
|
public string? WalkProvider { get; set; }
|
||||||
|
public int NivelBrilloMapa { get; set; }
|
||||||
|
public bool UsarColorRealLineaEnItinerarios { get; set; }
|
||||||
|
public bool UsarOsrmParaRanking { get; set; }
|
||||||
|
public string? CriterioPrioritarioRuta { get; set; }
|
||||||
|
public string? PreferenciaEtiquetaRuta { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Carga la configuración persistida del navegador y la aplica al estado actual.
|
||||||
|
/// </summary>
|
||||||
|
private async Task CargarConfiguracionLocalAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = await JS.InvokeAsync<string?>("rt.loadLocalValue", ClaveConfiguracionLocal);
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var config = JsonSerializer.Deserialize<ConfiguracionLocalPlanificador>(json);
|
||||||
|
if (config is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AplicarConfiguracionLocal(config);
|
||||||
|
}
|
||||||
|
catch (JSException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_configuracionLocalCargada = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Guarda la configuración actual del planificador en el navegador.
|
||||||
|
/// </summary>
|
||||||
|
private async Task PersistirConfiguracionAsync()
|
||||||
|
{
|
||||||
|
if (!_configuracionLocalCargada)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_usarHorariosTeoricos)
|
||||||
|
_segundosPorParadaBusManual = Math.Max(0, _segundosPorParadaBus);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(CrearConfiguracionLocal());
|
||||||
|
await JS.InvokeVoidAsync("rt.saveLocalValue", ClaveConfiguracionLocal, json);
|
||||||
|
}
|
||||||
|
catch (JSException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sincroniza el valor manual de segundos por parada y persiste la configuración.
|
||||||
|
/// </summary>
|
||||||
|
private async Task OnSegundosPorParadaBusChanged()
|
||||||
|
{
|
||||||
|
_segundosPorParadaBus = Math.Max(0, _segundosPorParadaBus);
|
||||||
|
_segundosPorParadaBusManual = _segundosPorParadaBus;
|
||||||
|
await PersistirConfiguracionAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construye la instantánea persistible a partir del estado actual.
|
||||||
|
/// </summary>
|
||||||
|
private ConfiguracionLocalPlanificador CrearConfiguracionLocal()
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
MostrarParadas = _mostrarParadas,
|
||||||
|
UsarHorariosTeoricos = _usarHorariosTeoricos,
|
||||||
|
SegundosPorParadaBusManual = Math.Max(0, _segundosPorParadaBusManual),
|
||||||
|
ActivarRangoLineas = _activarRangoLineas,
|
||||||
|
LineaMin = _lineaMin,
|
||||||
|
LineaMax = _lineaMax,
|
||||||
|
LineasPermitidas = ObtenerLineasPermitidasSeleccionadas().ToList(),
|
||||||
|
ActivarRangoItinerarios = _activarRangoItinerarios,
|
||||||
|
ItinerarioMin = _itinerarioMin,
|
||||||
|
ItinerarioMax = _itinerarioMax,
|
||||||
|
ActivarRangoTransbordos = _activarRangoTransbordos,
|
||||||
|
MinTransbordosPermitidos = _minTransbordosPermitidos,
|
||||||
|
MaxTransbordosPermitidos = _maxTransbordosPermitidos,
|
||||||
|
UsarHoraSimulada = _usarHoraSimulada,
|
||||||
|
FechaSimulada = _fechaSimulada.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||||
|
HoraSimulada = _horaSimulada.ToString("HH:mm:ss", CultureInfo.InvariantCulture),
|
||||||
|
FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie,
|
||||||
|
ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros,
|
||||||
|
CoeficienteEsfuerzoAltura = _coeficienteEsfuerzoAltura,
|
||||||
|
PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
|
||||||
|
VelocidadPieKmH = _velocidadPieKmH,
|
||||||
|
VelocidadBusKmH = _velocidadBusKmH,
|
||||||
|
ElevMaxPuntosPorTramo = _elevMaxPuntosPorTramo,
|
||||||
|
RadioCandidatasMetros = _radioCandidatasMetros,
|
||||||
|
MargenTransbordoSegundos = _margenTransbordoSegundos,
|
||||||
|
WalkProvider = _walkProvider.ToString(),
|
||||||
|
NivelBrilloMapa = _nivelBrilloMapa,
|
||||||
|
UsarColorRealLineaEnItinerarios = _usarColorRealLineaEnItinerarios,
|
||||||
|
UsarOsrmParaRanking = _usarOsrmParaRanking,
|
||||||
|
CriterioPrioritarioRuta = _criterioPrioritarioRuta,
|
||||||
|
PreferenciaEtiquetaRuta = _preferenciaEtiquetaRuta
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aplica al componente una configuración previamente persistida.
|
||||||
|
/// </summary>
|
||||||
|
private void AplicarConfiguracionLocal(ConfiguracionLocalPlanificador config)
|
||||||
|
{
|
||||||
|
_mostrarParadas = config.MostrarParadas ?? true;
|
||||||
|
_usarHorariosTeoricos = config.UsarHorariosTeoricos;
|
||||||
|
_segundosPorParadaBusManual = Math.Clamp(config.SegundosPorParadaBusManual, 0, 3600);
|
||||||
|
_segundosPorParadaBus = _segundosPorParadaBusManual;
|
||||||
|
|
||||||
|
_lineaMin = Math.Clamp(config.LineaMin, 1, 200);
|
||||||
|
_lineaMax = Math.Clamp(config.LineaMax, 1, 200);
|
||||||
|
_lineasPermitidas.Clear();
|
||||||
|
foreach (var linea in NormalizarLineasPermitidas(config.LineasPermitidas, config.LineaMin, config.LineaMax))
|
||||||
|
_lineasPermitidas.Add(linea);
|
||||||
|
_activarRangoLineas = config.ActivarRangoLineas;
|
||||||
|
|
||||||
|
_activarRangoItinerarios = config.ActivarRangoItinerarios;
|
||||||
|
_itinerarioMin = Math.Clamp(config.ItinerarioMin, 1, 99);
|
||||||
|
_itinerarioMax = Math.Clamp(config.ItinerarioMax, 1, 99);
|
||||||
|
|
||||||
|
_activarRangoTransbordos = config.ActivarRangoTransbordos;
|
||||||
|
_minTransbordosPermitidos = Math.Clamp(config.MinTransbordosPermitidos, 0, 10);
|
||||||
|
_maxTransbordosPermitidos = Math.Clamp(config.MaxTransbordosPermitidos, 0, 10);
|
||||||
|
|
||||||
|
_usarHoraSimulada = config.UsarHoraSimulada;
|
||||||
|
if (DateOnly.TryParse(config.FechaSimulada, out var fechaSimulada))
|
||||||
|
_fechaSimulada = fechaSimulada;
|
||||||
|
if (TimeOnly.TryParse(config.HoraSimulada, out var horaSimulada))
|
||||||
|
_horaSimulada = horaSimulada;
|
||||||
|
|
||||||
|
if (config.FactorPenalizacionDistanciaPie is { } factor && double.IsFinite(factor))
|
||||||
|
_factorPenalizacionDistanciaPie = Math.Abs(factor - 1.3) < 0.0001 ? 1.6 : factor;
|
||||||
|
if (config.ParametroPenalizacionDistanciaPieMetros is { } parametro && double.IsFinite(parametro))
|
||||||
|
_parametroPenalizacionDistanciaPieMetros = parametro;
|
||||||
|
if (config.CoeficienteEsfuerzoAltura is { } coeficienteAltura && double.IsFinite(coeficienteAltura))
|
||||||
|
_coeficienteEsfuerzoAltura = Math.Abs(coeficienteAltura - 1.6) < 0.0001
|
||||||
|
? CoeficienteEsfuerzoAlturaPorDefecto
|
||||||
|
: coeficienteAltura;
|
||||||
|
if (config.PesoTransbordoRankingMinutos is { } pesoTransbordo && double.IsFinite(pesoTransbordo))
|
||||||
|
_pesoTransbordoRankingMinutos = Math.Abs(pesoTransbordo - 8.0) < 0.0001 ? 4.0 : Math.Max(0, pesoTransbordo);
|
||||||
|
_velocidadPieKmH = Math.Clamp(config.VelocidadPieKmH, 0.5, 25.0);
|
||||||
|
_velocidadBusKmH = Math.Clamp(
|
||||||
|
Math.Abs(config.VelocidadBusKmH - 15.0) < 0.0001 ||
|
||||||
|
Math.Abs(config.VelocidadBusKmH - 16.6) < 0.0001
|
||||||
|
? 16.0
|
||||||
|
: config.VelocidadBusKmH,
|
||||||
|
1.0,
|
||||||
|
120.0);
|
||||||
|
_elevMaxPuntosPorTramo = Math.Clamp(config.ElevMaxPuntosPorTramo, 2, 999);
|
||||||
|
_radioCandidatasMetros = Math.Clamp(config.RadioCandidatasMetros, 200, 3500);
|
||||||
|
_margenTransbordoSegundos = Math.Clamp(config.MargenTransbordoSegundos, 0, 1800);
|
||||||
|
_nivelBrilloMapa = Math.Clamp(config.NivelBrilloMapa, 0, 4);
|
||||||
|
_usarColorRealLineaEnItinerarios = config.UsarColorRealLineaEnItinerarios;
|
||||||
|
_usarOsrmParaRanking = config.UsarOsrmParaRanking;
|
||||||
|
_criterioPrioritarioRuta = NormalizarCriterioRuta(config.CriterioPrioritarioRuta, config.PreferenciaEtiquetaRuta);
|
||||||
|
SincronizarPreferenciaDesdeCriterioRuta();
|
||||||
|
|
||||||
|
if (Enum.TryParse<ProveedorCaminata>(config.WalkProvider, ignoreCase: true, out var walkProvider))
|
||||||
|
_walkProvider = walkProvider;
|
||||||
|
|
||||||
|
SincronizarTextosPenalizacionPie();
|
||||||
|
SincronizarSegundosPorParadaConHorarios();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<int> NormalizarLineasPermitidas(
|
||||||
|
IReadOnlyList<int>? lineas,
|
||||||
|
int lineaMinLegacy,
|
||||||
|
int lineaMaxLegacy)
|
||||||
|
{
|
||||||
|
if (lineas is { Count: > 0 })
|
||||||
|
{
|
||||||
|
return lineas
|
||||||
|
.Where(linea => linea > 0 && linea <= 999)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(linea => linea)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineaMinLegacy <= 0 && lineaMaxLegacy <= 0)
|
||||||
|
return Array.Empty<int>();
|
||||||
|
|
||||||
|
var min = Math.Clamp(Math.Min(lineaMinLegacy, lineaMaxLegacy), 1, 999);
|
||||||
|
var max = Math.Clamp(Math.Max(lineaMinLegacy, lineaMaxLegacy), 1, 999);
|
||||||
|
var esRangoPorDefecto = min <= 1 && max >= 200;
|
||||||
|
if (esRangoPorDefecto)
|
||||||
|
return Array.Empty<int>();
|
||||||
|
|
||||||
|
return Enumerable.Range(min, max - min + 1).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abre el cuadro de confirmación para restablecer valores.
|
||||||
|
/// </summary>
|
||||||
|
private void PedirConfirmacionRestablecer()
|
||||||
|
{
|
||||||
|
_mostrarConfirmacionRestablecer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cierra el cuadro de confirmación de restablecer.
|
||||||
|
/// </summary>
|
||||||
|
private void CancelarConfirmacionRestablecer()
|
||||||
|
{
|
||||||
|
_mostrarConfirmacionRestablecer = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restablece la configuracion avanzada a los valores por defecto de la aplicacion.
|
||||||
|
/// </summary>
|
||||||
|
private async Task RestablecerConfiguracionPorDefectoAsync()
|
||||||
|
{
|
||||||
|
_mostrarConfirmacionRestablecer = false;
|
||||||
|
bool confirmado = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
confirmado = await JS.InvokeAsync<bool>("confirm", "¿Seguro que quieres restablecer la configuración avanzada a sus valores por defecto?");
|
||||||
|
}
|
||||||
|
catch (JSException)
|
||||||
|
{
|
||||||
|
confirmado = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmado)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_mostrarParadas = true;
|
||||||
|
_usarHorariosTeoricos = true;
|
||||||
|
_segundosPorParadaBusManual = 30;
|
||||||
|
_segundosPorParadaBus = 30;
|
||||||
|
|
||||||
|
_activarRangoItinerarios = true;
|
||||||
|
_itinerarioMin = 1;
|
||||||
|
_itinerarioMax = 99;
|
||||||
|
|
||||||
|
_activarRangoTransbordos = true;
|
||||||
|
_minTransbordosPermitidos = 0;
|
||||||
|
_maxTransbordosPermitidos = 3;
|
||||||
|
|
||||||
|
_factorPenalizacionDistanciaPie = 1.6;
|
||||||
|
_parametroPenalizacionDistanciaPieMetros = 100.0;
|
||||||
|
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
||||||
|
_pesoTransbordoRankingMinutos = 4.0;
|
||||||
|
_velocidadPieKmH = 3.6;
|
||||||
|
_velocidadBusKmH = 16.0;
|
||||||
|
_minutosTransbordo = 5;
|
||||||
|
_margenTransbordoSegundos = 120;
|
||||||
|
_elevMaxPuntosPorTramo = 999;
|
||||||
|
_radioCandidatasMetros = 1200;
|
||||||
|
_walkProvider = ProveedorCaminata.Valhalla;
|
||||||
|
_nivelBrilloMapa = 0;
|
||||||
|
_usarColorRealLineaEnItinerarios = false;
|
||||||
|
_usarOsrmParaRanking = false;
|
||||||
|
|
||||||
|
if (_usarHoraSimulada)
|
||||||
|
StopClock();
|
||||||
|
else
|
||||||
|
StartClock();
|
||||||
|
SincronizarTextosPenalizacionPie();
|
||||||
|
SincronizarSegundosPorParadaConHorarios();
|
||||||
|
await RefrescarHoraUIAsync();
|
||||||
|
await PersistirConfiguracionAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restablece la configuración desde el modal personalizado centrado.
|
||||||
|
/// </summary>
|
||||||
|
private async Task ConfirmarRestablecerConfiguracionAsync()
|
||||||
|
{
|
||||||
|
_mostrarConfirmacionRestablecer = false;
|
||||||
|
|
||||||
|
_mostrarParadas = true;
|
||||||
|
_usarHorariosTeoricos = true;
|
||||||
|
_segundosPorParadaBusManual = 30;
|
||||||
|
_segundosPorParadaBus = 30;
|
||||||
|
|
||||||
|
_activarRangoItinerarios = true;
|
||||||
|
_itinerarioMin = 1;
|
||||||
|
_itinerarioMax = 99;
|
||||||
|
|
||||||
|
_activarRangoTransbordos = true;
|
||||||
|
_minTransbordosPermitidos = 0;
|
||||||
|
_maxTransbordosPermitidos = 3;
|
||||||
|
|
||||||
|
_factorPenalizacionDistanciaPie = 1.6;
|
||||||
|
_parametroPenalizacionDistanciaPieMetros = 100.0;
|
||||||
|
_coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
|
||||||
|
_pesoTransbordoRankingMinutos = 4.0;
|
||||||
|
_velocidadPieKmH = 3.6;
|
||||||
|
_velocidadBusKmH = 16.0;
|
||||||
|
_minutosTransbordo = 5;
|
||||||
|
_margenTransbordoSegundos = 120;
|
||||||
|
_elevMaxPuntosPorTramo = 999;
|
||||||
|
_radioCandidatasMetros = 1200;
|
||||||
|
_walkProvider = ProveedorCaminata.Valhalla;
|
||||||
|
_nivelBrilloMapa = 0;
|
||||||
|
_usarColorRealLineaEnItinerarios = false;
|
||||||
|
_usarOsrmParaRanking = false;
|
||||||
|
|
||||||
|
if (_usarHoraSimulada)
|
||||||
|
StopClock();
|
||||||
|
else
|
||||||
|
StartClock();
|
||||||
|
SincronizarTextosPenalizacionPie();
|
||||||
|
SincronizarSegundosPorParadaConHorarios();
|
||||||
|
await RefrescarHoraUIAsync();
|
||||||
|
await PersistirConfiguracionAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
2285
RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs
Normal file
2285
RutasDBUS/Components/Pages/PlanificadorRutas.Planificacion.cs
Normal file
File diff suppressed because it is too large
Load Diff
844
RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs
Normal file
844
RutasDBUS/Components/Pages/PlanificadorRutas.Renderizado.cs
Normal file
@@ -0,0 +1,844 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using LeafletForBlazor;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using RutasDBUS.Modelos.Planificacion;
|
||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
using RutasDBUS.Servicios;
|
||||||
|
using RutasDBUS.Servicios.Caminata;
|
||||||
|
using RutasDBUS.Servicios.Horarios;
|
||||||
|
using RutasDBUS.Servicios.Planificacion;
|
||||||
|
using RutasDBUS.Servicios.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Components.Pages;
|
||||||
|
|
||||||
|
public partial class PlanificadorRutas : ComponentBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Asegura que elevacion tramo este disponible.
|
||||||
|
/// </summary>
|
||||||
|
private async Task EnsureElevacionTramoAsync(DetalleTramoRuta tramo)
|
||||||
|
{
|
||||||
|
if (tramo is null) return;
|
||||||
|
|
||||||
|
// Si ya está calculado, nada
|
||||||
|
if (tramo.MuestrasElevacion is not null && tramo.MuestrasElevacion.Count > 0) return;
|
||||||
|
|
||||||
|
// Necesitamos la geometría
|
||||||
|
if (tramo.Linea is null || tramo.Linea.GetLength(0) <= 0) return;
|
||||||
|
|
||||||
|
if (tramo.ElevacionCargando) return;
|
||||||
|
|
||||||
|
tramo.ElevacionCargando = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ctNone = CancellationToken.None;
|
||||||
|
|
||||||
|
// 1) Samples bajo demanda, priorizando G y dejando O como best-effort
|
||||||
|
var samples = await ConstruirMuestrasElevacionAsync(tramo.Linea, ctNone);
|
||||||
|
tramo.MuestrasElevacion = samples;
|
||||||
|
|
||||||
|
// 2) Resumen O solo con lo que ya haya llegado, sin bloquear más la UI
|
||||||
|
var muestrasOpen = samples
|
||||||
|
.Where(x => x.Elevacion is not null)
|
||||||
|
.Select(x => x.Elevacion!.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
tramo.ElevInicio = samples.FirstOrDefault()?.Elevacion;
|
||||||
|
tramo.ElevFin = samples.LastOrDefault()?.Elevacion;
|
||||||
|
tramo.ElevMin = muestrasOpen.Count > 0 ? muestrasOpen.Min() : null;
|
||||||
|
tramo.ElevMax = muestrasOpen.Count > 0 ? muestrasOpen.Max() : null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// silencioso: la elevación es informativa
|
||||||
|
tramo.MuestrasElevacion ??= new List<MuestraElevacion>();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
tramo.ElevacionCargando = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pinta opcion ruta completa.
|
||||||
|
/// </summary>
|
||||||
|
private async Task PintarOpcionRutaCompleta(AlternativaRuta opcion)
|
||||||
|
{
|
||||||
|
var versionPintado = IniciarNuevaVersionPintadoRuta();
|
||||||
|
var mensajeAnterior = _mensajeRuta;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ClearRoutesJsAsync();
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
|
||||||
|
if (opcion.EsSoloAPie)
|
||||||
|
{
|
||||||
|
await PintarRutaSoloAPie(opcion.LineaSoloAPie, opcion.DistanciaSoloAPie, versionPintado: versionPintado);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await PintarRutaDesdeAlternativa(opcion.OrigenParada, opcion.DestinoParada, opcion.AlternativaBus!, versionPintado);
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
await PintarParadasSolucionSeleccionadaAsync(opcion, versionPintado);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
await CerrarPopupsAsync();
|
||||||
|
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
await FitRoutesJsAsync();
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_mensajeRuta))
|
||||||
|
_mensajeRuta = mensajeAnterior;
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_mensajeRuta = $"No se ha podido representar la ruta seleccionada: {ex.Message}";
|
||||||
|
_hayRutaValida = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pinta las paradas recorridas por la solución seleccionada con el color de cada tramo de bus.
|
||||||
|
/// </summary>
|
||||||
|
private async Task PintarParadasSolucionSeleccionadaAsync(AlternativaRuta opcion, int versionPintado)
|
||||||
|
{
|
||||||
|
await ClearRouteStopsJsAsync();
|
||||||
|
LimpiarParadasRutaVisibles();
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
|
||||||
|
var tramos = opcion.AlternativaBus?.Tramos;
|
||||||
|
if (opcion.EsSoloAPie || tramos is null || tramos.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (var indice = 0; indice < tramos.Count; indice++)
|
||||||
|
{
|
||||||
|
var tramo = tramos[indice];
|
||||||
|
if (tramo?.Variante is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var paradasTramo = BuildParadasForTramoBus(tramo);
|
||||||
|
if (paradasTramo.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var stops = BuildStopsDesdeParadas(paradasTramo);
|
||||||
|
|
||||||
|
var id = $"route_stops_{indice}_{tramo.Variante.Linea.Codigo}_{tramo.Variante.CodigoItinerario}_{NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo)}";
|
||||||
|
var color = ColorCssDesdeVariante(tramo.Variante);
|
||||||
|
|
||||||
|
await DrawRouteStopsJsAsync(id, stops, color, radius: 8, weight: 2, opacity: 1.0, fillOpacity: 1.0);
|
||||||
|
RegistrarParadasRutaVisibles(id, paradasTramo);
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pinta ruta solo a pie.
|
||||||
|
/// </summary>
|
||||||
|
private async Task PintarRutaSoloAPie(double[,]? lineaPre = null, double distPre = 0, int versionPintado = 0)
|
||||||
|
{
|
||||||
|
if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null) return;
|
||||||
|
|
||||||
|
ResetEstadoRuta(limpiarMensaje: false);
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
|
||||||
|
double[,]? linea = lineaPre;
|
||||||
|
double distancia = distPre;
|
||||||
|
|
||||||
|
if (linea is null)
|
||||||
|
{
|
||||||
|
var w = await WalkCached(_coordenadasOrigen[0], _coordenadasOrigen[1], _coordenadasDestino[0], _coordenadasDestino[1]);
|
||||||
|
linea = w.line;
|
||||||
|
distancia = w.dist;
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linea is null)
|
||||||
|
{
|
||||||
|
_mensajeRuta = "No he podido calcular una ruta a pie entre los puntos seleccionados.";
|
||||||
|
_hayRutaValida = false;
|
||||||
|
StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ duración por velocidad configurada
|
||||||
|
double duracion = (distancia > 0) ? EstimarDuracionPieSegundos(distancia) : 0;
|
||||||
|
|
||||||
|
_distanciaAPieInicio = distancia;
|
||||||
|
_duracionTotalSegundos = duracion;
|
||||||
|
|
||||||
|
// ✅ Guardamos el tramo andando SIN calcular elevación
|
||||||
|
_tramosRuta.Add(new DetalleTramoRuta
|
||||||
|
{
|
||||||
|
Tipo = "A pie",
|
||||||
|
Descripcion = CapitalizarPalabrasClave("Camina desde tu punto de origen hasta tu destino final."),
|
||||||
|
DistanciaMetros = distancia,
|
||||||
|
DuracionSegundos = duracion,
|
||||||
|
|
||||||
|
// clave para lazy elevation
|
||||||
|
Linea = linea,
|
||||||
|
|
||||||
|
// elevación aún no
|
||||||
|
ElevInicio = null,
|
||||||
|
ElevFin = null,
|
||||||
|
ElevMin = null,
|
||||||
|
ElevMax = null,
|
||||||
|
MuestrasElevacion = null
|
||||||
|
});
|
||||||
|
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
await DrawRouteJsAsync("walk_full", linea, "#dc2626", 5, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
|
||||||
|
_mensajeRuta = CapitalizarPalabrasClave(
|
||||||
|
$"Puedes realizar todo el recorrido andando: {FormatearDistancia(distancia)} aproximadamente."
|
||||||
|
);
|
||||||
|
|
||||||
|
_hayRutaValida = true;
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pinta ruta desde alternativa.
|
||||||
|
/// </summary>
|
||||||
|
private async Task PintarRutaDesdeAlternativa(
|
||||||
|
Parada paradaOrigenCandidata,
|
||||||
|
Parada paradaDestinoCandidata,
|
||||||
|
RutaBusAlternativa alternativa,
|
||||||
|
int versionPintado)
|
||||||
|
{
|
||||||
|
if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null) return;
|
||||||
|
|
||||||
|
ResetEstadoRuta(limpiarMensaje: false);
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
|
||||||
|
var tramosBus = alternativa.Tramos;
|
||||||
|
var lineasBus = RedBusServicio.ConstruirLineasBus(tramosBus, out var distanciaBusTotal);
|
||||||
|
|
||||||
|
Parada paradaInicioBus = paradaOrigenCandidata;
|
||||||
|
Parada paradaFinBus = paradaDestinoCandidata;
|
||||||
|
|
||||||
|
if (tramosBus.Count > 0 && lineasBus.Count > 0)
|
||||||
|
{
|
||||||
|
var primerTramo = tramosBus.First();
|
||||||
|
var ultimoTramo = tramosBus.Last();
|
||||||
|
paradaInicioBus = ResolverParadaFisica(
|
||||||
|
primerTramo.CodigoParadaInicio,
|
||||||
|
ObtenerPrimerPunto(lineasBus[0]),
|
||||||
|
paradaOrigenCandidata);
|
||||||
|
paradaFinBus = ResolverParadaFisica(
|
||||||
|
ultimoTramo.CodigoParadaFin,
|
||||||
|
ObtenerUltimoPunto(lineasBus[^1]),
|
||||||
|
paradaDestinoCandidata);
|
||||||
|
}
|
||||||
|
|
||||||
|
_paradaOrigenSeleccionada = paradaInicioBus;
|
||||||
|
_paradaDestinoSeleccionada = paradaFinBus;
|
||||||
|
|
||||||
|
_numeroTransbordos = CalcularTransbordosReales(alternativa);
|
||||||
|
|
||||||
|
_primerTransbordoseleccionado = null;
|
||||||
|
if (alternativa.CodigosParadasTransbordo is not null && alternativa.CodigosParadasTransbordo.Count > 0)
|
||||||
|
{
|
||||||
|
var codigoPrimera = alternativa.CodigosParadasTransbordo[0];
|
||||||
|
_primerTransbordoseleccionado = ResolverParadaFisica(codigoPrimera);
|
||||||
|
}
|
||||||
|
|
||||||
|
_distanciaAPieInicio = 0;
|
||||||
|
_distanciaAPieFin = 0;
|
||||||
|
_distanciaEnBus = 0;
|
||||||
|
_duracionTotalSegundos = 0;
|
||||||
|
|
||||||
|
var now = AhoraLocal();
|
||||||
|
var tCursor = now;
|
||||||
|
|
||||||
|
if (_usarHorariosTeoricos)
|
||||||
|
{
|
||||||
|
await EnsureHorariosLoadedAsync();
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// Pie 1
|
||||||
|
// -----------------------
|
||||||
|
var w1 = await WalkCached(_coordenadasOrigen[0], _coordenadasOrigen[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
if (w1.line is not null)
|
||||||
|
{
|
||||||
|
_distanciaAPieInicio = w1.dist;
|
||||||
|
|
||||||
|
var durPie1 = (w1.dist > 0) ? EstimarDuracionPieSegundos(w1.dist) : 0;
|
||||||
|
var tIni = tCursor;
|
||||||
|
tCursor = AddSecondsSafe(tCursor, durPie1);
|
||||||
|
|
||||||
|
_duracionTotalSegundos += durPie1;
|
||||||
|
|
||||||
|
_tramosRuta.Add(new DetalleTramoRuta
|
||||||
|
{
|
||||||
|
Tipo = "A pie",
|
||||||
|
Descripcion = CapitalizarPalabrasClave(
|
||||||
|
$"{HHmm(tIni)} -> {HHmm(tCursor)} - Desde tu punto de origen hasta la Parada \"{paradaInicioBus.Nombre}\" ({CodigoParadaFormateado(paradaInicioBus.Codigo)})."
|
||||||
|
),
|
||||||
|
DistanciaMetros = w1.dist,
|
||||||
|
DuracionSegundos = durPie1,
|
||||||
|
Linea = w1.line
|
||||||
|
});
|
||||||
|
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
await DrawRouteJsAsync("walk_1", w1.line, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// Bus
|
||||||
|
// -----------------------
|
||||||
|
_distanciaEnBus = distanciaBusTotal;
|
||||||
|
var lineasUsadasVisuales = new List<string>(Math.Max(1, tramosBus.Count));
|
||||||
|
|
||||||
|
Parada paradaUltimaBus = paradaInicioBus;
|
||||||
|
var ultimoPuntoDibujado = w1.line is not null
|
||||||
|
? ObtenerUltimoPunto(w1.line)
|
||||||
|
: new[] { paradaInicioBus.Latitud, paradaInicioBus.Longitud };
|
||||||
|
|
||||||
|
for (int i = 0; i < tramosBus.Count; i++)
|
||||||
|
{
|
||||||
|
var tramo = tramosBus[i];
|
||||||
|
var esTransbordoReal = i > 0 && UtilidadesTransitoDbus.EsTransbordoReal(tramosBus[i - 1], tramo);
|
||||||
|
var infoVariante = tramo.Variante;
|
||||||
|
var lineaGeom = lineasBus[i];
|
||||||
|
|
||||||
|
string codParadaIni = infoVariante.CodigosParadas[tramo.IndiceInicio];
|
||||||
|
string codParadaFin = infoVariante.CodigosParadas[tramo.IndiceFin];
|
||||||
|
|
||||||
|
var puntoInicioTramo = ObtenerPrimerPunto(lineaGeom);
|
||||||
|
var puntoFinTramo = ObtenerUltimoPunto(lineaGeom);
|
||||||
|
|
||||||
|
var pIniTramo = ResolverParadaFisica(codParadaIni, puntoInicioTramo, i == 0 ? paradaInicioBus : paradaUltimaBus);
|
||||||
|
var pFinTramo = ResolverParadaFisica(codParadaFin, puntoFinTramo, i == tramosBus.Count - 1 ? paradaFinBus : paradaUltimaBus);
|
||||||
|
var distanciaUnion = CalcularDistanciaEntrePuntos(ultimoPuntoDibujado, puntoInicioTramo);
|
||||||
|
|
||||||
|
if (ultimoPuntoDibujado is not null && puntoInicioTramo is not null && distanciaUnion > 0)
|
||||||
|
{
|
||||||
|
if (distanciaUnion <= UmbralCosturaVisualMetros)
|
||||||
|
{
|
||||||
|
EstablecerPrimerPunto(lineaGeom, ultimoPuntoDibujado);
|
||||||
|
puntoInicioTramo = ultimoPuntoDibujado;
|
||||||
|
}
|
||||||
|
else if (!esTransbordoReal && i > 0 && distanciaUnion <= UmbralUnionPeatonalAutomaticaMetros)
|
||||||
|
{
|
||||||
|
EstablecerPrimerPunto(lineaGeom, ultimoPuntoDibujado);
|
||||||
|
puntoInicioTramo = ultimoPuntoDibujado;
|
||||||
|
}
|
||||||
|
else if ((i == 0 || esTransbordoReal) && distanciaUnion <= UmbralUnionPeatonalAutomaticaMetros)
|
||||||
|
{
|
||||||
|
var union = await WalkCached(
|
||||||
|
ultimoPuntoDibujado[0],
|
||||||
|
ultimoPuntoDibujado[1],
|
||||||
|
puntoInicioTramo[0],
|
||||||
|
puntoInicioTramo[1]);
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
|
||||||
|
var lineaUnion = union.line ?? ConstruirLineaDirecta(ultimoPuntoDibujado, puntoInicioTramo);
|
||||||
|
var distanciaUnionReal = union.dist > 0 ? union.dist : distanciaUnion;
|
||||||
|
var duracionUnion = EstimarDuracionPieSegundos(distanciaUnionReal);
|
||||||
|
var tIniWalkUnion = tCursor;
|
||||||
|
tCursor = AddSecondsSafe(tCursor, duracionUnion);
|
||||||
|
|
||||||
|
_duracionTotalSegundos += duracionUnion;
|
||||||
|
if (i == 0)
|
||||||
|
_distanciaAPieInicio += distanciaUnionReal;
|
||||||
|
|
||||||
|
_tramosRuta.Add(new DetalleTramoRuta
|
||||||
|
{
|
||||||
|
Tipo = i == 0 ? "A pie (Ajuste acceso)" : "A pie (Transbordo)",
|
||||||
|
Descripcion = CapitalizarPalabrasClave(
|
||||||
|
i == 0
|
||||||
|
? $"{HHmm(tIniWalkUnion)} -> {HHmm(tCursor)} - Camina desde el final del acceso peatonal hasta la Parada \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
|
||||||
|
: $"{HHmm(tIniWalkUnion)} -> {HHmm(tCursor)} - Camina desde la Parada \"{paradaUltimaBus.Nombre}\" ({CodigoParadaFormateado(paradaUltimaBus.Codigo)}) hasta \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
|
||||||
|
),
|
||||||
|
DistanciaMetros = distanciaUnionReal,
|
||||||
|
DuracionSegundos = duracionUnion,
|
||||||
|
Linea = lineaUnion
|
||||||
|
});
|
||||||
|
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
await DrawRouteJsAsync($"walk_union_{i}", lineaUnion, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaUnion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var distanciaTransferenciaPendiente = CalcularDistanciaEntrePuntos(ultimoPuntoDibujado, puntoInicioTramo);
|
||||||
|
if (i > 0 &&
|
||||||
|
esTransbordoReal &&
|
||||||
|
ultimoPuntoDibujado is not null &&
|
||||||
|
puntoInicioTramo is not null &&
|
||||||
|
distanciaTransferenciaPendiente > UmbralUnionPeatonalAutomaticaMetros)
|
||||||
|
{
|
||||||
|
_mensajeRuta = "La alternativa seleccionada requiere un transbordo a pie demasiado largo. Recalcula la ruta para elegir otra combinacion.";
|
||||||
|
StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i > 0 &&
|
||||||
|
esTransbordoReal &&
|
||||||
|
ultimoPuntoDibujado is not null &&
|
||||||
|
puntoInicioTramo is not null &&
|
||||||
|
distanciaTransferenciaPendiente > UmbralCosturaVisualMetros)
|
||||||
|
{
|
||||||
|
var wt = await WalkCached(
|
||||||
|
ultimoPuntoDibujado[0],
|
||||||
|
ultimoPuntoDibujado[1],
|
||||||
|
puntoInicioTramo[0],
|
||||||
|
puntoInicioTramo[1]);
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
|
||||||
|
var lineaTransferencia = wt.line ?? ConstruirLineaDirecta(ultimoPuntoDibujado, puntoInicioTramo);
|
||||||
|
var distanciaTransferenciaReal = wt.dist > 0 ? wt.dist : distanciaTransferenciaPendiente;
|
||||||
|
var duracionTransferencia = EstimarDuracionPieSegundos(distanciaTransferenciaReal);
|
||||||
|
var tIniWalkTransfer = tCursor;
|
||||||
|
tCursor = AddSecondsSafe(tCursor, duracionTransferencia);
|
||||||
|
|
||||||
|
_duracionTotalSegundos += duracionTransferencia;
|
||||||
|
|
||||||
|
_tramosRuta.Add(new DetalleTramoRuta
|
||||||
|
{
|
||||||
|
Tipo = "A pie (Transbordo)",
|
||||||
|
Descripcion = CapitalizarPalabrasClave(
|
||||||
|
$"{HHmm(tIniWalkTransfer)} -> {HHmm(tCursor)} - Camina desde la Parada \"{paradaUltimaBus.Nombre}\" ({CodigoParadaFormateado(paradaUltimaBus.Codigo)}) hasta \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
|
||||||
|
),
|
||||||
|
DistanciaMetros = distanciaTransferenciaReal,
|
||||||
|
DuracionSegundos = duracionTransferencia,
|
||||||
|
Linea = lineaTransferencia
|
||||||
|
});
|
||||||
|
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
await DrawRouteJsAsync($"walk_transfer_real_{i}", lineaTransferencia, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
|
||||||
|
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaTransferencia);
|
||||||
|
if (ultimoPuntoDibujado is not null)
|
||||||
|
{
|
||||||
|
EstablecerPrimerPunto(lineaGeom, ultimoPuntoDibujado);
|
||||||
|
puntoInicioTramo = ultimoPuntoDibujado;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// Transbordo andando
|
||||||
|
// -----------------------
|
||||||
|
if (false && i > 0 && ultimoPuntoDibujado is null)
|
||||||
|
{
|
||||||
|
if (!paradaUltimaBus.Codigo.Equals(codParadaIni, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var wt = await WalkCached(
|
||||||
|
paradaUltimaBus.Latitud, paradaUltimaBus.Longitud,
|
||||||
|
pIniTramo.Latitud, pIniTramo.Longitud);
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
|
||||||
|
if (wt.line is not null)
|
||||||
|
{
|
||||||
|
var durTrans = (wt.dist > 0) ? EstimarDuracionPieSegundos(wt.dist) : 0;
|
||||||
|
var tIniWalk = tCursor;
|
||||||
|
tCursor = AddSecondsSafe(tCursor, durTrans);
|
||||||
|
|
||||||
|
_duracionTotalSegundos += durTrans;
|
||||||
|
|
||||||
|
_tramosRuta.Add(new DetalleTramoRuta
|
||||||
|
{
|
||||||
|
Tipo = "A pie (Transbordo)",
|
||||||
|
Descripcion = CapitalizarPalabrasClave(
|
||||||
|
$"{HHmm(tIniWalk)} -> {HHmm(tCursor)} - Camina desde la Parada \"{paradaUltimaBus.Nombre}\" ({CodigoParadaFormateado(paradaUltimaBus.Codigo)}) hasta \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
|
||||||
|
),
|
||||||
|
DistanciaMetros = wt.dist,
|
||||||
|
DuracionSegundos = durTrans,
|
||||||
|
Linea = wt.line
|
||||||
|
});
|
||||||
|
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
await DrawRouteJsAsync($"walk_transfer_{i}", wt.line, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// Intentar horario real
|
||||||
|
// -----------------------
|
||||||
|
string codLineaItinSentido = CodigoLineaItinerarioSentido(tramo);
|
||||||
|
string codLineaItinSentidoMostrado = codLineaItinSentido;
|
||||||
|
string lineaCod = LineaCodigoStr(infoVariante);
|
||||||
|
int itin = infoVariante.CodigoItinerario;
|
||||||
|
string sentido = NormalizarSentidoCodigo(infoVariante.Sentido?.Codigo);
|
||||||
|
|
||||||
|
DateTime? depT = null;
|
||||||
|
DateTime? arrT = null;
|
||||||
|
|
||||||
|
if (_usarHorariosTeoricos && _horariosOk)
|
||||||
|
{
|
||||||
|
var primeraSalidaAceptable = esTransbordoReal
|
||||||
|
? tCursor.AddSeconds(Math.Clamp(_margenTransbordoSegundos, 0, 1800))
|
||||||
|
: tCursor;
|
||||||
|
var fecha = DateOnly.FromDateTime(primeraSalidaAceptable);
|
||||||
|
var horaMin = TimeOnly.FromDateTime(primeraSalidaAceptable);
|
||||||
|
|
||||||
|
if (Horarios.TryGetNextBusSegment(
|
||||||
|
codParadaIni,
|
||||||
|
codParadaFin,
|
||||||
|
lineaCod,
|
||||||
|
itin,
|
||||||
|
sentido,
|
||||||
|
fecha,
|
||||||
|
horaMin,
|
||||||
|
out var dep,
|
||||||
|
out var arr,
|
||||||
|
out var expIdReal))
|
||||||
|
{
|
||||||
|
depT = dep;
|
||||||
|
arrT = arr;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(expIdReal))
|
||||||
|
{
|
||||||
|
var codigoReal = Horarios.GetLineaItinerarioSentidoText(expIdReal, lineaCod);
|
||||||
|
if (!string.IsNullOrWhiteSpace(codigoReal) && !codigoReal.Contains('?'))
|
||||||
|
codLineaItinSentidoMostrado = codigoReal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// Si hay horario real: usarlo
|
||||||
|
// Si no: NO inventar horas
|
||||||
|
// -----------------------
|
||||||
|
if (depT is not null && arrT is not null)
|
||||||
|
{
|
||||||
|
void AgregarContinuidadCircularEnBus(DateTime inicio, DateTime fin, double duracionSegundos)
|
||||||
|
{
|
||||||
|
_tramosRuta.Add(new DetalleTramoRuta
|
||||||
|
{
|
||||||
|
Tipo = duracionSegundos > 1 ? "Espera en bus" : "Continuidad en bus",
|
||||||
|
Descripcion = CapitalizarPalabrasClave(
|
||||||
|
$"{HHmm(inicio)} -> {HHmm(fin)} - continuidad circular de la linea {codLineaItinSentidoMostrado}; Parada \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
|
||||||
|
),
|
||||||
|
DistanciaMetros = 0,
|
||||||
|
DuracionSegundos = Math.Max(0, duracionSegundos)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depT.Value > tCursor)
|
||||||
|
{
|
||||||
|
var wait = (depT.Value - tCursor).TotalSeconds;
|
||||||
|
if (wait > 1)
|
||||||
|
{
|
||||||
|
var tIniWait = tCursor;
|
||||||
|
tCursor = depT.Value;
|
||||||
|
var esperaEnBus = i > 0 && !esTransbordoReal;
|
||||||
|
|
||||||
|
_duracionTotalSegundos += wait;
|
||||||
|
|
||||||
|
if (esperaEnBus)
|
||||||
|
{
|
||||||
|
AgregarContinuidadCircularEnBus(tIniWait, tCursor, wait);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_tramosRuta.Add(new DetalleTramoRuta
|
||||||
|
{
|
||||||
|
Tipo = "Espera",
|
||||||
|
Descripcion = CapitalizarPalabrasClave(
|
||||||
|
$"{HHmm(tIniWait)} -> {HHmm(tCursor)} - En \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
|
||||||
|
),
|
||||||
|
DistanciaMetros = 0,
|
||||||
|
DuracionSegundos = wait
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (i > 0 && !esTransbordoReal && Math.Abs((depT.Value - tCursor).TotalSeconds) <= 1)
|
||||||
|
{
|
||||||
|
AgregarContinuidadCircularEnBus(tCursor, tCursor, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var durBus = (arrT.Value - depT.Value).TotalSeconds;
|
||||||
|
if (durBus < 0) durBus = 0;
|
||||||
|
|
||||||
|
_duracionTotalSegundos += durBus;
|
||||||
|
tCursor = arrT.Value;
|
||||||
|
|
||||||
|
_tramosRuta.Add(new DetalleTramoRuta
|
||||||
|
{
|
||||||
|
Tipo = "Bus",
|
||||||
|
Descripcion = CapitalizarPalabrasClave(
|
||||||
|
$"{HHmm(depT.Value)} -> {HHmm(arrT.Value)} - Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre})."
|
||||||
|
),
|
||||||
|
DistanciaMetros = 0,
|
||||||
|
DuracionSegundos = durBus,
|
||||||
|
VarianteBus = infoVariante
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
double distanciaTramo = 0;
|
||||||
|
for (int p = 1; p < lineaGeom.GetLength(0); p++)
|
||||||
|
{
|
||||||
|
distanciaTramo += CalcularDistanciaMetros(
|
||||||
|
lineaGeom[p - 1, 0], lineaGeom[p - 1, 1],
|
||||||
|
lineaGeom[p, 0], lineaGeom[p, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int paradasTramo = ContarParadasRecorridas(tramo);
|
||||||
|
double extraParadasSeg = paradasTramo * Math.Max(0, _segundosPorParadaBus);
|
||||||
|
double duracionBusSegundos = EstimarDuracionBusSegundos(distanciaTramo) + extraParadasSeg;
|
||||||
|
|
||||||
|
_duracionTotalSegundos += duracionBusSegundos;
|
||||||
|
tCursor = AddSecondsSafe(tCursor, duracionBusSegundos);
|
||||||
|
|
||||||
|
_tramosRuta.Add(new DetalleTramoRuta
|
||||||
|
{
|
||||||
|
Tipo = "Bus",
|
||||||
|
Descripcion = CapitalizarPalabrasClave(
|
||||||
|
$"Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre})."
|
||||||
|
),
|
||||||
|
DistanciaMetros = distanciaTramo,
|
||||||
|
DuracionSegundos = duracionBusSegundos,
|
||||||
|
VarianteBus = infoVariante
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
lineasUsadasVisuales.Add(codLineaItinSentidoMostrado);
|
||||||
|
|
||||||
|
string colorCss = RedBusServicio.ConvertirColorArgbACss(infoVariante.Linea.ColorArgb);
|
||||||
|
var fullId = $"full_{infoVariante.Linea.Codigo}_{infoVariante.CodigoItinerario}_{NormalizarSentidoCodigo(infoVariante.Sentido?.Codigo)}_{infoVariante.Variante.Codigo}_{i}_{tramo.IndiceInicio}_{tramo.IndiceFin}";
|
||||||
|
var dashBus = string.Equals(NormalizarSentidoCodigo(infoVariante.Sentido?.Codigo), "V", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? DashArrayBusVuelta
|
||||||
|
: null;
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
await DrawRouteJsAsync(fullId, lineaGeom, colorCss, 5, 0.9, dashArray: dashBus, arrowEnd: true);
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
|
||||||
|
paradaUltimaBus = pFinTramo;
|
||||||
|
ultimoPuntoDibujado = ObtenerUltimoPunto(lineaGeom) ?? new[] { pFinTramo.Latitud, pFinTramo.Longitud };
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// Pie 2
|
||||||
|
// -----------------------
|
||||||
|
var w2 = await WalkCached(paradaFinBus.Latitud, paradaFinBus.Longitud, _coordenadasDestino[0], _coordenadasDestino[1]);
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
|
||||||
|
if (w2.line is not null)
|
||||||
|
{
|
||||||
|
var inicioPieFinal = ObtenerPrimerPunto(w2.line);
|
||||||
|
var distanciaUnionFinal = CalcularDistanciaEntrePuntos(ultimoPuntoDibujado, inicioPieFinal);
|
||||||
|
if (ultimoPuntoDibujado is not null && inicioPieFinal is not null && distanciaUnionFinal > 0)
|
||||||
|
{
|
||||||
|
if (distanciaUnionFinal <= UmbralCosturaVisualMetros)
|
||||||
|
{
|
||||||
|
EstablecerPrimerPunto(w2.line, ultimoPuntoDibujado);
|
||||||
|
}
|
||||||
|
else if (distanciaUnionFinal <= UmbralUnionPeatonalAutomaticaMetros)
|
||||||
|
{
|
||||||
|
var unionFinal = await WalkCached(
|
||||||
|
ultimoPuntoDibujado[0],
|
||||||
|
ultimoPuntoDibujado[1],
|
||||||
|
inicioPieFinal[0],
|
||||||
|
inicioPieFinal[1]);
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
|
||||||
|
var lineaUnionFinal = unionFinal.line ?? ConstruirLineaDirecta(ultimoPuntoDibujado, inicioPieFinal);
|
||||||
|
var distanciaUnionFinalReal = unionFinal.dist > 0 ? unionFinal.dist : distanciaUnionFinal;
|
||||||
|
var duracionUnionFinal = EstimarDuracionPieSegundos(distanciaUnionFinalReal);
|
||||||
|
var tIniUnionFinal = tCursor;
|
||||||
|
tCursor = AddSecondsSafe(tCursor, duracionUnionFinal);
|
||||||
|
|
||||||
|
_duracionTotalSegundos += duracionUnionFinal;
|
||||||
|
_distanciaAPieFin += distanciaUnionFinalReal;
|
||||||
|
|
||||||
|
_tramosRuta.Add(new DetalleTramoRuta
|
||||||
|
{
|
||||||
|
Tipo = "A pie (Ajuste salida)",
|
||||||
|
Descripcion = CapitalizarPalabrasClave(
|
||||||
|
$"{HHmm(tIniUnionFinal)} -> {HHmm(tCursor)} - Camina desde el punto real de bajada hasta la Parada \"{paradaFinBus.Nombre}\" ({CodigoParadaFormateado(paradaFinBus.Codigo)})."
|
||||||
|
),
|
||||||
|
DistanciaMetros = distanciaUnionFinalReal,
|
||||||
|
DuracionSegundos = duracionUnionFinal,
|
||||||
|
Linea = lineaUnionFinal
|
||||||
|
});
|
||||||
|
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
await DrawRouteJsAsync("walk_union_final", lineaUnionFinal, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
EstablecerPrimerPunto(w2.line, ObtenerUltimoPunto(lineaUnionFinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w2.dist > 0)
|
||||||
|
_distanciaAPieFin += w2.dist;
|
||||||
|
|
||||||
|
var durPie2 = (w2.dist > 0) ? EstimarDuracionPieSegundos(w2.dist) : 0;
|
||||||
|
var tIni2 = tCursor;
|
||||||
|
tCursor = AddSecondsSafe(tCursor, durPie2);
|
||||||
|
|
||||||
|
_duracionTotalSegundos += durPie2;
|
||||||
|
|
||||||
|
_tramosRuta.Add(new DetalleTramoRuta
|
||||||
|
{
|
||||||
|
Tipo = "A pie",
|
||||||
|
Descripcion = CapitalizarPalabrasClave(
|
||||||
|
$"{HHmm(tIni2)} -> {HHmm(tCursor)} - Desde la Parada \"{paradaFinBus.Nombre}\" ({CodigoParadaFormateado(paradaFinBus.Codigo)}) hasta tu destino."
|
||||||
|
),
|
||||||
|
DistanciaMetros = w2.dist,
|
||||||
|
DuracionSegundos = durPie2,
|
||||||
|
Linea = w2.line
|
||||||
|
});
|
||||||
|
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
await DrawRouteJsAsync("walk_2", w2.line, "#dc2626", 4, 0.9, dashArray: DashArrayPiePunteado, arrowEnd: true, lineCap: "round");
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineasUsadas = lineasUsadasVisuales.Count > 0
|
||||||
|
? CompactarCodigosLineaConsecutivos(lineasUsadasVisuales)
|
||||||
|
: CompactarCodigosLineaConsecutivos(tramosBus.Select(t => CodigoLineaItinerarioSentido(t)));
|
||||||
|
|
||||||
|
string txtLineas = (lineasUsadas.Count == 0)
|
||||||
|
? ""
|
||||||
|
: $" Lineas: {string.Join(", ", lineasUsadas)}.";
|
||||||
|
|
||||||
|
int tramosCount = tramosBus?.Count ?? 0;
|
||||||
|
int trans = UtilidadesTransitoDbus.ContarTransbordosReales(tramosBus);
|
||||||
|
|
||||||
|
_mensajeRuta = CapitalizarPalabrasClave(
|
||||||
|
$"Inicio {HHmm(now)}. Camina {FormatearDistanciaDetalleRuta(_distanciaAPieInicio)} hasta \"{_paradaOrigenSeleccionada?.Nombre}\" ({CodigoParadaFormateado(_paradaOrigenSeleccionada?.Codigo)}). " +
|
||||||
|
$"Luego viaja en bus {FormatearDistanciaDetalleRuta(_distanciaEnBus)}" +
|
||||||
|
(tramosCount > 0 ? $" en {tramosCount} tramo(s)" : "") +
|
||||||
|
(trans == 0 ? "." : $" con {trans} transbordo(s).") +
|
||||||
|
txtLineas + " " +
|
||||||
|
$"Finalmente camina {FormatearDistanciaDetalleRuta(_distanciaAPieFin)} hasta tu destino."
|
||||||
|
);
|
||||||
|
|
||||||
|
_hayRutaValida = true;
|
||||||
|
ThrowIfPintadoObsoleto(versionPintado);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recalcula con parametros.
|
||||||
|
/// </summary>
|
||||||
|
private async Task RecalcularConParametros()
|
||||||
|
{
|
||||||
|
if (_coordenadasOrigen is null || _coordenadasDestino is null) return;
|
||||||
|
await PersistirConfiguracionAsync();
|
||||||
|
|
||||||
|
if (_mapa is not null)
|
||||||
|
{
|
||||||
|
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
|
||||||
|
await ClearRoutesJsAsync();
|
||||||
|
}
|
||||||
|
await CerrarPopupsAsync();
|
||||||
|
|
||||||
|
await CalcularRutaCompletaAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reinicia seleccion.
|
||||||
|
/// </summary>
|
||||||
|
private async Task ReiniciarSeleccion()
|
||||||
|
{
|
||||||
|
await CerrarTodasLasPreviewsAsync();
|
||||||
|
|
||||||
|
await CerrarPopupsAsync();
|
||||||
|
|
||||||
|
_ctsCalculo?.Cancel();
|
||||||
|
IniciarNuevaVersionPintadoRuta();
|
||||||
|
|
||||||
|
_coordenadasOrigen = null;
|
||||||
|
_coordenadasDestino = null;
|
||||||
|
_textoBusquedaOrigen = string.Empty;
|
||||||
|
_textoBusquedaDestino = string.Empty;
|
||||||
|
_ubicacionOrigenSeleccionada = null;
|
||||||
|
_ubicacionDestinoSeleccionada = null;
|
||||||
|
_sugerenciasBusquedaOrigen.Clear();
|
||||||
|
_sugerenciasBusquedaDestino.Clear();
|
||||||
|
_campoBusquedaActivo = null;
|
||||||
|
|
||||||
|
_mensajeRuta = string.Empty;
|
||||||
|
_hayRutaValida = false;
|
||||||
|
_paradaOrigenSeleccionada = null;
|
||||||
|
_paradaDestinoSeleccionada = null;
|
||||||
|
_primerTransbordoseleccionado = null;
|
||||||
|
_numeroTransbordos = 0;
|
||||||
|
_distanciaAPieInicio = _distanciaAPieFin = _distanciaEnBus = 0;
|
||||||
|
_paradaClicada = null;
|
||||||
|
_tramosRuta.Clear();
|
||||||
|
_duracionTotalSegundos = 0;
|
||||||
|
_opcionesRuta.Clear();
|
||||||
|
_indiceAlternativaSeleccionada = 0;
|
||||||
|
|
||||||
|
if (_mapa != null)
|
||||||
|
{
|
||||||
|
await ClearRoutesJsAsync();
|
||||||
|
await ClearEndpointMarkersJsAsync();
|
||||||
|
try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
|
||||||
|
|
||||||
|
await _mapa.Geometric.DisplayPointsFromArray.deleteAllPoints();
|
||||||
|
if (_mostrarParadas)
|
||||||
|
await DibujarParadasAsync();
|
||||||
|
else
|
||||||
|
await OcultarParadasAsync(); // (repone origen/destino si hubiera, aquí ya no hay, pero ok)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Distancia rápida
|
||||||
|
// =========================================================
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula distancia metros.
|
||||||
|
/// </summary>
|
||||||
|
private static double CalcularDistanciaMetros(double lat1, double lon1, double lat2, double lon2)
|
||||||
|
{
|
||||||
|
const double R = 6371000.0; // metros
|
||||||
|
static double ToRad(double x) => x * Math.PI / 180.0;
|
||||||
|
|
||||||
|
var dLat = ToRad(lat2 - lat1);
|
||||||
|
var dLon = ToRad(lon2 - lon1);
|
||||||
|
|
||||||
|
var a =
|
||||||
|
Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
|
||||||
|
Math.Cos(ToRad(lat1)) * Math.Cos(ToRad(lat2)) *
|
||||||
|
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
|
||||||
|
|
||||||
|
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
|
||||||
|
return R * c;
|
||||||
|
}
|
||||||
|
}
|
||||||
470
RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs
Normal file
470
RutasDBUS/Components/Pages/PlanificadorRutas.TiempoYHorarios.cs
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using LeafletForBlazor;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using RutasDBUS.Modelos.Planificacion;
|
||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
using RutasDBUS.Servicios;
|
||||||
|
using RutasDBUS.Servicios.Caminata;
|
||||||
|
using RutasDBUS.Servicios.Horarios;
|
||||||
|
using RutasDBUS.Servicios.Planificacion;
|
||||||
|
using RutasDBUS.Servicios.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Components.Pages;
|
||||||
|
|
||||||
|
public partial class PlanificadorRutas : ComponentBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona hmm.
|
||||||
|
/// </summary>
|
||||||
|
private static string HHmm(DateTime dt) => dt.ToString("HH:mm", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
private static DateTime Compose(DateOnly d, TimeOnly t)
|
||||||
|
=> d.ToDateTime(t);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Añade seconds safe.
|
||||||
|
/// </summary>
|
||||||
|
private static DateTime AddSecondsSafe(DateTime dt, double seconds)
|
||||||
|
{
|
||||||
|
if (seconds <= 0) return dt;
|
||||||
|
return dt.AddSeconds(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para sacar de InfoVariante lo que necesitamos para horarios
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona codigo str.
|
||||||
|
/// </summary>
|
||||||
|
private static string LineaCodigoStr(InfoVariante v) => v?.Linea?.Codigo?.ToString() ?? "?";
|
||||||
|
|
||||||
|
// =======================
|
||||||
|
// HORARIOS (ZIP)
|
||||||
|
// =======================
|
||||||
|
private bool _usarHorariosTeoricos = true;
|
||||||
|
|
||||||
|
private bool _horariosOk = false;
|
||||||
|
private string? _horariosError = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sincroniza la penalizacion manual por parada con el modo horario activo.
|
||||||
|
/// </summary>
|
||||||
|
private void SincronizarSegundosPorParadaConHorarios()
|
||||||
|
{
|
||||||
|
if (_usarHorariosTeoricos)
|
||||||
|
{
|
||||||
|
_segundosPorParadaBus = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_segundosPorParadaBus == 0 && _segundosPorParadaBusManual >= 0)
|
||||||
|
_segundosPorParadaBus = _segundosPorParadaBusManual;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona lista horarios con actual.
|
||||||
|
/// </summary>
|
||||||
|
private List<ProximaSalidaVm> ConstruirListaHorariosConActual(
|
||||||
|
List<(TimeOnly dep, string expId, string rutaId)> deps,
|
||||||
|
TimeOnly now)
|
||||||
|
{
|
||||||
|
var ordenados = deps
|
||||||
|
.OrderBy(x => x.dep)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
int idxProximo = ordenados.FindIndex(x => x.dep >= now);
|
||||||
|
|
||||||
|
var lista = new List<ProximaSalidaVm>(ordenados.Count);
|
||||||
|
|
||||||
|
for (int i = 0; i < ordenados.Count; i++)
|
||||||
|
{
|
||||||
|
var d = ordenados[i];
|
||||||
|
|
||||||
|
var itiClave = Horarios.GetLineaItinerarioSentidoText(d.expId, d.rutaId);
|
||||||
|
|
||||||
|
lista.Add(new ProximaSalidaVm(
|
||||||
|
Hora: d.dep.ToString("HH:mm"),
|
||||||
|
LineaTxt: Horarios.GetLineaItinerarioSentidoNombreText(d.expId, d.rutaId),
|
||||||
|
RutaId: d.rutaId,
|
||||||
|
ExpId: d.expId,
|
||||||
|
EsProximo: i == idxProximo,
|
||||||
|
AnchorId: i == idxProximo ? "horario-proximo-anchor" : "",
|
||||||
|
ItinerarioClave: itiClave));
|
||||||
|
}
|
||||||
|
|
||||||
|
return lista;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula el orden lógico de una línea visible del popup, respetando TB y enviando las líneas B al bloque 100.
|
||||||
|
/// </summary>
|
||||||
|
private static int ResolverOrdenLineaHorario(string? lineaVisible, string? rutaId)
|
||||||
|
{
|
||||||
|
var texto = (lineaVisible ?? "").Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
var matchTb = Regex.Match(texto, @"^TB(\d+)$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
if (matchTb.Success &&
|
||||||
|
int.TryParse(matchTb.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var tb))
|
||||||
|
{
|
||||||
|
return tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchBuho = Regex.Match(texto, @"^B(\d+)$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
if (matchBuho.Success &&
|
||||||
|
int.TryParse(matchBuho.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var buho))
|
||||||
|
{
|
||||||
|
return 100 + buho;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int.TryParse(texto, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeroDirecto))
|
||||||
|
return numeroDirecto;
|
||||||
|
|
||||||
|
if (int.TryParse(rutaId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var idRuta))
|
||||||
|
return idRuta;
|
||||||
|
|
||||||
|
var numeroExtraido = UtilidadesTransitoDbus.ExtraerNumeroLinea(texto);
|
||||||
|
return numeroExtraido ?? int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extrae el orden lógico visible de línea, itinerario y sentido para los horarios del popup de parada.
|
||||||
|
/// </summary>
|
||||||
|
private static (int lineaOrden, string lineaVisible, int itinerarioOrden, string sentidoOrden) ParseClaveOrdenHorario(string? rutaId, string? lineaTxt)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(lineaTxt))
|
||||||
|
return (ResolverOrdenLineaHorario(null, rutaId), "", int.MaxValue, "?");
|
||||||
|
|
||||||
|
var txt = lineaTxt.Trim();
|
||||||
|
var separadorDestino = txt.IndexOf(" - ", StringComparison.Ordinal);
|
||||||
|
if (separadorDestino > 0)
|
||||||
|
txt = txt[..separadorDestino].Trim();
|
||||||
|
|
||||||
|
var match = Regex.Match(
|
||||||
|
txt,
|
||||||
|
@"^(?<linea>[^.\s]+)\.(?<itin>\d+)-(?<sentido>[A-Za-z?]+)$",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
return (ResolverOrdenLineaHorario(null, rutaId), "", int.MaxValue, "?");
|
||||||
|
|
||||||
|
var lineaVisible = match.Groups["linea"].Value.Trim().ToUpperInvariant();
|
||||||
|
var sentido = match.Groups["sentido"].Value.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
int itinerario = int.MaxValue;
|
||||||
|
int.TryParse(match.Groups["itin"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out itinerario);
|
||||||
|
|
||||||
|
return (ResolverOrdenLineaHorario(lineaVisible, rutaId), lineaVisible, itinerario, sentido);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<ProximaSalidaVm> NextDepsOrdenados
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var ordenadosPorClave = _nextDeps
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
Item = x,
|
||||||
|
Orden = ParseClaveOrdenHorario(x.RutaId, x.LineaTxt)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_horarioOrden == HorarioOrden.PorHora)
|
||||||
|
{
|
||||||
|
return ordenadosPorClave
|
||||||
|
.OrderBy(x => x.Item.Hora, StringComparer.Ordinal)
|
||||||
|
.ThenBy(x => x.Orden.lineaOrden)
|
||||||
|
.ThenBy(x => x.Orden.lineaVisible, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ThenBy(x => x.Orden.itinerarioOrden)
|
||||||
|
.ThenBy(x => x.Orden.sentidoOrden, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(x => x.Item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ordenadosPorClave
|
||||||
|
.OrderBy(x => x.Orden.lineaOrden)
|
||||||
|
.ThenBy(x => x.Orden.lineaVisible, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ThenBy(x => x.Orden.itinerarioOrden)
|
||||||
|
.ThenBy(x => x.Orden.sentidoOrden, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ThenBy(x => x.Item.Hora, StringComparer.Ordinal)
|
||||||
|
.Select(x => x.Item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Alterna orden horarios.
|
||||||
|
/// </summary>
|
||||||
|
private void ToggleOrdenHorarios()
|
||||||
|
{
|
||||||
|
_horarioOrden = _horarioOrden == HorarioOrden.PorHora
|
||||||
|
? HorarioOrden.PorItinerario
|
||||||
|
: HorarioOrden.PorHora;
|
||||||
|
|
||||||
|
if (_horarioOrden == HorarioOrden.PorHora)
|
||||||
|
_scrollHorariosPendiente = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Próximas salidas para el popup de parada
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase ProximaSalidaVm.
|
||||||
|
/// </summary>
|
||||||
|
private sealed record ProximaSalidaVm(
|
||||||
|
string Hora,
|
||||||
|
string LineaTxt,
|
||||||
|
string RutaId,
|
||||||
|
string ExpId,
|
||||||
|
bool EsProximo,
|
||||||
|
string AnchorId,
|
||||||
|
string ItinerarioClave);
|
||||||
|
|
||||||
|
private List<ProximaSalidaVm> _nextDeps = new();
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona txt desde ruta id.
|
||||||
|
/// </summary>
|
||||||
|
private string LineaTxtDesdeRutaId(string? rutaId)
|
||||||
|
{
|
||||||
|
var rid = CodigoSinDecimal(rutaId);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(rid))
|
||||||
|
return $"L\u00ednea ?";
|
||||||
|
|
||||||
|
// Intenta mapear contra tu JSON de RedBus (Lineas)
|
||||||
|
var linea = RedBusServicio.Lineas
|
||||||
|
.FirstOrDefault(l => CodigoSinDecimal(l.Codigo?.ToString()) == rid);
|
||||||
|
|
||||||
|
if (linea is not null)
|
||||||
|
return $"L\u00ednea {CodigoSinDecimal(linea.Codigo?.ToString())} - {linea.Nombre}";
|
||||||
|
|
||||||
|
// Fallback si no encontramos la línea en el JSON
|
||||||
|
return $"L\u00ednea {rid}";
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona toggle hora simulada.
|
||||||
|
/// </summary>
|
||||||
|
private async Task OnToggleHoraSimulada(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
_usarHoraSimulada = e?.Value is bool b && b;
|
||||||
|
|
||||||
|
if (_usarHoraSimulada)
|
||||||
|
{
|
||||||
|
var ahora = DateTime.Now;
|
||||||
|
_fechaSimulada = DateOnly.FromDateTime(ahora);
|
||||||
|
_horaSimulada = TimeOnly.FromDateTime(ahora);
|
||||||
|
StopClock();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StartClock();
|
||||||
|
}
|
||||||
|
|
||||||
|
await RefrescarHoraUIAsync();
|
||||||
|
await PersistirConfiguracionAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== HORA (real / simulada) ======
|
||||||
|
private bool _usarHoraSimulada = true;
|
||||||
|
private TimeOnly _horaSimulada = TimeOnly.FromDateTime(DateTime.Now);
|
||||||
|
private DateOnly _fechaSimulada = DateOnly.FromDateTime(DateTime.Now);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona local.
|
||||||
|
/// </summary>
|
||||||
|
private DateTime AhoraLocal()
|
||||||
|
{
|
||||||
|
if (_usarHoraSimulada)
|
||||||
|
return _fechaSimulada.ToDateTime(_horaSimulada);
|
||||||
|
|
||||||
|
return DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa la hora simulada con la hora real actual y la deja parada.
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
SincronizarSegundosPorParadaConHorarios();
|
||||||
|
|
||||||
|
if (!_usarHoraSimulada)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var ahora = DateTime.Now;
|
||||||
|
_fechaSimulada = DateOnly.FromDateTime(ahora);
|
||||||
|
_horaSimulada = TimeOnly.FromDateTime(ahora);
|
||||||
|
|
||||||
|
var es = new CultureInfo("es-ES");
|
||||||
|
_horaAhoraTxt = es.TextInfo.ToTitleCase(ahora.ToString("ddd dd/MM HH:mm:ss", es));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona change fecha simulada.
|
||||||
|
/// </summary>
|
||||||
|
private async Task OnChangeFechaSimulada(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
var s = e?.Value?.ToString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(s) && DateOnly.TryParse(s, out var d))
|
||||||
|
_fechaSimulada = d;
|
||||||
|
|
||||||
|
if (_usarHoraSimulada)
|
||||||
|
await RefrescarHoraUIAsync();
|
||||||
|
|
||||||
|
await PersistirConfiguracionAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona change hora simulada.
|
||||||
|
/// </summary>
|
||||||
|
private async Task OnChangeHoraSimulada(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
var s = e?.Value?.ToString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(s) && TimeOnly.TryParse(s, out var t))
|
||||||
|
_horaSimulada = t;
|
||||||
|
|
||||||
|
if (_usarHoraSimulada)
|
||||||
|
await RefrescarHoraUIAsync();
|
||||||
|
|
||||||
|
await PersistirConfiguracionAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ajusta la fecha y la hora simuladas al momento actual.
|
||||||
|
/// </summary>
|
||||||
|
private async Task IrAAhoraSimuladoAsync()
|
||||||
|
{
|
||||||
|
var ahora = DateTime.Now;
|
||||||
|
_fechaSimulada = DateOnly.FromDateTime(ahora);
|
||||||
|
_horaSimulada = TimeOnly.FromDateTime(ahora);
|
||||||
|
|
||||||
|
if (_usarHoraSimulada)
|
||||||
|
await RefrescarHoraUIAsync();
|
||||||
|
|
||||||
|
await PersistirConfiguracionAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restablece hora pruebas.
|
||||||
|
/// </summary>
|
||||||
|
private async Task ResetHoraPruebas()
|
||||||
|
{
|
||||||
|
_usarHoraSimulada = false;
|
||||||
|
_fechaSimulada = DateOnly.FromDateTime(DateTime.Now);
|
||||||
|
_horaSimulada = TimeOnly.FromDateTime(DateTime.Now);
|
||||||
|
|
||||||
|
StartClock();
|
||||||
|
await RefrescarHoraUIAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private string _horaAhoraTxt = "";
|
||||||
|
private PeriodicTimer? _clockTimer;
|
||||||
|
private CancellationTokenSource? _clockCts;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona clock.
|
||||||
|
/// </summary>
|
||||||
|
private void StartClock()
|
||||||
|
{
|
||||||
|
if (_clockTimer is not null) return;
|
||||||
|
|
||||||
|
_clockCts = new CancellationTokenSource();
|
||||||
|
_clockTimer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (await _clockTimer.WaitForNextTickAsync(_clockCts.Token))
|
||||||
|
{
|
||||||
|
// ✅ Solo actualizamos si NO estamos en simulada
|
||||||
|
if (_usarHoraSimulada) continue;
|
||||||
|
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var es = new CultureInfo("es-ES");
|
||||||
|
_horaAhoraTxt = es.TextInfo.ToTitleCase(now.ToString("ddd dd/MM HH:mm:ss", es));
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged); // ✅ como antes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* cancel */ }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona clock.
|
||||||
|
/// </summary>
|
||||||
|
private void StopClock()
|
||||||
|
{
|
||||||
|
_clockCts?.Cancel();
|
||||||
|
_clockTimer?.Dispose();
|
||||||
|
_clockCts?.Dispose();
|
||||||
|
|
||||||
|
_clockTimer = null;
|
||||||
|
_clockCts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Actualiza hora ui.
|
||||||
|
/// </summary>
|
||||||
|
private async Task RefrescarHoraUIAsync()
|
||||||
|
{
|
||||||
|
// pinta la hora (real o simulada) y refresca UI
|
||||||
|
var es = new CultureInfo("es-ES");
|
||||||
|
|
||||||
|
DateTime now;
|
||||||
|
if (_usarHoraSimulada)
|
||||||
|
now = _fechaSimulada.ToDateTime(_horaSimulada);
|
||||||
|
else
|
||||||
|
now = DateTime.Now;
|
||||||
|
|
||||||
|
_horaAhoraTxt = es.TextInfo.ToTitleCase(now.ToString("ddd dd/MM HH:mm:ss", es));
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_clockCts?.Cancel();
|
||||||
|
_clockTimer?.Dispose();
|
||||||
|
_clockCts?.Dispose();
|
||||||
|
StopRefrescoHistorialCompartido();
|
||||||
|
|
||||||
|
_dotRef?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
private static IEnumerable<int> Range(int from, int to, int step = 1)
|
||||||
|
{
|
||||||
|
if (step <= 0) step = 1;
|
||||||
|
|
||||||
|
if (from <= to)
|
||||||
|
{
|
||||||
|
for (int i = from; i <= to; i += step) yield return i;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int i = from; i >= to; i -= step) yield return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<(double[,]? line, double dist, double dur)> WalkCached(double lat1, double lon1, double lat2, double lon2)
|
||||||
|
{
|
||||||
|
return _walkProvider == ProveedorCaminata.Valhalla
|
||||||
|
? WalkValhallaCached(lat1, lon1, lat2, lon2)
|
||||||
|
: WalkOsrmCached(lat1, lon1, lat2, lon2);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
1598
RutasDBUS/Components/Pages/PlanificadorRutas.razor
Normal file
1598
RutasDBUS/Components/Pages/PlanificadorRutas.razor
Normal file
File diff suppressed because it is too large
Load Diff
26
RutasDBUS/Components/Pages/PlanificadorRutas.razor.cs
Normal file
26
RutasDBUS/Components/Pages/PlanificadorRutas.razor.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using RutasDBUS.Servicios.Caminata;
|
||||||
|
using RutasDBUS.Servicios.Geocodificacion;
|
||||||
|
using RutasDBUS.Servicios.Historial;
|
||||||
|
using RutasDBUS.Servicios.Horarios;
|
||||||
|
using RutasDBUS.Servicios.Importacion;
|
||||||
|
using RutasDBUS.Servicios.Planificacion;
|
||||||
|
using RutasDBUS.Servicios.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Components.Pages;
|
||||||
|
|
||||||
|
public partial class PlanificadorRutas : ComponentBase
|
||||||
|
{
|
||||||
|
[Inject] protected ServicioHorariosDbus Horarios { get; set; } = default!;
|
||||||
|
[Inject] protected IServicioArchivoImportacionDbus ArchivoImportacionDbus { get; set; } = default!;
|
||||||
|
[Inject] protected IWebHostEnvironment Env { get; set; } = default!;
|
||||||
|
[Inject] protected IRedBusServicio RedBusServicio { get; set; } = default!;
|
||||||
|
[Inject] protected IPlanificadorRutas PlanificadorRutasServicio { get; set; } = default!;
|
||||||
|
[Inject] protected IHistorialRutasCompartidoService HistorialRutasCompartidoService { get; set; } = default!;
|
||||||
|
[Inject] protected IGeocodificadorLocalServicio GeocodificadorLocal { get; set; } = default!;
|
||||||
|
[Inject] protected IClienteOsrm EsrmCliente { get; set; } = default!;
|
||||||
|
[Inject] protected IJSRuntime JS { get; set; } = default!;
|
||||||
|
[Inject] protected IClienteValhalla ClienteValhalla { get; set; } = default!;
|
||||||
|
[Inject] protected HttpClient Http { get; set; } = default!;
|
||||||
|
}
|
||||||
6
RutasDBUS/Components/Routes.razor
Normal file
6
RutasDBUS/Components/Routes.razor
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<Router AppAssembly="typeof(Program).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
</Router>
|
||||||
12
RutasDBUS/Components/_Imports.razor
Normal file
12
RutasDBUS/Components/_Imports.razor
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using RutasDBUS
|
||||||
|
@using RutasDBUS.Components
|
||||||
|
@using LeafletForBlazor
|
||||||
|
|
||||||
BIN
RutasDBUS/Modelos/Callejero/GFA_DSET_AD_CSV.zip
Normal file
BIN
RutasDBUS/Modelos/Callejero/GFA_DSET_AD_CSV.zip
Normal file
Binary file not shown.
13
RutasDBUS/Modelos/Geocodificacion/LugarGeocodificado.cs
Normal file
13
RutasDBUS/Modelos/Geocodificacion/LugarGeocodificado.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace RutasDBUS.Modelos.Geocodificacion;
|
||||||
|
|
||||||
|
public sealed class LugarGeocodificado
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = "";
|
||||||
|
public string Nombre { get; init; } = "";
|
||||||
|
public string Subtitulo { get; init; } = "";
|
||||||
|
public string Tipo { get; init; } = "";
|
||||||
|
public string Fuente { get; init; } = "";
|
||||||
|
public string Municipio { get; init; } = "";
|
||||||
|
public double Latitud { get; init; }
|
||||||
|
public double Longitud { get; init; }
|
||||||
|
}
|
||||||
25
RutasDBUS/Modelos/Historial/RegistroHistorialRuta.cs
Normal file
25
RutasDBUS/Modelos/Historial/RegistroHistorialRuta.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
namespace RutasDBUS.Modelos.Historial;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Escenario guardado de origen/destino para reutilizar pruebas de rutas.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RegistroHistorialRuta
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||||
|
public string OrigenTexto { get; set; } = string.Empty;
|
||||||
|
public string DestinoTexto { get; set; } = string.Empty;
|
||||||
|
public double OrigenLatitud { get; set; }
|
||||||
|
public double OrigenLongitud { get; set; }
|
||||||
|
public double DestinoLatitud { get; set; }
|
||||||
|
public double DestinoLongitud { get; set; }
|
||||||
|
public bool UsarHorariosTeoricos { get; set; } = true;
|
||||||
|
public bool UsarHoraSimulada { get; set; } = true;
|
||||||
|
public string? FechaSimulada { get; set; }
|
||||||
|
public string? HoraSimulada { get; set; }
|
||||||
|
public string? CriterioPrioritarioRuta { get; set; }
|
||||||
|
public string? PreferenciaEtiquetaRuta { get; set; }
|
||||||
|
public bool ActivarFiltroLineas { get; set; }
|
||||||
|
public List<int> LineasPermitidas { get; set; } = new();
|
||||||
|
public DateTime FechaCreacionUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime FechaActualizacionUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
BIN
RutasDBUS/Modelos/Horarios/zip_DBUS_OPENDATA_20260408~0940.zip
Normal file
BIN
RutasDBUS/Modelos/Horarios/zip_DBUS_OPENDATA_20260408~0940.zip
Normal file
Binary file not shown.
BIN
RutasDBUS/Modelos/Horarios/zip_DBUS_OPENDATA_20260514~0800.zip
Normal file
BIN
RutasDBUS/Modelos/Horarios/zip_DBUS_OPENDATA_20260514~0800.zip
Normal file
Binary file not shown.
BIN
RutasDBUS/Modelos/Horarios/zip_Importacion_20260213~0900.zip
Normal file
BIN
RutasDBUS/Modelos/Horarios/zip_Importacion_20260213~0900.zip
Normal file
Binary file not shown.
31
RutasDBUS/Modelos/Importacion/ArchivoImportacionDbus.cs
Normal file
31
RutasDBUS/Modelos/Importacion/ArchivoImportacionDbus.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
namespace RutasDBUS.Modelos.Importacion;
|
||||||
|
|
||||||
|
public sealed class ArchivoImportacionDbus
|
||||||
|
{
|
||||||
|
public string RutaZip { get; init; } = "";
|
||||||
|
public DateTime FechaArchivoUtc { get; init; }
|
||||||
|
public IReadOnlyList<FilaCalendarioCsv> Calendarios { get; init; } = Array.Empty<FilaCalendarioCsv>();
|
||||||
|
public IReadOnlyList<FilaCalendarioExcepcionCsv> CalendarioExcepciones { get; init; } = Array.Empty<FilaCalendarioExcepcionCsv>();
|
||||||
|
public IReadOnlyList<FilaExpedicionCsv> Expediciones { get; init; } = Array.Empty<FilaExpedicionCsv>();
|
||||||
|
public IReadOnlyList<FilaOperadorCsv> Operadores { get; init; } = Array.Empty<FilaOperadorCsv>();
|
||||||
|
public IReadOnlyList<FilaParadaCsv> Paradas { get; init; } = Array.Empty<FilaParadaCsv>();
|
||||||
|
public IReadOnlyList<FilaPatronCsv> Patrones { get; init; } = Array.Empty<FilaPatronCsv>();
|
||||||
|
public IReadOnlyList<FilaRutaCsv> Rutas { get; init; } = Array.Empty<FilaRutaCsv>();
|
||||||
|
public IReadOnlyList<FilaTarifaCsv> Tarifas { get; init; } = Array.Empty<FilaTarifaCsv>();
|
||||||
|
public IReadOnlyList<FilaTiempoParadaCsv> TiemposParada { get; init; } = Array.Empty<FilaTiempoParadaCsv>();
|
||||||
|
public IReadOnlyList<FilaTiempoRealCsv> TiemposReales { get; init; } = Array.Empty<FilaTiempoRealCsv>();
|
||||||
|
public IReadOnlyList<FilaTransbordoCsv> Transbordos { get; init; } = Array.Empty<FilaTransbordoCsv>();
|
||||||
|
public IReadOnlyList<FilaVehiculoCsv> Vehiculos { get; init; } = Array.Empty<FilaVehiculoCsv>();
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<int, FilaParadaCsv> ParadasPorId { get; init; } = new Dictionary<int, FilaParadaCsv>();
|
||||||
|
public IReadOnlyDictionary<string, FilaRutaCsv> RutasPorId { get; init; } =
|
||||||
|
new Dictionary<string, FilaRutaCsv>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public IReadOnlyDictionary<string, FilaExpedicionCsv> ExpedicionesPorId { get; init; } =
|
||||||
|
new Dictionary<string, FilaExpedicionCsv>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public IReadOnlyDictionary<string, List<FilaTiempoParadaCsv>> TiemposParadaPorExpedicion { get; init; } =
|
||||||
|
new Dictionary<string, List<FilaTiempoParadaCsv>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public IReadOnlyDictionary<string, List<FilaPatronCsv>> PatronesPorId { get; init; } =
|
||||||
|
new Dictionary<string, List<FilaPatronCsv>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public IReadOnlyDictionary<string, List<FilaTransbordoCsv>> TransbordosPorRuta { get; init; } =
|
||||||
|
new Dictionary<string, List<FilaTransbordoCsv>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
149
RutasDBUS/Modelos/Importacion/FilasCsvDbus.cs
Normal file
149
RutasDBUS/Modelos/Importacion/FilasCsvDbus.cs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
namespace RutasDBUS.Modelos.Importacion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase FilaCalendarioCsv.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FilaCalendarioCsv(
|
||||||
|
string Id,
|
||||||
|
int Lunes,
|
||||||
|
int Martes,
|
||||||
|
int Miercoles,
|
||||||
|
int Jueves,
|
||||||
|
int Viernes,
|
||||||
|
int Sabado,
|
||||||
|
int Domingo,
|
||||||
|
DateOnly FechaInicio,
|
||||||
|
DateOnly FechaFin);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase FilaCalendarioExcepcionCsv.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FilaCalendarioExcepcionCsv(
|
||||||
|
string Id,
|
||||||
|
DateOnly FechaEx,
|
||||||
|
int TipoExcepcion);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase FilaExpedicionCsv.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FilaExpedicionCsv(
|
||||||
|
string Id,
|
||||||
|
string IdRuta,
|
||||||
|
string DirDestino,
|
||||||
|
int Direccion,
|
||||||
|
string CambioExp,
|
||||||
|
string IdPatronExp,
|
||||||
|
TimeOnly? HSalida,
|
||||||
|
string IdCal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase FilaOperadorCsv.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FilaOperadorCsv(
|
||||||
|
string Id,
|
||||||
|
string Nombre,
|
||||||
|
string Telefono,
|
||||||
|
string Url,
|
||||||
|
string Descripcion);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase FilaParadaCsv.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FilaParadaCsv(
|
||||||
|
int Id,
|
||||||
|
string CodigoSms,
|
||||||
|
string NombrePar,
|
||||||
|
double LatitudYPar,
|
||||||
|
double LongitudXPar,
|
||||||
|
string LocalidadPar,
|
||||||
|
string ZonaPar,
|
||||||
|
string DescripcionPar,
|
||||||
|
string UrlPar,
|
||||||
|
string TipoPar,
|
||||||
|
string EstacionPadre);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase FilaPatronCsv.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FilaPatronCsv(
|
||||||
|
string Id,
|
||||||
|
double PtoLatitudY,
|
||||||
|
double PtoLongitudX,
|
||||||
|
int PtoSecuencia,
|
||||||
|
double DistanciaRecorrida);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase FilaRutaCsv.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FilaRutaCsv(
|
||||||
|
string Id,
|
||||||
|
string Abreviatura,
|
||||||
|
string Nombre,
|
||||||
|
string Descripcion,
|
||||||
|
string TipoVehiculo,
|
||||||
|
string Url,
|
||||||
|
string Color);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase FilaTarifaCsv.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FilaTarifaCsv(
|
||||||
|
string Id,
|
||||||
|
string IdTarRu,
|
||||||
|
string IdParadaOrigen,
|
||||||
|
string IdParadaDestino,
|
||||||
|
string IdZonaOrigen,
|
||||||
|
string IdZonaDestino,
|
||||||
|
decimal? Precio,
|
||||||
|
string TipoPago,
|
||||||
|
int? TiempoTransbordo,
|
||||||
|
int? Transbordos);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase FilaTiempoParadaCsv.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FilaTiempoParadaCsv(
|
||||||
|
string IdExp,
|
||||||
|
TimeOnly? HoraLlegada,
|
||||||
|
TimeOnly? HoraSalida,
|
||||||
|
double DistanciaSiguienteParada,
|
||||||
|
int IdTiempoPar,
|
||||||
|
int SecPar,
|
||||||
|
string TipoSubidaPar,
|
||||||
|
string TipoBajadaPar,
|
||||||
|
string NuevaDirDestinoPar);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase FilaTiempoRealCsv.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FilaTiempoRealCsv(
|
||||||
|
string IdRu,
|
||||||
|
string IdServicio,
|
||||||
|
string IdTrans,
|
||||||
|
double LatitudY,
|
||||||
|
double LongitudX,
|
||||||
|
string HoraEmision,
|
||||||
|
string IdAntPar,
|
||||||
|
string FechaHoraPaso,
|
||||||
|
TimeOnly? HSalida,
|
||||||
|
int? MinutosDeDiferencia,
|
||||||
|
string Teorico);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase FilaTransbordoCsv.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FilaTransbordoCsv(
|
||||||
|
string IdRuta1,
|
||||||
|
string IdRuta2);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase FilaVehiculoCsv.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FilaVehiculoCsv(
|
||||||
|
string Id,
|
||||||
|
string IdTipo,
|
||||||
|
string DescripcionTipo,
|
||||||
|
int? CapacidadMaxima,
|
||||||
|
int? Asientos,
|
||||||
|
int? NumPuertas,
|
||||||
|
int? AforoActual);
|
||||||
10
RutasDBUS/Modelos/Importacion/ModeloHorarioDbusInfo.cs
Normal file
10
RutasDBUS/Modelos/Importacion/ModeloHorarioDbusInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace RutasDBUS.Modelos.Importacion;
|
||||||
|
|
||||||
|
public sealed record ModeloHorarioDbusInfo(
|
||||||
|
string Id,
|
||||||
|
string NombreArchivo,
|
||||||
|
string RutaRelativa,
|
||||||
|
string Origen,
|
||||||
|
long TamanoBytes,
|
||||||
|
DateTime FechaUtc,
|
||||||
|
bool Activo);
|
||||||
38
RutasDBUS/Modelos/Planificacion/AlternativaRuta.cs
Normal file
38
RutasDBUS/Modelos/Planificacion/AlternativaRuta.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Modelos.Planificacion;
|
||||||
|
|
||||||
|
public sealed class AlternativaRuta
|
||||||
|
{
|
||||||
|
public bool EsSoloAPie { get; set; }
|
||||||
|
public string ProveedorRuta { get; set; } = "";
|
||||||
|
|
||||||
|
public double[,]? LineaSoloAPie { get; set; }
|
||||||
|
public double DistanciaSoloAPie { get; set; }
|
||||||
|
public double DuracionSoloAPie { get; set; }
|
||||||
|
|
||||||
|
public Parada OrigenParada { get; set; } = default!;
|
||||||
|
public Parada DestinoParada { get; set; } = default!;
|
||||||
|
public RutaBusAlternativa? AlternativaBus { get; set; }
|
||||||
|
|
||||||
|
public double DistanciaPieInicioAprox { get; set; }
|
||||||
|
public double DistanciaPieTransbordosAprox { get; set; }
|
||||||
|
public double DistanciaPieFinAprox { get; set; }
|
||||||
|
public double DistanciaPieTotalAprox =>
|
||||||
|
DistanciaPieInicioAprox + DistanciaPieTransbordosAprox + DistanciaPieFinAprox;
|
||||||
|
|
||||||
|
public double TiempoPieInicio { get; set; }
|
||||||
|
public double TiempoPieFin { get; set; }
|
||||||
|
public double TiempoEsperaInicialSegundos { get; set; }
|
||||||
|
public double TiempoEsperaTransbordosSegundos { get; set; }
|
||||||
|
public double TiempoEsperaTotalSegundos =>
|
||||||
|
TiempoEsperaInicialSegundos + TiempoEsperaTransbordosSegundos;
|
||||||
|
public bool UsaHorariosReales { get; set; }
|
||||||
|
public bool HorarioCompletoDisponible { get; set; }
|
||||||
|
|
||||||
|
public double DuracionTotalAproxSegundos { get; set; }
|
||||||
|
public string Etiqueta { get; set; } = "";
|
||||||
|
public int NumeroTransbordosReales { get; set; }
|
||||||
|
|
||||||
|
public double PuntuacionCoste { get; set; }
|
||||||
|
}
|
||||||
20
RutasDBUS/Modelos/Planificacion/DetalleTramoRuta.cs
Normal file
20
RutasDBUS/Modelos/Planificacion/DetalleTramoRuta.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Modelos.Planificacion;
|
||||||
|
|
||||||
|
public sealed class DetalleTramoRuta
|
||||||
|
{
|
||||||
|
public string Tipo { get; set; } = "";
|
||||||
|
public string Descripcion { get; set; } = "";
|
||||||
|
public double DistanciaMetros { get; set; }
|
||||||
|
public double? DuracionSegundos { get; set; }
|
||||||
|
public double[,]? Linea { get; set; }
|
||||||
|
public double? ElevMin { get; set; }
|
||||||
|
public double? ElevMax { get; set; }
|
||||||
|
public double? ElevInicio { get; set; }
|
||||||
|
public double? ElevFin { get; set; }
|
||||||
|
public List<MuestraElevacion>? MuestrasElevacion { get; set; }
|
||||||
|
public bool ElevacionCargando { get; set; }
|
||||||
|
public bool ElevacionLista => MuestrasElevacion is not null && MuestrasElevacion.Count > 0;
|
||||||
|
public InfoVariante? VarianteBus { get; set; }
|
||||||
|
}
|
||||||
10
RutasDBUS/Modelos/Planificacion/MuestraElevacion.cs
Normal file
10
RutasDBUS/Modelos/Planificacion/MuestraElevacion.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace RutasDBUS.Modelos.Planificacion;
|
||||||
|
|
||||||
|
public sealed class MuestraElevacion
|
||||||
|
{
|
||||||
|
public int Indice { get; set; }
|
||||||
|
public double Latitud { get; set; }
|
||||||
|
public double Longitud { get; set; }
|
||||||
|
public double? Elevacion { get; set; }
|
||||||
|
public double? ElevacionIdee { get; set; }
|
||||||
|
}
|
||||||
74
RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs
Normal file
74
RutasDBUS/Modelos/Planificacion/ParadaCandidata.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Modelos.Planificacion;
|
||||||
|
|
||||||
|
public sealed class ParadaCandidata
|
||||||
|
{
|
||||||
|
public Parada Parada { get; set; } = default!;
|
||||||
|
public double DistanciaEuclidea { get; set; }
|
||||||
|
public double DistanciaAPie { get; set; }
|
||||||
|
public double TiempoAPie { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public sealed class ParametrosPlanificadorRuta
|
||||||
|
{
|
||||||
|
// velocidades
|
||||||
|
public double VelocidadPieKmH { get; set; } = 3.6;
|
||||||
|
public double VelocidadBusKmH { get; set; } = 16.0;
|
||||||
|
|
||||||
|
// penalizaciones
|
||||||
|
public int MinutosPorTransbordo { get; set; } = 5;
|
||||||
|
public int MargenTransbordoSegundos { get; set; } = 120;
|
||||||
|
public int SegundosPorParadaBus { get; set; } = 30;
|
||||||
|
public double FactorPenalizacionDistanciaPie { get; set; } = 1.6;
|
||||||
|
public double ParametroPenalizacionDistanciaPieMetros { get; set; } = 100.0;
|
||||||
|
public double PesoTransbordoRankingMinutos { get; set; } = 4.0;
|
||||||
|
|
||||||
|
// selección candidatas
|
||||||
|
public int RadioParadasCandidatasMetros { get; set; } = 1200;
|
||||||
|
public int MaxParadasCandidatasPorLado { get; set; } = 12;
|
||||||
|
public bool UsarOsrmParaRanking { get; set; } = false;
|
||||||
|
|
||||||
|
// filtro itinerarios
|
||||||
|
public bool ActivarRangoLineas { get; set; } = false;
|
||||||
|
public int LineaMin { get; set; } = 1;
|
||||||
|
public int LineaMax { get; set; } = 999;
|
||||||
|
public List<int> LineasPermitidas { get; set; } = new();
|
||||||
|
public bool ActivarRangoItinerarios { get; set; } = true;
|
||||||
|
public int ItinerarioMin { get; set; } = 1;
|
||||||
|
public int ItinerarioMax { get; set; } = 80;
|
||||||
|
|
||||||
|
// filtro de transbordos
|
||||||
|
public bool ActivarRangoTransbordos { get; set; } = true;
|
||||||
|
public int MinTransbordosPermitidos { get; set; } = 0;
|
||||||
|
public int MaxTransbordosPermitidos { get; set; } = 3;
|
||||||
|
|
||||||
|
// límites exploración
|
||||||
|
public int MaxParesEvaluados { get; set; } = 280;
|
||||||
|
public int MaxAlternativasBusPorPar { get; set; } = 4;
|
||||||
|
|
||||||
|
// planificación temporal
|
||||||
|
public bool UsarHorariosReales { get; set; } = true;
|
||||||
|
public DateOnly? FechaSalida { get; set; }
|
||||||
|
public TimeOnly? HoraSalida { get; set; }
|
||||||
|
public bool DescartarAlternativasSinHorario { get; set; } = false;
|
||||||
|
|
||||||
|
// filtros de espera
|
||||||
|
public int MaxEsperaInicialMinutos { get; set; } = 90;
|
||||||
|
public int MaxEsperaTransbordoMinutos { get; set; } = 60;
|
||||||
|
public int MaxEsperaTotalMinutos { get; set; } = 120;
|
||||||
|
|
||||||
|
// preferencia UI (null mantiene el criterio equilibrado por defecto)
|
||||||
|
public string? PreferenciaEtiquetaRuta { get; set; } = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ResultadoPlanificadorRuta
|
||||||
|
{
|
||||||
|
public bool Ok { get; set; }
|
||||||
|
public string Mensaje { get; set; } = "";
|
||||||
|
public List<AlternativaRuta> Alternativas { get; set; } = new();
|
||||||
|
public int IndicePreferido { get; set; } = 0;
|
||||||
|
}
|
||||||
67
RutasDBUS/Modelos/RedBus/InfoRutaBus.cs
Normal file
67
RutasDBUS/Modelos/RedBus/InfoRutaBus.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
namespace RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Información precalculada de una variante con la secuencia de paradas.
|
||||||
|
/// </summary>
|
||||||
|
public class InfoVariante
|
||||||
|
{
|
||||||
|
/// <summary>Línea a la que pertenece la variante.</summary>
|
||||||
|
public LineaBus Linea { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>Sentido dentro del itinerario.</summary>
|
||||||
|
public Sentido Sentido { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>Variante concreta de la línea.</summary>
|
||||||
|
public Variante Variante { get; set; } = default!;
|
||||||
|
|
||||||
|
public int CodigoItinerario { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lista ordenada de códigos de parada que recorre la variante.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> CodigosParadas { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indica en qué variante y en qué posición aparece una parada.
|
||||||
|
/// </summary>
|
||||||
|
public class AparicionParada
|
||||||
|
{
|
||||||
|
/// <summary>Variante en la que aparece la parada.</summary>
|
||||||
|
public InfoVariante Variante { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>Índice de la parada dentro de <see cref="InfoVariante.CodigosParadas"/>.</summary>
|
||||||
|
public int IndiceParada { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class TramoBus
|
||||||
|
{
|
||||||
|
public InfoVariante Variante { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Índice de parada inicial (incluida) dentro de la secuencia de paradas.
|
||||||
|
/// </summary>
|
||||||
|
public int IndiceInicio { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Índice de parada final (incluida) dentro de la secuencia de paradas.
|
||||||
|
/// OJO: al construir geometría se recorren trayectos: i = IndiceInicio .. IndiceFin-1.
|
||||||
|
/// </summary>
|
||||||
|
public int IndiceFin { get; set; }
|
||||||
|
|
||||||
|
public string CodigoParadaInicio =>
|
||||||
|
Variante.CodigosParadas[Math.Clamp(IndiceInicio, 0, Variante.CodigosParadas.Count - 1)];
|
||||||
|
|
||||||
|
public string CodigoParadaFin =>
|
||||||
|
Variante.CodigosParadas[Math.Clamp(IndiceFin, 0, Variante.CodigosParadas.Count - 1)];
|
||||||
|
|
||||||
|
public bool EsValido =>
|
||||||
|
IndiceInicio >= 0 &&
|
||||||
|
IndiceFin >= 0 &&
|
||||||
|
IndiceInicio < Variante.CodigosParadas.Count &&
|
||||||
|
IndiceFin < Variante.CodigosParadas.Count &&
|
||||||
|
IndiceInicio < IndiceFin;
|
||||||
|
}
|
||||||
|
|
||||||
59
RutasDBUS/Modelos/RedBus/LineaBus.cs
Normal file
59
RutasDBUS/Modelos/RedBus/LineaBus.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
namespace RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Representa una línea de autobús con sus itinerarios.
|
||||||
|
/// </summary>
|
||||||
|
public class LineaBus
|
||||||
|
{
|
||||||
|
/// <summary>Identificador original de la ruta en ruta.csv.</summary>
|
||||||
|
public string IdRuta { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Código interno de la línea.</summary>
|
||||||
|
public string Codigo { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Nombre descriptivo de la línea.</summary>
|
||||||
|
public string Nombre { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Color de fondo en formato ARGB (ej. FF0000FF).</summary>
|
||||||
|
public string ColorArgb { get; set; } = "FF0000FF";
|
||||||
|
|
||||||
|
/// <summary>Código del itinerario por defecto.</summary>
|
||||||
|
public int CodigoItinerarioPorDefecto { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Lista de itinerarios definidos para esta línea.</summary>
|
||||||
|
public List<Itinerario> Itinerarios { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Itinerario de una línea (por ejemplo ida / vuelta).</summary>
|
||||||
|
public class Itinerario
|
||||||
|
{
|
||||||
|
/// <summary>Código interno del itinerario.</summary>
|
||||||
|
public int Codigo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Sentidos configurados dentro de este itinerario.</summary>
|
||||||
|
public List<Sentido> Sentidos { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sentido de un itinerario (A→B, B→A, etc.).</summary>
|
||||||
|
public class Sentido
|
||||||
|
{
|
||||||
|
/// <summary>Código del sentido.</summary>
|
||||||
|
public string Codigo { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Variantes existentes en este sentido.</summary>
|
||||||
|
public List<Variante> Variantes { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Variante de un sentido, definida por la secuencia de trayectos que recorre.
|
||||||
|
/// </summary>
|
||||||
|
public class Variante
|
||||||
|
{
|
||||||
|
/// <summary>Código interno de la variante.</summary>
|
||||||
|
public int Codigo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identificadores de trayectos que se recorren en esta variante, en orden.
|
||||||
|
/// </summary>
|
||||||
|
public List<int> IdsTrayectos { get; set; } = new();
|
||||||
|
}
|
||||||
43
RutasDBUS/Modelos/RedBus/Parada.cs
Normal file
43
RutasDBUS/Modelos/RedBus/Parada.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
namespace RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Representa una parada de autobús con código, nombre y coordenadas.
|
||||||
|
/// </summary>
|
||||||
|
public class Parada
|
||||||
|
{
|
||||||
|
/// <summary>Código interno de la parada (por ejemplo 34.1).</summary>
|
||||||
|
public string Codigo { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Nombre descriptivo completo de la parada.</summary>
|
||||||
|
public string Nombre { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nombre corto de la parada (campo NombreCorto del fichero de líneas).
|
||||||
|
/// </summary>
|
||||||
|
public string NombreCorto { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abreviatura de la parada (campo Abreviatura).
|
||||||
|
/// </summary>
|
||||||
|
public string Abreviatura { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Altura de la parada en metros (campo Altura).
|
||||||
|
/// </summary>
|
||||||
|
public double Altura { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Latitud en grados decimales.</summary>
|
||||||
|
public double Latitud { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Longitud en grados decimales.</summary>
|
||||||
|
public double Longitud { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Zona previa de la parada (si se utiliza en el fichero).</summary>
|
||||||
|
public string PreZona { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Zona posterior de la parada (si se utiliza en el fichero).</summary>
|
||||||
|
public string PostZona { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Zona de salida de la parada (si se utiliza en el fichero).</summary>
|
||||||
|
public string ZonaSalida { get; set; } = "";
|
||||||
|
}
|
||||||
20
RutasDBUS/Modelos/RedBus/RutaBusAlternativa.cs
Normal file
20
RutasDBUS/Modelos/RedBus/RutaBusAlternativa.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Representa una posible ruta de bus entre dos paradas,
|
||||||
|
/// formada por varios tramos de bus consecutivos.
|
||||||
|
/// </summary>
|
||||||
|
public class RutaBusAlternativa
|
||||||
|
{
|
||||||
|
/// <summary>Tramos de bus que componen la ruta.</summary>
|
||||||
|
public List<TramoBus> Tramos { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>Número de Transbordos (cambios de línea/variante).</summary>
|
||||||
|
public int NumeroTransbordos { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Distancia total estimada en bus (metros).</summary>
|
||||||
|
public double DistanciaTotalBus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Código de paradas de Transbordo intermedias.</summary>
|
||||||
|
public List<string> CodigosParadasTransbordo { get; set; } = new();
|
||||||
|
}
|
||||||
28
RutasDBUS/Modelos/RedBus/Trayecto.cs
Normal file
28
RutasDBUS/Modelos/RedBus/Trayecto.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Representa un tramo de línea entre dos paradas con una lista de puntos intermedios.
|
||||||
|
/// </summary>
|
||||||
|
public class Trayecto
|
||||||
|
{
|
||||||
|
/// <summary>Identificador interno del trayecto.</summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Código de la parada origen de este trayecto.</summary>
|
||||||
|
public string CodigoParadaOrigen { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Código de la parada destino de este trayecto.</summary>
|
||||||
|
public string CodigoParadaDestino { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lista de coordenadas [latitud, longitud] que describen la geometría del trayecto.
|
||||||
|
/// </summary>
|
||||||
|
public List<double[]> Posiciones { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class OpcionesRedBus
|
||||||
|
{
|
||||||
|
public string ArchivoDatos { get; set; } = "";
|
||||||
|
public double DistanciaMaximaTransbordoMetros { get; set; } = 250;
|
||||||
|
}
|
||||||
79
RutasDBUS/Program.cs
Normal file
79
RutasDBUS/Program.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// Program.cs
|
||||||
|
using System.Text;
|
||||||
|
using RutasDBUS.Components;
|
||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
using RutasDBUS.Servicios.Caminata;
|
||||||
|
using RutasDBUS.Servicios.Historial;
|
||||||
|
using RutasDBUS.Servicios.Horarios;
|
||||||
|
using RutasDBUS.Servicios.Importacion;
|
||||||
|
using RutasDBUS.Servicios.Geocodificacion;
|
||||||
|
using RutasDBUS.Servicios.Planificacion;
|
||||||
|
using RutasDBUS.Servicios.RedBus;
|
||||||
|
|
||||||
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// Razor Components (Blazor Server interactivo)
|
||||||
|
// =====================================================
|
||||||
|
builder.Services
|
||||||
|
.AddRazorComponents()
|
||||||
|
.AddInteractiveServerComponents();
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// Configuración tipada (appsettings.json)
|
||||||
|
// =====================================================
|
||||||
|
// En appsettings.json tienes:
|
||||||
|
// "RedBus": { "ArchivoDatos": "...", "DistanciaMaximaTransbordoMetros": 250 }
|
||||||
|
builder.Services.Configure<OpcionesRedBus>(
|
||||||
|
builder.Configuration.GetSection("RedBus"));
|
||||||
|
|
||||||
|
// (Opcional) Si luego quieres opciones de Mapa/Elevación/Planificador, se hace igual:
|
||||||
|
// builder.Services.Configure<OpcionesMapa>(builder.Configuration.GetSection("Mapa"));
|
||||||
|
// builder.Services.Configure<OpcionesElevacion>(builder.Configuration.GetSection("Elevacion"));
|
||||||
|
// builder.Services.Configure<OpcionesPlanificadorRutas>(builder.Configuration.GetSection("PlanificadorRutas"));
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// Servicios propios
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
// Servicio de red de autobuses (paradas, líneas, trayectos...)
|
||||||
|
builder.Services.AddSingleton<IServicioArchivoImportacionDbus, ServicioArchivoImportacionDbusMonitorizado>();
|
||||||
|
builder.Services.AddSingleton<CreadorRedTransporteDbus>();
|
||||||
|
builder.Services.AddSingleton<IRedBusServicio, RedBusServicio>();
|
||||||
|
builder.Services.AddSingleton<IHistorialRutasCompartidoService, ServicioHistorialRutasCompartido>();
|
||||||
|
builder.Services.AddSingleton<IGeocodificadorLocalServicio, GeocodificadorLocalServicio>();
|
||||||
|
builder.Services.AddSingleton<ServicioHorariosDbus>();
|
||||||
|
builder.Services.AddSingleton<CreadorIndiceTransitoDbus>();
|
||||||
|
builder.Services.AddSingleton<ServicioIndiceTransitoDbus>();
|
||||||
|
builder.Services.AddSingleton<BuscadorRutasTemporalesDbus>();
|
||||||
|
builder.Services.AddSingleton<IPlanificadorRutas, PlanificadorRutasServicio>();
|
||||||
|
|
||||||
|
// HttpClient genérico (lo usas en el Razor: @inject HttpClient Http)
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
// Clientes HTTP tipados
|
||||||
|
builder.Services.AddHttpClient<IClienteOsrm, ClienteOsrm>();
|
||||||
|
builder.Services.AddHttpClient<IClienteValhalla, ClienteValhalla>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// Pipeline
|
||||||
|
// =====================================================
|
||||||
|
if (!app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||||
|
app.UseHsts();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.UseAntiforgery();
|
||||||
|
|
||||||
|
app.MapRazorComponents<App>()
|
||||||
|
.AddInteractiveServerRenderMode();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
|
||||||
38
RutasDBUS/Properties/launchSettings.json
Normal file
38
RutasDBUS/Properties/launchSettings.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:36806",
|
||||||
|
"sslPort": 44389
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "http://localhost:5170",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "https://localhost:7131;http://localhost:5170",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
RutasDBUS/RutasDBUS.csproj
Normal file
37
RutasDBUS/RutasDBUS.csproj
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Modelos\Horarios\zip_Importacion_20260213~0900.zip" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="LeafletForBlazor" Version="2.2.8.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Modelos\Horarios\**\*.zip">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Modelos\Callejero\**\*.zip">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="wwwroot\app.js">
|
||||||
|
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Update="wwwroot\favicon.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
152
RutasDBUS/Servicios/Caminata/ClienteOsrm.cs
Normal file
152
RutasDBUS/Servicios/Caminata/ClienteOsrm.cs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Caminata;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implementación del cliente OSRM para calcular rutas a pie.
|
||||||
|
/// </summary>
|
||||||
|
public class ClienteOsrm : IClienteOsrm
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase ClienteOsrm.
|
||||||
|
/// </summary>
|
||||||
|
public ClienteOsrm(HttpClient httpClient, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
|
||||||
|
_baseUrl = configuration["Caminata:Osrm:BaseUrl"] ?? "";
|
||||||
|
_baseUrl = _baseUrl.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_baseUrl))
|
||||||
|
throw new InvalidOperationException("Falta la configuración 'Caminata:Osrm:BaseUrl' en appsettings.json.");
|
||||||
|
|
||||||
|
_baseUrl = _baseUrl.TrimEnd('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<(double[,]? Linea, double DistanciaMetros, double DuracionSegundos)> ObtenerRutaAPieAsync(
|
||||||
|
double latitudDesde,
|
||||||
|
double longitudDesde,
|
||||||
|
double latitudHasta,
|
||||||
|
double longitudHasta)
|
||||||
|
{
|
||||||
|
// OSRM: /route/v1/walking/{lon1},{lat1};{lon2},{lat2}?overview=full&geometries=geojson&alternatives=false
|
||||||
|
var url =
|
||||||
|
$"{_baseUrl}/walking/" +
|
||||||
|
$"{longitudDesde.ToString(CultureInfo.InvariantCulture)}," +
|
||||||
|
$"{latitudDesde.ToString(CultureInfo.InvariantCulture)};" +
|
||||||
|
$"{longitudHasta.ToString(CultureInfo.InvariantCulture)}," +
|
||||||
|
$"{latitudHasta.ToString(CultureInfo.InvariantCulture)}" +
|
||||||
|
"?overview=full&geometries=geojson&alternatives=false";
|
||||||
|
|
||||||
|
RespuestaOsrm? respuesta;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
respuesta = await _httpClient.GetFromJsonAsync<RespuestaOsrm>(url);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (null, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var ruta = respuesta?.Rutas?.FirstOrDefault();
|
||||||
|
var coords = ruta?.Geometria?.Coordenadas;
|
||||||
|
|
||||||
|
if (ruta is null || coords is null || coords.Count == 0)
|
||||||
|
return (null, 0, 0);
|
||||||
|
|
||||||
|
var linea = new double[coords.Count, 2];
|
||||||
|
|
||||||
|
for (int i = 0; i < coords.Count; i++)
|
||||||
|
{
|
||||||
|
// GeoJSON: [lon, lat]
|
||||||
|
double lon = coords[i][0];
|
||||||
|
double lat = coords[i][1];
|
||||||
|
|
||||||
|
linea[i, 0] = lat;
|
||||||
|
linea[i, 1] = lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
AnclarExtremosLinea(linea, latitudDesde, longitudDesde, latitudHasta, longitudHasta),
|
||||||
|
ruta.Distancia,
|
||||||
|
ruta.Duracion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fuerza que la geometria empiece y termine exactamente en los puntos solicitados.
|
||||||
|
/// </summary>
|
||||||
|
private static double[,] AnclarExtremosLinea(
|
||||||
|
double[,] linea,
|
||||||
|
double latitudDesde,
|
||||||
|
double longitudDesde,
|
||||||
|
double latitudHasta,
|
||||||
|
double longitudHasta)
|
||||||
|
{
|
||||||
|
var puntos = linea.GetLength(0);
|
||||||
|
if (puntos <= 0)
|
||||||
|
{
|
||||||
|
return new double[,]
|
||||||
|
{
|
||||||
|
{ latitudDesde, longitudDesde },
|
||||||
|
{ latitudHasta, longitudHasta }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (puntos == 1)
|
||||||
|
{
|
||||||
|
return new double[,]
|
||||||
|
{
|
||||||
|
{ latitudDesde, longitudDesde },
|
||||||
|
{ latitudHasta, longitudHasta }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
linea[0, 0] = latitudDesde;
|
||||||
|
linea[0, 1] = longitudDesde;
|
||||||
|
linea[puntos - 1, 0] = latitudHasta;
|
||||||
|
linea[puntos - 1, 1] = longitudHasta;
|
||||||
|
return linea;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Modelos de respuesta OSRM
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
private sealed class RespuestaOsrm
|
||||||
|
{
|
||||||
|
[JsonPropertyName("routes")]
|
||||||
|
public System.Collections.Generic.List<RutaOsrm>? Rutas { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RutaOsrm
|
||||||
|
{
|
||||||
|
[JsonPropertyName("distance")]
|
||||||
|
public double Distancia { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("duration")]
|
||||||
|
public double Duracion { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("geometry")]
|
||||||
|
public GeometriaOsrm? Geometria { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class GeometriaOsrm
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string? Tipo { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("coordinates")]
|
||||||
|
public System.Collections.Generic.List<System.Collections.Generic.List<double>>? Coordenadas { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
275
RutasDBUS/Servicios/Caminata/ClienteValhalla.cs
Normal file
275
RutasDBUS/Servicios/Caminata/ClienteValhalla.cs
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Caminata;
|
||||||
|
|
||||||
|
public class ClienteValhalla : IClienteValhalla
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase ClienteValhalla.
|
||||||
|
/// </summary>
|
||||||
|
public ClienteValhalla(HttpClient httpClient, IConfiguration configuracion)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
|
||||||
|
_baseUrl = configuracion["Caminata:Valhalla:BaseUrl"]
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"Falta la configuración 'Caminata:Valhalla:BaseUrl' en appsettings.json.");
|
||||||
|
|
||||||
|
_baseUrl = _baseUrl.Trim().TrimEnd('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(double[,]? Linea, double DistanciaMetros, double DuracionSegundos)> ObtenerRutaAsync(
|
||||||
|
double latitudDesde,
|
||||||
|
double longitudDesde,
|
||||||
|
double latitudHasta,
|
||||||
|
double longitudHasta,
|
||||||
|
string costing = "pedestrian")
|
||||||
|
{
|
||||||
|
var cuerpo = new SolicitudValhalla
|
||||||
|
{
|
||||||
|
Ubicaciones = new List<LocalizacionValhalla>
|
||||||
|
{
|
||||||
|
new() { Latitud = latitudDesde, Longitud = longitudDesde },
|
||||||
|
new() { Latitud = latitudHasta, Longitud = longitudHasta }
|
||||||
|
},
|
||||||
|
PerfilCoste = string.IsNullOrWhiteSpace(costing) ? "pedestrian" : costing,
|
||||||
|
FormatoGeometria = "polyline6",
|
||||||
|
OpcionesIndicaciones = new OpcionesIndicacionesValhalla
|
||||||
|
{
|
||||||
|
Idioma = "es-ES",
|
||||||
|
Unidades = "kilometers"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
HttpResponseMessage respuesta;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
respuesta = await _httpClient.PostAsJsonAsync($"{_baseUrl}/route", cuerpo);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (null, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!respuesta.IsSuccessStatusCode)
|
||||||
|
return (null, 0, 0);
|
||||||
|
|
||||||
|
RespuestaRutaValhalla? data;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
data = await respuesta.Content.ReadFromJsonAsync<RespuestaRutaValhalla>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (null, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var viaje = data?.Viaje;
|
||||||
|
if (viaje is null || viaje.Tramos is null || viaje.Tramos.Count == 0)
|
||||||
|
return (null, 0, 0);
|
||||||
|
|
||||||
|
var forma = viaje.Tramos[0]?.Forma;
|
||||||
|
if (string.IsNullOrWhiteSpace(forma))
|
||||||
|
return (null, 0, 0);
|
||||||
|
|
||||||
|
var coords = DecodificarPolyline6(forma);
|
||||||
|
if (coords.Count == 0)
|
||||||
|
return (null, 0, 0);
|
||||||
|
|
||||||
|
var linea = new double[coords.Count, 2];
|
||||||
|
for (int i = 0; i < coords.Count; i++)
|
||||||
|
{
|
||||||
|
linea[i, 0] = coords[i].Latitud;
|
||||||
|
linea[i, 1] = coords[i].Longitud;
|
||||||
|
}
|
||||||
|
|
||||||
|
// summary.length depende de units
|
||||||
|
double longitud = viaje.Resumen?.Longitud ?? 0.0;
|
||||||
|
string unidades = (viaje.Unidades ?? "").Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
double distanciaMetros = unidades switch
|
||||||
|
{
|
||||||
|
"miles" => longitud * 1609.344,
|
||||||
|
_ => longitud * 1000.0 // "kilometers" o desconocido -> km
|
||||||
|
};
|
||||||
|
|
||||||
|
double duracionSegundos = viaje.Resumen?.Tiempo ?? 0.0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
AnclarExtremosLinea(linea, latitudDesde, longitudDesde, latitudHasta, longitudHasta),
|
||||||
|
distanciaMetros,
|
||||||
|
duracionSegundos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =======================
|
||||||
|
// Modelos request/response
|
||||||
|
// =======================
|
||||||
|
|
||||||
|
private sealed class SolicitudValhalla
|
||||||
|
{
|
||||||
|
[JsonPropertyName("locations")]
|
||||||
|
public List<LocalizacionValhalla> Ubicaciones { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("costing")]
|
||||||
|
public string PerfilCoste { get; set; } = "pedestrian";
|
||||||
|
|
||||||
|
[JsonPropertyName("shape_format")]
|
||||||
|
public string FormatoGeometria { get; set; } = "polyline6";
|
||||||
|
|
||||||
|
[JsonPropertyName("directions_options")]
|
||||||
|
public OpcionesIndicacionesValhalla OpcionesIndicaciones { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LocalizacionValhalla
|
||||||
|
{
|
||||||
|
[JsonPropertyName("lat")]
|
||||||
|
public double Latitud { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("lon")]
|
||||||
|
public double Longitud { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class OpcionesIndicacionesValhalla
|
||||||
|
{
|
||||||
|
[JsonPropertyName("language")]
|
||||||
|
public string Idioma { get; set; } = "es-ES";
|
||||||
|
|
||||||
|
[JsonPropertyName("units")]
|
||||||
|
public string Unidades { get; set; } = "kilometers";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RespuestaRutaValhalla
|
||||||
|
{
|
||||||
|
[JsonPropertyName("trip")]
|
||||||
|
public ViajeValhalla? Viaje { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ViajeValhalla
|
||||||
|
{
|
||||||
|
[JsonPropertyName("summary")]
|
||||||
|
public ResumenValhalla? Resumen { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("legs")]
|
||||||
|
public List<TramoValhalla?> Tramos { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("units")]
|
||||||
|
public string? Unidades { get; set; } // "kilometers" / "miles"
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ResumenValhalla
|
||||||
|
{
|
||||||
|
[JsonPropertyName("length")]
|
||||||
|
public double Longitud { get; set; } // en km si units=kilometers
|
||||||
|
|
||||||
|
[JsonPropertyName("time")]
|
||||||
|
public double Tiempo { get; set; } // segundos
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TramoValhalla
|
||||||
|
{
|
||||||
|
[JsonPropertyName("shape")]
|
||||||
|
public string? Forma { get; set; } // polyline6
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
private readonly record struct Coordenada(double Latitud, double Longitud);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodifica polyline6 (factor 1e6).
|
||||||
|
/// </summary>
|
||||||
|
private static List<Coordenada> DecodificarPolyline6(string encoded)
|
||||||
|
{
|
||||||
|
var coords = new List<Coordenada>();
|
||||||
|
if (string.IsNullOrEmpty(encoded)) return coords;
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
int lat = 0, lon = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (index < encoded.Length)
|
||||||
|
{
|
||||||
|
lat += LeerSiguienteValor(encoded, ref index);
|
||||||
|
lon += LeerSiguienteValor(encoded, ref index);
|
||||||
|
|
||||||
|
coords.Add(new Coordenada(lat / 1e6, lon / 1e6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// si llega corrupto o incompleto, devolvemos lo que haya (o vacío)
|
||||||
|
if (coords.Count == 0) return new List<Coordenada>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee siguiente valor.
|
||||||
|
/// </summary>
|
||||||
|
private static int LeerSiguienteValor(string encoded, ref int index)
|
||||||
|
{
|
||||||
|
int result = 0;
|
||||||
|
int shift = 0;
|
||||||
|
int b;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (index >= encoded.Length) throw new InvalidOperationException("Polyline incompleta.");
|
||||||
|
b = encoded[index++] - 63;
|
||||||
|
result |= (b & 0x1f) << shift;
|
||||||
|
shift += 5;
|
||||||
|
}
|
||||||
|
while (b >= 0x20);
|
||||||
|
|
||||||
|
// zigzag decode
|
||||||
|
return (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fuerza que la geometria empiece y termine exactamente en los puntos solicitados.
|
||||||
|
/// </summary>
|
||||||
|
private static double[,] AnclarExtremosLinea(
|
||||||
|
double[,] linea,
|
||||||
|
double latitudDesde,
|
||||||
|
double longitudDesde,
|
||||||
|
double latitudHasta,
|
||||||
|
double longitudHasta)
|
||||||
|
{
|
||||||
|
var puntos = linea.GetLength(0);
|
||||||
|
if (puntos <= 0)
|
||||||
|
{
|
||||||
|
return new double[,]
|
||||||
|
{
|
||||||
|
{ latitudDesde, longitudDesde },
|
||||||
|
{ latitudHasta, longitudHasta }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (puntos == 1)
|
||||||
|
{
|
||||||
|
return new double[,]
|
||||||
|
{
|
||||||
|
{ latitudDesde, longitudDesde },
|
||||||
|
{ latitudHasta, longitudHasta }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
linea[0, 0] = latitudDesde;
|
||||||
|
linea[0, 1] = longitudDesde;
|
||||||
|
linea[puntos - 1, 0] = latitudHasta;
|
||||||
|
linea[puntos - 1, 1] = longitudHasta;
|
||||||
|
return linea;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
RutasDBUS/Servicios/Caminata/IClienteOsrm.cs
Normal file
28
RutasDBUS/Servicios/Caminata/IClienteOsrm.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Caminata;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cliente para consumir el servicio OSRM y obtener rutas a pie.
|
||||||
|
/// </summary>
|
||||||
|
public interface IClienteOsrm
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene una ruta a pie entre dos coordenadas.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="latitudDesde">Latitud de origen.</param>
|
||||||
|
/// <param name="longitudDesde">Longitud de origen.</param>
|
||||||
|
/// <param name="latitudHasta">Latitud de destino.</param>
|
||||||
|
/// <param name="longitudHasta">Longitud de destino.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Una tupla con:
|
||||||
|
/// - Linea: matriz [n,2] con latitud/longitud de la polilínea.
|
||||||
|
/// - DistanciaMetros: distancia total en metros.
|
||||||
|
/// - DuracionSegundos: duración total en segundos.
|
||||||
|
/// </returns>
|
||||||
|
Task<(double[,]? Linea, double DistanciaMetros, double DuracionSegundos)> ObtenerRutaAPieAsync(
|
||||||
|
double latitudDesde,
|
||||||
|
double longitudDesde,
|
||||||
|
double latitudHasta,
|
||||||
|
double longitudHasta);
|
||||||
|
}
|
||||||
17
RutasDBUS/Servicios/Caminata/IClienteValhalla.cs
Normal file
17
RutasDBUS/Servicios/Caminata/IClienteValhalla.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Caminata;
|
||||||
|
|
||||||
|
public interface IClienteValhalla
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula una ruta entre dos puntos usando Valhalla.
|
||||||
|
/// Devuelve la línea (lat,lon), la distancia (m) y la duración (s).
|
||||||
|
/// </summary>
|
||||||
|
Task<(double[,]? Linea, double DistanciaMetros, double DuracionSegundos)> ObtenerRutaAsync(
|
||||||
|
double latitudDesde,
|
||||||
|
double longitudDesde,
|
||||||
|
double latitudHasta,
|
||||||
|
double longitudHasta,
|
||||||
|
string costing = "pedestrian"); // "pedestrian", "bicycle", etc.
|
||||||
|
}
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
|
using RutasDBUS.Modelos.Geocodificacion;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Geocodificacion;
|
||||||
|
|
||||||
|
public sealed class GeocodificadorLocalServicio : IGeocodificadorLocalServicio
|
||||||
|
{
|
||||||
|
private const string RutaCallejeroZipRelativa = "Modelos/Callejero/GFA_DSET_AD_CSV.zip";
|
||||||
|
private const string NombreCsvB5m = "GFA_DSET_AD.csv";
|
||||||
|
private const int MaxResultadosInternos = 60;
|
||||||
|
|
||||||
|
private readonly IWebHostEnvironment _entorno;
|
||||||
|
private readonly SemaphoreSlim _semaforoCarga = new(1, 1);
|
||||||
|
private IReadOnlyList<EntradaGeocodificada>? _indice;
|
||||||
|
|
||||||
|
public GeocodificadorLocalServicio(IWebHostEnvironment entorno)
|
||||||
|
{
|
||||||
|
_entorno = entorno;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<LugarGeocodificado>> BuscarAsync(
|
||||||
|
string texto,
|
||||||
|
int maxResultados = 8,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(texto))
|
||||||
|
return Array.Empty<LugarGeocodificado>();
|
||||||
|
|
||||||
|
var indice = await ObtenerIndiceAsync(cancellationToken);
|
||||||
|
if (indice.Count == 0)
|
||||||
|
return Array.Empty<LugarGeocodificado>();
|
||||||
|
|
||||||
|
var consulta = NormalizarParaBusqueda(texto);
|
||||||
|
var tokens = consulta
|
||||||
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Where(token => token.Length > 0)
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (tokens.Length == 0)
|
||||||
|
return Array.Empty<LugarGeocodificado>();
|
||||||
|
|
||||||
|
maxResultados = Math.Clamp(maxResultados, 1, 30);
|
||||||
|
|
||||||
|
return indice
|
||||||
|
.Select(entrada => new
|
||||||
|
{
|
||||||
|
Entrada = entrada,
|
||||||
|
Puntuacion = Puntuar(entrada, consulta, tokens)
|
||||||
|
})
|
||||||
|
.Where(x => x.Puntuacion < int.MaxValue)
|
||||||
|
.OrderBy(x => x.Puntuacion)
|
||||||
|
.ThenBy(x => x.Entrada.Nombre, StringComparer.CurrentCultureIgnoreCase)
|
||||||
|
.Take(maxResultados)
|
||||||
|
.Select(x => x.Entrada.Lugar)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> ContarLugaresAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var indice = await ObtenerIndiceAsync(cancellationToken);
|
||||||
|
return indice.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<EntradaGeocodificada>> ObtenerIndiceAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_indice is not null)
|
||||||
|
return _indice;
|
||||||
|
|
||||||
|
await _semaforoCarga.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_indice is not null)
|
||||||
|
return _indice;
|
||||||
|
|
||||||
|
_indice = CargarIndice(cancellationToken);
|
||||||
|
return _indice;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaforoCarga.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<EntradaGeocodificada> CargarIndice(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var rutaZip = Path.Combine(
|
||||||
|
_entorno.ContentRootPath,
|
||||||
|
RutaCallejeroZipRelativa.Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
|
||||||
|
if (!File.Exists(rutaZip))
|
||||||
|
return Array.Empty<EntradaGeocodificada>();
|
||||||
|
|
||||||
|
using var zip = ZipFile.OpenRead(rutaZip);
|
||||||
|
var entradaCsv = zip.Entries.FirstOrDefault(entrada =>
|
||||||
|
entrada.FullName.EndsWith(NombreCsvB5m, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (entradaCsv is null)
|
||||||
|
return Array.Empty<EntradaGeocodificada>();
|
||||||
|
|
||||||
|
using var stream = entradaCsv.Open();
|
||||||
|
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
||||||
|
|
||||||
|
var cabecera = reader.ReadLine();
|
||||||
|
if (string.IsNullOrWhiteSpace(cabecera))
|
||||||
|
return Array.Empty<EntradaGeocodificada>();
|
||||||
|
|
||||||
|
var columnas = ParsearCsv(cabecera);
|
||||||
|
var indices = columnas
|
||||||
|
.Select((nombre, indice) => new { nombre, indice })
|
||||||
|
.ToDictionary(x => x.nombre.Trim(), x => x.indice, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var resultado = new List<EntradaGeocodificada>(70000);
|
||||||
|
var vistos = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
while (!reader.EndOfStream)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var linea = reader.ReadLine();
|
||||||
|
if (string.IsNullOrWhiteSpace(linea))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var campos = ParsearCsv(linea);
|
||||||
|
if (!TryCrearEntradaB5m(campos, indices, vistos, out var entrada))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
resultado.Add(entrada);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultado;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryCrearEntradaB5m(
|
||||||
|
IReadOnlyList<string> campos,
|
||||||
|
IReadOnlyDictionary<string, int> indices,
|
||||||
|
ISet<string> vistos,
|
||||||
|
out EntradaGeocodificada entrada)
|
||||||
|
{
|
||||||
|
entrada = default!;
|
||||||
|
|
||||||
|
var coordenadas = Valor(campos, indices, "CEN_LONLAT");
|
||||||
|
if (!TryParsearLonLat(coordenadas, out var lon, out var lat))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var municipio = PrimerValor(
|
||||||
|
Valor(campos, indices, "MUNI_ES"),
|
||||||
|
Valor(campos, indices, "MUNI_EU"));
|
||||||
|
|
||||||
|
var nombre = PrimerValor(
|
||||||
|
Valor(campos, indices, "NAME_ES"),
|
||||||
|
Valor(campos, indices, "NAME"),
|
||||||
|
Valor(campos, indices, "NAME_EU"));
|
||||||
|
|
||||||
|
var calle = PrimerValor(
|
||||||
|
Valor(campos, indices, "STREET_ES"),
|
||||||
|
Valor(campos, indices, "STREET_EU"));
|
||||||
|
|
||||||
|
var portal = PrimerValor(
|
||||||
|
Valor(campos, indices, "HOUSE_NUMBER"),
|
||||||
|
Valor(campos, indices, "BIS"));
|
||||||
|
|
||||||
|
var edificio = PrimerValor(
|
||||||
|
Valor(campos, indices, "NAME_BUILDING_ES"),
|
||||||
|
Valor(campos, indices, "NAME_BUILDING_EU"));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(nombre) &&
|
||||||
|
string.IsNullOrWhiteSpace(calle) &&
|
||||||
|
string.IsNullOrWhiteSpace(edificio))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = Valor(campos, indices, "B5MCODE");
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
id = $"{lat:0.000000}|{lon:0.000000}|{nombre}|{edificio}";
|
||||||
|
|
||||||
|
if (!vistos.Add(id))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var etiqueta = ConstruirEtiqueta(nombre, calle, portal, edificio, municipio);
|
||||||
|
if (string.IsNullOrWhiteSpace(etiqueta))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var subtitulo = ConstruirSubtitulo(calle, portal, edificio, municipio);
|
||||||
|
var tipo = string.IsNullOrWhiteSpace(edificio)
|
||||||
|
? "Direccion"
|
||||||
|
: "Lugar";
|
||||||
|
|
||||||
|
var lugar = new LugarGeocodificado
|
||||||
|
{
|
||||||
|
Id = "b5m:" + id,
|
||||||
|
Nombre = etiqueta,
|
||||||
|
Subtitulo = subtitulo,
|
||||||
|
Tipo = tipo,
|
||||||
|
Fuente = "B5M",
|
||||||
|
Municipio = municipio,
|
||||||
|
Latitud = lat,
|
||||||
|
Longitud = lon
|
||||||
|
};
|
||||||
|
|
||||||
|
var textoBusqueda = string.Join(' ', new[]
|
||||||
|
{
|
||||||
|
etiqueta,
|
||||||
|
subtitulo,
|
||||||
|
nombre,
|
||||||
|
calle,
|
||||||
|
portal,
|
||||||
|
edificio,
|
||||||
|
municipio
|
||||||
|
}.Where(valor => !string.IsNullOrWhiteSpace(valor)));
|
||||||
|
|
||||||
|
entrada = new EntradaGeocodificada(
|
||||||
|
lugar,
|
||||||
|
etiqueta,
|
||||||
|
municipio,
|
||||||
|
NormalizarParaBusqueda(textoBusqueda),
|
||||||
|
NormalizarParaBusqueda(etiqueta),
|
||||||
|
NormalizarParaBusqueda(calle),
|
||||||
|
NormalizarParaBusqueda(edificio),
|
||||||
|
NormalizarParaBusqueda(portal));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Puntuar(EntradaGeocodificada entrada, string consulta, IReadOnlyList<string> tokens)
|
||||||
|
{
|
||||||
|
foreach (var token in tokens)
|
||||||
|
{
|
||||||
|
if (!entrada.TextoBusqueda.Contains(token, StringComparison.Ordinal))
|
||||||
|
return int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var puntuacion = 500;
|
||||||
|
|
||||||
|
if (entrada.EtiquetaNormalizada.Equals(consulta, StringComparison.Ordinal))
|
||||||
|
puntuacion -= 260;
|
||||||
|
else if (entrada.EtiquetaNormalizada.StartsWith(consulta, StringComparison.Ordinal))
|
||||||
|
puntuacion -= 220;
|
||||||
|
else if (!string.IsNullOrWhiteSpace(entrada.EdificioNormalizado) &&
|
||||||
|
entrada.EdificioNormalizado.StartsWith(consulta, StringComparison.Ordinal))
|
||||||
|
puntuacion -= 210;
|
||||||
|
else if (!string.IsNullOrWhiteSpace(entrada.CalleNormalizada) &&
|
||||||
|
entrada.CalleNormalizada.StartsWith(consulta, StringComparison.Ordinal))
|
||||||
|
puntuacion -= 190;
|
||||||
|
else if (entrada.TextoBusqueda.StartsWith(consulta, StringComparison.Ordinal))
|
||||||
|
puntuacion -= 150;
|
||||||
|
|
||||||
|
if (tokens.Count > 1 && tokens.All(token =>
|
||||||
|
entrada.EtiquetaNormalizada.Contains(token, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
puntuacion -= 45;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(entrada.PortalNormalizado) &&
|
||||||
|
tokens.Any(token => token.Equals(entrada.PortalNormalizado, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
puntuacion -= 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EsDonostia(entrada.Municipio))
|
||||||
|
puntuacion -= 55;
|
||||||
|
|
||||||
|
puntuacion += Math.Min(60, entrada.EtiquetaNormalizada.Length / 4);
|
||||||
|
return Math.Max(0, puntuacion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool EsDonostia(string municipio)
|
||||||
|
{
|
||||||
|
var normalizado = NormalizarParaBusqueda(municipio);
|
||||||
|
return normalizado.Contains("san sebastian", StringComparison.Ordinal) ||
|
||||||
|
normalizado.Contains("donostia", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ConstruirEtiqueta(string nombre, string calle, string portal, string edificio, string municipio)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(edificio))
|
||||||
|
{
|
||||||
|
var direccion = ConstruirDireccionCorta(calle, portal);
|
||||||
|
return string.IsNullOrWhiteSpace(direccion)
|
||||||
|
? edificio.Trim()
|
||||||
|
: $"{edificio.Trim()} - {direccion}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(nombre))
|
||||||
|
return QuitarMunicipioInicial(nombre, municipio).Trim();
|
||||||
|
|
||||||
|
return ConstruirDireccionCorta(calle, portal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ConstruirSubtitulo(string calle, string portal, string edificio, string municipio)
|
||||||
|
{
|
||||||
|
var partes = new List<string>();
|
||||||
|
var direccion = ConstruirDireccionCorta(calle, portal);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(edificio) &&
|
||||||
|
!string.IsNullOrWhiteSpace(direccion))
|
||||||
|
{
|
||||||
|
partes.Add(direccion);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(municipio))
|
||||||
|
partes.Add(municipio.Trim());
|
||||||
|
|
||||||
|
return string.Join(" · ", partes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ConstruirDireccionCorta(string calle, string portal)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(calle))
|
||||||
|
return portal.Trim();
|
||||||
|
|
||||||
|
calle = calle.Trim();
|
||||||
|
portal = portal.Trim();
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(portal)
|
||||||
|
? calle
|
||||||
|
: $"{calle} {portal}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string QuitarMunicipioInicial(string nombre, string municipio)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(nombre) || string.IsNullOrWhiteSpace(municipio))
|
||||||
|
return nombre;
|
||||||
|
|
||||||
|
nombre = nombre.Trim();
|
||||||
|
municipio = municipio.Trim();
|
||||||
|
|
||||||
|
return nombre.StartsWith(municipio + " ", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? nombre[municipio.Length..].Trim()
|
||||||
|
: nombre;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParsearLonLat(string texto, out double longitud, out double latitud)
|
||||||
|
{
|
||||||
|
longitud = 0;
|
||||||
|
latitud = 0;
|
||||||
|
|
||||||
|
var partes = (texto ?? "").Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (partes.Length < 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return double.TryParse(partes[0], NumberStyles.Float, CultureInfo.InvariantCulture, out longitud) &&
|
||||||
|
double.TryParse(partes[1], NumberStyles.Float, CultureInfo.InvariantCulture, out latitud) &&
|
||||||
|
double.IsFinite(longitud) &&
|
||||||
|
double.IsFinite(latitud);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Valor(IReadOnlyList<string> campos, IReadOnlyDictionary<string, int> indices, string columna)
|
||||||
|
{
|
||||||
|
if (!indices.TryGetValue(columna, out var indice) ||
|
||||||
|
indice < 0 ||
|
||||||
|
indice >= campos.Count)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return campos[indice].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string PrimerValor(params string[] valores)
|
||||||
|
=> valores.FirstOrDefault(valor => !string.IsNullOrWhiteSpace(valor))?.Trim() ?? "";
|
||||||
|
|
||||||
|
private static List<string> ParsearCsv(string linea)
|
||||||
|
{
|
||||||
|
var campos = new List<string>();
|
||||||
|
var actual = new StringBuilder(linea.Length);
|
||||||
|
var entreComillas = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < linea.Length; i++)
|
||||||
|
{
|
||||||
|
var ch = linea[i];
|
||||||
|
if (ch == '"')
|
||||||
|
{
|
||||||
|
if (entreComillas && i + 1 < linea.Length && linea[i + 1] == '"')
|
||||||
|
{
|
||||||
|
actual.Append('"');
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entreComillas = !entreComillas;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == ',' && !entreComillas)
|
||||||
|
{
|
||||||
|
campos.Add(actual.ToString());
|
||||||
|
actual.Clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
actual.Append(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
campos.Add(actual.ToString());
|
||||||
|
return campos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizarParaBusqueda(string texto)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(texto))
|
||||||
|
return "";
|
||||||
|
|
||||||
|
texto = texto.Trim().ToLowerInvariant();
|
||||||
|
const char marcadorEnye = '\uE000';
|
||||||
|
texto = texto.Replace('\u00f1', marcadorEnye);
|
||||||
|
|
||||||
|
var formD = texto.Normalize(NormalizationForm.FormD);
|
||||||
|
var sb = new StringBuilder(formD.Length);
|
||||||
|
foreach (var ch in formD)
|
||||||
|
{
|
||||||
|
var uc = CharUnicodeInfo.GetUnicodeCategory(ch);
|
||||||
|
if (uc == UnicodeCategory.NonSpacingMark)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch))
|
||||||
|
{
|
||||||
|
sb.Append(ch);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(' ', sb
|
||||||
|
.ToString()
|
||||||
|
.Normalize(NormalizationForm.FormC)
|
||||||
|
.Replace(marcadorEnye, '\u00f1')
|
||||||
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record EntradaGeocodificada(
|
||||||
|
LugarGeocodificado Lugar,
|
||||||
|
string Nombre,
|
||||||
|
string Municipio,
|
||||||
|
string TextoBusqueda,
|
||||||
|
string EtiquetaNormalizada,
|
||||||
|
string CalleNormalizada,
|
||||||
|
string EdificioNormalizado,
|
||||||
|
string PortalNormalizado);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using RutasDBUS.Modelos.Geocodificacion;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Geocodificacion;
|
||||||
|
|
||||||
|
public interface IGeocodificadorLocalServicio
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<LugarGeocodificado>> BuscarAsync(
|
||||||
|
string texto,
|
||||||
|
int maxResultados = 8,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<int> ContarLugaresAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using RutasDBUS.Modelos.Historial;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Historial;
|
||||||
|
|
||||||
|
public interface IHistorialRutasCompartidoService
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<RegistroHistorialRuta>> ObtenerAsync(CancellationToken ct = default);
|
||||||
|
Task<RegistroHistorialRuta> GuardarAsync(RegistroHistorialRuta registro, CancellationToken ct = default);
|
||||||
|
Task<bool> EliminarAsync(string id, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using RutasDBUS.Modelos.Historial;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Historial;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mantiene un historial compartido de escenarios de origen/destino para toda la app.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ServicioHistorialRutasCompartido : IHistorialRutasCompartidoService
|
||||||
|
{
|
||||||
|
private const int MaxItemsCompartidos = 100;
|
||||||
|
private const int IntentosAccesoFichero = 5;
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim _mutex = new(1, 1);
|
||||||
|
private readonly string _rutaFichero;
|
||||||
|
|
||||||
|
public ServicioHistorialRutasCompartido(IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
_rutaFichero = ResolverRutaFichero(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<RegistroHistorialRuta>> ObtenerAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _mutex.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var items = await LeerInternoAsync(ct);
|
||||||
|
return items
|
||||||
|
.OrderByDescending(x => x.FechaActualizacionUtc)
|
||||||
|
.Select(Copiar)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mutex.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RegistroHistorialRuta> GuardarAsync(RegistroHistorialRuta registro, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(registro);
|
||||||
|
|
||||||
|
await _mutex.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var items = await LeerInternoAsync(ct);
|
||||||
|
var clave = CalcularClaveEscenario(registro);
|
||||||
|
var ahora = DateTime.UtcNow;
|
||||||
|
var fechaCreacionEntrada = registro.FechaCreacionUtc != default
|
||||||
|
? registro.FechaCreacionUtc
|
||||||
|
: ahora;
|
||||||
|
var fechaActualizacionEntrada = registro.FechaActualizacionUtc != default
|
||||||
|
? registro.FechaActualizacionUtc
|
||||||
|
: fechaCreacionEntrada;
|
||||||
|
|
||||||
|
var existente = items.FirstOrDefault(x =>
|
||||||
|
string.Equals(CalcularClaveEscenario(x), clave, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (existente is null)
|
||||||
|
{
|
||||||
|
existente = Copiar(registro);
|
||||||
|
existente.Id = string.IsNullOrWhiteSpace(existente.Id)
|
||||||
|
? Guid.NewGuid().ToString("N")
|
||||||
|
: existente.Id;
|
||||||
|
existente.FechaCreacionUtc = fechaCreacionEntrada;
|
||||||
|
existente.FechaActualizacionUtc = fechaActualizacionEntrada;
|
||||||
|
items.Insert(0, existente);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existente.OrigenTexto = registro.OrigenTexto;
|
||||||
|
existente.DestinoTexto = registro.DestinoTexto;
|
||||||
|
existente.OrigenLatitud = registro.OrigenLatitud;
|
||||||
|
existente.OrigenLongitud = registro.OrigenLongitud;
|
||||||
|
existente.DestinoLatitud = registro.DestinoLatitud;
|
||||||
|
existente.DestinoLongitud = registro.DestinoLongitud;
|
||||||
|
existente.UsarHorariosTeoricos = registro.UsarHorariosTeoricos;
|
||||||
|
existente.UsarHoraSimulada = registro.UsarHoraSimulada;
|
||||||
|
existente.FechaSimulada = registro.FechaSimulada;
|
||||||
|
existente.HoraSimulada = registro.HoraSimulada;
|
||||||
|
existente.CriterioPrioritarioRuta = registro.CriterioPrioritarioRuta;
|
||||||
|
existente.PreferenciaEtiquetaRuta = registro.PreferenciaEtiquetaRuta;
|
||||||
|
existente.ActivarFiltroLineas = registro.ActivarFiltroLineas;
|
||||||
|
existente.LineasPermitidas = NormalizarLineas(registro.LineasPermitidas).ToList();
|
||||||
|
|
||||||
|
if (registro.FechaCreacionUtc != default)
|
||||||
|
existente.FechaCreacionUtc = registro.FechaCreacionUtc;
|
||||||
|
if (registro.FechaActualizacionUtc != default)
|
||||||
|
existente.FechaActualizacionUtc = registro.FechaActualizacionUtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
var finales = items
|
||||||
|
.OrderByDescending(x => x.FechaActualizacionUtc)
|
||||||
|
.Take(MaxItemsCompartidos)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await EscribirInternoAsync(finales, ct);
|
||||||
|
return Copiar(existente);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mutex.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> EliminarAsync(string id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await _mutex.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var items = await LeerInternoAsync(ct);
|
||||||
|
var eliminados = items.RemoveAll(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (eliminados == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await EscribirInternoAsync(items, ct);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mutex.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<RegistroHistorialRuta>> LeerInternoAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!File.Exists(_rutaFichero))
|
||||||
|
return new List<RegistroHistorialRuta>();
|
||||||
|
|
||||||
|
return await EjecutarConReintentosAsync(async () =>
|
||||||
|
{
|
||||||
|
await using var stream = new FileStream(
|
||||||
|
_rutaFichero,
|
||||||
|
FileMode.Open,
|
||||||
|
FileAccess.Read,
|
||||||
|
FileShare.ReadWrite | FileShare.Delete,
|
||||||
|
4096,
|
||||||
|
useAsync: true);
|
||||||
|
|
||||||
|
var items = await JsonSerializer.DeserializeAsync<List<RegistroHistorialRuta>>(stream, JsonOptions, ct);
|
||||||
|
return items ?? new List<RegistroHistorialRuta>();
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EscribirInternoAsync(List<RegistroHistorialRuta> items, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var carpeta = Path.GetDirectoryName(_rutaFichero);
|
||||||
|
if (!string.IsNullOrWhiteSpace(carpeta))
|
||||||
|
Directory.CreateDirectory(carpeta);
|
||||||
|
|
||||||
|
await EjecutarConReintentosAsync(async () =>
|
||||||
|
{
|
||||||
|
var temporal = Path.Combine(
|
||||||
|
carpeta ?? Path.GetTempPath(),
|
||||||
|
$"{Path.GetFileName(_rutaFichero)}.{Guid.NewGuid():N}.tmp");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using (var stream = new FileStream(
|
||||||
|
temporal,
|
||||||
|
FileMode.CreateNew,
|
||||||
|
FileAccess.Write,
|
||||||
|
FileShare.None,
|
||||||
|
4096,
|
||||||
|
useAsync: true))
|
||||||
|
{
|
||||||
|
await JsonSerializer.SerializeAsync(stream, items, JsonOptions, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Move(temporal, _rutaFichero, overwrite: true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TryDelete(temporal);
|
||||||
|
}
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<T> EjecutarConReintentosAsync<T>(Func<Task<T>> accion, CancellationToken ct)
|
||||||
|
{
|
||||||
|
for (var intento = 1; ; intento++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await accion();
|
||||||
|
}
|
||||||
|
catch (IOException) when (intento < IntentosAccesoFichero)
|
||||||
|
{
|
||||||
|
await Task.Delay(40 * intento, ct);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException) when (intento < IntentosAccesoFichero)
|
||||||
|
{
|
||||||
|
await Task.Delay(40 * intento, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task EjecutarConReintentosAsync(Func<Task> accion, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await EjecutarConReintentosAsync(async () =>
|
||||||
|
{
|
||||||
|
await accion();
|
||||||
|
return true;
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDelete(string ruta)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(ruta))
|
||||||
|
File.Delete(ruta);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolverRutaFichero(IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
foreach (var carpeta in EnumerarCarpetasCandidatas(env))
|
||||||
|
{
|
||||||
|
if (TryEnsureWritableDirectory(carpeta))
|
||||||
|
return Path.Combine(carpeta, "historial-rutas-compartido.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(env.ContentRootPath, "Data", "historial-rutas-compartido.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> EnumerarCarpetasCandidatas(IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
yield return Path.Combine(env.ContentRootPath, "Data");
|
||||||
|
|
||||||
|
var commonAppData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||||
|
if (!string.IsNullOrWhiteSpace(commonAppData))
|
||||||
|
yield return Path.Combine(commonAppData, "RutasDBUS");
|
||||||
|
|
||||||
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
if (!string.IsNullOrWhiteSpace(localAppData))
|
||||||
|
yield return Path.Combine(localAppData, "RutasDBUS");
|
||||||
|
|
||||||
|
yield return Path.Combine(Path.GetTempPath(), "RutasDBUS");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryEnsureWritableDirectory(string carpeta)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(carpeta);
|
||||||
|
var rutaPrueba = Path.Combine(carpeta, $".write-test-{Guid.NewGuid():N}.tmp");
|
||||||
|
|
||||||
|
using (var stream = new FileStream(
|
||||||
|
rutaPrueba,
|
||||||
|
FileMode.CreateNew,
|
||||||
|
FileAccess.Write,
|
||||||
|
FileShare.None,
|
||||||
|
256,
|
||||||
|
FileOptions.DeleteOnClose))
|
||||||
|
{
|
||||||
|
stream.WriteByte(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(rutaPrueba))
|
||||||
|
File.Delete(rutaPrueba);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RegistroHistorialRuta Copiar(RegistroHistorialRuta item)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Id = item.Id,
|
||||||
|
OrigenTexto = item.OrigenTexto,
|
||||||
|
DestinoTexto = item.DestinoTexto,
|
||||||
|
OrigenLatitud = item.OrigenLatitud,
|
||||||
|
OrigenLongitud = item.OrigenLongitud,
|
||||||
|
DestinoLatitud = item.DestinoLatitud,
|
||||||
|
DestinoLongitud = item.DestinoLongitud,
|
||||||
|
UsarHorariosTeoricos = item.UsarHorariosTeoricos,
|
||||||
|
UsarHoraSimulada = item.UsarHoraSimulada,
|
||||||
|
FechaSimulada = item.FechaSimulada,
|
||||||
|
HoraSimulada = item.HoraSimulada,
|
||||||
|
CriterioPrioritarioRuta = item.CriterioPrioritarioRuta,
|
||||||
|
PreferenciaEtiquetaRuta = item.PreferenciaEtiquetaRuta,
|
||||||
|
ActivarFiltroLineas = item.ActivarFiltroLineas,
|
||||||
|
LineasPermitidas = NormalizarLineas(item.LineasPermitidas).ToList(),
|
||||||
|
FechaCreacionUtc = item.FechaCreacionUtc,
|
||||||
|
FechaActualizacionUtc = item.FechaActualizacionUtc
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string CalcularClaveEscenario(RegistroHistorialRuta item)
|
||||||
|
{
|
||||||
|
var origenLat = Math.Round(item.OrigenLatitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
|
||||||
|
var origenLon = Math.Round(item.OrigenLongitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
|
||||||
|
var destinoLat = Math.Round(item.DestinoLatitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
|
||||||
|
var destinoLon = Math.Round(item.DestinoLongitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
|
||||||
|
var fecha = item.UsarHoraSimulada ? item.FechaSimulada?.Trim() ?? string.Empty : string.Empty;
|
||||||
|
var hora = item.UsarHoraSimulada ? item.HoraSimulada?.Trim() ?? string.Empty : string.Empty;
|
||||||
|
var criterio = NormalizarTextoClave(item.CriterioPrioritarioRuta);
|
||||||
|
if (string.IsNullOrWhiteSpace(criterio))
|
||||||
|
criterio = NormalizarTextoClave(item.PreferenciaEtiquetaRuta);
|
||||||
|
var lineas = item.ActivarFiltroLineas
|
||||||
|
? string.Join(",", NormalizarLineas(item.LineasPermitidas))
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
return string.Join("|",
|
||||||
|
origenLat,
|
||||||
|
origenLon,
|
||||||
|
destinoLat,
|
||||||
|
destinoLon,
|
||||||
|
item.UsarHorariosTeoricos ? "1" : "0",
|
||||||
|
item.UsarHoraSimulada ? "1" : "0",
|
||||||
|
fecha,
|
||||||
|
hora,
|
||||||
|
criterio,
|
||||||
|
item.ActivarFiltroLineas ? "1" : "0",
|
||||||
|
lineas);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<int> NormalizarLineas(IEnumerable<int>? lineas)
|
||||||
|
=> (lineas ?? Array.Empty<int>())
|
||||||
|
.Where(linea => linea > 0)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(linea => linea)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private static string NormalizarTextoClave(string? texto)
|
||||||
|
=> texto?.Trim().ToUpperInvariant() ?? string.Empty;
|
||||||
|
}
|
||||||
1792
RutasDBUS/Servicios/Horarios/ServicioHorariosDbus.cs
Normal file
1792
RutasDBUS/Servicios/Horarios/ServicioHorariosDbus.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
|||||||
|
using RutasDBUS.Modelos.Importacion;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Importacion;
|
||||||
|
|
||||||
|
public interface IServicioArchivoImportacionDbus
|
||||||
|
{
|
||||||
|
Task<ArchivoImportacionDbus> ObtenerAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<ModeloHorarioDbusInfo>> ListarModelosAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<ModeloHorarioDbusInfo?> ObtenerModeloActivoAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task SeleccionarModeloAsync(string idModelo, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<ModeloHorarioDbusInfo> GuardarModeloSubidoAsync(
|
||||||
|
string nombreArchivo,
|
||||||
|
Stream contenido,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,728 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using RutasDBUS.Modelos.Importacion;
|
||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Importacion;
|
||||||
|
|
||||||
|
public sealed class ServicioArchivoImportacionDbus
|
||||||
|
{
|
||||||
|
private readonly IWebHostEnvironment _entorno;
|
||||||
|
private readonly OpcionesRedBus _opciones;
|
||||||
|
private readonly SemaphoreSlim _semCarga = new(1, 1);
|
||||||
|
private ArchivoImportacionDbus? _archive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase ServicioArchivoImportacionDbus.
|
||||||
|
/// </summary>
|
||||||
|
public ServicioArchivoImportacionDbus(
|
||||||
|
IWebHostEnvironment entorno,
|
||||||
|
IOptions<OpcionesRedBus> opciones)
|
||||||
|
{
|
||||||
|
_entorno = entorno;
|
||||||
|
_opciones = opciones.Value ?? new OpcionesRedBus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public async Task<ArchivoImportacionDbus> ObtenerAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (_archive is not null)
|
||||||
|
return _archive;
|
||||||
|
|
||||||
|
await _semCarga.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_archive is not null)
|
||||||
|
return _archive;
|
||||||
|
|
||||||
|
var zipPath = ResolverRutaZip();
|
||||||
|
if (zipPath is null || !File.Exists(zipPath))
|
||||||
|
throw new FileNotFoundException("No se ha encontrado el ZIP de importación de dBUS.");
|
||||||
|
|
||||||
|
_archive = await CargarArchivoAsync(zipPath, cancellationToken);
|
||||||
|
return _archive;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semCarga.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ArchivoImportacionDbus> ObtenerDesdeRutaAsync(
|
||||||
|
string zipPath,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(zipPath) || !File.Exists(zipPath))
|
||||||
|
throw new FileNotFoundException("No se ha encontrado el ZIP de importacion de dBUS.", zipPath);
|
||||||
|
|
||||||
|
return CargarArchivoAsync(zipPath, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resuelve zip camino.
|
||||||
|
/// </summary>
|
||||||
|
private string? ResolverRutaZip()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(_opciones.ArchivoDatos))
|
||||||
|
{
|
||||||
|
var configured = Path.Combine(
|
||||||
|
_entorno.ContentRootPath,
|
||||||
|
_opciones.ArchivoDatos.Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
|
||||||
|
if (File.Exists(configured))
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
var folder = Path.Combine(_entorno.ContentRootPath, "Modelos", "horarios");
|
||||||
|
if (!Directory.Exists(folder))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return Directory.GetFiles(folder, "*.zip", SearchOption.TopDirectoryOnly)
|
||||||
|
.Select(path => new FileInfo(path))
|
||||||
|
.OrderByDescending(info => info.LastWriteTimeUtc)
|
||||||
|
.FirstOrDefault()
|
||||||
|
?.FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Carga el archivo.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<ArchivoImportacionDbus> CargarArchivoAsync(string zipPath, CancellationToken ct)
|
||||||
|
{
|
||||||
|
using var fs = File.OpenRead(zipPath);
|
||||||
|
using var zip = new ZipArchive(fs, ZipArchiveMode.Read, leaveOpen: false);
|
||||||
|
|
||||||
|
var calendarios = await ReadCalendariosAsync(FindEntry(zip, "calendario.csv"), ct);
|
||||||
|
var calendarioEx = await ReadCalendarioExcepcionesAsync(FindEntry(zip, "calendario_excepciones_puntuales.csv"), ct);
|
||||||
|
var expediciones = await ReadExpedicionesAsync(FindEntry(zip, "expedicion.csv"), ct);
|
||||||
|
var operadores = await ReadOperadoresAsync(FindEntry(zip, "operador.csv"), ct);
|
||||||
|
var paradas = await ReadParadasAsync(FindEntry(zip, "parada.csv"), ct);
|
||||||
|
var patrones = await ReadPatronesAsync(FindEntry(zip, "patron.csv"), ct);
|
||||||
|
var rutas = await ReadRutasAsync(FindEntry(zip, "ruta.csv"), ct);
|
||||||
|
var tarifas = await ReadTarifasAsync(FindEntry(zip, "tarifa.csv"), ct);
|
||||||
|
var tiemposParada = await ReadTiemposParadaAsync(FindEntry(zip, "tiempo_parada.csv"), ct);
|
||||||
|
var tiemposReales = await ReadTiemposRealesAsync(FindEntry(zip, "tiempo_real.csv"), ct);
|
||||||
|
var transbordos = await ReadTransbordosAsync(FindEntry(zip, "transbordos.csv"), ct);
|
||||||
|
var vehiculos = await ReadVehiculosAsync(FindEntry(zip, "vehiculos.csv"), ct);
|
||||||
|
|
||||||
|
return new ArchivoImportacionDbus
|
||||||
|
{
|
||||||
|
RutaZip = zipPath,
|
||||||
|
FechaArchivoUtc = File.GetLastWriteTimeUtc(zipPath),
|
||||||
|
Calendarios = calendarios,
|
||||||
|
CalendarioExcepciones = calendarioEx,
|
||||||
|
Expediciones = expediciones,
|
||||||
|
Operadores = operadores,
|
||||||
|
Paradas = paradas,
|
||||||
|
Patrones = patrones,
|
||||||
|
Rutas = rutas,
|
||||||
|
Tarifas = tarifas,
|
||||||
|
TiemposParada = tiemposParada,
|
||||||
|
TiemposReales = tiemposReales,
|
||||||
|
Transbordos = transbordos,
|
||||||
|
Vehiculos = vehiculos,
|
||||||
|
ParadasPorId = paradas.ToDictionary(x => x.Id),
|
||||||
|
RutasPorId = rutas.ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase),
|
||||||
|
ExpedicionesPorId = expediciones.ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase),
|
||||||
|
TiemposParadaPorExpedicion = tiemposParada
|
||||||
|
.GroupBy(x => x.IdExp, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderBy(x => x.SecPar).ToList(),
|
||||||
|
StringComparer.OrdinalIgnoreCase),
|
||||||
|
PatronesPorId = patrones
|
||||||
|
.GroupBy(x => x.Id, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderBy(x => x.PtoSecuencia).ToList(),
|
||||||
|
StringComparer.OrdinalIgnoreCase),
|
||||||
|
TransbordosPorRuta = transbordos
|
||||||
|
.SelectMany(x => new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, FilaTransbordoCsv>(x.IdRuta1, x),
|
||||||
|
new KeyValuePair<string, FilaTransbordoCsv>(x.IdRuta2, x)
|
||||||
|
})
|
||||||
|
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.Select(x => x.Value).ToList(),
|
||||||
|
StringComparer.OrdinalIgnoreCase)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca entrada.
|
||||||
|
/// </summary>
|
||||||
|
private static ZipArchiveEntry? FindEntry(ZipArchive zip, string fileName)
|
||||||
|
=> zip.Entries.FirstOrDefault(entry =>
|
||||||
|
entry.FullName.EndsWith("/" + fileName, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
entry.FullName.Equals(fileName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee csv rows.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<string[]>> ReadCsvRowsAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = new List<string[]>();
|
||||||
|
if (entry is null)
|
||||||
|
return rows;
|
||||||
|
|
||||||
|
var contenidoCsv = await ReadCsvTextAsync(entry, ct);
|
||||||
|
using var reader = new StringReader(contenidoCsv);
|
||||||
|
|
||||||
|
string? line;
|
||||||
|
while ((line = await reader.ReadLineAsync()) is not null)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
rows.Add(SplitCsvLine(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee el contenido de un CSV intentando UTF-8 y cayendo a Windows-1252 cuando el archivo viene en esa codificación.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<string> ReadCsvTextAsync(ZipArchiveEntry entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
using var stream = entry.Open();
|
||||||
|
using var memory = new MemoryStream();
|
||||||
|
await stream.CopyToAsync(memory, ct);
|
||||||
|
|
||||||
|
var bytes = memory.ToArray();
|
||||||
|
var utf8Strict = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return utf8Strict.GetString(bytes);
|
||||||
|
}
|
||||||
|
catch (DecoderFallbackException)
|
||||||
|
{
|
||||||
|
return Encoding.GetEncoding(1252).GetString(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona csv linea.
|
||||||
|
/// </summary>
|
||||||
|
private static string[] SplitCsvLine(string line)
|
||||||
|
{
|
||||||
|
var values = new List<string>(16);
|
||||||
|
var sb = new StringBuilder(line.Length);
|
||||||
|
var inQuotes = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < line.Length; i++)
|
||||||
|
{
|
||||||
|
var c = line[i];
|
||||||
|
|
||||||
|
if (c == '"')
|
||||||
|
{
|
||||||
|
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
|
||||||
|
{
|
||||||
|
sb.Append('"');
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == ',' && !inQuotes)
|
||||||
|
{
|
||||||
|
values.Add(sb.ToString().Trim());
|
||||||
|
sb.Clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
values.Add(sb.ToString().Trim());
|
||||||
|
return values.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona of.
|
||||||
|
/// </summary>
|
||||||
|
private static int IndexOf(string[] header, string column)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < header.Length; i++)
|
||||||
|
{
|
||||||
|
if (header[i].Equals(column, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
private static string Get(string[] row, int index)
|
||||||
|
=> index >= 0 && index < row.Length ? row[index] : "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Analiza int.
|
||||||
|
/// </summary>
|
||||||
|
private static int ParseInt(string[] row, int index)
|
||||||
|
=> int.TryParse(Get(row, index), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Analiza nullable int.
|
||||||
|
/// </summary>
|
||||||
|
private static int? ParseNullableInt(string[] row, int index)
|
||||||
|
=> int.TryParse(Get(row, index), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Analiza double.
|
||||||
|
/// </summary>
|
||||||
|
private static double ParseDouble(string[] row, int index)
|
||||||
|
=> double.TryParse(Get(row, index), NumberStyles.Any, CultureInfo.InvariantCulture, out var value) ? value : 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Analiza nullable decimal.
|
||||||
|
/// </summary>
|
||||||
|
private static decimal? ParseNullableDecimal(string[] row, int index)
|
||||||
|
=> decimal.TryParse(Get(row, index), NumberStyles.Any, CultureInfo.InvariantCulture, out var value) ? value : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Analiza date.
|
||||||
|
/// </summary>
|
||||||
|
private static DateOnly ParseDate(string value)
|
||||||
|
=> DateOnly.TryParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)
|
||||||
|
? date
|
||||||
|
: default;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Analiza tiempo.
|
||||||
|
/// </summary>
|
||||||
|
private static TimeOnly? ParseTime(string value)
|
||||||
|
=> TimeOnly.TryParseExact(value, "HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var time)
|
||||||
|
? time
|
||||||
|
: null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee calendarios.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<FilaCalendarioCsv>> ReadCalendariosAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await ReadCsvRowsAsync(entry, ct);
|
||||||
|
if (rows.Count == 0) return new();
|
||||||
|
|
||||||
|
var h = rows[0];
|
||||||
|
var res = new List<FilaCalendarioCsv>(Math.Max(0, rows.Count - 1));
|
||||||
|
|
||||||
|
int iId = IndexOf(h, "ID");
|
||||||
|
int iLun = IndexOf(h, "LUNES");
|
||||||
|
int iMar = IndexOf(h, "MARTES");
|
||||||
|
int iMie = IndexOf(h, "MIERCOLES");
|
||||||
|
int iJue = IndexOf(h, "JUEVES");
|
||||||
|
int iVie = IndexOf(h, "VIERNES");
|
||||||
|
int iSab = IndexOf(h, "SABADO");
|
||||||
|
int iDom = IndexOf(h, "DOMINGO");
|
||||||
|
int iIni = IndexOf(h, "FECHA_INICIO");
|
||||||
|
int iFin = IndexOf(h, "FECHA_FIN");
|
||||||
|
|
||||||
|
foreach (var row in rows.Skip(1))
|
||||||
|
{
|
||||||
|
res.Add(new FilaCalendarioCsv(
|
||||||
|
Get(row, iId),
|
||||||
|
ParseInt(row, iLun),
|
||||||
|
ParseInt(row, iMar),
|
||||||
|
ParseInt(row, iMie),
|
||||||
|
ParseInt(row, iJue),
|
||||||
|
ParseInt(row, iVie),
|
||||||
|
ParseInt(row, iSab),
|
||||||
|
ParseInt(row, iDom),
|
||||||
|
ParseDate(Get(row, iIni)),
|
||||||
|
ParseDate(Get(row, iFin))));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee calendario excepciones.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<FilaCalendarioExcepcionCsv>> ReadCalendarioExcepcionesAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await ReadCsvRowsAsync(entry, ct);
|
||||||
|
if (rows.Count == 0) return new();
|
||||||
|
|
||||||
|
var h = rows[0];
|
||||||
|
var res = new List<FilaCalendarioExcepcionCsv>(Math.Max(0, rows.Count - 1));
|
||||||
|
|
||||||
|
int iId = IndexOf(h, "ID");
|
||||||
|
int iFecha = IndexOf(h, "FECHA_EX");
|
||||||
|
int iTipo = IndexOf(h, "TIPO_EXCEPCION");
|
||||||
|
|
||||||
|
foreach (var row in rows.Skip(1))
|
||||||
|
{
|
||||||
|
res.Add(new FilaCalendarioExcepcionCsv(
|
||||||
|
Get(row, iId),
|
||||||
|
ParseDate(Get(row, iFecha)),
|
||||||
|
ParseInt(row, iTipo)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee expediciones.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<FilaExpedicionCsv>> ReadExpedicionesAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await ReadCsvRowsAsync(entry, ct);
|
||||||
|
if (rows.Count == 0) return new();
|
||||||
|
|
||||||
|
var h = rows[0];
|
||||||
|
var res = new List<FilaExpedicionCsv>(Math.Max(0, rows.Count - 1));
|
||||||
|
|
||||||
|
int iId = IndexOf(h, "ID");
|
||||||
|
int iRuta = IndexOf(h, "ID_RUTA");
|
||||||
|
int iDirDestino = IndexOf(h, "DIR_DESTINO");
|
||||||
|
int iDireccion = IndexOf(h, "DIRECCION");
|
||||||
|
int iCambio = IndexOf(h, "CAMBIO_EXP");
|
||||||
|
int iPatron = IndexOf(h, "ID_PATRON_EXP");
|
||||||
|
int iSalida = IndexOf(h, "H_SALIDA");
|
||||||
|
int iCal = IndexOf(h, "ID_CAL");
|
||||||
|
|
||||||
|
foreach (var row in rows.Skip(1))
|
||||||
|
{
|
||||||
|
res.Add(new FilaExpedicionCsv(
|
||||||
|
Get(row, iId),
|
||||||
|
Get(row, iRuta),
|
||||||
|
Get(row, iDirDestino),
|
||||||
|
ParseInt(row, iDireccion),
|
||||||
|
Get(row, iCambio),
|
||||||
|
Get(row, iPatron),
|
||||||
|
ParseTime(Get(row, iSalida)),
|
||||||
|
Get(row, iCal)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee operadores.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<FilaOperadorCsv>> ReadOperadoresAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await ReadCsvRowsAsync(entry, ct);
|
||||||
|
if (rows.Count == 0) return new();
|
||||||
|
|
||||||
|
var h = rows[0];
|
||||||
|
var res = new List<FilaOperadorCsv>(Math.Max(0, rows.Count - 1));
|
||||||
|
|
||||||
|
int iId = IndexOf(h, "ID");
|
||||||
|
int iNombre = IndexOf(h, "NOMBRE");
|
||||||
|
int iTelefono = IndexOf(h, "TELEFONO");
|
||||||
|
int iUrl = IndexOf(h, "URL");
|
||||||
|
int iDescripcion = IndexOf(h, "DESCRIPCION");
|
||||||
|
|
||||||
|
foreach (var row in rows.Skip(1))
|
||||||
|
{
|
||||||
|
res.Add(new FilaOperadorCsv(
|
||||||
|
Get(row, iId),
|
||||||
|
Get(row, iNombre),
|
||||||
|
Get(row, iTelefono),
|
||||||
|
Get(row, iUrl),
|
||||||
|
Get(row, iDescripcion)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee paradas.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<FilaParadaCsv>> ReadParadasAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await ReadCsvRowsAsync(entry, ct);
|
||||||
|
if (rows.Count == 0) return new();
|
||||||
|
|
||||||
|
var h = rows[0];
|
||||||
|
var res = new List<FilaParadaCsv>(Math.Max(0, rows.Count - 1));
|
||||||
|
|
||||||
|
int iId = IndexOf(h, "ID");
|
||||||
|
int iCodigo = IndexOf(h, "CODIGO_SMS");
|
||||||
|
int iNombre = IndexOf(h, "NOMBRE_PAR");
|
||||||
|
int iLat = IndexOf(h, "LATITUD_Y_PAR");
|
||||||
|
int iLon = IndexOf(h, "LONGITUD_X_PAR");
|
||||||
|
int iLocalidad = IndexOf(h, "LOCALIDAD_PAR");
|
||||||
|
int iZona = IndexOf(h, "ZONA_PAR");
|
||||||
|
int iDescripcion = IndexOf(h, "DESCRIPCION_PAR");
|
||||||
|
int iUrl = IndexOf(h, "URL_PAR");
|
||||||
|
int iTipo = IndexOf(h, "TIPO_PAR");
|
||||||
|
int iPadre = IndexOf(h, "ESTACION_PADRE");
|
||||||
|
|
||||||
|
foreach (var row in rows.Skip(1))
|
||||||
|
{
|
||||||
|
res.Add(new FilaParadaCsv(
|
||||||
|
ParseInt(row, iId),
|
||||||
|
Get(row, iCodigo),
|
||||||
|
Get(row, iNombre),
|
||||||
|
ParseDouble(row, iLat),
|
||||||
|
ParseDouble(row, iLon),
|
||||||
|
Get(row, iLocalidad),
|
||||||
|
Get(row, iZona),
|
||||||
|
Get(row, iDescripcion),
|
||||||
|
Get(row, iUrl),
|
||||||
|
Get(row, iTipo),
|
||||||
|
Get(row, iPadre)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee patrones.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<FilaPatronCsv>> ReadPatronesAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await ReadCsvRowsAsync(entry, ct);
|
||||||
|
if (rows.Count == 0) return new();
|
||||||
|
|
||||||
|
var h = rows[0];
|
||||||
|
var res = new List<FilaPatronCsv>(Math.Max(0, rows.Count - 1));
|
||||||
|
|
||||||
|
int iId = IndexOf(h, "ID");
|
||||||
|
int iLat = IndexOf(h, "PTO_LATITUD_Y");
|
||||||
|
int iLon = IndexOf(h, "PTO_LONGITUD_X");
|
||||||
|
int iSec = IndexOf(h, "PTO_SECUENCIA");
|
||||||
|
int iDist = IndexOf(h, "DISTANCIA_RECORRIDA");
|
||||||
|
|
||||||
|
foreach (var row in rows.Skip(1))
|
||||||
|
{
|
||||||
|
res.Add(new FilaPatronCsv(
|
||||||
|
Get(row, iId),
|
||||||
|
ParseDouble(row, iLat),
|
||||||
|
ParseDouble(row, iLon),
|
||||||
|
ParseInt(row, iSec),
|
||||||
|
ParseDouble(row, iDist)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee rutas.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<FilaRutaCsv>> ReadRutasAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await ReadCsvRowsAsync(entry, ct);
|
||||||
|
if (rows.Count == 0) return new();
|
||||||
|
|
||||||
|
var h = rows[0];
|
||||||
|
var res = new List<FilaRutaCsv>(Math.Max(0, rows.Count - 1));
|
||||||
|
|
||||||
|
int iId = IndexOf(h, "ID");
|
||||||
|
int iAbreviatura = IndexOf(h, "ABREVIATURA");
|
||||||
|
int iNombre = IndexOf(h, "NOMBRE");
|
||||||
|
int iDescripcion = IndexOf(h, "DESCRIPCION");
|
||||||
|
int iTipoVehiculo = IndexOf(h, "TIPO_VEHICULO");
|
||||||
|
int iUrl = IndexOf(h, "URL");
|
||||||
|
int iColor = IndexOf(h, "COLOR");
|
||||||
|
|
||||||
|
foreach (var row in rows.Skip(1))
|
||||||
|
{
|
||||||
|
res.Add(new FilaRutaCsv(
|
||||||
|
Get(row, iId),
|
||||||
|
Get(row, iAbreviatura),
|
||||||
|
Get(row, iNombre),
|
||||||
|
Get(row, iDescripcion),
|
||||||
|
Get(row, iTipoVehiculo),
|
||||||
|
Get(row, iUrl),
|
||||||
|
Get(row, iColor)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee tarifas.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<FilaTarifaCsv>> ReadTarifasAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await ReadCsvRowsAsync(entry, ct);
|
||||||
|
if (rows.Count == 0) return new();
|
||||||
|
|
||||||
|
var h = rows[0];
|
||||||
|
var res = new List<FilaTarifaCsv>(Math.Max(0, rows.Count - 1));
|
||||||
|
|
||||||
|
int iId = IndexOf(h, "ID");
|
||||||
|
int iTarRu = IndexOf(h, "ID_TAR_RU");
|
||||||
|
int iParadaOrigen = IndexOf(h, "ID_PARADA_ORIGEN");
|
||||||
|
int iParadaDestino = IndexOf(h, "ID_PARADA_DESTINO");
|
||||||
|
int iZonaOrigen = IndexOf(h, "ID_ZONA_ORIGEN");
|
||||||
|
int iZonaDestino = IndexOf(h, "ID_ZONA_DESTINO");
|
||||||
|
int iPrecio = IndexOf(h, "PRECIO");
|
||||||
|
int iTipoPago = IndexOf(h, "TIPO_PAGO");
|
||||||
|
int iTiempoTransbordo = IndexOf(h, "TIEMPO_TRANSBORDO");
|
||||||
|
int iTransbordos = IndexOf(h, "TRANSBORDOS");
|
||||||
|
|
||||||
|
foreach (var row in rows.Skip(1))
|
||||||
|
{
|
||||||
|
res.Add(new FilaTarifaCsv(
|
||||||
|
Get(row, iId),
|
||||||
|
Get(row, iTarRu),
|
||||||
|
Get(row, iParadaOrigen),
|
||||||
|
Get(row, iParadaDestino),
|
||||||
|
Get(row, iZonaOrigen),
|
||||||
|
Get(row, iZonaDestino),
|
||||||
|
ParseNullableDecimal(row, iPrecio),
|
||||||
|
Get(row, iTipoPago),
|
||||||
|
ParseNullableInt(row, iTiempoTransbordo),
|
||||||
|
ParseNullableInt(row, iTransbordos)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee tiempos parada.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<FilaTiempoParadaCsv>> ReadTiemposParadaAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await ReadCsvRowsAsync(entry, ct);
|
||||||
|
if (rows.Count == 0) return new();
|
||||||
|
|
||||||
|
var h = rows[0];
|
||||||
|
var res = new List<FilaTiempoParadaCsv>(Math.Max(0, rows.Count - 1));
|
||||||
|
|
||||||
|
int iExp = IndexOf(h, "ID_EXP");
|
||||||
|
int iLlegada = IndexOf(h, "HORA_LLEGADA");
|
||||||
|
int iSalida = IndexOf(h, "HORA_SALIDA");
|
||||||
|
int iDistancia = IndexOf(h, "DISTANCIA_SIGUIENTE_PARADA");
|
||||||
|
int iParada = IndexOf(h, "ID_TIEMPO_PAR");
|
||||||
|
int iSec = IndexOf(h, "SEC_PAR");
|
||||||
|
int iSubida = IndexOf(h, "TIPO_SUBIDA_PAR");
|
||||||
|
int iBajada = IndexOf(h, "TIPO_BAJADA_PAR");
|
||||||
|
int iDestino = IndexOf(h, "NUEVA_DIR_DESTINO_PAR");
|
||||||
|
|
||||||
|
foreach (var row in rows.Skip(1))
|
||||||
|
{
|
||||||
|
res.Add(new FilaTiempoParadaCsv(
|
||||||
|
Get(row, iExp),
|
||||||
|
ParseTime(Get(row, iLlegada)),
|
||||||
|
ParseTime(Get(row, iSalida)),
|
||||||
|
ParseDouble(row, iDistancia),
|
||||||
|
ParseInt(row, iParada),
|
||||||
|
ParseInt(row, iSec),
|
||||||
|
Get(row, iSubida),
|
||||||
|
Get(row, iBajada),
|
||||||
|
Get(row, iDestino)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee tiempos reales.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<FilaTiempoRealCsv>> ReadTiemposRealesAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await ReadCsvRowsAsync(entry, ct);
|
||||||
|
if (rows.Count == 0) return new();
|
||||||
|
|
||||||
|
var h = rows[0];
|
||||||
|
var res = new List<FilaTiempoRealCsv>(Math.Max(0, rows.Count - 1));
|
||||||
|
|
||||||
|
int iIdRu = IndexOf(h, "ID_RU");
|
||||||
|
int iServicio = IndexOf(h, "ID_SERVICIO");
|
||||||
|
int iTrans = IndexOf(h, "ID_TRANS");
|
||||||
|
int iLat = IndexOf(h, "LATITUD_Y");
|
||||||
|
int iLon = IndexOf(h, "LONGITUD_X");
|
||||||
|
int iHoraEmision = IndexOf(h, "HORA_EMISION");
|
||||||
|
int iAntPar = IndexOf(h, "ID_ANT_PAR");
|
||||||
|
int iFechaPaso = IndexOf(h, "FECHA_HORA_PASO");
|
||||||
|
int iSalida = IndexOf(h, "H_SALIDA");
|
||||||
|
int iDiff = IndexOf(h, "MINUTOS_DE_DIFERENCIA");
|
||||||
|
int iTeorico = IndexOf(h, "TEORICO");
|
||||||
|
|
||||||
|
foreach (var row in rows.Skip(1))
|
||||||
|
{
|
||||||
|
res.Add(new FilaTiempoRealCsv(
|
||||||
|
Get(row, iIdRu),
|
||||||
|
Get(row, iServicio),
|
||||||
|
Get(row, iTrans),
|
||||||
|
ParseDouble(row, iLat),
|
||||||
|
ParseDouble(row, iLon),
|
||||||
|
Get(row, iHoraEmision),
|
||||||
|
Get(row, iAntPar),
|
||||||
|
Get(row, iFechaPaso),
|
||||||
|
ParseTime(Get(row, iSalida)),
|
||||||
|
ParseNullableInt(row, iDiff),
|
||||||
|
Get(row, iTeorico)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee transbordos.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<FilaTransbordoCsv>> ReadTransbordosAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await ReadCsvRowsAsync(entry, ct);
|
||||||
|
if (rows.Count == 0) return new();
|
||||||
|
|
||||||
|
var h = rows[0];
|
||||||
|
var res = new List<FilaTransbordoCsv>(Math.Max(0, rows.Count - 1));
|
||||||
|
|
||||||
|
int iRuta1 = IndexOf(h, "ID_RUTA_1");
|
||||||
|
int iRuta2 = IndexOf(h, "ID_RUTA_2");
|
||||||
|
|
||||||
|
foreach (var row in rows.Skip(1))
|
||||||
|
{
|
||||||
|
res.Add(new FilaTransbordoCsv(Get(row, iRuta1), Get(row, iRuta2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lee vehiculos.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<FilaVehiculoCsv>> ReadVehiculosAsync(ZipArchiveEntry? entry, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await ReadCsvRowsAsync(entry, ct);
|
||||||
|
if (rows.Count == 0) return new();
|
||||||
|
|
||||||
|
var h = rows[0];
|
||||||
|
var res = new List<FilaVehiculoCsv>(Math.Max(0, rows.Count - 1));
|
||||||
|
|
||||||
|
int iId = IndexOf(h, "ID");
|
||||||
|
int iTipo = IndexOf(h, "ID_TIPO");
|
||||||
|
int iDescripcion = IndexOf(h, "DESCRIPCION_TIPO");
|
||||||
|
int iCapacidad = IndexOf(h, "CAPACIDAD_MAXIMA");
|
||||||
|
int iAsientos = IndexOf(h, "ASIENTOS");
|
||||||
|
int iPuertas = IndexOf(h, "NUM_PUERTAS");
|
||||||
|
int iAforo = IndexOf(h, "AFORO_ACTUAL");
|
||||||
|
|
||||||
|
foreach (var row in rows.Skip(1))
|
||||||
|
{
|
||||||
|
res.Add(new FilaVehiculoCsv(
|
||||||
|
Get(row, iId),
|
||||||
|
Get(row, iTipo),
|
||||||
|
Get(row, iDescripcion),
|
||||||
|
ParseNullableInt(row, iCapacidad),
|
||||||
|
ParseNullableInt(row, iAsientos),
|
||||||
|
ParseNullableInt(row, iPuertas),
|
||||||
|
ParseNullableInt(row, iAforo)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using RutasDBUS.Modelos.Importacion;
|
||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Importacion;
|
||||||
|
|
||||||
|
public sealed class ServicioArchivoImportacionDbusMonitorizado : IServicioArchivoImportacionDbus
|
||||||
|
{
|
||||||
|
private const long MaxModeloSubidoBytes = 250L * 1024 * 1024;
|
||||||
|
|
||||||
|
private readonly IWebHostEnvironment _entorno;
|
||||||
|
private readonly IOptions<OpcionesRedBus> _opciones;
|
||||||
|
private readonly SemaphoreSlim _semaforo = new(1, 1);
|
||||||
|
|
||||||
|
private ArchivoImportacionDbus? _archivo;
|
||||||
|
private string? _rutaZipCargada;
|
||||||
|
private DateTime _fechaZipCargadaUtc;
|
||||||
|
private string? _carpetaDatos;
|
||||||
|
|
||||||
|
public ServicioArchivoImportacionDbusMonitorizado(
|
||||||
|
IWebHostEnvironment entorno,
|
||||||
|
IOptions<OpcionesRedBus> opciones)
|
||||||
|
{
|
||||||
|
_entorno = entorno;
|
||||||
|
_opciones = opciones;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ArchivoImportacionDbus> ObtenerAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var rutaZip = ResolverRutaZip();
|
||||||
|
if (rutaZip is null || !File.Exists(rutaZip))
|
||||||
|
throw new FileNotFoundException("No se ha encontrado el ZIP de importacion de dBUS.");
|
||||||
|
|
||||||
|
var infoZip = new FileInfo(rutaZip);
|
||||||
|
if (_archivo is not null &&
|
||||||
|
string.Equals(_rutaZipCargada, infoZip.FullName, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
_fechaZipCargadaUtc == infoZip.LastWriteTimeUtc)
|
||||||
|
{
|
||||||
|
return _archivo;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _semaforo.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_archivo is not null &&
|
||||||
|
string.Equals(_rutaZipCargada, infoZip.FullName, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
_fechaZipCargadaUtc == infoZip.LastWriteTimeUtc)
|
||||||
|
{
|
||||||
|
return _archivo;
|
||||||
|
}
|
||||||
|
|
||||||
|
var importador = new ServicioArchivoImportacionDbus(_entorno, _opciones);
|
||||||
|
_archivo = await importador.ObtenerDesdeRutaAsync(infoZip.FullName, cancellationToken);
|
||||||
|
_rutaZipCargada = infoZip.FullName;
|
||||||
|
_fechaZipCargadaUtc = infoZip.LastWriteTimeUtc;
|
||||||
|
return _archivo;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaforo.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<ModeloHorarioDbusInfo>> ListarModelosAsync(
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var rutaActiva = ResolverRutaZip();
|
||||||
|
var modelos = EnumerarRutasModelos()
|
||||||
|
.Select(modelo => CrearInfoModelo(modelo.Ruta, modelo.Origen, rutaActiva))
|
||||||
|
.OrderByDescending(modelo => modelo.Activo)
|
||||||
|
.ThenByDescending(modelo => modelo.FechaUtc)
|
||||||
|
.ThenBy(modelo => modelo.NombreArchivo, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Task.FromResult<IReadOnlyList<ModeloHorarioDbusInfo>>(modelos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ModeloHorarioDbusInfo?> ObtenerModeloActivoAsync(
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var modelos = await ListarModelosAsync(cancellationToken);
|
||||||
|
return modelos.FirstOrDefault(modelo => modelo.Activo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SeleccionarModeloAsync(
|
||||||
|
string idModelo,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(idModelo))
|
||||||
|
throw new ArgumentException("No se ha indicado el modelo de horarios.", nameof(idModelo));
|
||||||
|
|
||||||
|
var modelo = EnumerarRutasModelos()
|
||||||
|
.FirstOrDefault(x => string.Equals(CrearIdModelo(x.Ruta), idModelo, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(modelo.Ruta) || !File.Exists(modelo.Ruta))
|
||||||
|
throw new FileNotFoundException("No se ha encontrado el modelo de horarios seleccionado.");
|
||||||
|
|
||||||
|
await ValidarModeloAsync(modelo.Ruta, cancellationToken);
|
||||||
|
await SeleccionarRutaModeloAsync(modelo.Ruta, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ModeloHorarioDbusInfo> GuardarModeloSubidoAsync(
|
||||||
|
string nombreArchivo,
|
||||||
|
Stream contenido,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (contenido is null)
|
||||||
|
throw new ArgumentNullException(nameof(contenido));
|
||||||
|
|
||||||
|
var nombreSeguro = CrearNombreArchivoSeguro(nombreArchivo);
|
||||||
|
var carpetaDestino = ResolverCarpetaModelosSubidos(crear: true);
|
||||||
|
var rutaDestino = ResolverRutaDestinoUnica(carpetaDestino, nombreSeguro);
|
||||||
|
|
||||||
|
await CopiarStreamLimitadoAsync(contenido, rutaDestino, MaxModeloSubidoBytes, cancellationToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ValidarModeloAsync(rutaDestino, cancellationToken);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(rutaDestino))
|
||||||
|
File.Delete(rutaDestino);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SeleccionarRutaModeloAsync(rutaDestino, cancellationToken);
|
||||||
|
|
||||||
|
return CrearInfoModelo(rutaDestino, "Subido", rutaDestino);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidarModeloAsync(string rutaZip, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var importador = new ServicioArchivoImportacionDbus(_entorno, _opciones);
|
||||||
|
await importador.ObtenerDesdeRutaAsync(rutaZip, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeleccionarRutaModeloAsync(string rutaZip, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var rutaCompleta = Path.GetFullPath(rutaZip);
|
||||||
|
if (!File.Exists(rutaCompleta))
|
||||||
|
throw new FileNotFoundException("No se ha encontrado el modelo de horarios seleccionado.", rutaCompleta);
|
||||||
|
|
||||||
|
await _semaforo.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
GuardarRutaActiva(rutaCompleta);
|
||||||
|
InvalidarCacheArchivo();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaforo.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InvalidarCacheArchivo()
|
||||||
|
{
|
||||||
|
_archivo = null;
|
||||||
|
_rutaZipCargada = null;
|
||||||
|
_fechaZipCargadaUtc = default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ResolverRutaZip()
|
||||||
|
{
|
||||||
|
var rutaActiva = LeerRutaActiva();
|
||||||
|
if (!string.IsNullOrWhiteSpace(rutaActiva) && File.Exists(rutaActiva))
|
||||||
|
return Path.GetFullPath(rutaActiva);
|
||||||
|
|
||||||
|
var opciones = _opciones.Value ?? new OpcionesRedBus();
|
||||||
|
if (!string.IsNullOrWhiteSpace(opciones.ArchivoDatos))
|
||||||
|
{
|
||||||
|
var configurado = Path.Combine(
|
||||||
|
_entorno.ContentRootPath,
|
||||||
|
opciones.ArchivoDatos.Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
|
||||||
|
if (File.Exists(configurado))
|
||||||
|
return Path.GetFullPath(configurado);
|
||||||
|
}
|
||||||
|
|
||||||
|
var carpeta = Path.Combine(_entorno.ContentRootPath, "Modelos", "Horarios");
|
||||||
|
if (!Directory.Exists(carpeta))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return Directory.GetFiles(carpeta, "*.zip", SearchOption.TopDirectoryOnly)
|
||||||
|
.Select(ruta => new FileInfo(ruta))
|
||||||
|
.OrderByDescending(info => info.LastWriteTimeUtc)
|
||||||
|
.FirstOrDefault()
|
||||||
|
?.FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<(string Ruta, string Origen)> EnumerarRutasModelos()
|
||||||
|
{
|
||||||
|
var vistos = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var ruta in EnumerarZips(Path.Combine(_entorno.ContentRootPath, "Modelos", "Horarios")))
|
||||||
|
{
|
||||||
|
if (vistos.Add(ruta))
|
||||||
|
yield return (ruta, "Incluido");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var ruta in EnumerarZips(ResolverCarpetaModelosSubidos(crear: false)))
|
||||||
|
{
|
||||||
|
if (vistos.Add(ruta))
|
||||||
|
yield return (ruta, "Subido");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> EnumerarZips(string carpeta)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(carpeta) || !Directory.Exists(carpeta))
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
foreach (var ruta in Directory.GetFiles(carpeta, "*.zip", SearchOption.TopDirectoryOnly))
|
||||||
|
yield return Path.GetFullPath(ruta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModeloHorarioDbusInfo CrearInfoModelo(string ruta, string origen, string? rutaActiva)
|
||||||
|
{
|
||||||
|
var info = new FileInfo(ruta);
|
||||||
|
return new ModeloHorarioDbusInfo(
|
||||||
|
CrearIdModelo(info.FullName),
|
||||||
|
info.Name,
|
||||||
|
CrearRutaMostrable(info.FullName),
|
||||||
|
origen,
|
||||||
|
info.Length,
|
||||||
|
info.LastWriteTimeUtc,
|
||||||
|
!string.IsNullOrWhiteSpace(rutaActiva) &&
|
||||||
|
string.Equals(Path.GetFullPath(rutaActiva), info.FullName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CrearIdModelo(string ruta)
|
||||||
|
{
|
||||||
|
var rutaNormalizada = Path.GetFullPath(ruta).ToUpperInvariant();
|
||||||
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(rutaNormalizada));
|
||||||
|
return Convert.ToHexString(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CrearRutaMostrable(string ruta)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var relativa = Path.GetRelativePath(_entorno.ContentRootPath, ruta);
|
||||||
|
return relativa.StartsWith("..", StringComparison.Ordinal)
|
||||||
|
? ruta
|
||||||
|
: relativa.Replace('\\', '/');
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return ruta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolverCarpetaDatos()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(_carpetaDatos))
|
||||||
|
return _carpetaDatos;
|
||||||
|
|
||||||
|
var candidatas = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(_entorno.ContentRootPath, "App_Data"),
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "RutasDBUS"),
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "RutasDBUS"),
|
||||||
|
Path.Combine(Path.GetTempPath(), "RutasDBUS")
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var candidata in candidatas.Where(x => !string.IsNullOrWhiteSpace(x)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(candidata);
|
||||||
|
var prueba = Path.Combine(candidata, $".write-test-{Guid.NewGuid():N}.tmp");
|
||||||
|
File.WriteAllText(prueba, "ok", Encoding.UTF8);
|
||||||
|
File.Delete(prueba);
|
||||||
|
_carpetaDatos = candidata;
|
||||||
|
return _carpetaDatos;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException("No se ha encontrado una carpeta escribible para guardar modelos de horarios.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolverCarpetaModelosSubidos(bool crear)
|
||||||
|
{
|
||||||
|
var carpeta = Path.Combine(ResolverCarpetaDatos(), "ModelosHorarios");
|
||||||
|
if (crear)
|
||||||
|
Directory.CreateDirectory(carpeta);
|
||||||
|
return carpeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string RutaEstadoActivo => Path.Combine(ResolverCarpetaDatos(), "modelo-horarios-activo.json");
|
||||||
|
|
||||||
|
private string? LeerRutaActiva()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(RutaEstadoActivo))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var json = File.ReadAllText(RutaEstadoActivo, Encoding.UTF8);
|
||||||
|
var estado = JsonSerializer.Deserialize<EstadoModeloHorario>(json);
|
||||||
|
return estado?.Ruta;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GuardarRutaActiva(string ruta)
|
||||||
|
{
|
||||||
|
var estado = new EstadoModeloHorario { Ruta = Path.GetFullPath(ruta) };
|
||||||
|
var json = JsonSerializer.Serialize(estado, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
File.WriteAllText(RutaEstadoActivo, json, Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CrearNombreArchivoSeguro(string nombreArchivo)
|
||||||
|
{
|
||||||
|
var nombre = Path.GetFileName(nombreArchivo);
|
||||||
|
if (string.IsNullOrWhiteSpace(nombre))
|
||||||
|
nombre = "modelo_dbus.zip";
|
||||||
|
|
||||||
|
if (!string.Equals(Path.GetExtension(nombre), ".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidOperationException("Solo se pueden subir archivos .zip.");
|
||||||
|
|
||||||
|
var baseNombre = Path.GetFileNameWithoutExtension(nombre);
|
||||||
|
if (string.IsNullOrWhiteSpace(baseNombre))
|
||||||
|
baseNombre = "modelo_dbus";
|
||||||
|
|
||||||
|
var invalidos = Path.GetInvalidFileNameChars();
|
||||||
|
var limpio = new string(baseNombre
|
||||||
|
.Select(ch => invalidos.Contains(ch) ? '_' : ch)
|
||||||
|
.ToArray())
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(limpio))
|
||||||
|
limpio = "modelo_dbus";
|
||||||
|
|
||||||
|
return limpio + ".zip";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolverRutaDestinoUnica(string carpeta, string nombreArchivo)
|
||||||
|
{
|
||||||
|
var ruta = Path.Combine(carpeta, nombreArchivo);
|
||||||
|
if (!File.Exists(ruta))
|
||||||
|
return ruta;
|
||||||
|
|
||||||
|
var baseNombre = Path.GetFileNameWithoutExtension(nombreArchivo);
|
||||||
|
var extension = Path.GetExtension(nombreArchivo);
|
||||||
|
var marca = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
for (var i = 1; i <= 999; i++)
|
||||||
|
{
|
||||||
|
var candidato = Path.Combine(carpeta, $"{baseNombre}_{marca}_{i}{extension}");
|
||||||
|
if (!File.Exists(candidato))
|
||||||
|
return candidato;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(carpeta, $"{baseNombre}_{Guid.NewGuid():N}{extension}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task CopiarStreamLimitadoAsync(
|
||||||
|
Stream origen,
|
||||||
|
string rutaDestino,
|
||||||
|
long maxBytes,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var total = 0L;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var destino = new FileStream(
|
||||||
|
rutaDestino,
|
||||||
|
FileMode.CreateNew,
|
||||||
|
FileAccess.Write,
|
||||||
|
FileShare.None,
|
||||||
|
bufferSize: 81920,
|
||||||
|
useAsync: true);
|
||||||
|
|
||||||
|
var buffer = new byte[81920];
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var leidos = await origen.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
|
||||||
|
if (leidos == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
total += leidos;
|
||||||
|
if (total > maxBytes)
|
||||||
|
throw new InvalidOperationException("El ZIP supera el tamano maximo permitido.");
|
||||||
|
|
||||||
|
await destino.WriteAsync(buffer.AsMemory(0, leidos), cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(rutaDestino))
|
||||||
|
File.Delete(rutaDestino);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class EstadoModeloHorario
|
||||||
|
{
|
||||||
|
public string? Ruta { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
1313
RutasDBUS/Servicios/Planificacion/BuscadorRutasTemporalesDbus.cs
Normal file
1313
RutasDBUS/Servicios/Planificacion/BuscadorRutasTemporalesDbus.cs
Normal file
File diff suppressed because it is too large
Load Diff
407
RutasDBUS/Servicios/Planificacion/CreadorIndiceTransitoDbus.cs
Normal file
407
RutasDBUS/Servicios/Planificacion/CreadorIndiceTransitoDbus.cs
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
using RutasDBUS.Modelos.Importacion;
|
||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
using RutasDBUS.Servicios.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Planificacion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construye el indice temporal compartido a partir del ZIP y de la red visual.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreadorIndiceTransitoDbus
|
||||||
|
{
|
||||||
|
private const int EscalaGrid = 400;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construye el indice temporal de la red.
|
||||||
|
/// </summary>
|
||||||
|
public IndiceTransitoDbus Crear(
|
||||||
|
IRedBusServicio redBusServicio,
|
||||||
|
ArchivoImportacionDbus archivo,
|
||||||
|
int distanciaMaximaTransferenciaMetros = 250)
|
||||||
|
{
|
||||||
|
var paradasPorCodigo = redBusServicio.Paradas
|
||||||
|
.GroupBy(parada => parada.Codigo, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(grupo => grupo.Key, grupo => grupo.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var codigosPorIdParada = ConstruirCodigosPorIdParada(archivo, paradasPorCodigo);
|
||||||
|
var variantes = ConstruirVariantes(redBusServicio);
|
||||||
|
var variantesPorFirma = variantes.ToDictionary(
|
||||||
|
variante => UtilidadesTransitoDbus.FirmaVariante(
|
||||||
|
UtilidadesTransitoDbus.LineaCodigoStr(variante),
|
||||||
|
variante.CodigoItinerario,
|
||||||
|
UtilidadesTransitoDbus.NormalizarSentidoCodigo(variante.Sentido?.Codigo),
|
||||||
|
variante.CodigosParadas),
|
||||||
|
variante => variante,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var variantesPorFallback = variantes
|
||||||
|
.GroupBy(CrearClaveFallbackVariante, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(grupo => grupo.Key, grupo => grupo.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var codigoLineaPorRuta = archivo.Rutas.ToDictionary(
|
||||||
|
fila => fila.Id,
|
||||||
|
fila => !string.IsNullOrWhiteSpace(fila.Abreviatura)
|
||||||
|
? UtilidadesTransitoDbus.CodigoSinDecimal(fila.Abreviatura)
|
||||||
|
: UtilidadesTransitoDbus.CodigoSinDecimal(fila.Id),
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var patrones = new Dictionary<string, PatronTransitoDbus>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var expedicion in archivo.Expediciones)
|
||||||
|
{
|
||||||
|
if (!archivo.TiemposParadaPorExpedicion.TryGetValue(expedicion.Id, out var tiemposParada))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var tiemposOrdenados = tiemposParada.OrderBy(fila => fila.SecPar).ToList();
|
||||||
|
var codigosParada = tiemposOrdenados
|
||||||
|
.Select(fila => codigosPorIdParada.TryGetValue(fila.IdTiempoPar, out var codigo) ? codigo : "")
|
||||||
|
.Where(codigo => !string.IsNullOrWhiteSpace(codigo))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (codigosParada.Count < 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var codigoLinea = !string.IsNullOrWhiteSpace(expedicion.IdRuta) &&
|
||||||
|
codigoLineaPorRuta.TryGetValue(expedicion.IdRuta, out var codigo)
|
||||||
|
? codigo
|
||||||
|
: UtilidadesTransitoDbus.CodigoSinDecimal(expedicion.IdRuta);
|
||||||
|
var sentido = UtilidadesTransitoDbus.DireccionZipASentido(expedicion.Direccion);
|
||||||
|
var codigoItinerario = UtilidadesTransitoDbus.ExtraerCodigoItinerario(expedicion.IdRuta, expedicion.IdPatronExp);
|
||||||
|
var firmaVariante = UtilidadesTransitoDbus.FirmaVariante(codigoLinea, codigoItinerario, sentido, codigosParada);
|
||||||
|
|
||||||
|
if (!variantesPorFirma.TryGetValue(firmaVariante, out var variante) &&
|
||||||
|
!variantesPorFallback.TryGetValue(CrearClaveFallback(codigoLinea, codigoItinerario, sentido, codigosParada), out variante))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clavePatron = $"{expedicion.IdRuta}|{expedicion.IdPatronExp}|{sentido}|{codigoItinerario}|{string.Join(">", codigosParada)}";
|
||||||
|
if (!patrones.TryGetValue(clavePatron, out var patron))
|
||||||
|
{
|
||||||
|
patron = new PatronTransitoDbus
|
||||||
|
{
|
||||||
|
Id = clavePatron,
|
||||||
|
IdPatronExp = expedicion.IdPatronExp ?? "",
|
||||||
|
IdRuta = expedicion.IdRuta ?? "",
|
||||||
|
CodigoLinea = codigoLinea,
|
||||||
|
NumeroLinea = UtilidadesTransitoDbus.ExtraerNumeroLinea(expedicion.IdRuta)
|
||||||
|
?? UtilidadesTransitoDbus.ExtraerNumeroLinea(codigoLinea),
|
||||||
|
CodigoItinerario = codigoItinerario,
|
||||||
|
Sentido = sentido,
|
||||||
|
Destino = UtilidadesTransitoDbus.ResolverDestinoDescriptivo(expedicion.DirDestino, codigosParada, paradasPorCodigo),
|
||||||
|
Variante = variante,
|
||||||
|
CodigosParada = codigosParada,
|
||||||
|
DistanciaAcumuladaPorParadaMetros = ConstruirDistanciaAcumulada(variante, redBusServicio, paradasPorCodigo)
|
||||||
|
};
|
||||||
|
|
||||||
|
patrones[clavePatron] = patron;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expedicionProgramada = ConstruirExpedicion(expedicion, tiemposOrdenados, patron, codigosPorIdParada);
|
||||||
|
if (expedicionProgramada is not null)
|
||||||
|
patron.Expediciones.Add(expedicionProgramada);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var patron in patrones.Values)
|
||||||
|
{
|
||||||
|
patron.Expediciones.Sort((izquierda, derecha) =>
|
||||||
|
{
|
||||||
|
var salidaIzquierda = izquierda.SalidasSegundos.Count > 0 ? izquierda.SalidasSegundos[0] : int.MaxValue;
|
||||||
|
var salidaDerecha = derecha.SalidasSegundos.Count > 0 ? derecha.SalidasSegundos[0] : int.MaxValue;
|
||||||
|
return salidaIzquierda.CompareTo(salidaDerecha);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var patronesPorParada = new Dictionary<string, List<ReferenciaPatronParadaDbus>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var patron in patrones.Values)
|
||||||
|
{
|
||||||
|
for (var indice = 0; indice < patron.CodigosParada.Count; indice++)
|
||||||
|
{
|
||||||
|
var codigo = patron.CodigosParada[indice];
|
||||||
|
if (!patronesPorParada.TryGetValue(codigo, out var lista))
|
||||||
|
{
|
||||||
|
lista = new List<ReferenciaPatronParadaDbus>(4);
|
||||||
|
patronesPorParada[codigo] = lista;
|
||||||
|
}
|
||||||
|
|
||||||
|
lista.Add(new ReferenciaPatronParadaDbus
|
||||||
|
{
|
||||||
|
Patron = patron,
|
||||||
|
IndiceParada = indice
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var gridParadas = ConstruirGridParadas(redBusServicio.Paradas);
|
||||||
|
var transferenciasPorParada = ConstruirTransferencias(redBusServicio.Paradas, gridParadas, distanciaMaximaTransferenciaMetros);
|
||||||
|
|
||||||
|
return new IndiceTransitoDbus(
|
||||||
|
paradasPorCodigo,
|
||||||
|
patrones.Values.ToList(),
|
||||||
|
patronesPorParada,
|
||||||
|
transferenciasPorParada,
|
||||||
|
gridParadas,
|
||||||
|
EscalaGrid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construye el mapa de stop id interno a codigo de parada.
|
||||||
|
/// </summary>
|
||||||
|
private static Dictionary<int, string> ConstruirCodigosPorIdParada(
|
||||||
|
ArchivoImportacionDbus archivo,
|
||||||
|
IReadOnlyDictionary<string, Parada> paradasPorCodigo)
|
||||||
|
{
|
||||||
|
var resultado = new Dictionary<int, string>();
|
||||||
|
|
||||||
|
foreach (var fila in archivo.Paradas)
|
||||||
|
{
|
||||||
|
var codigo = !string.IsNullOrWhiteSpace(fila.CodigoSms)
|
||||||
|
? UtilidadesTransitoDbus.NormalizarCodigoParada(fila.CodigoSms)
|
||||||
|
: UtilidadesTransitoDbus.NormalizarCodigoParada(fila.Id.ToString());
|
||||||
|
|
||||||
|
if (paradasPorCodigo.ContainsKey(codigo))
|
||||||
|
resultado[fila.Id] = codigo;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultado;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construye las variantes enriquecidas de la red visual.
|
||||||
|
/// </summary>
|
||||||
|
private static List<InfoVariante> ConstruirVariantes(IRedBusServicio redBusServicio)
|
||||||
|
{
|
||||||
|
var variantes = new List<InfoVariante>(2048);
|
||||||
|
|
||||||
|
foreach (var linea in redBusServicio.Lineas)
|
||||||
|
{
|
||||||
|
foreach (var itinerario in linea.Itinerarios)
|
||||||
|
{
|
||||||
|
foreach (var sentido in itinerario.Sentidos)
|
||||||
|
{
|
||||||
|
foreach (var variante in sentido.Variantes)
|
||||||
|
{
|
||||||
|
if (variante.IdsTrayectos.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!redBusServicio.Trayectos.TryGetValue(variante.IdsTrayectos[0], out var trayectoInicial))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var info = new InfoVariante
|
||||||
|
{
|
||||||
|
Linea = linea,
|
||||||
|
Sentido = sentido,
|
||||||
|
Variante = variante,
|
||||||
|
CodigoItinerario = itinerario.Codigo
|
||||||
|
};
|
||||||
|
|
||||||
|
info.CodigosParadas.Add(trayectoInicial.CodigoParadaOrigen);
|
||||||
|
foreach (var idTrayecto in variante.IdsTrayectos)
|
||||||
|
{
|
||||||
|
if (redBusServicio.Trayectos.TryGetValue(idTrayecto, out var trayecto))
|
||||||
|
info.CodigosParadas.Add(trayecto.CodigoParadaDestino);
|
||||||
|
}
|
||||||
|
|
||||||
|
variantes.Add(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return variantes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construye una expedicion programada alineada con el patron.
|
||||||
|
/// </summary>
|
||||||
|
private static ExpedicionTransitoDbus? ConstruirExpedicion(
|
||||||
|
FilaExpedicionCsv expedicion,
|
||||||
|
IReadOnlyList<FilaTiempoParadaCsv> tiemposOrdenados,
|
||||||
|
PatronTransitoDbus patron,
|
||||||
|
IReadOnlyDictionary<int, string> codigosPorIdParada)
|
||||||
|
{
|
||||||
|
if (tiemposOrdenados.Count != patron.CodigosParada.Count)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var llegadas = new int[tiemposOrdenados.Count];
|
||||||
|
var salidas = new int[tiemposOrdenados.Count];
|
||||||
|
var ultimoSegundo = -1;
|
||||||
|
|
||||||
|
for (var indice = 0; indice < tiemposOrdenados.Count; indice++)
|
||||||
|
{
|
||||||
|
var tiempo = tiemposOrdenados[indice];
|
||||||
|
if (!codigosPorIdParada.TryGetValue(tiempo.IdTiempoPar, out var codigoParada) ||
|
||||||
|
!codigoParada.Equals(patron.CodigosParada[indice], StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var llegadaBase = (tiempo.HoraLlegada ?? tiempo.HoraSalida ?? TimeOnly.MinValue).ToTimeSpan();
|
||||||
|
var salidaBase = (tiempo.HoraSalida ?? tiempo.HoraLlegada ?? TimeOnly.MinValue).ToTimeSpan();
|
||||||
|
|
||||||
|
var llegadaSegundos = (int)llegadaBase.TotalSeconds;
|
||||||
|
var salidaSegundos = (int)salidaBase.TotalSeconds;
|
||||||
|
|
||||||
|
while (llegadaSegundos < ultimoSegundo)
|
||||||
|
llegadaSegundos += 24 * 3600;
|
||||||
|
|
||||||
|
while (salidaSegundos < llegadaSegundos)
|
||||||
|
salidaSegundos += 24 * 3600;
|
||||||
|
|
||||||
|
llegadas[indice] = llegadaSegundos;
|
||||||
|
salidas[indice] = salidaSegundos;
|
||||||
|
ultimoSegundo = salidaSegundos;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ExpedicionTransitoDbus
|
||||||
|
{
|
||||||
|
Id = expedicion.Id ?? "",
|
||||||
|
IdCalendario = expedicion.IdCal ?? "",
|
||||||
|
LlegadasSegundos = llegadas,
|
||||||
|
SalidasSegundos = salidas
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula la distancia acumulada por parada de una variante.
|
||||||
|
/// </summary>
|
||||||
|
private static IReadOnlyList<double> ConstruirDistanciaAcumulada(
|
||||||
|
InfoVariante variante,
|
||||||
|
IRedBusServicio redBusServicio,
|
||||||
|
IReadOnlyDictionary<string, Parada> paradasPorCodigo)
|
||||||
|
{
|
||||||
|
var acumulada = new double[variante.CodigosParadas.Count];
|
||||||
|
|
||||||
|
for (var indice = 0; indice < variante.CodigosParadas.Count - 1; indice++)
|
||||||
|
{
|
||||||
|
var distancia = 0.0;
|
||||||
|
|
||||||
|
if (indice < variante.Variante.IdsTrayectos.Count &&
|
||||||
|
redBusServicio.Trayectos.TryGetValue(variante.Variante.IdsTrayectos[indice], out var trayecto))
|
||||||
|
{
|
||||||
|
for (var p = 1; p < trayecto.Posiciones.Count; p++)
|
||||||
|
{
|
||||||
|
distancia += UtilidadesTransitoDbus.CalcularDistanciaMetros(
|
||||||
|
trayecto.Posiciones[p - 1][0],
|
||||||
|
trayecto.Posiciones[p - 1][1],
|
||||||
|
trayecto.Posiciones[p][0],
|
||||||
|
trayecto.Posiciones[p][1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (paradasPorCodigo.TryGetValue(variante.CodigosParadas[indice], out var paradaOrigen) &&
|
||||||
|
paradasPorCodigo.TryGetValue(variante.CodigosParadas[indice + 1], out var paradaDestino))
|
||||||
|
{
|
||||||
|
distancia = UtilidadesTransitoDbus.CalcularDistanciaMetros(
|
||||||
|
paradaOrigen.Latitud,
|
||||||
|
paradaOrigen.Longitud,
|
||||||
|
paradaDestino.Latitud,
|
||||||
|
paradaDestino.Longitud);
|
||||||
|
}
|
||||||
|
|
||||||
|
acumulada[indice + 1] = acumulada[indice] + distancia;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acumulada;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construye el grid espacial de paradas.
|
||||||
|
/// </summary>
|
||||||
|
private static Dictionary<(int cx, int cy), List<Parada>> ConstruirGridParadas(IReadOnlyList<Parada> paradas)
|
||||||
|
{
|
||||||
|
var resultado = new Dictionary<(int cx, int cy), List<Parada>>();
|
||||||
|
|
||||||
|
foreach (var parada in paradas)
|
||||||
|
{
|
||||||
|
var celda = Celda(parada.Latitud, parada.Longitud, EscalaGrid);
|
||||||
|
if (!resultado.TryGetValue(celda, out var lista))
|
||||||
|
{
|
||||||
|
lista = new List<Parada>(16);
|
||||||
|
resultado[celda] = lista;
|
||||||
|
}
|
||||||
|
|
||||||
|
lista.Add(parada);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultado;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construye las transferencias a pie entre paradas cercanas.
|
||||||
|
/// </summary>
|
||||||
|
private static Dictionary<string, List<TransferenciaPeatonalDbus>> ConstruirTransferencias(
|
||||||
|
IReadOnlyList<Parada> paradas,
|
||||||
|
IReadOnlyDictionary<(int cx, int cy), List<Parada>> gridParadas,
|
||||||
|
int distanciaMaximaTransferenciaMetros)
|
||||||
|
{
|
||||||
|
var transferencias = new Dictionary<string, List<TransferenciaPeatonalDbus>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var deg = distanciaMaximaTransferenciaMetros / 111_000.0;
|
||||||
|
var paso = Math.Max(1, (int)Math.Ceiling(deg * EscalaGrid));
|
||||||
|
|
||||||
|
foreach (var parada in paradas)
|
||||||
|
{
|
||||||
|
var celdaOrigen = Celda(parada.Latitud, parada.Longitud, EscalaGrid);
|
||||||
|
for (var dx = -paso; dx <= paso; dx++)
|
||||||
|
{
|
||||||
|
for (var dy = -paso; dy <= paso; dy++)
|
||||||
|
{
|
||||||
|
if (!gridParadas.TryGetValue((celdaOrigen.cx + dx, celdaOrigen.cy + dy), out var vecinas))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var vecina in vecinas)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(parada, vecina))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var distancia = UtilidadesTransitoDbus.CalcularDistanciaMetros(
|
||||||
|
parada.Latitud,
|
||||||
|
parada.Longitud,
|
||||||
|
vecina.Latitud,
|
||||||
|
vecina.Longitud);
|
||||||
|
|
||||||
|
if (distancia > distanciaMaximaTransferenciaMetros)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!transferencias.TryGetValue(parada.Codigo, out var lista))
|
||||||
|
{
|
||||||
|
lista = new List<TransferenciaPeatonalDbus>(8);
|
||||||
|
transferencias[parada.Codigo] = lista;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lista.Any(x => x.CodigoParadaDestino.Equals(vecina.Codigo, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
lista.Add(new TransferenciaPeatonalDbus
|
||||||
|
{
|
||||||
|
CodigoParadaDestino = vecina.Codigo,
|
||||||
|
DistanciaMetros = distancia
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transferencias;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea la clave de respaldo para una variante.
|
||||||
|
/// </summary>
|
||||||
|
private static string CrearClaveFallbackVariante(InfoVariante variante)
|
||||||
|
=> CrearClaveFallback(
|
||||||
|
UtilidadesTransitoDbus.LineaCodigoStr(variante),
|
||||||
|
variante.CodigoItinerario,
|
||||||
|
UtilidadesTransitoDbus.NormalizarSentidoCodigo(variante.Sentido?.Codigo),
|
||||||
|
variante.CodigosParadas);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea una clave de respaldo por extremos y longitud.
|
||||||
|
/// </summary>
|
||||||
|
private static string CrearClaveFallback(string codigoLinea, int codigoItinerario, string sentido, IReadOnlyList<string> codigosParada)
|
||||||
|
=> codigosParada.Count == 0
|
||||||
|
? $"{codigoLinea}|{codigoItinerario}|{sentido}|0"
|
||||||
|
: $"{codigoLinea}|{codigoItinerario}|{sentido}|{codigosParada[0]}|{codigosParada[^1]}|{codigosParada.Count}";
|
||||||
|
|
||||||
|
private static (int cx, int cy) Celda(double latitud, double longitud, int escala)
|
||||||
|
=> ((int)Math.Floor(latitud * escala), (int)Math.Floor(longitud * escala));
|
||||||
|
}
|
||||||
14
RutasDBUS/Servicios/Planificacion/IPlanificadorRutas.cs
Normal file
14
RutasDBUS/Servicios/Planificacion/IPlanificadorRutas.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using RutasDBUS.Modelos.Planificacion;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Planificacion;
|
||||||
|
|
||||||
|
public interface IPlanificadorRutas
|
||||||
|
{
|
||||||
|
void InvalidarCachesModelo();
|
||||||
|
|
||||||
|
Task<ResultadoPlanificadorRuta> CalcularAsync(
|
||||||
|
double[] origenLatLon,
|
||||||
|
double[] destinoLatLon,
|
||||||
|
ParametrosPlanificadorRuta parametros,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
}
|
||||||
200
RutasDBUS/Servicios/Planificacion/IndiceTransitoDbus.cs
Normal file
200
RutasDBUS/Servicios/Planificacion/IndiceTransitoDbus.cs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Planificacion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indice compartido de paradas, patrones y transferencias del motor de transito.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class IndiceTransitoDbus
|
||||||
|
{
|
||||||
|
private readonly Dictionary<(int cx, int cy), List<Parada>> _gridParadas;
|
||||||
|
private readonly int _escalaGrid;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia del indice.
|
||||||
|
/// </summary>
|
||||||
|
public IndiceTransitoDbus(
|
||||||
|
IReadOnlyDictionary<string, Parada> paradasPorCodigo,
|
||||||
|
IReadOnlyList<PatronTransitoDbus> patrones,
|
||||||
|
IReadOnlyDictionary<string, List<ReferenciaPatronParadaDbus>> patronesPorParada,
|
||||||
|
IReadOnlyDictionary<string, List<TransferenciaPeatonalDbus>> transferenciasPorParada,
|
||||||
|
Dictionary<(int cx, int cy), List<Parada>> gridParadas,
|
||||||
|
int escalaGrid)
|
||||||
|
{
|
||||||
|
ParadasPorCodigo = paradasPorCodigo;
|
||||||
|
Patrones = patrones;
|
||||||
|
PatronesPorParada = patronesPorParada;
|
||||||
|
TransferenciasPorParada = transferenciasPorParada;
|
||||||
|
_gridParadas = gridParadas;
|
||||||
|
_escalaGrid = escalaGrid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Paradas cargadas por codigo.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyDictionary<string, Parada> ParadasPorCodigo { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patrones transitables construidos desde expediciones reales.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<PatronTransitoDbus> Patrones { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patrones que pasan por cada parada.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyDictionary<string, List<ReferenciaPatronParadaDbus>> PatronesPorParada { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transferencias a pie entre paradas cercanas.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyDictionary<string, List<TransferenciaPeatonalDbus>> TransferenciasPorParada { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Devuelve las paradas cercanas a unas coordenadas.
|
||||||
|
/// </summary>
|
||||||
|
public List<Parada> QueryParadasEnRadio(double latitud, double longitud, int radioMetros)
|
||||||
|
{
|
||||||
|
var deg = Math.Max(0.0005, radioMetros / 111_000.0);
|
||||||
|
var paso = Math.Max(1, (int)Math.Ceiling(deg * _escalaGrid));
|
||||||
|
var celda = Celda(latitud, longitud, _escalaGrid);
|
||||||
|
var resultado = new List<Parada>(128);
|
||||||
|
|
||||||
|
for (var dx = -paso; dx <= paso; dx++)
|
||||||
|
{
|
||||||
|
for (var dy = -paso; dy <= paso; dy++)
|
||||||
|
{
|
||||||
|
if (_gridParadas.TryGetValue((celda.cx + dx, celda.cy + dy), out var paradas))
|
||||||
|
resultado.AddRange(paradas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultado;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int cx, int cy) Celda(double latitud, double longitud, int escala)
|
||||||
|
=> ((int)Math.Floor(latitud * escala), (int)Math.Floor(longitud * escala));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patron de viaje con su secuencia de paradas y sus expediciones.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PatronTransitoDbus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identificador estable del patron.
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identificador del patron de expedicion del ZIP.
|
||||||
|
/// </summary>
|
||||||
|
public string IdPatronExp { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identificador bruto de la ruta del ZIP.
|
||||||
|
/// </summary>
|
||||||
|
public string IdRuta { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Codigo visible de la linea.
|
||||||
|
/// </summary>
|
||||||
|
public string CodigoLinea { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Codigo numerico de la linea si existe.
|
||||||
|
/// </summary>
|
||||||
|
public int? NumeroLinea { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Codigo de itinerario.
|
||||||
|
/// </summary>
|
||||||
|
public int CodigoItinerario { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sentido normalizado.
|
||||||
|
/// </summary>
|
||||||
|
public string Sentido { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Destino descriptivo.
|
||||||
|
/// </summary>
|
||||||
|
public string Destino { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Variante de la red visual asociada al patron.
|
||||||
|
/// </summary>
|
||||||
|
public InfoVariante Variante { get; init; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Secuencia ordenada de codigos de parada.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> CodigosParada { get; init; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Distancia acumulada por parada para calculos aproximados.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<double> DistanciaAcumuladaPorParadaMetros { get; init; } = Array.Empty<double>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Expediciones que circulan por este patron.
|
||||||
|
/// </summary>
|
||||||
|
public List<ExpedicionTransitoDbus> Expediciones { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Expedicion concreta de un patron.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExpedicionTransitoDbus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identificador de la expedicion.
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calendario asociado.
|
||||||
|
/// </summary>
|
||||||
|
public string IdCalendario { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tiempos de llegada absolutos respecto al inicio del dia de servicio.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<int> LlegadasSegundos { get; init; } = Array.Empty<int>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tiempos de salida absolutos respecto al inicio del dia de servicio.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<int> SalidasSegundos { get; init; } = Array.Empty<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Referencia de una parada dentro de un patron.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReferenciaPatronParadaDbus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Patron al que pertenece la parada.
|
||||||
|
/// </summary>
|
||||||
|
public PatronTransitoDbus Patron { get; init; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indice de la parada dentro del patron.
|
||||||
|
/// </summary>
|
||||||
|
public int IndiceParada { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Conexion peatonal entre dos paradas.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TransferenciaPeatonalDbus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Codigo de la parada destino.
|
||||||
|
/// </summary>
|
||||||
|
public string CodigoParadaDestino { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Distancia aproximada de la transferencia.
|
||||||
|
/// </summary>
|
||||||
|
public double DistanciaMetros { get; init; }
|
||||||
|
}
|
||||||
3422
RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs
Normal file
3422
RutasDBUS/Servicios/Planificacion/PlanificadorRutasServicio.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
|||||||
|
using RutasDBUS.Servicios.Importacion;
|
||||||
|
using RutasDBUS.Servicios.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Planificacion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Proporciona un indice temporal compartido y cacheado para toda la app.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ServicioIndiceTransitoDbus
|
||||||
|
{
|
||||||
|
private readonly IRedBusServicio _redBusServicio;
|
||||||
|
private readonly IServicioArchivoImportacionDbus _servicioArchivoImportacion;
|
||||||
|
private readonly CreadorIndiceTransitoDbus _creadorIndice;
|
||||||
|
private readonly SemaphoreSlim _semaforo = new(1, 1);
|
||||||
|
private IndiceTransitoDbus? _indice;
|
||||||
|
private object? _archivoCargado;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia del servicio.
|
||||||
|
/// </summary>
|
||||||
|
public ServicioIndiceTransitoDbus(
|
||||||
|
IRedBusServicio redBusServicio,
|
||||||
|
IServicioArchivoImportacionDbus servicioArchivoImportacion,
|
||||||
|
CreadorIndiceTransitoDbus creadorIndice)
|
||||||
|
{
|
||||||
|
_redBusServicio = redBusServicio;
|
||||||
|
_servicioArchivoImportacion = servicioArchivoImportacion;
|
||||||
|
_creadorIndice = creadorIndice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Devuelve el indice temporal, construyendolo una sola vez.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IndiceTransitoDbus> ObtenerAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var archivo = await _servicioArchivoImportacion.ObtenerAsync(cancellationToken);
|
||||||
|
if (_indice is not null && ReferenceEquals(_archivoCargado, archivo))
|
||||||
|
return _indice;
|
||||||
|
|
||||||
|
await _semaforo.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_indice is not null && ReferenceEquals(_archivoCargado, archivo))
|
||||||
|
return _indice;
|
||||||
|
|
||||||
|
await _redBusServicio.CargarRedAsync(cancellationToken);
|
||||||
|
_indice = _creadorIndice.Crear(_redBusServicio, archivo);
|
||||||
|
_archivoCargado = archivo;
|
||||||
|
return _indice;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaforo.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
405
RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs
Normal file
405
RutasDBUS/Servicios/Planificacion/UtilidadesTransitoDbus.cs
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using RutasDBUS.Modelos.Planificacion;
|
||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.Planificacion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Agrupa utilidades comunes del motor de planificacion.
|
||||||
|
/// </summary>
|
||||||
|
internal static class UtilidadesTransitoDbus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Quita prefijos y decimales de un codigo numerico.
|
||||||
|
/// </summary>
|
||||||
|
public static string CodigoSinDecimal(string? codigo)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(codigo))
|
||||||
|
return "";
|
||||||
|
|
||||||
|
codigo = codigo.Trim();
|
||||||
|
if (codigo.StartsWith("P-", StringComparison.OrdinalIgnoreCase))
|
||||||
|
codigo = codigo[2..];
|
||||||
|
|
||||||
|
var index = codigo.IndexOf('.');
|
||||||
|
if (index > 0)
|
||||||
|
codigo = codigo[..index];
|
||||||
|
|
||||||
|
return codigo.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normaliza el codigo de parada al formato usado por la app.
|
||||||
|
/// </summary>
|
||||||
|
public static string NormalizarCodigoParada(string? codigo)
|
||||||
|
=> CodigoSinDecimal(codigo);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convierte la direccion del CSV a un sentido normalizado.
|
||||||
|
/// </summary>
|
||||||
|
public static string DireccionZipASentido(int direccion)
|
||||||
|
=> direccion switch
|
||||||
|
{
|
||||||
|
1 => "I",
|
||||||
|
2 => "V",
|
||||||
|
0 => "I",
|
||||||
|
_ => "?"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normaliza un codigo de sentido.
|
||||||
|
/// </summary>
|
||||||
|
public static string NormalizarSentidoCodigo(string? codigo)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(codigo))
|
||||||
|
return "?";
|
||||||
|
|
||||||
|
var texto = codigo.Trim();
|
||||||
|
if (texto.Equals("I", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
texto.Equals("Ida", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "I";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (texto.Equals("V", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
texto.Equals("Vuelta", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "V";
|
||||||
|
}
|
||||||
|
|
||||||
|
return char.ToUpperInvariant(texto[0]) switch
|
||||||
|
{
|
||||||
|
'I' => "I",
|
||||||
|
'V' => "V",
|
||||||
|
_ => "?"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extrae el codigo de itinerario a partir de la ruta y del patron.
|
||||||
|
/// </summary>
|
||||||
|
public static int ExtraerCodigoItinerario(string? idRuta, string? idPatronExp)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(idPatronExp))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var patron = idPatronExp.Trim();
|
||||||
|
var ruta = CodigoSinDecimal(idRuta);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(ruta) &&
|
||||||
|
patron.StartsWith(ruta, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
patron = patron[ruta.Length..];
|
||||||
|
}
|
||||||
|
|
||||||
|
patron = patron.Trim();
|
||||||
|
if (patron.Length >= 4)
|
||||||
|
{
|
||||||
|
var sinDir = patron[..^2].TrimStart('0');
|
||||||
|
if (int.TryParse(sinDir, NumberStyles.Integer, CultureInfo.InvariantCulture, out var directo))
|
||||||
|
return directo;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = Regex.Match(patron, @"(\d{1,3})");
|
||||||
|
return match.Success &&
|
||||||
|
int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var valor)
|
||||||
|
? valor
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extrae el codigo de variante desde el identificador del patron.
|
||||||
|
/// </summary>
|
||||||
|
public static int ExtraerCodigoVariante(string? idPatronExp)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(idPatronExp))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var texto = idPatronExp.Trim();
|
||||||
|
if (int.TryParse(texto, NumberStyles.Integer, CultureInfo.InvariantCulture, out var directo))
|
||||||
|
return directo;
|
||||||
|
|
||||||
|
var match = Regex.Match(texto, @"(\d+)$");
|
||||||
|
return match.Success &&
|
||||||
|
int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var valor)
|
||||||
|
? valor
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resuelve el instante efectivo de salida.
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ResolverInstanteSalida(ParametrosPlanificadorRuta parametros)
|
||||||
|
{
|
||||||
|
var fecha = parametros.FechaSalida ?? DateOnly.FromDateTime(DateTime.Now);
|
||||||
|
var hora = parametros.HoraSalida ?? TimeOnly.FromDateTime(DateTime.Now);
|
||||||
|
return fecha.ToDateTime(hora);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Suma segundos evitando cambios inutiles cuando la duracion es cero.
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime AddSecondsSafe(DateTime fecha, double segundos)
|
||||||
|
=> segundos <= 0 ? fecha : fecha.AddSeconds(segundos);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene el codigo textual de una linea.
|
||||||
|
/// </summary>
|
||||||
|
public static string LineaCodigoStr(InfoVariante variante)
|
||||||
|
=> variante.Linea.Codigo?.ToString() ?? "?";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indica si pasar de un tramo al siguiente exige cambiar realmente de bus.
|
||||||
|
/// </summary>
|
||||||
|
public static bool EsTransbordoReal(TramoBus? anterior, TramoBus? siguiente)
|
||||||
|
{
|
||||||
|
if (anterior?.Variante is null || siguiente?.Variante is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return EsTransbordoReal(anterior.Variante, siguiente.Variante);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool EsTransbordoReal(InfoVariante? anterior, InfoVariante? siguiente)
|
||||||
|
{
|
||||||
|
if (anterior is null || siguiente is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return !string.Equals(
|
||||||
|
LineaCodigoStr(anterior),
|
||||||
|
LineaCodigoStr(siguiente),
|
||||||
|
StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
anterior.CodigoItinerario != siguiente.CodigoItinerario;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cuenta solo los cambios que obligan a bajarse y subir a otro bus.
|
||||||
|
/// </summary>
|
||||||
|
public static int ContarTransbordosReales(IReadOnlyList<TramoBus>? tramos)
|
||||||
|
{
|
||||||
|
if (tramos is null || tramos.Count <= 1)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var total = 0;
|
||||||
|
for (var indice = 1; indice < tramos.Count; indice++)
|
||||||
|
{
|
||||||
|
if (EsTransbordoReal(tramos[indice - 1], tramos[indice]))
|
||||||
|
total++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detecta rutas que encadenan demasiadas continuidades de la misma linea sin transbordo real.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TieneContinuidadesNoRealesConsecutivasExcesivas(
|
||||||
|
IReadOnlyList<TramoBus>? tramos,
|
||||||
|
int maxContinuidadesConsecutivas)
|
||||||
|
{
|
||||||
|
if (tramos is null || tramos.Count <= 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var maximo = Math.Max(0, maxContinuidadesConsecutivas);
|
||||||
|
var consecutivas = 0;
|
||||||
|
for (var indice = 1; indice < tramos.Count; indice++)
|
||||||
|
{
|
||||||
|
if (EsTransbordoReal(tramos[indice - 1], tramos[indice]))
|
||||||
|
{
|
||||||
|
consecutivas = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
consecutivas++;
|
||||||
|
if (consecutivas > maximo)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extrae el numero de linea desde codigos numericos o alfanumericos como TB6.
|
||||||
|
/// </summary>
|
||||||
|
public static int? ExtraerNumeroLinea(string? codigoLinea)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(codigoLinea))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var texto = CodigoSinDecimal(codigoLinea);
|
||||||
|
if (int.TryParse(texto, NumberStyles.Integer, CultureInfo.InvariantCulture, out var directo))
|
||||||
|
return directo;
|
||||||
|
|
||||||
|
var match = Regex.Match(texto, @"(\d+)");
|
||||||
|
return match.Success &&
|
||||||
|
int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var valor)
|
||||||
|
? valor
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Comprueba si un destino descriptivo merece mostrarse tal cual al usuario.
|
||||||
|
/// </summary>
|
||||||
|
public static bool DestinoDescriptivoEsValido(string? destino)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(destino))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var texto = destino.Trim();
|
||||||
|
return !texto.Equals("N.A.", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!texto.Equals("N/A", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!texto.Equals("NA", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!texto.Equals("-", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!texto.Equals("?", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resuelve un destino visible usando el texto original o, si no sirve, la ultima parada conocida.
|
||||||
|
/// </summary>
|
||||||
|
public static string ResolverDestinoDescriptivo(
|
||||||
|
string? destinoOriginal,
|
||||||
|
IReadOnlyList<string> codigosParada,
|
||||||
|
IReadOnlyDictionary<string, Parada> paradasPorCodigo)
|
||||||
|
{
|
||||||
|
if (DestinoDescriptivoEsValido(destinoOriginal))
|
||||||
|
return destinoOriginal!.Trim();
|
||||||
|
|
||||||
|
if (codigosParada.Count > 0 &&
|
||||||
|
paradasPorCodigo.TryGetValue(codigosParada[^1], out var ultimaParada) &&
|
||||||
|
!string.IsNullOrWhiteSpace(ultimaParada.Nombre))
|
||||||
|
{
|
||||||
|
return ultimaParada.Nombre.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula la distancia entre dos coordenadas con Haversine.
|
||||||
|
/// </summary>
|
||||||
|
public static double CalcularDistanciaMetros(double lat1, double lon1, double lat2, double lon2)
|
||||||
|
{
|
||||||
|
const double radioTierra = 6371000.0;
|
||||||
|
static double ToRad(double valor) => valor * Math.PI / 180.0;
|
||||||
|
|
||||||
|
var dLat = ToRad(lat2 - lat1);
|
||||||
|
var dLon = ToRad(lon2 - lon1);
|
||||||
|
|
||||||
|
var a =
|
||||||
|
Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
|
||||||
|
Math.Cos(ToRad(lat1)) * Math.Cos(ToRad(lat2)) *
|
||||||
|
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
|
||||||
|
|
||||||
|
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
|
||||||
|
return radioTierra * c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estima la duracion andando.
|
||||||
|
/// </summary>
|
||||||
|
public static double EstimarDuracionPieSegundos(double distanciaMetros, ParametrosPlanificadorRuta parametros)
|
||||||
|
{
|
||||||
|
var velocidad = parametros.VelocidadPieKmH * 1000.0 / 3600.0;
|
||||||
|
return distanciaMetros / Math.Max(0.1, velocidad);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula la distancia peatonal corregida para coste interno sin reducir nunca la distancia real.
|
||||||
|
/// </summary>
|
||||||
|
public static double CalcularDistanciaPieCorregidaMetros(double distanciaMetros, ParametrosPlanificadorRuta parametros)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(distanciaMetros) || distanciaMetros <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var factor = double.IsFinite(parametros.FactorPenalizacionDistanciaPie)
|
||||||
|
? parametros.FactorPenalizacionDistanciaPie
|
||||||
|
: 1.6;
|
||||||
|
var parametroBaseMetros = double.IsFinite(parametros.ParametroPenalizacionDistanciaPieMetros)
|
||||||
|
? Math.Abs(parametros.ParametroPenalizacionDistanciaPieMetros)
|
||||||
|
: 100.0;
|
||||||
|
|
||||||
|
if (parametroBaseMetros <= 0)
|
||||||
|
parametroBaseMetros = 1.0;
|
||||||
|
|
||||||
|
var corregida = Math.Pow(distanciaMetros / parametroBaseMetros, factor) * parametroBaseMetros;
|
||||||
|
return Math.Max(distanciaMetros, corregida);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula un coste interno que penaliza de forma progresiva la caminata sin cambiar el tiempo real.
|
||||||
|
/// </summary>
|
||||||
|
public static double CalcularCosteConPenalizacionPieSegundos(
|
||||||
|
double duracionRealSegundos,
|
||||||
|
ParametrosPlanificadorRuta parametros,
|
||||||
|
params double[] distanciasPieMetros)
|
||||||
|
{
|
||||||
|
var coste = Math.Max(0, duracionRealSegundos);
|
||||||
|
|
||||||
|
foreach (var distancia in distanciasPieMetros)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(distancia) || distancia <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var distanciaCorregida = CalcularDistanciaPieCorregidaMetros(distancia, parametros);
|
||||||
|
var excesoDistancia = Math.Max(0, distanciaCorregida - distancia);
|
||||||
|
coste += EstimarDuracionPieSegundos(excesoDistancia, parametros);
|
||||||
|
}
|
||||||
|
|
||||||
|
return coste;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula la penalizacion interna por transbordos reales sin cambiar el tiempo visible.
|
||||||
|
/// </summary>
|
||||||
|
public static double CalcularPenalizacionTransbordosRankingSegundos(
|
||||||
|
int numeroTransbordosReales,
|
||||||
|
ParametrosPlanificadorRuta parametros)
|
||||||
|
{
|
||||||
|
if (numeroTransbordosReales <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var pesoMinutos = double.IsFinite(parametros.PesoTransbordoRankingMinutos)
|
||||||
|
? parametros.PesoTransbordoRankingMinutos
|
||||||
|
: 4.0;
|
||||||
|
|
||||||
|
if (pesoMinutos <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return numeroTransbordosReales * pesoMinutos * 60.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula el coste interno aplicando preferencias de caminata y transbordos reales.
|
||||||
|
/// </summary>
|
||||||
|
public static double CalcularCosteConPenalizacionesRankingSegundos(
|
||||||
|
double duracionRealSegundos,
|
||||||
|
ParametrosPlanificadorRuta parametros,
|
||||||
|
int numeroTransbordosReales,
|
||||||
|
params double[] distanciasPieMetros)
|
||||||
|
{
|
||||||
|
return CalcularCosteConPenalizacionPieSegundos(duracionRealSegundos, parametros, distanciasPieMetros)
|
||||||
|
+ CalcularPenalizacionTransbordosRankingSegundos(numeroTransbordosReales, parametros);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estima la duracion en bus.
|
||||||
|
/// </summary>
|
||||||
|
public static double EstimarDuracionBusSegundos(double distanciaMetros, ParametrosPlanificadorRuta parametros)
|
||||||
|
{
|
||||||
|
var velocidad = parametros.VelocidadBusKmH * 1000.0 / 3600.0;
|
||||||
|
return distanciaMetros / Math.Max(0.1, velocidad);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea una firma estable de una variante.
|
||||||
|
/// </summary>
|
||||||
|
public static string FirmaVariante(string codigoLinea, int codigoItinerario, string sentido, IReadOnlyList<string> codigosParada)
|
||||||
|
=> $"{codigoLinea}|{codigoItinerario}|{sentido}|{string.Join(">", codigosParada)}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea una firma estable de una ruta de bus.
|
||||||
|
/// </summary>
|
||||||
|
public static string FirmaRutaBus(RutaBusAlternativa alternativa)
|
||||||
|
=> string.Join(";", alternativa.Tramos.Select(tramo =>
|
||||||
|
$"{LineaCodigoStr(tramo.Variante)}-{tramo.Variante.CodigoItinerario}-{NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo)}-{tramo.Variante.Variante.Codigo}-{tramo.IndiceInicio}-{tramo.IndiceFin}"));
|
||||||
|
}
|
||||||
582
RutasDBUS/Servicios/RedBus/CreadorRedTransporteDbus.cs
Normal file
582
RutasDBUS/Servicios/RedBus/CreadorRedTransporteDbus.cs
Normal file
@@ -0,0 +1,582 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using RutasDBUS.Modelos.Importacion;
|
||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.RedBus;
|
||||||
|
|
||||||
|
public sealed class CreadorRedTransporteDbus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public RedTransporteDbus Build(ArchivoImportacionDbus archive)
|
||||||
|
{
|
||||||
|
var paradas = new List<Parada>(archive.Paradas.Count);
|
||||||
|
var trayectos = new Dictionary<int, Trayecto>();
|
||||||
|
var lineas = new List<LineaBus>(archive.Rutas.Count);
|
||||||
|
|
||||||
|
var paradaById = new Dictionary<int, Parada>();
|
||||||
|
|
||||||
|
foreach (var row in archive.Paradas)
|
||||||
|
{
|
||||||
|
var (lat, lon) = ConvertirCoordenadaZipALatLon(row.LongitudXPar, row.LatitudYPar);
|
||||||
|
if (!EsLatLonValida(lat, lon))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var codigo = !string.IsNullOrWhiteSpace(row.CodigoSms)
|
||||||
|
? NormalizarCodigoParada(row.CodigoSms)
|
||||||
|
: NormalizarCodigoParada(row.Id.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
|
var parada = new Parada
|
||||||
|
{
|
||||||
|
Codigo = codigo,
|
||||||
|
Nombre = row.NombrePar ?? "",
|
||||||
|
NombreCorto = !string.IsNullOrWhiteSpace(row.DescripcionPar) ? row.DescripcionPar : (row.NombrePar ?? ""),
|
||||||
|
Abreviatura = row.DescripcionPar ?? "",
|
||||||
|
PreZona = "",
|
||||||
|
PostZona = "",
|
||||||
|
ZonaSalida = "",
|
||||||
|
Altura = 0,
|
||||||
|
Latitud = lat,
|
||||||
|
Longitud = lon
|
||||||
|
};
|
||||||
|
|
||||||
|
paradas.Add(parada);
|
||||||
|
paradaById[row.Id] = parada;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineaByRutaId = new Dictionary<string, LineaBus>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var row in archive.Rutas)
|
||||||
|
{
|
||||||
|
var linea = new LineaBus
|
||||||
|
{
|
||||||
|
IdRuta = row.Id ?? "",
|
||||||
|
Codigo = !string.IsNullOrWhiteSpace(row.Abreviatura)
|
||||||
|
? CodigoSinDecimal(row.Abreviatura)
|
||||||
|
: CodigoSinDecimal(row.Id),
|
||||||
|
Nombre = !string.IsNullOrWhiteSpace(row.Nombre) ? row.Nombre : (row.Descripcion ?? ""),
|
||||||
|
ColorArgb = NormalizarColorArgb(row.Color),
|
||||||
|
CodigoItinerarioPorDefecto = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
lineas.Add(linea);
|
||||||
|
lineaByRutaId[row.Id ?? ""] = linea;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stopSequenceByPattern = archive.Expediciones
|
||||||
|
.Where(exp => !string.IsNullOrWhiteSpace(exp.IdPatronExp))
|
||||||
|
.Select(exp => new
|
||||||
|
{
|
||||||
|
exp.IdPatronExp,
|
||||||
|
Sequence = archive.TiemposParadaPorExpedicion.TryGetValue(exp.Id, out var seq)
|
||||||
|
? seq.Select(x => x.IdTiempoPar).ToList()
|
||||||
|
: new List<int>()
|
||||||
|
})
|
||||||
|
.Where(x => x.Sequence.Count >= 2)
|
||||||
|
.GroupBy(x => x.IdPatronExp, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.First().Sequence,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var variantesTemp = new Dictionary<string, VarianteTemporal>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var exp in archive.Expediciones)
|
||||||
|
{
|
||||||
|
if (!lineaByRutaId.TryGetValue(exp.IdRuta, out var linea))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!stopSequenceByPattern.TryGetValue(exp.IdPatronExp, out var stopIds) || stopIds.Count < 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var sentido = DireccionZipASentido(exp.Direccion);
|
||||||
|
var codigoItinerario = ExtraerCodigoItinerario(exp.IdRuta, exp.IdPatronExp);
|
||||||
|
var codigoVariante = ExtraerCodigoVariante(exp.IdPatronExp);
|
||||||
|
var key = $"{exp.IdRuta}|{codigoItinerario}|{sentido}|{exp.IdPatronExp}";
|
||||||
|
|
||||||
|
if (!variantesTemp.ContainsKey(key))
|
||||||
|
{
|
||||||
|
variantesTemp[key] = new VarianteTemporal
|
||||||
|
{
|
||||||
|
RutaId = exp.IdRuta,
|
||||||
|
CodigoItinerario = codigoItinerario,
|
||||||
|
Sentido = sentido,
|
||||||
|
CodigoVariante = codigoVariante,
|
||||||
|
IdPatron = exp.IdPatronExp,
|
||||||
|
Destino = exp.DirDestino ?? "",
|
||||||
|
StopIds = stopIds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextTrayectoId = 1;
|
||||||
|
foreach (var temp in variantesTemp.Values)
|
||||||
|
{
|
||||||
|
if (!lineaByRutaId.TryGetValue(temp.RutaId, out var linea))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var itinerario = linea.Itinerarios.FirstOrDefault(x => x.Codigo == temp.CodigoItinerario);
|
||||||
|
if (itinerario is null)
|
||||||
|
{
|
||||||
|
itinerario = new Itinerario { Codigo = temp.CodigoItinerario };
|
||||||
|
linea.Itinerarios.Add(itinerario);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sentido = itinerario.Sentidos.FirstOrDefault(x =>
|
||||||
|
string.Equals(NormalizarSentidoCodigo(x.Codigo), temp.Sentido, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (sentido is null)
|
||||||
|
{
|
||||||
|
sentido = new Sentido { Codigo = temp.Sentido };
|
||||||
|
itinerario.Sentidos.Add(sentido);
|
||||||
|
}
|
||||||
|
|
||||||
|
var variante = new Variante { Codigo = temp.CodigoVariante };
|
||||||
|
var stopIdsValidos = temp.StopIds.Where(paradaById.ContainsKey).ToList();
|
||||||
|
var codigosParadas = stopIdsValidos
|
||||||
|
.Select(id => paradaById[id].Codigo)
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (codigosParadas.Count < 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var patronLatLon = archive.PatronesPorId.TryGetValue(temp.IdPatron, out var puntosPatron)
|
||||||
|
? puntosPatron
|
||||||
|
.OrderBy(x => x.PtoSecuencia)
|
||||||
|
.Select(p =>
|
||||||
|
{
|
||||||
|
var (lat, lon) = ConvertirCoordenadaZipALatLon(p.PtoLongitudX, p.PtoLatitudY);
|
||||||
|
return new[] { lat, lon };
|
||||||
|
})
|
||||||
|
.Where(p => EsLatLonValida(p[0], p[1]))
|
||||||
|
.ToList()
|
||||||
|
: new List<double[]>();
|
||||||
|
|
||||||
|
var stopLatLon = stopIdsValidos
|
||||||
|
.Select(id => new[] { paradaById[id].Latitud, paradaById[id].Longitud })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var segmentos = ConstruirSegmentosDelPatron(stopLatLon, patronLatLon);
|
||||||
|
|
||||||
|
for (var i = 0; i < codigosParadas.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var codigoOrigen = codigosParadas[i];
|
||||||
|
var codigoDestino = codigosParadas[i + 1];
|
||||||
|
|
||||||
|
var puntos = (i < segmentos.Count && segmentos[i].Count >= 2)
|
||||||
|
? segmentos[i]
|
||||||
|
: new List<double[]>
|
||||||
|
{
|
||||||
|
new[] { stopLatLon[i][0], stopLatLon[i][1] },
|
||||||
|
new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] }
|
||||||
|
};
|
||||||
|
|
||||||
|
trayectos[nextTrayectoId] = new Trayecto
|
||||||
|
{
|
||||||
|
Id = nextTrayectoId,
|
||||||
|
CodigoParadaOrigen = codigoOrigen,
|
||||||
|
CodigoParadaDestino = codigoDestino,
|
||||||
|
Posiciones = puntos
|
||||||
|
};
|
||||||
|
|
||||||
|
variante.IdsTrayectos.Add(nextTrayectoId);
|
||||||
|
nextTrayectoId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variante.IdsTrayectos.Count > 0)
|
||||||
|
sentido.Variantes.Add(variante);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var linea in lineas)
|
||||||
|
{
|
||||||
|
linea.Itinerarios = linea.Itinerarios
|
||||||
|
.Where(it => it.Sentidos.Any(s => s.Variantes.Any()))
|
||||||
|
.OrderBy(it => it.Codigo)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var itinerario in linea.Itinerarios)
|
||||||
|
{
|
||||||
|
itinerario.Sentidos = itinerario.Sentidos
|
||||||
|
.Where(s => s.Variantes.Any())
|
||||||
|
.OrderBy(s => NormalizarSentidoCodigo(s.Codigo))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lineas.RemoveAll(x => x.Itinerarios.Count == 0);
|
||||||
|
|
||||||
|
return new RedTransporteDbus
|
||||||
|
{
|
||||||
|
Paradas = paradas,
|
||||||
|
Trayectos = trayectos,
|
||||||
|
Lineas = lineas
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class VarianteTemporal
|
||||||
|
{
|
||||||
|
public string RutaId { get; set; } = "";
|
||||||
|
public int CodigoItinerario { get; set; }
|
||||||
|
public string Sentido { get; set; } = "";
|
||||||
|
public int CodigoVariante { get; set; }
|
||||||
|
public string IdPatron { get; set; } = "";
|
||||||
|
public string Destino { get; set; } = "";
|
||||||
|
public List<int> StopIds { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona segmentos del patron.
|
||||||
|
/// </summary>
|
||||||
|
private static List<List<double[]>> ConstruirSegmentosDelPatron(
|
||||||
|
List<double[]> stopLatLon,
|
||||||
|
List<double[]> patronLatLon)
|
||||||
|
{
|
||||||
|
var res = new List<List<double[]>>();
|
||||||
|
|
||||||
|
if (stopLatLon.Count < 2)
|
||||||
|
return res;
|
||||||
|
|
||||||
|
if (patronLatLon.Count < 2)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < stopLatLon.Count - 1; i++)
|
||||||
|
{
|
||||||
|
res.Add(new List<double[]>
|
||||||
|
{
|
||||||
|
new[] { stopLatLon[i][0], stopLatLon[i][1] },
|
||||||
|
new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
var indices = CalcularIndicesParadasEnPatron(stopLatLon, patronLatLon);
|
||||||
|
|
||||||
|
for (var i = 0; i < indices.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var a = indices[i];
|
||||||
|
var b = Math.Max(a, indices[i + 1]);
|
||||||
|
var segmento = new List<double[]>();
|
||||||
|
|
||||||
|
for (var j = a; j <= b && j < patronLatLon.Count; j++)
|
||||||
|
segmento.Add(new[] { patronLatLon[j][0], patronLatLon[j][1] });
|
||||||
|
|
||||||
|
if (segmento.Count == 0)
|
||||||
|
{
|
||||||
|
segmento.Add(new[] { stopLatLon[i][0], stopLatLon[i][1] });
|
||||||
|
segmento.Add(new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] });
|
||||||
|
}
|
||||||
|
else if (segmento.Count == 1)
|
||||||
|
{
|
||||||
|
segmento.Insert(0, new[] { stopLatLon[i][0], stopLatLon[i][1] });
|
||||||
|
segmento.Add(new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
segmento[0] = new[] { stopLatLon[i][0], stopLatLon[i][1] };
|
||||||
|
segmento[^1] = new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] };
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Add(QuitarDuplicadosConsecutivos(segmento));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula para cada parada el indice del punto del patron que mejor la representa,
|
||||||
|
/// forzando una progresion monotona para no perder detalle entre paradas consecutivas.
|
||||||
|
/// </summary>
|
||||||
|
private static List<int> CalcularIndicesParadasEnPatron(
|
||||||
|
List<double[]> stopLatLon,
|
||||||
|
List<double[]> patronLatLon)
|
||||||
|
{
|
||||||
|
var indices = new List<int>(stopLatLon.Count);
|
||||||
|
var ultimoIndicePatron = patronLatLon.Count - 1;
|
||||||
|
|
||||||
|
for (var i = 0; i < stopLatLon.Count; i++)
|
||||||
|
{
|
||||||
|
var minIdx = indices.Count == 0
|
||||||
|
? 0
|
||||||
|
: Math.Min(ultimoIndicePatron, indices[^1] + 1);
|
||||||
|
|
||||||
|
var paradasRestantes = stopLatLon.Count - i - 1;
|
||||||
|
var maxIdx = Math.Max(minIdx, ultimoIndicePatron - paradasRestantes);
|
||||||
|
|
||||||
|
indices.Add(BuscarIndicePatronMasCercano(stopLatLon[i], patronLatLon, minIdx, maxIdx));
|
||||||
|
}
|
||||||
|
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca el punto del patron mas cercano a una parada dentro de un rango valido.
|
||||||
|
/// </summary>
|
||||||
|
private static int BuscarIndicePatronMasCercano(
|
||||||
|
double[] paradaLatLon,
|
||||||
|
List<double[]> patronLatLon,
|
||||||
|
int minIdx,
|
||||||
|
int maxIdx)
|
||||||
|
{
|
||||||
|
var inicio = Math.Clamp(minIdx, 0, patronLatLon.Count - 1);
|
||||||
|
var fin = Math.Clamp(maxIdx, inicio, patronLatLon.Count - 1);
|
||||||
|
|
||||||
|
var mejorIdx = inicio;
|
||||||
|
var mejorDist = double.MaxValue;
|
||||||
|
|
||||||
|
for (var j = inicio; j <= fin; j++)
|
||||||
|
{
|
||||||
|
var dist = Dist2(paradaLatLon[0], paradaLatLon[1], patronLatLon[j][0], patronLatLon[j][1]);
|
||||||
|
if (dist < mejorDist)
|
||||||
|
{
|
||||||
|
mejorDist = dist;
|
||||||
|
mejorIdx = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mejorIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quita duplicados consecutivos.
|
||||||
|
/// </summary>
|
||||||
|
private static List<double[]> QuitarDuplicadosConsecutivos(List<double[]> puntos)
|
||||||
|
{
|
||||||
|
var res = new List<double[]>(puntos.Count);
|
||||||
|
foreach (var punto in puntos)
|
||||||
|
{
|
||||||
|
if (res.Count == 0)
|
||||||
|
{
|
||||||
|
res.Add(punto);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ultimo = res[^1];
|
||||||
|
if (Math.Abs(ultimo[0] - punto[0]) < 1e-9 && Math.Abs(ultimo[1] - punto[1]) < 1e-9)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
res.Add(punto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
private static double Dist2(double lat1, double lon1, double lat2, double lon2)
|
||||||
|
{
|
||||||
|
var dLat = lat1 - lat2;
|
||||||
|
var dLon = (lon1 - lon2) * Math.Cos((lat1 + lat2) / 2 * Math.PI / 180.0);
|
||||||
|
return dLat * dLat + dLon * dLon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona lat lon valida.
|
||||||
|
/// </summary>
|
||||||
|
private static bool EsLatLonValida(double lat, double lon)
|
||||||
|
=> lat >= 42.0 && lat <= 44.5 && lon >= -3.5 && lon <= 0.5;
|
||||||
|
|
||||||
|
private const double OffsetLat = -0.0018141507864726236;
|
||||||
|
private const double OffsetLon = -0.0013070642408454791;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
private static (double lat, double lon) ConvertirCoordenadaZipALatLon(double x, double y)
|
||||||
|
{
|
||||||
|
if (x == 0 || y == 0)
|
||||||
|
return (0, 0);
|
||||||
|
|
||||||
|
var (lat, lon) = Utm30ToLatLonWgs84(x, y);
|
||||||
|
return (lat + OffsetLat, lon + OffsetLon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
private static (double lat, double lon) Utm30ToLatLonWgs84(double easting, double northing)
|
||||||
|
{
|
||||||
|
const int zone = 30;
|
||||||
|
const double a = 6378137.0;
|
||||||
|
const double eccSquared = 0.0066943799901413165;
|
||||||
|
const double k0 = 0.9996;
|
||||||
|
|
||||||
|
var x = easting - 500000.0;
|
||||||
|
var y = northing;
|
||||||
|
var longOrigin = (zone - 1) * 6 - 180 + 3;
|
||||||
|
var eccPrimeSquared = eccSquared / (1 - eccSquared);
|
||||||
|
var m = y / k0;
|
||||||
|
var mu = m / (a * (1 - eccSquared / 4.0 - 3.0 * eccSquared * eccSquared / 64.0 - 5.0 * Math.Pow(eccSquared, 3) / 256.0));
|
||||||
|
var e1 = (1.0 - Math.Sqrt(1.0 - eccSquared)) / (1.0 + Math.Sqrt(1.0 - eccSquared));
|
||||||
|
|
||||||
|
var phi1Rad =
|
||||||
|
mu
|
||||||
|
+ (3.0 * e1 / 2.0 - 27.0 * Math.Pow(e1, 3) / 32.0) * Math.Sin(2.0 * mu)
|
||||||
|
+ (21.0 * e1 * e1 / 16.0 - 55.0 * Math.Pow(e1, 4) / 32.0) * Math.Sin(4.0 * mu)
|
||||||
|
+ (151.0 * Math.Pow(e1, 3) / 96.0) * Math.Sin(6.0 * mu)
|
||||||
|
+ (1097.0 * Math.Pow(e1, 4) / 512.0) * Math.Sin(8.0 * mu);
|
||||||
|
|
||||||
|
var n1 = a / Math.Sqrt(1.0 - eccSquared * Math.Sin(phi1Rad) * Math.Sin(phi1Rad));
|
||||||
|
var t1 = Math.Tan(phi1Rad) * Math.Tan(phi1Rad);
|
||||||
|
var c1 = eccPrimeSquared * Math.Cos(phi1Rad) * Math.Cos(phi1Rad);
|
||||||
|
var r1 = a * (1.0 - eccSquared) / Math.Pow(1.0 - eccSquared * Math.Sin(phi1Rad) * Math.Sin(phi1Rad), 1.5);
|
||||||
|
var d = x / (n1 * k0);
|
||||||
|
|
||||||
|
var latRad =
|
||||||
|
phi1Rad
|
||||||
|
- (n1 * Math.Tan(phi1Rad) / r1) *
|
||||||
|
(
|
||||||
|
d * d / 2.0
|
||||||
|
- (5.0 + 3.0 * t1 + 10.0 * c1 - 4.0 * c1 * c1 - 9.0 * eccPrimeSquared) * Math.Pow(d, 4) / 24.0
|
||||||
|
+ (61.0 + 90.0 * t1 + 298.0 * c1 + 45.0 * t1 * t1 - 252.0 * eccPrimeSquared - 3.0 * c1 * c1) * Math.Pow(d, 6) / 720.0
|
||||||
|
);
|
||||||
|
|
||||||
|
var lonRad =
|
||||||
|
(
|
||||||
|
d
|
||||||
|
- (1.0 + 2.0 * t1 + c1) * Math.Pow(d, 3) / 6.0
|
||||||
|
+ (5.0 - 2.0 * c1 + 28.0 * t1 - 3.0 * c1 * c1 + 8.0 * eccPrimeSquared + 24.0 * t1 * t1) * Math.Pow(d, 5) / 120.0
|
||||||
|
) / Math.Cos(phi1Rad);
|
||||||
|
|
||||||
|
return (latRad * 180.0 / Math.PI, longOrigin + lonRad * 180.0 / Math.PI);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona sin decimal.
|
||||||
|
/// </summary>
|
||||||
|
private static string CodigoSinDecimal(string? codigo)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(codigo)) return "";
|
||||||
|
codigo = codigo.Trim();
|
||||||
|
|
||||||
|
if (codigo.StartsWith("P-", StringComparison.OrdinalIgnoreCase))
|
||||||
|
codigo = codigo[2..];
|
||||||
|
|
||||||
|
var i = codigo.IndexOf('.');
|
||||||
|
if (i > 0)
|
||||||
|
codigo = codigo[..i];
|
||||||
|
|
||||||
|
return codigo.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normaliza codigo parada.
|
||||||
|
/// </summary>
|
||||||
|
private static string NormalizarCodigoParada(string? codigo)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(codigo)) return "";
|
||||||
|
|
||||||
|
codigo = codigo.Trim();
|
||||||
|
codigo = codigo.Replace("P-", "", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var i = codigo.IndexOf('.');
|
||||||
|
if (i > 0)
|
||||||
|
codigo = codigo[..i];
|
||||||
|
|
||||||
|
return codigo.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normaliza color argb.
|
||||||
|
/// </summary>
|
||||||
|
private static string NormalizarColorArgb(string? color)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(color))
|
||||||
|
return "FF2563EB";
|
||||||
|
|
||||||
|
color = color.Trim().TrimStart('#');
|
||||||
|
|
||||||
|
if (color.Length == 6)
|
||||||
|
return "FF" + color.ToUpperInvariant();
|
||||||
|
|
||||||
|
if (color.Length == 8)
|
||||||
|
return color.ToUpperInvariant();
|
||||||
|
|
||||||
|
return "FF2563EB";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona zip a sentido.
|
||||||
|
/// </summary>
|
||||||
|
private static string DireccionZipASentido(int direccion) =>
|
||||||
|
direccion switch
|
||||||
|
{
|
||||||
|
1 => "I",
|
||||||
|
2 => "V",
|
||||||
|
0 => "I",
|
||||||
|
_ => "?"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normaliza sentido codigo.
|
||||||
|
/// </summary>
|
||||||
|
private static string NormalizarSentidoCodigo(string? codigo)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(codigo)) return "?";
|
||||||
|
|
||||||
|
var s = codigo.Trim();
|
||||||
|
if (s.Equals("V", StringComparison.OrdinalIgnoreCase)) return "V";
|
||||||
|
if (s.Equals("I", StringComparison.OrdinalIgnoreCase)) return "I";
|
||||||
|
if (s.Equals("Vuelta", StringComparison.OrdinalIgnoreCase)) return "V";
|
||||||
|
if (s.Equals("Ida", StringComparison.OrdinalIgnoreCase)) return "I";
|
||||||
|
|
||||||
|
var c = char.ToUpperInvariant(s[0]);
|
||||||
|
return c switch
|
||||||
|
{
|
||||||
|
'V' => "V",
|
||||||
|
'I' => "I",
|
||||||
|
_ => "?"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona codigo variante.
|
||||||
|
/// </summary>
|
||||||
|
private static int ExtraerCodigoVariante(string? idPatronExp)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(idPatronExp))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var s = idPatronExp.Trim();
|
||||||
|
if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n))
|
||||||
|
return n;
|
||||||
|
|
||||||
|
var match = Regex.Match(s, @"(\d+)$");
|
||||||
|
return match.Success && int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out n)
|
||||||
|
? n
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestiona codigo itinerario.
|
||||||
|
/// </summary>
|
||||||
|
private static int ExtraerCodigoItinerario(string? idRuta, string? idPatronExp)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(idPatronExp))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var patron = idPatronExp.Trim();
|
||||||
|
var ruta = CodigoSinDecimal(idRuta);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(ruta) &&
|
||||||
|
patron.StartsWith(ruta, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
patron = patron[ruta.Length..];
|
||||||
|
}
|
||||||
|
|
||||||
|
patron = patron.Trim();
|
||||||
|
if (patron.Length >= 4)
|
||||||
|
{
|
||||||
|
var sinDir = patron[..^2].TrimStart('0');
|
||||||
|
if (int.TryParse(sinDir, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = Regex.Match(patron, @"(\d{1,3})");
|
||||||
|
return match.Success && int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var fallback)
|
||||||
|
? fallback
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
69
RutasDBUS/Servicios/RedBus/IRedBusServicio.cs
Normal file
69
RutasDBUS/Servicios/RedBus/IRedBusServicio.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.RedBus;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Define las operaciones sobre la red de autobuses.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRedBusServicio
|
||||||
|
{
|
||||||
|
/// <summary>Paradas cargadas en la red.</summary>
|
||||||
|
IReadOnlyList<Parada> Paradas { get; }
|
||||||
|
|
||||||
|
/// <summary>Trayectos cargados en la red.</summary>
|
||||||
|
IReadOnlyDictionary<int, Trayecto> Trayectos { get; }
|
||||||
|
|
||||||
|
/// <summary>Líneas de bus cargadas en la red.</summary>
|
||||||
|
IReadOnlyList<LineaBus> Lineas { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Carga la red (paradas, trayectos, líneas) desde el fichero XML.
|
||||||
|
/// </summary>
|
||||||
|
Task CargarRedAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula la distancia aproximada en metros entre dos coordenadas.
|
||||||
|
/// </summary>
|
||||||
|
double CalcularDistanciaMetros(double lat1, double lon1, double lat2, double lon2);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Devuelve la parada más cercana a las coordenadas indicadas.
|
||||||
|
/// </summary>
|
||||||
|
Parada? BuscarParadaMasCercana(double latitud, double longitud);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Devuelve la parada más cercana dentro de un radio máximo. Si no hay ninguna dentro de ese radio, devuelve null.
|
||||||
|
/// </summary>
|
||||||
|
Parada? BuscarParadaCercanaEnRango(double latitud, double longitud, double maxDistMetros);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convierte un color ARGB (8 caracteres hex) al formato CSS "#RRGGBB".
|
||||||
|
/// </summary>
|
||||||
|
string ConvertirColorArgbACss(string argb);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca una ruta de bus (una sola alternativa) entre dos paradas, devolviendo tramos y paradas de Transbordo.
|
||||||
|
/// </summary>
|
||||||
|
List<TramoBus>? BuscarRutaBus(
|
||||||
|
string codigoParadaOrigen,
|
||||||
|
string codigoParadaDestino,
|
||||||
|
out List<string> codigosParadasTransbordo);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construye las polilíneas de los tramos de bus y calcula la distancia total recorrida.
|
||||||
|
/// </summary>
|
||||||
|
List<double[,]> ConstruirLineasBus(
|
||||||
|
IReadOnlyList<TramoBus> tramos,
|
||||||
|
out double distanciaTotalBus);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca varias rutas alternativas de bus entre dos paradas.
|
||||||
|
/// De momento, la implementación devuelve al menos la ruta "clásica" encontrada por <see cref="BuscarRutaBus"/>.
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyList<RutaBusAlternativa> BuscarRutasBusAlternativas(
|
||||||
|
string codigoParadaOrigen,
|
||||||
|
string codigoParadaDestino,
|
||||||
|
int maxAlternativas = 3);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
1805
RutasDBUS/Servicios/RedBus/RedBusServicio.cs
Normal file
1805
RutasDBUS/Servicios/RedBus/RedBusServicio.cs
Normal file
File diff suppressed because it is too large
Load Diff
10
RutasDBUS/Servicios/RedBus/RedTransporteDbus.cs
Normal file
10
RutasDBUS/Servicios/RedBus/RedTransporteDbus.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using RutasDBUS.Modelos.RedBus;
|
||||||
|
|
||||||
|
namespace RutasDBUS.Servicios.RedBus;
|
||||||
|
|
||||||
|
public sealed class RedTransporteDbus
|
||||||
|
{
|
||||||
|
public IReadOnlyList<Parada> Paradas { get; init; } = Array.Empty<Parada>();
|
||||||
|
public IReadOnlyDictionary<int, Trayecto> Trayectos { get; init; } = new Dictionary<int, Trayecto>();
|
||||||
|
public IReadOnlyList<LineaBus> Lineas { get; init; } = Array.Empty<LineaBus>();
|
||||||
|
}
|
||||||
8
RutasDBUS/appsettings.Development.json
Normal file
8
RutasDBUS/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
RutasDBUS/appsettings.json
Normal file
58
RutasDBUS/appsettings.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"RedBus": {
|
||||||
|
"ArchivoDatos": "Modelos/horarios/zip_DBUS_OPENDATA_20260514~0800.zip",
|
||||||
|
"DistanciaMaximaTransbordoMetros": 250
|
||||||
|
},
|
||||||
|
|
||||||
|
"Mapa": {
|
||||||
|
"UrlTeselas": "https://osm.tecnosis.net/osm_tiles/{z}/{x}/{y}.png",
|
||||||
|
"Atribucion": "© OpenStreetMap",
|
||||||
|
"LatitudInicial": 43.307251,
|
||||||
|
"LongitudInicial": -1.979497,
|
||||||
|
"ZoomInicial": 14,
|
||||||
|
"ZoomMinimo": 3,
|
||||||
|
"ZoomMaximo": 19
|
||||||
|
},
|
||||||
|
|
||||||
|
"Caminata": {
|
||||||
|
"Osrm": {
|
||||||
|
"BaseUrl": "https://osrmw.tecnosis.net/route/v1"
|
||||||
|
},
|
||||||
|
"Valhalla": {
|
||||||
|
"BaseUrl": "http://192.168.41.12:8002"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"Elevacion": {
|
||||||
|
"OpenElevation": {
|
||||||
|
"UrlLookup": "https://openelevation.tecnosis.net/api/v1/lookup"
|
||||||
|
},
|
||||||
|
"Idee": {
|
||||||
|
"UrlEjecucion": "https://api-processes.idee.es/processes/getElevation/execution"
|
||||||
|
},
|
||||||
|
"MaxPuntosPorTramo": 100,
|
||||||
|
"DecimalesCache": 6
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"PlanificadorRutas": {
|
||||||
|
"RadioParadasCandidatasMetros": 1200,
|
||||||
|
"MaxParadasCandidatasPorLado": 12,
|
||||||
|
"VelocidadPieKmH": 3.6,
|
||||||
|
"VelocidadBusKmH": 15.0,
|
||||||
|
"MinutosPorTransbordo": 5,
|
||||||
|
"SegundosPorParadaBus": 30,
|
||||||
|
"MaxAlternativasBusPorPar": 4,
|
||||||
|
"MaxParesEvaluados": 280
|
||||||
|
},
|
||||||
|
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
2409
RutasDBUS/wwwroot/app.css
Normal file
2409
RutasDBUS/wwwroot/app.css
Normal file
File diff suppressed because it is too large
Load Diff
1140
RutasDBUS/wwwroot/app.js
Normal file
1140
RutasDBUS/wwwroot/app.js
Normal file
File diff suppressed because it is too large
Load Diff
7
RutasDBUS/wwwroot/bootstrap/bootstrap.min.css
vendored
Normal file
7
RutasDBUS/wwwroot/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
RutasDBUS/wwwroot/bootstrap/bootstrap.min.css.map
Normal file
1
RutasDBUS/wwwroot/bootstrap/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
BIN
RutasDBUS/wwwroot/favicon.png
Normal file
BIN
RutasDBUS/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
38
RutasDBUS/wwwroot/service-worker.js
Normal file
38
RutasDBUS/wwwroot/service-worker.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const TILE_CACHE = 'tile-cache-v1';
|
||||||
|
const TILE_PREFIX = 'https://osm.tecnosis.net/osm_tiles/';
|
||||||
|
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', event => {
|
||||||
|
event.waitUntil(self.clients.claim());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
const req = event.request;
|
||||||
|
const url = req.url;
|
||||||
|
|
||||||
|
// Sólo tiles de tu servidor OSM
|
||||||
|
if (!url.startsWith(TILE_PREFIX)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
caches.open(TILE_CACHE).then(async cache => {
|
||||||
|
const cached = await cache.match(req);
|
||||||
|
if (cached) {
|
||||||
|
// 💾 Tile ya guardado -> no hay descarga nueva
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(req);
|
||||||
|
|
||||||
|
if (resp && (resp.status === 200 || resp.type === 'opaque')) {
|
||||||
|
cache.put(req, resp.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user