From 3675949239512873e99a6fabef9442a2a93fff59 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Fri, 13 Dec 2019 14:58:11 -0700 Subject: [PATCH 01/21] Adding internal implementation layers and a private `DirectionsError` protocol to allow for custom directions services to provide their own errors --- Sources/MapboxDirections/Directions.swift | 39 ++++++++++++------- .../MapboxDirections/DirectionsError.swift | 9 +++-- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index d79a1e2b4..69643617b 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -141,6 +141,7 @@ open class Directions: NSObject { // MARK: Getting Directions + internal typealias RouteCompletionHandlerInteral = (_ waypoints: [Waypoint]?, _ routes: [Route]?, _ error: DirectionsServiceError?) -> Void /** Begins asynchronously calculating routes using the given options and delivers the results to a closure. @@ -153,22 +154,24 @@ open class Directions: NSObject { - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel this task. */ @discardableResult open func calculate(_ options: RouteOptions, completionHandler: @escaping RouteCompletionHandler) -> URLSessionDataTask { + + let complete = completionHandler as RouteCompletionHandlerInteral let fetchStart = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in let responseEndDate = Date() guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { - completionHandler(nil, nil, .invalidResponse) + complete(nil, nil, .invalidResponse) return } guard let data = possibleData else { - completionHandler(nil, nil, .noData) + complete(nil, nil, .noData) return } if let error = possibleError { - completionHandler(nil, nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) + complete(nil, nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) return } @@ -183,7 +186,7 @@ open class Directions: NSObject { } guard let routes = result.routes else { - completionHandler(result.waypoints, nil, .unableToRoute) + complete(result.waypoints, nil, .unableToRoute) return } @@ -206,6 +209,8 @@ open class Directions: NSObject { return requestTask } + + internal typealias MatchCompletionHandlerInternal = (_ matches: [Match]?, _ error: DirectionsServiceError?) -> Void /** Begins asynchronously calculating matches using the given options and delivers the results to a closure. @@ -218,22 +223,24 @@ open class Directions: NSObject { - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting matches, cancel this task. */ @discardableResult open func calculate(_ options: MatchOptions, completionHandler: @escaping MatchCompletionHandler) -> URLSessionDataTask { + + let complete = completionHandler as MatchCompletionHandlerInternal let fetchStart = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in let responseEndDate = Date() guard let response = possibleResponse, response.mimeType == "application/json" else { - completionHandler(nil, .invalidResponse) + complete(nil, .invalidResponse) return } guard let data = possibleData else { - completionHandler(nil, .noData) + complete(nil, .noData) return } if let error = possibleError { - completionHandler(nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) + complete(nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) return } @@ -248,7 +255,7 @@ open class Directions: NSObject { } guard let matches = result.matches else { - completionHandler(nil, .unableToRoute) + complete(nil, .unableToRoute) return } @@ -259,7 +266,7 @@ open class Directions: NSObject { } } catch { DispatchQueue.main.async { - completionHandler(nil, .unknown(response: response, underlying: error, code: nil, message: nil)) + complete(nil, .unknown(response: response, underlying: error, code: nil, message: nil)) } } } @@ -282,22 +289,24 @@ open class Directions: NSObject { - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel this task. */ @discardableResult open func calculateRoutes(matching options: MatchOptions, completionHandler: @escaping RouteCompletionHandler) -> URLSessionDataTask { + + let complete = completionHandler as RouteCompletionHandlerInteral let fetchStart = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in let responseEndDate = Date() guard let response = possibleResponse, response.mimeType == "application/json" else { - completionHandler(nil, nil, .invalidResponse) + complete(nil, nil, .invalidResponse) return } guard let data = possibleData else { - completionHandler(nil, nil, .noData) + complete(nil, nil, .noData) return } if let error = possibleError { - completionHandler(nil, nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) + complete(nil, nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) return } @@ -312,7 +321,7 @@ open class Directions: NSObject { } guard let routes = result.routes else { - completionHandler(result.waypoints, nil, .unableToRoute) + complete(result.waypoints, nil, .unableToRoute) return } @@ -323,7 +332,7 @@ open class Directions: NSObject { } } catch { DispatchQueue.main.async { - completionHandler(nil, nil, .unknown(response: response, underlying: error, code: nil, message: nil)) + complete(nil, nil, .unknown(response: response, underlying: error, code: nil, message: nil)) } } } @@ -402,7 +411,7 @@ 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(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) -> DirectionsError { + static func informativeError(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) -> DirectionsServiceError { if let response = response as? HTTPURLResponse { switch (response.statusCode, code ?? "") { case (200, "NoRoute"): diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 7d084beeb..0f7d46ba3 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -1,9 +1,12 @@ import Foundation +public protocol DirectionsError: LocalizedError { } + + /** An error that occurs when calculating directions. */ -public enum DirectionsError: LocalizedError { +public enum DirectionsServiceError: DirectionsError { /** The server returned an empty response. */ @@ -131,8 +134,8 @@ public enum DirectionsError: LocalizedError { } } -extension DirectionsError: Equatable { - public static func == (lhs: DirectionsError, rhs: DirectionsError) -> Bool { +extension DirectionsServiceError: Equatable { + public static func == (lhs: DirectionsServiceError, rhs: DirectionsServiceError) -> Bool { switch (lhs, rhs) { case (.noData, .noData), (.invalidResponse, .invalidResponse), From e1a2d391ebdfe252c3a8cf2ba446fff20077acfb Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Fri, 13 Dec 2019 15:09:53 -0700 Subject: [PATCH 02/21] Making RouteResponse public so we can decode offline directions in nav-sdk --- Sources/MapboxDirections/RouteResponse.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index ee628bdbd..b5ee41f7f 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -1,12 +1,12 @@ import Foundation -struct RouteResponse { - var code: String? - var message: String? - var error: String? - let uuid: String? - let routes: [Route]? - let waypoints: [Waypoint]? +public struct RouteResponse { + public var code: String? + public var message: String? + public var error: String? + public let uuid: String? + public let routes: [Route]? + public let waypoints: [Waypoint]? } extension RouteResponse: Codable { @@ -19,7 +19,7 @@ extension RouteResponse: Codable { case waypoints } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.code = try container.decodeIfPresent(String.self, forKey: .code) From 56bf20116b02e45058f0a76386ce534900be7e60 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Fri, 13 Dec 2019 15:44:09 -0700 Subject: [PATCH 03/21] Going to need to take Lane public. --- Sources/MapboxDirections/Lane.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/MapboxDirections/Lane.swift b/Sources/MapboxDirections/Lane.swift index 990e89c6e..cdf7df223 100644 --- a/Sources/MapboxDirections/Lane.swift +++ b/Sources/MapboxDirections/Lane.swift @@ -3,15 +3,15 @@ import Foundation /** A lane on the road approaching an intersection. */ -struct Lane: Equatable { +public struct Lane: Equatable { /** The lane indications specifying the maneuvers that may be executed from the lane. */ - let indications: LaneIndication + public let indications: LaneIndication - var isValid: Bool + public var isValid: Bool - init(indications: LaneIndication, valid: Bool = false) { + public init(indications: LaneIndication, valid: Bool = false) { self.indications = indications self.isValid = valid } @@ -23,13 +23,13 @@ extension Lane: Codable { case valid } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(indications, forKey: .indications) try container.encode(isValid, forKey: .valid) } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) indications = try container.decode(LaneIndication.self, forKey: .indications) isValid = try container.decode(Bool.self, forKey: .valid) From 864743fd8051f7261d2dd7063afbe6c936b1bbf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Fri, 20 Dec 2019 20:50:56 -0800 Subject: [PATCH 04/21] Revert "Going to need to take Lane public." This reverts commit 48587460a282e83062c559487fca94171c0bb438. --- Sources/MapboxDirections/Lane.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/MapboxDirections/Lane.swift b/Sources/MapboxDirections/Lane.swift index cdf7df223..990e89c6e 100644 --- a/Sources/MapboxDirections/Lane.swift +++ b/Sources/MapboxDirections/Lane.swift @@ -3,15 +3,15 @@ import Foundation /** A lane on the road approaching an intersection. */ -public struct Lane: Equatable { +struct Lane: Equatable { /** The lane indications specifying the maneuvers that may be executed from the lane. */ - public let indications: LaneIndication + let indications: LaneIndication - public var isValid: Bool + var isValid: Bool - public init(indications: LaneIndication, valid: Bool = false) { + init(indications: LaneIndication, valid: Bool = false) { self.indications = indications self.isValid = valid } @@ -23,13 +23,13 @@ extension Lane: Codable { case valid } - public func encode(to encoder: Encoder) throws { + func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(indications, forKey: .indications) try container.encode(isValid, forKey: .valid) } - public init(from decoder: Decoder) throws { + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) indications = try container.decode(LaneIndication.self, forKey: .indications) isValid = try container.decode(Bool.self, forKey: .valid) From e287b4e506fa752fedab356151346d6efb8e0365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Fri, 20 Dec 2019 21:37:37 -0800 Subject: [PATCH 05/21] Revert "Adding internal implementation layers and a private `DirectionsError` protocol to allow for custom directions services to provide their own errors" This reverts commit 110566d82e534d64158391ae36b545206876dbcb. --- Sources/MapboxDirections/Directions.swift | 39 +++++++------------ .../MapboxDirections/DirectionsError.swift | 9 ++--- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index 69643617b..d79a1e2b4 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -141,7 +141,6 @@ open class Directions: NSObject { // MARK: Getting Directions - internal typealias RouteCompletionHandlerInteral = (_ waypoints: [Waypoint]?, _ routes: [Route]?, _ error: DirectionsServiceError?) -> Void /** Begins asynchronously calculating routes using the given options and delivers the results to a closure. @@ -154,24 +153,22 @@ open class Directions: NSObject { - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel this task. */ @discardableResult open func calculate(_ options: RouteOptions, completionHandler: @escaping RouteCompletionHandler) -> URLSessionDataTask { - - let complete = completionHandler as RouteCompletionHandlerInteral let fetchStart = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in let responseEndDate = Date() guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { - complete(nil, nil, .invalidResponse) + completionHandler(nil, nil, .invalidResponse) return } guard let data = possibleData else { - complete(nil, nil, .noData) + completionHandler(nil, nil, .noData) return } if let error = possibleError { - complete(nil, nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) + completionHandler(nil, nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) return } @@ -186,7 +183,7 @@ open class Directions: NSObject { } guard let routes = result.routes else { - complete(result.waypoints, nil, .unableToRoute) + completionHandler(result.waypoints, nil, .unableToRoute) return } @@ -209,8 +206,6 @@ open class Directions: NSObject { return requestTask } - - internal typealias MatchCompletionHandlerInternal = (_ matches: [Match]?, _ error: DirectionsServiceError?) -> Void /** Begins asynchronously calculating matches using the given options and delivers the results to a closure. @@ -223,24 +218,22 @@ open class Directions: NSObject { - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting matches, cancel this task. */ @discardableResult open func calculate(_ options: MatchOptions, completionHandler: @escaping MatchCompletionHandler) -> URLSessionDataTask { - - let complete = completionHandler as MatchCompletionHandlerInternal let fetchStart = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in let responseEndDate = Date() guard let response = possibleResponse, response.mimeType == "application/json" else { - complete(nil, .invalidResponse) + completionHandler(nil, .invalidResponse) return } guard let data = possibleData else { - complete(nil, .noData) + completionHandler(nil, .noData) return } if let error = possibleError { - complete(nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) + completionHandler(nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) return } @@ -255,7 +248,7 @@ open class Directions: NSObject { } guard let matches = result.matches else { - complete(nil, .unableToRoute) + completionHandler(nil, .unableToRoute) return } @@ -266,7 +259,7 @@ open class Directions: NSObject { } } catch { DispatchQueue.main.async { - complete(nil, .unknown(response: response, underlying: error, code: nil, message: nil)) + completionHandler(nil, .unknown(response: response, underlying: error, code: nil, message: nil)) } } } @@ -289,24 +282,22 @@ open class Directions: NSObject { - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel this task. */ @discardableResult open func calculateRoutes(matching options: MatchOptions, completionHandler: @escaping RouteCompletionHandler) -> URLSessionDataTask { - - let complete = completionHandler as RouteCompletionHandlerInteral let fetchStart = Date() let request = urlRequest(forCalculating: options) let requestTask = URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in let responseEndDate = Date() guard let response = possibleResponse, response.mimeType == "application/json" else { - complete(nil, nil, .invalidResponse) + completionHandler(nil, nil, .invalidResponse) return } guard let data = possibleData else { - complete(nil, nil, .noData) + completionHandler(nil, nil, .noData) return } if let error = possibleError { - complete(nil, nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) + completionHandler(nil, nil, .unknown(response: possibleResponse, underlying: error, code: nil, message: nil)) return } @@ -321,7 +312,7 @@ open class Directions: NSObject { } guard let routes = result.routes else { - complete(result.waypoints, nil, .unableToRoute) + completionHandler(result.waypoints, nil, .unableToRoute) return } @@ -332,7 +323,7 @@ open class Directions: NSObject { } } catch { DispatchQueue.main.async { - complete(nil, nil, .unknown(response: response, underlying: error, code: nil, message: nil)) + completionHandler(nil, nil, .unknown(response: response, underlying: error, code: nil, message: nil)) } } } @@ -411,7 +402,7 @@ 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(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) -> DirectionsServiceError { + static func informativeError(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) -> DirectionsError { if let response = response as? HTTPURLResponse { switch (response.statusCode, code ?? "") { case (200, "NoRoute"): diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 0f7d46ba3..7d084beeb 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -1,12 +1,9 @@ import Foundation -public protocol DirectionsError: LocalizedError { } - - /** An error that occurs when calculating directions. */ -public enum DirectionsServiceError: DirectionsError { +public enum DirectionsError: LocalizedError { /** The server returned an empty response. */ @@ -134,8 +131,8 @@ public enum DirectionsServiceError: DirectionsError { } } -extension DirectionsServiceError: Equatable { - public static func == (lhs: DirectionsServiceError, rhs: DirectionsServiceError) -> Bool { +extension DirectionsError: Equatable { + public static func == (lhs: DirectionsError, rhs: DirectionsError) -> Bool { switch (lhs, rhs) { case (.noData, .noData), (.invalidResponse, .invalidResponse), From 723ca65eccdd3ab883ec8954c98d9ac750af7c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Fri, 20 Dec 2019 21:39:18 -0800 Subject: [PATCH 06/21] Added public initializers for visual instruction components --- .../VisualInstructionComponent.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Sources/MapboxDirections/VisualInstructionComponent.swift b/Sources/MapboxDirections/VisualInstructionComponent.swift index 42af6c023..9a72e908b 100644 --- a/Sources/MapboxDirections/VisualInstructionComponent.swift +++ b/Sources/MapboxDirections/VisualInstructionComponent.swift @@ -63,6 +63,15 @@ public extension VisualInstruction.Component { A textual representation of a visual instruction component. */ struct TextRepresentation: Equatable { + /** + Initializes a text representation bearing the given abbreviatable text. + */ + public init(text: String, abbreviation: String?, abbreviationPriority: Int?) { + self.text = text + self.abbreviation = abbreviation + self.abbreviationPriority = abbreviationPriority + } + /** The plain text representation of this component. */ @@ -95,6 +104,13 @@ public extension VisualInstruction.Component { case svg } + /** + Initializes an image representation bearing the image at the given base URL. + */ + public init(imageBaseURL: URL?) { + self.imageBaseURL = imageBaseURL + } + /** The URL whose path is the prefix of all the possible URLs returned by `imageURL(scale:format:)`. */ From 4e5162ba4902ed530483750984af985837ed0098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 20 Dec 2019 22:14:09 -0800 Subject: [PATCH 07/21] Made Match and Route equatable --- Sources/MapboxDirections/MapMatching/Match.swift | 13 +++++++++++++ Sources/MapboxDirections/Route.swift | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/Sources/MapboxDirections/MapMatching/Match.swift b/Sources/MapboxDirections/MapMatching/Match.swift index 1a82e3464..df5ff0cc4 100644 --- a/Sources/MapboxDirections/MapMatching/Match.swift +++ b/Sources/MapboxDirections/MapMatching/Match.swift @@ -65,3 +65,16 @@ open class Match: DirectionsResult { */ public let matchOptions: MatchOptions } + +extension Match: Equatable { + public static func ==(lhs: Match, rhs: Match) -> Bool { + return lhs.routeIdentifier == rhs.routeIdentifier && + lhs.distance == rhs.distance && + lhs.expectedTravelTime == rhs.expectedTravelTime && + lhs.speechLocale == rhs.speechLocale && + lhs.confidence == rhs.confidence && + lhs.tracepoints == rhs.tracepoints && + lhs.legs == rhs.legs && + lhs.shape == rhs.shape + } +} diff --git a/Sources/MapboxDirections/Route.swift b/Sources/MapboxDirections/Route.swift index 84a20ff3d..305f51a7c 100644 --- a/Sources/MapboxDirections/Route.swift +++ b/Sources/MapboxDirections/Route.swift @@ -31,3 +31,14 @@ open class Route: DirectionsResult { } public var routeOptions: RouteOptions } + +extension Route: Equatable { + public static func ==(lhs: Route, rhs: Route) -> Bool { + return lhs.routeIdentifier == rhs.routeIdentifier && + lhs.distance == rhs.distance && + lhs.expectedTravelTime == rhs.expectedTravelTime && + lhs.speechLocale == rhs.speechLocale && + lhs.legs == rhs.legs && + lhs.shape == rhs.shape + } +} From 991a15944f452b4276d39ef803a9c032cb9627f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 20 Dec 2019 22:28:45 -0800 Subject: [PATCH 08/21] Always call completion handlers on main thread --- Sources/MapboxDirections/Directions.swift | 24 +++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index d79a1e2b4..6f110796a 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -178,12 +178,16 @@ open class Directions: NSObject { let result = try decoder.decode(RouteResponse.self, from: data) guard (result.code == nil && result.message == nil) || result.code == "Ok" else { let apiError = Directions.informativeError(code: result.code, message: result.message, response: response, underlyingError: possibleError) - completionHandler(nil, nil, apiError) + DispatchQueue.main.async { + completionHandler(nil, nil, apiError) + } return } guard let routes = result.routes else { - completionHandler(result.waypoints, nil, .unableToRoute) + DispatchQueue.main.async { + completionHandler(result.waypoints, nil, .unableToRoute) + } return } @@ -243,12 +247,16 @@ open class Directions: NSObject { let result = try decoder.decode(MatchResponse.self, from: data) guard result.code == "Ok" else { let apiError = Directions.informativeError(code: result.code, message: result.message, response: response, underlyingError: possibleError) - completionHandler(nil, apiError) + DispatchQueue.main.async { + completionHandler(nil, apiError) + } return } guard let matches = result.matches else { - completionHandler(nil, .unableToRoute) + DispatchQueue.main.async { + completionHandler(nil, .unableToRoute) + } return } @@ -307,12 +315,16 @@ open class Directions: NSObject { let result = try decoder.decode(MapMatchingResponse.self, from: data) guard result.code == "Ok" else { let apiError = Directions.informativeError(code: result.code, message:nil, response: response, underlyingError: possibleError) - completionHandler(nil, nil, apiError) + DispatchQueue.main.async { + completionHandler(nil, nil, apiError) + } return } guard let routes = result.routes else { - completionHandler(result.waypoints, nil, .unableToRoute) + DispatchQueue.main.async { + completionHandler(result.waypoints, nil, .unableToRoute) + } return } From 051c3cd3878afe9f33fa589e62e681f97d8cef77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 20 Dec 2019 22:58:17 -0800 Subject: [PATCH 09/21] Negative accuracies are unset accuracies --- Sources/MapboxDirections/DirectionsOptions.swift | 12 ++++++------ Sources/MapboxDirections/Waypoint.swift | 3 ++- Tests/MapboxDirectionsTests/WaypointTests.swift | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/Sources/MapboxDirections/DirectionsOptions.swift b/Sources/MapboxDirections/DirectionsOptions.swift index 0eb831f05..b7c3e822c 100644 --- a/Sources/MapboxDirections/DirectionsOptions.swift +++ b/Sources/MapboxDirections/DirectionsOptions.swift @@ -352,20 +352,20 @@ open class DirectionsOptions: Codable { return queryItems } - private var bearings: String? { - if waypoints.compactMap({ $0.heading }).isEmpty { + var bearings: String? { + guard waypoints.contains(where: { $0.heading ?? -1 >= 0 }) else { return nil } return waypoints.map({ $0.headingDescription }).joined(separator: ";") } - private var radiuses: String? { - guard !self.waypoints.filter({ $0.coordinateAccuracy != nil}).isEmpty else { + var radiuses: String? { + guard waypoints.contains(where: { $0.coordinateAccuracy ?? -1 >= 0 }) else { return nil } - let accuracies = self.waypoints.map { (waypoint) -> String in - guard let accuracy = waypoint.coordinateAccuracy else { + let accuracies = self.waypoints.map { (waypoint) -> String in + guard let accuracy = waypoint.coordinateAccuracy, accuracy >= 0 else { return "unlimited" } return String(accuracy) diff --git a/Sources/MapboxDirections/Waypoint.swift b/Sources/MapboxDirections/Waypoint.swift index 53bc38cbc..679f09190 100644 --- a/Sources/MapboxDirections/Waypoint.swift +++ b/Sources/MapboxDirections/Waypoint.swift @@ -155,7 +155,8 @@ public class Waypoint: Codable { public var headingAccuracy: CLLocationDirection? = nil internal var headingDescription: String { - guard let heading = self.heading, let accuracy = self.headingAccuracy else { + guard let heading = heading, heading >= 0, + let accuracy = headingAccuracy, accuracy >= 0 else { return "" } diff --git a/Tests/MapboxDirectionsTests/WaypointTests.swift b/Tests/MapboxDirectionsTests/WaypointTests.swift index 60933fce1..ffaf2322b 100644 --- a/Tests/MapboxDirectionsTests/WaypointTests.swift +++ b/Tests/MapboxDirectionsTests/WaypointTests.swift @@ -135,4 +135,19 @@ class WaypointTests: XCTestCase { // right = Tracepoint(coordinate: CLLocationCoordinate2D(), countOfAlternatives: 0, name: "") // XCTAssertNotEqual(left, right) } + + func testAccuracies() { + let from = Waypoint(location: CLLocation(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0), altitude: -1, horizontalAccuracy: -1, verticalAccuracy: -1, timestamp: Date())) + let to = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0)) + let options = RouteOptions(waypoints: [from, to]) + XCTAssertNil(options.bearings) + XCTAssertNil(options.radiuses) + + from.heading = 90 + from.headingAccuracy = 45 + XCTAssertEqual(options.bearings, "90.0,45.0;") + + from.coordinateAccuracy = 5 + XCTAssertEqual(options.radiuses, "5.0;unlimited") + } } From 762a6c667851d49dc34189f26400bdbe54443716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 20 Dec 2019 23:30:07 -0800 Subject: [PATCH 10/21] Made the other response types public too --- .../MapMatching/MapMatchingResponse.swift | 14 ++++++++------ .../MapMatching/MatchResponse.swift | 16 +++++++++------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index 4520bb18d..e44c844c6 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -1,17 +1,19 @@ import Foundation -class MapMatchingResponse: Decodable { - var code: String - var routes : [Route]? - var waypoints: [Waypoint] - +public struct MapMatchingResponse { + public var code: String + public var routes : [Route]? + public var waypoints: [Waypoint] +} + +extension MapMatchingResponse: Decodable { private enum CodingKeys: String, CodingKey { case code case matches = "matchings" case tracepoints } - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) code = try container.decode(String.self, forKey: .code) routes = try container.decodeIfPresent([Route].self, forKey: .matches) diff --git a/Sources/MapboxDirections/MapMatching/MatchResponse.swift b/Sources/MapboxDirections/MapMatching/MatchResponse.swift index 328186d59..a665a6cbd 100644 --- a/Sources/MapboxDirections/MapMatching/MatchResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MatchResponse.swift @@ -1,11 +1,13 @@ import Foundation -class MatchResponse: Codable { - var code: String - var message: String? - var matches : [Match]? - var tracepoints: [Tracepoint?]? - +public struct MatchResponse { + public var code: String + public var message: String? + public var matches : [Match]? + public var tracepoints: [Tracepoint?]? +} + +extension MatchResponse: Codable { private enum CodingKeys: String, CodingKey { case code case message @@ -13,7 +15,7 @@ class MatchResponse: Codable { case tracepoints } - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) code = try container.decode(String.self, forKey: .code) message = try container.decodeIfPresent(String.self, forKey: .message) From 8b76d0e2a79d259c295cacdab67e3d322613fac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 20 Dec 2019 23:42:06 -0800 Subject: [PATCH 11/21] Allow initializing visual instruction without maneuver --- Sources/MapboxDirections/VisualInstruction.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/MapboxDirections/VisualInstruction.swift b/Sources/MapboxDirections/VisualInstruction.swift index ac8ef4960..518815165 100644 --- a/Sources/MapboxDirections/VisualInstruction.swift +++ b/Sources/MapboxDirections/VisualInstruction.swift @@ -18,7 +18,7 @@ open class VisualInstruction: Codable { /** Initializes a new visual instruction banner object that displays the given information. */ - public init(text: String?, maneuverType: ManeuverType, maneuverDirection: ManeuverDirection, components: [Component], degrees: CLLocationDegrees? = nil) { + public init(text: String?, maneuverType: ManeuverType?, maneuverDirection: ManeuverDirection?, components: [Component], degrees: CLLocationDegrees? = nil) { self.text = text self.maneuverType = maneuverType self.maneuverDirection = maneuverDirection From 67f1e29ec8e8f13827bc11c20d2b1ebc6a7f8369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sat, 21 Dec 2019 11:09:13 -0800 Subject: [PATCH 12/21] Added route initializers Also reshuffled RouteStep a bit. --- .../MapboxDirections/DirectionsResult.swift | 18 ++- .../MapboxDirections/MapMatching/Match.swift | 23 +++- Sources/MapboxDirections/Route.swift | 21 +++- Sources/MapboxDirections/RouteLeg.swift | 33 +++++- Sources/MapboxDirections/RouteStep.swift | 103 +++++++++++++----- 5 files changed, 159 insertions(+), 39 deletions(-) diff --git a/Sources/MapboxDirections/DirectionsResult.swift b/Sources/MapboxDirections/DirectionsResult.swift index f5db27075..a973073ff 100644 --- a/Sources/MapboxDirections/DirectionsResult.swift +++ b/Sources/MapboxDirections/DirectionsResult.swift @@ -23,6 +23,14 @@ open class DirectionsResult: Codable { // MARK: Creating a Directions Result + init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval, options: DirectionsOptions) { + self.legs = legs + self.shape = shape + self.distance = distance + self.expectedTravelTime = expectedTravelTime + _directionsOptions = options + } + public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) legs = try container.decode([RouteLeg].self, forKey: .legs) @@ -83,9 +91,9 @@ open class DirectionsResult: Codable { // MARK: Getting the Shape of the Route /** - An array of geographic coordinates defining the path of the route from start to finish. + The roads or paths taken as a contiguous polyline. - This array may be `nil` or simplified depending on the `DirectionsOptions.routeShapeResolution` property of the original `RouteOptions` or `MatchOptions` object. + The shape may be `nil` or simplified depending on the `DirectionsOptions.routeShapeResolution` property of the original `RouteOptions` or `MatchOptions` 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`. */ @@ -94,7 +102,7 @@ open class DirectionsResult: Codable { // MARK: Getting the Legs Along the Route /** - An array of `RouteLeg` objects representing the legs of the route. + The legs that are traversed in order. 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. @@ -131,7 +139,7 @@ open class DirectionsResult: Codable { 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 + open var expectedTravelTime: TimeInterval // MARK: Configuring Speech Synthesis @@ -145,7 +153,7 @@ open class DirectionsResult: Codable { // MARK: Reproducing the Route /** - `RouteOptions` used to create the directions request. + Criteria for reproducing this route. 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. */ diff --git a/Sources/MapboxDirections/MapMatching/Match.swift b/Sources/MapboxDirections/MapMatching/Match.swift index df5ff0cc4..b2052555a 100644 --- a/Sources/MapboxDirections/MapMatching/Match.swift +++ b/Sources/MapboxDirections/MapMatching/Match.swift @@ -1,6 +1,7 @@ import Foundation import CoreLocation import Polyline +import struct Turf.LineString extension CodingUserInfoKey { static let tracepoints = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.tracepoints")! @@ -8,7 +9,7 @@ extension CodingUserInfoKey { /** A `Match` object defines a single route that was created from a series of points that were matched against a road network. - Typically, you do not create instances of this class directly. Instead, you receive match objects when you pass a `MatchOptions` object into the `Directions.calculate(_:completionHandler:)` or `Directions.calculateRoutes(matching:completionHandler:)` method. + Typically, you do not create instances of this class directly. Instead, you receive match objects when you pass a `MatchOptions` object into the `Directions.calculate(_:completionHandler:)` method. */ open class Match: DirectionsResult { private enum CodingKeys: String, CodingKey { @@ -17,6 +18,26 @@ open class Match: DirectionsResult { case matchOptions } + /** + Initializes a match. + + Typically, you do not create instances of this class directly. Instead, you receive match objects when you request matches using the `Directions.calculate(_:completionHandler:)` method. + + - parameter legs: The legs that are traversed in order. + - parameter shape: The matching roads or paths as a contiguous polyline. + - parameter distance: The matched path’s cumulative distance, measured in meters. + - parameter expectedTravelTime: The route’s expected travel time, measured in seconds. + - parameter confidence: A number between 0 and 1 that indicates the Map Matching API’s confidence that the match is accurate. A higher confidence means the match is more likely to be accurate. + - parameter tracepoints: Tracepoints on the road network that match the tracepoints in `options`. + - parameter options: The criteria to match. + */ + public init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval, confidence: Float, tracepoints: [Tracepoint?], options: MatchOptions) { + matchOptions = options + self.confidence = confidence + self.tracepoints = tracepoints + super.init(legs: legs, shape: shape, distance: distance, expectedTravelTime: expectedTravelTime, options: options) + } + /** Creates a match from a decoder. diff --git a/Sources/MapboxDirections/Route.swift b/Sources/MapboxDirections/Route.swift index 305f51a7c..44bfd02d6 100644 --- a/Sources/MapboxDirections/Route.swift +++ b/Sources/MapboxDirections/Route.swift @@ -1,13 +1,32 @@ +import CoreLocation +import Turf + /** 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. + Typically, you do not create instances of this class directly. Instead, you receive route objects when you request directions using the `Directions.calculate(_:completionHandler:)` or `Directions.calculateRoutes(matching:completionHandler:)` method. However, if you use the `Directions.url(forCalculating:)` method instead, you can use `JSONDecoder` to convert the HTTP response into a `RouteResponse` or `MapMatchingResponse` object and access the `RouteResponse.routes` or `MapMatchingResponse.routes` property. */ open class Route: DirectionsResult { private enum CodingKeys: String, CodingKey { case routeOptions } + /** + Initializes a route. + + 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. + + - parameter legs: The legs that are traversed in order. + - parameter shape: The roads or paths taken as a contiguous polyline. + - parameter distance: The route’s distance, measured in meters. + - parameter expectedTravelTime: The route’s expected travel time, measured in seconds. + - parameter options: The criteria for producing a route with these parameters. + */ + public init(legs: [RouteLeg], shape: LineString?, distance: CLLocationDistance, expectedTravelTime: TimeInterval, options: RouteOptions) { + routeOptions = options + super.init(legs: legs, shape: shape, distance: distance, expectedTravelTime: expectedTravelTime, options: options) + } + /** Creates a route from a decoder. diff --git a/Sources/MapboxDirections/RouteLeg.swift b/Sources/MapboxDirections/RouteLeg.swift index 987793ae5..934c50dfc 100644 --- a/Sources/MapboxDirections/RouteLeg.swift +++ b/Sources/MapboxDirections/RouteLeg.swift @@ -29,6 +29,27 @@ open class RouteLeg: Codable { // MARK: Creating a Leg + /** + Initializes a route leg. + + - parameter steps: The steps that are traversed in order. + - parameter name: A name that describes the route leg. + - parameter expectedTravelTime: The route leg’s expected travel time, measured in seconds. + - parameter profileIdentifier: The primary mode of transportation for the route leg. + */ + public init(steps: [RouteStep], name: String, distance: CLLocationDistance, expectedTravelTime: TimeInterval, profileIdentifier: DirectionsProfileIdentifier) { + self.steps = steps + self.name = name + self.distance = distance + self.expectedTravelTime = expectedTravelTime + self.profileIdentifier = profileIdentifier + + segmentDistances = nil + expectedSegmentTravelTimes = nil + segmentSpeeds = nil + segmentCongestionLevels = nil + } + /** Creates a route leg from a decoder. @@ -114,7 +135,7 @@ open class RouteLeg: Codable { This property is set if the `RouteOptions.attributeOptions` property contains `.distance`. */ - public let segmentDistances: [CLLocationDistance]? + open var segmentDistances: [CLLocationDistance]? /** An array containing the expected travel time (measured in seconds) between each coordinate in the route leg geometry. @@ -123,7 +144,7 @@ open class RouteLeg: Codable { This property is set if the `RouteOptions.attributeOptions` property contains `.expectedTravelTime`. */ - public let expectedSegmentTravelTimes: [TimeInterval]? + open var expectedSegmentTravelTimes: [TimeInterval]? /** An array containing the expected average speed (measured in meters per second) between each coordinate in the route leg geometry. @@ -132,7 +153,7 @@ open class RouteLeg: Codable { This property is set if the `RouteOptions.attributeOptions` property contains `.speed`. */ - public let segmentSpeeds: [CLLocationSpeed]? + open var segmentSpeeds: [CLLocationSpeed]? /** An array containing the traffic congestion level along each road segment in the route leg geometry. @@ -143,7 +164,7 @@ open class RouteLeg: Codable { This property is set if the `RouteOptions.attributeOptions` property contains `.congestionLevel`. */ - public let segmentCongestionLevels: [CongestionLevel]? + open var segmentCongestionLevels: [CongestionLevel]? // MARK: Getting Statistics About the Leg @@ -170,12 +191,12 @@ open class RouteLeg: Codable { Do not assume that the user would travel along the leg at a fixed speed. For the expected travel time on each individual segment along the leg, use the `RouteStep.expectedTravelTimes` property. For more granularity, specify the `AttributeOptions.expectedTravelTime` option and use the `expectedSegmentTravelTimes` property. */ - public let expectedTravelTime: TimeInterval + open var expectedTravelTime: TimeInterval // MARK: Reproducing the Route /** - A string specifying the primary mode of transportation for the route leg. + The primary mode of transportation for the route leg. The value of this property depends on the `RouteOptions.profileIdentifier` property of the original `RouteOptions` object. This property reflects the primary mode of transportation used for the route leg. Individual steps along the route leg might use different modes of transportation as necessary. */ diff --git a/Sources/MapboxDirections/RouteStep.swift b/Sources/MapboxDirections/RouteStep.swift index cb0a78fef..26a09b05a 100644 --- a/Sources/MapboxDirections/RouteStep.swift +++ b/Sources/MapboxDirections/RouteStep.swift @@ -384,6 +384,55 @@ open class RouteStep: Codable { // MARK: Creating a Step + /** + Initializes a step. + + - parameter transportType: The mode of transportation used for the step. + - parameter maneuverLocation: The location of the maneuver at the beginning of this step. + - parameter maneuverType: The type of maneuver required for beginning this step. + - parameter maneuverDirection: Additional directional information to clarify the maneuver type. + - parameter instructions: A string with instructions explaining how to perform the step’s maneuver. + - parameter initialHeading: The user’s heading immediately before performing the maneuver. + - parameter finalHeading: The user’s heading immediately after performing the maneuver. + - parameter drivingSide: Indicates what side of a bidirectional road the driver must be driving on. Also referred to as the rule of the road. + - parameter exitCodes: Any [exit numbers](https://en.wikipedia.org/wiki/Exit_number) assigned to the highway exit at the maneuver. + - parameter exitNames: The names of the roundabout exit. + - parameter phoneticExitNames: A phonetic or phonemic transcription indicating how to pronounce the names in the `exitNames` property. + - parameter distance: The step’s distance, measured in meters. + - parameter expectedTravelTime: expectedTravelTime + - parameter names: The names of the road or path leading from this step’s maneuver to the next step’s maneuver. + - parameter phoneticNames: A phonetic or phonemic transcription indicating how to pronounce the names in the `names` property. + - parameter codes: Any route reference codes assigned to the road or path leading from this step’s maneuver to the next step’s maneuver. + - parameter destinationCodes: Any route reference codes that appear on guide signage for the road leading from this step’s maneuver to the next step’s maneuver. + - parameter destinations: Destinations, such as [control cities](https://en.wikipedia.org/wiki/Control_city), that appear on guide signage for the road leading from this step’s maneuver to the next step’s maneuver. + - parameter intersections: An array of intersections along the step. + - parameter instructionsSpokenAlongStep: Instructions about the next step’s maneuver, optimized for speech synthesis. + - parameter instructionsDisplayedAlongStep: Instructions about the next step’s maneuver, optimized for display in real time. + */ + public init(transportType: TransportType, maneuverLocation: CLLocationCoordinate2D, maneuverType: ManeuverType, maneuverDirection: ManeuverDirection? = nil, instructions: String, initialHeading: CLLocationDirection? = nil, finalHeading: CLLocationDirection? = nil, drivingSide: DrivingSide, exitCodes: [String]? = nil, exitNames: [String]? = nil, phoneticExitNames: [String]? = nil, distance: CLLocationDistance, expectedTravelTime: TimeInterval, names: [String]? = nil, phoneticNames: [String]? = nil, codes: [String]? = nil, destinationCodes: [String]? = nil, destinations: [String]? = nil, intersections: [Intersection]? = nil, instructionsSpokenAlongStep: [SpokenInstruction]? = nil, instructionsDisplayedAlongStep: [VisualInstructionBanner]? = nil) { + self.transportType = transportType + self.maneuverLocation = maneuverLocation + self.maneuverType = maneuverType + self.maneuverDirection = maneuverDirection + self.instructions = instructions + self.initialHeading = initialHeading + self.finalHeading = finalHeading + self.drivingSide = drivingSide + self.exitCodes = exitCodes + self.exitNames = exitNames + self.phoneticExitNames = phoneticExitNames + self.distance = distance + self.expectedTravelTime = expectedTravelTime + self.names = names + self.phoneticNames = phoneticNames + self.codes = codes + self.destinationCodes = destinationCodes + self.destinations = destinations + self.intersections = nil + self.instructionsSpokenAlongStep = nil + self.instructionsDisplayedAlongStep = nil + } + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(instructionsSpokenAlongStep, forKey: .instructionsSpokenAlongStep) @@ -499,8 +548,32 @@ open class RouteStep: Codable { */ public var shape: LineString? + // MARK: Getting the Mode of Transportation + + /** + The mode of transportation used for the step. + + This step may use a different mode of transportation than the overall route. + */ + public let transportType: TransportType + // MARK: Getting Details About the Maneuver + /** + The location of the maneuver at the beginning of this step. + */ + public let maneuverLocation: CLLocationCoordinate2D + + /** + The type of maneuver required for beginning this step. + */ + public let maneuverType: ManeuverType + + /** + Additional directional information to clarify the maneuver type. + */ + public let maneuverDirection: ManeuverDirection? + /** A string with instructions explaining how to perform the step’s maneuver. @@ -522,26 +595,11 @@ open class RouteStep: Codable { */ public let finalHeading: CLLocationDirection? - /** - The type of maneuver required for beginning this step. - */ - public let maneuverType: ManeuverType - - /** - Additional directional information to clarify the maneuver type. - */ - public let maneuverDirection: ManeuverDirection? - /** Indicates what side of a bidirectional road the driver must be driving on. Also referred to as the rule of the road. */ public let drivingSide: DrivingSide - /** - The location of the maneuver at the beginning of this step. - */ - public let maneuverLocation: CLLocationCoordinate2D - /** The number of exits from the previous maneuver up to and including this step’s maneuver. @@ -549,7 +607,7 @@ open class RouteStep: Codable { In some cases, the number of exits leading to a maneuver may be more useful to the user than the distance to the maneuver. */ - public let exitIndex: Int? + open var exitIndex: Int? /** Any [exit numbers](https://en.wikipedia.org/wiki/Exit_number) assigned to the highway exit at the maneuver. @@ -594,7 +652,7 @@ open class RouteStep: Codable { Do not assume that the user would travel along the step at a fixed speed. For the expected travel time on each individual segment along the leg, specify the `AttributeOptions.expectedTravelTime` option and use the `RouteLeg.expectedSegmentTravelTimes` property. */ - public let expectedTravelTime: TimeInterval + open var expectedTravelTime: TimeInterval /** The names of the road or path leading from this step’s maneuver to the next step’s maneuver. @@ -659,19 +717,12 @@ open class RouteStep: Codable { /** Instructions about the next step’s maneuver, optimized for display in real time. + As the user traverses this step, you can give them advance notice of the upcoming maneuver by displaying each item in this array in order as the user reaches the specified distances along this step. The text and images of the visual instructions refer to the details in the next step, but the distances are measured from the beginning of this step. + This property is non-`nil` if the `RouteOptions.includesVisualInstructions` option is set to `true`. For instructions designed for speech synthesis, use the `instructionsSpokenAlongStep` property. For instructions designed for display in a static list, use the `instructions` property. */ public let instructionsDisplayedAlongStep: [VisualInstructionBanner]? - - // MARK: Getting the Mode of Transportation - - /** - The mode of transportation used for the step. - - This step may use a different mode of transportation than the overall route. - */ - public let transportType: TransportType } extension RouteStep: Equatable { From c16acf2a952261634c42edfc621d30cb3d1a410e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sat, 21 Dec 2019 14:54:27 -0800 Subject: [PATCH 13/21] Test route, match creation --- MapboxDirections.xcodeproj/project.pbxproj | 16 +++++++ Sources/MapboxDirections/QuickLook.swift | 6 +-- Tests/MapboxDirectionsTests/MatchTests.swift | 48 ++++++++++++++++++- .../QuickLookTests.swift | 16 +++++++ Tests/MapboxDirectionsTests/RouteTests.swift | 42 ++++++++++++++++ 5 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 Tests/MapboxDirectionsTests/QuickLookTests.swift create mode 100644 Tests/MapboxDirectionsTests/RouteTests.swift diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index 8a0b5e22e..00b045332 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -251,6 +251,12 @@ DADD27C81E5AAE3100D31FAD /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DADD27C71E5AAE3100D31FAD /* Launch Screen.storyboard */; }; DADD27F31E5ABD4300D31FAD /* Mapbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DADD27F21E5ABD4300D31FAD /* Mapbox.framework */; }; DADD27F71E5AC8E900D31FAD /* MapboxDirections.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA6C9D881CAE442B00094FBC /* MapboxDirections.framework */; }; + DAE2DF6823AECB120065057A /* QuickLookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2DF6723AECB120065057A /* QuickLookTests.swift */; }; + DAE2DF6923AECB120065057A /* QuickLookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2DF6723AECB120065057A /* QuickLookTests.swift */; }; + DAE2DF6A23AECB120065057A /* QuickLookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2DF6723AECB120065057A /* QuickLookTests.swift */; }; + DAE2DF6C23AED2280065057A /* RouteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2DF6B23AED2280065057A /* RouteTests.swift */; }; + DAE2DF6D23AED2280065057A /* RouteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2DF6B23AED2280065057A /* RouteTests.swift */; }; + DAE2DF6E23AED2280065057A /* RouteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2DF6B23AED2280065057A /* RouteTests.swift */; }; DAE33A1B1F215DF600C06039 /* IntersectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE33A1A1F215DF600C06039 /* IntersectionTests.swift */; }; DAE33A1C1F215DF600C06039 /* IntersectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE33A1A1F215DF600C06039 /* IntersectionTests.swift */; }; DAE33A1D1F215DF600C06039 /* IntersectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE33A1A1F215DF600C06039 /* IntersectionTests.swift */; }; @@ -402,6 +408,8 @@ DADD27C31E5AAAD800D31FAD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DADD27C71E5AAE3100D31FAD /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; DADD27F21E5ABD4300D31FAD /* Mapbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Mapbox.framework; path = Carthage/Build/iOS/Mapbox.framework; sourceTree = ""; }; + DAE2DF6723AECB120065057A /* QuickLookTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookTests.swift; sourceTree = ""; }; + DAE2DF6B23AED2280065057A /* RouteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteTests.swift; sourceTree = ""; }; DAE33A1A1F215DF600C06039 /* IntersectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntersectionTests.swift; sourceTree = ""; }; DAE9E0F31EB7DE2E001E8E8B /* RouteOptionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RouteOptionsTests.swift; path = Tests/MapboxDirectionsTests/RouteOptionsTests.swift; sourceTree = SOURCE_ROOT; }; DD6254731AE70CB700017857 /* Directions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Directions.swift; sourceTree = ""; }; @@ -635,7 +643,9 @@ 3556CE9922649CF2009397B5 /* MapboxDirectionsTests-Bridging-Header.h */, C5DAACAE201AA92B001F9261 /* MatchTests.swift */, 35DBF013217E199E0009D2AE /* OfflineDirectionsTests.swift */, + DAE2DF6723AECB120065057A /* QuickLookTests.swift */, C59666382048A20E00C45CE5 /* RoutableMatchTests.swift */, + DAE2DF6B23AED2280065057A /* RouteTests.swift */, DAE9E0F31EB7DE2E001E8E8B /* RouteOptionsTests.swift */, F4D785EE1DDD82C100FF4665 /* RouteStepTests.swift */, DAABF78D2395ABA900CEEB61 /* SpokenInstructionTests.swift */, @@ -1196,6 +1206,7 @@ buildActionMask = 2147483647; files = ( DAE9E0F51EB7DE2E001E8E8B /* RouteOptionsTests.swift in Sources */, + DAE2DF6D23AED2280065057A /* RouteTests.swift in Sources */, 35DBF015217E199E0009D2AE /* OfflineDirectionsTests.swift in Sources */, C53A02291E92C27A009837BD /* AnnotationTests.swift in Sources */, DA688B3F21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, @@ -1205,6 +1216,7 @@ DAE33A1C1F215DF600C06039 /* IntersectionTests.swift in Sources */, C5DAACB0201AA92B001F9261 /* MatchTests.swift in Sources */, DA1A10CE1D00F972009F82FA /* Fixture.swift in Sources */, + DAE2DF6923AECB120065057A /* QuickLookTests.swift in Sources */, DA1A110C1D01045E009F82FA /* DirectionsTests.swift in Sources */, C5D1D7F31F6AFBD600A1C4F1 /* VisualInstructionTests.swift in Sources */, DA4F84EE21C08BFB008A0434 /* WaypointTests.swift in Sources */, @@ -1265,6 +1277,7 @@ buildActionMask = 2147483647; files = ( DAE9E0F61EB7DE2E001E8E8B /* RouteOptionsTests.swift in Sources */, + DAE2DF6E23AED2280065057A /* RouteTests.swift in Sources */, 35DBF016217E199E0009D2AE /* OfflineDirectionsTests.swift in Sources */, C53A022A1E92C27B009837BD /* AnnotationTests.swift in Sources */, DA688B4021B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, @@ -1274,6 +1287,7 @@ DAE33A1D1F215DF600C06039 /* IntersectionTests.swift in Sources */, C5DAACB1201AA92B001F9261 /* MatchTests.swift in Sources */, DA1A10F51D010251009F82FA /* Fixture.swift in Sources */, + DAE2DF6A23AECB120065057A /* QuickLookTests.swift in Sources */, DA1A110D1D01045E009F82FA /* DirectionsTests.swift in Sources */, C5D1D7F41F6AFBD600A1C4F1 /* VisualInstructionTests.swift in Sources */, DA4F84EF21C08BFB008A0434 /* WaypointTests.swift in Sources */, @@ -1378,6 +1392,7 @@ buildActionMask = 2147483647; files = ( DAE9E0F41EB7DE2E001E8E8B /* RouteOptionsTests.swift in Sources */, + DAE2DF6C23AED2280065057A /* RouteTests.swift in Sources */, 35DBF014217E199E0009D2AE /* OfflineDirectionsTests.swift in Sources */, C5247D711E818A24004B6154 /* AnnotationTests.swift in Sources */, DA688B3E21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, @@ -1387,6 +1402,7 @@ DAE33A1B1F215DF600C06039 /* IntersectionTests.swift in Sources */, C5DAACAF201AA92B001F9261 /* MatchTests.swift in Sources */, DA6C9DB21CAECA0E00094FBC /* Fixture.swift in Sources */, + DAE2DF6823AECB120065057A /* QuickLookTests.swift in Sources */, DA1A110B1D01045E009F82FA /* DirectionsTests.swift in Sources */, C52CE3931F6AF6E70069963D /* VisualInstructionTests.swift in Sources */, DA4F84ED21C08BFB008A0434 /* WaypointTests.swift in Sources */, diff --git a/Sources/MapboxDirections/QuickLook.swift b/Sources/MapboxDirections/QuickLook.swift index a8b08bd3b..83d1dd7b2 100644 --- a/Sources/MapboxDirections/QuickLook.swift +++ b/Sources/MapboxDirections/QuickLook.swift @@ -15,8 +15,8 @@ protocol CustomQuickLookConvertible { /** Returns a URL to an image representation of the given coordinates via the [Mapbox Static Images API](https://docs.mapbox.com/api/maps/#static-images). */ -func debugQuickLookURL(illustrating shape: LineString, profileIdentifier: DirectionsProfileIdentifier = .automobile) -> URL? { - guard let accessToken = defaultAccessToken else { +func debugQuickLookURL(illustrating shape: LineString, profileIdentifier: DirectionsProfileIdentifier = .automobile, accessToken: String? = defaultAccessToken) -> URL? { + guard let accessToken = accessToken else { return nil } @@ -48,5 +48,5 @@ func debugQuickLookURL(illustrating shape: LineString, profileIdentifier: Direct URLQueryItem(name: "access_token", value: accessToken), ] - return URL(string: "https://api.mapbox.com\(path)?\(components.percentEncodedQuery!)") + return URL(string: "\(defaultApiEndPointURLString ?? "https://api.mapbox.com")\(path)?\(components.percentEncodedQuery!)") } diff --git a/Tests/MapboxDirectionsTests/MatchTests.swift b/Tests/MapboxDirectionsTests/MatchTests.swift index b22be28b3..2e56a586f 100644 --- a/Tests/MapboxDirectionsTests/MatchTests.swift +++ b/Tests/MapboxDirectionsTests/MatchTests.swift @@ -1,14 +1,18 @@ import XCTest #if !SWIFT_PACKAGE import OHHTTPStubs +#endif @testable import MapboxDirections class MatchTests: XCTestCase { override func tearDown() { + #if !SWIFT_PACKAGE OHHTTPStubs.removeAllStubs() + #endif super.tearDown() } + #if !SWIFT_PACKAGE func testMatch() { let expectation = self.expectation(description: "calculating directions should return results") let locations = [CLLocationCoordinate2D(latitude: 32.712041, longitude: -117.172836), @@ -148,5 +152,47 @@ class MatchTests: XCTestCase { XCTAssertEqual(match.matchOptions, unarchivedMatch.matchOptions) XCTAssertEqual(match.tracepoints, unarchivedMatch.tracepoints) } + #endif + + func testCoding() { + // https://api.mapbox.com/matching/v5/mapbox/driving/-84.51200,39.09740;-84.51118,39.09638;-84.51021,39.09687.json?geometries=polyline&overview=false&tidy=false&access_token=… + let matchJSON: [String: Any?] = [ + "confidence": 0.00007401405321383336, + "legs": [ + [ + "summary": "", + "weight": 46.7, + "duration": 34.7, + "steps": [], + "distance": 169, + ], + [ + "summary": "", + "weight": 31, + "duration": 25.6, + "steps": [], + "distance": 128.1, + ], + ], + "weight_name": "routability", + "weight": 77.7, + "duration": 60.300000000000004, + "distance": 297.1, + ] + let matchData = try! JSONSerialization.data(withJSONObject: matchJSON, options: []) + + let options = MatchOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 39.09740, longitude: -84.51200), + CLLocationCoordinate2D(latitude: 39.09638, longitude: -84.51118), + CLLocationCoordinate2D(latitude: 39.09687, longitude: -84.51021), + ]) + options.routeShapeResolution = .none + + let decoder = JSONDecoder() + var match: Match? + XCTAssertThrowsError(match = try decoder.decode(Match.self, from: matchData)) + decoder.userInfo[.options] = options + XCTAssertNoThrow(match = try decoder.decode(Match.self, from: matchData)) + XCTAssertNotNil(match) + } } -#endif diff --git a/Tests/MapboxDirectionsTests/QuickLookTests.swift b/Tests/MapboxDirectionsTests/QuickLookTests.swift new file mode 100644 index 000000000..d52dfad40 --- /dev/null +++ b/Tests/MapboxDirectionsTests/QuickLookTests.swift @@ -0,0 +1,16 @@ +import XCTest +import Turf +@testable import MapboxDirections + +class QuickLookTests: XCTestCase { + func testQuickLookURL() { + let lineString = LineString([ + CLLocationCoordinate2D(latitude: 0, longitude: 0), + CLLocationCoordinate2D(latitude: 1, longitude: 1), + ]) + XCTAssertNil(debugQuickLookURL(illustrating: lineString)) + XCTAssertEqual(debugQuickLookURL(illustrating: lineString, accessToken: BogusToken), URL(string: "https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/path-10+3802DA-0.6(%3F%3F_ibE_ibE)/auto/680x360@2x?before_layer=building-number-label&access_token=\(BogusToken)")) + XCTAssertEqual(debugQuickLookURL(illustrating: lineString, profileIdentifier: .automobileAvoidingTraffic, accessToken: BogusToken), URL(string: "https://api.mapbox.com/styles/v1/mapbox/navigation-preview-day-v4/static/path-10+3802DA-0.6(%3F%3F_ibE_ibE)/auto/680x360@2x?before_layer=waterway-label&access_token=\(BogusToken)")) + XCTAssertEqual(debugQuickLookURL(illustrating: lineString, profileIdentifier: .cycling, accessToken: BogusToken), URL(string: "https://api.mapbox.com/styles/v1/mapbox/outdoors-v11/static/path-10+3802DA-0.6(%3F%3F_ibE_ibE)/auto/680x360@2x?before_layer=contour-label&access_token=\(BogusToken)")) + } +} diff --git a/Tests/MapboxDirectionsTests/RouteTests.swift b/Tests/MapboxDirectionsTests/RouteTests.swift new file mode 100644 index 000000000..2580f2640 --- /dev/null +++ b/Tests/MapboxDirectionsTests/RouteTests.swift @@ -0,0 +1,42 @@ +import XCTest +@testable import MapboxDirections + +class RouteTests: XCTestCase { + func testCoding() { + // https://api.mapbox.com/directions/v5/mapbox/driving-traffic/-105.08198579860195%2C39.73843005470756;-104.954255,39.662569.json?overview=false&access_token=… + let routeJSON: [String: Any?] = [ + "legs": [ + [ + "summary": "West 6th Avenue Freeway, South University Boulevard", + "weight": 1346.3, + "duration": 1083.4, + "steps": [], + "distance": 17036.8, + ], + ], + "weight_name": "routability", + "weight": 1346.3, + "duration": 1083.4, + "distance": 17036.8, + ] + let routeData = try! JSONSerialization.data(withJSONObject: routeJSON, options: []) + + let options = RouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 39.73843005470756, longitude: -105.08198579860195), + CLLocationCoordinate2D(latitude: 39.662569, longitude: -104.954255), + ], profileIdentifier: .automobileAvoidingTraffic) + options.routeShapeResolution = .none + + let decoder = JSONDecoder() + var route: Route? + XCTAssertThrowsError(route = try decoder.decode(Route.self, from: routeData)) + decoder.userInfo[.options] = options + XCTAssertNoThrow(route = try decoder.decode(Route.self, from: routeData)) + + let expectedLeg = RouteLeg(steps: [], name: "West 6th Avenue Freeway, South University Boulevard", distance: 17036.8, expectedTravelTime: 1083.4, profileIdentifier: .automobileAvoidingTraffic) + expectedLeg.source = options.waypoints[0] + expectedLeg.destination = options.waypoints[1] + let expectedRoute = Route(legs: [expectedLeg], shape: nil, distance: 17036.8, expectedTravelTime: 1083.4, options: options) + XCTAssertEqual(route, expectedRoute) + } +} From 99f6e77377f07438ed5605e54417d2062891f715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sat, 21 Dec 2019 23:46:57 -0800 Subject: [PATCH 14/21] Only encode non-nil maneuver direction --- Sources/MapboxDirections/RouteStep.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/MapboxDirections/RouteStep.swift b/Sources/MapboxDirections/RouteStep.swift index 26a09b05a..095d6bc3b 100644 --- a/Sources/MapboxDirections/RouteStep.swift +++ b/Sources/MapboxDirections/RouteStep.swift @@ -469,7 +469,7 @@ open class RouteStep: Codable { var maneuver = container.nestedContainer(keyedBy: ManeuverCodingKeys.self, forKey: .maneuver) try maneuver.encode(instructions, forKey: .instruction) try maneuver.encode(maneuverType, forKey: .type) - try maneuver.encode(maneuverDirection, forKey: .direction) + try maneuver.encodeIfPresent(maneuverDirection, forKey: .direction) try maneuver.encodeIfPresent(maneuverLocation, forKey: .location) try maneuver.encodeIfPresent(initialHeading, forKey: .initialHeading) try maneuver.encodeIfPresent(finalHeading, forKey: .finalHeading) From 222551cc6b9b1d6ae24b9c3d54a322666a937379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 22 Dec 2019 00:30:48 -0800 Subject: [PATCH 15/21] Fixed intersection round-tripping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also made approachIndex and outletIndex optional instead of setting them to −1 at the route endpoints. --- Sources/MapboxDirections/Intersection.swift | 21 ++++---- .../IntersectionTests.swift | 49 ++++++++++++++----- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/Sources/MapboxDirections/Intersection.swift b/Sources/MapboxDirections/Intersection.swift index 5ab3b5625..0ac52c381 100644 --- a/Sources/MapboxDirections/Intersection.swift +++ b/Sources/MapboxDirections/Intersection.swift @@ -54,13 +54,17 @@ public struct Intersection { /** The index of the item in the `headings` array that corresponds to the road that the containing route step uses to approach the intersection. + + This property is set to `nil` for a departure maneuver. */ - public let approachIndex: Int + public let approachIndex: Int? /** The index of the item in the `headings` array that corresponds to the road that the containing route step uses to leave the intersection. + + This property is set to `nil` for an arrival maneuver. */ - public let outletIndex: Int + public let outletIndex: Int? /** The road classes of the road that the containing step uses to leave the intersection. @@ -102,11 +106,10 @@ extension Intersection: Codable { try container.encode(location, forKey: .location) try container.encode(headings, forKey: .headings) - try container.encode(approachIndex, forKey: .approachIndex) - try container.encode(outletIndex, forKey: .outletIndex) + try container.encodeIfPresent(approachIndex, forKey: .approachIndex) + try container.encodeIfPresent(outletIndex, forKey: .outletIndex) - var outletArray: [Bool] = Array.init(repeating: false, count: (outletIndexes.max()! + 1)) - + var outletArray = headings.map { _ in false } for index in outletIndexes { outletArray[index] = true } @@ -121,7 +124,7 @@ extension Intersection: Codable { lanes![i].isValid = true } } - try container.encode(lanes, forKey: .lanes) + try container.encodeIfPresent(lanes, forKey: .lanes) if let classes = outletRoadClasses?.description.components(separatedBy: ",") { try container.encode(classes, forKey: .outletRoadClasses) @@ -145,8 +148,8 @@ extension Intersection: Codable { let outletsArray = try container.decode([Bool].self, forKey: .outletIndexes) outletIndexes = outletsArray.indices { $0 } - outletIndex = try container.decodeIfPresent(Int.self, forKey: .outletIndex) ?? -1 - approachIndex = try container.decodeIfPresent(Int.self, forKey: .approachIndex) ?? -1 + outletIndex = try container.decodeIfPresent(Int.self, forKey: .outletIndex) + approachIndex = try container.decodeIfPresent(Int.self, forKey: .approachIndex) } } diff --git a/Tests/MapboxDirectionsTests/IntersectionTests.swift b/Tests/MapboxDirectionsTests/IntersectionTests.swift index 694f470a2..c3134cec2 100644 --- a/Tests/MapboxDirectionsTests/IntersectionTests.swift +++ b/Tests/MapboxDirectionsTests/IntersectionTests.swift @@ -5,32 +5,47 @@ class IntersectionTests: XCTestCase { func testCoding() { let intersectionsJSON: [[String: Any?]] = [ [ - "location": [13.426579, 52.508068], + "out": 0, "in": -1, - "classes": ["toll", "restricted"], - "bearings": [80], "entry": [true], - "out": 0, - "lanes": nil, + "bearings": [80], + "location": [13.426579, 52.508068], + "classes": ["toll", "restricted"], ], [ - "location": [13.426688, 52.508022], + "out": 1, "in": 2, - "bearings": [30, 120, 300], "entry": [false, true, true], - "out": 1, - "lanes": nil, + "bearings": [30, 120, 300], + "location": [13.426688, 52.508022], + ], + [ + "lanes": [ + [ + "valid": true, + "indications": ["straight"], + ], + [ + "valid": true, + "indications": ["right", "straight"], + ], + ], + "out": 0, + "in": 2, + "entry": [true, true, false], + "bearings": [45, 135, 255], + "location": [-84.503956, 39.102483], ], ] let intersectionsData = try! JSONSerialization.data(withJSONObject: intersectionsJSON, options: []) var intersections: [Intersection]? XCTAssertNoThrow(intersections = try JSONDecoder().decode([Intersection].self, from: intersectionsData)) - XCTAssertEqual(intersections?.count, 2) + XCTAssertEqual(intersections?.count, 3) if let intersection = intersections?.first { - XCTAssert(intersection.outletRoadClasses == [.toll, .restricted]) - XCTAssert(intersection.headings == [80.0]) - XCTAssert(intersection.location == CLLocationCoordinate2D(latitude: 52.508068, longitude: 13.426579)) + XCTAssertEqual(intersection.outletRoadClasses, [.toll, .restricted]) + XCTAssertEqual(intersection.headings, [80.0]) + XCTAssertEqual(intersection.location, CLLocationCoordinate2D(latitude: 52.508068, longitude: 13.426579)) } intersections = [ @@ -49,6 +64,14 @@ class IntersectionTests: XCTestCase { outletIndexes: IndexSet([1, 2]), approachLanes: nil, usableApproachLanes: nil, + outletRoadClasses: nil), + Intersection(location: CLLocationCoordinate2D(latitude: 39.102483, longitude: -84.503956), + headings: [45, 135, 255], + approachIndex: 2, + outletIndex: 0, + outletIndexes: IndexSet([0, 1]), + approachLanes: [.straightAhead, [.straightAhead, .right]], + usableApproachLanes: IndexSet([0, 1]), outletRoadClasses: nil) ] From 6d6768793dbb1893d78023b284fd8afe637b43df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 22 Dec 2019 12:42:02 -0800 Subject: [PATCH 16/21] Fixed route round-tripping --- .../MapboxDirections/DirectionsOptions.swift | 7 ++-- .../MapboxDirections/DirectionsResult.swift | 12 +++---- Sources/MapboxDirections/RouteLeg.swift | 12 ++++--- Tests/MapboxDirectionsTests/RouteTests.swift | 35 +++++++++++++++++++ 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/Sources/MapboxDirections/DirectionsOptions.swift b/Sources/MapboxDirections/DirectionsOptions.swift index b7c3e822c..204a8bfc4 100644 --- a/Sources/MapboxDirections/DirectionsOptions.swift +++ b/Sources/MapboxDirections/DirectionsOptions.swift @@ -145,12 +145,12 @@ open class DirectionsOptions: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(waypoints, forKey: .waypoints) - try container.encode(profileIdentifier.rawValue, forKey: .profileIdentifier) + try container.encode(profileIdentifier, forKey: .profileIdentifier) try container.encode(includesSteps, forKey: .includesSteps) try container.encode(shapeFormat, forKey: .shapeFormat) try container.encode(routeShapeResolution, forKey: .routeShapeResolution) try container.encode(attributeOptions, forKey: .attributeOptions) - try container.encode(locale, forKey: .locale) + try container.encode(locale.identifier, forKey: .locale) try container.encode(includesSpokenInstructions, forKey: .includesSpokenInstructions) try container.encode(distanceMeasurementSystem, forKey: .distanceMeasurementSystem) try container.encode(includesVisualInstructions, forKey: .includesVisualInstructions) @@ -168,7 +168,8 @@ open class DirectionsOptions: Codable { shapeFormat = try container.decode(RouteShapeFormat.self, forKey: .shapeFormat) routeShapeResolution = try container.decode(RouteShapeResolution.self, forKey: .routeShapeResolution) attributeOptions = try container.decode(AttributeOptions.self, forKey: .attributeOptions) - locale = try container.decode(Locale.self, forKey: .locale) + let identifier = try container.decode(String.self, forKey: .locale) + locale = Locale(identifier: identifier) includesSpokenInstructions = try container.decode(Bool.self, forKey: .includesSpokenInstructions) distanceMeasurementSystem = try container.decode(MeasurementSystem.self, forKey: .distanceMeasurementSystem) includesVisualInstructions = try container.decode(Bool.self, forKey: .includesVisualInstructions) diff --git a/Sources/MapboxDirections/DirectionsResult.swift b/Sources/MapboxDirections/DirectionsResult.swift index a973073ff..dc614272c 100644 --- a/Sources/MapboxDirections/DirectionsResult.swift +++ b/Sources/MapboxDirections/DirectionsResult.swift @@ -62,14 +62,10 @@ open class DirectionsResult: Codable { apiEndpoint = try container.decodeIfPresent(URL.self, forKey: .apiEndpoint) routeIdentifier = try container.decodeIfPresent(String.self, forKey: .routeIdentifier) - do { - speechLocale = try container.decodeIfPresent(Locale.self, forKey: .speechLocale) - } catch let DecodingError.typeMismatch(mismatchedType, context) { - guard mismatchedType == [String: Any].self else { - throw DecodingError.typeMismatch(mismatchedType, context) - } - let identifier = try container.decode(String.self, forKey: .speechLocale) + if let identifier = try container.decodeIfPresent(String.self, forKey: .speechLocale) { speechLocale = Locale(identifier: identifier) + } else { + speechLocale = nil } } @@ -85,7 +81,7 @@ open class DirectionsResult: Codable { try container.encodeIfPresent(accessToken, forKey: .accessToken) try container.encodeIfPresent(apiEndpoint, forKey: .apiEndpoint) try container.encodeIfPresent(routeIdentifier, forKey: .routeIdentifier) - try container.encodeIfPresent(speechLocale, forKey: .speechLocale) + try container.encodeIfPresent(speechLocale?.identifier, forKey: .speechLocale) } // MARK: Getting the Shape of the Route diff --git a/Sources/MapboxDirections/RouteLeg.swift b/Sources/MapboxDirections/RouteLeg.swift index 934c50dfc..b2169de54 100644 --- a/Sources/MapboxDirections/RouteLeg.swift +++ b/Sources/MapboxDirections/RouteLeg.swift @@ -90,11 +90,13 @@ open class RouteLeg: Codable { try container.encode(expectedTravelTime, forKey: .expectedTravelTime) try container.encode(profileIdentifier, forKey: .profileIdentifier) - var annotation = container.nestedContainer(keyedBy: AnnotationCodingKeys.self, forKey: .annotation) - try annotation.encode(segmentDistances, forKey: .segmentDistances) - try annotation.encode(expectedSegmentTravelTimes, forKey: .expectedSegmentTravelTimes) - try annotation.encode(segmentSpeeds, forKey: .segmentSpeeds) - try annotation.encode(segmentCongestionLevels, forKey: .segmentCongestionLevels) + if segmentDistances != nil || expectedSegmentTravelTimes != nil || segmentSpeeds != nil || segmentCongestionLevels != nil { + var annotationContainer = container.nestedContainer(keyedBy: AnnotationCodingKeys.self, forKey: .annotation) + try annotationContainer.encodeIfPresent(segmentDistances, forKey: .segmentDistances) + try annotationContainer.encodeIfPresent(expectedSegmentTravelTimes, forKey: .expectedSegmentTravelTimes) + try annotationContainer.encodeIfPresent(segmentSpeeds, forKey: .segmentSpeeds) + try annotationContainer.encodeIfPresent(segmentCongestionLevels, forKey: .segmentCongestionLevels) + } } // MARK: Getting the Endpoints of the Leg diff --git a/Tests/MapboxDirectionsTests/RouteTests.swift b/Tests/MapboxDirectionsTests/RouteTests.swift index 2580f2640..9f16374c5 100644 --- a/Tests/MapboxDirectionsTests/RouteTests.swift +++ b/Tests/MapboxDirectionsTests/RouteTests.swift @@ -38,5 +38,40 @@ class RouteTests: XCTestCase { expectedLeg.destination = options.waypoints[1] let expectedRoute = Route(legs: [expectedLeg], shape: nil, distance: 17036.8, expectedTravelTime: 1083.4, options: options) XCTAssertEqual(route, expectedRoute) + + if let route = route { + let encoder = JSONEncoder() + encoder.userInfo[.options] = options + var encodedRouteData: Data? + XCTAssertNoThrow(encodedRouteData = try encoder.encode(route)) + XCTAssertNotNil(encodedRouteData) + + if let encodedRouteData = encodedRouteData { + var encodedRouteJSON: [String: Any?]? + XCTAssertNoThrow(encodedRouteJSON = try JSONSerialization.jsonObject(with: encodedRouteData, options: []) as? [String: Any?]) + XCTAssertNotNil(encodedRouteJSON) + + // Remove keys not found in the original API response. + encodedRouteJSON?.removeValue(forKey: "source") + encodedRouteJSON?.removeValue(forKey: "destination") + encodedRouteJSON?.removeValue(forKey: "profileIdentifier") + if var encodedLegJSON = encodedRouteJSON?["legs"] as? [[String: Any?]] { + encodedLegJSON[0].removeValue(forKey: "source") + encodedLegJSON[0].removeValue(forKey: "destination") + encodedLegJSON[0].removeValue(forKey: "profileIdentifier") + encodedRouteJSON?["legs"] = encodedLegJSON + } + + // https://github.com/mapbox/MapboxDirections.swift/issues/125 + var referenceRouteJSON = routeJSON + referenceRouteJSON.removeValue(forKey: "weight") + referenceRouteJSON.removeValue(forKey: "weight_name") + var referenceLegJSON = referenceRouteJSON["legs"] as! [[String: Any?]] + referenceLegJSON[0].removeValue(forKey: "weight") + referenceRouteJSON["legs"] = referenceLegJSON + + XCTAssert(JSONSerialization.objectsAreEqual(referenceRouteJSON, encodedRouteJSON, approximate: true)) + } + } } } From d9089286ba52864131edd2c9f118f5830a3994e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 22 Dec 2019 13:54:37 -0800 Subject: [PATCH 17/21] Fixed round-tripping of option sets Splitting an empty string results in an array containing an empty string. --- Sources/MapboxDirections/AttributeOptions.swift | 2 +- Sources/MapboxDirections/Intersection.swift | 2 +- Sources/MapboxDirections/RoadClasses.swift | 10 +++------- Sources/MapboxDirections/RouteOptions.swift | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Sources/MapboxDirections/AttributeOptions.swift b/Sources/MapboxDirections/AttributeOptions.swift index 0406ec732..1504e663f 100644 --- a/Sources/MapboxDirections/AttributeOptions.swift +++ b/Sources/MapboxDirections/AttributeOptions.swift @@ -87,7 +87,7 @@ public struct AttributeOptions: OptionSet, CustomStringConvertible { extension AttributeOptions: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - try container.encode(description.components(separatedBy: ",")) + try container.encode(description.components(separatedBy: ",").filter { !$0.isEmpty }) } public init(from decoder: Decoder) throws { diff --git a/Sources/MapboxDirections/Intersection.swift b/Sources/MapboxDirections/Intersection.swift index 0ac52c381..705b2751c 100644 --- a/Sources/MapboxDirections/Intersection.swift +++ b/Sources/MapboxDirections/Intersection.swift @@ -126,7 +126,7 @@ extension Intersection: Codable { } try container.encodeIfPresent(lanes, forKey: .lanes) - if let classes = outletRoadClasses?.description.components(separatedBy: ",") { + if let classes = outletRoadClasses?.description.components(separatedBy: ",").filter({ !$0.isEmpty }) { try container.encode(classes, forKey: .outletRoadClasses) } } diff --git a/Sources/MapboxDirections/RoadClasses.swift b/Sources/MapboxDirections/RoadClasses.swift index e2cfcbfc1..8cc0fd519 100644 --- a/Sources/MapboxDirections/RoadClasses.swift +++ b/Sources/MapboxDirections/RoadClasses.swift @@ -60,8 +60,8 @@ public struct RoadClasses: OptionSet, CustomStringConvertible { roadClasses.insert(.ferry) case "tunnel": roadClasses.insert(.tunnel) - case "none": - break + case "": + continue default: return nil } @@ -70,10 +70,6 @@ public struct RoadClasses: OptionSet, CustomStringConvertible { } public var description: String { - if isEmpty { - return "" - } - var descriptions: [String] = [] if contains(.toll) { descriptions.append("toll") @@ -97,7 +93,7 @@ public struct RoadClasses: OptionSet, CustomStringConvertible { extension RoadClasses: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - try container.encode(description.components(separatedBy: ",")) + try container.encode(description.components(separatedBy: ",").filter { !$0.isEmpty }) } public init(from decoder: Decoder) throws { diff --git a/Sources/MapboxDirections/RouteOptions.swift b/Sources/MapboxDirections/RouteOptions.swift index 9ad37999a..3563c3c21 100644 --- a/Sources/MapboxDirections/RouteOptions.swift +++ b/Sources/MapboxDirections/RouteOptions.swift @@ -176,7 +176,7 @@ open class RouteOptions: DirectionsOptions { } if !roadClassesToAvoid.isEmpty { - let allRoadClasses = roadClassesToAvoid.description.components(separatedBy: ",") + let allRoadClasses = roadClassesToAvoid.description.components(separatedBy: ",").filter { !$0.isEmpty } precondition(allRoadClasses.count < 2, "You can only avoid one road class at a time.") if let firstRoadClass = allRoadClasses.first { params.append(URLQueryItem(name: "exclude", value: firstRoadClass)) From dc188deffdd0ca5241d9da74b2a74992798ab518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 22 Dec 2019 16:37:25 -0800 Subject: [PATCH 18/21] Test polyline 6 encoding --- .../RouteStepTests.swift | 54 +++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/Tests/MapboxDirectionsTests/RouteStepTests.swift b/Tests/MapboxDirectionsTests/RouteStepTests.swift index aa94e089a..d5e3221c1 100644 --- a/Tests/MapboxDirectionsTests/RouteStepTests.swift +++ b/Tests/MapboxDirectionsTests/RouteStepTests.swift @@ -146,7 +146,13 @@ class RouteStepTests: XCTestCase { } func testCoding() { - let stepJSON = [ + let options = RouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 52.50881, longitude: 13.42467), + CLLocationCoordinate2D(latitude: 52.506794, longitude: 13.42326), + ]) + options.shapeFormat = .polyline + + var stepJSON = [ "intersections": [ [ "out": 1, @@ -177,21 +183,47 @@ class RouteStepTests: XCTestCase { "name": "Adalbertstraße", "mode": "driving", ] as [String : Any?] - let stepData = try! JSONSerialization.data(withJSONObject: stepJSON, options: []) + var stepData = try! JSONSerialization.data(withJSONObject: stepJSON, options: []) + + let decoder = JSONDecoder() + decoder.userInfo[.options] = options var step: RouteStep? - XCTAssertNoThrow(step = try JSONDecoder().decode(RouteStep.self, from: stepData)) + XCTAssertNoThrow(step = try decoder.decode(RouteStep.self, from: stepData)) + XCTAssertNotNil(step) + + let encoder = JSONEncoder() + encoder.userInfo[.options] = options + var encodedStepData: Data? + if let step = step { + XCTAssertNoThrow(encodedStepData = try encoder.encode(step)) + XCTAssertNotNil(encodedStepData) + + if let encodedStepData = encodedStepData { + var encodedStepJSON: Any? + XCTAssertNoThrow(encodedStepJSON = try JSONSerialization.jsonObject(with: encodedStepData, options: [])) + XCTAssertNotNil(encodedStepJSON) + + // https://github.com/mapbox/MapboxDirections.swift/issues/125 + var referenceStepJSON = stepJSON + referenceStepJSON.removeValue(forKey: "weight") + + XCTAssert(JSONSerialization.objectsAreEqual(referenceStepJSON, encodedStepJSON, approximate: true)) + } + } + + options.shapeFormat = .polyline6 + stepJSON["geometry"] = "sg{ccB{`krXzxBbwAvB?" + stepData = try! JSONSerialization.data(withJSONObject: stepJSON, options: []) + XCTAssertNoThrow(step = try decoder.decode(RouteStep.self, from: stepData)) XCTAssertNotNil(step) if let step = step { - let options = RouteOptions(coordinates: [ - CLLocationCoordinate2D(latitude: 0, longitude: 0), - CLLocationCoordinate2D(latitude: 1, longitude: 1), - ]) - options.shapeFormat = .polyline + XCTAssertEqual(step.shape?.coordinates.count, 3) + XCTAssertEqual(step.shape?.coordinates.first?.latitude ?? 0, 52.50881, accuracy: 1e-5) + XCTAssertEqual(step.shape?.coordinates.first?.longitude ?? 0, 13.42467, accuracy: 1e-5) + XCTAssertEqual(step.shape?.coordinates.last?.latitude ?? 0, 52.506794, accuracy: 1e-5) + XCTAssertEqual(step.shape?.coordinates.last?.longitude ?? 0, 13.42326, accuracy: 1e-5) - let encoder = JSONEncoder() - encoder.userInfo[.options] = options - var encodedStepData: Data? XCTAssertNoThrow(encodedStepData = try encoder.encode(step)) XCTAssertNotNil(encodedStepData) From 1fd6d32cfe3f2485e72bdba05d8ca4f26180d7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 24 Dec 2019 12:01:27 -0800 Subject: [PATCH 19/21] Fixed lane component round-tripping --- .../VisualInstructionComponent.swift | 2 +- .../VisualInstructionComponentTests.swift | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Sources/MapboxDirections/VisualInstructionComponent.swift b/Sources/MapboxDirections/VisualInstructionComponent.swift index 9a72e908b..de01b268a 100644 --- a/Sources/MapboxDirections/VisualInstructionComponent.swift +++ b/Sources/MapboxDirections/VisualInstructionComponent.swift @@ -229,7 +229,7 @@ extension VisualInstruction.Component: Codable { textRepresentation = text case .lane(let indications, let isUsable): try container.encode(Kind.lane, forKey: .kind) - textRepresentation = nil + textRepresentation = .init(text: "", abbreviation: nil, abbreviationPriority: nil) try container.encode(indications, forKey: .directions) try container.encode(isUsable, forKey: .isActive) } diff --git a/Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift b/Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift index 765e0aa77..f91f900c7 100644 --- a/Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift +++ b/Tests/MapboxDirectionsTests/VisualInstructionComponentTests.swift @@ -63,6 +63,41 @@ class VisualInstructionComponentTests: XCTestCase { } } + func testLaneComponent() { + let componentJSON: [String: Any?] = [ + "text": "", + "type": "lane", + "active": true, + "directions": ["right", "straight"], + ] + let componentData = try! JSONSerialization.data(withJSONObject: componentJSON, options: []) + var component: VisualInstruction.Component? + XCTAssertNoThrow(component = try JSONDecoder().decode(VisualInstruction.Component.self, from: componentData)) + XCTAssertNotNil(component) + if let component = component { + if case let .lane(indications, isUsable) = component { + XCTAssertEqual(indications, [.straightAhead, .right]) + XCTAssertTrue(isUsable) + } else { + XCTFail("Lane component should not be decoded as any other kind of component.") + } + } + + component = .lane(indications: [.straightAhead, .right], isUsable: true) + let encoder = JSONEncoder() + var encodedData: Data? + XCTAssertNoThrow(encodedData = try encoder.encode(component)) + XCTAssertNotNil(encodedData) + + if let encodedData = encodedData { + var encodedComponentJSON: [String: Any?]? + XCTAssertNoThrow(encodedComponentJSON = try JSONSerialization.jsonObject(with: encodedData, options: []) as? [String: Any?]) + XCTAssertNotNil(encodedComponentJSON) + + XCTAssert(JSONSerialization.objectsAreEqual(componentJSON, encodedComponentJSON, approximate: false)) + } + } + func testUnrecognizedComponent() { let componentJSON = [ "type": "emoji", From e556d6e9f650f58b3e8a4c262c2a23eb54e8c3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 24 Dec 2019 16:02:58 -0800 Subject: [PATCH 20/21] Fixed map matching response decoding --- Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index e44c844c6..6bb2c6c8b 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -19,7 +19,8 @@ extension MapMatchingResponse: Decodable { routes = try container.decodeIfPresent([Route].self, forKey: .matches) // Decode waypoints from the response and update their names according to the waypoints from DirectionsOptions.waypoints. - let decodedWaypoints = try container.decode([Waypoint].self, forKey: .tracepoints) + // Map Matching API responses can contain null tracepoints. Null tracepoints can’t correspond to waypoints, so they’re irrelevant to the decoded structure. + let decodedWaypoints = try container.decode([Waypoint?].self, forKey: .tracepoints).compactMap { $0 } if let options = decoder.userInfo[.options] as? DirectionsOptions { // The response lists the same number of tracepoints as the waypoints in the request, whether or not a given waypoint is leg-separating. waypoints = zip(decodedWaypoints, options.waypoints).map { (pair) -> Waypoint in From 472cbced199ef2d9c5c2fa49a21e8f6b249e9bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Wed, 25 Dec 2019 00:47:50 -0800 Subject: [PATCH 21/21] Updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80e9e7646..fcb5b81b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ * Removed `TransportType.none`, `ManeuverType.none`, and `ManeuverDirection.none`. Unrecognized `TransportType` and `ManeuverDirection` values now raise decoding errors. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) * `RouteStep.maneuverType` is now optional. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) * Renamed the `Tracepoint.alternateCount` property to `Tracepoint.countOfAlternatives`. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) +* The `Intersection.approachIndex` and `Intersection.outletIndex` properties are now optional, not −1, in the case of a departure or arrival maneuver. ([#393](https://github.com/mapbox/MapboxDirections.swift/pull/393)) +* Added initializers for `Route`, `Match`, `RouteLeg`, and `RouteStep`. ([#393](https://github.com/mapbox/MapboxDirections.swift/pull/393)) +* Various properties of `Route`, `RouteLeg`, and `RouteStep` are now writable. ([#393](https://github.com/mapbox/MapboxDirections.swift/pull/393)) ## v0.30.0