diff --git a/SportsTime/Core/Services/AppleMapsLauncher.swift b/SportsTime/Core/Services/AppleMapsLauncher.swift new file mode 100644 index 0000000..1075c4a --- /dev/null +++ b/SportsTime/Core/Services/AppleMapsLauncher.swift @@ -0,0 +1,136 @@ +// +// 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]) + } + + // 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..= StoreManager.freeTripLimit {