diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1e65468..3bea4aa7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## master +* Added a `Waypoint.targetCoordinate` property for specifying a more specific destination for arrival instructions. ([#326](https://github.com/mapbox/MapboxDirections.swift/pull/326)) * Fixed an issue where the `Waypoint.allowsArrivingOnOppositeSide` property was not copied when copying a `Waypoint` object. ([#326](https://github.com/mapbox/MapboxDirections.swift/pull/326)) ## v0.25.2 diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index 77b74aee2..f51598400 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -201,6 +201,9 @@ DA1A110D1D01045E009F82FA /* DirectionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA1A110A1D01045E009F82FA /* DirectionsTests.swift */; }; DA2E03E91CB0E0B000D1269A /* MBRouteStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2E03E81CB0E0B000D1269A /* MBRouteStep.swift */; }; DA2E03EB1CB0E13D00D1269A /* MBRouteOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2E03EA1CB0E13D00D1269A /* MBRouteOptions.swift */; }; + DA4F84ED21C08BFB008A0434 /* WaypointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4F84EC21C08BFB008A0434 /* WaypointTests.swift */; }; + DA4F84EE21C08BFB008A0434 /* WaypointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4F84EC21C08BFB008A0434 /* WaypointTests.swift */; }; + DA4F84EF21C08BFB008A0434 /* WaypointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4F84EC21C08BFB008A0434 /* WaypointTests.swift */; }; DA688B3E21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA688B3D21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift */; }; DA688B3F21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA688B3D21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift */; }; DA688B4021B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA688B3D21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift */; }; @@ -365,6 +368,7 @@ DA1A110A1D01045E009F82FA /* DirectionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectionsTests.swift; sourceTree = ""; }; DA2E03E81CB0E0B000D1269A /* MBRouteStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MBRouteStep.swift; sourceTree = ""; }; DA2E03EA1CB0E13D00D1269A /* MBRouteOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MBRouteOptions.swift; sourceTree = ""; }; + DA4F84EC21C08BFB008A0434 /* WaypointTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointTests.swift; sourceTree = ""; }; DA688B3D21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualInstructionComponentTests.swift; sourceTree = SOURCE_ROOT; }; DA6C9D881CAE442B00094FBC /* MapboxDirections.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MapboxDirections.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DA6C9D8A1CAE442B00094FBC /* MapboxDirections.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MapboxDirections.h; sourceTree = ""; }; @@ -628,6 +632,7 @@ DA737EE71D0611CB005BDA16 /* V4Tests.swift */, DA6C9DAB1CAEC72800094FBC /* V5Tests.swift */, DA6C9DB11CAECA0E00094FBC /* Fixture.swift */, + DA4F84EC21C08BFB008A0434 /* WaypointTests.swift */, C5247D701E818A24004B6154 /* AnnotationTests.swift */, DAE33A1A1F215DF600C06039 /* IntersectionTests.swift */, C52CE3921F6AF6E70069963D /* IntructionsTests.swift */, @@ -1268,6 +1273,7 @@ DA1A10CE1D00F972009F82FA /* Fixture.swift in Sources */, DA1A110C1D01045E009F82FA /* DirectionsTests.swift in Sources */, C5D1D7F31F6AFBD600A1C4F1 /* IntructionsTests.swift in Sources */, + DA4F84EE21C08BFB008A0434 /* WaypointTests.swift in Sources */, F4D785F01DDD82C100FF4665 /* RouteStepTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1327,6 +1333,7 @@ DA1A10F51D010251009F82FA /* Fixture.swift in Sources */, DA1A110D1D01045E009F82FA /* DirectionsTests.swift in Sources */, C5D1D7F41F6AFBD600A1C4F1 /* IntructionsTests.swift in Sources */, + DA4F84EF21C08BFB008A0434 /* WaypointTests.swift in Sources */, F4D785F11DDD82C100FF4665 /* RouteStepTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1425,6 +1432,7 @@ DA6C9DB21CAECA0E00094FBC /* Fixture.swift in Sources */, DA1A110B1D01045E009F82FA /* DirectionsTests.swift in Sources */, C52CE3931F6AF6E70069963D /* IntructionsTests.swift in Sources */, + DA4F84ED21C08BFB008A0434 /* WaypointTests.swift in Sources */, F4D785EF1DDD82C100FF4665 /* RouteStepTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MapboxDirections/Extensions/CLLocationCoordinate2D.swift b/MapboxDirections/Extensions/CLLocationCoordinate2D.swift index 6af4e3040..91ac34cca 100644 --- a/MapboxDirections/Extensions/CLLocationCoordinate2D.swift +++ b/MapboxDirections/Extensions/CLLocationCoordinate2D.swift @@ -33,4 +33,14 @@ extension CLLocationCoordinate2D { let coordinates = lineString["coordinates"] as! [[Double]] return coordinates.map { self.init(geoJSON: $0) } } + + /** + A string representation of the coordinate suitable for insertion in a Directions API request URL. + */ + internal var stringForRequestURL: String? { + guard CLLocationCoordinate2DIsValid(self) else { + return nil + } + return "\(longitude.rounded(to: 1e6)),\(latitude.rounded(to: 1e6))" + } } diff --git a/MapboxDirections/MBDirectionsOptions.swift b/MapboxDirections/MBDirectionsOptions.swift index 0617cd643..281adf783 100644 --- a/MapboxDirections/MBDirectionsOptions.swift +++ b/MapboxDirections/MBDirectionsOptions.swift @@ -328,7 +328,7 @@ open class DirectionsOptions: NSObject, NSSecureCoding, NSCopying { An array of directions query strings to include in the request URL. */ internal var queries: [String] { - return waypoints.compactMap { return "\($0.coordinate.longitude.rounded(to: 1e6)),\($0.coordinate.latitude.rounded(to: 1e6))" } + return waypoints.compactMap{ $0.coordinate.stringForRequestURL } } internal var path: String { diff --git a/MapboxDirections/MBRouteOptions.swift b/MapboxDirections/MBRouteOptions.swift index 963e51a42..07a9b4407 100644 --- a/MapboxDirections/MBRouteOptions.swift +++ b/MapboxDirections/MBRouteOptions.swift @@ -142,6 +142,11 @@ open class RouteOptions: DirectionsOptions { params.append(URLQueryItem(name: "exclude", value: firstRoadClass)) } } + + if waypoints.first(where: { CLLocationCoordinate2DIsValid($0.targetCoordinate) }) != nil { + let targetCoordinates = waypoints.map { $0.targetCoordinate.stringForRequestURL ?? "" }.joined(separator: ";") + params.append(URLQueryItem(name: "waypoint_targets", value: targetCoordinates)) + } return params } diff --git a/MapboxDirections/MBWaypoint.swift b/MapboxDirections/MBWaypoint.swift index 9573bb5b6..1b3395658 100644 --- a/MapboxDirections/MBWaypoint.swift +++ b/MapboxDirections/MBWaypoint.swift @@ -67,6 +67,9 @@ open class Waypoint: NSObject, NSCopying, NSSecureCoding { let longitude = decoder.decodeDouble(forKey: "longitude") coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) coordinateAccuracy = decoder.decodeDouble(forKey: "coordinateAccuracy") + let targetLatitude = decoder.decodeDouble(forKey: "targetLatitude") + let targetLongitude = decoder.decodeDouble(forKey: "targetLongitude") + targetCoordinate = CLLocationCoordinate2D(latitude: targetLatitude, longitude: targetLongitude) heading = decoder.decodeDouble(forKey: "heading") headingAccuracy = decoder.decodeDouble(forKey: "headingAccuracy") name = decoder.decodeObject(of: NSString.self, forKey: "name") as String? @@ -77,6 +80,8 @@ open class Waypoint: NSObject, NSCopying, NSSecureCoding { coder.encode(coordinate.latitude, forKey: "latitude") coder.encode(coordinate.longitude, forKey: "longitude") coder.encode(coordinateAccuracy, forKey: "coordinateAccuracy") + coder.encode(targetCoordinate.latitude, forKey: "targetLatitude") + coder.encode(targetCoordinate.longitude, forKey: "targetLongitude") coder.encode(heading, forKey: "heading") coder.encode(headingAccuracy, forKey: "headingAccuracy") coder.encode(name, forKey: "name") @@ -85,6 +90,7 @@ open class Waypoint: NSObject, NSCopying, NSSecureCoding { open func copy(with zone: NSZone?) -> Any { let copy = Waypoint(coordinate: coordinate, coordinateAccuracy: coordinateAccuracy, name: name) + copy.targetCoordinate = targetCoordinate copy.heading = heading copy.headingAccuracy = headingAccuracy copy.allowsArrivingOnOppositeSide = allowsArrivingOnOppositeSide @@ -107,6 +113,17 @@ open class Waypoint: NSObject, NSCopying, NSSecureCoding { */ @objc open var coordinateAccuracy: CLLocationAccuracy = -1 + /** + The geographic coordinate of the waypoint’s target. + + The waypoint’s target affects arrival instructions without affecting the route’s shape. For example, a delivery or ride hailing application may specify a waypoint target that represents a drop-off location. The target determines whether the arrival visual and spoken instructions indicate that the destination is “on the left” or “on the right”. + + By default, this property is set to `kCLLocationCoordinate2DInvalid`, meaning the waypoint has no target. This property is ignored if `DirectionsOptions.includesSteps` is `false`. + + This property corresponds to the [`waypoint_targets`](https://www.mapbox.com/api-documentation/#retrieve-directions) query parameter in the Mapbox Directions API. + */ + @objc open var targetCoordinate: CLLocationCoordinate2D = kCLLocationCoordinate2DInvalid + // MARK: Getting the Direction of Approach /** @@ -145,6 +162,8 @@ open class Waypoint: NSObject, NSCopying, NSSecureCoding { The name of the waypoint. This parameter does not affect the route, but you can set the name of a waypoint you pass into a `RouteOptions` object to help you distinguish one waypoint from another in the array of waypoints passed into the completion handler of the `Directions.calculate(_:completionHandler:)` method. + + This property corresponds to the [`waypoint_names`](https://www.mapbox.com/api-documentation/#retrieve-directions) query parameter in the Mapbox Directions API. */ @objc open var name: String? @@ -152,6 +171,8 @@ open class Waypoint: NSObject, NSCopying, NSSecureCoding { A boolean value indicating whether arriving on opposite side is allowed. This property has no effect if `RouteOptions.includesSteps` is set to `false`. + + This property corresponds to the [`approaches`](https://www.mapbox.com/api-documentation/#retrieve-directions) query parameter in the Mapbox Directions API. */ @objc open var allowsArrivingOnOppositeSide = true diff --git a/MapboxDirectionsTests/RouteOptionsTests.swift b/MapboxDirectionsTests/RouteOptionsTests.swift index 58113955a..899573d9d 100644 --- a/MapboxDirectionsTests/RouteOptionsTests.swift +++ b/MapboxDirectionsTests/RouteOptionsTests.swift @@ -126,6 +126,17 @@ class RouteOptionsTests: XCTestCase { XCTAssert(answer == correct, "Coordinates should be truncated.") } + + 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") + destination.targetCoordinate = CLLocationCoordinate2D(latitude: 39.13115, longitude: -84.51619) + let options = RouteOptions(waypoints: [origin, destination]) + + XCTAssertEqual(options.queries, ["-84.47182,39.15031", "-84.51638,39.12971"]) + XCTAssertTrue(options.params.contains(URLQueryItem(name: "waypoint_names", value: "XU;UC"))) + XCTAssertTrue(options.params.contains(URLQueryItem(name: "waypoint_targets", value: ";-84.51619,39.13115"))) + } } private extension RouteOptions { diff --git a/MapboxDirectionsTests/WaypointTests.swift b/MapboxDirectionsTests/WaypointTests.swift new file mode 100644 index 000000000..15ad59fd5 --- /dev/null +++ b/MapboxDirectionsTests/WaypointTests.swift @@ -0,0 +1,57 @@ +import XCTest +import MapboxDirections + +class WaypointTests: XCTestCase { + func testCopying() { + 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 + + guard let copy = originalWaypoint.copy() as? Waypoint else { + return XCTFail("Waypoint copy method should an object of same type") + } + + XCTAssertEqual(copy.coordinate.latitude, originalWaypoint.coordinate.latitude) + XCTAssertEqual(copy.coordinate.longitude, originalWaypoint.coordinate.longitude) + XCTAssertEqual(copy.coordinateAccuracy, originalWaypoint.coordinateAccuracy) + XCTAssertEqual(copy.targetCoordinate.latitude, originalWaypoint.targetCoordinate.latitude) + XCTAssertEqual(copy.targetCoordinate.longitude, originalWaypoint.targetCoordinate.longitude) + XCTAssertEqual(copy.heading, originalWaypoint.heading) + XCTAssertEqual(copy.headingAccuracy, originalWaypoint.headingAccuracy) + XCTAssertEqual(copy.allowsArrivingOnOppositeSide, originalWaypoint.allowsArrivingOnOppositeSide) + } + + 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 encodedData = NSMutableData() + let coder = NSKeyedArchiver(forWritingWith: encodedData) + coder.requiresSecureCoding = true + coder.encode(originalWaypoint, forKey: "waypoint") + coder.finishEncoding() + + let decoder = NSKeyedUnarchiver(forReadingWith: encodedData as Data) + decoder.requiresSecureCoding = true + defer { + decoder.finishDecoding() + } + guard let decodedWaypoint = decoder.decodeObject(of: Waypoint.self, forKey: "waypoint") else { + return XCTFail("Unable to decode waypoint") + } + + XCTAssertEqual(decodedWaypoint.coordinate.latitude, originalWaypoint.coordinate.latitude) + XCTAssertEqual(decodedWaypoint.coordinate.longitude, originalWaypoint.coordinate.longitude) + XCTAssertEqual(decodedWaypoint.coordinateAccuracy, originalWaypoint.coordinateAccuracy) + XCTAssertEqual(decodedWaypoint.targetCoordinate.latitude, originalWaypoint.targetCoordinate.latitude) + XCTAssertEqual(decodedWaypoint.targetCoordinate.longitude, originalWaypoint.targetCoordinate.longitude) + XCTAssertEqual(decodedWaypoint.heading, originalWaypoint.heading) + XCTAssertEqual(decodedWaypoint.headingAccuracy, originalWaypoint.headingAccuracy) + XCTAssertEqual(decodedWaypoint.allowsArrivingOnOppositeSide, originalWaypoint.allowsArrivingOnOppositeSide) + } +}