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:
@@ -82,8 +82,13 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
|
||||
controller.setTableHeader(hostingController.view)
|
||||
|
||||
// Load initial data
|
||||
let (days, validRanges, allItemsForConstraints) = buildItineraryData()
|
||||
controller.reloadData(days: days, travelValidRanges: validRanges, itineraryItems: allItemsForConstraints)
|
||||
let (days, validRanges, allItemsForConstraints, travelSegmentIndices) = buildItineraryData()
|
||||
controller.reloadData(
|
||||
days: days,
|
||||
travelValidRanges: validRanges,
|
||||
itineraryItems: allItemsForConstraints,
|
||||
travelSegmentIndices: travelSegmentIndices
|
||||
)
|
||||
|
||||
return controller
|
||||
}
|
||||
@@ -100,15 +105,21 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
|
||||
// This avoids recreating the view hierarchy and prevents infinite loops
|
||||
context.coordinator.headerHostingController?.rootView = headerContent
|
||||
|
||||
let (days, validRanges, allItemsForConstraints) = buildItineraryData()
|
||||
controller.reloadData(days: days, travelValidRanges: validRanges, itineraryItems: allItemsForConstraints)
|
||||
let (days, validRanges, allItemsForConstraints, travelSegmentIndices) = buildItineraryData()
|
||||
controller.reloadData(
|
||||
days: days,
|
||||
travelValidRanges: validRanges,
|
||||
itineraryItems: allItemsForConstraints,
|
||||
travelSegmentIndices: travelSegmentIndices
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Build Itinerary Data
|
||||
|
||||
private func buildItineraryData() -> ([ItineraryDayData], [String: ClosedRange<Int>], [ItineraryItem]) {
|
||||
private func buildItineraryData() -> ([ItineraryDayData], [String: ClosedRange<Int>], [ItineraryItem], [UUID: Int]) {
|
||||
let tripDays = calculateTripDays()
|
||||
var travelValidRanges: [String: ClosedRange<Int>] = [:]
|
||||
let travelSegmentIndices = Dictionary(uniqueKeysWithValues: trip.travelSegments.enumerated().map { ($1.id, $0) })
|
||||
|
||||
// Build game items from RichGame data for constraint validation
|
||||
var gameItems: [ItineraryItem] = []
|
||||
@@ -183,13 +194,7 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
|
||||
day: placement.day,
|
||||
sortOrder: placement.sortOrder,
|
||||
kind: .travel(
|
||||
TravelInfo(
|
||||
fromCity: fromCity,
|
||||
toCity: toCity,
|
||||
segmentIndex: segmentIndex,
|
||||
distanceMeters: segment.distanceMeters,
|
||||
durationSeconds: segment.durationSeconds
|
||||
)
|
||||
TravelInfo(segment: segment, segmentIndex: segmentIndex)
|
||||
)
|
||||
)
|
||||
travelItems.append(travelItem)
|
||||
@@ -218,20 +223,13 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
|
||||
.filter { $0.day == dayNum }
|
||||
.sorted { $0.sortOrder < $1.sortOrder }
|
||||
for travel in travelsForDay {
|
||||
// Find the segment matching this travel by segment index (preferred) or city pair (legacy)
|
||||
if let info = travel.travelInfo {
|
||||
let seg: TravelSegment?
|
||||
if let idx = info.segmentIndex, idx < trip.travelSegments.count {
|
||||
seg = trip.travelSegments[idx]
|
||||
} else {
|
||||
seg = trip.travelSegments.first(where: {
|
||||
$0.fromLocation.name.lowercased() == info.fromCity.lowercased()
|
||||
&& $0.toLocation.name.lowercased() == info.toCity.lowercased()
|
||||
})
|
||||
}
|
||||
if let seg {
|
||||
rows.append(.travel(seg, dayNumber: dayNum))
|
||||
}
|
||||
guard let idx = info.segmentIndex,
|
||||
idx >= 0,
|
||||
idx < trip.travelSegments.count else { continue }
|
||||
let seg = trip.travelSegments[idx]
|
||||
guard info.matches(segment: seg) else { continue }
|
||||
rows.append(.travel(seg, dayNumber: dayNum))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +239,7 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
|
||||
switch r {
|
||||
case .customItem(let it): return it.sortOrder
|
||||
case .travel(let seg, _):
|
||||
let segIdx = trip.travelSegments.firstIndex(where: { $0.id == seg.id }) ?? 0
|
||||
let segIdx = travelSegmentIndices[seg.id] ?? 0
|
||||
let id = stableTravelAnchorId(seg, at: segIdx)
|
||||
return (travelOverrides[id]?.sortOrder)
|
||||
?? (travelItems.first(where: { ti in
|
||||
@@ -265,8 +263,8 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
|
||||
)
|
||||
days.append(dayData)
|
||||
}
|
||||
|
||||
return (days, travelValidRanges, gameItems + itineraryItems + travelItems)
|
||||
|
||||
return (days, travelValidRanges, gameItems + itineraryItems + travelItems, travelSegmentIndices)
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
@@ -294,7 +292,9 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
|
||||
}
|
||||
|
||||
private func stableTravelAnchorId(_ segment: TravelSegment, at index: Int) -> String {
|
||||
"travel:\(index):\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
||||
let from = TravelInfo.normalizeCityName(segment.fromLocation.name)
|
||||
let to = TravelInfo.normalizeCityName(segment.toLocation.name)
|
||||
return "travel:\(index):\(from)->\(to)"
|
||||
}
|
||||
|
||||
private func findLastGameDay(in city: String, tripDays: [Date]) -> Int {
|
||||
|
||||
Reference in New Issue
Block a user