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:
134
SportsTime/Planning/Engine/ItineraryBuilder.swift
Normal file
134
SportsTime/Planning/Engine/ItineraryBuilder.swift
Normal file
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user