// // PDFAssetPrefetcher.swift // SportsTime // // Orchestrates parallel prefetching of all assets needed for PDF generation. // import Foundation import UIKit actor PDFAssetPrefetcher { // MARK: - Types struct PrefetchedAssets { let routeMap: UIImage? let cityMaps: [String: UIImage] let teamLogos: [String: UIImage] let stadiumPhotos: [String: UIImage] let cityPOIs: [String: [POISearchService.POI]] var isEmpty: Bool { routeMap == nil && cityMaps.isEmpty && teamLogos.isEmpty && stadiumPhotos.isEmpty && cityPOIs.isEmpty } } struct PrefetchProgress { var routeMapComplete: Bool = false var cityMapsComplete: Bool = false var logosComplete: Bool = false var photosComplete: Bool = false var poisComplete: Bool = false var percentComplete: Double { let completedSteps = [routeMapComplete, cityMapsComplete, logosComplete, photosComplete, poisComplete] .filter { $0 }.count return Double(completedSteps) / 5.0 } var currentStep: String { if !routeMapComplete { return "Generating route map..." } if !cityMapsComplete { return "Generating city maps..." } if !logosComplete { return "Downloading team logos..." } if !photosComplete { return "Downloading stadium photos..." } if !poisComplete { return "Finding nearby attractions..." } return "Complete" } } // MARK: - Properties private let mapService = MapSnapshotService() private let imageService = RemoteImageService() private let poiService = POISearchService() // MARK: - Public Methods /// Prefetch all assets needed for PDF generation /// - Parameters: /// - trip: The trip to generate PDF for /// - games: Map of game IDs to RichGame objects /// - progressCallback: Optional callback for progress updates /// - Returns: All prefetched assets func prefetchAssets( for trip: Trip, games: [String: RichGame], progressCallback: ((PrefetchProgress) async -> Void)? = nil ) async -> PrefetchedAssets { var progress = PrefetchProgress() // Collect unique teams and stadiums from games var teams: [Team] = [] var stadiums: [Stadium] = [] var seenTeamIds: Set = [] var seenStadiumIds: Set = [] for (_, richGame) in games { if !seenTeamIds.contains(richGame.homeTeam.id) { teams.append(richGame.homeTeam) seenTeamIds.insert(richGame.homeTeam.id) } if !seenTeamIds.contains(richGame.awayTeam.id) { teams.append(richGame.awayTeam) seenTeamIds.insert(richGame.awayTeam.id) } if !seenStadiumIds.contains(richGame.stadium.id) { stadiums.append(richGame.stadium) seenStadiumIds.insert(richGame.stadium.id) } } // 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: teamsToFetch) async let photosTask = imageService.fetchStadiumPhotos(stadiums: stadiumsToFetch) async let poisTask = poiService.findPOIsForCities(stops: trip.stops, limit: 5) // Await each result and update progress let routeMap = await routeMapTask progress.routeMapComplete = true await progressCallback?(progress) let cityMaps = await cityMapsTask progress.cityMapsComplete = true await progressCallback?(progress) let teamLogos = await logosTask progress.logosComplete = true await progressCallback?(progress) let stadiumPhotos = await photosTask progress.photosComplete = true await progressCallback?(progress) let cityPOIs = await poisTask progress.poisComplete = true await progressCallback?(progress) return PrefetchedAssets( routeMap: routeMap, cityMaps: cityMaps, teamLogos: teamLogos, stadiumPhotos: stadiumPhotos, cityPOIs: cityPOIs ) } // MARK: - Private Methods private func fetchRouteMap(stops: [TripStop]) async -> UIImage? { do { // PDF page is 612 wide, minus margins let mapSize = CGSize(width: 512, height: 350) return try await mapService.generateRouteMap(stops: stops, size: mapSize) } catch { return nil } } private func fetchCityMaps(stops: [TripStop]) async -> [String: UIImage] { var results: [String: UIImage] = [:] let mapSize = CGSize(width: 250, height: 200) await withTaskGroup(of: (String, UIImage?).self) { group in // Deduplicate cities var seenCities: Set = [] for stop in stops { guard !seenCities.contains(stop.city) else { continue } seenCities.insert(stop.city) group.addTask { do { let map = try await self.mapService.generateCityMap(stop: stop, size: mapSize) return (stop.city, map) } catch { return (stop.city, nil) } } } for await (city, map) in group { if let map = map { results[city] = map } } } return results } }