Remove debug prints and fix build warnings

- Remove all print statements from planning engine, data providers, and PDF generation
- Fix deprecated CLGeocoder usage with MKLocalSearch for iOS 26
- Fix Swift 6 actor isolation by converting PDFGenerator/ExportService to @MainActor
- Add @retroactive to CLLocationCoordinate2D protocol conformances
- Fix unused variable warnings in GameDAGRouter and scenario planners
- Remove unreachable catch blocks in SettingsViewModel

🤖 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-08 13:25:27 -06:00
parent fbb5ae683e
commit 045fcd9c07
20 changed files with 188 additions and 303 deletions

View File

@@ -176,7 +176,7 @@ struct LocationInput: Codable, Hashable {
var isResolved: Bool { coordinate != nil } var isResolved: Bool { coordinate != nil }
} }
extension CLLocationCoordinate2D: Codable, Hashable { extension CLLocationCoordinate2D: @retroactive Codable, @retroactive Hashable, @retroactive Equatable {
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case latitude, longitude case latitude, longitude
} }

View File

@@ -39,11 +39,9 @@ final class AppDataProvider: ObservableObject {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
self.provider = StubDataProvider() self.provider = StubDataProvider()
self.isUsingStubData = true self.isUsingStubData = true
print("📱 Using StubDataProvider (Simulator)")
#else #else
self.provider = CloudKitDataProvider() self.provider = CloudKitDataProvider()
self.isUsingStubData = false self.isUsingStubData = false
print("☁️ Using CloudKitDataProvider (Device)")
#endif #endif
} }
@@ -66,16 +64,12 @@ final class AppDataProvider: ObservableObject {
// Build lookup dictionaries // Build lookup dictionaries
self.teamsById = Dictionary(uniqueKeysWithValues: loadedTeams.map { ($0.id, $0) }) self.teamsById = Dictionary(uniqueKeysWithValues: loadedTeams.map { ($0.id, $0) })
self.stadiumsById = Dictionary(uniqueKeysWithValues: loadedStadiums.map { ($0.id, $0) }) self.stadiumsById = Dictionary(uniqueKeysWithValues: loadedStadiums.map { ($0.id, $0) })
print("✅ Loaded \(teams.count) teams, \(stadiums.count) stadiums")
} catch let cloudKitError as CloudKitError { } catch let cloudKitError as CloudKitError {
self.error = cloudKitError self.error = cloudKitError
self.errorMessage = cloudKitError.errorDescription self.errorMessage = cloudKitError.errorDescription
print("❌ CloudKit error: \(cloudKitError.errorDescription ?? "Unknown")")
} catch { } catch {
self.error = error self.error = error
self.errorMessage = error.localizedDescription self.errorMessage = error.localizedDescription
print("❌ Failed to load data: \(error)")
} }
isLoading = false isLoading = false

View File

@@ -74,7 +74,6 @@ actor LoadingTextGenerator {
return message return message
} catch { } catch {
print("[LoadingTextGenerator] Foundation Models error: \(error)")
return nil return nil
} }
} }

View File

@@ -96,9 +96,7 @@ extension LocationPermissionManager: CLLocationManagerDelegate {
} }
nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
Task { @MainActor in // Location error handled silently
print("Location error: \(error.localizedDescription)")
}
} }
} }

View File

@@ -10,28 +10,34 @@ import MapKit
actor LocationService { actor LocationService {
static let shared = LocationService() static let shared = LocationService()
private let geocoder = CLGeocoder()
private init() {} private init() {}
// MARK: - Geocoding // MARK: - Geocoding
func geocode(_ address: String) async throws -> CLLocationCoordinate2D? { func geocode(_ address: String) async throws -> CLLocationCoordinate2D? {
let placemarks = try await geocoder.geocodeAddressString(address) let request = MKLocalSearch.Request()
return placemarks.first?.location?.coordinate request.naturalLanguageQuery = address
request.resultTypes = .address
let search = MKLocalSearch(request: request)
let response = try await search.start()
return response.mapItems.first?.location.coordinate
} }
func reverseGeocode(_ coordinate: CLLocationCoordinate2D) async throws -> String? { func reverseGeocode(_ coordinate: CLLocationCoordinate2D) async throws -> String? {
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) let request = MKLocalSearch.Request()
let placemarks = try await geocoder.reverseGeocodeLocation(location) request.region = MKCoordinateRegion(
center: coordinate,
latitudinalMeters: 100,
longitudinalMeters: 100
)
request.resultTypes = .address
guard let placemark = placemarks.first else { return nil } let search = MKLocalSearch(request: request)
let response = try await search.start()
var components: [String] = [] guard let item = response.mapItems.first else { return nil }
if let city = placemark.locality { components.append(city) } return formatMapItem(item)
if let state = placemark.administrativeArea { components.append(state) }
return components.isEmpty ? nil : components.joined(separator: ", ")
} }
func resolveLocation(_ input: LocationInput) async throws -> LocationInput { func resolveLocation(_ input: LocationInput) async throws -> LocationInput {
@@ -66,19 +72,27 @@ actor LocationService {
return response.mapItems.map { item in return response.mapItems.map { item in
LocationSearchResult( LocationSearchResult(
name: item.name ?? "Unknown", name: item.name ?? "Unknown",
address: formatAddress(item.placemark), address: formatMapItem(item),
coordinate: item.placemark.coordinate coordinate: item.location.coordinate
) )
} }
} }
private func formatAddress(_ placemark: MKPlacemark) -> String { @available(iOS, deprecated: 26.0, message: "Uses placemark for address formatting")
private func formatMapItem(_ item: MKMapItem) -> String {
var components: [String] = [] var components: [String] = []
if let city = placemark.locality { components.append(city) } if let locality = item.placemark.locality {
if let state = placemark.administrativeArea { components.append(state) } components.append(locality)
if let country = placemark.country, country != "United States" { }
if let state = item.placemark.administrativeArea {
components.append(state)
}
if let country = item.placemark.country, country != "United States" {
components.append(country) components.append(country)
} }
if components.isEmpty {
return item.name ?? ""
}
return components.joined(separator: ", ") return components.joined(separator: ", ")
} }
@@ -98,8 +112,10 @@ actor LocationService {
to: CLLocationCoordinate2D to: CLLocationCoordinate2D
) async throws -> RouteInfo { ) async throws -> RouteInfo {
let request = MKDirections.Request() let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: from)) let fromLocation = CLLocation(latitude: from.latitude, longitude: from.longitude)
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: to)) let toLocation = CLLocation(latitude: to.latitude, longitude: to.longitude)
request.source = MKMapItem(location: fromLocation, address: nil)
request.destination = MKMapItem(location: toLocation, address: nil)
request.transportType = .automobile request.transportType = .automobile
request.requestsAlternateRoutes = false request.requestsAlternateRoutes = false

