Skip to content

Instantly share code, notes, and snippets.

@SweetAsNZ
Last active December 10, 2025 08:34
Show Gist options
  • Select an option

  • Save SweetAsNZ/186271638d8d1a994af2bdc3d68df63b to your computer and use it in GitHub Desktop.

Select an option

Save SweetAsNZ/186271638d8d1a994af2bdc3d68df63b to your computer and use it in GitHub Desktop.
Calculate a boat's current and new arrival time and average VMG for course changes using polar diagram data supply recommendations as to whether to change course to that angle or not
function Get-SpeedToMakeWithCourseDeviation {
<#
.SYNOPSIS
Calculate arrival time and average VMG for course changes using polar diagram data.
.DESCRIPTION
Calculates whether a course deviation is worthwhile by determining:
- Expected boat speed at the new course angle using polar diagram
- Velocity Made Good (VMG) toward the waypoint
- Estimated arrival time for current course vs altered course
.PARAMETER SOG
Current Speed Over Ground in knots
.PARAMETER CourseTrue
Current course heading in degrees (0-360) relative to true north
.PARAMETER AWS
Apparent Wind Speed in knots
.PARAMETER AWA
Apparent Wind Angle in degrees relative to boat heading
.PARAMETER CourseDeviation
Course change angle in degrees (positive = turn to starboard, negative = turn to port)
.PARAMETER CurrentAngleTrue
Current angle to waypoint in degrees (0-360) - optional, calculated from CourseTrue and WaypointBearingTrue if not provided
.PARAMETER CurrentSpeed
Current boat speed in knots - optional, uses SOG if not provided
.PARAMETER TideDirectionTrue
Direction the tide is flowing TO in degrees (0-360) - optional
.PARAMETER TideSpeed
Speed of the tide/current in knots - optional
.PARAMETER DistanceToWaypoint
Distance to waypoint in nautical miles
.PARAMETER WaypointBearingTrue
True bearing to waypoint in degrees (0-360)
.PARAMETER PolarDiagramData
Path to CSV file containing polar diagram data with columns for wind angles and speeds
.EXAMPLE
Get-SpeedToMakeWithCourseDeviation # Launches GUI for input
.EXAMPLE
Get-SpeedToMakeWithCourseDeviation -SOG 6.5 -CourseTrue 45 -AWS 12 -AWA 70 -CourseDeviation 10
.EXAMPLE
Get-SpeedToMakeWithCourseDeviation -SOG 5 -CourseTrue 45 -AWS 95 -AWA 45 -CourseDeviation -45 -DistanceToWaypoint 5000 -WaypointBearingTrue 0
.EXAMPLE
Get-SpeedToMakeWithCourseDeviation -SOG 6.5 -CourseTrue 45 -AWS 12 -AWA 70 -CourseDeviation 10 -DistanceToWaypoint 50 -WaypointBearingTrue 50 -CurrentSpeed 1.0 -CurrentAngleTrue 5
.EXAMPLE
Get-SpeedToMakeWithCourseDeviation -SOG 6.5 -CourseTrue 45 -AWS 12 -AWA 70 -CourseDeviation 10 -DistanceToWaypoint 50 -WaypointBearingTrue 50 -CurrentSpeed 1.0 -CurrentAngleTrue 5 -TideDirectionTrue 90 -TideSpeed 1.5
.NOTES
Author: Tim West
Company: Sweet As Chocolate Ltd
Created: 2025-12-08
Updated: 2025-12-08
Status: WIP - Work In Progress
Version: 0.2.1
License: This project is licensed under the MIT License (free to use and modify)
.TODO
- Improve polar data interpolation
- Add error handling for edge cases
- Validate input ranges
- Test with real polar data and boat data
.CHANGELOG
2025-12-08: Initial version
2025-12-10: Added tide/current effects to VMG calculation
2025-12-11: Improved GUI input handling and validation
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false, HelpMessage = "Current Speed Over Ground in knots")]
[double]$SOG = 0,
[Parameter(Mandatory = $false, HelpMessage = "Current course heading in degrees")]
[double]$CourseTrue = 0,
[Parameter(Mandatory = $false, HelpMessage = "Apparent Wind Speed in knots")]
[double]$AWS = 0,
[Parameter(Mandatory = $false, HelpMessage = "Apparent Wind Angle in degrees")]
[double]$AWA = 0,
[Parameter(Mandatory = $false, HelpMessage = "Course deviation angle in degrees (+ starboard, - port)")]
[double]$CourseDeviation = 0,
[Parameter(Mandatory = $false, HelpMessage = "Current angle to waypoint in degrees")]
[double]$CurrentAngleTrue = 0,
[Parameter(Mandatory = $false, HelpMessage = "Current boat speed in knots")]
[double]$CurrentSpeed = 0,
[Parameter(Mandatory = $false, HelpMessage = "Direction tide is flowing TO in degrees")]
[double]$TideDirectionTrue = 0,
[Parameter(Mandatory = $false, HelpMessage = "Speed of tide/current in knots")]
[double]$TideSpeed = 0,
[Parameter(Mandatory = $false, HelpMessage = "Distance to waypoint in nautical miles")]
[double]$DistanceToWaypoint = 0,
[Parameter(Mandatory = $false, HelpMessage = "True bearing to waypoint in degrees")]
[double]$WaypointBearingTrue = 0,
[Parameter(HelpMessage = "Path to polar diagram CSV file")]
[string]$PolarDiagramData = "$ENV:OneDriveConsumer\Documents\Boating\Example_Polar_With_Correct_Headings_29-04-25.csv"
)
# Check if we should show GUI (only if NO parameters provided)
$showGUI = ($PSBoundParameters.Count -eq 0)
# Show GUI for input if no parameters are provided
if ($showGUI) {
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Course Deviation Calculator'
$form.Size = New-Object System.Drawing.Size(500, 650)
$form.StartPosition = 'CenterScreen'
$form.FormBorderStyle = 'FixedDialog'
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$form.TopMost = $true
$form.ShowInTaskbar = $true
$y = 20
# Helper function to create labeled text box
function Add-TextBox {
param($labelText, $defaultValue, $yPosition)
$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Point(20, $yPosition)
$label.Size = New-Object System.Drawing.Size(200, 20)
$label.Text = $labelText
$form.Controls.Add($label)
$textBox = New-Object System.Windows.Forms.TextBox
$textBox.Location = New-Object System.Drawing.Point(230, $yPosition)
$textBox.Size = New-Object System.Drawing.Size(220, 20)
$textBox.Text = $defaultValue
$form.Controls.Add($textBox)
return $textBox
}
# Required fields
$txtSOG = Add-TextBox 'SOG (knots) *' $SOG $y
$y += 30
$txtCourseTrue = Add-TextBox 'Course True (°) *' $CourseTrue $y
$y += 30
$txtAWS = Add-TextBox 'AWS (knots) *' $AWS $y
$y += 30
$txtAWA = Add-TextBox 'AWA (°) *' $AWA $y
$y += 30
$txtCourseDeviation = Add-TextBox 'Course Deviation (°) *' $CourseDeviation $y
$y += 30
# Separator
$separator = New-Object System.Windows.Forms.Label
$separator.Location = New-Object System.Drawing.Point(20, $y)
$separator.Size = New-Object System.Drawing.Size(430, 2)
$separator.BorderStyle = 'Fixed3D'
$form.Controls.Add($separator)
$y += 10
# Optional fields
$lblOptional = New-Object System.Windows.Forms.Label
$lblOptional.Location = New-Object System.Drawing.Point(20, $y)
$lblOptional.Size = New-Object System.Drawing.Size(200, 20)
$lblOptional.Text = 'Optional Parameters:'
$lblOptional.Font = New-Object System.Drawing.Font("Arial", 9, [System.Drawing.FontStyle]::Bold)
$form.Controls.Add($lblOptional)
$y += 25
# Optional fields
$txtCurrentAngle = Add-TextBox 'Current Angle to WP (°)' $(if ($CurrentAngleTrue -ge 0) { $CurrentAngleTrue } else { '' }) $y
$y += 30
$txtCurrentSpeed = Add-TextBox 'Current Speed (knots)' $(if ($CurrentSpeed -gt 0) { $CurrentSpeed } else { '' }) $y
$y += 30
$txtTideDirection = Add-TextBox 'Tide Direction True (°)' $(if ($TideDirectionTrue -ge 0) { $TideDirectionTrue } else { '' }) $y
$y += 30
$txtTideSpeed = Add-TextBox 'Tide Speed (knots)' $TideSpeed $y
$y += 30
$txtDistance = Add-TextBox 'Distance to WP (nm)' $DistanceToWaypoint $y
$y += 30
$txtWaypointBearing = Add-TextBox 'Waypoint Bearing True (°)' $WaypointBearingTrue $y
$y += 30
# Polar file selector GUI
$lblPolar = New-Object System.Windows.Forms.Label
$lblPolar.Location = New-Object System.Drawing.Point(20, $y)
$lblPolar.Size = New-Object System.Drawing.Size(200, 20)
$lblPolar.Text = 'Polar Diagram CSV:'
$form.Controls.Add($lblPolar)
$txtPolarPath = New-Object System.Windows.Forms.TextBox
$txtPolarPath.Location = New-Object System.Drawing.Point(20, ($y + 25))
$txtPolarPath.Size = New-Object System.Drawing.Size(350, 20)
$txtPolarPath.Text = $PolarDiagramData
$form.Controls.Add($txtPolarPath)
$btnBrowse = New-Object System.Windows.Forms.Button
$btnBrowse.Location = New-Object System.Drawing.Point(380, ($y + 23))
$btnBrowse.Size = New-Object System.Drawing.Size(70, 25)
$btnBrowse.Text = 'Browse...'
$btnBrowse.Add_Click({
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$openFileDialog.Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*"
$openFileDialog.Title = "Select Polar Diagram CSV"
if ($openFileDialog.ShowDialog() -eq 'OK') {
$txtPolarPath.Text = $openFileDialog.FileName
}
})
$form.Controls.Add($btnBrowse)
$y += 60
# OK and Cancel buttons
$btnOK = New-Object System.Windows.Forms.Button
$btnOK.Location = New-Object System.Drawing.Point(200, $y)
$btnOK.Size = New-Object System.Drawing.Size(100, 30)
$btnOK.Text = 'Calculate'
$btnOK.DialogResult = [System.Windows.Forms.DialogResult]::OK
$form.Controls.Add($btnOK)
$form.AcceptButton = $btnOK
$btnCancel = New-Object System.Windows.Forms.Button
$btnCancel.Location = New-Object System.Drawing.Point(310, $y)
$btnCancel.Size = New-Object System.Drawing.Size(100, 30)
$btnCancel.Text = 'Cancel'
$btnCancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$form.Controls.Add($btnCancel)
$form.CancelButton = $btnCancel
$result = $form.ShowDialog()
# Process form result
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
# Parse inputs from GUI
try {
$SOG = [double]$txtSOG.Text
$CourseTrue = [double]$txtCourseTrue.Text
$AWS = [double]$txtAWS.Text
$AWA = [double]$txtAWA.Text
$CourseDeviation = [double]$txtCourseDeviation.Text
if ($txtCurrentAngle.Text) { $CurrentAngleTrue = [double]$txtCurrentAngle.Text }
if ($txtCurrentSpeed.Text) { $CurrentSpeed = [double]$txtCurrentSpeed.Text }
if ($txtTideDirection.Text) { $TideDirectionTrue = [double]$txtTideDirection.Text }
if ($txtTideSpeed.Text) { $TideSpeed = [double]$txtTideSpeed.Text }
if ($txtDistance.Text) { $DistanceToWaypoint = [double]$txtDistance.Text }
if ($txtWaypointBearing.Text) { $WaypointBearingTrue = [double]$txtWaypointBearing.Text }
if ($txtPolarPath.Text) { $PolarDiagramData = $txtPolarPath.Text }
}
catch {
Write-Error "Invalid input: $_"
return
}
}
else {
Write-Host "Cancelled by user" -ForegroundColor Yellow
return
}
$form.Dispose()
}
# Validate required inputs (if not using GUI)
if (-not $showGUI) {
if ($SOG -eq 0 -or $CourseTrue -eq 0 -or $AWS -eq 0 -or $AWA -eq 0 -or $CourseDeviation -eq 0) {
Write-Error "Required parameters missing. Provide SOG, CourseTrue, AWS, AWA, and CourseDeviation, or call without parameters to use GUI."
return
}
}
# Validate inputs
if (-not (Test-Path $PolarDiagramData)) {
Write-Error "Polar diagram file not found: $PolarDiagramData"
return
}
# Load Polar Diagram Data
Write-Host "`nLoading polar diagram data..." -ForegroundColor Cyan
$polarData = Import-Csv -Path $PolarDiagramData
# Helper function to normalize angles to 0-360
function ConvertTo-NormalizedAngle {
param([double]$angle)
$normalized = $angle % 360
if ($normalized -lt 0) { $normalized += 360 }
return $normalized
}
# Helper function to get boat speed from polar diagram
function Get-BoatSpeedFromPolar {
param(
[object[]]$PolarData,
[double]$WindSpeed,
[double]$WindAngle
)
# Normalize wind angle to 0-180 (polar diagrams are typically symmetric)
$normalizedAngle = [Math]::Abs($WindAngle)
if ($normalizedAngle -gt 180) {
$normalizedAngle = 360 - $normalizedAngle
}
# Round to nearest angle in polar (typically 5-degree increments)
$roundedAngle = [Math]::Round($normalizedAngle / 5) * 5
# Get column headers (TWS values are in the header row)
$headers = $PolarData[0].PSObject.Properties.Name
# Get all numeric TWS values from headers
$twsValues = @()
foreach ($header in $headers) {
try {
$twsValues += [double]$header
} catch {
# Skip non-numeric headers
}
}
$twsValues = $twsValues | Sort-Object
$maxTWS = $twsValues[-1]
# If wind speed exceeds polar data, cap it and warn
$cappedWindSpeed = $WindSpeed
if ($WindSpeed -gt $maxTWS) {
Write-Warning "Wind speed $([Math]::Round($WindSpeed,1)) knots exceeds polar data max ($maxTWS knots). Using max available."
$cappedWindSpeed = $maxTWS
}
# Find the wind angle row (look for the angle in the first column)
$angleRow = $PolarData | Where-Object {
$firstCol = $_.PSObject.Properties.Value | Select-Object -First 1
try {
[double]$firstCol -eq $roundedAngle
} catch {
$false
}
}
if (-not $angleRow) {
# Try to interpolate between nearby angles
$allAngles = @()
foreach ($row in $PolarData) {
$firstCol = $row.PSObject.Properties.Value | Select-Object -First 1
try {
$allAngles += [double]$firstCol
} catch {
# Skip non-numeric
}
}
$allAngles = $allAngles | Sort-Object
# Find closest angles
$lowerAngle = $allAngles | Where-Object { $_ -le $roundedAngle } | Select-Object -Last 1
$upperAngle = $allAngles | Where-Object { $_ -ge $roundedAngle } | Select-Object -First 1
if ($lowerAngle -and $upperAngle -and $lowerAngle -ne $upperAngle) {
# Interpolate between angles
$lowerRow = $PolarData | Where-Object {
$firstCol = $_.PSObject.Properties.Value | Select-Object -First 1
try { [double]$firstCol -eq $lowerAngle } catch { $false }
}
$upperRow = $PolarData | Where-Object {
$firstCol = $_.PSObject.Properties.Value | Select-Object -First 1
try { [double]$firstCol -eq $upperAngle } catch { $false }
}
# Find closest TWS column
$closestTWSColumn = $null
$closestDiff = [double]::MaxValue
foreach ($header in $headers) {
try {
$headerValue = [double]$header
$diff = [Math]::Abs($headerValue - $cappedWindSpeed)
if ($diff -lt $closestDiff) {
$closestDiff = $diff
$closestTWSColumn = $header
}
} catch {
continue
}
}
if ($closestTWSColumn -and $lowerRow.$closestTWSColumn -and $upperRow.$closestTWSColumn) {
try {
$lowerSpeed = [double]$lowerRow.$closestTWSColumn
$upperSpeed = [double]$upperRow.$closestTWSColumn
# Linear interpolation
$ratio = ($roundedAngle - $lowerAngle) / ($upperAngle - $lowerAngle)
$interpolatedSpeed = $lowerSpeed + ($upperSpeed - $lowerSpeed) * $ratio
Write-Host " Polar lookup: TWA=$roundedAngle° (interpolated) TWS=$([Math]::Round([double]$closestTWSColumn,1)) → Speed=$([Math]::Round($interpolatedSpeed,2)) knots" -ForegroundColor DarkGray
return $interpolatedSpeed
} catch {
# Fall through to estimation
}
}
}
Write-Warning "Wind angle $roundedAngle not found in polar data. Using estimation based on typical boat performance."
# Better estimation: can't sail directly into wind, reaching is fastest
if ($roundedAngle -lt 30) {
return 0 # Cannot sail directly upwind (no-go zone)
} elseif ($roundedAngle -lt 50) {
return $SOG * 0.85 # Close hauled - typically slower
} elseif ($roundedAngle -lt 100) {
return $SOG * 1.1 # Reaching - typically fastest
} elseif ($roundedAngle -lt 140) {
return $SOG * 1.0 # Broad reach
} else {
return $SOG * 0.9 # Running
}
}
# Find closest TWS column (headers contain wind speeds)
$closestTWSColumn = $null
$closestDiff = [double]::MaxValue
foreach ($header in $headers) {
try {
$headerValue = [double]$header
$diff = [Math]::Abs($headerValue - $cappedWindSpeed)
if ($diff -lt $closestDiff) {
$closestDiff = $diff
$closestTWSColumn = $header
}
} catch {
# Skip non-numeric headers (like angle column)
continue
}
}
if ($closestTWSColumn -and $angleRow.$closestTWSColumn) {
try {
$speed = [double]$angleRow.$closestTWSColumn
Write-Host " Polar lookup: TWA=$roundedAngle° TWS=$([Math]::Round([double]$closestTWSColumn,1)) → Speed=$([Math]::Round($speed,2)) knots" -ForegroundColor DarkGray
return $speed
} catch {
Write-Warning "Could not parse boat speed from polar data."
}
}
# Fallback: estimate based on current SOG and angle
Write-Warning "Polar data not found for TWS=$WindSpeed TWA=$roundedAngle. Using estimation."
if ($roundedAngle -lt 30) {
return 0 # Cannot sail directly upwind (no-go zone)
} elseif ($roundedAngle -lt 50) {
return $SOG * 0.85
} elseif ($roundedAngle -lt 100) {
return $SOG * 1.1
} elseif ($roundedAngle -lt 140) {
return $SOG * 1.0
} else {
return $SOG * 0.9
}
}
# Calculate True Wind from Apparent Wind
Write-Host "`nCalculating true wind..." -ForegroundColor Cyan
$awaRadians = $AWA * [Math]::PI / 180
$awsX = $AWS * [Math]::Cos($awaRadians)
$awsY = $AWS * [Math]::Sin($awaRadians)
$twsX = $awsX - $SOG
$twsY = $awsY
$TWS = [Math]::Sqrt([Math]::Pow($twsX, 2) + [Math]::Pow($twsY, 2))
$twaRadians = [Math]::Atan2($twsY, $twsX)
$TWA = $twaRadians * 180 / [Math]::PI
Write-Host " True Wind Speed: $([Math]::Round($TWS, 2)) knots" -ForegroundColor Gray
Write-Host " True Wind Angle: $([Math]::Round($TWA, 2))°" -ForegroundColor Gray
# Look up expected speed from polar for current conditions
$polarCurrentSpeed = Get-BoatSpeedFromPolar -PolarData $polarData -WindSpeed $TWS -WindAngle $TWA
# Calculate wave-assisted speed bonus if SOG exceeds polar speed
# Wave action typically adds speed when sailing downwind (TWA 90-150°)
$waveBonusFactor = 1.0
if ($SOG -gt $polarCurrentSpeed) {
$speedDifference = $SOG - $polarCurrentSpeed
$normalizedTWA = [Math]::Abs($TWA)
if ($normalizedTWA -gt 180) { $normalizedTWA = 360 - $normalizedTWA }
# Maximum wave effect around 120° TWA, tapering off towards 90° and 150°
if ($normalizedTWA -ge 90 -and $normalizedTWA -le 150) {
# Calculate wave effect coefficient (peaks at 120°)
$angleFrom120 = [Math]::Abs($normalizedTWA - 120)
$waveCoefficient = 1.0 - ($angleFrom120 / 30.0) # 1.0 at 120°, 0 at 90° and 150°
if ($waveCoefficient -gt 0) {
# Calculate bonus factor based on how much SOG exceeds polar
$bonusPercentage = ($speedDifference / $polarCurrentSpeed) * $waveCoefficient
$waveBonusFactor = 1.0 + $bonusPercentage
Write-Host " Wave-assisted sailing detected (TWA favorable for surfing/wave action)" -ForegroundColor Cyan
Write-Host " Wave bonus factor: $([Math]::Round($waveBonusFactor, 3))x (peak at 120° TWA)" -ForegroundColor Cyan
}
}
}
Write-Host " Polar expected speed at current TWA: $([Math]::Round($polarCurrentSpeed, 2)) knots vs SOG: $SOG knots" -ForegroundColor Gray
# Calculate new course and apparent wind angle after deviation
$newCourse = ConvertTo-NormalizedAngle ($CourseTrue + $CourseDeviation)
$newAWA = ConvertTo-NormalizedAngle ($AWA - $CourseDeviation)
# We need to estimate the new boat speed from polar
# For now, use TWS and calculate new TWA based on new course
# True wind direction remains constant
$trueWindDirection = ConvertTo-NormalizedAngle ($CourseTrue + $TWA)
$newTWA = ConvertTo-NormalizedAngle ($trueWindDirection - $newCourse)
if ($newTWA -gt 180) { $newTWA = 360 - $newTWA }
Write-Host " New course TWA: $([Math]::Round($newTWA, 2))°" -ForegroundColor Gray
# Get expected boat speed from polar diagram for new TWA
$expectedSpeed = Get-BoatSpeedFromPolar -PolarData $polarData -WindSpeed $TWS -WindAngle $newTWA
# Apply wave bonus factor to new course if in favorable wave angle
$normalizedNewTWA = [Math]::Abs($newTWA)
if ($normalizedNewTWA -gt 180) { $normalizedNewTWA = 360 - $normalizedNewTWA }
# Apply wave bonus only if original wave bonus was applicable
if ($waveBonusFactor -gt 1.0 -and $normalizedNewTWA -ge 90 -and $normalizedNewTWA -le 150) {
# Calculate wave effect for new TWA
$angleFrom120 = [Math]::Abs($normalizedNewTWA - 120)
$newWaveCoefficient = 1.0 - ($angleFrom120 / 30.0)
if ($newWaveCoefficient -gt 0) {
# Scale the bonus based on new TWA's wave favorability
$normalizedCurrentTWA = [Math]::Abs($TWA)
if ($normalizedCurrentTWA -gt 180) { $normalizedCurrentTWA = 360 - $normalizedCurrentTWA }
$currentAngleFrom120 = [Math]::Abs($normalizedCurrentTWA - 120)
$currentWaveCoefficient = [Math]::Max(0, 1.0 - ($currentAngleFrom120 / 30.0))
# Adjust wave bonus proportionally
if ($currentWaveCoefficient -gt 0) {
$adjustedWaveBonusFactor = 1.0 + (($waveBonusFactor - 1.0) * ($newWaveCoefficient / $currentWaveCoefficient))
$expectedSpeed = $expectedSpeed * $adjustedWaveBonusFactor
Write-Host " Applied wave bonus to new course: $([Math]::Round($adjustedWaveBonusFactor, 3))x" -ForegroundColor DarkGray
}
}
}
# Use provided CurrentSpeed or fall back to SOG
$actualCurrentSpeed = if ($CurrentSpeed -gt 0) { $CurrentSpeed } else { $SOG }
# Calculate tide effects if provided
$tideVMGCurrent = 0
$tideVMGNew = 0
if ($TideSpeed -gt 0 -and $TideDirectionTrue -ge 0) {
Write-Host "`nTide/Current Effects:" -ForegroundColor Cyan
Write-Host " Tide flowing to: $TideDirectionTrue° at $TideSpeed knots" -ForegroundColor Gray
# Calculate tide component toward waypoint for current course
if ($WaypointBearingTrue -ne 0) {
$tideAngleToCurrent = [Math]::Abs($TideDirectionTrue - $WaypointBearingTrue)
if ($tideAngleToCurrent -gt 180) { $tideAngleToCurrent = 360 - $tideAngleToCurrent }
$tideVMGCurrent = $TideSpeed * [Math]::Cos($tideAngleToCurrent * [Math]::PI / 180)
Write-Host " Tide VMG contribution (current): $([Math]::Round($tideVMGCurrent, 2)) knots" -ForegroundColor Gray
}
# Tide effect is same for new course (tide doesn't change)
$tideVMGNew = $tideVMGCurrent
}
# Calculate VMG (Velocity Made Good toward waypoint)
# VMG = Boat Speed × cos(angle between course and waypoint) + Tide contribution
# Current VMG - use provided CurrentAngleTrue if available
if ($CurrentAngleTrue -ge 0) {
# Use the provided current angle to waypoint
$currentAngleToWaypoint = $CurrentAngleTrue
} elseif ($WaypointBearingTrue -eq 0) {
# If no waypoint bearing specified, assume we're heading directly to it
$currentAngleToWaypoint = 0
} else {
# Calculate from course and waypoint bearing
$currentAngleToWaypoint = [Math]::Abs($WaypointBearingTrue - $CourseTrue)
if ($currentAngleToWaypoint -gt 180) {
$currentAngleToWaypoint = 360 - $currentAngleToWaypoint
}
}
# Current VMG
$boatVMGCurrent = $actualCurrentSpeed * [Math]::Cos($currentAngleToWaypoint * [Math]::PI / 180)
$currentVMG = $boatVMGCurrent + $tideVMGCurrent
# New VMG after course change
$newAngleToWaypoint = [Math]::Abs($WaypointBearingTrue - $newCourse)
if ($newAngleToWaypoint -gt 180) {
$newAngleToWaypoint = 360 - $newAngleToWaypoint
}
# New VMG
$boatVMGNew = $expectedSpeed * [Math]::Cos($newAngleToWaypoint * [Math]::PI / 180)
$newVMG = $boatVMGNew + $tideVMGNew
# Calculate arrival times
if ($currentVMG -le 0) {
$currentArrivalTime = [double]::PositiveInfinity
$currentArrivalTimeFormatted = "Never (sailing away)"
} else {
$currentArrivalTime = $DistanceToWaypoint / $currentVMG
$currentHours = [Math]::Floor($currentArrivalTime)
$currentMinutes = [Math]::Round(($currentArrivalTime - $currentHours) * 60)
$currentArrivalTimeFormatted = "${currentHours}h ${currentMinutes}m"
}
# New arrival time
if ($newVMG -le 0) {
$newArrivalTime = [double]::PositiveInfinity
$newArrivalTimeFormatted = "Never (sailing away)"
} else {
$newArrivalTime = $DistanceToWaypoint / $newVMG
$newHours = [Math]::Floor($newArrivalTime)
$newMinutes = [Math]::Round(($newArrivalTime - $newHours) * 60)
$newArrivalTimeFormatted = "${newHours}h ${newMinutes}m"
}
# Calculate time saved or lost
$timeSaved = $currentArrivalTime - $newArrivalTime
$timeSavedHours = [Math]::Floor([Math]::Abs($timeSaved))
$timeSavedMinutes = [Math]::Round(([Math]::Abs($timeSaved) - $timeSavedHours) * 60)
if ($timeSaved -gt 0) {
$timeSavedFormatted = "Save ${timeSavedHours}h ${timeSavedMinutes}m"
$recommendation = "YES - Course change is worthwhile"
} elseif ($timeSaved -lt 0) {
$timeSavedFormatted = "Lose ${timeSavedHours}h ${timeSavedMinutes}m"
$recommendation = "NO - Stay on current course"
} else {
$timeSavedFormatted = "No difference"
$recommendation = "NEUTRAL - No significant difference"
}
# Create result object
$result = [PSCustomObject]@{
CurrentCourse = $CourseTrue
CurrentSOG = $SOG
CurrentSpeed = $actualCurrentSpeed
CurrentAngleToWaypoint = [Math]::Round($currentAngleToWaypoint, 2)
CurrentAWA = $AWA
CurrentAWS = $AWS
CurrentVMG = [Math]::Round($currentVMG, 2)
CurrentBoatVMG = [Math]::Round($boatVMGCurrent, 2)
TideVMG = [Math]::Round($tideVMGCurrent, 2)
CurrentArrivalTime = $currentArrivalTimeFormatted
NewCourse = $newCourse
CourseDeviation = $CourseDeviation
NewAWA = [Math]::Round($newAWA, 2)
ExpectedSpeed = [Math]::Round($expectedSpeed, 2)
NewVMG = [Math]::Round($newVMG, 2)
NewArrivalTime = $newArrivalTimeFormatted
TimeDifference = $timeSavedFormatted
DistanceToWaypoint = $DistanceToWaypoint
WaypointBearing = $WaypointBearingTrue
Recommendation = $recommendation
TrueWindSpeed = [Math]::Round($TWS, 2)
TrueWindAngle = [Math]::Round($TWA, 2)
}
# Display results in table format
Write-Host "`n=== Course Deviation Analysis ===" -ForegroundColor Cyan
Write-Host ""
# Create comparison table with additional info column
$comparisonData = [PSCustomObject]@{
'Metric' = 'Course (°)'
'Current' = $result.CurrentCourse
'Proposed' = "$($result.NewCourse) (${CourseDeviation}° deviation)"
'Additional Info' = "Distance to WP: $($result.DistanceToWaypoint) nm"
}
$tableData = @($comparisonData)
$tableData += [PSCustomObject]@{
'Metric' = 'Speed (knots)'
'Current' = $result.CurrentSOG
'Proposed' = $result.ExpectedSpeed
'Additional Info' = "WP Bearing: $($result.WaypointBearing)°"
}
$tableData += [PSCustomObject]@{
'Metric' = 'AWA (°)'
'Current' = $result.CurrentAWA
'Proposed' = $result.NewAWA
'Additional Info' = "AWS: $($result.CurrentAWS) knots"
}
$tableData += [PSCustomObject]@{
'Metric' = 'Angle to WP (°)'
'Current' = $result.CurrentAngleToWaypoint
'Proposed' = [Math]::Round($newAngleToWaypoint, 2)
'Additional Info' = "TWS: $($result.TrueWindSpeed) knots"
}
if ($TideSpeed -gt 0 -and $TideDirectionTrue -ge 0) {
$tableData += [PSCustomObject]@{
'Metric' = 'Boat VMG (knots)'
'Current' = $result.CurrentBoatVMG
'Proposed' = [Math]::Round($boatVMGNew, 2)
'Additional Info' = "TWA: $($result.TrueWindAngle)°"
}
$tableData += [PSCustomObject]@{
'Metric' = 'Tide VMG (knots)'
'Current' = $result.TideVMG
'Proposed' = [Math]::Round($tideVMGNew, 2)
'Additional Info' = "Tide: $TideDirectionTrue° @ $TideSpeed kts"
}
$tableData += [PSCustomObject]@{
'Metric' = 'Total VMG (knots)'
'Current' = $result.CurrentVMG
'Proposed' = $result.NewVMG
'Additional Info' = "Time Diff: $($result.TimeDifference)"
}
} else {
$tableData += [PSCustomObject]@{
'Metric' = 'VMG (knots)'
'Current' = $result.CurrentVMG
'Proposed' = $result.NewVMG
'Additional Info' = "TWA: $($result.TrueWindAngle)°"
}
$tableData += [PSCustomObject]@{
'Metric' = ''
'Current' = ''
'Proposed' = ''
'Additional Info' = "Time Diff: $($result.TimeDifference)"
}
}
$tableData += [PSCustomObject]@{
'Metric' = 'ETA'
'Current' = $result.CurrentArrivalTime
'Proposed' = $result.NewArrivalTime
'Additional Info' = ''
}
# Display table
$tableData | Format-Table -AutoSize
# Display recommendation
Write-Host " RECOMMENDATION: $($result.Recommendation)" -ForegroundColor $(if ($timeSaved -gt 0) { "Green" } elseif ($timeSaved -lt 0) { "Red" } else { "Yellow" })
Write-Host ""
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment