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:
Trey t
2026-02-13 08:55:23 -06:00
parent 1c97f35754
commit 9736773475
19 changed files with 928 additions and 171 deletions

View File

@@ -42,6 +42,51 @@ struct TravelInfo: Codable, Hashable {
var distanceMeters: Double?
var durationSeconds: Double?
init(
fromCity: String,
toCity: String,
segmentIndex: Int? = nil,
distanceMeters: Double? = nil,
durationSeconds: Double? = nil
) {
self.fromCity = fromCity.trimmingCharacters(in: .whitespacesAndNewlines)
self.toCity = toCity.trimmingCharacters(in: .whitespacesAndNewlines)
self.segmentIndex = segmentIndex
self.distanceMeters = distanceMeters
self.durationSeconds = durationSeconds
}
init(
segment: TravelSegment,
segmentIndex: Int? = nil,
distanceMeters: Double? = nil,
durationSeconds: Double? = nil
) {
self.init(
fromCity: segment.fromLocation.name,
toCity: segment.toLocation.name,
segmentIndex: segmentIndex,
distanceMeters: distanceMeters ?? segment.distanceMeters,
durationSeconds: durationSeconds ?? segment.durationSeconds
)
}
static func normalizeCityName(_ city: String) -> String {
city.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
}
var normalizedFromCity: String { Self.normalizeCityName(fromCity) }
var normalizedToCity: String { Self.normalizeCityName(toCity) }
func matches(from: String, to: String) -> Bool {
normalizedFromCity == Self.normalizeCityName(from)
&& normalizedToCity == Self.normalizeCityName(to)
}
func matches(segment: TravelSegment) -> Bool {
matches(from: segment.fromLocation.name, to: segment.toLocation.name)
}
var formattedDistance: String {
guard let meters = distanceMeters else { return "" }
let miles = meters / 1609.34