fix(planning): enforce daily driving limit for same-day games and group itinerary by city

Two fixes for route planning and display:

1. GameDAGRouter: Same-day game transitions now respect maxDailyDrivingHours
   constraint. Previously, a 12:05 AM game in Arlington could connect to an
   11:40 PM game in Milwaukee (19+ hour drive) because the code only checked
   available time, not the 8-hour daily limit.

2. TripDetailView: Itinerary sections now group by (day, city) not just day.
   Games in different cities on the same calendar day are shown as separate
   sections with travel segments between them.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-15 09:42:08 -06:00
parent 57eab22746
commit 00a5e4ef0e
2 changed files with 29 additions and 16 deletions

View File

@@ -348,36 +348,47 @@ struct TripDetailView: View {
} }
} }
/// Build itinerary sections: days with travel between different cities /// Build itinerary sections: group by day AND city, with travel between different cities
private var itinerarySections: [ItinerarySection] { private var itinerarySections: [ItinerarySection] {
var sections: [ItinerarySection] = [] var sections: [ItinerarySection] = []
// Build day sections for days with games // Build day+city sections for days with games
var daySections: [(dayNumber: Int, date: Date, city: String, games: [RichGame])] = [] var dayCitySections: [(dayNumber: Int, date: Date, city: String, games: [RichGame])] = []
let days = tripDays let days = tripDays
for (index, dayDate) in days.enumerated() { for (index, dayDate) in days.enumerated() {
let dayNum = index + 1 let dayNum = index + 1
let gamesOnDay = gamesOn(date: dayDate) let gamesOnDay = gamesOn(date: dayDate)
// Get city from games (preferred) or from stops as fallback guard !gamesOnDay.isEmpty else { continue }
let cityForDay = gamesOnDay.first?.stadium.city ?? cityOn(date: dayDate) ?? ""
// Include days with games // Group games by city, maintaining chronological order
// Skip empty days at the end (departure day after last game) var gamesByCity: [(city: String, games: [RichGame])] = []
if !gamesOnDay.isEmpty { for game in gamesOnDay {
daySections.append((dayNum, dayDate, cityForDay, gamesOnDay)) let city = game.stadium.city
if let lastIndex = gamesByCity.indices.last, gamesByCity[lastIndex].city == city {
// Same city as previous game - add to existing group
gamesByCity[lastIndex].games.append(game)
} else {
// Different city - start new group
gamesByCity.append((city, [game]))
}
}
// Add each city group as a separate section
for cityGroup in gamesByCity {
dayCitySections.append((dayNum, dayDate, cityGroup.city, cityGroup.games))
} }
} }
// Build sections: insert travel BEFORE each day when coming from different city // Build sections: insert travel BEFORE each section when coming from different city
for (index, daySection) in daySections.enumerated() { for (index, section) in dayCitySections.enumerated() {
// Check if we need travel BEFORE this day (coming from different city) // Check if we need travel BEFORE this section (coming from different city)
if index > 0 { if index > 0 {
let prevSection = daySections[index - 1] let prevSection = dayCitySections[index - 1]
let prevCity = prevSection.city let prevCity = prevSection.city
let currentCity = daySection.city let currentCity = section.city
// If cities differ, find travel segment from prev -> current // If cities differ, find travel segment from prev -> current
if !prevCity.isEmpty && !currentCity.isEmpty && prevCity != currentCity { if !prevCity.isEmpty && !currentCity.isEmpty && prevCity != currentCity {
@@ -388,7 +399,7 @@ struct TripDetailView: View {
} }
// Add the day section // Add the day section
sections.append(.day(dayNumber: daySection.dayNumber, date: daySection.date, games: daySection.games)) sections.append(.day(dayNumber: section.dayNumber, date: section.date, games: section.games))
} }
return sections return sections

View File

@@ -542,8 +542,10 @@ enum GameDAGRouter {
let availableHours = deadline.timeIntervalSince(departureTime) / 3600.0 let availableHours = deadline.timeIntervalSince(departureTime) / 3600.0
// Calculate driving hours available // Calculate driving hours available
// For same-day games: enforce both time availability AND daily driving limit
// For multi-day trips: use total available driving hours across days
let maxDrivingHoursAvailable = daysBetween == 0 let maxDrivingHoursAvailable = daysBetween == 0
? max(0, availableHours) ? min(max(0, availableHours), constraints.maxDailyDrivingHours)
: Double(daysBetween) * constraints.maxDailyDrivingHours : Double(daysBetween) * constraints.maxDailyDrivingHours
let feasible = drivingHours <= maxDrivingHoursAvailable && drivingHours <= availableHours let feasible = drivingHours <= maxDrivingHoursAvailable && drivingHours <= availableHours