Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions scripts/Build-Trips.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,17 @@ $reportsDir = if ($paths -and $paths.PSObject.Properties['Reports'] -and $paths.
$logFile = Join-Path $reportsDir "rename-log.csv"
$tripsOut = Join-Path $reportsDir "trips.csv"

$logsDir = if ($paths -and $paths.PSObject.Properties['Logs'] -and $paths.Logs) {
$paths.Logs
} else { Join-Path $PSScriptRoot ".." "logs" }
if (-not (Test-Path $logsDir)) { New-Item -ItemType Directory -Path $logsDir | Out-Null }
$transcriptPath = Join-Path $logsDir "build-trips-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
Start-Transcript -Path $transcriptPath -Append | Out-Null

Write-Information "[Build-Trips] Starting — reports: $reportsDir" -InformationAction Continue

try {

$configErrors = @()
if (-not $Folder) { $configErrors += "settings.json: 'Paths.Source' (or 'Folder') is required" }
if (-not (Test-Path $logFile)) { $configErrors += "rename-log.csv not found (run Rename-Photos.ps1 first): $logFile" }
Expand Down Expand Up @@ -186,6 +197,7 @@ foreach ($row in $rawLog) {

$odom = 0
[int]::TryParse($row.Odometer, [ref]$odom) | Out-Null
Write-Verbose " Parsed entry: $($row.NewFile) | $dt | $($row.Location) | odom=$odom | conf=$($row.OdometerConfidence)"

$entries += [PSCustomObject]@{
File = $row.NewFile
Expand All @@ -199,7 +211,7 @@ foreach ($row in $rawLog) {
# Sort by timestamp
$entries = $entries | Sort-Object DateTime

Write-Host "Loaded $($entries.Count) log entries. Building trips..."
Write-Information "Loaded $($entries.Count) log entries. Building trips..." -InformationAction Continue

# ---------------------------------------------------------------------------
# Deduplication pre-pass
Expand All @@ -217,7 +229,8 @@ foreach ($entry in $entries) {
$sameOdom = $entry.Odometer -eq $prevEntry.Odometer

if ($sameLocation -and $sameOdom -and $gap -lt $DuplicateWindowSeconds) {
Write-Host " Deduplicating $($entry.File) (duplicate of $($prevEntry.File), $([int]$gap)s gap)"
Write-Verbose " Dedup check: same=$sameLocation/$sameOdom gap=$([int]$gap)s — removing $($entry.File)"
Write-Information " Deduplicating $($entry.File) (duplicate of $($prevEntry.File), $([int]$gap)s gap)" -InformationAction Continue
$dupCount++
continue
}
Expand All @@ -227,7 +240,7 @@ foreach ($entry in $entries) {
}

if ($dupCount -gt 0) {
Write-Host " Removed $dupCount duplicate(s). Proceeding with $($deduped.Count) entries."
Write-Information " Removed $dupCount duplicate(s). Proceeding with $($deduped.Count) entries." -InformationAction Continue
}

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -283,6 +296,7 @@ foreach ($entry in $deduped) {
# --- Different locations: this is a trip --------------------------------
$odomDelta = $entry.Odometer - $pending.Odometer
$expectedDist = Get-ExpectedDistance $pending.Location $entry.Location $locationMap $RoadFactor
Write-Verbose " Pairing: '$($pending.Location)' -> '$($entry.Location)' | odomDelta=$odomDelta | expectedDist=$expectedDist"

$ocrBad = ($pending.OdometerConfidence -ne "ok") -or ($entry.OdometerConfidence -ne "ok")
$unknownLoc = ($pending.Location -eq "Unknown") -or ($entry.Location -eq "Unknown")
Expand Down Expand Up @@ -318,6 +332,7 @@ foreach ($entry in $deduped) {

if ($expectedDist -gt 0 -and -not $ocrBad -and -not $unknownLoc) {
$deviation = [Math]::Abs($odomDelta - $expectedDist) / $expectedDist
Write-Verbose " Distance check: deviation=$([Math]::Round($deviation*100,1))% tolerance=$([Math]::Round($TolerancePct*100,0))%"
if ($deviation -le $TolerancePct) {
$status = "auto"
}
Expand All @@ -328,6 +343,7 @@ foreach ($entry in $deduped) {
elseif ($expectedDist -lt 0) {
$notes += "no route data for this location pair"
}
Write-Verbose " Trip status: $status$(if ($notes) { ' — ' + ($notes -join '; ') })"

$trips += [PSCustomObject]@{
Date = Format-TripDate $pending.DateTime
Expand Down Expand Up @@ -376,18 +392,24 @@ $review = ($trips | Where-Object { $_.Status -eq "review" }).Count
$unpaired = ($trips | Where-Object { $_.Status -eq "unpaired" }).Count
$stops = ($trips | Where-Object { $_.Status -eq "stop" }).Count

Write-Host " Auto-matched : $auto"
Write-Host " Needs review : $review"
Write-Host " Unpaired : $unpaired"
Write-Host " Stops (ref) : $stops"
Write-Information " Auto-matched : $auto" -InformationAction Continue
Write-Information " Needs review : $review" -InformationAction Continue
Write-Information " Unpaired : $unpaired" -InformationAction Continue
Write-Information " Stops (ref) : $stops" -InformationAction Continue

# Export CSV
if ($PSCmdlet.ShouldProcess($tripsOut, "Write trips CSV")) {
$trips | Export-Csv -Path $tripsOut -NoTypeInformation -Encoding utf8
Write-Host ""
Write-Host "Trips written to: $tripsOut"
Write-Information "" -InformationAction Continue
Write-Information "Trips written to: $tripsOut" -InformationAction Continue
if ($review -gt 0 -or $unpaired -gt 0) {
Write-Host ""
Write-Host "Open trips.csv and manually complete rows where Status = 'review' or 'unpaired'."
Write-Information "" -InformationAction Continue
Write-Information "Open trips.csv and manually complete rows where Status = 'review' or 'unpaired'." -InformationAction Continue
}
}

Write-Information "[Build-Trips] Done." -InformationAction Continue

} finally {
Stop-Transcript | Out-Null
}
45 changes: 34 additions & 11 deletions scripts/Rename-Photos.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,17 @@ $reportsDir = if ($paths -and $paths.PSObject.Properties['Reports'] -and $paths.
} else { Join-Path $PSScriptRoot ".." "logs" }
$logFile = Join-Path $reportsDir "rename-log.json"

$logsDir = if ($paths -and $paths.PSObject.Properties['Logs'] -and $paths.Logs) {
$paths.Logs
} else { Join-Path $PSScriptRoot ".." "logs" }
if (-not (Test-Path $logsDir)) { New-Item -ItemType Directory -Path $logsDir | Out-Null }
$transcriptPath = Join-Path $logsDir "rename-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
Start-Transcript -Path $transcriptPath -Append | Out-Null

Write-Information "[Rename-Photos] Starting — source: $Folder" -InformationAction Continue

try {

$logEntries = @()
$lastOdometer = $null
$gapSinceLastGood = 0
Expand All @@ -254,23 +265,24 @@ if (Test-Path $logFile) {

$photos = Get-ChildItem -Path $Folder -Filter "IMG_*.jpg"
if ($photos.Count -eq 0) {
Write-Host "No IMG_*.jpg files found in $Folder"
Write-Information "No IMG_*.jpg files found in $Folder" -InformationAction Continue
exit 0
}

foreach ($file in $photos) {

# Skip already-renamed files (pattern: yyMMdd-hhmm ...)
if ($file.Name -match '^\d{6}-\d{4} ') {
Write-Host "Skipping already renamed: $($file.Name)"
Write-Warning " Already renamed, skipping: $($file.Name)"
continue
}

Write-Host "Processing $($file.Name)..."
Write-Information "Processing $($file.Name)..." -InformationAction Continue

# --- EXIF: date/time and GPS ----------------------------------------
$exifOut = & $ExifToolPath -s3 -DateTimeOriginal -GPSLatitude# -GPSLongitude# "$($file.FullName)" 2>&1
$exifLines = @($exifOut | Where-Object { $_ -match '\S' })
Write-Verbose " EXIF raw output ($($exifLines.Count) lines): $($exifLines -join ' | ')"

if ($exifLines.Count -lt 1 -or $exifLines[0] -notmatch '^\d{4}:\d{2}:\d{2} \d{2}:\d{2}:\d{2}') {
Write-Warning " No DateTimeOriginal - skipping $($file.Name)"
Expand All @@ -279,6 +291,7 @@ foreach ($file in $photos) {
}

$dateTimeRaw = $exifLines[0].Trim() # e.g. "2026:03:01 14:32:15"
Write-Verbose " EXIF DateTimeOriginal: $dateTimeRaw"
if ($dateTimeRaw -match '^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2})') {
$yy = $matches[1].Substring(2, 2)
$mm = $matches[2]
Expand All @@ -301,12 +314,15 @@ foreach ($file in $photos) {
if ($exifLines.Count -ge 3) {
$latStr = $exifLines[1].Trim()
$lonStr = $exifLines[2].Trim()
Write-Verbose " GPS raw: lat=$latStr lon=$lonStr"
if ($latStr -match '^-?\d+(\.\d+)?$' -and $lonStr -match '^-?\d+(\.\d+)?$') {
$gpsLat = $latStr
$gpsLon = $lonStr
$match = Get-NearestLocation -Lat ([double]$latStr) -Lon ([double]$lonStr) `
-Locations $locations -ThresholdMiles $ProximityThresholdMiles
if ($match) {
$matchDist = Get-HaversineDistance ([double]$latStr) ([double]$lonStr) $match.lat $match.lon
Write-Verbose " GPS matched '$($match.name)' at $([Math]::Round($matchDist,3)) mi"
$locationName = $match.name
}
else {
Expand All @@ -327,9 +343,11 @@ foreach ($file in $photos) {

# --- OCR: odometer reading ------------------------------------------
$ocr = Get-OdometerReading -ImagePath $file.FullName
Write-Verbose " OCR result: reading=$($ocr.Reading) confidence=$($ocr.Confidence)"
if ($ocr.Confidence -eq 'ok') {
$reading = [int]$ocr.Reading
$maxDelta = ($gapSinceLastGood + 1) * $MaxTripMiles
Write-Verbose " Odometer check: read=$reading prev=$lastOdometer maxDelta=$maxDelta"
if ($null -ne $lastOdometer -and ($reading -lt $lastOdometer -or $reading -gt ($lastOdometer + $maxDelta))) {
Write-Warning " Odometer suspect: read $reading, previous was $lastOdometer (max delta $maxDelta mi)"
$ocr = @{ Reading = "00000"; Confidence = "suspect:got=$reading,prev=$lastOdometer" }
Expand Down Expand Up @@ -362,7 +380,7 @@ foreach ($file in $photos) {
# --- Rename and log -------------------------------------------------
if ($PSCmdlet.ShouldProcess($file.FullName, "Rename to $newName")) {
Rename-Item -Path $file.FullName -NewName $newName
Write-Host " Renamed -> $newName"
Write-Information " Renamed -> $newName" -InformationAction Continue
$renamedCount++

$logEntries += [PSCustomObject]@{
Expand All @@ -380,14 +398,19 @@ foreach ($file in $photos) {
}

$processedCount = @($photos | Where-Object { $_.Name -notmatch '^\d{6}-\d{4} ' }).Count
Write-Host ""
Write-Host "--- Summary ---"
Write-Host " Processed : $processedCount"
Write-Host " Renamed : $renamedCount"
Write-Host " Skipped : $($skipped.Count)"
Write-Information "" -InformationAction Continue
Write-Information "--- Summary ---" -InformationAction Continue
Write-Information " Processed : $processedCount" -InformationAction Continue
Write-Information " Renamed : $renamedCount" -InformationAction Continue
Write-Information " Skipped : $($skipped.Count)" -InformationAction Continue
if ($skipped.Count -gt 0) {
foreach ($s in $skipped) {
Write-Host " - $($s.File): $($s.Reason)"
Write-Information " - $($s.File): $($s.Reason)" -InformationAction Continue
}
}
Write-Host " Log : $logFile"
Write-Information " Log : $logFile" -InformationAction Continue
Write-Information "[Rename-Photos] Done." -InformationAction Continue

} finally {
Stop-Transcript | Out-Null
}
Loading