From b420ce1130215c8356183bd4826619dad4bea740 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:02:42 +0000 Subject: [PATCH 1/6] Initial plan From b7b7491a6e2840a4d976c899805c07fde690030a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:05:59 +0000 Subject: [PATCH 2/6] feat: add Windows MSVC Win32 RelWithDebInfo GitHub Actions workflow and vcpkg.json Co-authored-by: rgleason <2363959+rgleason@users.noreply.github.com> --- .github/workflows/bldwin32-rdeb.yml | 44 +++++++++++++++++++++++++++++ vcpkg.json | 6 ++++ 2 files changed, 50 insertions(+) create mode 100644 .github/workflows/bldwin32-rdeb.yml create mode 100644 vcpkg.json diff --git a/.github/workflows/bldwin32-rdeb.yml b/.github/workflows/bldwin32-rdeb.yml new file mode 100644 index 000000000..e5629542d --- /dev/null +++ b/.github/workflows/bldwin32-rdeb.yml @@ -0,0 +1,44 @@ +name: bldwin32-rdeb + +on: + workflow_dispatch: + push: + pull_request: + +jobs: + build-win32: + runs-on: windows-2022 + + steps: + - uses: actions/checkout@v4 + + - name: MSVC dev cmd (x86) + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x86 + + - name: Install wxWidgets via vcpkg (x86-windows) + uses: lukka/run-vcpkg@v11 + with: + vcpkgTriplet: x86-windows + runVcpkgInstall: true + vcpkgJsonGlob: vcpkg.json + + - name: Configure (CMake, Win32, RelWithDebInfo) + shell: pwsh + run: | + cmake -S . -B build ` + -G "Visual Studio 17 2022" -A Win32 -T v143 ` + -DOCPN_TARGET=MSVC ` + -DwxWidgets_ROOT_DIR="${env:VCPKG_INSTALLED_DIR}\x86-windows" ` + -DwxWidgets_LIB_DIR="${env:VCPKG_INSTALLED_DIR}\x86-windows\lib" ` + -DCMAKE_TOOLCHAIN_FILE="${env:VCPKG_ROOT}\scripts\buildsystems\vcpkg.cmake" + + - name: Build (RelWithDebInfo) + run: cmake --build build --target package --config RelWithDebInfo + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: windows-win32-rdeb-package + path: build/** diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 000000000..492e8a0a8 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,6 @@ +{ + "name": "weather-routing-pi", + "version-string": "0.0.0", + "builtin-baseline": "26283ac5e8a068561a718ce18b169bfad84c7dab", + "dependencies": ["wxwidgets"] +} From 00ca1d3265d83482d3e83c2422fb80a1a1cfc3a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 03:36:11 +0000 Subject: [PATCH 3/6] Initial plan From 47012820bba16190d984645272df28f86573a878 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 03:37:58 +0000 Subject: [PATCH 4/6] Update vcpkg.json builtin-baseline to full GitHub URL Co-authored-by: rgleason <2363959+rgleason@users.noreply.github.com> --- vcpkg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcpkg.json b/vcpkg.json index 492e8a0a8..efaed484d 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,6 @@ { "name": "weather-routing-pi", "version-string": "0.0.0", - "builtin-baseline": "26283ac5e8a068561a718ce18b169bfad84c7dab", + "builtin-baseline":"https://github.com/microsoft/vcpkg/commit/26283ac5e8a068561a718ce18b169bfad84c7dab", "dependencies": ["wxwidgets"] } From d1e32b2a38f40e694d1700ee6b007cffb54cf51a Mon Sep 17 00:00:00 2001 From: Rick Gleason Date: Thu, 26 Feb 2026 17:35:50 +1300 Subject: [PATCH 5/6] Update builtin-baseline URL format in vcpkg.json --- vcpkg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcpkg.json b/vcpkg.json index efaed484d..bff638f08 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,6 @@ { "name": "weather-routing-pi", "version-string": "0.0.0", - "builtin-baseline":"https://github.com/microsoft/vcpkg/commit/26283ac5e8a068561a718ce18b169bfad84c7dab", + "builtin-baseline":"26283ac5e8a068561a718ce18b169bfad84c7dab", "dependencies": ["wxwidgets"] } From 3f9192a597eaa3b8f409e9054701cbce1929cbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Rheinl=C3=A4nder?= Date: Wed, 25 Mar 2026 21:37:52 +0100 Subject: [PATCH 6/6] Implement option to limit the course angles to the boat's optimal angles from the current polar --- include/ConfigurationDialog.h | 1 + include/RouteMap.h | 5 +++ include/WeatherRoutingUI.h | 4 ++ src/ConfigurationDialog.cpp | 21 +++++++++ src/Position.cpp | 81 +++++++++++++++++++++++++++++++++-- src/WeatherRouting.cpp | 3 ++ src/WeatherRoutingUI.cpp | 16 +++++++ 7 files changed, 128 insertions(+), 3 deletions(-) diff --git a/include/ConfigurationDialog.h b/include/ConfigurationDialog.h index dea08bfe1..b8c996c99 100644 --- a/include/ConfigurationDialog.h +++ b/include/ConfigurationDialog.h @@ -130,6 +130,7 @@ class ConfigurationDialog : public ConfigurationDialogBase { void OnEndAtWaypoint(wxCommandEvent& event); void OnAvoidCyclones(wxCommandEvent& event); void OnUseMotor(wxCommandEvent& event); + void OnUseOptimalAngles(wxCommandEvent& event); void OnAddDegreeStep(wxCommandEvent& event); void OnRemoveDegreeStep(wxCommandEvent& event); void OnClearDegreeSteps(wxCommandEvent& event); diff --git a/include/RouteMap.h b/include/RouteMap.h index 30ef24dcf..536a202e6 100644 --- a/include/RouteMap.h +++ b/include/RouteMap.h @@ -528,6 +528,11 @@ struct RouteMapConfiguration { * calculation. The default value is 180 degrees. */ double ToDegree; + /** + * Use the optimal angles calculated from the boats's polar instead of + * FromDegree and ToDegree + */ + bool UseOptimalAngles; /** * The angular resolution at each step of the route calculation, in degrees. * Lower values provide finer resolution but increase computation time. diff --git a/include/WeatherRoutingUI.h b/include/WeatherRoutingUI.h index dfa304cf5..8c61a34e1 100644 --- a/include/WeatherRoutingUI.h +++ b/include/WeatherRoutingUI.h @@ -493,6 +493,7 @@ class ConfigurationDialogBase : public wxDialog { virtual void EnableSpin(wxMouseEvent& event) { event.Skip(); } virtual void EnableSpinDouble(wxMouseEvent& event) { event.Skip(); } virtual void OnUpdateSpin(wxSpinEvent& event) { event.Skip(); } + virtual void OnUseOptimalAngles(wxCommandEvent& event) { event.Skip(); } virtual void OnClose(wxCommandEvent& event) { event.Skip(); } virtual void OnAvoidCyclones(wxCommandEvent& event) { event.Skip(); } virtual void OnUseMotor(wxCommandEvent& event) { event.Skip(); } @@ -512,6 +513,9 @@ class ConfigurationDialogBase : public wxDialog { //!< night sailing wxSpinCtrl* m_sFromDegree; //!< Minimum course relative to true wind. wxSpinCtrl* m_sToDegree; //!< Maximum course relative to true wind. + wxCheckBox* m_cbUseOptimalAngles; //!< Use polar optimal angles for minimum + //!< and maximum course relative to true + //IsChecked(); + if (use_optimal_angles) { + m_sFromDegree->SetValue(0.0); + m_sToDegree->SetValue(180.0); + m_edited_controls.push_back(m_sFromDegree); + m_edited_controls.push_back(m_sToDegree); + } + m_sFromDegree->Enable(!use_optimal_angles); + m_sToDegree->Enable(!use_optimal_angles); + Update(); +} + void ConfigurationDialog::OnBoatFilename(wxCommandEvent& event) { wxFileDialog openDialog( this, _("Select Boat File"), wxFileName(m_tBoat->GetValue()).GetPath(), @@ -379,6 +392,10 @@ void ConfigurationDialog::SetConfigurations( SET_SPIN(FromDegree); SET_SPIN(ToDegree); + SET_CHECKBOX(UseOptimalAngles); + // Enable/disable degree controls based on checkbox state + m_sFromDegree->Enable(!m_cbUseOptimalAngles->IsChecked()); + m_sToDegree->Enable(!m_cbUseOptimalAngles->IsChecked()); SET_SPIN_DOUBLE(ByDegrees); // Motor settings @@ -503,6 +520,9 @@ void ConfigurationDialog::OnResetAdvanced(wxCommandEvent& event) { m_sFromDegree->SetValue(0); m_sToDegree->SetValue(180); + m_cbUseOptimalAngles->SetValue(false); + m_sFromDegree->Enable(true); + m_sToDegree->Enable(true); m_sByDegrees->SetValue(5.0); // Motor settings @@ -725,6 +745,7 @@ void ConfigurationDialog::Update() { GET_SPIN(FromDegree); GET_SPIN(ToDegree); + GET_CHECKBOX(UseOptimalAngles); GET_SPIN(ByDegrees); // Motor settings diff --git a/src/Position.cpp b/src/Position.cpp index 99b8764c2..35d93f58f 100644 --- a/src/Position.cpp +++ b/src/Position.cpp @@ -17,6 +17,8 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * **************************************************************************/ +#include + #include #include "Position.h" @@ -242,12 +244,85 @@ bool Position::Propagate(IsoRouteList& routelist, bearing2 = heading_resolve(parent_bearing + configuration.MaxSearchAngle); } - for (const double& twa : configuration.DegreeSteps) { + std::vector degree_steps; + degree_steps.reserve(configuration.DegreeSteps.size()); + + // Do not waste time exploring directions outside the configured optimal + // angles + if (configuration.UseOptimalAngles) { + int polar_idx = polar; + if (polar_idx < 0) { + // Find a reasonable polar for the first propagation + PolarSpeedStatus status; + polar_idx = configuration.boat.FindBestPolarForCondition( + polar, weather_data.twsOverWater, 90.0, weather_data.swell, + configuration.OptimizeTacking, &status); + if (polar_idx < 0 || status != POLAR_SPEED_SUCCESS) polar_idx = 0; + } + Polar& the_polar = configuration.boat.Polars[polar_idx]; + + // Note: Optimal angles are in the range of 0 to 360, where values + // greater than 180 are for the port tack + SailingVMG opt_angles = the_polar.GetVMGTrueWind(weather_data.twsOverWater); + opt_angles.values[SailingVMG::PORT_DOWNWIND] -= 360.0; + opt_angles.values[SailingVMG::PORT_UPWIND] -= 360.0; + + // If wind is too light etc. there may not be any optimal angles + // Check sanity to avoid indexing beyond limits of DegreeSteps + if (opt_angles.values[SailingVMG::PORT_DOWNWIND] != NAN && + configuration.DegreeSteps.front() < + opt_angles.values[SailingVMG::PORT_DOWNWIND] && + configuration.DegreeSteps.back() > + *std::max_element(opt_angles.values, opt_angles.values + 4)) { + size_t step_idx = 0; + while (configuration.DegreeSteps[step_idx] < + opt_angles.values[SailingVMG::PORT_DOWNWIND]) + ++step_idx; + degree_steps.emplace_back(opt_angles.values[SailingVMG::PORT_DOWNWIND]); + while (configuration.DegreeSteps[step_idx] < + opt_angles.values[SailingVMG::PORT_UPWIND]) { + degree_steps.emplace_back(configuration.DegreeSteps[step_idx]); + ++step_idx; + } + degree_steps.emplace_back(opt_angles.values[SailingVMG::PORT_UPWIND]); + while (configuration.DegreeSteps[step_idx] < + opt_angles.values[SailingVMG::STARBOARD_UPWIND]) + ++step_idx; + degree_steps.emplace_back( + opt_angles.values[SailingVMG::STARBOARD_UPWIND]); + while (configuration.DegreeSteps[step_idx] < + opt_angles.values[SailingVMG::STARBOARD_DOWNWIND]) { + degree_steps.emplace_back(configuration.DegreeSteps[step_idx]); + ++step_idx; + } + degree_steps.emplace_back( + opt_angles.values[SailingVMG::STARBOARD_DOWNWIND]); + + if (parent != nullptr) { + /* add a position behind the lines to ensure our route intersects + with the previous one to nicely merge the resulting graph */ + first_avoid = false; + rp = new Position(this); + double dp = .95; + rp->lat = (1 - dp) * lat + dp * parent->lat; + rp->lon = (1 - dp) * lon + dp * parent->lon; + rp->propagated = true; + rp->prev = rp->next = rp; + points = rp; + ++count; + } + } else { + degree_steps = configuration.DegreeSteps; + } + } else + degree_steps = configuration.DegreeSteps; + + for (const double& twa : degree_steps) { double timeseconds = configuration.UsedDeltaTime; double ctw = weather_data.twdOverWater + twa; /* rotated relative to true wind */ - // Do no waste time exploring directions outside the configured search + // Do not waste time exploring directions outside the configured search // angle. if (!std::isnan(bearing1)) { double bearing3 = heading_resolve(ctw); @@ -531,4 +606,4 @@ SkipPosition* SkipPosition::Copy() { fp->prev = np; np->next = fp; return fs; -} \ No newline at end of file +} diff --git a/src/WeatherRouting.cpp b/src/WeatherRouting.cpp index 7d07d294a..065f3e1d2 100644 --- a/src/WeatherRouting.cpp +++ b/src/WeatherRouting.cpp @@ -2365,6 +2365,7 @@ bool WeatherRouting::OpenXML(wxString filename, bool reportfailure) { configuration.FromDegree = AttributeDouble(e, "FromDegree", 0); configuration.ToDegree = AttributeDouble(e, "ToDegree", 180); + configuration.UseOptimalAngles = AttributeBool(e, "UseOptimalAngles", false); configuration.ByDegrees = AttributeDouble(e, "ByDegrees", 5.); // Motor configuration loading @@ -2495,6 +2496,7 @@ void WeatherRouting::SaveXML(wxString filename) { c->SetDoubleAttribute("FromDegree", configuration.FromDegree); c->SetDoubleAttribute("ToDegree", configuration.ToDegree); + c->SetAttribute("UseOptimalAngles", configuration.UseOptimalAngles); c->SetDoubleAttribute("ByDegrees", configuration.ByDegrees); // Motor configuration saving @@ -3605,6 +3607,7 @@ RouteMapConfiguration WeatherRouting::DefaultConfiguration() { configuration.FromDegree = 0; configuration.ToDegree = 180; + configuration.UseOptimalAngles = false; configuration.ByDegrees = 5; return configuration; diff --git a/src/WeatherRoutingUI.cpp b/src/WeatherRoutingUI.cpp index 56fbae509..25b84423d 100644 --- a/src/WeatherRoutingUI.cpp +++ b/src/WeatherRoutingUI.cpp @@ -2257,6 +2257,15 @@ ConfigurationDialogBase::ConfigurationDialogBase(wxWindow* parent, wxSize(140, -1), wxSP_ARROW_KEYS, 0, 180, 180); bSizer4->Add(m_sToDegree, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + m_cbUseOptimalAngles = + new wxCheckBox(sbCourses->GetStaticBox(), wxID_ANY, + _("Use optimal angles"), wxDefaultPosition, + wxDefaultSize, wxCHK_2STATE); + m_cbUseOptimalAngles->SetToolTip( + _("When enabled, uses the optimal upwind and downwind angles " + "calculated from the boat's polar instead of fixed values.")); + bSizer4->Add(m_cbUseOptimalAngles, 0, wxALL, 5); + sbCourses->Add(bSizer4, 1, wxEXPAND, 5); wxBoxSizer* bSizer3; @@ -2913,6 +2922,9 @@ ConfigurationDialogBase::ConfigurationDialogBase(wxWindow* parent, m_sToDegree->Connect( wxEVT_COMMAND_SPINCTRL_UPDATED, wxSpinEventHandler(ConfigurationDialogBase::OnUpdateSpin), NULL, this); + m_cbUseOptimalAngles->Connect( + wxEVT_COMMAND_CHECKBOX_CLICKED, + wxCommandEventHandler(ConfigurationDialogBase::OnUseOptimalAngles), NULL, this); m_sByDegrees->Connect( wxEVT_COMMAND_SPINCTRLDOUBLE_UPDATED, wxSpinEventHandler(ConfigurationDialogBase::OnUpdateSpin), NULL, this); @@ -3545,6 +3557,10 @@ ConfigurationDialogBase::~ConfigurationDialogBase() { m_sToDegree->Disconnect( wxEVT_COMMAND_SPINCTRL_UPDATED, wxSpinEventHandler(ConfigurationDialogBase::OnUpdateSpin), NULL, this); + m_cbUseOptimalAngles->Disconnect( + wxEVT_COMMAND_CHECKBOX_CLICKED, + wxCommandEventHandler(ConfigurationDialogBase::OnUseOptimalAngles), NULL, + this); m_sByDegrees->Disconnect( wxEVT_COMMAND_SPINCTRLDOUBLE_UPDATED, wxCommandEventHandler(ConfigurationDialogBase::OnUpdate), NULL, this);