View File

@@ -165,48 +165,22 @@ actor StubDataProvider: DataProvider {
return true return true
} }
cachedGames = uniqueJsonGames.compactMap { convertGame($0) } cachedGames = uniqueJsonGames.compactMap { convertGame($0) }
print("StubDataProvider loaded: \(cachedGames?.count ?? 0) games, \(cachedTeams?.count ?? 0) teams, \(cachedStadiums?.count ?? 0) stadiums")
} }
private func loadGamesJSON() throws -> [JSONGame] { private func loadGamesJSON() throws -> [JSONGame] {
guard let url = Bundle.main.url(forResource: "games", withExtension: "json") else { guard let url = Bundle.main.url(forResource: "games", withExtension: "json") else {
print("Warning: games.json not found in bundle")
return [] return []
} }
let data = try Data(contentsOf: url) let data = try Data(contentsOf: url)
do { return try JSONDecoder().decode([JSONGame].self, from: data)
return try JSONDecoder().decode([JSONGame].self, from: data)
} catch let DecodingError.keyNotFound(key, context) {
print("❌ Games JSON missing key '\(key.stringValue)' at path: \(context.codingPath.map { $0.stringValue }.joined(separator: "."))")
throw DecodingError.keyNotFound(key, context)
} catch let DecodingError.typeMismatch(type, context) {
print("❌ Games JSON type mismatch for \(type) at path: \(context.codingPath.map { $0.stringValue }.joined(separator: "."))")
throw DecodingError.typeMismatch(type, context)
} catch {
print("❌ Games JSON decode error: \(error)")
throw error
}
} }
private func loadStadiumsJSON() throws -> [JSONStadium] { private func loadStadiumsJSON() throws -> [JSONStadium] {
guard let url = Bundle.main.url(forResource: "stadiums", withExtension: "json") else { guard let url = Bundle.main.url(forResource: "stadiums", withExtension: "json") else {
print("Warning: stadiums.json not found in bundle")
return [] return []
} }
let data = try Data(contentsOf: url) let data = try Data(contentsOf: url)
do { return try JSONDecoder().decode([JSONStadium].self, from: data)
return try JSONDecoder().decode([JSONStadium].self, from: data)
} catch let DecodingError.keyNotFound(key, context) {
print("❌ Stadiums JSON missing key '\(key.stringValue)' at path: \(context.codingPath.map { $0.stringValue }.joined(separator: "."))")
throw DecodingError.keyNotFound(key, context)
} catch let DecodingError.typeMismatch(type, context) {
print("❌ Stadiums JSON type mismatch for \(type) at path: \(context.codingPath.map { $0.stringValue }.joined(separator: "."))")
throw DecodingError.typeMismatch(type, context)
} catch {
print("❌ Stadiums JSON decode error: \(error)")
throw error
}
} }
// MARK: - Conversion Helpers // MARK: - Conversion Helpers
@@ -326,7 +300,6 @@ actor StubDataProvider: DataProvider {
} }
// Generate deterministic ID for unknown venues // Generate deterministic ID for unknown venues
print("[StubDataProvider] No stadium match for venue: '\(venue)'")
return deterministicUUID(from: "venue_\(venue)") return deterministicUUID(from: "venue_\(venue)")
} }

View File

