Public/Update-TCXFile.ps1
|
<#
.SYNOPSIS Updates a Garmin TCX activity file by recalculating and rewriting lap and trackpoint data. .DESCRIPTION `Update-TCXFile` loads a TCX activity, applies lap configuration values, and writes an updated TCX output file. The function supports two lap input modes: - Treadmill: Distance and ascent are calculated from lap duration, speed (km/h), and incline (%). - Stairmaster (or manual mode): Distance and ascent values are taken directly from configured lap entries. For each lap, the function: - Distributes lap distance and ascent across all trackpoints. - Rewrites `Trackpoint.DistanceMeters` and `Trackpoint.AltitudeMeters` as cumulative values. - Removes `Trackpoint.Position` nodes. - Updates lap-level `DistanceMeters`. An editable lap table is shown through `Edit-ActivityLapsInGrid` before applying updates. The modified activity is saved as a new file with `_updated` appended to the original file name. .PARAMETER tcxFile Path to the input `.tcx` file. If omitted, a file picker dialog is shown. .PARAMETER EnableLogging Optional switch to enable module logging behavior. .OUTPUTS None The function updates and saves a new TCX file on disk and writes status information to the host/output helpers. .NOTES - Requires helper functions in module scope: `Get-FunctionName`, `Write-Log`, `Invoke-Output`, `Get-RunTime`, `Edit-ActivityLapsInGrid`, `Get-DistanceInMeters`, `Get-HeightGain`. - The lap configuration count must match the number of laps in the TCX file. - Existing altitude, distance, and position values in processed trackpoints are overwritten/removed. - Output file naming pattern: `<originalName>_updated.tcx`. .EXAMPLE Update-TCXFile -tcxFile "C:\Data\workout.tcx" Loads the specified TCX file, opens lap editing, applies recalculated values, and saves `workout_updated.tcx` in the same directory. .EXAMPLE Update-TCXFile Opens a file picker dialog, then processes the selected TCX file. #> Function Update-TCXFile { [CmdletBinding()] param( [Parameter(Position = 0)] [ValidatePattern('(?i)\.tcx$')] [string]$tcxFile, [switch]$EnableLogging ) $CurrentFunction = Get-FunctionName Write-Log -Message "### Start Function $CurrentFunction ###" $StartRunTime = (Get-Date).ToString($Script:DateFormatLog) #################### main code | out- host ##################### Invoke-Output -Type Header -Message "Updating TCX file with new distance and elevation values ..." if (-not $tcxFile) { Add-Type -AssemblyName System.Windows.Forms $dialog = New-Object System.Windows.Forms.OpenFileDialog $dialog.Title = "Select TCX input file" $dialog.Filter = "TCX files (*.tcx)|*.tcx" $dialog.Multiselect = $false $dialog.CheckFileExists = $true $dialog.InitialDirectory = (Get-Location).Path $dialogResult = $dialog.ShowDialog() if ($dialogResult -ne [System.Windows.Forms.DialogResult]::OK -or [string]::IsNullOrWhiteSpace($dialog.FileName)) { #throw "No TCX file selected. Script canceled." Write-Host " [!] Canceled by user. No tcx file was selected." -ForegroundColor Yellow return } $tcxFile = $dialog.FileName } if (-not (Test-Path -LiteralPath $tcxFile -PathType Leaf)) { throw "TCX file not found: $tcxFile" } if ([System.IO.Path]::GetExtension($tcxFile) -notmatch '^(?i)\.tcx$') { throw "Only .tcx files are allowed: $tcxFile" } $tcxFile = (Resolve-Path -LiteralPath $tcxFile).Path $UpdatedtcxFile = $tcxFile.replace(".tcx", "_updated.tcx") <# Option A: Calculate Distance and Ascent based on Speed and Incline Incline in percent, e.g. 10.0 = 10% Speed in km/h, e.g. 8.0 = 8 km/h Option B: Use distance and ascent from cardio machine Distance in meters, e.g. 1000.0 = 1 kilometer Ascent in meters, e.g. 10.0 = 10 meters #> $ActivityLaps = ( [pscustomobject]@{Lap = "01"; Option = "Stairmaster"; Speed = "9.0"; Incline = "3.0"; Distance = "1100"; Ascent = "0.0"; Description = "Warm Up" }, [pscustomobject]@{Lap = "02"; Option = "Stairmaster"; Speed = "8.0"; Incline = "12.5"; Distance = "1400"; Ascent = "187"; Description = "Warm Up" }, [pscustomobject]@{Lap = "03"; Option = "Stairmaster"; Speed = "6.0"; Incline = "15.0"; Distance = "400"; Ascent = "0"; Description = "Exercise" }, [pscustomobject]@{Lap = "04"; Option = "Stairmaster"; Speed = "5.0"; Incline = "15.0"; Distance = "1350"; Ascent = "182"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "05"; Option = "Stairmaster"; Speed = "6.0"; Incline = "15.0"; Distance = "400"; Ascent = "0.0"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "06"; Option = "Stairmaster"; Speed = "5.0"; Incline = "15.0"; Distance = "1450.0"; Ascent = "190.0"; Description = "Einheit 3" }, [pscustomobject]@{Lap = "07"; Option = "Stairmaster"; Speed = "6.0"; Incline = "15.0"; Distance = "200"; Ascent = "0"; Description = "Einheit 3" }, [pscustomobject]@{Lap = "08"; Option = "Stairmaster"; Speed = "5.5"; Incline = "15.0"; Distance = "1000"; Ascent = "80"; Description = "Einheit 4" }, [pscustomobject]@{Lap = "09"; Option = "Stairmaster"; Speed = "6.0"; Incline = "15.0"; Distance = "145"; Ascent = "0"; Description = "Einheit 4" }, [pscustomobject]@{Lap = "10"; Option = "Stairmaster"; Speed = "5.0"; Incline = "15.0"; Distance = "800"; Ascent = "0"; Description = "Einheit 5" }, [pscustomobject]@{Lap = "11"; Option = "Treadmill"; Speed = "6.0"; Incline = "15.0"; Distance = "1100"; Ascent = "227"; Description = "Einheit 5" }, [pscustomobject]@{Lap = "12"; Option = "Treadmill"; Speed = "5.0"; Incline = "15.0"; Distance = "900"; Ascent = "185"; Description = "Einheit 6" }, [pscustomobject]@{Lap = "13"; Option = "Treadmill"; Speed = "6.0"; Incline = "15.0"; Distance = "650"; Ascent = "0"; Description = "Cool Down" }, [pscustomobject]@{Lap = "14"; Option = "Treadmill"; Speed = "5.0"; Incline = "15.0"; Distance = "1000.0"; Ascent = "-44.0"; Description = "Cool Down" }, [pscustomobject]@{Lap = "15"; Option = "Treadmill"; Speed = "6.0"; Incline = "15.0"; Distance = "1000.0"; Ascent = "-6"; Description = "Lap 15" }, [pscustomobject]@{Lap = "16"; Option = "Treadmill"; Speed = "4.5"; Incline = "15.0"; Distance = "640.0"; Ascent = "-12"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "17"; Option = "Treadmill"; Speed = "5.7"; Incline = "15.0"; Distance = "1300.0"; Ascent = "350"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "18"; Option = "Treadmill"; Speed = "3.0"; Incline = "3.0"; Distance = "140.0"; Ascent = "0"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "19"; Option = "Stairmaster"; Speed = "8.0"; Incline = "8.0"; Distance = "1200.0"; Ascent = "318"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "20"; Option = "Stairmaster"; Speed = "8.0"; Incline = "8.0"; Distance = "10"; Ascent = "0"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "21"; Option = "Stairmaster"; Speed = "8.0"; Incline = "8.0"; Distance = "25"; Ascent = "0"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "22"; Option = "Stairmaster"; Speed = "8.0"; Incline = "8.0"; Distance = "0"; Ascent = "0"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "23"; Option = "Stairmaster"; Speed = "8.0"; Incline = "8.0"; Distance = "270.0"; Ascent = "0"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "24"; Option = "Stairmaster"; Speed = "8.0"; Incline = "8.0"; Distance = "0"; Ascent = "0"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "25"; Option = "Stairmaster"; Speed = "8.0"; Incline = "8.0"; Distance = "0"; Ascent = "0"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "26"; Option = "Stairmaster"; Speed = "8.0"; Incline = "8.0"; Distance = "0"; Ascent = "0"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "27"; Option = "Stairmaster"; Speed = "8.0"; Incline = "8.0"; Distance = "0"; Ascent = "0"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "28"; Option = "Stairmaster"; Speed = "8.0"; Incline = "8.0"; Distance = "0"; Ascent = "0"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "29"; Option = "Stairmaster"; Speed = "8.0"; Incline = "8.0"; Distance = "0"; Ascent = "0"; Description = "Einheit 1" }, [pscustomobject]@{Lap = "30"; Option = "Stairmaster"; Speed = "8.0"; Incline = "8.0"; Distance = "0"; Ascent = "0"; Description = "Einheit 1" } ) # Check if the file exists if (-not (Test-Path $tcxFile)) { Write-Host "File not found: $tcxFile" -ForegroundColor Red exit } # load TCX-file [xml]$tcx = Get-Content $tcxFile # number of Laps $NoLaps = ($tcx.TrainingCenterDatabase.Activities.Activity.Lap).count invoke-output -type Bullet -Message "Number of Laps:" -TextMaker $NoLaps $allLaps = $tcx.TrainingCenterDatabase.Activities.Activity.Lap $allLaps | Select-Object TotalTimeSeconds, DistanceMeters, Intensity | Format-Table $editedLaps = Edit-ActivityLapsInGrid -InputLaps $ActivityLaps -LapNodes $allLaps -NumberOfLaps $NoLaps if ($null -eq $editedLaps) { Write-Host "Canceled by user. No TCX changes were written." -ForegroundColor Yellow return } $ActivityLaps = $editedLaps if ($ActivityLaps.Count -ne $NoLaps) { Write-Host "ActivityLaps count ($($ActivityLaps.Count)) does not match TCX lap count ($NoLaps)." -ForegroundColor Red return } $script:i = 1 $allLaps | Select-Object @{Name = 'Lab'; Expression = { $script:i; $script:i++ } }, StartTime, TotalTimeSeconds, DistanceMeters | Format-Table [double]$startAltitude = 0 [double]$Distance = 625.0 [double]$StartDistance = 0 $i = 0 foreach ($Lap in $allLaps) { If ($ActivityLaps[$i].Option -eq "Treadmill") { $ActivityLaps[$i].Distance = Get-DistanceInMeters -SpeedKmh $ActivityLaps[$i].Speed -TimeSeconds $Lap.TotalTimeSeconds $ActivityLaps[$i].Ascent = Get-HeightGain -DistanceMeters $ActivityLaps[$i].Distance -SlopePercentage $ActivityLaps[$i].Incline } $i += 1 } $ActivityLaps | Format-Table pause $i = 0 $previousCumulativeAltitude = 0 foreach ($Lap in $allLaps) { [double]$Distance = 0.0 [double]$Ascent = 0.0 [double]$CumulativeAltitude = 0.0 [double]$CumulativeDistance = 0.0 $NoTrackPoints = ($Lap.Track.Trackpoint).count $Ascent = $ActivityLaps[$i].Ascent / $NoTrackPoints $Ascent = [math]::Round($Ascent, 3) $Distance = $ActivityLaps[$i].Distance / $NoTrackPoints $Distance = [math]::Round($Distance, 3) $lab = $i + 1 write-host "[>] Updating Lap $lab`/$NoLaps | $($Lap.StartTime) with '$NoTrackPoints' TrackPoints ... " -ForegroundColor Yellow $CumulativeAltitude = $startAltitude $CumulativeDistance = $StartDistance foreach ($tp in $Lap.track.trackpoint) { if ($tp.AltitudeMeters -and $tp.AltitudeMeters -match '^\d+(\.\d+)?$') { $CumulativeAltitude += $Ascent $tp.AltitudeMeters = $CumulativeAltitude.ToString("F6", [System.Globalization.CultureInfo]::InvariantCulture) $CumulativeDistance += $Distance $tp.DistanceMeters = $CumulativeDistance.ToString("F6", [System.Globalization.CultureInfo]::InvariantCulture) $pos = $tp.Position if ($null -ne $pos) { $tp.RemoveChild($pos) | Out-Null } } } $temp = $ActivityLaps[$i].Distance $Lap.DistanceMeters = ([double]$temp).ToString("F6", [System.Globalization.CultureInfo]::InvariantCulture) $startAltitude = $CumulativeAltitude $StartDistance = $CumulativeDistance Write-Host " ... done added $([int]$Lap.DistanceMeters) meters and $($ActivityLaps[$i].Ascent) meters (ascent) [Total elevation: $([int]$CumulativeAltitude) m] to $($Lap.StartTime)" -ForegroundColor Cyan $i += 1 $previousCumulativeAltitude = $CumulativeAltitude } #$tcx.TrainingCenterDatabase.Activities.Activity.Id = [System.Guid]::NewGuid().ToString() $tcx.Save($UpdatedtcxFile) Invoke-output -Type Success -Message "The updated TCX file is now ready to upload to Garmin Connect and Strava." Invoke-output -Type Info -Message "Updated TCX file saved as: $UpdatedtcxFile" Write-Log -Message " >> using " ######################## main code ############################ $runtime = Get-RunTime -StartRunTime $StartRunTime Write-Log -Message " Run Time: $runtime [h] ###" Write-Log -Message "### End Function $CurrentFunction ###" } |