Files
Sportstime/SportsTime/Planning/Engine/ItineraryBuilder.swift
Trey t 9088b46563 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>
2026-01-07 00:46:40 -06:00

135 lines
5.8 KiB
Swift

//
// ItineraryBuilder.swift
// SportsTime
//
// Shared utility for building itineraries with travel segments.
// Used by all scenario planners to convert stops into complete itineraries.
//
import Foundation
/// Result of building an itinerary from stops.
struct BuiltItinerary {
let stops: [ItineraryStop]
let travelSegments: [TravelSegment]
let totalDrivingHours: Double
let totalDistanceMiles: Double
}
/// Shared logic for building itineraries across all scenario planners.
enum ItineraryBuilder {
/// Validation that can be performed on each travel segment.
/// Return `true` if the segment is valid, `false` to reject the itinerary.
typealias SegmentValidator = (TravelSegment, _ fromStop: ItineraryStop, _ toStop: ItineraryStop) -> Bool
/// Builds a complete itinerary with travel segments between consecutive stops.
///
/// Algorithm:
/// 1. Handle edge case: single stop = no travel needed
/// 2. For each consecutive pair of stops, estimate travel
/// 3. Optionally validate each segment with custom validator
/// 4. Accumulate driving hours and distance
/// 5. Verify invariant: travelSegments.count == stops.count - 1
///
/// - Parameters:
/// - stops: The stops to connect with travel segments
/// - constraints: Driving constraints (drivers, max hours per day)
/// - logPrefix: Prefix for log messages (e.g., "[ScenarioA]")
/// - segmentValidator: Optional validation for each segment
///
/// - Returns: Built itinerary if successful, nil if any segment fails
///
static func build(
stops: [ItineraryStop],
constraints: DrivingConstraints,
logPrefix: String = "[ItineraryBuilder]",
segmentValidator: SegmentValidator? = nil
) -> BuiltItinerary? {
//
// Edge case: Single stop or empty = no travel needed
//
if stops.count <= 1 {
return BuiltItinerary(
stops: stops,
travelSegments: [],
totalDrivingHours: 0,
totalDistanceMiles: 0
)
}
//
// Build travel segments between consecutive stops
//
var travelSegments: [TravelSegment] = []
var totalDrivingHours: Double = 0
var totalDistance: Double = 0
for index in 0..<(stops.count - 1) {
let fromStop = stops[index]
let toStop = stops[index + 1]
// Estimate travel for this segment
guard let segment = TravelEstimator.estimate(
from: fromStop,
to: toStop,
constraints: constraints
) else {
print("\(logPrefix) Failed to estimate travel: \(fromStop.city) -> \(toStop.city)")
return nil
}
// Run optional validator (e.g., arrival time check for Scenario B)
if let validator = segmentValidator {
if !validator(segment, fromStop, toStop) {
print("\(logPrefix) Segment validation failed: \(fromStop.city) -> \(toStop.city)")
return nil
}
}
travelSegments.append(segment)
totalDrivingHours += segment.estimatedDrivingHours
totalDistance += segment.estimatedDistanceMiles
}
//
// Verify invariant: segments = stops - 1
//
guard travelSegments.count == stops.count - 1 else {
print("\(logPrefix) Invariant violated: \(travelSegments.count) segments for \(stops.count) stops")
return nil
}
return BuiltItinerary(
stops: stops,
travelSegments: travelSegments,
totalDrivingHours: totalDrivingHours,
totalDistanceMiles: totalDistance
)
}
// MARK: - Common Validators
/// Validator that ensures arrival time is before game start (with buffer).
/// Used by Scenario B where selected games have fixed start times.
///
/// - Parameter bufferSeconds: Time buffer before game start (default 1 hour)
/// - Returns: Validator closure
///
static func arrivalBeforeGameStart(bufferSeconds: TimeInterval = 3600) -> SegmentValidator {
return { segment, _, toStop in
guard let gameStart = toStop.firstGameStart else {
return true // No game = no constraint
}
let deadline = gameStart.addingTimeInterval(-bufferSeconds)
if segment.arrivalTime > deadline {
print("[ItineraryBuilder] Cannot arrive in time: arrival \(segment.arrivalTime) > deadline \(deadline)")
return false
}
return true
}
}
}