@@ -9,7 +9,8 @@ import Foundation
import PDFKit import PDFKit
import UIKit import UIKit
actor PDFGenerator { @MainActor
final class PDFGenerator {
// MARK: - Constants // MARK: - Constants
@@ -380,7 +381,7 @@ actor PDFGenerator {
assets: PDFAssetPrefetcher.PrefetchedAssets?, assets: PDFAssetPrefetcher.PrefetchedAssets?,
y: CGFloat y: CGFloat
) -> CGFloat { ) -> CGFloat {
var currentY = y let currentY = y
// Card background // Card background
let cardRect = CGRect(x: margin + 10, y: currentY, width: contentWidth - 20, height: 65) let cardRect = CGRect(x: margin + 10, y: currentY, width: contentWidth - 20, height: 65)
@@ -486,7 +487,7 @@ actor PDFGenerator {
} }
private func drawTravelSegment(segment: TravelSegment, y: CGFloat) -> CGFloat { private func drawTravelSegment(segment: TravelSegment, y: CGFloat) -> CGFloat {
var currentY = y let currentY = y
let travelRect = CGRect(x: margin + 10, y: currentY, width: contentWidth - 20, height: 35) let travelRect = CGRect(x: margin + 10, y: currentY, width: contentWidth - 20, height: 35)
let travelPath = UIBezierPath(roundedRect: travelRect, cornerRadius: 6) let travelPath = UIBezierPath(roundedRect: travelRect, cornerRadius: 6)
@@ -601,7 +602,7 @@ actor PDFGenerator {
} }
private func drawPOIItem(poi: POISearchService.POI, index: Int, y: CGFloat) -> CGFloat { private func drawPOIItem(poi: POISearchService.POI, index: Int, y: CGFloat) -> CGFloat {
var currentY = y let currentY = y
// Number badge // Number badge
let badgeRect = CGRect(x: margin + 10, y: currentY, width: 22, height: 22) let badgeRect = CGRect(x: margin + 10, y: currentY, width: 22, height: 22)
@@ -887,7 +888,8 @@ extension UIColor {
// MARK: - Export Service // MARK: - Export Service
actor ExportService { @MainActor
final class ExportService {
private let pdfGenerator = PDFGenerator() private let pdfGenerator = PDFGenerator()
private let assetPrefetcher = PDFAssetPrefetcher() private let assetPrefetcher = PDFAssetPrefetcher()

View File

@@ -89,11 +89,15 @@ actor PDFAssetPrefetcher {
} }
} }
// Create immutable copies for concurrent access
let teamsToFetch = teams
let stadiumsToFetch = stadiums
// Run all fetches in parallel // Run all fetches in parallel
async let routeMapTask = fetchRouteMap(stops: trip.stops) async let routeMapTask = fetchRouteMap(stops: trip.stops)
async let cityMapsTask = fetchCityMaps(stops: trip.stops) async let cityMapsTask = fetchCityMaps(stops: trip.stops)
async let logosTask = imageService.fetchTeamLogos(teams: teams) async let logosTask = imageService.fetchTeamLogos(teams: teamsToFetch)
async let photosTask = imageService.fetchStadiumPhotos(stadiums: stadiums) async let photosTask = imageService.fetchStadiumPhotos(stadiums: stadiumsToFetch)
async let poisTask = poiService.findPOIsForCities(stops: trip.stops, limit: 5) async let poisTask = poiService.findPOIsForCities(stops: trip.stops, limit: 5)
// Await each result and update progress // Await each result and update progress
@@ -117,13 +121,6 @@ actor PDFAssetPrefetcher {
progress.poisComplete = true progress.poisComplete = true
await progressCallback?(progress) await progressCallback?(progress)
print("[PDFAssetPrefetcher] Prefetch complete:")
print(" - Route map: \(routeMap != nil ? "OK" : "Failed")")
print(" - City maps: \(cityMaps.count) cities")
print(" - Team logos: \(teamLogos.count) logos")
print(" - Stadium photos: \(stadiumPhotos.count) photos")
print(" - POIs: \(cityPOIs.values.reduce(0) { $0 + $1.count }) total POIs")
return PrefetchedAssets( return PrefetchedAssets(
routeMap: routeMap, routeMap: routeMap,
cityMaps: cityMaps, cityMaps: cityMaps,
@@ -141,7 +138,6 @@ actor PDFAssetPrefetcher {
let mapSize = CGSize(width: 512, height: 350) let mapSize = CGSize(width: 512, height: 350)
return try await mapService.generateRouteMap(stops: stops, size: mapSize) return try await mapService.generateRouteMap(stops: stops, size: mapSize)
} catch { } catch {
print("[PDFAssetPrefetcher] Route map failed: \(error.localizedDescription)")
return nil return nil
} }
} }
@@ -162,7 +158,6 @@ actor PDFAssetPrefetcher {
let map = try await self.mapService.generateCityMap(stop: stop, size: mapSize) let map = try await self.mapService.generateCityMap(stop: stop, size: mapSize)
return (stop.city, map) return (stop.city, map)
} catch { } catch {
print("[PDFAssetPrefetcher] City map for \(stop.city) failed: \(error.localizedDescription)")
return (stop.city, nil) return (stop.city, nil)
} }
} }

View File

@@ -128,7 +128,6 @@ actor POISearchService {
limit: limitPerCategory limit: limitPerCategory
) )
} catch { } catch {
print("[POISearchService] Search failed for \(category): \(error.localizedDescription)")
return [] return []
} }
} }
@@ -167,7 +166,6 @@ actor POISearchService {
// Take top N overall // Take top N overall
return (stop.city, Array(pois.prefix(limit))) return (stop.city, Array(pois.prefix(limit)))
} catch { } catch {
print("[POISearchService] Failed for \(stop.city): \(error.localizedDescription)")
return (stop.city, []) return (stop.city, [])
} }
} }
@@ -208,11 +206,8 @@ actor POISearchService {
let pois: [POI] = response.mapItems.prefix(limit).compactMap { item in let pois: [POI] = response.mapItems.prefix(limit).compactMap { item in
guard let name = item.name else { return nil } guard let name = item.name else { return nil }
let itemLocation = CLLocation( let itemCoordinate = item.location.coordinate
latitude: item.placemark.coordinate.latitude, let distance = referenceLocation.distance(from: item.location)
longitude: item.placemark.coordinate.longitude
)
let distance = referenceLocation.distance(from: itemLocation)
// Only include POIs within radius // Only include POIs within radius
guard distance <= radiusMeters else { return nil } guard distance <= radiusMeters else { return nil }
@@ -221,22 +216,23 @@ actor POISearchService {
id: UUID(), id: UUID(),
name: name, name: name,
category: category, category: category,
coordinate: item.placemark.coordinate, coordinate: itemCoordinate,
distanceMeters: distance, distanceMeters: distance,
address: formatAddress(item.placemark) address: formatAddress(item)
) )
} }
return pois return pois
} }
private func formatAddress(_ placemark: MKPlacemark) -> String? { @available(iOS, deprecated: 26.0, message: "Uses placemark for address formatting")
private func formatAddress(_ item: MKMapItem) -> String? {
var components: [String] = [] var components: [String] = []
if let subThoroughfare = placemark.subThoroughfare { if let subThoroughfare = item.placemark.subThoroughfare {
components.append(subThoroughfare) components.append(subThoroughfare)
} }
if let thoroughfare = placemark.thoroughfare { if let thoroughfare = item.placemark.thoroughfare {
components.append(thoroughfare) components.append(thoroughfare)
} }

View File

@@ -95,7 +95,6 @@ actor RemoteImageService {
let image = try await self.fetchImage(from: url) let image = try await self.fetchImage(from: url)
return (url, image) return (url, image)
} catch { } catch {
print("[RemoteImageService] Failed to fetch \(url): \(error.localizedDescription)")
return (url, nil) return (url, nil)
} }
} }

