From 9e7e860f2c3bfaabbf10c14ffce615a1876b3e7e Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Tue, 1 Oct 2019 17:00:46 -0600 Subject: [PATCH] Starting on Coder integration and setting up proper swift error type --- MapboxDirections.xcodeproj/project.pbxproj | 26 + .../MapboxDirections/DirectionsError.swift | 78 +++ .../MapboxDirections/DirectionsRespone.swift | 60 ++ .../Extensions/CLLocationCoordinate2D.swift | 26 +- .../MapboxDirections/Extensions/Codable.swift | 53 ++ .../Extensions/CoreLocation.Swift | 29 + .../MapboxDirections/Extensions/GeoJSON.swift | 49 ++ Sources/MapboxDirections/MBCongestion.swift | 36 +- Sources/MapboxDirections/MBDirections.swift | 182 +++-- .../MapboxDirections/MBDirectionsResult.swift | 410 ++++++------ Sources/MapboxDirections/MBRoute.swift | 222 +++++-- Sources/MapboxDirections/MBRouteLeg.swift | 3 +- Sources/MapboxDirections/MBRouteOptions.swift | 629 ++++++++++++------ Sources/MapboxDirections/MBWaypoint.swift | 229 ++----- 14 files changed, 1247 insertions(+), 785 deletions(-) create mode 100644 Sources/MapboxDirections/DirectionsError.swift create mode 100644 Sources/MapboxDirections/DirectionsRespone.swift create mode 100644 Sources/MapboxDirections/Extensions/Codable.swift create mode 100644 Sources/MapboxDirections/Extensions/CoreLocation.Swift create mode 100644 Sources/MapboxDirections/Extensions/GeoJSON.swift diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index 5b4d9ae47..615b2d16d 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -39,10 +39,18 @@ 35EFD00C207DFACA00BF3873 /* MBVisualInstruction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35EFD00A207DFACA00BF3873 /* MBVisualInstruction.swift */; }; 35EFD00D207DFACA00BF3873 /* MBVisualInstruction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35EFD00A207DFACA00BF3873 /* MBVisualInstruction.swift */; }; 35EFD00E207DFACA00BF3873 /* MBVisualInstruction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35EFD00A207DFACA00BF3873 /* MBVisualInstruction.swift */; }; + 43208BA72343F7C300D8BD89 /* Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43208BA62343F7C300D8BD89 /* Codable.swift */; }; + 43208BA92343F7E900D8BD89 /* CoreLocation.Swift in Sources */ = {isa = PBXBuildFile; fileRef = 43208BA82343F7E900D8BD89 /* CoreLocation.Swift */; }; + 43208BAB2343F81900D8BD89 /* GeoJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43208BAA2343F81900D8BD89 /* GeoJSON.swift */; }; + 43208BAD2343FF5500D8BD89 /* DirectionsRespone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43208BAC2343FF5500D8BD89 /* DirectionsRespone.swift */; }; 438BFEC2233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; 438BFEC3233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; 438BFEC4233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; 438BFEC5233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; + 439255772344113B006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; + 439255792344113D006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; + 4392557A2344113E006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; + 4392557B2344113F006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; 8D381B611FD9F5B1008D5A58 /* noDestinationName.json in Resources */ = {isa = PBXBuildFile; fileRef = 8D381B601FD9F5B1008D5A58 /* noDestinationName.json */; }; 8D381B631FDB01D1008D5A58 /* apiDestinationName.json in Resources */ = {isa = PBXBuildFile; fileRef = 8D381B621FDB01D1008D5A58 /* apiDestinationName.json */; }; 8D381B641FDB0898008D5A58 /* noDestinationName.json in Resources */ = {isa = PBXBuildFile; fileRef = 8D381B601FD9F5B1008D5A58 /* noDestinationName.json */; }; @@ -302,9 +310,14 @@ 35DBF013217E199E0009D2AE /* OfflineDirectionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineDirectionsTests.swift; sourceTree = ""; }; 35DBF018217F38A30009D2AE /* versions.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = versions.json; sourceTree = ""; }; 35EFD00A207DFACA00BF3873 /* MBVisualInstruction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBVisualInstruction.swift; sourceTree = ""; }; + 43208BA62343F7C300D8BD89 /* Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Codable.swift; sourceTree = ""; }; + 43208BA82343F7E900D8BD89 /* CoreLocation.Swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreLocation.Swift; sourceTree = ""; }; + 43208BAA2343F81900D8BD89 /* GeoJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSON.swift; sourceTree = ""; }; + 43208BAC2343FF5500D8BD89 /* DirectionsRespone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsRespone.swift; sourceTree = ""; }; 438BFEBC233D7FA900457294 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 438BFEC0233D805500457294 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsProfileIdentifier.swift; sourceTree = ""; }; + 4392557523440EC2006EEE88 /* DirectionsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsError.swift; sourceTree = ""; }; 8D381B601FD9F5B1008D5A58 /* noDestinationName.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = noDestinationName.json; sourceTree = ""; }; 8D381B621FDB01D1008D5A58 /* apiDestinationName.json */ = {isa = PBXFileReference; explicitFileType = text.json; path = apiDestinationName.json; sourceTree = ""; }; 8D381B691FDB101F008D5A58 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; @@ -503,6 +516,9 @@ isa = PBXGroup; children = ( 8D381B691FDB101F008D5A58 /* String.swift */, + 43208BA62343F7C300D8BD89 /* Codable.swift */, + 43208BA82343F7E900D8BD89 /* CoreLocation.Swift */, + 43208BAA2343F81900D8BD89 /* GeoJSON.swift */, C582BA2D2073ED6300647DAA /* Array.swift */, 35DBF009217E172C0009D2AE /* CLLocationCoordinate2D.swift */, 8D434678219E1167008B7BF3 /* Double.swift */, @@ -569,7 +585,9 @@ C5DAAC9E20191683001F9261 /* Match */, DA6C9D8A1CAE442B00094FBC /* MapboxDirections.h */, DD6254731AE70CB700017857 /* MBDirections.swift */, + 4392557523440EC2006EEE88 /* DirectionsError.swift */, C59094C0203DE6BC00EB2417 /* MBDirectionsResult.swift */, + 43208BAC2343FF5500D8BD89 /* DirectionsRespone.swift */, DAC05F171CFC075300FA0071 /* MBRoute.swift */, DAC05F191CFC077C00FA0071 /* MBRouteLeg.swift */, DA2E03EA1CB0E13D00D1269A /* MBRouteOptions.swift */, @@ -1160,6 +1178,7 @@ C54549FC2073F1EF002E273F /* Array.swift in Sources */, C547EC691DB59F8F009817F3 /* MBLane.swift in Sources */, AEDC212120B6125C0052DED8 /* MBComponentRepresentable.swift in Sources */, + 439255792344113D006EEE88 /* DirectionsError.swift in Sources */, DA1A10C71D00F969009F82FA /* MBDirections.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1221,6 +1240,7 @@ C54549FD2073F1F0002E273F /* Array.swift in Sources */, C547EC6A1DB59F90009817F3 /* MBLane.swift in Sources */, AEDC212220B6125D0052DED8 /* MBComponentRepresentable.swift in Sources */, + 4392557A2344113E006EEE88 /* DirectionsError.swift in Sources */, DA1A10ED1D010247009F82FA /* MBDirections.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1282,6 +1302,7 @@ C54549FE2073F1F1002E273F /* Array.swift in Sources */, C547EC6B1DB59F91009817F3 /* MBLane.swift in Sources */, AEDC212320B6125E0052DED8 /* MBComponentRepresentable.swift in Sources */, + 4392557B2344113F006EEE88 /* DirectionsError.swift in Sources */, DA1A11041D0103A3009F82FA /* MBDirections.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1321,6 +1342,7 @@ C56516841FE1A2DD00A0AD18 /* MBVisualInstructionType.swift in Sources */, C57D55011DB5669600B94B74 /* MBIntersection.swift in Sources */, AEDC211D20B6104B0052DED8 /* MBComponentRepresentable.swift in Sources */, + 439255772344113B006EEE88 /* DirectionsError.swift in Sources */, DA2E03E91CB0E0B000D1269A /* MBRouteStep.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1352,8 +1374,12 @@ buildActionMask = 2147483647; files = ( DADD27BA1E5AAAD800D31FAD /* ViewController.swift in Sources */, + 43208BA92343F7E900D8BD89 /* CoreLocation.Swift in Sources */, C59094B5203B5C7F00EB2417 /* MBDrawingView.swift in Sources */, + 43208BA72343F7C300D8BD89 /* Codable.swift in Sources */, DADD27B81E5AAAD800D31FAD /* AppDelegate.swift in Sources */, + 43208BAB2343F81900D8BD89 /* GeoJSON.swift in Sources */, + 43208BAD2343FF5500D8BD89 /* DirectionsRespone.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift new file mode 100644 index 000000000..46a1e83fd --- /dev/null +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -0,0 +1,78 @@ +import Foundation +import CoreLocation + + +public enum DirectionsError: RawRepresentable { + public init?(rawValue: String) { + assertionFailure("Do not use init(rawValue:) for DirectionsError.") + return nil + } + + public var rawValue: String { + return """ + Error: \(String(describing: self)) + Failure Reason: \(failureReason) + Recovery Suggestion: \(recoverySuggestion) + """ + } + public var failureReason: String { + switch self { + case .noData: + return "No data was returned from the server." + case .unableToRoute: + return "No route could be found between the specified locations." + case .unableToLocate: + return "A specified location could not be associated with a roadway or pathway." + case .profileNotFound: + return "Unrecognized profile identifier." + case .requestTooLarge: + return "The request is too large." + case let .rateLimited(rateLimitInterval: interval, rateLimit: limit, _): + let intervalFormatter = DateComponentsFormatter() + intervalFormatter.unitsStyle = .full + guard let interval = interval, let limit = limit else { + return "Too many requests." + } + let formattedInterval = intervalFormatter.string(from: interval) ?? "\(interval) seconds" + let formattedCount = NumberFormatter.localizedString(from: NSNumber(value: limit), number: .decimal) + return "More than \(formattedCount) requests have been made with this access token within a period of \(formattedInterval)" + case let .unknown(response: response, underlying: error): + return "Unknown Error. Response: \(response.debugDescription) Underlying Error: \(error.debugDescription)" + + } + } + + public var recoverySuggestion: String { + switch self { + case .noData: + return "Make sure you have an active internet connection." + case .unableToRoute: + return "Make sure it is possible to travel between the locations with the mode of transportation implied by the profileIdentifier option. For example, it is impossible to travel by car from one continent to another without either a land bridge or a ferry connection." + case .unableToLocate: + return "Make sure the locations are close enough to a roadway or pathway. Try setting the coordinateAccuracy property of all the waypoints to a negative value." + case .profileNotFound: + return "Make sure the profileIdentifier option is set to one of the provided constants, such as MBDirectionsProfileIdentifierAutomobile." + case .requestTooLarge: + return "Try specifying fewer waypoints or giving the waypoints shorter names." + case let .rateLimited(rateLimitInterval: _, rateLimit: _, resetTime: reset): + guard let reset = reset else { + return "Wait a little while before retrying." + } + let formattedDate: String = DateFormatter.localizedString(from: reset, dateStyle: .long, timeStyle: .long) + return "Wait until \(formattedDate) before retrying." + case .unknown(_,_): + return "Please contact Mapbox Support." + } + } + public typealias RawValue = String + + + case noData + case unableToRoute + case unableToLocate + case profileNotFound + case requestTooLarge + case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?) + case unknown(response: URLResponse?, underlying: Error?) + +} diff --git a/Sources/MapboxDirections/DirectionsRespone.swift b/Sources/MapboxDirections/DirectionsRespone.swift new file mode 100644 index 000000000..ea4d886ba --- /dev/null +++ b/Sources/MapboxDirections/DirectionsRespone.swift @@ -0,0 +1,60 @@ +import Foundation + +struct DirectionsResponse: Codable { + + enum CodingKeys: String, CodingKey { + case code + case message + case error + case uuid + case routes + case waypoints + } + + var code: String? + var message: String? + var error: String? + let uuid: String? + let routes: [Route]? + let waypoints: [Waypoint]? + + init(code: String?, message: String?, error: String?) { + self.code = code + self.message = message + self.error = error + self.uuid = nil + self.routes = nil + self.waypoints = nil + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let uuid = try container.decodeIfPresent(String.self, forKey: .uuid) + self.uuid = uuid + + let waypoints = try container.decodeIfPresent([Waypoint].self, forKey: .waypoints) + self.waypoints = waypoints + + let rawRoutes = try container.decodeIfPresent([Route].self, forKey: .routes) + var routesWithDestinations: [Route]? = rawRoutes + if let destinations = waypoints?.dropFirst() { + routesWithDestinations = rawRoutes?.map({ (route) -> Route in + for (leg, destination) in zip(route.legs, destinations) { + if leg.destination?.name?.nonEmptyString == nil { + leg.destination = destination + } + } + return route + }) + } + + let routesWithIdentifiers = routesWithDestinations?.map({ (route) -> Route in + route.routeIdentifier = uuid + return route + }) + + self.routes = routesWithIdentifiers + + } +} diff --git a/Sources/MapboxDirections/Extensions/CLLocationCoordinate2D.swift b/Sources/MapboxDirections/Extensions/CLLocationCoordinate2D.swift index 91ac34cca..efabd1052 100644 --- a/Sources/MapboxDirections/Extensions/CLLocationCoordinate2D.swift +++ b/Sources/MapboxDirections/Extensions/CLLocationCoordinate2D.swift @@ -1,6 +1,7 @@ import Foundation import CoreLocation +#warning("Check this") extension CLLocation { /** Initializes a CLLocation object with the given coordinate pair. @@ -11,30 +12,7 @@ extension CLLocation { } extension CLLocationCoordinate2D { - /** - Initializes a coordinate pair based on the given GeoJSON coordinates array. - */ - internal init(geoJSON array: [Double]) { - assert(array.count == 2) - self.init(latitude: array[1], longitude: array[0]) - } - - /** - Initializes a coordinate pair based on the given GeoJSON point object. - */ - internal init(geoJSON point: JSONDictionary) { - assert(point["type"] as? String == "Point") - self.init(geoJSON: point["coordinates"] as! [Double]) - } - - internal static func coordinates(geoJSON lineString: JSONDictionary) -> [CLLocationCoordinate2D] { - let type = lineString["type"] as? String - assert(type == "LineString" || type == "Point") - let coordinates = lineString["coordinates"] as! [[Double]] - return coordinates.map { self.init(geoJSON: $0) } - } - - /** +/** A string representation of the coordinate suitable for insertion in a Directions API request URL. */ internal var stringForRequestURL: String? { diff --git a/Sources/MapboxDirections/Extensions/Codable.swift b/Sources/MapboxDirections/Extensions/Codable.swift new file mode 100644 index 000000000..001b5e759 --- /dev/null +++ b/Sources/MapboxDirections/Extensions/Codable.swift @@ -0,0 +1,53 @@ +import Foundation +import Polyline +import CoreLocation + + +extension Decodable { + static internal func from(json: String, using encoding: String.Encoding = .utf8) -> T? { + guard let data = json.data(using: encoding) else { return nil } + return from(data: data) as T? + } + + static internal func from(data: Data) -> T? { + let decoder = JSONDecoder() + return try! decoder.decode(T.self, from: data) as T? + } +} + +struct UncertainCodable: Codable { + var t: T? + var u: U? + + var value: Codable? { + return t ?? u + } + + var coordinates: [CLLocationCoordinate2D] { + if let geo = value as? Geometry { + return geo.coordinates + } else if let geo = value as? String { + return decodePolyline(geo, precision: 1e5)! + } else { + return [] + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + t = try? container.decode(T.self) + if t == nil { + u = try? container.decode(U.self) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + if let t = t { + try? container.encode(t) + } + if let u = u { + try? container.encode(u) + } + } +} diff --git a/Sources/MapboxDirections/Extensions/CoreLocation.Swift b/Sources/MapboxDirections/Extensions/CoreLocation.Swift new file mode 100644 index 000000000..7150a90b6 --- /dev/null +++ b/Sources/MapboxDirections/Extensions/CoreLocation.Swift @@ -0,0 +1,29 @@ +import Foundation +import CoreLocation + +extension CLLocationCoordinate2D: Codable { + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(longitude) + try container.encode(latitude) + } + + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + longitude = try container.decode(CLLocationDegrees.self) + latitude = try container.decode(CLLocationDegrees.self) + } + + static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { + return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude + } +} + +extension CLLocation { + /** + Initializes a CLLocation object with the given coordinate pair. + */ + internal convenience init(coordinate: CLLocationCoordinate2D) { + self.init(latitude: coordinate.latitude, longitude: coordinate.longitude) + } +} diff --git a/Sources/MapboxDirections/Extensions/GeoJSON.swift b/Sources/MapboxDirections/Extensions/GeoJSON.swift new file mode 100644 index 000000000..04fc9293f --- /dev/null +++ b/Sources/MapboxDirections/Extensions/GeoJSON.swift @@ -0,0 +1,49 @@ +import Foundation +import CoreLocation + +struct Geometry: Codable { + enum GeometryType: String, CustomStringConvertible, Codable { + case point = "Point" + case lineString = "LineString" + + var description: String { + switch self { + case .point: + return "Point" + case .lineString: + return "LineString" + } + } + } + + let coordinates: [CLLocationCoordinate2D] + let type: GeometryType + + private enum CodingKeys: String, CodingKey { + case coordinates + case type + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try container.decode(GeometryType.self, forKey: .type) + switch type { + case .lineString: + coordinates = try container.decode([CLLocationCoordinate2D].self, forKey: .coordinates) + case .point: + coordinates = [try container.decode(CLLocationCoordinate2D.self, forKey: .coordinates)] + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type, forKey: .type) + + switch type { + case .lineString: + try container.encode(coordinates, forKey: .coordinates) + case .point: + try container.encode(coordinates.first!, forKey: .coordinates) + } + } +} diff --git a/Sources/MapboxDirections/MBCongestion.swift b/Sources/MapboxDirections/MBCongestion.swift index aba2c5fd7..c1a3ee404 100644 --- a/Sources/MapboxDirections/MBCongestion.swift +++ b/Sources/MapboxDirections/MBCongestion.swift @@ -4,7 +4,7 @@ import Foundation A `CongestionLevel` indicates the level of traffic congestion along a road segment relative to the normal flow of traffic along that segment. You can color-code a route line according to the congestion level along each segment of the route. */ -public enum CongestionLevel: Int, CustomStringConvertible { +public enum CongestionLevel: String, Codable{ /** There is not enough data to determine the level of congestion along the road segment. */ @@ -37,38 +37,4 @@ public enum CongestionLevel: Int, CustomStringConvertible { Severe congestion levels are conventionally highlighted in red. */ case severe - - public init?(description: String) { - let level: CongestionLevel - switch description { - case "unknown": - level = .unknown - case "low": - level = .low - case "moderate": - level = .moderate - case "heavy": - level = .heavy - case "severe": - level = .severe - default: - return nil - } - self.init(rawValue: level.rawValue) - } - - public var description: String { - switch self { - case .unknown: - return "unknown" - case .low: - return "low" - case .moderate: - return "moderate" - case .heavy: - return "heavy" - case .severe: - return "severe" - } - } } diff --git a/Sources/MapboxDirections/MBDirections.swift b/Sources/MapboxDirections/MBDirections.swift index b12b2f430..5df3af858 100644 --- a/Sources/MapboxDirections/MBDirections.swift +++ b/Sources/MapboxDirections/MBDirections.swift @@ -79,7 +79,7 @@ open class Directions: NSObject { If the request was canceled or there was an error obtaining the routes, this parameter is `nil`. This is not to be confused with the situation in which no results were found, in which case the array is present but empty. - parameter error: The error that occurred, or `nil` if the placemarks were obtained successfully. */ - public typealias RouteCompletionHandler = (_ waypoints: [Waypoint]?, _ routes: [Route]?, _ error: NSError?) -> Void + public typealias RouteCompletionHandler = (_ waypoints: [Waypoint]?, _ routes: [Route]?, _ error: DirectionsError?) -> Void /** A closure (block) to be called when a map matching request is complete. @@ -144,6 +144,7 @@ open class Directions: NSObject { } // MARK: Getting Directions + /** Begins asynchronously calculating routes using the given options and delivers the results to a closure. @@ -158,19 +159,34 @@ open class Directions: NSObject { */ @discardableResult open func calculate(_ options: RouteOptions, completionHandler: @escaping RouteCompletionHandler) -> URLSessionDataTask { - let fetchStartDate = Date() - let task = dataTask(forCalculating: options, completionHandler: { (json) in - let responseEndDate = Date() - let response = options.response(from: json) - if let routes = response.1 { - self.postprocess(routes, fetchStartDate: fetchStartDate, responseEndDate: responseEndDate, uuid: json["uuid"] as? String) + let fetchStart = Date() + let requestURL = url(forCalculating: options) + let request = URLRequest(url: requestURL) + let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in + guard let data = possibleData else { + completionHandler(nil, nil, .noData) + } + if let error = possibleError { + completionHandler(nil, nil, .unknown(underlying: error)) } - completionHandler(response.0, response.1, nil) - }) { (error) in - completionHandler(nil, nil, error) + + } - task.resume() - return task + + +// let fetchStartDate = Date() +// let task = dataTask(forCalculating: options, completionHandler: { (json) in +// let responseEndDate = Date() +// let response = options.response(from: json) +// if let routes = response.1 { +// self.postprocess(routes, fetchStartDate: fetchStartDate, responseEndDate: responseEndDate, uuid: json["uuid"] as? String) +// } +// completionHandler(response.0, response.1, nil) +// }) { (error) in +// completionHandler(nil, nil, error) +// } +// task.resume() +// return task } /** @@ -186,19 +202,19 @@ open class Directions: NSObject { */ @discardableResult open func calculate(_ options: MatchOptions, completionHandler: @escaping MatchCompletionHandler) -> URLSessionDataTask { - let fetchStartDate = Date() - let task = dataTask(forCalculating: options, completionHandler: { (json) in - let responseEndDate = Date() - let response = options.response(from: json) - if let matches = response { - self.postprocess(matches, fetchStartDate: fetchStartDate, responseEndDate: responseEndDate, uuid: json["uuid"] as? String) - } - completionHandler(response, nil) - }) { (error) in - completionHandler(nil, error) - } - task.resume() - return task +// let fetchStartDate = Date() +// let task = dataTask(forCalculating: options, completionHandler: { (json) in +// let responseEndDate = Date() +// let response = options.response(from: json) +// if let matches = response { +// self.postprocess(matches, fetchStartDate: fetchStartDate, responseEndDate: responseEndDate, uuid: json["uuid"] as? String) +// } +// completionHandler(response, nil) +// }) { (error) in +// completionHandler(nil, error) +// } +// task.resume() +// return task } /** @@ -214,19 +230,19 @@ open class Directions: NSObject { */ @discardableResult open func calculateRoutes(matching options: MatchOptions, completionHandler: @escaping RouteCompletionHandler) -> URLSessionDataTask { - let fetchStartDate = Date() - let task = dataTask(forCalculating: options, completionHandler: { (json) in - let responseEndDate = Date() - let response = options.response(containingRoutesFrom: json) - if let routes = response.1 { - self.postprocess(routes, fetchStartDate: fetchStartDate, responseEndDate: responseEndDate, uuid: json["uuid"] as? String) - } - completionHandler(response.0, response.1, nil) - }) { (error) in - completionHandler(nil, nil, error) - } - task.resume() - return task +// let fetchStartDate = Date() +// let task = dataTask(forCalculating: options, completionHandler: { (json) in +// let responseEndDate = Date() +// let response = options.response(containingRoutesFrom: json) +// if let routes = response.1 { +// self.postprocess(routes, fetchStartDate: fetchStartDate, responseEndDate: responseEndDate, uuid: json["uuid"] as? String) +// } +// completionHandler(response.0, response.1, nil) +// }) { (error) in +// completionHandler(nil, nil, error) +// } +// task.resume() +// return task } /** @@ -239,31 +255,31 @@ open class Directions: NSObject { - postcondition: The caller must resume the returned task. */ fileprivate func dataTask(forCalculating options: DirectionsOptions, completionHandler: @escaping (_ json: JSONDictionary) -> Void, errorHandler: @escaping (_ error: NSError) -> Void) -> URLSessionDataTask { - let request = urlRequest(forCalculating: options) - return URLSession.shared.dataTask(with: request) { (data, response, error) in - var json: JSONDictionary = [:] - if let data = data, response?.mimeType == "application/json" { - do { - json = try JSONSerialization.jsonObject(with: data, options: []) as! JSONDictionary - } catch { - assert(false, "Invalid data") - } - } - - let apiStatusCode = json["code"] as? String - let apiMessage = json["message"] as? String - guard !json.isEmpty, data != nil, error == nil && ((apiStatusCode == nil && apiMessage == nil) || apiStatusCode == "Ok") else { - let apiError = Directions.informativeError(describing: json, response: response, underlyingError: error as NSError?) - DispatchQueue.main.async { - errorHandler(apiError) - } - return - } - - DispatchQueue.main.async { - completionHandler(json) - } - } +// let request = urlRequest(forCalculating: options) +// return URLSession.shared.dataTask(with: request) { (data, response, error) in +// var json: JSONDictionary = [:] +// if let data = data, response?.mimeType == "application/json" { +// do { +// json = try JSONSerialization.jsonObject(with: data, options: []) as! JSONDictionary +// } catch { +// assert(false, "Invalid data") +// } +// } +// +// let apiStatusCode = json["code"] as? String +// let apiMessage = json["message"] as? String +// guard !json.isEmpty, data != nil, error == nil && ((apiStatusCode == nil && apiMessage == nil) || apiStatusCode == "Ok") else { +// let apiError = Directions.informativeError(describing: json, response: response, underlyingError: error as NSError?) +// DispatchQueue.main.async { +// errorHandler(apiError) +// } +// return +// } +// +// DispatchQueue.main.async { +// completionHandler(json) +// } +// } } /** @@ -335,48 +351,24 @@ open class Directions: NSObject { /** Returns an error that supplements the given underlying error with additional information from the an HTTP response’s body or headers. */ - static func informativeError(describing json: JSONDictionary, response: URLResponse?, underlyingError error: NSError?) -> NSError { - let apiStatusCode = json["code"] as? String - var userInfo = error?.userInfo ?? [:] + static func informativeError(code: String?, response: URLResponse?, underlyingError error: Error?) -> DirectionsError { if let response = response as? HTTPURLResponse { - var failureReason: String? = nil - var recoverySuggestion: String? = nil - switch (response.statusCode, apiStatusCode ?? "") { + switch (response.statusCode, code ?? "") { case (200, "NoRoute"): - failureReason = "No route could be found between the specified locations." - recoverySuggestion = "Make sure it is possible to travel between the locations with the mode of transportation implied by the profileIdentifier option. For example, it is impossible to travel by car from one continent to another without either a land bridge or a ferry connection." + return .unableToRoute case (200, "NoSegment"): - failureReason = "A specified location could not be associated with a roadway or pathway." - recoverySuggestion = "Make sure the locations are close enough to a roadway or pathway. Try setting the coordinateAccuracy property of all the waypoints to a negative value." + return .unableToLocate case (404, "ProfileNotFound"): - failureReason = "Unrecognized profile identifier." - recoverySuggestion = "Make sure the profileIdentifier option is set to one of the provided constants, such as MBDirectionsProfileIdentifierAutomobile." + return .profileNotFound case (413, _): - failureReason = "The request is too large." - recoverySuggestion = "Try specifying fewer waypoints or giving the waypoints shorter names." + return .requestTooLarge case (429, _): - if let timeInterval = response.rateLimitInterval, let maximumCountOfRequests = response.rateLimit { - let intervalFormatter = DateComponentsFormatter() - intervalFormatter.unitsStyle = .full - let formattedInterval = intervalFormatter.string(from: timeInterval) ?? "\(timeInterval) seconds" - let formattedCount = NumberFormatter.localizedString(from: NSNumber(value: maximumCountOfRequests), number: .decimal) - failureReason = "More than \(formattedCount) requests have been made with this access token within a period of \(formattedInterval)." - } - if let rolloverTime = response.rateLimitResetTime { - let formattedDate = DateFormatter.localizedString(from: rolloverTime, dateStyle: .long, timeStyle: .long) - recoverySuggestion = "Wait until \(formattedDate) before retrying." - } + return .rateLimited(rateLimitInterval: response.rateLimitInterval, rateLimit: response.rateLimit, resetTime: response.rateLimitResetTime) default: - // `message` is v4 or v5; `error` is v4 - failureReason = json["message"] as? String ?? json["error"] as? String + return .unknown(response: response, underlying: error) } - userInfo[NSLocalizedFailureReasonErrorKey] = failureReason ?? userInfo[NSLocalizedFailureReasonErrorKey] ?? HTTPURLResponse.localizedString(forStatusCode: error?.code ?? -1) - userInfo[NSLocalizedRecoverySuggestionErrorKey] = recoverySuggestion ?? userInfo[NSLocalizedRecoverySuggestionErrorKey] - } - if let error = error { - userInfo[NSUnderlyingErrorKey] = error } return NSError(domain: error?.domain ?? MBDirectionsErrorDomain, code: error?.code ?? -1, userInfo: userInfo) } diff --git a/Sources/MapboxDirections/MBDirectionsResult.swift b/Sources/MapboxDirections/MBDirectionsResult.swift index dfb0b5b09..9007cf99c 100644 --- a/Sources/MapboxDirections/MBDirectionsResult.swift +++ b/Sources/MapboxDirections/MBDirectionsResult.swift @@ -1,205 +1,205 @@ -import Foundation -import Polyline -import CoreLocation - - -/** - A `DirectionsResult` represents a result returned from either the Mapbox Directions service. - - You do not create instances of this class directly. Instead, you receive `Route` or `Match` objects when you request directions using the `Directions.calculate(_:completionHandler:)` or `Directions.calculateRoutes(matching:completionHandler:)` method. - */ - -open class DirectionsResult: NSObject, NSSecureCoding { - - public var json: [String: Any]? - - internal init(json: [String: Any]? = nil, legs: [RouteLeg], distance: CLLocationDistance, expectedTravelTime: TimeInterval, coordinates: [CLLocationCoordinate2D]?, speechLocale: Locale?, options: DirectionsOptions) { - self.json = json - self.directionsOptions = options - self.legs = legs - self.distance = distance - self.expectedTravelTime = expectedTravelTime - self.coordinates = coordinates - self.speechLocale = speechLocale - } - - public required init?(coder decoder: NSCoder) { - accessToken = decoder.decodeObject(of: NSString.self, forKey: "accessToken") as String? - apiEndpoint = decoder.decodeObject(of: NSURL.self, forKey: "apiEndpoint") as URL? - - let coordinateDictionaries = decoder.decodeObject(of: [NSArray.self, NSDictionary.self, NSString.self, NSNumber.self], forKey: "coordinates") as? [[String: CLLocationDegrees]] - coordinates = coordinateDictionaries?.compactMap({ (coordinateDictionary) -> CLLocationCoordinate2D? in - if let latitude = coordinateDictionary["latitude"], - let longitude = coordinateDictionary["longitude"] { - return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) - } else { - return nil - } - }) - - legs = decoder.decodeObject(of: [NSArray.self, RouteLeg.self], forKey: "legs") as? [RouteLeg] ?? [] - distance = decoder.decodeDouble(forKey: "distance") - expectedTravelTime = decoder.decodeDouble(forKey: "expectedTravelTime") - - guard let options = decoder.decodeObject(of: [DirectionsOptions.self], forKey: "directionsOptions") as? DirectionsOptions else { - return nil - } - directionsOptions = options - - routeIdentifier = decoder.decodeObject(of: NSString.self, forKey: "routeIdentifier") as String? - - speechLocale = decoder.decodeObject(of: NSLocale.self, forKey: "speechLocale") as Locale? - } - - public class var supportsSecureCoding: Bool { - return true - } - - public func encode(with coder: NSCoder) { - coder.encode(accessToken, forKey: "accessToken") - coder.encode(apiEndpoint, forKey: "apiEndpoint") - - let coordinateDictionaries = coordinates?.map { [ - "latitude": $0.latitude, - "longitude": $0.longitude, - ] } - coder.encode(coordinateDictionaries, forKey: "coordinates") - - coder.encode(legs, forKey: "legs") - coder.encode(distance, forKey: "distance") - coder.encode(expectedTravelTime, forKey: "expectedTravelTime") - coder.encode(directionsOptions, forKey: "directionsOptions") - coder.encode(routeIdentifier, forKey: "routeIdentifier") - coder.encode(speechLocale, forKey: "speechLocale") - } - - /** - An array of geographic coordinates defining the path of the route from start to finish. - - This array may be `nil` or simplified depending on the `routeShapeResolution` property of the original `RouteOptions` object. - - Using the [Mapbox Maps SDK for iOS](https://docs.mapbox.com/ios/maps/) or [Mapbox Maps SDK for macOS](https://mapbox.github.io/mapbox-gl-native/macos/), you can create an `MGLPolyline` object using these coordinates to display an overview of the route on an `MGLMapView`. - */ - public let coordinates: [CLLocationCoordinate2D]? - - /** - The number of coordinates. - - The value of this property may be zero or reduced depending on the `routeShapeResolution` property of the original `RouteOptions` object. - - - note: This initializer is intended for Objective-C usage. In Swift code, use the `coordinates.count` property. - */ - open var coordinateCount: UInt { - return UInt(coordinates?.count ?? 0) - } - - /** - Retrieves the coordinates. - - The array may be empty or simplified depending on the `routeShapeResolution` property of the original `RouteOptions` object. - - Using the [Mapbox Maps SDK for iOS](https://docs.mapbox.com/ios/maps/) or [Mapbox Maps SDK for macOS](https://mapbox.github.io/mapbox-gl-native/macos/), you can create an `MGLPolyline` object using these coordinates to display an overview of the route on an `MGLMapView`. - - - parameter coordinates: A pointer to a C array of `CLLocationCoordinate2D` instances. On output, this array contains all the vertices of the overlay. - - - precondition: `coordinates` must be large enough to hold `coordinateCount` instances of `CLLocationCoordinate2D`. - - - note: This initializer is intended for Objective-C usage. In Swift code, use the `coordinates` property. - */ - open func getCoordinates(_ coordinates: UnsafeMutablePointer) { - for i in 0..<(self.coordinates?.count ?? 0) { - coordinates.advanced(by: i).pointee = self.coordinates![i] - } - } - - /** - An array of `RouteLeg` objects representing the legs of the route. - - The number of legs in this array depends on the number of waypoints. A route with two waypoints (the source and destination) has one leg, a route with three waypoints (the source, an intermediate waypoint, and the destination) has two legs, and so on. - - To determine the name of the route, concatenate the names of the route’s legs. - */ - public let legs: [RouteLeg] - - open override var description: String { - return legs.map { $0.name }.joined(separator: " – ") - } - - // MARK: Getting Additional Route Details - - /** - The route’s distance, measured in meters. - - The value of this property accounts for the distance that the user must travel to traverse the path of the route. It is the sum of the `distance` properties of the route’s legs, not the sum of the direct distances between the route’s waypoints. You should not assume that the user would travel along this distance at a fixed speed. - */ - public let distance: CLLocationDistance - - /** - The route’s expected travel time, measured in seconds. - - The value of this property reflects the time it takes to traverse the entire route. It is the sum of the `expectedTravelTime` properties of the route’s legs. If the route was calculated using the `MBDirectionsProfileIdentifierAutomobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin the route. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the route makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. - - Do not assume that the user would travel along the route at a fixed speed. For more granular travel times, use the `RouteLeg.expectedTravelTime` or `RouteStep.expectedTravelTime`. For even more granularity, specify the `AttributeOptions.expectedTravelTime` option and use the `RouteLeg.expectedSegmentTravelTimes` property. - */ - public let expectedTravelTime: TimeInterval - - /** - `RouteOptions` used to create the directions request. - - The route options object’s profileIdentifier property reflects the primary mode of transportation used for the route. Individual steps along the route might use different modes of transportation as necessary. - */ - public let directionsOptions: DirectionsOptions - - /** - The [access token](https://docs.mapbox.com/help/glossary/access-token/) used to make the directions request. - - This property is set automatically if a request is made via `Directions.calculate(_:completionHandler:)`. - */ - open var accessToken: String? - - /** - The endpoint used to make the directions request. - - This property is set automatically if a request is made via `Directions.calculate(_:completionHandler:)`. - */ - open var apiEndpoint: URL? - - func debugQuickLookObject() -> Any? { - if let coordinates = coordinates { - return debugQuickLookURL(illustrating: coordinates, profileIdentifier: directionsOptions.profileIdentifier) - } - return nil - } - - /** - A unique identifier for a directions request. - - Each route produced by a single call to `Directions.calculate(_:completionHandler:)` has the same route identifier. - */ - open var routeIdentifier: String? - - /** - The locale to use for spoken instructions. - - This locale is specific to Mapbox Voice API. If `nil` is returned, the instruction should be spoken with an alternative speech synthesizer. - */ - open var speechLocale: Locale? - - /** - The time immediately before a `Directions` object fetched this result. - - If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to `nil`; use the `URLSessionTaskTransactionMetrics.fetchStartDate` property instead. This property may also be set to `nil` if you create this result from a JSON object or encoded object. - - This property does not persist after encoding and decoding. - */ - open var fetchStartDate: Date? - - /** - The time immediately before a `Directions` object received the last byte of this result. - - If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to `nil`; use the `URLSessionTaskTransactionMetrics.responseEndDate` property instead. This property may also be set to `nil` if you create this result from a JSON object or encoded object. - - This property does not persist after encoding and decoding. - */ - open var responseEndDate: Date? -} +//import Foundation +//import Polyline +//import CoreLocation +// +// +///** +// A `DirectionsResult` represents a result returned from either the Mapbox Directions service. +// +// You do not create instances of this class directly. Instead, you receive `Route` or `Match` objects when you request directions using the `Directions.calculate(_:completionHandler:)` or `Directions.calculateRoutes(matching:completionHandler:)` method. +// */ +// +//open class DirectionsResult: NSObject, NSSecureCoding { +// +// public var json: [String: Any]? +// +// internal init(json: [String: Any]? = nil, legs: [RouteLeg], distance: CLLocationDistance, expectedTravelTime: TimeInterval, coordinates: [CLLocationCoordinate2D]?, speechLocale: Locale?, options: DirectionsOptions) { +// self.json = json +// self.directionsOptions = options +// self.legs = legs +// self.distance = distance +// self.expectedTravelTime = expectedTravelTime +// self.coordinates = coordinates +// self.speechLocale = speechLocale +// } +// +// public required init?(coder decoder: NSCoder) { +// accessToken = decoder.decodeObject(of: NSString.self, forKey: "accessToken") as String? +// apiEndpoint = decoder.decodeObject(of: NSURL.self, forKey: "apiEndpoint") as URL? +// +// let coordinateDictionaries = decoder.decodeObject(of: [NSArray.self, NSDictionary.self, NSString.self, NSNumber.self], forKey: "coordinates") as? [[String: CLLocationDegrees]] +// coordinates = coordinateDictionaries?.compactMap({ (coordinateDictionary) -> CLLocationCoordinate2D? in +// if let latitude = coordinateDictionary["latitude"], +// let longitude = coordinateDictionary["longitude"] { +// return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) +// } else { +// return nil +// } +// }) +// +// legs = decoder.decodeObject(of: [NSArray.self, RouteLeg.self], forKey: "legs") as? [RouteLeg] ?? [] +// distance = decoder.decodeDouble(forKey: "distance") +// expectedTravelTime = decoder.decodeDouble(forKey: "expectedTravelTime") +// +// guard let options = decoder.decodeObject(of: [DirectionsOptions.self], forKey: "directionsOptions") as? DirectionsOptions else { +// return nil +// } +// directionsOptions = options +// +// routeIdentifier = decoder.decodeObject(of: NSString.self, forKey: "routeIdentifier") as String? +// +// speechLocale = decoder.decodeObject(of: NSLocale.self, forKey: "speechLocale") as Locale? +// } +// +// public class var supportsSecureCoding: Bool { +// return true +// } +// +// public func encode(with coder: NSCoder) { +// coder.encode(accessToken, forKey: "accessToken") +// coder.encode(apiEndpoint, forKey: "apiEndpoint") +// +// let coordinateDictionaries = coordinates?.map { [ +// "latitude": $0.latitude, +// "longitude": $0.longitude, +// ] } +// coder.encode(coordinateDictionaries, forKey: "coordinates") +// +// coder.encode(legs, forKey: "legs") +// coder.encode(distance, forKey: "distance") +// coder.encode(expectedTravelTime, forKey: "expectedTravelTime") +// coder.encode(directionsOptions, forKey: "directionsOptions") +// coder.encode(routeIdentifier, forKey: "routeIdentifier") +// coder.encode(speechLocale, forKey: "speechLocale") +// } +// +// /** +// An array of geographic coordinates defining the path of the route from start to finish. +// +// This array may be `nil` or simplified depending on the `routeShapeResolution` property of the original `RouteOptions` object. +// +// Using the [Mapbox Maps SDK for iOS](https://docs.mapbox.com/ios/maps/) or [Mapbox Maps SDK for macOS](https://mapbox.github.io/mapbox-gl-native/macos/), you can create an `MGLPolyline` object using these coordinates to display an overview of the route on an `MGLMapView`. +// */ +// public let coordinates: [CLLocationCoordinate2D]? +// +// /** +// The number of coordinates. +// +// The value of this property may be zero or reduced depending on the `routeShapeResolution` property of the original `RouteOptions` object. +// +// - note: This initializer is intended for Objective-C usage. In Swift code, use the `coordinates.count` property. +// */ +// open var coordinateCount: UInt { +// return UInt(coordinates?.count ?? 0) +// } +// +// /** +// Retrieves the coordinates. +// +// The array may be empty or simplified depending on the `routeShapeResolution` property of the original `RouteOptions` object. +// +// Using the [Mapbox Maps SDK for iOS](https://docs.mapbox.com/ios/maps/) or [Mapbox Maps SDK for macOS](https://mapbox.github.io/mapbox-gl-native/macos/), you can create an `MGLPolyline` object using these coordinates to display an overview of the route on an `MGLMapView`. +// +// - parameter coordinates: A pointer to a C array of `CLLocationCoordinate2D` instances. On output, this array contains all the vertices of the overlay. +// +// - precondition: `coordinates` must be large enough to hold `coordinateCount` instances of `CLLocationCoordinate2D`. +// +// - note: This initializer is intended for Objective-C usage. In Swift code, use the `coordinates` property. +// */ +// open func getCoordinates(_ coordinates: UnsafeMutablePointer) { +// for i in 0..<(self.coordinates?.count ?? 0) { +// coordinates.advanced(by: i).pointee = self.coordinates![i] +// } +// } +// +// /** +// An array of `RouteLeg` objects representing the legs of the route. +// +// The number of legs in this array depends on the number of waypoints. A route with two waypoints (the source and destination) has one leg, a route with three waypoints (the source, an intermediate waypoint, and the destination) has two legs, and so on. +// +// To determine the name of the route, concatenate the names of the route’s legs. +// */ +// public let legs: [RouteLeg] +// +// open override var description: String { +// return legs.map { $0.name }.joined(separator: " – ") +// } +// +// // MARK: Getting Additional Route Details +// +// /** +// The route’s distance, measured in meters. +// +// The value of this property accounts for the distance that the user must travel to traverse the path of the route. It is the sum of the `distance` properties of the route’s legs, not the sum of the direct distances between the route’s waypoints. You should not assume that the user would travel along this distance at a fixed speed. +// */ +// public let distance: CLLocationDistance +// +// /** +// The route’s expected travel time, measured in seconds. +// +// The value of this property reflects the time it takes to traverse the entire route. It is the sum of the `expectedTravelTime` properties of the route’s legs. If the route was calculated using the `MBDirectionsProfileIdentifierAutomobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin the route. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the route makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. +// +// Do not assume that the user would travel along the route at a fixed speed. For more granular travel times, use the `RouteLeg.expectedTravelTime` or `RouteStep.expectedTravelTime`. For even more granularity, specify the `AttributeOptions.expectedTravelTime` option and use the `RouteLeg.expectedSegmentTravelTimes` property. +// */ +// public let expectedTravelTime: TimeInterval +// +// /** +// `RouteOptions` used to create the directions request. +// +// The route options object’s profileIdentifier property reflects the primary mode of transportation used for the route. Individual steps along the route might use different modes of transportation as necessary. +// */ +// public let directionsOptions: DirectionsOptions +// +// /** +// The [access token](https://docs.mapbox.com/help/glossary/access-token/) used to make the directions request. +// +// This property is set automatically if a request is made via `Directions.calculate(_:completionHandler:)`. +// */ +// open var accessToken: String? +// +// /** +// The endpoint used to make the directions request. +// +// This property is set automatically if a request is made via `Directions.calculate(_:completionHandler:)`. +// */ +// open var apiEndpoint: URL? +// +// func debugQuickLookObject() -> Any? { +// if let coordinates = coordinates { +// return debugQuickLookURL(illustrating: coordinates, profileIdentifier: directionsOptions.profileIdentifier) +// } +// return nil +// } +// +// /** +// A unique identifier for a directions request. +// +// Each route produced by a single call to `Directions.calculate(_:completionHandler:)` has the same route identifier. +// */ +// open var routeIdentifier: String? +// +// /** +// The locale to use for spoken instructions. +// +// This locale is specific to Mapbox Voice API. If `nil` is returned, the instruction should be spoken with an alternative speech synthesizer. +// */ +// open var speechLocale: Locale? +// +// /** +// The time immediately before a `Directions` object fetched this result. +// +// If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to `nil`; use the `URLSessionTaskTransactionMetrics.fetchStartDate` property instead. This property may also be set to `nil` if you create this result from a JSON object or encoded object. +// +// This property does not persist after encoding and decoding. +// */ +// open var fetchStartDate: Date? +// +// /** +// The time immediately before a `Directions` object received the last byte of this result. +// +// If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to `nil`; use the `URLSessionTaskTransactionMetrics.responseEndDate` property instead. This property may also be set to `nil` if you create this result from a JSON object or encoded object. +// +// This property does not persist after encoding and decoding. +// */ +// open var responseEndDate: Date? +//} diff --git a/Sources/MapboxDirections/MBRoute.swift b/Sources/MapboxDirections/MBRoute.swift index c8173fdf1..ecf4e418b 100644 --- a/Sources/MapboxDirections/MBRoute.swift +++ b/Sources/MapboxDirections/MBRoute.swift @@ -1,82 +1,190 @@ -import Foundation -import CoreLocation import Polyline - /** A `Route` object defines a single route that the user can follow to visit a series of waypoints in order. The route object includes information about the route, such as its distance and expected travel time. Depending on the criteria used to calculate the route, the route object may also include detailed turn-by-turn instructions. Typically, you do not create instances of this class directly. Instead, you receive route objects when you request directions using the `Directions.calculate(_:completionHandler:)` method. However, if you use the `Directions.url(forCalculating:)` method instead, you can pass the results of the HTTP request into this class’s initializer. */ - -open class Route: DirectionsResult { - // MARK: Creating a Route +@objc(MBRoute) +open class Route: NSObject, Codable { - internal override init(json: [String : Any]? = nil, legs: [RouteLeg], distance: CLLocationDistance, expectedTravelTime: TimeInterval, coordinates: [CLLocationCoordinate2D]?, speechLocale: Locale?, options: DirectionsOptions) { - super.init(json: json, legs: legs, distance: distance, expectedTravelTime: expectedTravelTime, coordinates: coordinates, speechLocale: speechLocale, options: options) + private enum CodingKeys: String, CodingKey { + case distance + case routeIdentifier + case legs + case v4steps = "steps" + case v4summary = "summary" + case expectedTravelTime = "duration" + case routeOptions + case geometry } - /** - Initializes a new route object with the given JSON dictionary representation and waypoints. - - This initializer is intended for use in conjunction with the `Directions.url(forCalculating:)` method. - - - parameter json: A JSON dictionary representation of the route as returned by the Mapbox Directions API. - - parameter waypoints: An array of waypoints that the route visits in chronological order. - - parameter options: The options used when requesting the route. - */ + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(legs, forKey: .legs) + try container.encode(distance, forKey: .distance) + try container.encode(expectedTravelTime, forKey: .expectedTravelTime) + try container.encode(routeOptions, forKey: .routeOptions) + } - public init(json: [String: Any], waypoints: [Waypoint], options: RouteOptions) { - // Associate each leg JSON with a source and destination. The sequence of destinations is offset by one from the sequence of sources. - let legInfo = zip(zip(waypoints.prefix(upTo: waypoints.endIndex - 1), waypoints.suffix(from: 1)), - json["legs"] as? [JSONDictionary] ?? []) - let legs = legInfo.map { (endpoints, json) -> RouteLeg in - RouteLeg(json: json, source: endpoints.0, destination: endpoints.1, options: options) + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let routeOptions = try container.decodeIfPresent(RouteOptions.self, forKey: .routeOptions) { + self.routeOptions = routeOptions + } else { + self.routeOptions = decoder.userInfo[CodingUserInfoKey.routeOptions!] as! RouteOptions } - let distance = json["distance"] as! Double - let expectedTravelTime = json["duration"] as! Double - let coordinates = options.shapeFormat.coordinates(from: json["geometry"]) + let geometry = try container.decodeIfPresent(UncertainCodable.self, forKey: .geometry) + if let geo = geometry?.value as? String { + if routeOptions is RouteOptionsV4 { + coordinates = decodePolyline(geo, precision: 1e6) + } else { + coordinates = decodePolyline(geo, precision: 1e5) + } + } else if let geo = geometry?.value as? Geometry { + coordinates = geo.coordinates + } else { + coordinates = nil + } + + distance = try container.decode(CLLocationDistance.self, forKey: .distance) + expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime) - var speechLocale: Locale? - if let locale = json["voiceLocale"] as? String { - speechLocale = Locale(identifier: locale) + if let legs = try container.decodeIfPresent([RouteLeg].self, forKey: .legs) { + self.legs = legs + } else { + // V4 + let legName = try container.decodeIfPresent(String.self, forKey: .v4summary) ?? "" + let steps = try container.decodeIfPresent([RouteStep].self, forKey: .v4steps) + self.legs = [RouteLeg(steps: steps!, source: routeOptions.waypoints.first!, + destination: routeOptions.waypoints.last!, name: legName, distance: distance, + expectedTravelTime: expectedTravelTime, profileIdentifier: routeOptions.profileIdentifier)] } - super.init(json: json, legs: legs, distance: distance, expectedTravelTime: expectedTravelTime, coordinates: coordinates, speechLocale: speechLocale, options: options) + // Associate each leg JSON with a source and destination. The sequence of destinations is offset by one from the sequence of sources. + let waypoints = routeOptions.waypoints + let legInfos = zip(zip(waypoints.prefix(upTo: waypoints.endIndex - 1), waypoints.suffix(from: 1)), legs) + for legInfo in legInfos { + legInfo.1.source = legInfo.0.0 + legInfo.1.destination = legInfo.0.1 + } } - public var routeOptions: RouteOptions { - return super.directionsOptions as! RouteOptions + // MARK: Creating a Route + + @objc internal init(routeOptions: RouteOptions, legs: [RouteLeg], distance: CLLocationDistance, expectedTravelTime: TimeInterval, coordinates: [CLLocationCoordinate2D]?) { + self.routeOptions = routeOptions + self.legs = legs + self.distance = distance + self.expectedTravelTime = expectedTravelTime + self.coordinates = coordinates } - public required init?(coder decoder: NSCoder) { - super.init(coder: decoder) + // MARK: Getting the Route Geometry + + /** + An array of geographic coordinates defining the path of the route from start to finish. + + This array may be `nil` or simplified depending on the `routeShapeResolution` property of the original `RouteOptions` object. + + Using the [Mapbox Maps SDK for iOS](https://www.mapbox.com/ios-sdk/) or [Mapbox Maps SDK for macOS](https://github.com/mapbox/mapbox-gl-native/tree/master/platform/macos/), you can create an `MGLPolyline` object using these coordinates to display an overview of the route on an `MGLMapView`. + */ + @objc open let coordinates: [CLLocationCoordinate2D]? + + /** + The number of coordinates. + + The value of this property may be zero or reduced depending on the `routeShapeResolution` property of the original `RouteOptions` object. + + - note: This initializer is intended for Objective-C usage. In Swift code, use the `coordinates.count` property. + */ + @objc open var coordinateCount: UInt { + return UInt(coordinates?.count ?? 0) } - override public class var supportsSecureCoding: Bool { - return true + /** + Retrieves the coordinates. + + The array may be empty or simplified depending on the `routeShapeResolution` property of the original `RouteOptions` object. + + Using the [Mapbox Maps SDK for iOS](https://www.mapbox.com/ios-sdk/) or [Mapbox Maps SDK for macOS](https://github.com/mapbox/mapbox-gl-native/tree/master/platform/macos/), you can create an `MGLPolyline` object using these coordinates to display an overview of the route on an `MGLMapView`. + + - parameter coordinates: A pointer to a C array of `CLLocationCoordinate2D` instances. On output, this array contains all the vertices of the overlay. + + - precondition: `coordinates` must be large enough to hold `coordinateCount` instances of `CLLocationCoordinate2D`. + + - note: This initializer is intended for Objective-C usage. In Swift code, use the `coordinates` property. + */ + @objc open func getCoordinates(_ coordinates: UnsafeMutablePointer) { + for i in 0..<(self.coordinates?.count ?? 0) { + coordinates.advanced(by: i).pointee = self.coordinates![i] + } } -} - -// MARK: Support for Directions API v4 - -internal class RouteV4: Route { - convenience override init(json: JSONDictionary, waypoints: [Waypoint], options: RouteOptions) { - let leg = RouteLegV4(json: json, source: waypoints.first!, destination: waypoints.last!, options: options) - let distance = json["distance"] as! Double - let expectedTravelTime = json["duration"] as! Double - - var coordinates: [CLLocationCoordinate2D]? - switch json["geometry"] { - case let geometry as JSONDictionary: - coordinates = CLLocationCoordinate2D.coordinates(geoJSON: geometry) - case let geometry as String: - coordinates = decodePolyline(geometry, precision: 1e6)! - default: - coordinates = nil + + /** + An array of `RouteLeg` objects representing the legs of the route. + + The number of legs in this array depends on the number of waypoints. A route with two waypoints (the source and destination) has one leg, a route with three waypoints (the source, an intermediate waypoint, and the destination) has two legs, and so on. + + To determine the name of the route, concatenate the names of the route’s legs. + */ + @objc open let legs: [RouteLeg] + + @objc open override var description: String { + return legs.map { $0.name }.joined(separator: " – ") + } + + // MARK: Getting Additional Route Details + + /** + The route’s distance, measured in meters. + + The value of this property accounts for the distance that the user must travel to traverse the path of the route. It is the sum of the `distance` properties of the route’s legs, not the sum of the direct distances between the route’s waypoints. You should not assume that the user would travel along this distance at a fixed speed. + */ + @objc open let distance: CLLocationDistance + + /** + The route’s expected travel time, measured in seconds. + + The value of this property reflects the time it takes to traverse the entire route. It is the sum of the `expectedTravelTime` properties of the route’s legs. If the route was calculated using the `MBDirectionsProfileIdentifierAutomobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin the route. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the route makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. + + Do not assume that the user would travel along the route at a fixed speed. For more granular travel times, use the `RouteLeg.expectedTravelTime` or `RouteStep.expectedTravelTime`. For even more granularity, specify the `AttributeOptions.expectedTravelTime` option and use the `RouteLeg.expectedSegmentTravelTimes` property. + */ + @objc open let expectedTravelTime: TimeInterval + + /** + `RouteOptions` used to create the directions request. + + The route options object’s profileIdentifier property reflects the primary mode of transportation used for the route. Individual steps along the route might use different modes of transportation as necessary. + */ + @objc open let routeOptions: RouteOptions + + /** + The [access token](https://www.mapbox.com/help/define-access-token/) used to make the directions request. + + This property is set automatically if a request is made via `Directions.calculate(_:completionHandler:)`. + */ + @objc open var accessToken: String? + + /** + The endpoint used to make the directions request. + + This property is set automatically if a request is made via `Directions.calculate(_:completionHandler:)`. + */ + @objc open var apiEndpoint: URL? + + func debugQuickLookObject() -> Any? { + if let coordinates = coordinates { + return debugQuickLookURL(illustrating: coordinates, profileIdentifier: routeOptions.profileIdentifier) } - - self.init(legs: [leg], distance: distance, expectedTravelTime: expectedTravelTime, coordinates: coordinates, speechLocale: nil, options: options) + return nil } -} + + /** + A unique identifier for a directions request. + + Each route produced by a single call to `Directions.calculate(_:completionHandler:)` has the same route identifier. + */ + @objc open var routeIdentifier: String? diff --git a/Sources/MapboxDirections/MBRouteLeg.swift b/Sources/MapboxDirections/MBRouteLeg.swift index 8b3f1de3d..c468f6e35 100644 --- a/Sources/MapboxDirections/MBRouteLeg.swift +++ b/Sources/MapboxDirections/MBRouteLeg.swift @@ -43,7 +43,8 @@ open class RouteLeg: NSObject, NSSecureCoding { } if let congestion = jsonAttributes["congestion"] as? [String] { congestionLevels = congestion.map { - CongestionLevel(description: $0)! + #warning("derail") + return CongestionLevel(rawValue: $0)! } } } diff --git a/Sources/MapboxDirections/MBRouteOptions.swift b/Sources/MapboxDirections/MBRouteOptions.swift index 346d61abb..3dd53cf7d 100644 --- a/Sources/MapboxDirections/MBRouteOptions.swift +++ b/Sources/MapboxDirections/MBRouteOptions.swift @@ -1,31 +1,172 @@ -import Foundation -import CoreLocation -#if SWIFT_PACKAGE -import CMapboxDirections -#endif - - /** - By default, pedestrians are assumed to walk at an average rate of 1.42 meters per second (5.11 kilometers per hour or 3.18 miles per hour), corresponding to a typical preferred walking speed. + A `RouteShapeFormat` indicates the format of a route’s shape in the raw HTTP response. */ -public let MBDefaultWalkingSpeed: CLLocationSpeed = 1.42 +@objc(MBRouteShapeFormat) +public enum RouteShapeFormat: UInt, CustomStringConvertible { + /** + The route’s shape is delivered in [GeoJSON](http://geojson.org/) format. + + This standard format is human-readable and can be parsed straightforwardly, but it is far more verbose than `polyline`. + */ + case geoJSON + /** + The route’s shape is delivered in [encoded polyline algorithm](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) format with 1×10−5 precision. + + This machine-readable format is considerably more compact than `geoJSON` but less precise than `polyline6`. + */ + case polyline + /** + The route’s shape is delivered in [encoded polyline algorithm](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) format with 1×10−6 precision. + + This format is an order of magnitude more precise than `polyline`. + */ + case polyline6 + + public init?(description: String) { + let format: RouteShapeFormat + switch description { + case "geojson": + format = .geoJSON + case "polyline": + format = .polyline + case "polyline6": + format = .polyline6 + default: + return nil + } + self.init(rawValue: format.rawValue) + } + + public var description: String { + switch self { + case .geoJSON: + return "geojson" + case .polyline: + return "polyline" + case .polyline6: + return "polyline6" + } + } +} /** - Pedestrians are assumed to walk no slower than 0.14 meters per second (0.50 kilometers per hour or 0.31 miles per hour) on average. + A `RouteShapeResolution` indicates the level of detail in a route’s shape, or whether the shape is present at all. */ -public let MBMinimumWalkingSpeed: CLLocationSpeed = 0.14 +@objc(MBRouteShapeResolution) +public enum RouteShapeResolution: UInt, CustomStringConvertible { + /** + The route’s shape is omitted. + + Specify this resolution if you do not intend to show the route line to the user or analyze the route line in any way. + */ + case none + /** + The route’s shape is simplified. + + This resolution considerably reduces the size of the response. The resulting shape is suitable for display at a low zoom level, but it lacks the detail necessary for focusing on individual segments of the route. + */ + case low + /** + The route’s shape is as detailed as possible. + + The resulting shape is equivalent to concatenating the shapes of all the route’s consitituent steps. You can focus on individual segments of this route while faithfully representing the path of the route. If you only intend to show a route overview and do not need to analyze the route line in any way, consider specifying `low` instead to considerably reduce the size of the response. + */ + case full + + public init?(description: String) { + let granularity: RouteShapeResolution + switch description { + case "false": + granularity = .none + case "simplified": + granularity = .low + case "full": + granularity = .full + default: + return nil + } + self.init(rawValue: granularity.rawValue) + } + + public var description: String { + switch self { + case .none: + return "false" + case .low: + return "simplified" + case .full: + return "full" + } + } +} /** - Pedestrians are assumed to walk no faster than 6.94 meters per second (25.0 kilometers per hour or 15.5 miles per hour) on average. + A system of units of measuring distances and other quantities. */ -public let MBMaximumWalkingSpeed: CLLocationSpeed = 6.94 +@objc(MBMeasurementSystem) +public enum MeasurementSystem: UInt, CustomStringConvertible { + + /** + U.S. customary and British imperial units. + + Distances are measured in miles and feet. + */ + case imperial + + /** + The metric system. + + Distances are measured in kilometers and meters. + */ + case metric + + public init?(description: String) { + let measurementSystem: MeasurementSystem + switch description { + case "imperial": + measurementSystem = .imperial + case "metric": + measurementSystem = .metric + default: + return nil + } + self.init(rawValue: measurementSystem.rawValue) + } + + public var description: String { + switch self { + case .imperial: + return "imperial" + case .metric: + return "metric" + } + } +} /** A `RouteOptions` object is a structure that specifies the criteria for results returned by the Mapbox Directions API. Pass an instance of this class into the `Directions.calculate(_:completionHandler:)` method. */ -open class RouteOptions: DirectionsOptions { +@objc(MBRouteOptions) +open class RouteOptions: NSObject, Codable, NSCopying { + // MARK: Creating a Route Options Object + + /** + Initializes a route options object for routes between the given waypoints and an optional profile identifier. + + - parameter waypoints: An array of `Waypoint` objects representing locations that the route should visit in chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 waypoints. (Some profiles, such as `MBDirectionsProfileIdentifierAutomobileAvoidingTraffic`, [may have lower limits](https://www.mapbox.com/api-documentation/#directions).) + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. This parameter, if set, should be set to `MBDirectionsProfileIdentifierAutomobile`, `MBDirectionsProfileIdentifierAutomobileAvoidingTraffic`, `MBDirectionsProfileIdentifierCycling`, or `MBDirectionsProfileIdentifierWalking`. `MBDirectionsProfileIdentifierAutomobile` is used by default. + */ + @objc public init(waypoints: [Waypoint], profileIdentifier: MBDirectionsProfileIdentifier? = nil) { + assert(waypoints.count >= 2, "A route requires at least a source and destination.") + assert(waypoints.count <= 25, "A route may not have more than 25 waypoints.") + + self.waypoints = waypoints + self.profileIdentifier = profileIdentifier ?? .automobile + self.allowsUTurnAtWaypoint = ![MBDirectionsProfileIdentifier.automobile.rawValue, MBDirectionsProfileIdentifier.automobileAvoidingTraffic.rawValue].contains(self.profileIdentifier.rawValue) + } + /** Initializes a route options object for routes between the given locations and an optional profile identifier. @@ -34,7 +175,7 @@ open class RouteOptions: DirectionsOptions { - parameter locations: An array of `CLLocation` objects representing locations that the route should visit in chronological order. The array should contain at least two locations (the source and destination) and at most 25 locations. Each location object is converted into a `Waypoint` object. This class respects the `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `Waypoint` class’s `coordinate` and `coordinateAccuracy` properties, respectively. - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. This parameter, if set, should be set to `MBDirectionsProfileIdentifierAutomobile`, `MBDirectionsProfileIdentifierAutomobileAvoidingTraffic`, `MBDirectionsProfileIdentifierCycling`, or `MBDirectionsProfileIdentifierWalking`. `MBDirectionsProfileIdentifierAutomobile` is used by default. */ - public convenience init(locations: [CLLocation], profileIdentifier: DirectionsProfileIdentifier? = nil) { + @objc public convenience init(locations: [CLLocation], profileIdentifier: MBDirectionsProfileIdentifier? = nil) { let waypoints = locations.map { Waypoint(location: $0) } self.init(waypoints: waypoints, profileIdentifier: profileIdentifier) } @@ -45,64 +186,74 @@ open class RouteOptions: DirectionsOptions { - parameter coordinates: An array of geographic coordinates representing locations that the route should visit in chronological order. The array should contain at least two locations (the source and destination) and at most 25 locations. Each coordinate is converted into a `Waypoint` object. - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. This parameter, if set, should be set to `MBDirectionsProfileIdentifierAutomobile`, `MBDirectionsProfileIdentifierAutomobileAvoidingTraffic`, `MBDirectionsProfileIdentifierCycling`, or `MBDirectionsProfileIdentifierWalking`. `MBDirectionsProfileIdentifierAutomobile` is used by default. */ - public convenience init(coordinates: [CLLocationCoordinate2D], profileIdentifier: DirectionsProfileIdentifier? = nil) { + @objc public convenience init(coordinates: [CLLocationCoordinate2D], profileIdentifier: MBDirectionsProfileIdentifier? = nil) { let waypoints = coordinates.map { Waypoint(coordinate: $0) } self.init(waypoints: waypoints, profileIdentifier: profileIdentifier) } - /** - Initializes a route options object for routes between the given waypoints and an optional profile identifier. - - - parameter waypoints: An array of `Waypoint` objects representing locations that the route should visit in chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 waypoints. (Some profiles, such as `MBDirectionsProfileIdentifierAutomobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. This parameter, if set, should be set to `MBDirectionsProfileIdentifierAutomobile`, `MBDirectionsProfileIdentifierAutomobileAvoidingTraffic`, `MBDirectionsProfileIdentifierCycling`, or `MBDirectionsProfileIdentifierWalking`. `MBDirectionsProfileIdentifierAutomobile` is used by default. - */ - public required init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier? = nil) { - super.init(waypoints: waypoints, profileIdentifier: profileIdentifier) - self.allowsUTurnAtWaypoint = ![MBDirectionsProfileIdentifier.automobile.rawValue, MBDirectionsProfileIdentifier.automobileAvoidingTraffic.rawValue].contains(self.profileIdentifier.rawValue) + private enum CodingKeys: String, CodingKey { + case waypoints + case allowsUTurnAtWaypoint + case profileIdentifier + case includesAlternativeRoutes + case includesSteps + case shapeFormat + case routeShapeResolution + case attributeOptions + case includesExitRoundaboutManeuver + case locale + case includesSpokenInstructions + case distanceMeasurementSystem + case includesVisualInstructions + case roadClassesToAvoid } - - internal convenience init(matchOptions: MatchOptions) { - self.init(waypoints: matchOptions.waypoints, profileIdentifier: matchOptions.profileIdentifier) - self.includesSteps = matchOptions.includesSteps - self.shapeFormat = matchOptions.shapeFormat - self.attributeOptions = matchOptions.attributeOptions - self.routeShapeResolution = matchOptions.routeShapeResolution - self.locale = matchOptions.locale - self.includesSpokenInstructions = matchOptions.includesSpokenInstructions - self.includesVisualInstructions = matchOptions.includesVisualInstructions + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(waypoints, forKey: .waypoints) + try container.encode(allowsUTurnAtWaypoint, forKey: .allowsUTurnAtWaypoint) + try container.encode(profileIdentifier.rawValue, forKey: .profileIdentifier) + try container.encode(includesAlternativeRoutes, forKey: .includesAlternativeRoutes) + try container.encode(includesSteps, forKey: .includesSteps) + try container.encode(shapeFormat.rawValue, forKey: .shapeFormat) + try container.encode(routeShapeResolution.rawValue, forKey: .routeShapeResolution) + try container.encode(attributeOptions.rawValue, forKey: .attributeOptions) + try container.encode(includesExitRoundaboutManeuver, forKey: .includesExitRoundaboutManeuver) + try container.encode(locale, forKey: .locale) + try container.encode(includesSpokenInstructions, forKey: .includesSpokenInstructions) + try container.encode(distanceMeasurementSystem.rawValue, forKey: .distanceMeasurementSystem) + try container.encode(includesVisualInstructions, forKey: .includesVisualInstructions) + try container.encode(roadClassesToAvoid, forKey: .roadClassesToAvoid) } - - public required init?(coder decoder: NSCoder) { - super.init(coder: decoder) - - allowsUTurnAtWaypoint = decoder.decodeBool(forKey: "allowsUTurnAtWaypoint") - - includesAlternativeRoutes = decoder.decodeBool(forKey: "includesAlternativeRoutes") - - includesExitRoundaboutManeuver = decoder.decodeBool(forKey: "includesExitRoundaboutManeuver") - - let roadClassesToAvoidDescriptions = decoder.decodeObject(of: NSString.self, forKey: "roadClassesToAvoid") as String? - roadClassesToAvoid = RoadClasses(descriptions: roadClassesToAvoidDescriptions?.components(separatedBy: ",") ?? []) ?? [] - - alleyPriority = DirectionsPriority(rawValue: decoder.decodeDouble(forKey: "alleyPriority")) - walkwayPriority = DirectionsPriority(rawValue: decoder.decodeDouble(forKey: "walkwayPriority")) - speed = decoder.decodeDouble(forKey: "speed") + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + waypoints = try container.decode([Waypoint].self, forKey: .waypoints) + allowsUTurnAtWaypoint = try container.decode(Bool.self, forKey: .allowsUTurnAtWaypoint) + profileIdentifier = MBDirectionsProfileIdentifier(rawValue: try container.decode(String.self, forKey: .profileIdentifier)) + includesAlternativeRoutes = try container.decode(Bool.self, forKey: .includesAlternativeRoutes) + includesSteps = try container.decode(Bool.self, forKey: .includesSteps) + shapeFormat = RouteShapeFormat(rawValue: try container.decode(UInt.self, forKey: .shapeFormat))! + routeShapeResolution = RouteShapeResolution(rawValue: try container.decode(UInt.self, forKey: .routeShapeResolution))! + attributeOptions = AttributeOptions(rawValue: try container.decode(UInt.self, forKey: .attributeOptions)) + includesExitRoundaboutManeuver = try container.decode(Bool.self, forKey: .includesExitRoundaboutManeuver) + locale = try container.decode(Locale.self, forKey: .locale) + includesSpokenInstructions = try container.decode(Bool.self, forKey: .includesSpokenInstructions) + distanceMeasurementSystem = MeasurementSystem(rawValue: try container.decode(UInt.self, forKey: .distanceMeasurementSystem))! + includesVisualInstructions = try container.decode(Bool.self, forKey: .includesVisualInstructions) + roadClassesToAvoid = try container.decode(RoadClasses.self, forKey: .roadClassesToAvoid) } + + // MARK: Specifying the Path of the Route - public override func encode(with coder: NSCoder) { - super.encode(with: coder) - coder.encode(allowsUTurnAtWaypoint, forKey: "allowsUTurnAtWaypoint") - coder.encode(includesAlternativeRoutes, forKey: "includesAlternativeRoutes") - coder.encode(includesExitRoundaboutManeuver, forKey: "includesExitRoundaboutManeuver") - coder.encode(roadClassesToAvoid.description, forKey: "roadClassesToAvoid") - coder.encode(alleyPriority.rawValue, forKey: "alleyPriority") - coder.encode(walkwayPriority.rawValue, forKey: "walkwayPriority") - coder.encode(speed, forKey: "speed") - } + /** + An array of `Waypoint` objects representing locations that the route should visit in chronological order. - internal override var abridgedPath: String { - return "directions/v5/\(profileIdentifier.rawValue)" - } + A waypoint object indicates a location to visit, as well as an optional heading from which to approach the location. + + The array should contain at least two waypoints (the source and destination) and at most 25 waypoints. + */ + @objc open var waypoints: [Waypoint] /** A Boolean value that indicates whether a returned route may require a point U-turn at an intermediate waypoint. @@ -113,7 +264,18 @@ open class RouteOptions: DirectionsOptions { The default value of this property is `false` when the profile identifier is `MBDirectionsProfileIdentifierAutomobile` or `MBDirectionsProfileIdentifierAutomobileAvoidingTraffic` and `true` otherwise. */ - open var allowsUTurnAtWaypoint: Bool = false + @objc open var allowsUTurnAtWaypoint: Bool + + // MARK: Specifying Transportation Options + + /** + A string specifying the primary mode of transportation for the routes. + + This property should be set to `MBDirectionsProfileIdentifierAutomobile`, `MBDirectionsProfileIdentifierAutomobileAvoidingTraffic`, `MBDirectionsProfileIdentifierCycling`, or `MBDirectionsProfileIdentifierWalking`. The default value of this property is `MBDirectionsProfileIdentifierAutomobile`, which specifies driving directions. + */ + @objc open var profileIdentifier: MBDirectionsProfileIdentifier + + // MARK: Specifying the Response Format /** A Boolean value indicating whether alternative routes should be included in the response. @@ -124,162 +286,253 @@ open class RouteOptions: DirectionsOptions { The default value of this property is `false`. */ - open var includesAlternativeRoutes = false + @objc open var includesAlternativeRoutes = false + + /** + A Boolean value indicating whether `MBRouteStep` objects should be included in the response. + + If the value of this property is `true`, the returned route contains turn-by-turn instructions. Each returned `MBRoute` object contains one or more `MBRouteLeg` object that in turn contains one or more `MBRouteStep` objects. On the other hand, if the value of this property is `false`, the `MBRouteLeg` objects contain no `MBRouteStep` objects. + + If you only want to know the distance or estimated travel time to a destination, set this property to `false` to minimize the size of the response and the time it takes to calculate the response. If you need to display turn-by-turn instructions, set this property to `true`. + + The default value of this property is `false`. + */ + @objc open var includesSteps = false + + /** + Format of the data from which the shapes of the returned route and its steps are derived. + + This property has no effect on the returned shape objects, although the choice of format can significantly affect the size of the underlying HTTP response. + + The default value of this property is `polyline`. + */ + @objc open var shapeFormat = RouteShapeFormat.polyline + + /** + Resolution of the shape of the returned route. + + This property has no effect on the shape of the returned route’s steps. + + The default value of this property is `low`, specifying a low-resolution route shape. + */ + @objc open var routeShapeResolution = RouteShapeResolution.low + + /** + AttributeOptions for the route. Any combination of `AttributeOptions` can be specified. + + By default, no attribute options are specified. It is recommended that `routeShapeResolution` be set to `.full`. + */ + @objc open var attributeOptions: AttributeOptions = [] + + // MARK: Constructing the Request URL + + /** + The path of the request URL, not including the hostname or any parameters. + */ + internal var path: String { + assert(!queries.isEmpty, "No query") + + let queryComponent = queries.joined(separator: ";") + return "directions/v5/\(profileIdentifier.rawValue)/\(queryComponent).json" + } + + /** + An array of directions query strings to include in the request URL. + */ + internal var queries: [String] { + return waypoints.map { "\($0.coordinate.longitude),\($0.coordinate.latitude)" } + } /** A Boolean value indicating whether the route includes a `ManeuverType.exitRoundabout` or `ManeuverType.exitRotary` step when traversing a roundabout or rotary, respectively. If this option is set to `true`, a route that traverses a roundabout includes both a `ManeuverType.takeRoundabout` step and a `ManeuverType.exitRoundabout` step; likewise, a route that traverses a large, named roundabout includes both a `ManeuverType.takeRotary` step and a `ManeuverType.exitRotary` step. Otherwise, it only includes a `ManeuverType.takeRoundabout` or `ManeuverType.takeRotary` step. This option is set to `false` by default. */ - open var includesExitRoundaboutManeuver = false + @objc open var includesExitRoundaboutManeuver = false /** - The route classes that the calculated routes will avoid. + The locale in which the route’s instructions are written. - Currently, you can only specify a single road class to avoid. + If you use MapboxDirections.swift with the Mapbox Directions API, this property affects the sentence contained within the `RouteStep.instructions` property, but it does not affect any road names contained in that property or other properties such as `RouteStep.name`. + + The Directions API can provide instructions in [a number of languages](https://www.mapbox.com/api-documentation/#instructions-languages). Set this property to `Bundle.main.preferredLocalizations.first` or `Locale.autoupdatingCurrent` to match the application’s language or the system language, respectively. + + By default, this property is set to the current system locale. */ - open var roadClassesToAvoid: RoadClasses = [] + @objc open var locale = Locale.autoupdatingCurrent { + didSet { + self.distanceMeasurementSystem = locale.usesMetric ? .metric : .imperial + } + } /** - A number that influences whether the route should prefer or avoid alleys or narrow service roads between buildings. - - This property has no effect unless the profile identifier is set to `MBDirectionsProfileIdentifier.walking`. - - The value of this property must be at least `MBDirectionsPriority.low` and at most `MBDirectionsPriority.high`. The default value of `MBDirectionsPriority.default` neither prefers nor avoids alleys, while a negative value between `MBDirectionsPriority.low` and `MBDirectionsPriority.default` avoids alleys, and a positive value between `MBDirectionsPriority.default` and `MBDirectionsPriority.high` prefers alleys. A value of 0.9 is suitable for pedestrians who are comfortable with walking down alleys. + A Boolean value indicating whether each route step includes an array of `SpokenInstructions`. + + If this option is set to true, the `RouteStep.instructionsSpokenAlongStep` property is set to an array of `SpokenInstructions`. + */ + @objc open var includesSpokenInstructions = false + + /** + The measurement system used in spoken instructions included in route steps. + + If the `includesSpokenInstructions` property is set to `true`, this property determines the units used for measuring the distance remaining until an upcoming maneuver. If the `includesSpokenInstructions` property is set to `false`, this property has no effect. + + You should choose a measurement system appropriate for the current region. You can also allow the user to indicate their preferred measurement system via a setting. */ - open var alleyPriority: DirectionsPriority = .default + @objc open var distanceMeasurementSystem: MeasurementSystem = Locale.autoupdatingCurrent.usesMetric ? .metric : .imperial /** - A number that influences whether the route should prefer or avoid roads or paths that are set aside for pedestrian-only use (walkways or footpaths). + :nodoc: + If true, each `RouteStep` will contain the property `visualInstructionsAlongStep`. - This property has no effect unless the profile identifier is set to `MBDirectionsProfileIdentifier.walking`. You can adjust this property to avoid [sidewalks and crosswalks that are mapped as separate footpaths](https://wiki.openstreetmap.org/wiki/Sidewalks#Sidewalk_as_separate_way), which may be more granular than needed for some forms of pedestrian navigation. + `visualInstructionsAlongStep` contains an array of `VisualInstruction` used for visually conveying information about a given `RouteStep`. + */ + @objc open var includesVisualInstructions = false + + /** + The route classes that the calculated routes will avoid. - The value of this property must be at least `MBDirectionsPriority.low` and at most `MBDirectionsPriority.high`. The default value of `MBDirectionsPriority.default` neither prefers nor avoids walkways, while a negative value between `MBDirectionsPriority.low` and `MBDirectionsPriority.default` avoids walkways, and a positive value between `MBDirectionsPriority.default` and `MBDirectionsPriority.high` prefers walkways. A value of −0.1 results in less verbose routes in cities where sidewalks and crosswalks are generally mapped as separate footpaths. + Currently, you can only specify a single road class to avoid. */ - open var walkwayPriority: DirectionsPriority = .default + @objc open var roadClassesToAvoid: RoadClasses = [] /** - The expected uniform travel speed measured in meters per second. - - This property has no effect unless the profile identifier is set to `MBDirectionsProfileIdentifier.walking`. You can adjust this property to account for running or for faster or slower gaits. When the profile identifier is set to another profile identifier, such as `MBDirectionsProfileIdentifier.driving`, this property is ignored in favor of the expected travel speed on each road along the route. This property may be supported by other routing profiles in the future. - - The value of this property must be at least `MBMinimumWalkingSpeed` and at most `MBMaximumWalkingSpeed`. The default value is `MBDefaultWalkingSpeed`. + An array of URL parameters to include in the request URL. */ - open var speed: CLLocationSpeed = MBDefaultWalkingSpeed - - override open var urlQueryItems: [URLQueryItem] { - var queryItems = super.urlQueryItems - - queryItems.append(contentsOf: [ + internal var params: [URLQueryItem] { + var params: [URLQueryItem] = [ URLQueryItem(name: "alternatives", value: String(includesAlternativeRoutes)), - URLQueryItem(name: "continue_straight", value: String(!allowsUTurnAtWaypoint)) - ]) - + URLQueryItem(name: "geometries", value: String(describing: shapeFormat)), + URLQueryItem(name: "overview", value: String(describing: routeShapeResolution)), + URLQueryItem(name: "steps", value: String(includesSteps)), + URLQueryItem(name: "continue_straight", value: String(!allowsUTurnAtWaypoint)), + URLQueryItem(name: "language", value: locale.identifier) + ] + if includesExitRoundaboutManeuver { - queryItems.append(URLQueryItem(name: "roundabout_exits", value: String(includesExitRoundaboutManeuver))) + params.append(URLQueryItem(name: "roundabout_exits", value: String(includesExitRoundaboutManeuver))) + } + + if includesSpokenInstructions { + params.append(URLQueryItem(name: "voice_instructions", value: String(includesSpokenInstructions))) + params.append(URLQueryItem(name: "voice_units", value: String(describing: distanceMeasurementSystem))) } - if profileIdentifier == .walking { - queryItems.append(URLQueryItem(name: "alley_bias", value: String(alleyPriority.rawValue))) - queryItems.append(URLQueryItem(name: "walkway_bias", value: String(walkwayPriority.rawValue))) - queryItems.append(URLQueryItem(name: "walking_speed", value: String(speed))) + if includesVisualInstructions { + params.append(URLQueryItem(name: "banner_instructions", value: String(includesVisualInstructions))) } - + if !roadClassesToAvoid.isEmpty { let allRoadClasses = roadClassesToAvoid.description.components(separatedBy: ",") if allRoadClasses.count > 1 { assert(false, "`roadClassesToAvoid` only accepts one `RoadClasses`.") } if let firstRoadClass = allRoadClasses.first { - queryItems.append(URLQueryItem(name: "exclude", value: firstRoadClass)) + params.append(URLQueryItem(name: "exclude", value: firstRoadClass)) } } - if waypoints.first(where: { CLLocationCoordinate2DIsValid($0.targetCoordinate) }) != nil { - let targetCoordinates = waypoints.map { $0.targetCoordinate.stringForRequestURL ?? "" }.joined(separator: ";") - queryItems.append(URLQueryItem(name: "waypoint_targets", value: targetCoordinates)) + // Include headings and heading accuracies if any waypoint has a nonnegative heading. + if !waypoints.filter({ $0.heading >= 0 }).isEmpty { + let headings = waypoints.map { $0.headingDescription }.joined(separator: ";") + params.append(URLQueryItem(name: "bearings", value: headings)) } - return queryItems - } - - /** - Returns response objects that represent the given JSON dictionary data. - - - parameter json: The API response in JSON dictionary format. - - returns: A tuple containing an array of waypoints and an array of routes. - */ - public func response(from json: [String: Any]) -> ([Waypoint]?, [Route]?) { - var namedWaypoints: [Waypoint]? - if let jsonWaypoints = (json["waypoints"] as? [JSONDictionary]) { - namedWaypoints = zip(jsonWaypoints, self.waypoints).map { (api, local) -> Waypoint in - let location = api["location"] as! [Double] - let coordinate = CLLocationCoordinate2D(geoJSON: location) - let possibleAPIName = api["name"] as? String - let apiName = possibleAPIName?.nonEmptyString - let waypoint = local.copy() as! Waypoint - waypoint.coordinate = coordinate - waypoint.name = waypoint.name ?? apiName - return waypoint - } + // Include location accuracies if any waypoint has a nonnegative coordinate accuracy. + if !waypoints.filter({ $0.coordinateAccuracy >= 0 }).isEmpty { + let accuracies = waypoints.map { + $0.coordinateAccuracy >= 0 ? String($0.coordinateAccuracy) : "unlimited" + }.joined(separator: ";") + params.append(URLQueryItem(name: "radiuses", value: accuracies)) } - let waypoints = namedWaypoints ?? self.waypoints - waypoints.first?.separatesLegs = true - waypoints.last?.separatesLegs = true - let legSeparators = waypoints.filter { $0.separatesLegs } + if !attributeOptions.isEmpty { + let attributesStrings = String(describing:attributeOptions) - let routes = (json["routes"] as? [JSONDictionary])?.map { - Route(json: $0, waypoints: legSeparators, options: self) + params.append(URLQueryItem(name: "annotations", value: attributesStrings)) } - return (waypoints, routes) - } - override public class var supportsSecureCoding: Bool { - return true + return params } - - + // MARK: NSCopying - override open func copy(with zone: NSZone? = nil) -> Any { - let copy = super.copy(with: zone) as! RouteOptions - copy.allowsUTurnAtWaypoint = allowsUTurnAtWaypoint - copy.includesAlternativeRoutes = includesAlternativeRoutes - copy.includesExitRoundaboutManeuver = includesExitRoundaboutManeuver - copy.roadClassesToAvoid = roadClassesToAvoid - copy.alleyPriority = alleyPriority - copy.walkwayPriority = walkwayPriority - copy.speed = speed - return copy + public func copy(with zone: NSZone? = nil) -> Any { + let data = try! JSONEncoder().encode(self) + return try! JSONDecoder().decode(RouteOptions.self, from: data) } - + //MARK: - OBJ-C Equality open override func isEqual(_ object: Any?) -> Bool { guard let opts = object as? RouteOptions else { return false } return isEqual(to: opts) } - + @objc(isEqualToRouteOptions:) open func isEqual(to routeOptions: RouteOptions?) -> Bool { guard let other = routeOptions else { return false } - guard super.isEqual(to: routeOptions) else { return false } - guard allowsUTurnAtWaypoint == other.allowsUTurnAtWaypoint, - includesAlternativeRoutes == other.includesAlternativeRoutes, + guard waypoints == other.waypoints, + profileIdentifier == other.profileIdentifier, + allowsUTurnAtWaypoint == other.allowsUTurnAtWaypoint, + includesSteps == other.includesSteps, + shapeFormat == other.shapeFormat, + routeShapeResolution == other.routeShapeResolution, + attributeOptions == other.attributeOptions, includesExitRoundaboutManeuver == other.includesExitRoundaboutManeuver, + locale == other.locale, + includesSpokenInstructions == other.includesSpokenInstructions, + includesVisualInstructions == other.includesVisualInstructions, roadClassesToAvoid == other.roadClassesToAvoid, - alleyPriority == other.alleyPriority, - walkwayPriority == other.walkwayPriority, - speed == other.speed else { return false } + distanceMeasurementSystem == other.distanceMeasurementSystem else { return false } return true } } +// MARK: Support for Directions API v4 + +/** + A `RouteShapeFormat` indicates the format of a route’s shape in the raw HTTP response. + */ +@objc(MBInstructionFormat) +public enum InstructionFormat: UInt, CustomStringConvertible { + /** + The route steps’ instructions are delivered in plain text format. + */ + case text + /** + The route steps’ instructions are delivered in HTML format. + + Key phrases are boldfaced. + */ + case html + + public init?(description: String) { + let format: InstructionFormat + switch description { + case "text": + format = .text + case "html": + format = .html + default: + return nil + } + self.init(rawValue: format.rawValue) + } + + public var description: String { + switch self { + case .text: + return "text" + case .html: + return "html" + } + } +} + /** A `RouteOptionsV4` object is a structure that specifies the criteria for results returned by the Mapbox Directions API v4. Pass an instance of this class into the `Directions.calculate(_:completionHandler:)` method. */ -@objcMembers @objc(MBRouteOptionsV4) open class RouteOptionsV4: RouteOptions { // MARK: Specifying the Response Format @@ -289,7 +542,7 @@ open class RouteOptionsV4: RouteOptions { By default, the value of this property is `text`, specifying plain text instructions. */ - open var instructionFormat: InstructionFormat = .text + @objc open var instructionFormat: InstructionFormat = .text /** A Boolean value indicating whether the returned routes and their route steps should include any geographic coordinate data. @@ -298,47 +551,17 @@ open class RouteOptionsV4: RouteOptions { The default value of this property is `true`. */ - open var includesShapes: Bool = true - - public required init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier?) { - super.init(waypoints: waypoints, profileIdentifier: profileIdentifier) - } - - public required init?(coder decoder: NSCoder) { - super.init(coder: decoder) - - if let description = decoder.decodeObject(of: NSString.self, forKey: "instructionFormat") as String?, - let format = InstructionFormat(description: description) { - instructionFormat = format - } - - includesShapes = decoder.decodeBool(forKey: "includesShapes") - } - - public override func encode(with coder: NSCoder) { - super.encode(with: coder) - - coder.encode(instructionFormat.description, forKey: "instructionFormat") - coder.encode(includesShapes, forKey: "includesShapes") - } - - override public class var supportsSecureCoding: Bool { - return true - } - - override open func copy(with zone: NSZone? = nil) -> Any { - let copy = super.copy(with: zone) as! RouteOptionsV4 - copy.instructionFormat = instructionFormat - copy.includesShapes = includesShapes - return copy - } - - internal override var abridgedPath: String { + @objc open var includesShapes: Bool = true + + override var path: String { + assert(!queries.isEmpty, "No query") + let profileIdentifier = self.profileIdentifier.rawValue.replacingOccurrences(of: "/", with: ".") - return "v4/directions/\(profileIdentifier)" + let queryComponent = queries.joined(separator: ";") + return "v4/directions/\(profileIdentifier)/\(queryComponent).json" } - override open var urlQueryItems: [URLQueryItem] { + override var params: [URLQueryItem] { return [ URLQueryItem(name: "alternatives", value: String(includesAlternativeRoutes)), URLQueryItem(name: "instructions", value: String(describing: instructionFormat)), @@ -346,15 +569,13 @@ open class RouteOptionsV4: RouteOptions { URLQueryItem(name: "steps", value: String(includesSteps)), ] } +} - override public func response(from json: [String: Any]) -> ([Waypoint]?, [Route]?) { - let sourceWaypoint = Waypoint(geoJSON: json["origin"] as! JSONDictionary)! - let destinationWaypoint = Waypoint(geoJSON: json["destination"] as! JSONDictionary)! - let intermediateWaypoints = (json["waypoints"] as! [JSONDictionary]).compactMap { Waypoint(geoJSON: $0) } - let waypoints = [sourceWaypoint] + intermediateWaypoints + [destinationWaypoint] - let routes = (json["routes"] as? [JSONDictionary])?.map { - RouteV4(json: $0, waypoints: waypoints, options: self) +extension Locale { + fileprivate var usesMetric: Bool { + guard let measurementSystem = (self as NSLocale).object(forKey: .measurementSystem) as? String else { + return false } - return (waypoints, routes) + return measurementSystem == "Metric" } } diff --git a/Sources/MapboxDirections/MBWaypoint.swift b/Sources/MapboxDirections/MBWaypoint.swift index 2336320c3..9437546fb 100644 --- a/Sources/MapboxDirections/MBWaypoint.swift +++ b/Sources/MapboxDirections/MBWaypoint.swift @@ -1,40 +1,50 @@ -import Foundation -import CoreLocation - - /** A `Waypoint` object indicates a location along a route. It may be the route’s origin or destination, or it may be another location that the route visits. A waypoint object indicates the location’s geographic location along with other optional information, such as a name or the user’s direction approaching the waypoint. You create a `RouteOptions` object using waypoint objects and also receive waypoint objects in the completion handler of the `Directions.calculate(_:completionHandler:)` method. */ - - -open class Waypoint: NSObject, NSCopying, NSSecureCoding { +@objc(MBWaypoint) +open class Waypoint: NSObject, Codable { // MARK: Creating a Waypoint Object - - public class var supportsSecureCoding: Bool { - return true + + private enum CodingKeys: String, CodingKey { + case coordinate = "location" + case coordinateAccuracy + case name + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + coordinate = try container.decode(CLLocationCoordinate2D.self, forKey: .coordinate) + coordinateAccuracy = try container.decodeIfPresent(CLLocationAccuracy.self, forKey: .coordinateAccuracy) ?? -1 + + if let name = try container.decodeIfPresent(String.self, forKey: .name), + !name.isEmpty { + self.name = name + } else { + name = nil + } } - + /** Initializes a new waypoint object with the given geographic coordinate and an optional accuracy and name. - + - parameter coordinate: The geographic coordinate of the waypoint. - parameter coordinateAccuracy: The maximum distance away from the waypoint that the route may come and still be considered viable. This parameter is measured in meters. A negative value means the route may be an indefinite number of meters away from the route and still be considered viable. - + It is recommended that the value of this parameter be greater than the `horizontalAccuracy` property of a `CLLocation` object obtained from a `CLLocationManager` object. There is a high likelihood that the user may be located some distance away from a navigable road, for instance if the user is currently on a driveway or inside a building. - parameter name: The name of the waypoint. This parameter does not affect the route but may help you to distinguish one waypoint from another. */ - public init(coordinate: CLLocationCoordinate2D, coordinateAccuracy: CLLocationAccuracy = -1, name: String? = nil) { + @objc public init(coordinate: CLLocationCoordinate2D, coordinateAccuracy: CLLocationAccuracy = -1, name: String? = nil) { self.coordinate = coordinate self.coordinateAccuracy = coordinateAccuracy self.name = name } - + #if os(tvOS) || os(watchOS) /** Initializes a new waypoint object with the given `CLLocation` object and an optional heading value and name. - + - note: This initializer is intended for `CLLocation` objects created using the `CLLocation.init(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid overfitting, since the `Waypoint` class’s `coordinateAccuracy` property represents the maximum allowed deviation from the waypoint. There is a high likelihood that the user may be located some distance away from a navigable road, for instance if the user is currently on a driveway of inside a building. - + - parameter location: A `CLLocation` object representing the waypoint’s location. This initializer respects the `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `coordinate` and `coordinateAccuracy` properties, respectively. - parameter heading: A `CLLocationDirection` value representing the direction from which the route must approach the waypoint in order to be considered viable. This value is stored in the `headingAccuracy` property. - parameter name: The name of the waypoint. This parameter does not affect the route but may help you to distinguish one waypoint from another. @@ -50,14 +60,14 @@ open class Waypoint: NSObject, NSCopying, NSSecureCoding { #else /** Initializes a new waypoint object with the given `CLLocation` object and an optional `CLHeading` object and name. - + - note: This initializer is intended for `CLLocation` objects created using the `CLLocation.init(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid overfitting, since the `Waypoint` class’s `coordinateAccuracy` property represents the maximum allowed deviation from the waypoint. There is a high likelihood that the user may be located some distance away from a navigable road, for instance if the user is currently on a driveway of inside a building. - + - parameter location: A `CLLocation` object representing the waypoint’s location. This initializer respects the `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `coordinate` and `coordinateAccuracy` properties, respectively. - parameter heading: A `CLHeading` object representing the direction from which the route must approach the waypoint in order to be considered viable. This initializer respects the `CLHeading` class’s `trueHeading` property or `magneticHeading` property, converting it into the `headingAccuracy` property. - parameter name: The name of the waypoint. This parameter does not affect the route but may help you to distinguish one waypoint from another. */ - public init(location: CLLocation, heading: CLHeading? = nil, name: String? = nil) { + @objc public init(location: CLLocation, heading: CLHeading? = nil, name: String? = nil) { coordinate = location.coordinate coordinateAccuracy = location.horizontalAccuracy if let heading = heading { @@ -66,190 +76,81 @@ open class Waypoint: NSObject, NSCopying, NSSecureCoding { self.name = name } #endif - - public required init?(coder decoder: NSCoder) { - let latitude = decoder.decodeDouble(forKey: "latitude") - let longitude = decoder.decodeDouble(forKey: "longitude") - coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) - coordinateAccuracy = decoder.decodeDouble(forKey: "coordinateAccuracy") - let targetLatitude = decoder.decodeDouble(forKey: "targetLatitude") - let targetLongitude = decoder.decodeDouble(forKey: "targetLongitude") - targetCoordinate = CLLocationCoordinate2D(latitude: targetLatitude, longitude: targetLongitude) - heading = decoder.decodeDouble(forKey: "heading") - headingAccuracy = decoder.decodeDouble(forKey: "headingAccuracy") - name = decoder.decodeObject(of: NSString.self, forKey: "name") as String? - allowsArrivingOnOppositeSide = decoder.decodeBool(forKey: "allowsArrivingOnOppositeSide") - separatesLegs = decoder.decodeBool(forKey: "separatesLegs") - } - - open func encode(with coder: NSCoder) { - coder.encode(coordinate.latitude, forKey: "latitude") - coder.encode(coordinate.longitude, forKey: "longitude") - coder.encode(coordinateAccuracy, forKey: "coordinateAccuracy") - coder.encode(targetCoordinate.latitude, forKey: "targetLatitude") - coder.encode(targetCoordinate.longitude, forKey: "targetLongitude") - coder.encode(heading, forKey: "heading") - coder.encode(headingAccuracy, forKey: "headingAccuracy") - coder.encode(name, forKey: "name") - coder.encode(allowsArrivingOnOppositeSide, forKey: "allowsArrivingOnOppositeSide") - coder.encode(separatesLegs, forKey: "separatesLegs") - } - + open func copy(with zone: NSZone?) -> Any { let copy = Waypoint(coordinate: coordinate, coordinateAccuracy: coordinateAccuracy, name: name) - copy.targetCoordinate = targetCoordinate copy.heading = heading copy.headingAccuracy = headingAccuracy - copy.allowsArrivingOnOppositeSide = allowsArrivingOnOppositeSide - copy.separatesLegs = separatesLegs return copy } - // MARK: Objective-C equality - open override func isEqual(_ object: Any?) -> Bool { - guard let opts = object as? Waypoint else { return false } - return isEqual(to: opts) - } - + // MARK: Getting the Waypoint’s Location - open func isEqual(to other: Waypoint?) -> Bool { - guard let other = other else { return false } - return type(of: self) == type(of: other) && - coordinate.latitude == other.coordinate.latitude && - coordinate.longitude == other.coordinate.longitude && - coordinateAccuracy == other.coordinateAccuracy && - targetCoordinate.latitude == other.targetCoordinate.latitude && - targetCoordinate.longitude == other.targetCoordinate.longitude && - heading == other.heading && - headingAccuracy == other.headingAccuracy && - name == other.name && - allowsArrivingOnOppositeSide == other.allowsArrivingOnOppositeSide && - separatesLegs && other.separatesLegs - } - - // MARK: Specifying the Waypoint’s Location - /** The geographic coordinate of the waypoint. */ - public internal(set) var coordinate: CLLocationCoordinate2D - + @objc open let coordinate: CLLocationCoordinate2D + /** The radius of uncertainty for the waypoint, measured in meters. - + For a route to be considered viable, it must enter this waypoint’s circle of uncertainty. The `coordinate` property identifies the center of the circle, while this property indicates the circle’s radius. If the value of this property is negative, a route is considered viable regardless of whether it enters this waypoint’s circle of uncertainty, subject to an undefined maximum distance. - + By default, the value of this property is a negative number. - - This property corresponds to the [`radiuses`](https://docs.mapbox.com/api/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. - */ - open var coordinateAccuracy: CLLocationAccuracy = -1 - - /** - The geographic coordinate of the waypoint’s target. - - The waypoint’s target affects arrival instructions without affecting the route’s shape. For example, a delivery or ride hailing application may specify a waypoint target that represents a drop-off location. The target determines whether the arrival visual and spoken instructions indicate that the destination is “on the left” or “on the right”. - - By default, this property is set to `kCLLocationCoordinate2DInvalid`, meaning the waypoint has no target. This property is ignored on the first waypoint of a `RouteOptions` object, on any waypoint of a `MatchOptions` object, or on any waypoint of a `RouteOptions` object if `DirectionsOptions.includesSteps` is set to `false`. - - This property corresponds to the [`waypoint_targets`](https://docs.mapbox.com/api/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. */ - open var targetCoordinate: CLLocationCoordinate2D = kCLLocationCoordinate2DInvalid - - // MARK: Specifying How the User Approaches the Waypoint - + @objc open var coordinateAccuracy: CLLocationAccuracy = -1 + + // MARK: Getting the Direction of Approach + /** The direction from which a route must approach this waypoint in order to be considered viable. - + This property is measured in degrees clockwise from true north. A value of 0 degrees means due north, 90 degrees means due east, 180 degrees means due south, and so on. If the value of this property is negative, a route is considered viable regardless of the direction from which it approaches this waypoint. - + If this waypoint is the first waypoint (the source waypoint), the route must start out by heading in the direction specified by this property. You should always set the `headingAccuracy` property in conjunction with this property. If the `headingAccuracy` property is set to a negative value, this property is ignored. - + For driving directions, this property can be useful for avoiding a route that begins by going in the direction opposite the current direction of travel. For example, if you know the user is moving eastwardly and the first waypoint is the user’s current location, specifying a heading of 90 degrees and a heading accuracy of 90 degrees for the first waypoint avoids a route that begins with a “head west” instruction. - + You should be certain that the user is in motion before specifying a heading and heading accuracy; otherwise, you may be unnecessarily filtering out the best route. For example, suppose the user is sitting in a car parked in a driveway, facing due north, with the garage in front and the street to the rear. In that case, specifying a heading of 0 degrees and a heading accuracy of 90 degrees may result in a route that begins on the back alley or, worse, no route at all. For this reason, it is recommended that you only specify a heading and heading accuracy when automatically recalculating directions due to the user deviating from the route. - + By default, the value of this property is a negative number, meaning that a route is considered viable regardless of the direction of approach. - - This property corresponds to the angles in the [`bearings`](https://docs.mapbox.com/api/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. */ - open var heading: CLLocationDirection = -1 - + @objc open var heading: CLLocationDirection = -1 + /** - The maximum tolerance, in degrees, within which a route’s approach to a waypoint may differ from `heading` in either direction but still be considered viable. - + The maximum amount, in degrees, by which a route’s approach to a waypoint may differ from `heading` in either direction in order to be considered viable. + A value of 0 degrees means that the approach must match the specified `heading` exactly – an unlikely scenario. A value of 180 degrees or more means that the approach may be as much as 180 degrees in either direction from the specified `heading`, effectively allowing a candidate route to approach the waypoint from any direction. - + If you set the `heading` property, you should set this property to a value such as 90 degrees, to avoid filtering out routes whose approaches differ only slightly from the specified `heading`. Otherwise, if the `heading` property is set to a negative value, this property is ignored. - + By default, the value of this property is a negative number, meaning that a route is considered viable regardless of the direction of approach. - - This property corresponds to the ranges in the [`bearings`](https://docs.mapbox.com/api/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. */ - open var headingAccuracy: CLLocationDirection = -1 - + @objc open var headingAccuracy: CLLocationDirection = -1 + internal var headingDescription: String { return heading >= 0 && headingAccuracy >= 0 ? "\(heading.truncatingRemainder(dividingBy: 360)),\(min(headingAccuracy, 180))" : "" } - - /** - A boolean value indicating whether arriving on opposite side is allowed. - - This property has no effect if `DirectionsOptions.includesSteps` is set to `false`. - - This property corresponds to the [`approaches`](https://www.mapbox.com/api-documentation/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. - */ - open var allowsArrivingOnOppositeSide = true - - // MARK: Identifying the Waypoint - - /** - The name of the waypoint. - - This property does not affect the route, but you can set the name of a waypoint you pass into a `RouteOptions` object to help you distinguish one waypoint from another in the array of waypoints passed into the completion handler of the `Directions.calculate(_:completionHandler:)` method. This property has no effect if `DirectionsOptions.includesSteps` is set to `false`. - - This property corresponds to the [`waypoint_names`](https://docs.mapbox.com/api/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. - */ - open var name: String? + + // MARK: Getting the Waypoint’s Name /** - A Boolean value indicating whether the waypoint is significant enough to appear in the resulting routes as a waypoint separating two legs, along with corresponding guidance instructions. - - By default, this property is set to `true`, which means that each resulting route will include a leg that ends by arriving at the waypoint as `RouteLeg.destination` and a subsequent leg that begins by departing from the waypoint as `RouteLeg.source`. Otherwise, if this property is set to `false`, a single leg passes through the waypoint without specifically mentioning it. Regardless of the value of this property, each resulting route passes through the location specified by the `coordinate` property, accounting for approach-related properties such as `heading`. + The name of the waypoint. - With the Mapbox Directions API, set this property to `false` if you want the waypoint’s location to influence the path that the route follows without attaching any meaning to the waypoint object itself. With the Mapbox Map Matching API, use this property when the `DirectionsOptions.includesSteps` property is `true` or when `coordinates` represents a trace with a high sample rate. - - This property has no effect if `DirectionsOptions.includesSteps` is set to `false`, or if `MatchOptions.waypointIndices` is non-nil. - - This property corresponds to the [`approaches`](https://docs.mapbox.com/api/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. + This parameter does not affect the route, but you can set the name of a waypoint you pass into a `RouteOptions` object to help you distinguish one waypoint from another in the array of waypoints passed into the completion handler of the `Directions.calculate(_:completionHandler:)` method. */ - open var separatesLegs: Bool = true - - open override var description: String { + @objc open var name: String? + + @objc open override var description: String { return name ?? "" } - + func debugQuickLookObject() -> Any { return CLLocation(coordinate: coordinate, altitude: 0, horizontalAccuracy: coordinateAccuracy, verticalAccuracy: -1, course: heading, speed: -1, timestamp: Date()) } -} - -// MARK: Support for Directions API v4 - -extension Waypoint { - /** - Initializes a new waypoint object with the given GeoJSON point feature data. - - - parameter json: A point feature in GeoJSON format. - */ - internal convenience init?(geoJSON json: JSONDictionary) { - assert(json["type"] as? String == "Feature") - - let coordinate = CLLocationCoordinate2D(geoJSON: json["geometry"] as! JSONDictionary) - - let propertiesJSON = json["properties"] as? JSONDictionary - let name = propertiesJSON?["name"] as? String - - self.init(coordinate: coordinate, name: name) + + open override func isEqual(_ object: Any?) -> Bool { + guard let other = object as? Waypoint else { return false} + return self.coordinate == other.coordinate && self.name == other.name && self.coordinateAccuracy == other.coordinateAccuracy } }