Initial commit: SportsTime trip planning app
- Three-scenario planning engine (A: date range, B: selected games, C: directional routes) - GeographicRouteExplorer with anchor game support for route exploration - Shared ItineraryBuilder for travel segment calculation - TravelEstimator for driving time/distance estimation - SwiftUI views for trip creation and detail display - CloudKit integration for schedule data - Python scraping scripts for sports schedules 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
232
SportsTime/Planning/Engine/RouteCandidateBuilder.swift
Normal file
232
SportsTime/Planning/Engine/RouteCandidateBuilder.swift
Normal file
@@ -0,0 +1,232 @@
|
||||
//
|
||||
// RouteCandidateBuilder.swift
|
||||
// SportsTime
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
/// Builds route candidates for different planning scenarios
|
||||
enum RouteCandidateBuilder {
|
||||
|
||||
// MARK: - Scenario A: Linear Candidates (Date Range)
|
||||
|
||||
/// Builds linear route candidates from games sorted chronologically
|
||||
/// - Parameters:
|
||||
/// - games: Available games sorted by start time
|
||||
/// - mustStop: Optional must-stop location
|
||||
/// - Returns: Array of route candidates
|
||||
static func buildLinearCandidates(
|
||||
games: [Game],
|
||||
stadiums: [UUID: Stadium],
|
||||
mustStop: LocationInput?
|
||||
) -> [RouteCandidate] {
|
||||
guard !games.isEmpty else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Group games by stadium
|
||||
var stadiumGames: [UUID: [Game]] = [:]
|
||||
for game in games {
|
||||
stadiumGames[game.stadiumId, default: []].append(game)
|
||||
}
|
||||
|
||||
// Build stops from chronological game order
|
||||
var stops: [ItineraryStop] = []
|
||||
var processedStadiums: Set<UUID> = []
|
||||
|
||||
for game in games {
|
||||
guard !processedStadiums.contains(game.stadiumId) else { continue }
|
||||
processedStadiums.insert(game.stadiumId)
|
||||
|
||||
let gamesAtStop = stadiumGames[game.stadiumId] ?? [game]
|
||||
let sortedGames = gamesAtStop.sorted { $0.startTime < $1.startTime }
|
||||
|
||||
// Look up stadium for coordinates and city info
|
||||
let stadium = stadiums[game.stadiumId]
|
||||
let city = stadium?.city ?? "Unknown"
|
||||
let state = stadium?.state ?? ""
|
||||
let coordinate = stadium?.coordinate
|
||||
|
||||
let location = LocationInput(
|
||||
name: city,
|
||||
coordinate: coordinate,
|
||||
address: stadium?.fullAddress
|
||||
)
|
||||
|
||||
let stop = ItineraryStop(
|
||||
city: city,
|
||||
state: state,
|
||||
coordinate: coordinate,
|
||||
games: sortedGames.map { $0.id },
|
||||
arrivalDate: sortedGames.first?.gameDate ?? Date(),
|
||||
departureDate: sortedGames.last?.gameDate ?? Date(),
|
||||
location: location,
|
||||
firstGameStart: sortedGames.first?.startTime
|
||||
)
|
||||
stops.append(stop)
|
||||
}
|
||||
|
||||
guard !stops.isEmpty else {
|
||||
return []
|
||||
}
|
||||
|
||||
return [RouteCandidate(
|
||||
stops: stops,
|
||||
rationale: "Linear route through \(stops.count) cities"
|
||||
)]
|
||||
}
|
||||
|
||||
// MARK: - Scenario B: Expand Around Anchors (Selected Games)
|
||||
|
||||
/// Expands route around user-selected anchor games
|
||||
/// - Parameters:
|
||||
/// - anchors: User-selected games (must-see)
|
||||
/// - allGames: All available games
|
||||
/// - dateRange: Trip date range
|
||||
/// - mustStop: Optional must-stop location
|
||||
/// - Returns: Array of route candidates
|
||||
static func expandAroundAnchors(
|
||||
anchors: [Game],
|
||||
allGames: [Game],
|
||||
stadiums: [UUID: Stadium],
|
||||
dateRange: DateInterval,
|
||||
mustStop: LocationInput?
|
||||
) -> [RouteCandidate] {
|
||||
guard !anchors.isEmpty else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Start with anchor games as the core route
|
||||
let sortedAnchors = anchors.sorted { $0.startTime < $1.startTime }
|
||||
|
||||
// Build stops from anchor games
|
||||
var stops: [ItineraryStop] = []
|
||||
|
||||
for game in sortedAnchors {
|
||||
let stadium = stadiums[game.stadiumId]
|
||||
let city = stadium?.city ?? "Unknown"
|
||||
let state = stadium?.state ?? ""
|
||||
let coordinate = stadium?.coordinate
|
||||
|
||||
let location = LocationInput(
|
||||
name: city,
|
||||
coordinate: coordinate,
|
||||
address: stadium?.fullAddress
|
||||
)
|
||||
|
||||
let stop = ItineraryStop(
|
||||
city: city,
|
||||
state: state,
|
||||
coordinate: coordinate,
|
||||
games: [game.id],
|
||||
arrivalDate: game.gameDate,
|
||||
departureDate: game.gameDate,
|
||||
location: location,
|
||||
firstGameStart: game.startTime
|
||||
)
|
||||
stops.append(stop)
|
||||
}
|
||||
|
||||
guard !stops.isEmpty else {
|
||||
return []
|
||||
}
|
||||
|
||||
return [RouteCandidate(
|
||||
stops: stops,
|
||||
rationale: "Route connecting \(anchors.count) selected games"
|
||||
)]
|
||||
}
|
||||
|
||||
// MARK: - Scenario C: Directional Routes (Start + End)
|
||||
|
||||
/// Builds directional routes from start to end location
|
||||
/// - Parameters:
|
||||
/// - start: Start location
|
||||
/// - end: End location
|
||||
/// - games: Available games
|
||||
/// - dateRange: Optional trip date range
|
||||
/// - Returns: Array of route candidates
|
||||
static func buildDirectionalRoutes(
|
||||
start: LocationInput,
|
||||
end: LocationInput,
|
||||
games: [Game],
|
||||
stadiums: [UUID: Stadium],
|
||||
dateRange: DateInterval?
|
||||
) -> [RouteCandidate] {
|
||||
// Filter games by date range if provided
|
||||
let filteredGames: [Game]
|
||||
if let range = dateRange {
|
||||
filteredGames = games.filter { range.contains($0.startTime) }
|
||||
} else {
|
||||
filteredGames = games
|
||||
}
|
||||
|
||||
guard !filteredGames.isEmpty else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Sort games chronologically
|
||||
let sortedGames = filteredGames.sorted { $0.startTime < $1.startTime }
|
||||
|
||||
// Build stops: start -> games -> end
|
||||
var stops: [ItineraryStop] = []
|
||||
|
||||
// Start stop (no games)
|
||||
let startStop = ItineraryStop(
|
||||
city: start.name,
|
||||
state: "",
|
||||
coordinate: start.coordinate,
|
||||
games: [],
|
||||
arrivalDate: sortedGames.first?.gameDate.addingTimeInterval(-86400) ?? Date(),
|
||||
departureDate: sortedGames.first?.gameDate.addingTimeInterval(-86400) ?? Date(),
|
||||
location: start,
|
||||
firstGameStart: nil
|
||||
)
|
||||
stops.append(startStop)
|
||||
|
||||
// Game stops
|
||||
for game in sortedGames {
|
||||
let stadium = stadiums[game.stadiumId]
|
||||
let city = stadium?.city ?? "Unknown"
|
||||
let state = stadium?.state ?? ""
|
||||
let coordinate = stadium?.coordinate
|
||||
|
||||
let location = LocationInput(
|
||||
name: city,
|
||||
coordinate: coordinate,
|
||||
address: stadium?.fullAddress
|
||||
)
|
||||
|
||||
let stop = ItineraryStop(
|
||||
city: city,
|
||||
state: state,
|
||||
coordinate: coordinate,
|
||||
games: [game.id],
|
||||
arrivalDate: game.gameDate,
|
||||
departureDate: game.gameDate,
|
||||
location: location,
|
||||
firstGameStart: game.startTime
|
||||
)
|
||||
stops.append(stop)
|
||||
}
|
||||
|
||||
// End stop (no games)
|
||||
let endStop = ItineraryStop(
|
||||
city: end.name,
|
||||
state: "",
|
||||
coordinate: end.coordinate,
|
||||
games: [],
|
||||
arrivalDate: sortedGames.last?.gameDate.addingTimeInterval(86400) ?? Date(),
|
||||
departureDate: sortedGames.last?.gameDate.addingTimeInterval(86400) ?? Date(),
|
||||
location: end,
|
||||
firstGameStart: nil
|
||||
)
|
||||
stops.append(endStop)
|
||||
|
||||
return [RouteCandidate(
|
||||
stops: stops,
|
||||
rationale: "Directional route from \(start.name) to \(end.name)"
|
||||
)]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user