View File

@@ -88,11 +88,9 @@ final class ScheduleViewModel {
} catch let cloudKitError as CloudKitError { } catch let cloudKitError as CloudKitError {
self.error = cloudKitError self.error = cloudKitError
self.errorMessage = cloudKitError.errorDescription self.errorMessage = cloudKitError.errorDescription
print("CloudKit error loading games: \(cloudKitError.errorDescription ?? "Unknown")")
} catch { } catch {
self.error = error self.error = error
self.errorMessage = error.localizedDescription self.errorMessage = error.localizedDescription
print("Failed to load games: \(error)")
} }
isLoading = false isLoading = false

View File

@@ -78,15 +78,11 @@ final class SettingsViewModel {
isSyncing = true isSyncing = true
syncError = nil syncError = nil
do { // Trigger data reload from provider
// Trigger data reload from provider await AppDataProvider.shared.loadInitialData()
await AppDataProvider.shared.loadInitialData()
lastSyncDate = Date() lastSyncDate = Date()
UserDefaults.standard.set(lastSyncDate, forKey: "lastSyncDate") UserDefaults.standard.set(lastSyncDate, forKey: "lastSyncDate")
} catch {
syncError = error.localizedDescription
}
isSyncing = false isSyncing = false
} }

View File

@@ -222,128 +222,121 @@ final class TripCreationViewModel {
viewState = .planning viewState = .planning
do { // Mode-specific setup
// Mode-specific setup var effectiveStartDate = startDate
var effectiveStartDate = startDate var effectiveEndDate = endDate
var effectiveEndDate = endDate var resolvedStartLocation: LocationInput?
var resolvedStartLocation: LocationInput? var resolvedEndLocation: LocationInput?
var resolvedEndLocation: LocationInput?
switch planningMode { switch planningMode {
case .dateRange: case .dateRange:
// Use provided date range, no location needed // Use provided date range, no location needed
// Games will be found within the date range across all regions // Games will be found within the date range across all regions
effectiveStartDate = startDate effectiveStartDate = startDate
effectiveEndDate = endDate effectiveEndDate = endDate
case .gameFirst: case .gameFirst:
// Calculate date range from selected games + buffer // Calculate date range from selected games + buffer
if let dateRange = gameFirstDateRange { if let dateRange = gameFirstDateRange {
effectiveStartDate = dateRange.start effectiveStartDate = dateRange.start
effectiveEndDate = dateRange.end effectiveEndDate = dateRange.end
} }
// Derive start/end locations from first/last game stadiums // Derive start/end locations from first/last game stadiums
if let firstGame = selectedGames.sorted(by: { $0.game.dateTime < $1.game.dateTime }).first, if let firstGame = selectedGames.sorted(by: { $0.game.dateTime < $1.game.dateTime }).first,
let lastGame = selectedGames.sorted(by: { $0.game.dateTime < $1.game.dateTime }).last { let lastGame = selectedGames.sorted(by: { $0.game.dateTime < $1.game.dateTime }).last {
resolvedStartLocation = LocationInput( resolvedStartLocation = LocationInput(
name: firstGame.stadium.city, name: firstGame.stadium.city,
coordinate: firstGame.stadium.coordinate, coordinate: firstGame.stadium.coordinate,
address: "\(firstGame.stadium.city), \(firstGame.stadium.state)" address: "\(firstGame.stadium.city), \(firstGame.stadium.state)"
) )
resolvedEndLocation = LocationInput( resolvedEndLocation = LocationInput(
name: lastGame.stadium.city, name: lastGame.stadium.city,
coordinate: lastGame.stadium.coordinate, coordinate: lastGame.stadium.coordinate,
address: "\(lastGame.stadium.city), \(lastGame.stadium.state)" address: "\(lastGame.stadium.city), \(lastGame.stadium.state)"
) )
}
case .locations:
// Resolve provided locations
await resolveLocations()
resolvedStartLocation = startLocation
resolvedEndLocation = endLocation
guard resolvedStartLocation != nil, resolvedEndLocation != nil else {
viewState = .error("Could not resolve start or end location")
return
}
} }
// Ensure we have games data case .locations:
if games.isEmpty { // Resolve provided locations
await loadScheduleData() await resolveLocations()
resolvedStartLocation = startLocation
resolvedEndLocation = endLocation
guard resolvedStartLocation != nil, resolvedEndLocation != nil else {
viewState = .error("Could not resolve start or end location")
return
}
}
// Ensure we have games data
if games.isEmpty {
await loadScheduleData()
}
// Read max trip options from settings (default 10)
let savedMaxOptions = UserDefaults.standard.integer(forKey: "maxTripOptions")
let maxTripOptions = savedMaxOptions > 0 ? min(20, savedMaxOptions) : 10
// Build preferences
let preferences = TripPreferences(
planningMode: planningMode,
startLocation: resolvedStartLocation,
endLocation: resolvedEndLocation,
sports: selectedSports,
mustSeeGameIds: mustSeeGameIds,
travelMode: travelMode,
startDate: effectiveStartDate,
endDate: effectiveEndDate,
numberOfStops: useStopCount ? numberOfStops : nil,
tripDuration: useStopCount ? nil : tripDurationDays,
leisureLevel: leisureLevel,
mustStopLocations: mustStopLocations,
preferredCities: preferredCities,
routePreference: routePreference,
needsEVCharging: needsEVCharging,
lodgingType: lodgingType,
numberOfDrivers: numberOfDrivers,
maxDrivingHoursPerDriver: maxDrivingHoursPerDriver,
maxTripOptions: maxTripOptions
)
// Build planning request
let request = PlanningRequest(
preferences: preferences,
availableGames: games,
teams: teams,
stadiums: stadiums
)
// Plan the trip
let result = planningEngine.planItineraries(request: request)
switch result {
case .success(var options):
guard !options.isEmpty else {
viewState = .error("No valid itinerary found")
return
} }
// Read max trip options from settings (default 10) // Enrich with EV chargers if requested and feature is enabled
let savedMaxOptions = UserDefaults.standard.integer(forKey: "maxTripOptions") if FeatureFlags.enableEVCharging && needsEVCharging {
let maxTripOptions = savedMaxOptions > 0 ? min(20, savedMaxOptions) : 10 options = await ItineraryBuilder.enrichWithEVChargers(options)
// Build preferences
let preferences = TripPreferences(
planningMode: planningMode,
startLocation: resolvedStartLocation,
endLocation: resolvedEndLocation,
sports: selectedSports,
mustSeeGameIds: mustSeeGameIds,
travelMode: travelMode,
startDate: effectiveStartDate,
endDate: effectiveEndDate,
numberOfStops: useStopCount ? numberOfStops : nil,
tripDuration: useStopCount ? nil : tripDurationDays,
leisureLevel: leisureLevel,
mustStopLocations: mustStopLocations,
preferredCities: preferredCities,
routePreference: routePreference,
needsEVCharging: needsEVCharging,
lodgingType: lodgingType,
numberOfDrivers: numberOfDrivers,
maxDrivingHoursPerDriver: maxDrivingHoursPerDriver,
maxTripOptions: maxTripOptions
)
// Build planning request
let request = PlanningRequest(
preferences: preferences,
availableGames: games,
teams: teams,
stadiums: stadiums
)
// Plan the trip
let result = planningEngine.planItineraries(request: request)
switch result {
case .success(var options):
guard !options.isEmpty else {
viewState = .error("No valid itinerary found")
return
}
// Enrich with EV chargers if requested and feature is enabled
if FeatureFlags.enableEVCharging && needsEVCharging {
print("[TripCreation] Enriching \(options.count) options with EV chargers...")
options = await ItineraryBuilder.enrichWithEVChargers(options)
print("[TripCreation] EV charger enrichment complete")
}
// Store preferences for later conversion
currentPreferences = preferences
if options.count == 1 {
// Only one option - go directly to detail
let trip = convertToTrip(option: options[0], preferences: preferences)
viewState = .completed(trip)
} else {
// Multiple options - show selection view
viewState = .selectingOption(options)
}
case .failure(let failure):
viewState = .error(failureMessage(for: failure))
} }
} catch { // Store preferences for later conversion
viewState = .error("Trip planning failed: \(error.localizedDescription)") currentPreferences = preferences
if options.count == 1 {
// Only one option - go directly to detail
let trip = convertToTrip(option: options[0], preferences: preferences)
viewState = .completed(trip)
} else {
// Multiple options - show selection view
viewState = .selectingOption(options)
}
case .failure(let failure):
viewState = .error(failureMessage(for: failure))
} }
} }

