feat: improve planning engine travel handling, itinerary reordering, and scenario planners
Add TravelInfo initializers and city normalization helpers to fix repeat city-pair disambiguation. Improve drag-and-drop reordering with segment index tracking and source-row-aware zone calculation. Enhance all five scenario planners with better next-day departure handling and travel segment placement. Add comprehensive tests across all planners. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,7 @@ import CoreLocation
|
||||
/// - date_range: Required. The trip dates.
|
||||
/// - selectedRegions: Optional. Filter to specific regions.
|
||||
/// - useHomeLocation: Whether to start/end from user's home.
|
||||
/// - startLocation: Required if useHomeLocation is true.
|
||||
/// - startLocation: Used as start/end home stop when provided.
|
||||
///
|
||||
/// Output:
|
||||
/// - Success: Ranked list of itinerary options
|
||||
@@ -173,6 +173,11 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// Step 5: Prepare for routing
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
let homeLocation: LocationInput? = {
|
||||
guard request.preferences.useHomeLocation else { return nil }
|
||||
return request.startLocation
|
||||
}()
|
||||
|
||||
// NOTE: We do NOT filter by repeat city here. The GameDAGRouter handles
|
||||
// allowRepeatCities internally, which allows it to pick the optimal game
|
||||
// per city for route feasibility (e.g., pick July 29 Anaheim instead of
|
||||
@@ -232,10 +237,13 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
||||
|
||||
for (index, routeGames) in validRoutes.enumerated() {
|
||||
// Build stops for this route
|
||||
let stops = buildStops(from: routeGames, stadiums: request.stadiums)
|
||||
|
||||
var stops = buildStops(from: routeGames, stadiums: request.stadiums)
|
||||
guard !stops.isEmpty else { continue }
|
||||
|
||||
if let homeLocation {
|
||||
stops = buildStopsWithHomeEndpoints(home: homeLocation, gameStops: stops)
|
||||
}
|
||||
|
||||
// Calculate travel segments using shared ItineraryBuilder
|
||||
guard let itinerary = ItineraryBuilder.build(
|
||||
stops: stops,
|
||||
@@ -401,6 +409,45 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
||||
)
|
||||
}
|
||||
|
||||
/// Wraps game stops with optional home start/end waypoints.
|
||||
private func buildStopsWithHomeEndpoints(
|
||||
home: LocationInput,
|
||||
gameStops: [ItineraryStop]
|
||||
) -> [ItineraryStop] {
|
||||
guard !gameStops.isEmpty else { return [] }
|
||||
|
||||
let calendar = Calendar.current
|
||||
let firstGameDay = gameStops.first?.arrivalDate ?? Date()
|
||||
let startDay = calendar.date(byAdding: .day, value: -1, to: firstGameDay) ?? firstGameDay
|
||||
|
||||
let startStop = ItineraryStop(
|
||||
city: home.name,
|
||||
state: "",
|
||||
coordinate: home.coordinate,
|
||||
games: [],
|
||||
arrivalDate: startDay,
|
||||
departureDate: startDay,
|
||||
location: home,
|
||||
firstGameStart: nil
|
||||
)
|
||||
|
||||
let lastGameDay = gameStops.last?.departureDate ?? firstGameDay
|
||||
let endDay = calendar.date(byAdding: .day, value: 1, to: lastGameDay) ?? lastGameDay
|
||||
|
||||
let endStop = ItineraryStop(
|
||||
city: home.name,
|
||||
state: "",
|
||||
coordinate: home.coordinate,
|
||||
games: [],
|
||||
arrivalDate: endDay,
|
||||
departureDate: endDay,
|
||||
location: home,
|
||||
firstGameStart: nil
|
||||
)
|
||||
|
||||
return [startStop] + gameStops + [endStop]
|
||||
}
|
||||
|
||||
// MARK: - Route Deduplication
|
||||
|
||||
/// Removes duplicate routes (routes with identical game IDs).
|
||||
|
||||
Reference in New Issue
Block a user