// // AppleMapsLauncher.swift // SportsTime // // Service for opening trip routes in Apple Maps // import Foundation import MapKit import CoreLocation /// Result of preparing waypoints for Apple Maps enum AppleMapsLaunchResult { /// Single route can be opened directly (≤16 waypoints) case ready([MKMapItem]) /// Multiple routes needed (>16 waypoints) case multipleRoutes([[MKMapItem]]) /// No routable waypoints available case noWaypoints } /// Handles opening trip routes in Apple Maps with chunking for large trips struct AppleMapsLauncher { /// Maximum waypoints Apple Maps supports in a single route static let maxWaypoints = 16 /// Represents a waypoint for the route struct Waypoint { let name: String let coordinate: CLLocationCoordinate2D let sortKey: (day: Int, order: Double) } // MARK: - Public API /// Prepares waypoints from trip stops and custom items /// - Parameters: /// - stops: Trip stops with optional coordinates /// - customItems: Custom itinerary items that may have coordinates /// - Returns: Launch result indicating if route is ready or needs chunking static func prepare( stops: [TripStop], customItems: [ItineraryItem] ) -> AppleMapsLaunchResult { let waypoints = collectWaypoints(stops: stops, customItems: customItems) guard !waypoints.isEmpty else { return .noWaypoints } let mapItems = waypoints.map { waypoint -> MKMapItem in let placemark = MKPlacemark(coordinate: waypoint.coordinate) let item = MKMapItem(placemark: placemark) item.name = waypoint.name return item } if mapItems.count <= maxWaypoints { return .ready(mapItems) } else { let chunks = chunk(mapItems, size: maxWaypoints) return .multipleRoutes(chunks) } } /// Opens a single route in Apple Maps /// - Parameter mapItems: Array of map items (≤16) static func open(_ mapItems: [MKMapItem]) { guard !mapItems.isEmpty else { return } MKMapItem.openMaps(with: mapItems, launchOptions: [ MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving ]) } /// Opens a specific chunk of a multi-route trip /// - Parameters: /// - chunks: All route chunks /// - index: Which chunk to open (0-based) static func open(chunk index: Int, of chunks: [[MKMapItem]]) { guard index >= 0 && index < chunks.count else { return } open(chunks[index]) } /// Opens a single location in Apple Maps /// - Parameters: /// - coordinate: Location to display /// - name: Display name for the pin static func openLocation(coordinate: CLLocationCoordinate2D, name: String) { let placemark = MKPlacemark(coordinate: coordinate) let mapItem = MKMapItem(placemark: placemark) mapItem.name = name mapItem.openInMaps(launchOptions: nil) } /// Opens driving directions between two points in Apple Maps /// - Parameters: /// - from: Starting location /// - fromName: Display name for origin /// - to: Destination location /// - toName: Display name for destination static func openDirections( from: CLLocationCoordinate2D, fromName: String, to: CLLocationCoordinate2D, toName: String ) { let fromPlacemark = MKPlacemark(coordinate: from) let fromItem = MKMapItem(placemark: fromPlacemark) fromItem.name = fromName let toPlacemark = MKPlacemark(coordinate: to) let toItem = MKMapItem(placemark: toPlacemark) toItem.name = toName MKMapItem.openMaps(with: [fromItem, toItem], launchOptions: [ MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving ]) } // MARK: - Private Helpers private static func collectWaypoints( stops: [TripStop], customItems: [ItineraryItem] ) -> [Waypoint] { var waypoints: [Waypoint] = [] // Add trip stops with coordinates for (index, stop) in stops.enumerated() { guard let coordinate = stop.coordinate else { continue } waypoints.append(Waypoint( name: stop.city, coordinate: coordinate, // Use stop number for day approximation, index for order sortKey: (day: stop.stopNumber, order: Double(index)) )) } // Add mappable custom items for item in customItems { guard let info = item.customInfo, let coordinate = info.coordinate else { continue } waypoints.append(Waypoint( name: info.title, coordinate: coordinate, sortKey: (day: item.day, order: item.sortOrder) )) } // Sort by day, then by sort order within day waypoints.sort { lhs, rhs in if lhs.sortKey.day != rhs.sortKey.day { return lhs.sortKey.day < rhs.sortKey.day } return lhs.sortKey.order < rhs.sortKey.order } return waypoints } private static func chunk(_ items: [MKMapItem], size: Int) -> [[MKMapItem]] { stride(from: 0, to: items.count, by: size).map { start in Array(items[start..