View File

@@ -62,9 +62,7 @@ struct TripDetailView: View {
.toolbar { .toolbar {
ToolbarItemGroup(placement: .primaryAction) { ToolbarItemGroup(placement: .primaryAction) {
Button { Button {
Task { shareTrip()
await shareTrip()
}
} label: { } label: {
Image(systemName: "square.and.arrow.up") Image(systemName: "square.and.arrow.up")
.foregroundStyle(Theme.warmOrange) .foregroundStyle(Theme.warmOrange)
@@ -429,8 +427,10 @@ struct TripDetailView: View {
let destination = stops[i + 1] let destination = stops[i + 1]
let request = MKDirections.Request() let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: source.coordinate)) let sourceLocation = CLLocation(latitude: source.coordinate.latitude, longitude: source.coordinate.longitude)
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destination.coordinate)) let destLocation = CLLocation(latitude: destination.coordinate.latitude, longitude: destination.coordinate.longitude)
request.source = MKMapItem(location: sourceLocation, address: nil)
request.destination = MKMapItem(location: destLocation, address: nil)
request.transportType = .automobile request.transportType = .automobile
let directions = MKDirections(request: request) let directions = MKDirections(request: request)
@@ -504,14 +504,14 @@ struct TripDetailView: View {
exportURL = url exportURL = url
showExportSheet = true showExportSheet = true
} catch { } catch {
print("Failed to export PDF: \(error)") // PDF export failed silently
} }
isExporting = false isExporting = false
} }
private func shareTrip() async { private func shareTrip() {
shareURL = await exportService.shareTrip(trip) shareURL = exportService.shareTrip(trip)
showShareSheet = true showShareSheet = true
} }
@@ -525,7 +525,6 @@ struct TripDetailView: View {
private func saveTrip() { private func saveTrip() {
guard let savedTrip = SavedTrip.from(trip, games: games, status: .planned) else { guard let savedTrip = SavedTrip.from(trip, games: games, status: .planned) else {
print("Failed to create SavedTrip")
return return
} }
@@ -537,7 +536,7 @@ struct TripDetailView: View {
isSaved = true isSaved = true
} }
} catch { } catch {
print("Failed to save trip: \(error)") // Save failed silently
} }
} }
@@ -557,7 +556,7 @@ struct TripDetailView: View {
isSaved = false isSaved = false
} }
} catch { } catch {
print("Failed to unsave trip: \(error)") // Unsave failed silently
} }
} }

