// // LocationService.swift // SportsTime // import Foundation import CoreLocation import MapKit extension MKPolyline: @unchecked Sendable {} actor LocationService { static let shared = LocationService() private init() {} // MARK: - Geocoding func geocode(_ address: String) async throws -> CLLocationCoordinate2D? { 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 request = MKLocalSearch.Request() request.region = MKCoordinateRegion( center: coordinate, latitudinalMeters: 100, longitudinalMeters: 100 ) request.resultTypes = .address let search = MKLocalSearch(request: request) let response = try await search.start() guard let item = response.mapItems.first else { return nil } return formatMapItem(item) } func resolveLocation(_ input: LocationInput) async throws -> LocationInput { if input.isResolved { return input } let searchText = input.address ?? input.name guard let coordinate = try await geocode(searchText) else { throw LocationError.geocodingFailed } return LocationInput( name: input.name, coordinate: coordinate, address: input.address ) } // MARK: - Location Search func searchLocations(_ query: String) async throws -> [LocationSearchResult] { guard !query.trimmingCharacters(in: .whitespaces).isEmpty else { return [] } let request = MKLocalSearch.Request() request.naturalLanguageQuery = query request.resultTypes = [.address, .pointOfInterest] let search = MKLocalSearch(request: request) let response = try await search.start() return response.mapItems.map { item in LocationSearchResult( name: item.name ?? "Unknown", address: formatMapItem(item), coordinate: item.location.coordinate ) } } @available(iOS, deprecated: 26.0, message: "Uses placemark for address formatting") private func formatMapItem(_ item: MKMapItem) -> String { var components: [String] = [] 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: ", ") } // MARK: - Distance Calculations func calculateDistance( from: CLLocationCoordinate2D, to: CLLocationCoordinate2D ) -> CLLocationDistance { let fromLocation = CLLocation(latitude: from.latitude, longitude: from.longitude) let toLocation = CLLocation(latitude: to.latitude, longitude: to.longitude) return fromLocation.distance(from: toLocation) } func calculateDrivingRoute( from: CLLocationCoordinate2D, to: CLLocationCoordinate2D ) async throws -> RouteInfo { let request = MKDirections.Request() 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 let directions = MKDirections(request: request) let response = try await directions.calculate() guard let route = response.routes.first else { throw LocationError.routeNotFound } return RouteInfo( distance: route.distance, expectedTravelTime: route.expectedTravelTime, polyline: route.polyline ) } func calculateDrivingMatrix( origins: [CLLocationCoordinate2D], destinations: [CLLocationCoordinate2D] ) async throws -> [[RouteInfo?]] { var matrix: [[RouteInfo?]] = [] for origin in origins { var row: [RouteInfo?] = [] for destination in destinations { do { let route = try await calculateDrivingRoute(from: origin, to: destination) row.append(route) } catch { row.append(nil) } } matrix.append(row) } return matrix } } // MARK: - Route Info struct RouteInfo: Sendable { let distance: CLLocationDistance // meters let expectedTravelTime: TimeInterval // seconds nonisolated(unsafe) let polyline: MKPolyline? var distanceMiles: Double { distance * 0.000621371 } var travelTimeHours: Double { expectedTravelTime / 3600.0 } } // MARK: - Location Search Result struct LocationSearchResult: Identifiable, Hashable { let id = UUID() let name: String let address: String let coordinate: CLLocationCoordinate2D var displayName: String { if address.isEmpty || name == address { return name } return "\(name), \(address)" } func toLocationInput() -> LocationInput { LocationInput( name: name, coordinate: coordinate, address: address.isEmpty ? nil : address ) } } // MARK: - Errors enum LocationError: Error, LocalizedError { case geocodingFailed case routeNotFound case permissionDenied var errorDescription: String? { switch self { case .geocodingFailed: return "Unable to find location" case .routeNotFound: return "Unable to calculate route" case .permissionDenied: return "Location permission required" } } }