diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a588f66..5cb7a5328 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ * Swift is now required to directly use public types and methods defined by this library. If your application is written in Objective-C or Cocoa-AppleScript, you need to implement your own wrapper in Swift that bridges to Objective-C. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) * This library now depends on [Turf](https://github.com/mapbox/turf-swift/). ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) +### Locations and geometry + +* Replaced the `Waypoint` and `Tracepoint` classes with separate classes for requests and responses. Now you create a `RouteOptions` or `MatchOptions` using a series of `DirectionsOptions.Waypoint` instances (also known as `RouteOptions.Waypoint` or `MatchOptions.Waypoint`). The `RouteCompletionHandler` and `MatchCompletionHandler` closures and the response types represent waypoints as `DirectionsResult.Waypoint` (also known as `RouteOptions.Waypoint`) and tracepoints as `Match.Tracepoint`. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) +* Replaced the `Route.coordinates` property with `Route.shape` and the `RouteStep.coordinates` property with `RouteStep.shape`. The `Route.coordinateCount` and `RouteStep.coordinateCount` properties have been removed, but you can use the `LineString.coordinates` property to get the array of `CLLocationCoordinate2D`s. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) +* The ` +* Renamed the `Tracepoint.alternateCount` property to `Tracepoint.countOfAlternatives`. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) + ### Error handling * The `RouteCompletionHandler` and `MatchCompletionHandler` closures’ `error` argument is now a `DirectionsError` instead of an `NSError`. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) @@ -23,7 +30,6 @@ * Removed support for [Mapbox Directions API v4](https://docs.mapbox.com/api/legacy/directions-v4/). ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) * Replaced the `MBDefaultWalkingSpeed`, `MBMinimumWalkingSpeed`, and `MBMaximumWalkingSpeed` constants with `CLLocationSpeed.normalWalking`, `CLLocationSpeed.minimumWalking`, and `CLLocationSpeed.maximumWalking`, respectively. -* Replaced the `Route.coordinates` property with `Route.shape` and the `RouteStep.coordinates` property with `RouteStep.shape`. The `Route.coordinateCount` and `RouteStep.coordinateCount` properties have been removed, but you can use the `LineString.coordinates` property to get the array of `CLLocationCoordinate2D`s. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382)) ## v0.30.0 diff --git a/README.md b/README.md index 9bc0c9bec..a368091f9 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ With the directions object in hand, construct a RouteOptions object and pass it // main.swift let waypoints = [ - Waypoint(coordinate: CLLocationCoordinate2D(latitude: 38.9131752, longitude: -77.0324047), name: "Mapbox"), - Waypoint(coordinate: CLLocationCoordinate2D(latitude: 38.8977, longitude: -77.0365), name: "White House"), + RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 38.9131752, longitude: -77.0324047), name: "Mapbox"), + RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 38.8977, longitude: -77.0365), name: "White House"), ] let options = RouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) options.includesSteps = true diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index d79a1e2b4..45204d6db 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -77,7 +77,7 @@ open class Directions: NSObject { If the request was canceled or there was an error obtaining the routes, this argument is `nil`. This is not to be confused with the situation in which no results were found, in which case the array is present but empty. - parameter error: The error that occurred, or `nil` if the placemarks were obtained successfully. */ - public typealias RouteCompletionHandler = (_ waypoints: [Waypoint]?, _ routes: [Route]?, _ error: DirectionsError?) -> Void + public typealias RouteCompletionHandler = (_ waypoints: [Route.Waypoint]?, _ routes: [Route]?, _ error: DirectionsError?) -> Void /** A closure (block) to be called when a map matching request is complete. @@ -306,7 +306,7 @@ open class Directions: NSObject { let decoder = DirectionsDecoder(options: options) 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) + let apiError = Directions.informativeError(code: result.code, message: nil, response: response, underlyingError: possibleError) completionHandler(nil, nil, apiError) return } @@ -464,9 +464,9 @@ public class DirectionsDecoder: JSONDecoder { } } - var tracepoints: [Tracepoint?]? { + var tracepoints: [Match.Tracepoint?]? { get { - return userInfo[.tracepoints] as? [Tracepoint?] + return userInfo[.tracepoints] as? [Match.Tracepoint?] } set { userInfo[.tracepoints] = newValue } diff --git a/Sources/MapboxDirections/DirectionsResult.swift b/Sources/MapboxDirections/DirectionsResult.swift index b78b27ecd..dda6973ae 100644 --- a/Sources/MapboxDirections/DirectionsResult.swift +++ b/Sources/MapboxDirections/DirectionsResult.swift @@ -41,10 +41,9 @@ open class DirectionsResult: Codable { } // Associate each leg JSON with a source and destination. The sequence of destinations is offset by one from the sequence of sources. - - let waypoints = directionsOptions.legSeparators //we don't want to name via points + // Create waypoints from waypoints in the options. Skip waypoints that don’t separate legs. + let waypoints = directionsOptions.legSeparators.map { Waypoint(coordinate: $0.coordinate, correction: 0, name: $0.name) } let legInfo = zip(zip(waypoints.prefix(upTo: waypoints.endIndex - 1), waypoints.suffix(from: 1)), legs) - for (endpoints, leg) in legInfo { (leg.source, leg.destination) = endpoints } diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index 4520bb18d..0c6e4aaed 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -3,7 +3,7 @@ import Foundation class MapMatchingResponse: Decodable { var code: String var routes : [Route]? - var waypoints: [Waypoint] + var waypoints: [Match.Waypoint] private enum CodingKeys: String, CodingKey { case code @@ -17,17 +17,17 @@ class 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) + let decodedWaypoints = try container.decode([Match.Waypoint].self, forKey: .tracepoints) 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 + waypoints = zip(decodedWaypoints, options.waypoints).map { (pair) -> Match.Waypoint in let (decodedWaypoint, waypointInOptions) = pair - let waypoint = Waypoint(coordinate: decodedWaypoint.coordinate, coordinateAccuracy: waypointInOptions.coordinateAccuracy, name: waypointInOptions.name?.nonEmptyString ?? decodedWaypoint.name) - waypoint.separatesLegs = waypointInOptions.separatesLegs + var waypoint = decodedWaypoint + if waypointInOptions.separatesLegs, let name = waypointInOptions.name?.nonEmptyString { + waypoint.name = name + } return waypoint } - waypoints.first?.separatesLegs = true - waypoints.last?.separatesLegs = true } else { waypoints = decodedWaypoints } @@ -36,7 +36,8 @@ class MapMatchingResponse: Decodable { // Postprocess each route. for route in routes { // Imbue each route’s legs with the leg-separating waypoints refined above. - route.legSeparators = waypoints.filter { $0.separatesLegs } + // TODO: Filter these waypoints by whether they separate legs, based on the options, if given. + route.legSeparators = waypoints } self.routes = routes } else { diff --git a/Sources/MapboxDirections/MapMatching/MatchResponse.swift b/Sources/MapboxDirections/MapMatching/MatchResponse.swift index 328186d59..a13b00852 100644 --- a/Sources/MapboxDirections/MapMatching/MatchResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MatchResponse.swift @@ -4,7 +4,7 @@ class MatchResponse: Codable { var code: String var message: String? var matches : [Match]? - var tracepoints: [Tracepoint?]? + var tracepoints: [Match.Tracepoint?]? private enum CodingKeys: String, CodingKey { case code @@ -17,12 +17,14 @@ class MatchResponse: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) code = try container.decode(String.self, forKey: .code) message = try container.decodeIfPresent(String.self, forKey: .message) - tracepoints = try container.decodeIfPresent([Tracepoint?].self, forKey: .tracepoints) + tracepoints = try container.decodeIfPresent([Match.Tracepoint?].self, forKey: .tracepoints) matches = try container.decodeIfPresent([Match].self, forKey: .matches) - if let points = self.tracepoints { - matches?.forEach { - $0.tracepoints = points + if let tracepoints = self.tracepoints, let matches = matches { + for match in matches { + // TODO: Filter on matchings_index. + // TODO: Push tracepoints down to individual legs to reflect waypoint_index. + match.tracepoints = tracepoints } } } diff --git a/Sources/MapboxDirections/MapMatching/Tracepoint.swift b/Sources/MapboxDirections/MapMatching/Tracepoint.swift index 3295c51bc..62ae861e1 100644 --- a/Sources/MapboxDirections/MapMatching/Tracepoint.swift +++ b/Sources/MapboxDirections/MapMatching/Tracepoint.swift @@ -1,40 +1,44 @@ import Foundation import CoreLocation -/** - A `Tracepoint` represents a location matched to the road network. - */ -public class Tracepoint: Waypoint { +public extension Match { /** - Number of probable alternative matchings for this tracepoint. A value of zero indicates that this point was matched unambiguously. + A tracepoint represents a location matched to the road network. */ - public let alternateCount: Int - - private enum CodingKeys: String, CodingKey { - case alternateCount = "alternatives_count" - } - - init(coordinate: CLLocationCoordinate2D, alternateCount: Int?, name: String?) { - self.alternateCount = alternateCount ?? NSNotFound - super.init(coordinate: coordinate, name: name) - } - - required public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - alternateCount = try container.decode(Int.self, forKey: .alternateCount) - try super.init(from: decoder) + struct Tracepoint: Matchpoint, Equatable { + // MARK: Positioning the Waypoint + + /** + The geographic coordinate of the waypoint, snapped to the road network. + */ + public var coordinate: CLLocationCoordinate2D + + /** + The straight-line distance from this waypoint to the corresponding waypoint in the `RouteOptions` or `MatchOptions` object. + + The requested waypoint is snapped to the road network. This property contains the straight-line distance from the original requested waypoint’s `DirectionsOptions.Waypoint.coordinate` property to the `coordinate` property. + */ + public var correction: CLLocationDistance + + // MARK: Determining the Degree of Confidence + + /** + Number of probable alternative matchings for this tracepoint. A value of zero indicates that this point was matched unambiguously. + */ + public var countOfAlternatives: Int } - - public override func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(alternateCount, forKey: .alternateCount) - try super.encode(to: encoder) +} + +extension Match.Tracepoint: Codable { + private enum CodingKeys: String, CodingKey { + case coordinate = "location" + case correction = "distance" + case countOfAlternatives = "alternatives_count" } } -extension Tracepoint { //Equatable - public static func ==(lhs: Tracepoint, rhs: Tracepoint) -> Bool { - let superEquals = (lhs as Waypoint == rhs as Waypoint) - return superEquals && lhs.alternateCount == rhs.alternateCount +extension Match.Tracepoint: CustomStringConvertible { + public var description: String { + return "" } } diff --git a/Sources/MapboxDirections/RouteLeg.swift b/Sources/MapboxDirections/RouteLeg.swift index 761bccda3..35e29d4ea 100644 --- a/Sources/MapboxDirections/RouteLeg.swift +++ b/Sources/MapboxDirections/RouteLeg.swift @@ -36,8 +36,9 @@ open class RouteLeg: Codable { */ public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - source = try container.decodeIfPresent(Waypoint.self, forKey: .source) - destination = try container.decodeIfPresent(Waypoint.self, forKey: .destination) + + source = try container.decodeIfPresent(Route.Waypoint.self, forKey: .source) + destination = try container.decodeIfPresent(Route.Waypoint.self, forKey: .destination) steps = try container.decode([RouteStep].self, forKey: .steps) name = try container.decode(String.self, forKey: .name) distance = try container.decode(CLLocationDistance.self, forKey: .distance) @@ -84,7 +85,7 @@ open class RouteLeg: Codable { This property is set to `nil` if the leg was decoded from a JSON RouteLeg object. */ - public var source: Waypoint? + public var source: Route.Waypoint? /** The endpoint of the route leg. @@ -93,7 +94,7 @@ open class RouteLeg: Codable { This property is set to `nil` if the leg was decoded from a JSON RouteLeg object. */ - public var destination: Waypoint? + public var destination: Route.Waypoint? // MARK: Getting the Steps Along the Leg diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index 5da674b5d..9ee991bff 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -6,7 +6,7 @@ struct RouteResponse { var error: String? let uuid: String? let routes: [Route]? - let waypoints: [Waypoint]? + let waypoints: [Route.Waypoint]? init(code: String?, message: String?, error: String?) { self.code = code @@ -37,17 +37,17 @@ extension RouteResponse: Codable { self.uuid = try container.decodeIfPresent(String.self, forKey: .uuid) // Decode waypoints from the response and update their names according to the waypoints from DirectionsOptions.waypoints. - let decodedWaypoints = try container.decodeIfPresent([Waypoint].self, forKey: .waypoints) + let decodedWaypoints = try container.decodeIfPresent([Route.Waypoint].self, forKey: .waypoints) if let decodedWaypoints = decodedWaypoints, 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 + waypoints = zip(decodedWaypoints, options.waypoints).map { (pair) -> Route.Waypoint in let (decodedWaypoint, waypointInOptions) = pair - let waypoint = Waypoint(coordinate: decodedWaypoint.coordinate, coordinateAccuracy: waypointInOptions.coordinateAccuracy, name: waypointInOptions.name?.nonEmptyString ?? decodedWaypoint.name) - waypoint.separatesLegs = waypointInOptions.separatesLegs + var waypoint = decodedWaypoint + if waypointInOptions.separatesLegs, let name = waypointInOptions.name?.nonEmptyString { + waypoint.name = name + } return waypoint } - waypoints?.first?.separatesLegs = true - waypoints?.last?.separatesLegs = true } else { waypoints = decodedWaypoints } @@ -57,8 +57,9 @@ extension RouteResponse: Codable { for route in routes { route.routeIdentifier = uuid // Imbue each route’s legs with the waypoints refined above. + // TODO: Filter these waypoints by whether they separate legs, based on the options, if given. if let waypoints = waypoints { - route.legSeparators = waypoints.filter { $0.separatesLegs } + route.legSeparators = waypoints } } self.routes = routes diff --git a/Sources/MapboxDirections/Waypoint.swift b/Sources/MapboxDirections/Waypoint.swift index f1f1e08dd..d55e2e309 100644 --- a/Sources/MapboxDirections/Waypoint.swift +++ b/Sources/MapboxDirections/Waypoint.swift @@ -1,203 +1,270 @@ import CoreLocation -/** - A `Waypoint` object indicates a location along a route. It may be the route’s origin or destination, or it may be another location that the route visits. A waypoint object indicates the location’s geographic location along with other optional information, such as a name or the user’s direction approaching the waypoint. You create a `RouteOptions` object using waypoint objects and also receive waypoint objects in the completion handler of the `Directions.calculate(_:completionHandler:)` method. - */ -public class Waypoint: Codable { +extension DirectionsOptions { + /** + A waypoint indicates a location along a route. It may be the route’s origin or destination, or it may be another location that the route visits. A waypoint object indicates the location’s geographic location along with other optional information, such as a name or the user’s direction approaching the waypoint. You create a `RouteOptions` or `MatchOptions` object using waypoints. + */ + public struct Waypoint: Codable, Equatable { + // MARK: Creating a Waypoint + + /** + Initializes a new waypoint object with the given geographic coordinate and an optional accuracy and name. + + - parameter coordinate: The geographic coordinate of the waypoint. + - parameter coordinateAccuracy: The maximum distance away from the waypoint that the route may come and still be considered viable. This argument is measured in meters. A negative value means the route may be an indefinite number of meters away from the route and still be considered viable. + + It is recommended that the value of this argument be greater than the `horizontalAccuracy` property of a `CLLocation` object obtained from a `CLLocationManager` object. There is a high likelihood that the user may be located some distance away from a navigable road, for instance if the user is currently on a driveway or inside a building. + - parameter name: The name of the waypoint. This argument does not affect the route but may help you to distinguish one waypoint from another. + */ + public init(coordinate: CLLocationCoordinate2D, coordinateAccuracy: CLLocationAccuracy? = nil, name: String? = nil) { + self.coordinate = coordinate + self.coordinateAccuracy = coordinateAccuracy + self.name = name + } + + #if os(tvOS) || os(watchOS) + /** + Initializes a new waypoint object with the given `CLLocation` object and an optional heading value and name. + + - note: This initializer is intended for `CLLocation` objects created using the `CLLocation.init(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid overfitting, since the `Waypoint` class’s `coordinateAccuracy` property represents the maximum allowed deviation from the waypoint. There is a high likelihood that the user may be located some distance away from a navigable road, for instance if the user is currently on a driveway of inside a building. + + - parameter location: A `CLLocation` object representing the waypoint’s location. This initializer respects the `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `coordinate` and `coordinateAccuracy` properties, respectively. + - parameter heading: A `CLLocationDirection` value representing the direction from which the route must approach the waypoint in order to be considered viable. This value is stored in the `headingAccuracy` property. + - parameter name: The name of the waypoint. This argument does not affect the route but may help you to distinguish one waypoint from another. + */ + public init(location: CLLocation, heading: CLLocationDirection? = nil, name: String? = nil) { + coordinate = location.coordinate + coordinateAccuracy = location.horizontalAccuracy + if let heading = heading, heading >= 0 { + self.heading = heading + } + self.name = name + } + #else + /** + Initializes a new waypoint object with the given `CLLocation` object and an optional `CLHeading` object and name. + + - note: This initializer is intended for `CLLocation` objects created using the `CLLocation.init(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid overfitting, since the `Waypoint` class’s `coordinateAccuracy` property represents the maximum allowed deviation from the waypoint. There is a high likelihood that the user may be located some distance away from a navigable road, for instance if the user is currently on a driveway of inside a building. + + - parameter location: A `CLLocation` object representing the waypoint’s location. This initializer respects the `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `coordinate` and `coordinateAccuracy` properties, respectively. + - parameter heading: A `CLHeading` object representing the direction from which the route must approach the waypoint in order to be considered viable. This initializer respects the `CLHeading` class’s `trueHeading` property or `magneticHeading` property, converting it into the `headingAccuracy` property. + - parameter name: The name of the waypoint. This argument does not affect the route but may help you to distinguish one waypoint from another. + */ + public init(location: CLLocation, heading: CLHeading? = nil, name: String? = nil) { + coordinate = location.coordinate + coordinateAccuracy = location.horizontalAccuracy + if let heading = heading { + self.heading = heading.trueHeading >= 0 ? heading.trueHeading : heading.magneticHeading + } + self.name = name + } + #endif + + // MARK: Positioning the Waypoint + + /** + The geographic coordinate of the waypoint. + */ + public var coordinate: CLLocationCoordinate2D + + /** + The radius of uncertainty for the waypoint, measured in meters. + + For a route to be considered viable, it must enter this waypoint’s circle of uncertainty. The `coordinate` property identifies the center of the circle, while this property indicates the circle’s radius. If the value of this property is negative, a route is considered viable regardless of whether it enters this waypoint’s circle of uncertainty, subject to an undefined maximum distance. + + By default, the value of this property is `nil`. + */ + public var coordinateAccuracy: CLLocationAccuracy? + + /** + The geographic coordinate of the waypoint’s target. + + The waypoint’s target affects arrival instructions without affecting the route’s shape. For example, a delivery or ride hailing application may specify a waypoint target that represents a drop-off location. The target determines whether the arrival visual and spoken instructions indicate that the destination is “on the left” or “on the right”. + + By default, this property is set to `nil`, meaning the waypoint has no target. This property is ignored on the first waypoint of a `RouteOptions` object, on any waypoint of a `MatchOptions` object, or on any waypoint of a `RouteOptions` object if `DirectionsOptions.includesSteps` is set to `false`. + + This property corresponds to the [`waypoint_targets`](https://docs.mapbox.com/api/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. + */ + public var targetCoordinate: CLLocationCoordinate2D? + + // MARK: Getting the Direction of Approach + + /** + The direction from which a route must approach this waypoint in order to be considered viable. + + This property is measured in degrees clockwise from true north. A value of 0 degrees means due north, 90 degrees means due east, 180 degrees means due south, and so on. If the value of this property is negative, a route is considered viable regardless of the direction from which it approaches this waypoint. + + If this waypoint is the first waypoint (the source waypoint), the route must start out by heading in the direction specified by this property. You should always set the `headingAccuracy` property in conjunction with this property. If the `headingAccuracy` property is set to `nil`, this property is ignored. + + For driving directions, this property can be useful for avoiding a route that begins by going in the direction opposite the current direction of travel. For example, if you know the user is moving eastwardly and the first waypoint is the user’s current location, specifying a heading of 90 degrees and a heading accuracy of 90 degrees for the first waypoint avoids a route that begins with a “head west” instruction. + + You should be certain that the user is in motion before specifying a heading and heading accuracy; otherwise, you may be unnecessarily filtering out the best route. For example, suppose the user is sitting in a car parked in a driveway, facing due north, with the garage in front and the street to the rear. In that case, specifying a heading of 0 degrees and a heading accuracy of 90 degrees may result in a route that begins on the back alley or, worse, no route at all. For this reason, it is recommended that you only specify a heading and heading accuracy when automatically recalculating directions due to the user deviating from the route. + + By default, the value of this property is `nil`, meaning that a route is considered viable regardless of the direction of approach. + */ + public var heading: CLLocationDirection? + + /** + The maximum amount, in degrees, by which a route’s approach to a waypoint may differ from `heading` in either direction in order to be considered viable. + + A value of 0 degrees means that the approach must match the specified `heading` exactly – an unlikely scenario. A value of 180 degrees or more means that the approach may be as much as 180 degrees in either direction from the specified `heading`, effectively allowing a candidate route to approach the waypoint from any direction. + + If you set the `heading` property, you should set this property to a value such as 90 degrees, to avoid filtering out routes whose approaches differ only slightly from the specified `heading`. Otherwise, if the `heading` property is set to a negative value, this property is ignored. + + By default, the value of this property is `nil`, meaning that a route is considered viable regardless of the direction of approach. + */ + public var headingAccuracy: CLLocationDirection? + + /** + A Boolean value indicating whether arriving on opposite side is allowed. + This property has no effect if `DirectionsOptions.includesSteps` is set to `false`. + This property corresponds to the [`approaches`](https://www.mapbox.com/api-documentation/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. + */ + public var allowsArrivingOnOppositeSide = true + + // MARK: Identifying the Waypoint + + /** + The name of the waypoint. + + This property does not affect the route, but the name is included in the arrival instruction, to help the user distinguish between multiple destinations. The name can also help you distinguish one waypoint from another in the array of waypoints passed into the completion handler of the `Directions.calculate(_:completionHandler:)` method. + */ + public var name: String? + + // MARK: Separating the Routes Into Legs + + /** + A Boolean value indicating whether the waypoint is significant enough to appear in the resulting routes as a waypoint separating two legs, along with corresponding guidance instructions. + + By default, this property is set to `true`, which means that each resulting route will include a leg that ends by arriving at the waypoint as `RouteLeg.destination` and a subsequent leg that begins by departing from the waypoint as `RouteLeg.source`. Otherwise, if this property is set to `false`, a single leg passes through the waypoint without specifically mentioning it. Regardless of the value of this property, each resulting route passes through the location specified by the `coordinate` property, accounting for approach-related properties such as `heading`. + + With the Mapbox Directions API, set this property to `false` if you want the waypoint’s location to influence the path that the route follows without attaching any meaning to the waypoint object itself. With the Mapbox Map Matching API, use this property when the `DirectionsOptions.includesSteps` property is `true` or when `coordinates` represents a trace with a high sample rate. + This property has no effect if `DirectionsOptions.includesSteps` is set to `false`, or if `MatchOptions.waypointIndices` is non-nil. + This property corresponds to the [`approaches`](https://docs.mapbox.com/api/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. + */ + public var separatesLegs: Bool = true + + var headingDescription: String { + guard let heading = self.heading, let accuracy = self.headingAccuracy else { + return "" + } + + return "\(heading.truncatingRemainder(dividingBy: 360)),\(min(accuracy, 180))" + } + } +} + +extension DirectionsOptions.Waypoint { private enum CodingKeys: String, CodingKey { case coordinate = "location" case coordinateAccuracy case targetCoordinate case heading case headingAccuracy - case separatesLegs - case name case allowsArrivingOnOppositeSide + case name + case separatesLegs } - // MARK: Creating a Waypoint - - required public init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) coordinate = try container.decode(CLLocationCoordinate2D.self, forKey: .coordinate) - coordinateAccuracy = try container.decodeIfPresent(CLLocationAccuracy.self, forKey: .coordinateAccuracy) - targetCoordinate = try container.decodeIfPresent(CLLocationCoordinate2D.self, forKey: .targetCoordinate) - heading = try container.decodeIfPresent(CLLocationDirection.self, forKey: .heading) - headingAccuracy = try container.decodeIfPresent(CLLocationDirection.self, forKey: .headingAccuracy) - if let separates = try container.decodeIfPresent(Bool.self, forKey: .separatesLegs) { - separatesLegs = separates - } - - if let allows = try container.decodeIfPresent(Bool.self, forKey: .allowsArrivingOnOppositeSide) { - allowsArrivingOnOppositeSide = allows + if let allowsArrivingOnOppositeSide = try container.decodeIfPresent(Bool.self, forKey: .allowsArrivingOnOppositeSide) { + self.allowsArrivingOnOppositeSide = allowsArrivingOnOppositeSide } - if let name = try container.decodeIfPresent(String.self, forKey: .name), - !name.isEmpty { + if let name = try container.decodeIfPresent(String.self, forKey: .name)?.nonEmptyString { self.name = name - } else { - name = nil } - } - - /** - Initializes a new waypoint object with the given geographic coordinate and an optional accuracy and name. - - - parameter coordinate: The geographic coordinate of the waypoint. - - parameter coordinateAccuracy: The maximum distance away from the waypoint that the route may come and still be considered viable. This argument is measured in meters. A negative value means the route may be an indefinite number of meters away from the route and still be considered viable. - It is recommended that the value of this argument be greater than the `horizontalAccuracy` property of a `CLLocation` object obtained from a `CLLocationManager` object. There is a high likelihood that the user may be located some distance away from a navigable road, for instance if the user is currently on a driveway or inside a building. - - parameter name: The name of the waypoint. This argument does not affect the route but may help you to distinguish one waypoint from another. - */ - public init(coordinate: CLLocationCoordinate2D, coordinateAccuracy: CLLocationAccuracy? = nil, name: String? = nil) { - self.coordinate = coordinate - self.coordinateAccuracy = coordinateAccuracy - self.name = name - } - - #if os(tvOS) || os(watchOS) - /** - Initializes a new waypoint object with the given `CLLocation` object and an optional heading value and name. - - - note: This initializer is intended for `CLLocation` objects created using the `CLLocation.init(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid overfitting, since the `Waypoint` class’s `coordinateAccuracy` property represents the maximum allowed deviation from the waypoint. There is a high likelihood that the user may be located some distance away from a navigable road, for instance if the user is currently on a driveway of inside a building. - - - parameter location: A `CLLocation` object representing the waypoint’s location. This initializer respects the `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `coordinate` and `coordinateAccuracy` properties, respectively. - - parameter heading: A `CLLocationDirection` value representing the direction from which the route must approach the waypoint in order to be considered viable. This value is stored in the `headingAccuracy` property. - - parameter name: The name of the waypoint. This argument does not affect the route but may help you to distinguish one waypoint from another. - */ - public init(location: CLLocation, heading: CLLocationDirection? = nil, name: String? = nil) { - coordinate = location.coordinate - coordinateAccuracy = location.horizontalAccuracy - if let heading = heading , heading >= 0 { - self.heading = heading + if let separatesLegs = try container.decodeIfPresent(Bool.self, forKey: .separatesLegs) { + self.separatesLegs = separatesLegs } - self.name = name } - #else - /** - Initializes a new waypoint object with the given `CLLocation` object and an optional `CLHeading` object and name. - - - note: This initializer is intended for `CLLocation` objects created using the `CLLocation.init(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid overfitting, since the `Waypoint` class’s `coordinateAccuracy` property represents the maximum allowed deviation from the waypoint. There is a high likelihood that the user may be located some distance away from a navigable road, for instance if the user is currently on a driveway of inside a building. - - - parameter location: A `CLLocation` object representing the waypoint’s location. This initializer respects the `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `coordinate` and `coordinateAccuracy` properties, respectively. - - parameter heading: A `CLHeading` object representing the direction from which the route must approach the waypoint in order to be considered viable. This initializer respects the `CLHeading` class’s `trueHeading` property or `magneticHeading` property, converting it into the `headingAccuracy` property. - - parameter name: The name of the waypoint. This argument does not affect the route but may help you to distinguish one waypoint from another. - */ - public init(location: CLLocation, heading: CLHeading? = nil, name: String? = nil) { - coordinate = location.coordinate - coordinateAccuracy = location.horizontalAccuracy - if let heading = heading { - self.heading = heading.trueHeading >= 0 ? heading.trueHeading : heading.magneticHeading - } - self.name = name +} + +extension DirectionsOptions.Waypoint: CustomStringConvertible { + public var description: String { + return name ?? "" } - #endif - - // MARK: Positioning the Waypoint - +} + +/** + A matchpoint represents a location matched to the road network. + */ +public protocol Matchpoint { /** - The geographic coordinate of the waypoint. + The geographic coordinate of the matchpoint. */ - public let coordinate: CLLocationCoordinate2D + var coordinate: CLLocationCoordinate2D { get } /** - The radius of uncertainty for the waypoint, measured in meters. - - For a route to be considered viable, it must enter this waypoint’s circle of uncertainty. The `coordinate` property identifies the center of the circle, while this property indicates the circle’s radius. If the value of this property is negative, a route is considered viable regardless of whether it enters this waypoint’s circle of uncertainty, subject to an undefined maximum distance. + The straight-line distance from this matchpoint to the corresponding waypoint in the `RouteOptions` or `MatchOptions` object. - By default, the value of this property is `nil`. + The requested waypoint is snapped to the road network. This property contains the straight-line distance from the original requested waypoint’s `DirectionsOptions.Waypoint.coordinate` property to the `coordinate` property. */ - public var coordinateAccuracy: CLLocationAccuracy? - - /** - The geographic coordinate of the waypoint’s target. - - The waypoint’s target affects arrival instructions without affecting the route’s shape. For example, a delivery or ride hailing application may specify a waypoint target that represents a drop-off location. The target determines whether the arrival visual and spoken instructions indicate that the destination is “on the left” or “on the right”. - - By default, this property is set to `nil`, meaning the waypoint has no target. This property is ignored on the first waypoint of a `RouteOptions` object, on any waypoint of a `MatchOptions` object, or on any waypoint of a `RouteOptions` object if `DirectionsOptions.includesSteps` is set to `false`. + var correction: CLLocationDistance { get } +} - This property corresponds to the [`waypoint_targets`](https://docs.mapbox.com/api/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. - */ - public var targetCoordinate: CLLocationCoordinate2D? - - // MARK: Getting the Direction of Approach - +public extension DirectionsResult { /** - The direction from which a route must approach this waypoint in order to be considered viable. - - This property is measured in degrees clockwise from true north. A value of 0 degrees means due north, 90 degrees means due east, 180 degrees means due south, and so on. If the value of this property is negative, a route is considered viable regardless of the direction from which it approaches this waypoint. - - If this waypoint is the first waypoint (the source waypoint), the route must start out by heading in the direction specified by this property. You should always set the `headingAccuracy` property in conjunction with this property. If the `headingAccuracy` property is set to `nil`, this property is ignored. - - For driving directions, this property can be useful for avoiding a route that begins by going in the direction opposite the current direction of travel. For example, if you know the user is moving eastwardly and the first waypoint is the user’s current location, specifying a heading of 90 degrees and a heading accuracy of 90 degrees for the first waypoint avoids a route that begins with a “head west” instruction. - - You should be certain that the user is in motion before specifying a heading and heading accuracy; otherwise, you may be unnecessarily filtering out the best route. For example, suppose the user is sitting in a car parked in a driveway, facing due north, with the garage in front and the street to the rear. In that case, specifying a heading of 0 degrees and a heading accuracy of 90 degrees may result in a route that begins on the back alley or, worse, no route at all. For this reason, it is recommended that you only specify a heading and heading accuracy when automatically recalculating directions due to the user deviating from the route. - - By default, the value of this property is `nil`, meaning that a route is considered viable regardless of the direction of approach. + A `Waypoint` object indicates a location along a route. It may be the route’s origin or destination, or it may be another location that the route visits. A waypoint object indicates the location’s geographic location along with other optional information, such as a name or the user’s direction approaching the waypoint. You receive waypoints in the completion handler of the `Directions.calculate(_:completionHandler:)` method. */ - public var heading: CLLocationDirection? = nil - - /** - The maximum amount, in degrees, by which a route’s approach to a waypoint may differ from `heading` in either direction in order to be considered viable. - - A value of 0 degrees means that the approach must match the specified `heading` exactly – an unlikely scenario. A value of 180 degrees or more means that the approach may be as much as 180 degrees in either direction from the specified `heading`, effectively allowing a candidate route to approach the waypoint from any direction. - - If you set the `heading` property, you should set this property to a value such as 90 degrees, to avoid filtering out routes whose approaches differ only slightly from the specified `heading`. Otherwise, if the `heading` property is set to a negative value, this property is ignored. - - By default, the value of this property is `nil`, meaning that a route is considered viable regardless of the direction of approach. - */ - public var headingAccuracy: CLLocationDirection? = nil - - internal var headingDescription: String { - guard let heading = self.heading, let accuracy = self.headingAccuracy else { - return "" - } + struct Waypoint: Matchpoint, Equatable { + // MARK: Positioning the Waypoint + + /** + The geographic coordinate of the waypoint, snapped to the road network. + */ + public var coordinate: CLLocationCoordinate2D - return "\(heading.truncatingRemainder(dividingBy: 360)),\(min(accuracy, 180))" + /** + The straight-line distance from this waypoint to the corresponding waypoint in the `RouteOptions` or `MatchOptions` object. + + The requested waypoint is snapped to the road network. This property contains the straight-line distance from the original requested waypoint’s `DirectionsOptions.Waypoint.coordinate` property to the `coordinate` property. + */ + public var correction: CLLocationDistance + + // MARK: Identifying the Waypoint + + /** + The name of the waypoint. + + If you did not specify a name for the corresponding waypoint in the `RouteOptions` or `MatchOptions` object, this property may contain the name of the road or path to which the waypoint was snapped. + */ + public var name: String? + } +} + +extension DirectionsResult.Waypoint: Codable { + private enum CodingKeys: String, CodingKey { + case coordinate = "location" + case correction = "distance" + case name } - /** - A Boolean value indicating whether arriving on opposite side is allowed. - This property has no effect if `DirectionsOptions.includesSteps` is set to `false`. - This property corresponds to the [`approaches`](https://www.mapbox.com/api-documentation/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. - */ - open var allowsArrivingOnOppositeSide = true - - // MARK: Identifying the Waypoint - - /** - The name of the waypoint. - - This property does not affect the route, but the name is included in the arrival instruction, to help the user distinguish between multiple destinations. The name can also help you distinguish one waypoint from another in the array of waypoints passed into the completion handler of the `Directions.calculate(_:completionHandler:)` method. - */ - public var name: String? - - // MARK: Separating the Routes Into Legs - - /** - A Boolean value indicating whether the waypoint is significant enough to appear in the resulting routes as a waypoint separating two legs, along with corresponding guidance instructions. - - By default, this property is set to `true`, which means that each resulting route will include a leg that ends by arriving at the waypoint as `RouteLeg.destination` and a subsequent leg that begins by departing from the waypoint as `RouteLeg.source`. Otherwise, if this property is set to `false`, a single leg passes through the waypoint without specifically mentioning it. Regardless of the value of this property, each resulting route passes through the location specified by the `coordinate` property, accounting for approach-related properties such as `heading`. - - With the Mapbox Directions API, set this property to `false` if you want the waypoint’s location to influence the path that the route follows without attaching any meaning to the waypoint object itself. With the Mapbox Map Matching API, use this property when the `DirectionsOptions.includesSteps` property is `true` or when `coordinates` represents a trace with a high sample rate. - This property has no effect if `DirectionsOptions.includesSteps` is set to `false`, or if `MatchOptions.waypointIndices` is non-nil. - This property corresponds to the [`approaches`](https://docs.mapbox.com/api/navigation/#retrieve-directions) query parameter in the Mapbox Directions and Map Matching APIs. - */ - public var separatesLegs: Bool = true - - public var description: String { - return name ?? "" + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + coordinate = try container.decode(CLLocationCoordinate2D.self, forKey: .coordinate) + correction = try container.decode(CLLocationDistance.self, forKey: .correction) + + if let name = try container.decodeIfPresent(String.self, forKey: .name)?.nonEmptyString { + self.name = name + } else { + name = nil + } } } -extension Waypoint: Equatable { - public static func == (lhs: Waypoint, rhs: Waypoint) -> Bool { - return lhs.coordinate == rhs.coordinate && lhs.name == rhs.name && lhs.coordinateAccuracy == rhs.coordinateAccuracy +extension DirectionsResult.Waypoint: CustomStringConvertible { + public var description: String { + return name ?? "" } } diff --git a/Tests/MapboxDirectionsTests/InstructionsTests.swift b/Tests/MapboxDirectionsTests/InstructionsTests.swift index 68befdc63..72e8e3799 100644 --- a/Tests/MapboxDirectionsTests/InstructionsTests.swift +++ b/Tests/MapboxDirectionsTests/InstructionsTests.swift @@ -31,8 +31,8 @@ class SpokenInstructionsTests: XCTestCase { return OHHTTPStubsResponse(fileAtPath: path!, statusCode: 200, headers: ["Content-Type": "application/json"]) } - let startWaypoint = Waypoint(location: CLLocation(latitude: 37.780602, longitude: -122.431373), heading: nil, name: "the hotel") - let endWaypoint = Waypoint(location: CLLocation(latitude: 37.758859, longitude: -122.404058), heading: nil, name: "the gym") + let startWaypoint = RouteOptions.Waypoint(location: CLLocation(latitude: 37.780602, longitude: -122.431373), heading: nil, name: "the hotel") + let endWaypoint = RouteOptions.Waypoint(location: CLLocation(latitude: 37.758859, longitude: -122.404058), heading: nil, name: "the gym") let options = RouteOptions(waypoints: [startWaypoint, endWaypoint], profileIdentifier: .automobileAvoidingTraffic) options.shapeFormat = .polyline @@ -123,8 +123,8 @@ class SpokenInstructionsTests: XCTestCase { return OHHTTPStubsResponse(fileAtPath: path!, statusCode: 200, headers: ["Content-Type": "application/json"]) } - let startWaypoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.132063, longitude: -84.531074)) - let endWaypoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.138953, longitude: -84.532934)) + let startWaypoint = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.132063, longitude: -84.531074)) + let endWaypoint = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.138953, longitude: -84.532934)) let options = RouteOptions(waypoints: [startWaypoint, endWaypoint], profileIdentifier: .automobileAvoidingTraffic) options.shapeFormat = .polyline @@ -193,8 +193,8 @@ class SpokenInstructionsTests: XCTestCase { return OHHTTPStubsResponse(fileAtPath: path!, statusCode: 200, headers: ["Content-Type": "application/json"]) } - let startWaypoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.775469, longitude: -122.449158)) - let endWaypoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.347439837741376, longitude: -121.92883115196378)) + let startWaypoint = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.775469, longitude: -122.449158)) + let endWaypoint = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.347439837741376, longitude: -121.92883115196378)) let options = RouteOptions(waypoints: [startWaypoint, endWaypoint], profileIdentifier: .automobileAvoidingTraffic) options.shapeFormat = .polyline diff --git a/Tests/MapboxDirectionsTests/MatchTests.swift b/Tests/MapboxDirectionsTests/MatchTests.swift index fd65b3305..ef16a56f9 100644 --- a/Tests/MapboxDirectionsTests/MatchTests.swift +++ b/Tests/MapboxDirectionsTests/MatchTests.swift @@ -57,8 +57,7 @@ class MatchTests: XCTestCase { let tracePoints = match.tracepoints XCTAssertNotNil(tracePoints) - XCTAssertEqual(tracePoints.first!!.alternateCount, 0) - XCTAssertEqual(tracePoints.last!!.name, "West G Street") + XCTAssertEqual(tracePoints.first!!.countOfAlternatives, 0) // confirming actual decoded values is important because the Directions API // uses an atypical precision level for polyline encoding diff --git a/Tests/MapboxDirectionsTests/RoutableMatchTests.swift b/Tests/MapboxDirectionsTests/RoutableMatchTests.swift index 358cf39c8..a11d0bc57 100644 --- a/Tests/MapboxDirectionsTests/RoutableMatchTests.swift +++ b/Tests/MapboxDirectionsTests/RoutableMatchTests.swift @@ -27,14 +27,15 @@ class RoutableMatchTest: XCTestCase { } var route: Route! - var waypoints: [Waypoint]! + var waypoints: [Route.Waypoint]! - let matchOptions = MatchOptions(coordinates: locations) + let matchOptions = MatchOptions(waypoints: locations.map { (location) in + var waypoint = MatchOptions.Waypoint(coordinate: location) + waypoint.separatesLegs = false + return waypoint + }) matchOptions.includesSteps = true matchOptions.routeShapeResolution = .full - for waypoint in matchOptions.waypoints[1..<(locations.count - 1)] { - waypoint.separatesLegs = false - } let task = Directions(accessToken: BogusToken).calculateRoutes(matching: matchOptions) { (wpoints, routes, error) in XCTAssertNil(error, "Error: \(error!)") diff --git a/Tests/MapboxDirectionsTests/RouteOptionsTests.swift b/Tests/MapboxDirectionsTests/RouteOptionsTests.swift index 2c1616c35..9e3ffb932 100644 --- a/Tests/MapboxDirectionsTests/RouteOptionsTests.swift +++ b/Tests/MapboxDirectionsTests/RouteOptionsTests.swift @@ -33,14 +33,14 @@ class RouteOptionsTests: XCTestCase { // MARK: API name-handling tests - private static var testWaypoints: [Waypoint] { + private static var testWaypoints: [RouteOptions.Waypoint] { return [ - Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.27664, longitude:-84.41139)), - Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.27277, longitude:-84.41226)), + RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.27664, longitude:-84.41139)), + RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.27277, longitude:-84.41226)), ] } - private func response(for fixtureName: String, waypoints: [Waypoint] = testWaypoints) -> (waypoints:[Waypoint], route:Route)? { + private func response(for fixtureName: String, waypoints: [RouteOptions.Waypoint] = testWaypoints) -> (waypoints: [Route.Waypoint], route: Route)? { let testBundle = Bundle(for: type(of: self)) guard let fixtureURL = testBundle.url(forResource:fixtureName, withExtension:"json") else { XCTFail() @@ -81,8 +81,8 @@ class RouteOptionsTests: XCTestCase { } func testResponseWithManuallySetDestinationName() { - let manuallySet = RouteOptionsTests.testWaypoints - manuallySet.last!.name = "manuallyset" + var manuallySet = RouteOptionsTests.testWaypoints + manuallySet[manuallySet.endIndex.advanced(by: -1)].name = "manuallyset" let response = self.response(for: "apiDestinationName", waypoints: manuallySet)! XCTAssertEqual(response.route.legs.last?.destination?.name, "manuallyset", "Waypoint with manually set name should override any computed name.") @@ -90,12 +90,12 @@ class RouteOptionsTests: XCTestCase { func testApproachesURLQueryParams() { let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0) - let wp1 = Waypoint(coordinate: coordinate, coordinateAccuracy: 0) + var wp1 = RouteOptions.Waypoint(coordinate: coordinate, coordinateAccuracy: 0) wp1.allowsArrivingOnOppositeSide = false let waypoints = [ - Waypoint(coordinate: coordinate, coordinateAccuracy: 0), + RouteOptions.Waypoint(coordinate: coordinate, coordinateAccuracy: 0), wp1, - Waypoint(coordinate: coordinate, coordinateAccuracy: 0) + RouteOptions.Waypoint(coordinate: coordinate, coordinateAccuracy: 0) ] let routeOptions = RouteOptions(waypoints: waypoints) @@ -108,9 +108,9 @@ class RouteOptionsTests: XCTestCase { func testMissingApproaches() { let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0) let waypoints = [ - Waypoint(coordinate: coordinate, coordinateAccuracy: 0), - Waypoint(coordinate: coordinate, coordinateAccuracy: 0), - Waypoint(coordinate: coordinate, coordinateAccuracy: 0) + RouteOptions.Waypoint(coordinate: coordinate, coordinateAccuracy: 0), + RouteOptions.Waypoint(coordinate: coordinate, coordinateAccuracy: 0), + RouteOptions.Waypoint(coordinate: coordinate, coordinateAccuracy: 0) ] let routeOptions = RouteOptions(waypoints: waypoints) @@ -130,8 +130,8 @@ class RouteOptionsTests: XCTestCase { } func testWaypointSerialization() { - let origin = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.15031, longitude: -84.47182), name: "XU") - let destination = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.12971, longitude: -84.51638), name: "UC") + let origin = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.15031, longitude: -84.47182), name: "XU") + var destination = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.12971, longitude: -84.51638), name: "UC") destination.targetCoordinate = CLLocationCoordinate2D(latitude: 39.13115, longitude: -84.51619) let options = RouteOptions(waypoints: [origin, destination]) XCTAssertEqual(options.coordinates, "-84.47182,39.15031;-84.51638,39.12971") diff --git a/Tests/MapboxDirectionsTests/V5Tests.swift b/Tests/MapboxDirectionsTests/V5Tests.swift index 63039f006..1df49eaba 100644 --- a/Tests/MapboxDirectionsTests/V5Tests.swift +++ b/Tests/MapboxDirectionsTests/V5Tests.swift @@ -231,16 +231,14 @@ class V5Tests: XCTestCase { return OHHTTPStubsResponse(jsonObject: jsonObject, statusCode: 200, headers: ["Content-Type": "application/json"]) } - let waypoints = [ - Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.33841036211459, longitude: -85.20623174166413), coordinateAccuracy: -1, name: "From"), - Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.34181048315713, longitude: -85.20399062653789), coordinateAccuracy: -1, name: "Via"), - Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.34204769474999, longitude: -85.19969651878529), coordinateAccuracy: -1, name: "To"), - ] - for waypoint in waypoints { - waypoint.separatesLegs = false - } + var from = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.33841036211459, longitude: -85.20623174166413), coordinateAccuracy: -1, name: "From") + from.separatesLegs = false + var via = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.34181048315713, longitude: -85.20399062653789), coordinateAccuracy: -1, name: "Via") + via.separatesLegs = false + var to = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 39.34204769474999, longitude: -85.19969651878529), coordinateAccuracy: -1, name: "To") + to.separatesLegs = false - let options = RouteOptions(waypoints: waypoints) + let options = RouteOptions(waypoints: [from, via, to]) XCTAssertEqual(options.shapeFormat, .polyline, "Route shape format should be Polyline by default.") options.shapeFormat = .polyline @@ -270,12 +268,12 @@ class V5Tests: XCTestCase { XCTAssertEqual(route?.legs.count, 1) let leg = route?.legs.first - XCTAssertEqual(leg?.source!.name, waypoints[0].name) - XCTAssertEqual(leg?.source?.coordinate.latitude ?? 0, waypoints[0].coordinate.latitude, accuracy: 1e-4) - XCTAssertEqual(leg?.source?.coordinate.longitude ?? 0, waypoints[0].coordinate.longitude, accuracy: 1e-4) - XCTAssertEqual(leg?.destination!.name, waypoints[2].name) - XCTAssertEqual(leg?.destination?.coordinate.latitude ?? 0, waypoints[2].coordinate.latitude, accuracy: 1e-4) - XCTAssertEqual(leg?.destination?.coordinate.longitude ?? 0, waypoints[2].coordinate.longitude, accuracy: 1e-4) + XCTAssertEqual(leg?.source!.name, from.name) + XCTAssertEqual(leg?.source?.coordinate.latitude ?? 0, from.coordinate.latitude, accuracy: 1e-4) + XCTAssertEqual(leg?.source?.coordinate.longitude ?? 0, from.coordinate.longitude, accuracy: 1e-4) + XCTAssertEqual(leg?.destination!.name, to.name) + XCTAssertEqual(leg?.destination?.coordinate.latitude ?? 0, to.coordinate.latitude, accuracy: 1e-4) + XCTAssertEqual(leg?.destination?.coordinate.longitude ?? 0, to.coordinate.longitude, accuracy: 1e-4) XCTAssertEqual(leg?.name, "Perlen Strasse, Haupt Strasse") } diff --git a/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift b/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift index 0217c4a5b..a555d72ec 100644 --- a/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift +++ b/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift @@ -6,8 +6,8 @@ import CoreLocation class WalkingOptionsTests: XCTestCase { func testURLQueryParams() { let waypoints = [ - Waypoint(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 1)), - Waypoint(coordinate: CLLocationCoordinate2D(latitude: 2, longitude: 3)) + RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 1)), + RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 2, longitude: 3)) ] let options = RouteOptions(waypoints: waypoints, profileIdentifier: DirectionsProfileIdentifier.walking) diff --git a/Tests/MapboxDirectionsTests/WaypointTests.swift b/Tests/MapboxDirectionsTests/WaypointTests.swift index f834c8ba1..1c2a1d6a2 100644 --- a/Tests/MapboxDirectionsTests/WaypointTests.swift +++ b/Tests/MapboxDirectionsTests/WaypointTests.swift @@ -4,40 +4,70 @@ import CoreLocation class WaypointTests: XCTestCase { func testCoding() { - let originalWaypoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 38.8977, longitude: -77.0365), coordinateAccuracy: 5, name: "White House") - originalWaypoint.targetCoordinate = CLLocationCoordinate2D(latitude: 38.8952261, longitude: -77.0327882) - originalWaypoint.heading = 90 - originalWaypoint.headingAccuracy = 10 - originalWaypoint.allowsArrivingOnOppositeSide = false - - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted] - - let encodedData = try! encoder.encode(originalWaypoint) - let encodedString = String(data: encodedData, encoding: .utf8)! + let waypointJSON: [String: Any?] = [ + "location": [-77.036500000000004, 38.8977], + "name": "White House", + ] + let waypointData = try! JSONSerialization.data(withJSONObject: waypointJSON, options: []) + var waypoint: RouteOptions.Waypoint? + XCTAssertNoThrow(waypoint = try JSONDecoder().decode(RouteOptions.Waypoint.self, from: waypointData)) + XCTAssertNotNil(waypoint) - XCTAssertEqual(encodedString, pass) - - let decoder = JSONDecoder() + if let waypoint = waypoint { + XCTAssertEqual(waypoint.coordinate.latitude, 38.8977, accuracy: 1e-5) + XCTAssertEqual(waypoint.coordinate.longitude, -77.03650, accuracy: 1e-5) + XCTAssertNil(waypoint.coordinateAccuracy) + XCTAssertNil(waypoint.targetCoordinate) + + XCTAssertNil(waypoint.heading) + XCTAssertNil(waypoint.headingAccuracy) + XCTAssertEqual(waypoint.allowsArrivingOnOppositeSide, false) + XCTAssertEqual(waypoint.separatesLegs, true) + } - let decodedWaypoint = try! decoder.decode(Waypoint.self, from: encodedData) + waypoint = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 38.8977, longitude: -77.0365), coordinateAccuracy: 5, name: "White House") + waypoint?.targetCoordinate = CLLocationCoordinate2D(latitude: 38.8952261, longitude: -77.0327882) + waypoint?.heading = 90 + waypoint?.headingAccuracy = 10 + waypoint?.allowsArrivingOnOppositeSide = false + + let encoder = JSONEncoder() + var encodedData: Data? + XCTAssertNoThrow(encodedData = try encoder.encode(waypoint)) + XCTAssertNotNil(encodedData) - XCTAssertEqual(decodedWaypoint.coordinate.latitude, originalWaypoint.coordinate.latitude) - XCTAssertEqual(decodedWaypoint.coordinate.longitude, originalWaypoint.coordinate.longitude) - XCTAssertEqual(decodedWaypoint.coordinateAccuracy, originalWaypoint.coordinateAccuracy) - XCTAssert(decodedWaypoint.targetCoordinate == originalWaypoint.targetCoordinate) - - XCTAssertEqual(decodedWaypoint.heading, originalWaypoint.heading) - XCTAssertEqual(decodedWaypoint.headingAccuracy, originalWaypoint.headingAccuracy) - XCTAssertEqual(decodedWaypoint.allowsArrivingOnOppositeSide, originalWaypoint.allowsArrivingOnOppositeSide) - XCTAssertEqual(decodedWaypoint.separatesLegs, originalWaypoint.separatesLegs) + if let encodedData = encodedData { + var encodedWaypointJSON: [String: Any?]? + XCTAssertNoThrow(encodedWaypointJSON = try JSONSerialization.jsonObject(with: encodedData, options: []) as? [String: Any?]) + XCTAssertNotNil(encodedWaypointJSON) + + XCTAssertEqual(encodedWaypointJSON?["headingAccuracy"] as? CLLocationDirection, waypoint?.headingAccuracy) + encodedWaypointJSON?.removeValue(forKey: "headingAccuracy") + XCTAssertEqual(encodedWaypointJSON?["coordinateAccuracy"] as? CLLocationAccuracy, waypoint?.coordinateAccuracy) + encodedWaypointJSON?.removeValue(forKey: "coordinateAccuracy") + XCTAssertEqual(encodedWaypointJSON?["allowsArrivingOnOppositeSide"] as? Bool, waypoint?.allowsArrivingOnOppositeSide) + encodedWaypointJSON?.removeValue(forKey: "allowsArrivingOnOppositeSide") + XCTAssertEqual(encodedWaypointJSON?["heading"] as? CLLocationDirection, waypoint?.heading) + encodedWaypointJSON?.removeValue(forKey: "heading") + XCTAssertEqual(encodedWaypointJSON?["separatesLegs"] as? Bool, waypoint?.separatesLegs) + encodedWaypointJSON?.removeValue(forKey: "separatesLegs") + + let targetCoordinateJSON = encodedWaypointJSON?["targetCoordinate"] as? [CLLocationDegrees] + XCTAssertNotNil(targetCoordinateJSON) + XCTAssertEqual(targetCoordinateJSON?.count, 2) + XCTAssertEqual(targetCoordinateJSON?[0] ?? 0, waypoint?.targetCoordinate?.longitude ?? 0, accuracy: 1e-5) + XCTAssertEqual(targetCoordinateJSON?[1] ?? 0, waypoint?.targetCoordinate?.latitude ?? 0, accuracy: 1e-5) + encodedWaypointJSON?.removeValue(forKey: "targetCoordinate") + + XCTAssert(JSONSerialization.objectsAreEqual(waypointJSON, encodedWaypointJSON, approximate: true)) + } } func testSeparatesLegs() { - let one = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 1, longitude: 1)) - let two = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 2, longitude: 2)) - let three = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 3, longitude: 3)) - let four = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 4, longitude: 4)) + let one = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 1, longitude: 1)) + var two = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 2, longitude: 2)) + let three = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 3, longitude: 3)) + let four = RouteOptions.Waypoint(coordinate: CLLocationCoordinate2D(latitude: 4, longitude: 4)) let routeOptions = RouteOptions(waypoints: [one, two, three, four]) let matchOptions = MatchOptions(waypoints: [one, two, three, four], profileIdentifier: nil) @@ -56,22 +86,3 @@ class WaypointTests: XCTestCase { XCTAssertEqual(matchOptions.urlQueryItems.first { $0.name == "waypoints" }?.value, "0;2;3") } } - -fileprivate let pass = """ -{ - \"headingAccuracy\" : 10, - \"location\" : [ - -77.036500000000004, - 38.8977 - ], - \"targetCoordinate\" : [ - -77.032788199999999, - 38.895226100000002 - ], - \"coordinateAccuracy\" : 5, - \"allowsArrivingOnOppositeSide\" : false, - \"heading\" : 90, - \"separatesLegs\" : true, - \"name\" : \"White House\" -} -"""