View File

@@ -92,8 +92,6 @@ enum GameDAGRouter {
guard !sortedDays.isEmpty else { return [] } guard !sortedDays.isEmpty else { return [] }
print("[GameDAGRouter] \(games.count) games across \(sortedDays.count) days")
print("[GameDAGRouter] Games per day: \(sortedDays.map { buckets[$0]?.count ?? 0 })")
// Step 3: Initialize beam with first day's games // Step 3: Initialize beam with first day's games
var beam: [[Game]] = [] var beam: [[Game]] = []
@@ -113,10 +111,9 @@ enum GameDAGRouter {
} }
} }
print("[GameDAGRouter] Initial beam size: \(beam.count)")
// Step 4: Expand beam day by day // Step 4: Expand beam day by day
for (index, dayIndex) in sortedDays.dropFirst().enumerated() { for (_, dayIndex) in sortedDays.dropFirst().enumerated() {
let todaysGames = buckets[dayIndex] ?? [] let todaysGames = buckets[dayIndex] ?? []
var nextBeam: [[Game]] = [] var nextBeam: [[Game]] = []
@@ -131,14 +128,11 @@ enum GameDAGRouter {
continue continue
} }
var addedAny = false
// Try adding each of today's games // Try adding each of today's games
for candidate in todaysGames { for candidate in todaysGames {
if canTransition(from: lastGame, to: candidate, stadiums: stadiums, constraints: constraints) { if canTransition(from: lastGame, to: candidate, stadiums: stadiums, constraints: constraints) {
let newPath = path + [candidate] let newPath = path + [candidate]
nextBeam.append(newPath) nextBeam.append(newPath)
addedAny = true
} }
} }
@@ -148,7 +142,6 @@ enum GameDAGRouter {
// Dominance pruning + beam truncation // Dominance pruning + beam truncation
beam = pruneAndTruncate(nextBeam, beamWidth: beamWidth, stadiums: stadiums) beam = pruneAndTruncate(nextBeam, beamWidth: beamWidth, stadiums: stadiums)
print("[GameDAGRouter] Day \(dayIndex): nextBeam=\(nextBeam.count), after prune=\(beam.count), max games=\(beam.map { $0.count }.max() ?? 0)")
} }
// Step 5: Filter routes that contain all anchors // Step 5: Filter routes that contain all anchors
@@ -160,15 +153,7 @@ enum GameDAGRouter {
// Step 6: Ensure geographic diversity in results // Step 6: Ensure geographic diversity in results
// Group routes by their primary region (city with most games) // Group routes by their primary region (city with most games)
// Then pick the best route from each region // Then pick the best route from each region
let diverseRoutes = selectDiverseRoutes(routesWithAnchors, stadiums: stadiums, maxCount: maxOptions) return selectDiverseRoutes(routesWithAnchors, stadiums: stadiums, maxCount: maxOptions)
print("[GameDAGRouter] Found \(routesWithAnchors.count) routes with anchors, returning \(diverseRoutes.count) diverse routes")
for (i, route) in diverseRoutes.prefix(5).enumerated() {
let cities = route.compactMap { stadiums[$0.stadiumId]?.city }.joined(separator: "")
print("[GameDAGRouter] Route \(i+1): \(route.count) games - \(cities)")
}
return diverseRoutes
} }
/// Compatibility wrapper that matches GeographicRouteExplorer's interface. /// Compatibility wrapper that matches GeographicRouteExplorer's interface.
@@ -320,7 +305,6 @@ enum GameDAGRouter {
return score1 > score2 return score1 > score2
} }
print("[GameDAGRouter] Found \(sortedRegions.count) distinct regions: \(sortedRegions.prefix(10).joined(separator: ", "))")
// Pick routes round-robin from each region to ensure diversity // Pick routes round-robin from each region to ensure diversity
var selectedRoutes: [[Game]] = [] var selectedRoutes: [[Game]] = []

View File

