Add region-based filtering and route length diversity

- Add RegionMapSelector UI for geographic trip filtering (East/Central/West)
- Add RouteFilters module for allowRepeatCities preference
- Improve GameDAGRouter to preserve route length diversity
  - Routes now grouped by city count before scoring
  - Ensures 2-city trips appear alongside longer trips
  - Increased beam width and max options for better coverage
- Add TripOptionsView filters (max cities slider, pace filter)
- Remove TravelStyle section from trip creation (replaced by region selector)
- Clean up debug logging from DataProvider and ScenarioAPlanner

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-09 15:18:37 -06:00
parent 3e778473e6
commit f5e509a9ae
20 changed files with 952 additions and 3245 deletions

View File

@@ -35,6 +35,7 @@ struct PlanningFailure: Error {
case travelSegmentMissing
case constraintsUnsatisfiable
case geographicBacktracking
case repeatCityViolation(cities: [String])
static func == (lhs: FailureReason, rhs: FailureReason) -> Bool {
switch (lhs, rhs) {
@@ -50,6 +51,8 @@ struct PlanningFailure: Error {
return true
case (.dateRangeViolation(let g1), .dateRangeViolation(let g2)):
return g1.map { $0.id } == g2.map { $0.id }
case (.repeatCityViolation(let c1), .repeatCityViolation(let c2)):
return c1 == c2
default:
return false
}
@@ -74,6 +77,10 @@ struct PlanningFailure: Error {
case .travelSegmentMissing: return "Travel segment could not be created"
case .constraintsUnsatisfiable: return "Cannot satisfy all trip constraints"
case .geographicBacktracking: return "Route requires excessive backtracking"
case .repeatCityViolation(let cities):
let cityList = cities.prefix(3).joined(separator: ", ")
let suffix = cities.count > 3 ? " and \(cities.count - 3) more" : ""
return "Cannot visit cities on multiple days: \(cityList)\(suffix)"
}
}
}
@@ -182,8 +189,7 @@ struct ItineraryOption: Identifiable {
/// - Parameters:
/// - options: The itinerary options to sort
/// - leisureLevel: The user's leisure preference
/// - limit: Maximum number of options to return (default 10)
/// - Returns: Sorted and ranked options
/// - Returns: Sorted and ranked options (all options, no limit)
///
/// Sorting behavior:
/// - Packed: Most games first, then least driving
@@ -191,8 +197,7 @@ struct ItineraryOption: Identifiable {
/// - Relaxed: Least driving first, then fewer games
static func sortByLeisure(
_ options: [ItineraryOption],
leisureLevel: LeisureLevel,
limit: Int = 10
leisureLevel: LeisureLevel
) -> [ItineraryOption] {
let sorted = options.sorted { a, b in
let aGames = a.totalGames
@@ -220,8 +225,8 @@ struct ItineraryOption: Identifiable {
}
}
// Re-rank after sorting
return Array(sorted.prefix(limit)).enumerated().map { index, option in
// Re-rank after sorting (no limit - return all options)
return sorted.enumerated().map { index, option in
ItineraryOption(
rank: index + 1,
stops: option.stops,