diff --git a/SportsTime/Core/Models/Domain/TripPreferences.swift b/SportsTime/Core/Models/Domain/TripPreferences.swift index 4096a05..c33cdab 100644 --- a/SportsTime/Core/Models/Domain/TripPreferences.swift +++ b/SportsTime/Core/Models/Domain/TripPreferences.swift @@ -176,7 +176,7 @@ struct LocationInput: Codable, Hashable { var isResolved: Bool { coordinate != nil } } -extension CLLocationCoordinate2D: Codable, Hashable { +extension CLLocationCoordinate2D: @retroactive Codable, @retroactive Hashable, @retroactive Equatable { enum CodingKeys: String, CodingKey { case latitude, longitude } diff --git a/SportsTime/Core/Services/DataProvider.swift b/SportsTime/Core/Services/DataProvider.swift index 3e5cf1f..74831a2 100644 --- a/SportsTime/Core/Services/DataProvider.swift +++ b/SportsTime/Core/Services/DataProvider.swift @@ -39,11 +39,9 @@ final class AppDataProvider: ObservableObject { #if targetEnvironment(simulator) self.provider = StubDataProvider() self.isUsingStubData = true - print("📱 Using StubDataProvider (Simulator)") #else self.provider = CloudKitDataProvider() self.isUsingStubData = false - print("☁️ Using CloudKitDataProvider (Device)") #endif } @@ -66,16 +64,12 @@ final class AppDataProvider: ObservableObject { // Build lookup dictionaries self.teamsById = Dictionary(uniqueKeysWithValues: loadedTeams.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 { self.error = cloudKitError self.errorMessage = cloudKitError.errorDescription - print("❌ CloudKit error: \(cloudKitError.errorDescription ?? "Unknown")") } catch { self.error = error self.errorMessage = error.localizedDescription - print("❌ Failed to load data: \(error)") } isLoading = false diff --git a/SportsTime/Core/Services/LoadingTextGenerator.swift b/SportsTime/Core/Services/LoadingTextGenerator.swift index bd44b59..75ab0a2 100644 --- a/SportsTime/Core/Services/LoadingTextGenerator.swift +++ b/SportsTime/Core/Services/LoadingTextGenerator.swift @@ -74,7 +74,6 @@ actor LoadingTextGenerator { return message } catch { - print("[LoadingTextGenerator] Foundation Models error: \(error)") return nil } } diff --git a/SportsTime/Core/Services/LocationPermissionManager.swift b/SportsTime/Core/Services/LocationPermissionManager.swift index 263ba08..48351ff 100644 --- a/SportsTime/Core/Services/LocationPermissionManager.swift +++ b/SportsTime/Core/Services/LocationPermissionManager.swift @@ -96,9 +96,7 @@ extension LocationPermissionManager: CLLocationManagerDelegate { } nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - Task { @MainActor in - print("Location error: \(error.localizedDescription)") - } + // Location error handled silently } } diff --git a/SportsTime/Core/Services/LocationService.swift b/SportsTime/Core/Services/LocationService.swift index 24b14af..936e92f 100644 --- a/SportsTime/Core/Services/LocationService.swift +++ b/SportsTime/Core/Services/LocationService.swift @@ -10,28 +10,34 @@ import MapKit actor LocationService { static let shared = LocationService() - private let geocoder = CLGeocoder() - private init() {} // MARK: - Geocoding func geocode(_ address: String) async throws -> CLLocationCoordinate2D? { - let placemarks = try await geocoder.geocodeAddressString(address) - return placemarks.first?.location?.coordinate + let request = MKLocalSearch.Request() + 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? { - let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) - let placemarks = try await geocoder.reverseGeocodeLocation(location) + let request = MKLocalSearch.Request() + 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] = [] - if let city = placemark.locality { components.append(city) } - if let state = placemark.administrativeArea { components.append(state) } - - return components.isEmpty ? nil : components.joined(separator: ", ") + guard let item = response.mapItems.first else { return nil } + return formatMapItem(item) } func resolveLocation(_ input: LocationInput) async throws -> LocationInput { @@ -66,19 +72,27 @@ actor LocationService { return response.mapItems.map { item in LocationSearchResult( name: item.name ?? "Unknown", - address: formatAddress(item.placemark), - coordinate: item.placemark.coordinate + address: formatMapItem(item), + 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] = [] - if let city = placemark.locality { components.append(city) } - if let state = placemark.administrativeArea { components.append(state) } - if let country = placemark.country, country != "United States" { + if let locality = item.placemark.locality { + components.append(locality) + } + if let state = item.placemark.administrativeArea { + components.append(state) + } + if let country = item.placemark.country, country != "United States" { components.append(country) } + if components.isEmpty { + return item.name ?? "" + } return components.joined(separator: ", ") } @@ -98,8 +112,10 @@ actor LocationService { to: CLLocationCoordinate2D ) async throws -> RouteInfo { let request = MKDirections.Request() - request.source = MKMapItem(placemark: MKPlacemark(coordinate: from)) - request.destination = MKMapItem(placemark: MKPlacemark(coordinate: to)) + let fromLocation = CLLocation(latitude: from.latitude, longitude: from.longitude) + 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.requestsAlternateRoutes = false diff --git a/SportsTime/Core/Services/StubDataProvider.swift b/SportsTime/Core/Services/StubDataProvider.swift index 8c61411..4d428bc 100644 --- a/SportsTime/Core/Services/StubDataProvider.swift +++ b/SportsTime/Core/Services/StubDataProvider.swift @@ -165,48 +165,22 @@ actor StubDataProvider: DataProvider { return true } 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] { guard let url = Bundle.main.url(forResource: "games", withExtension: "json") else { - print("Warning: games.json not found in bundle") return [] } let data = try Data(contentsOf: url) - do { - 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 - } + return try JSONDecoder().decode([JSONGame].self, from: data) } private func loadStadiumsJSON() throws -> [JSONStadium] { guard let url = Bundle.main.url(forResource: "stadiums", withExtension: "json") else { - print("Warning: stadiums.json not found in bundle") return [] } let data = try Data(contentsOf: url) - do { - 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 - } + return try JSONDecoder().decode([JSONStadium].self, from: data) } // MARK: - Conversion Helpers @@ -326,7 +300,6 @@ actor StubDataProvider: DataProvider { } // Generate deterministic ID for unknown venues - print("[StubDataProvider] No stadium match for venue: '\(venue)'") return deterministicUUID(from: "venue_\(venue)") } diff --git a/SportsTime/Export/PDFGenerator.swift b/SportsTime/Export/PDFGenerator.swift index 08bf281..9f15b56 100644 --- a/SportsTime/Export/PDFGenerator.swift +++ b/SportsTime/Export/PDFGenerator.swift @@ -9,7 +9,8 @@ import Foundation import PDFKit import UIKit -actor PDFGenerator { +@MainActor +final class PDFGenerator { // MARK: - Constants @@ -380,7 +381,7 @@ actor PDFGenerator { assets: PDFAssetPrefetcher.PrefetchedAssets?, y: CGFloat ) -> CGFloat { - var currentY = y + let currentY = y // Card background 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 { - var currentY = y + let currentY = y let travelRect = CGRect(x: margin + 10, y: currentY, width: contentWidth - 20, height: 35) let travelPath = UIBezierPath(roundedRect: travelRect, cornerRadius: 6) @@ -601,7 +602,7 @@ actor PDFGenerator { } private func drawPOIItem(poi: POISearchService.POI, index: Int, y: CGFloat) -> CGFloat { - var currentY = y + let currentY = y // Number badge let badgeRect = CGRect(x: margin + 10, y: currentY, width: 22, height: 22) @@ -887,7 +888,8 @@ extension UIColor { // MARK: - Export Service -actor ExportService { +@MainActor +final class ExportService { private let pdfGenerator = PDFGenerator() private let assetPrefetcher = PDFAssetPrefetcher() diff --git a/SportsTime/Export/Services/PDFAssetPrefetcher.swift b/SportsTime/Export/Services/PDFAssetPrefetcher.swift index 26f8dcc..b2bc389 100644 --- a/SportsTime/Export/Services/PDFAssetPrefetcher.swift +++ b/SportsTime/Export/Services/PDFAssetPrefetcher.swift @@ -89,11 +89,15 @@ actor PDFAssetPrefetcher { } } + // Create immutable copies for concurrent access + let teamsToFetch = teams + let stadiumsToFetch = stadiums + // Run all fetches in parallel async let routeMapTask = fetchRouteMap(stops: trip.stops) async let cityMapsTask = fetchCityMaps(stops: trip.stops) - async let logosTask = imageService.fetchTeamLogos(teams: teams) - async let photosTask = imageService.fetchStadiumPhotos(stadiums: stadiums) + async let logosTask = imageService.fetchTeamLogos(teams: teamsToFetch) + async let photosTask = imageService.fetchStadiumPhotos(stadiums: stadiumsToFetch) async let poisTask = poiService.findPOIsForCities(stops: trip.stops, limit: 5) // Await each result and update progress @@ -117,13 +121,6 @@ actor PDFAssetPrefetcher { progress.poisComplete = true 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( routeMap: routeMap, cityMaps: cityMaps, @@ -141,7 +138,6 @@ actor PDFAssetPrefetcher { let mapSize = CGSize(width: 512, height: 350) return try await mapService.generateRouteMap(stops: stops, size: mapSize) } catch { - print("[PDFAssetPrefetcher] Route map failed: \(error.localizedDescription)") return nil } } @@ -162,7 +158,6 @@ actor PDFAssetPrefetcher { let map = try await self.mapService.generateCityMap(stop: stop, size: mapSize) return (stop.city, map) } catch { - print("[PDFAssetPrefetcher] City map for \(stop.city) failed: \(error.localizedDescription)") return (stop.city, nil) } } diff --git a/SportsTime/Export/Services/POISearchService.swift b/SportsTime/Export/Services/POISearchService.swift index d135b96..2d9fa71 100644 --- a/SportsTime/Export/Services/POISearchService.swift +++ b/SportsTime/Export/Services/POISearchService.swift @@ -128,7 +128,6 @@ actor POISearchService { limit: limitPerCategory ) } catch { - print("[POISearchService] Search failed for \(category): \(error.localizedDescription)") return [] } } @@ -167,7 +166,6 @@ actor POISearchService { // Take top N overall return (stop.city, Array(pois.prefix(limit))) } catch { - print("[POISearchService] Failed for \(stop.city): \(error.localizedDescription)") return (stop.city, []) } } @@ -208,11 +206,8 @@ actor POISearchService { let pois: [POI] = response.mapItems.prefix(limit).compactMap { item in guard let name = item.name else { return nil } - let itemLocation = CLLocation( - latitude: item.placemark.coordinate.latitude, - longitude: item.placemark.coordinate.longitude - ) - let distance = referenceLocation.distance(from: itemLocation) + let itemCoordinate = item.location.coordinate + let distance = referenceLocation.distance(from: item.location) // Only include POIs within radius guard distance <= radiusMeters else { return nil } @@ -221,22 +216,23 @@ actor POISearchService { id: UUID(), name: name, category: category, - coordinate: item.placemark.coordinate, + coordinate: itemCoordinate, distanceMeters: distance, - address: formatAddress(item.placemark) + address: formatAddress(item) ) } 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] = [] - if let subThoroughfare = placemark.subThoroughfare { + if let subThoroughfare = item.placemark.subThoroughfare { components.append(subThoroughfare) } - if let thoroughfare = placemark.thoroughfare { + if let thoroughfare = item.placemark.thoroughfare { components.append(thoroughfare) } diff --git a/SportsTime/Export/Services/RemoteImageService.swift b/SportsTime/Export/Services/RemoteImageService.swift index 5a4b227..7c732f3 100644 --- a/SportsTime/Export/Services/RemoteImageService.swift +++ b/SportsTime/Export/Services/RemoteImageService.swift @@ -95,7 +95,6 @@ actor RemoteImageService { let image = try await self.fetchImage(from: url) return (url, image) } catch { - print("[RemoteImageService] Failed to fetch \(url): \(error.localizedDescription)") return (url, nil) } } diff --git a/SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift b/SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift index 71abf2c..60b6fbd 100644 --- a/SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift +++ b/SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift @@ -88,11 +88,9 @@ final class ScheduleViewModel { } catch let cloudKitError as CloudKitError { self.error = cloudKitError self.errorMessage = cloudKitError.errorDescription - print("CloudKit error loading games: \(cloudKitError.errorDescription ?? "Unknown")") } catch { self.error = error self.errorMessage = error.localizedDescription - print("Failed to load games: \(error)") } isLoading = false diff --git a/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift b/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift index f5f5eeb..e1f9816 100644 --- a/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift +++ b/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift @@ -78,15 +78,11 @@ final class SettingsViewModel { isSyncing = true syncError = nil - do { - // Trigger data reload from provider - await AppDataProvider.shared.loadInitialData() + // Trigger data reload from provider + await AppDataProvider.shared.loadInitialData() - lastSyncDate = Date() - UserDefaults.standard.set(lastSyncDate, forKey: "lastSyncDate") - } catch { - syncError = error.localizedDescription - } + lastSyncDate = Date() + UserDefaults.standard.set(lastSyncDate, forKey: "lastSyncDate") isSyncing = false } diff --git a/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift b/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift index 2d645f0..3fec394 100644 --- a/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift +++ b/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift @@ -222,128 +222,121 @@ final class TripCreationViewModel { viewState = .planning - do { - // Mode-specific setup - var effectiveStartDate = startDate - var effectiveEndDate = endDate - var resolvedStartLocation: LocationInput? - var resolvedEndLocation: LocationInput? + // Mode-specific setup + var effectiveStartDate = startDate + var effectiveEndDate = endDate + var resolvedStartLocation: LocationInput? + var resolvedEndLocation: LocationInput? - switch planningMode { - case .dateRange: - // Use provided date range, no location needed - // Games will be found within the date range across all regions - effectiveStartDate = startDate - effectiveEndDate = endDate + switch planningMode { + case .dateRange: + // Use provided date range, no location needed + // Games will be found within the date range across all regions + effectiveStartDate = startDate + effectiveEndDate = endDate - case .gameFirst: - // Calculate date range from selected games + buffer - if let dateRange = gameFirstDateRange { - effectiveStartDate = dateRange.start - effectiveEndDate = dateRange.end - } - // Derive start/end locations from first/last game stadiums - 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 { - resolvedStartLocation = LocationInput( - name: firstGame.stadium.city, - coordinate: firstGame.stadium.coordinate, - address: "\(firstGame.stadium.city), \(firstGame.stadium.state)" - ) - resolvedEndLocation = LocationInput( - name: lastGame.stadium.city, - coordinate: lastGame.stadium.coordinate, - 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 - } + case .gameFirst: + // Calculate date range from selected games + buffer + if let dateRange = gameFirstDateRange { + effectiveStartDate = dateRange.start + effectiveEndDate = dateRange.end + } + // Derive start/end locations from first/last game stadiums + 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 { + resolvedStartLocation = LocationInput( + name: firstGame.stadium.city, + coordinate: firstGame.stadium.coordinate, + address: "\(firstGame.stadium.city), \(firstGame.stadium.state)" + ) + resolvedEndLocation = LocationInput( + name: lastGame.stadium.city, + coordinate: lastGame.stadium.coordinate, + address: "\(lastGame.stadium.city), \(lastGame.stadium.state)" + ) } - // Ensure we have games data - if games.isEmpty { - await loadScheduleData() + 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 + 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) - 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 - } - - // 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)) + // Enrich with EV chargers if requested and feature is enabled + if FeatureFlags.enableEVCharging && needsEVCharging { + options = await ItineraryBuilder.enrichWithEVChargers(options) } - } catch { - viewState = .error("Trip planning failed: \(error.localizedDescription)") + // 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)) } } diff --git a/SportsTime/Features/Trip/Views/TripDetailView.swift b/SportsTime/Features/Trip/Views/TripDetailView.swift index a556bd3..69a4e36 100644 --- a/SportsTime/Features/Trip/Views/TripDetailView.swift +++ b/SportsTime/Features/Trip/Views/TripDetailView.swift @@ -62,9 +62,7 @@ struct TripDetailView: View { .toolbar { ToolbarItemGroup(placement: .primaryAction) { Button { - Task { - await shareTrip() - } + shareTrip() } label: { Image(systemName: "square.and.arrow.up") .foregroundStyle(Theme.warmOrange) @@ -429,8 +427,10 @@ struct TripDetailView: View { let destination = stops[i + 1] let request = MKDirections.Request() - request.source = MKMapItem(placemark: MKPlacemark(coordinate: source.coordinate)) - request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destination.coordinate)) + let sourceLocation = CLLocation(latitude: source.coordinate.latitude, longitude: source.coordinate.longitude) + 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 let directions = MKDirections(request: request) @@ -504,14 +504,14 @@ struct TripDetailView: View { exportURL = url showExportSheet = true } catch { - print("Failed to export PDF: \(error)") + // PDF export failed silently } isExporting = false } - private func shareTrip() async { - shareURL = await exportService.shareTrip(trip) + private func shareTrip() { + shareURL = exportService.shareTrip(trip) showShareSheet = true } @@ -525,7 +525,6 @@ struct TripDetailView: View { private func saveTrip() { guard let savedTrip = SavedTrip.from(trip, games: games, status: .planned) else { - print("Failed to create SavedTrip") return } @@ -537,7 +536,7 @@ struct TripDetailView: View { isSaved = true } } catch { - print("Failed to save trip: \(error)") + // Save failed silently } } @@ -557,7 +556,7 @@ struct TripDetailView: View { isSaved = false } } catch { - print("Failed to unsave trip: \(error)") + // Unsave failed silently } } diff --git a/SportsTime/Planning/Engine/GameDAGRouter.swift b/SportsTime/Planning/Engine/GameDAGRouter.swift index 1bc9536..6b6bbb0 100644 --- a/SportsTime/Planning/Engine/GameDAGRouter.swift +++ b/SportsTime/Planning/Engine/GameDAGRouter.swift @@ -92,8 +92,6 @@ enum GameDAGRouter { 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 var beam: [[Game]] = [] @@ -113,10 +111,9 @@ enum GameDAGRouter { } } - print("[GameDAGRouter] Initial beam size: \(beam.count)") // Step 4: Expand beam day by day - for (index, dayIndex) in sortedDays.dropFirst().enumerated() { + for (_, dayIndex) in sortedDays.dropFirst().enumerated() { let todaysGames = buckets[dayIndex] ?? [] var nextBeam: [[Game]] = [] @@ -131,14 +128,11 @@ enum GameDAGRouter { continue } - var addedAny = false - // Try adding each of today's games for candidate in todaysGames { if canTransition(from: lastGame, to: candidate, stadiums: stadiums, constraints: constraints) { let newPath = path + [candidate] nextBeam.append(newPath) - addedAny = true } } @@ -148,7 +142,6 @@ enum GameDAGRouter { // Dominance pruning + beam truncation 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 @@ -160,15 +153,7 @@ enum GameDAGRouter { // Step 6: Ensure geographic diversity in results // Group routes by their primary region (city with most games) // Then pick the best route from each region - let diverseRoutes = 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 + return selectDiverseRoutes(routesWithAnchors, stadiums: stadiums, maxCount: maxOptions) } /// Compatibility wrapper that matches GeographicRouteExplorer's interface. @@ -320,7 +305,6 @@ enum GameDAGRouter { 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 var selectedRoutes: [[Game]] = [] diff --git a/SportsTime/Planning/Engine/ItineraryBuilder.swift b/SportsTime/Planning/Engine/ItineraryBuilder.swift index a2429dd..96cfd04 100644 --- a/SportsTime/Planning/Engine/ItineraryBuilder.swift +++ b/SportsTime/Planning/Engine/ItineraryBuilder.swift @@ -76,14 +76,12 @@ enum ItineraryBuilder { to: toStop, constraints: constraints ) else { - print("\(logPrefix) Failed to estimate travel: \(fromStop.city) -> \(toStop.city)") return nil } // Run optional validator (e.g., arrival time check for Scenario B) if let validator = segmentValidator { if !validator(segment, fromStop, toStop) { - print("\(logPrefix) Segment validation failed: \(fromStop.city) -> \(toStop.city)") return nil } } @@ -97,7 +95,6 @@ enum ItineraryBuilder { // Verify invariant: segments = stops - 1 // ────────────────────────────────────────────────────────────────── guard travelSegments.count == stops.count - 1 else { - print("\(logPrefix) Invariant violated: \(travelSegments.count) segments for \(stops.count) stops") return nil } @@ -147,9 +144,8 @@ enum ItineraryBuilder { searchRadiusMiles: 5.0, intervalMiles: 100.0 ) - print("[ItineraryBuilder] Found \(evChargers.count) EV chargers: \(segment.fromLocation.name) -> \(segment.toLocation.name)") } 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) } catch { - print("[ItineraryBuilder] Route calculation failed, keeping original: \(error.localizedDescription)") + // Route calculation failed - keep original segment enrichedSegments.append(segment) } } @@ -252,7 +248,6 @@ enum ItineraryBuilder { // Get coordinates guard let fromCoord = fromStop.coordinate, let toCoord = toStop.coordinate else { - print("\(logPrefix) Missing coordinates: \(fromStop.city) -> \(toStop.city)") // Fall back to estimate guard let segment = TravelEstimator.estimate( from: fromStop, @@ -283,9 +278,7 @@ enum ItineraryBuilder { searchRadiusMiles: 5.0, intervalMiles: 100.0 ) - print("\(logPrefix) Found \(evChargers.count) EV chargers: \(fromStop.city) -> \(toStop.city)") } catch { - print("\(logPrefix) EV charger search failed: \(error.localizedDescription)") // Continue without chargers - not a critical failure } } @@ -302,7 +295,6 @@ enum ItineraryBuilder { // Run optional validator if let validator = segmentValidator { if !validator(segment, fromStop, toStop) { - print("\(logPrefix) Segment validation failed: \(fromStop.city) -> \(toStop.city)") return nil } } @@ -312,7 +304,6 @@ enum ItineraryBuilder { totalDistance += segment.estimatedDistanceMiles } catch { - print("\(logPrefix) Route calculation failed, using estimate: \(error.localizedDescription)") // Fall back to estimate guard let segment = TravelEstimator.estimate( from: fromStop, @@ -329,7 +320,6 @@ enum ItineraryBuilder { // Verify invariant guard travelSegments.count == stops.count - 1 else { - print("\(logPrefix) Invariant violated: \(travelSegments.count) segments for \(stops.count) stops") return nil } @@ -366,7 +356,6 @@ enum ItineraryBuilder { let deadline = gameStart.addingTimeInterval(-bufferSeconds) if earliestArrival > deadline { - print("[ItineraryBuilder] Cannot arrive in time: earliest arrival \(earliestArrival) > deadline \(deadline)") return false } return true diff --git a/SportsTime/Planning/Engine/ScenarioAPlanner.swift b/SportsTime/Planning/Engine/ScenarioAPlanner.swift index 915a65c..157779e 100644 --- a/SportsTime/Planning/Engine/ScenarioAPlanner.swift +++ b/SportsTime/Planning/Engine/ScenarioAPlanner.swift @@ -68,9 +68,6 @@ final class ScenarioAPlanner: ScenarioPlanner { .filter { dateRange.contains($0.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. if gamesInRange.isEmpty { return .failure( @@ -100,11 +97,6 @@ final class ScenarioAPlanner: ScenarioPlanner { stopBuilder: buildStops ) - print("[ScenarioA] GameDAGRouter returned \(validRoutes.count) routes") - if !validRoutes.isEmpty { - print("[ScenarioA] Route sizes: \(validRoutes.map { $0.count })") - } - if validRoutes.isEmpty { return .failure( PlanningFailure( @@ -136,23 +128,16 @@ final class ScenarioAPlanner: ScenarioPlanner { // Build stops for this route let stops = buildStops(from: routeGames, stadiums: request.stadiums) guard !stops.isEmpty else { - print("[ScenarioA] Route \(index + 1) produced no stops, skipping") routesFailed += 1 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 guard let itinerary = ItineraryBuilder.build( stops: stops, - constraints: request.drivingConstraints, - logPrefix: "[ScenarioA]" + constraints: request.drivingConstraints ) else { // This route fails driving constraints, skip it - print("[ScenarioA] Route \(index + 1) failed driving constraints, skipping") routesFailed += 1 continue } @@ -176,8 +161,6 @@ final class ScenarioAPlanner: ScenarioPlanner { // If no routes passed all constraints, fail. // Otherwise, return all valid options for the user to choose from. // - print("[ScenarioA] Routes attempted: \(routesAttempted), failed: \(routesFailed), succeeded: \(itineraryOptions.count)") - if itineraryOptions.isEmpty { return .failure( PlanningFailure( @@ -201,7 +184,6 @@ final class ScenarioAPlanner: ScenarioPlanner { limit: request.preferences.maxTripOptions ) - print("[ScenarioA] Returning \(rankedOptions.count) itinerary options (leisure: \(leisureLevel.rawValue))") return .success(rankedOptions) } diff --git a/SportsTime/Planning/Engine/ScenarioBPlanner.swift b/SportsTime/Planning/Engine/ScenarioBPlanner.swift index 511d4bf..7fba8da 100644 --- a/SportsTime/Planning/Engine/ScenarioBPlanner.swift +++ b/SportsTime/Planning/Engine/ScenarioBPlanner.swift @@ -168,7 +168,6 @@ final class ScenarioBPlanner: ScenarioPlanner { limit: request.preferences.maxTripOptions ) - print("[ScenarioB] Returning \(rankedOptions.count) itinerary options (leisure: \(leisureLevel.rawValue))") return .success(Array(rankedOptions)) } @@ -249,13 +248,7 @@ final class ScenarioBPlanner: ScenarioPlanner { // Last window: first selected game is on first day of window // Window start = firstGameDate - // Window end = start + duration days let lastWindowStart = firstGameDate - let lastWindowEnd = Calendar.current.date( - byAdding: .day, - value: duration, - to: lastWindowStart - )! // Slide from first window to last window var currentStart = firstWindowStart @@ -277,7 +270,6 @@ final class ScenarioBPlanner: ScenarioPlanner { )! } - print("[ScenarioB] Generated \(dateRanges.count) sliding windows for \(duration)-day trip") return dateRanges } diff --git a/SportsTime/Planning/Engine/ScenarioCPlanner.swift b/SportsTime/Planning/Engine/ScenarioCPlanner.swift index 2c70d3a..88c314a 100644 --- a/SportsTime/Planning/Engine/ScenarioCPlanner.swift +++ b/SportsTime/Planning/Engine/ScenarioCPlanner.swift @@ -175,8 +175,6 @@ final class ScenarioCPlanner: ScenarioPlanner { stadiums: request.stadiums ) - print("[ScenarioC] Found \(directionalStadiums.count) directional stadiums from \(startLocation.name) to \(endLocation.name)") - // ────────────────────────────────────────────────────────────────── // Step 5: For each date range, explore routes // ────────────────────────────────────────────────────────────────── @@ -267,7 +265,6 @@ final class ScenarioCPlanner: ScenarioPlanner { limit: request.preferences.maxTripOptions ) - print("[ScenarioC] Returning \(rankedOptions.count) itinerary options (leisure: \(leisureLevel.rawValue))") return .success(Array(rankedOptions)) } @@ -318,7 +315,6 @@ final class ScenarioCPlanner: ScenarioPlanner { let detourDistance = toStadium + fromStadium // Also check that stadium is making progress (closer to end than start is) - let distanceFromStart = distanceBetween(start, stadiumCoord) let distanceToEnd = distanceBetween(stadiumCoord, 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 } @@ -561,7 +556,6 @@ final class ScenarioCPlanner: ScenarioPlanner { // Allow increases up to tolerance percentage let allowedIncrease = prev * forwardProgressTolerance if currentDistance > prev + allowedIncrease { - print("[ScenarioC] Backtracking: \(stop.city) increases distance to end (\(Int(currentDistance))mi vs \(Int(prev))mi)") return false } } diff --git a/SportsTime/Planning/Engine/TripPlanningEngine.swift b/SportsTime/Planning/Engine/TripPlanningEngine.swift index b024b8a..1d99dc3 100644 --- a/SportsTime/Planning/Engine/TripPlanningEngine.swift +++ b/SportsTime/Planning/Engine/TripPlanningEngine.swift @@ -19,23 +19,9 @@ final class TripPlanningEngine { func planItineraries(request: PlanningRequest) -> ItineraryResult { // Detect scenario and get the appropriate planner - let scenario = ScenarioPlannerFactory.classify(request) let planner = ScenarioPlannerFactory.planner(for: request) - print("[TripPlanningEngine] Detected scenario: \(scenario)") - print("[TripPlanningEngine] Using planner: \(type(of: planner))") - // Delegate to the scenario planner - let result = 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 + return planner.plan(request: request) } }