@@ -76,14 +76,12 @@ enum ItineraryBuilder {
to: toStop, to: toStop,
constraints: constraints constraints: constraints
) else { ) else {
print("\(logPrefix) Failed to estimate travel: \(fromStop.city) -> \(toStop.city)")
return nil return nil
} }
// Run optional validator (e.g., arrival time check for Scenario B) // Run optional validator (e.g., arrival time check for Scenario B)
if let validator = segmentValidator { if let validator = segmentValidator {
if !validator(segment, fromStop, toStop) { if !validator(segment, fromStop, toStop) {
print("\(logPrefix) Segment validation failed: \(fromStop.city) -> \(toStop.city)")
return nil return nil
} }
} }
@@ -97,7 +95,6 @@ enum ItineraryBuilder {
// Verify invariant: segments = stops - 1 // Verify invariant: segments = stops - 1
// //
guard travelSegments.count == stops.count - 1 else { guard travelSegments.count == stops.count - 1 else {
print("\(logPrefix) Invariant violated: \(travelSegments.count) segments for \(stops.count) stops")
return nil return nil
} }
@@ -147,9 +144,8 @@ enum ItineraryBuilder {
searchRadiusMiles: 5.0, searchRadiusMiles: 5.0,
intervalMiles: 100.0 intervalMiles: 100.0
) )
print("[ItineraryBuilder] Found \(evChargers.count) EV chargers: \(segment.fromLocation.name) -> \(segment.toLocation.name)")
} catch { } catch {
print("[ItineraryBuilder] EV charger search failed: \(error.localizedDescription)") // EV charger search failed - continue without chargers
} }
} }
@@ -167,7 +163,7 @@ enum ItineraryBuilder {
enrichedSegments.append(enrichedSegment) enrichedSegments.append(enrichedSegment)
} catch { } catch {
print("[ItineraryBuilder] Route calculation failed, keeping original: \(error.localizedDescription)") // Route calculation failed - keep original segment
enrichedSegments.append(segment) enrichedSegments.append(segment)
} }
} }
@@ -252,7 +248,6 @@ enum ItineraryBuilder {
// Get coordinates // Get coordinates
guard let fromCoord = fromStop.coordinate, guard let fromCoord = fromStop.coordinate,
let toCoord = toStop.coordinate else { let toCoord = toStop.coordinate else {
print("\(logPrefix) Missing coordinates: \(fromStop.city) -> \(toStop.city)")
// Fall back to estimate // Fall back to estimate
guard let segment = TravelEstimator.estimate( guard let segment = TravelEstimator.estimate(
from: fromStop, from: fromStop,
@@ -283,9 +278,7 @@ enum ItineraryBuilder {
searchRadiusMiles: 5.0, searchRadiusMiles: 5.0,
intervalMiles: 100.0 intervalMiles: 100.0
) )
print("\(logPrefix) Found \(evChargers.count) EV chargers: \(fromStop.city) -> \(toStop.city)")
} catch { } catch {
print("\(logPrefix) EV charger search failed: \(error.localizedDescription)")
// Continue without chargers - not a critical failure // Continue without chargers - not a critical failure
} }
} }
@@ -302,7 +295,6 @@ enum ItineraryBuilder {
// Run optional validator // Run optional validator
if let validator = segmentValidator { if let validator = segmentValidator {
if !validator(segment, fromStop, toStop) { if !validator(segment, fromStop, toStop) {
print("\(logPrefix) Segment validation failed: \(fromStop.city) -> \(toStop.city)")
return nil return nil
} }
} }
@@ -312,7 +304,6 @@ enum ItineraryBuilder {
totalDistance += segment.estimatedDistanceMiles totalDistance += segment.estimatedDistanceMiles
} catch { } catch {
print("\(logPrefix) Route calculation failed, using estimate: \(error.localizedDescription)")
// Fall back to estimate // Fall back to estimate
guard let segment = TravelEstimator.estimate( guard let segment = TravelEstimator.estimate(
from: fromStop, from: fromStop,
@@ -329,7 +320,6 @@ enum ItineraryBuilder {
// Verify invariant // Verify invariant
guard travelSegments.count == stops.count - 1 else { guard travelSegments.count == stops.count - 1 else {
print("\(logPrefix) Invariant violated: \(travelSegments.count) segments for \(stops.count) stops")
return nil return nil
} }
@@ -366,7 +356,6 @@ enum ItineraryBuilder {
let deadline = gameStart.addingTimeInterval(-bufferSeconds) let deadline = gameStart.addingTimeInterval(-bufferSeconds)
if earliestArrival > deadline { if earliestArrival > deadline {
print("[ItineraryBuilder] Cannot arrive in time: earliest arrival \(earliestArrival) > deadline \(deadline)")
return false return false
} }
return true return true

View File

@@ -68,9 +68,6 @@ final class ScenarioAPlanner: ScenarioPlanner {
.filter { dateRange.contains($0.startTime) } .filter { dateRange.contains($0.startTime) }
.sorted { $0.startTime < $1.startTime } .sorted { $0.startTime < $1.startTime }
print("[ScenarioA] Found \(gamesInRange.count) games in date range")
print("[ScenarioA] Stadiums available: \(request.stadiums.count)")
// No games? Nothing to plan. // No games? Nothing to plan.
if gamesInRange.isEmpty { if gamesInRange.isEmpty {
return .failure( return .failure(
@@ -100,11 +97,6 @@ final class ScenarioAPlanner: ScenarioPlanner {
stopBuilder: buildStops stopBuilder: buildStops
) )
print("[ScenarioA] GameDAGRouter returned \(validRoutes.count) routes")
if !validRoutes.isEmpty {
print("[ScenarioA] Route sizes: \(validRoutes.map { $0.count })")
}
if validRoutes.isEmpty { if validRoutes.isEmpty {
return .failure( return .failure(
PlanningFailure( PlanningFailure(
@@ -136,23 +128,16 @@ final class ScenarioAPlanner: ScenarioPlanner {
// Build stops for this route // Build stops for this route
let stops = buildStops(from: routeGames, stadiums: request.stadiums) let stops = buildStops(from: routeGames, stadiums: request.stadiums)
guard !stops.isEmpty else { guard !stops.isEmpty else {
print("[ScenarioA] Route \(index + 1) produced no stops, skipping")
routesFailed += 1 routesFailed += 1
continue continue
} }
// Log stop details
let stopCities = stops.map { "\($0.city) (coord: \($0.coordinate != nil))" }
print("[ScenarioA] Route \(index + 1): \(stops.count) stops - \(stopCities.joined(separator: ""))")
// Calculate travel segments using shared ItineraryBuilder // Calculate travel segments using shared ItineraryBuilder
guard let itinerary = ItineraryBuilder.build( guard let itinerary = ItineraryBuilder.build(
stops: stops, stops: stops,
constraints: request.drivingConstraints, constraints: request.drivingConstraints
logPrefix: "[ScenarioA]"
) else { ) else {
// This route fails driving constraints, skip it // This route fails driving constraints, skip it
print("[ScenarioA] Route \(index + 1) failed driving constraints, skipping")
routesFailed += 1 routesFailed += 1
continue continue
} }
@@ -176,8 +161,6 @@ final class ScenarioAPlanner: ScenarioPlanner {
// If no routes passed all constraints, fail. // If no routes passed all constraints, fail.
// Otherwise, return all valid options for the user to choose from. // Otherwise, return all valid options for the user to choose from.
// //
print("[ScenarioA] Routes attempted: \(routesAttempted), failed: \(routesFailed), succeeded: \(itineraryOptions.count)")
if itineraryOptions.isEmpty { if itineraryOptions.isEmpty {
return .failure( return .failure(
PlanningFailure( PlanningFailure(
@@ -201,7 +184,6 @@ final class ScenarioAPlanner: ScenarioPlanner {
limit: request.preferences.maxTripOptions limit: request.preferences.maxTripOptions
) )
print("[ScenarioA] Returning \(rankedOptions.count) itinerary options (leisure: \(leisureLevel.rawValue))")
return .success(rankedOptions) return .success(rankedOptions)
} }

View File

@@ -168,7 +168,6 @@ final class ScenarioBPlanner: ScenarioPlanner {
limit: request.preferences.maxTripOptions limit: request.preferences.maxTripOptions
) )
print("[ScenarioB] Returning \(rankedOptions.count) itinerary options (leisure: \(leisureLevel.rawValue))")
return .success(Array(rankedOptions)) return .success(Array(rankedOptions))
} }
@@ -249,13 +248,7 @@ final class ScenarioBPlanner: ScenarioPlanner {
// Last window: first selected game is on first day of window // Last window: first selected game is on first day of window
// Window start = firstGameDate // Window start = firstGameDate
// Window end = start + duration days
let lastWindowStart = firstGameDate let lastWindowStart = firstGameDate
let lastWindowEnd = Calendar.current.date(
byAdding: .day,
value: duration,
to: lastWindowStart
)!
// Slide from first window to last window // Slide from first window to last window
var currentStart = firstWindowStart var currentStart = firstWindowStart
@@ -277,7 +270,6 @@ final class ScenarioBPlanner: ScenarioPlanner {
)! )!
} }
print("[ScenarioB] Generated \(dateRanges.count) sliding windows for \(duration)-day trip")
return dateRanges return dateRanges
} }

View File

@@ -175,8 +175,6 @@ final class ScenarioCPlanner: ScenarioPlanner {
stadiums: request.stadiums stadiums: request.stadiums
) )
print("[ScenarioC] Found \(directionalStadiums.count) directional stadiums from \(startLocation.name) to \(endLocation.name)")
// //
// Step 5: For each date range, explore routes // Step 5: For each date range, explore routes
// //
@@ -267,7 +265,6 @@ final class ScenarioCPlanner: ScenarioPlanner {
limit: request.preferences.maxTripOptions limit: request.preferences.maxTripOptions
) )
print("[ScenarioC] Returning \(rankedOptions.count) itinerary options (leisure: \(leisureLevel.rawValue))")
return .success(Array(rankedOptions)) return .success(Array(rankedOptions))
} }
@@ -318,7 +315,6 @@ final class ScenarioCPlanner: ScenarioPlanner {
let detourDistance = toStadium + fromStadium let detourDistance = toStadium + fromStadium
// Also check that stadium is making progress (closer to end than start is) // Also check that stadium is making progress (closer to end than start is)
let distanceFromStart = distanceBetween(start, stadiumCoord)
let distanceToEnd = distanceBetween(stadiumCoord, end) let distanceToEnd = distanceBetween(stadiumCoord, end)
// Stadium should be within the "cone" from start to end // Stadium should be within the "cone" from start to end
@@ -408,7 +404,6 @@ final class ScenarioCPlanner: ScenarioPlanner {
} }
} }
print("[ScenarioC] Generated \(dateRanges.count) date ranges for \(daySpan)-day trip")
return dateRanges return dateRanges
} }
@@ -561,7 +556,6 @@ final class ScenarioCPlanner: ScenarioPlanner {
// Allow increases up to tolerance percentage // Allow increases up to tolerance percentage
let allowedIncrease = prev * forwardProgressTolerance let allowedIncrease = prev * forwardProgressTolerance
if currentDistance > prev + allowedIncrease { if currentDistance > prev + allowedIncrease {
print("[ScenarioC] Backtracking: \(stop.city) increases distance to end (\(Int(currentDistance))mi vs \(Int(prev))mi)")
return false return false
} }
} }

View File

@@ -19,23 +19,9 @@ final class TripPlanningEngine {
func planItineraries(request: PlanningRequest) -> ItineraryResult { func planItineraries(request: PlanningRequest) -> ItineraryResult {
// Detect scenario and get the appropriate planner // Detect scenario and get the appropriate planner
let scenario = ScenarioPlannerFactory.classify(request)
let planner = ScenarioPlannerFactory.planner(for: request) let planner = ScenarioPlannerFactory.planner(for: request)
print("[TripPlanningEngine] Detected scenario: \(scenario)")
print("[TripPlanningEngine] Using planner: \(type(of: planner))")
// Delegate to the scenario planner // Delegate to the scenario planner
let result = planner.plan(request: request) return planner.plan(request: request)
// Log result
switch result {
case .success(let options):
print("[TripPlanningEngine] Success: \(options.count) itinerary options")
case .failure(let failure):
print("[TripPlanningEngine] Failure: \(failure.reason)")
}
return result
} }
} }