From a8c930070fe38013070ab518a7a4389e99630864 Mon Sep 17 00:00:00 2001 From: Fredrik Karlsson Date: Mon, 25 Jun 2018 21:49:58 +0200 Subject: [PATCH] Decode polyline6 (#281) * Decode polyline6 * Transform polyline5 to 6 in v5 test * Add a blurb in the changelog * Fix geometry decoding for match --- CHANGELOG.MD | 1 + Cartfile.resolved | 2 +- MapboxDirections/MBDirectionsOptions.swift | 22 +++++++++ MapboxDirections/MBDirectionsResult.swift | 2 +- MapboxDirections/MBRoute.swift | 26 ++++------ MapboxDirections/MBRouteLeg.swift | 17 ++++--- MapboxDirections/MBRouteOptions.swift | 4 +- MapboxDirections/MBRouteStep.swift | 14 ++---- MapboxDirections/Match/MBMatch.swift | 14 ++---- MapboxDirections/Match/MBMatchOptions.swift | 2 +- MapboxDirectionsTests/V5Tests.swift | 53 +++++++++++++++++++-- 11 files changed, 101 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 8aa2835a7..c38ba1cbc 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -3,3 +3,4 @@ ## master * Removed `MBAttributeOpenStreetMapNodeIdentifier, as it is no longer being tracked by the API. This is a breaking change. +* Fixed a bug which caused coordinates to be off by a factor of 10 when requesting `.polyline6` shape format. \ No newline at end of file diff --git a/Cartfile.resolved b/Cartfile.resolved index add73cd12..6aa633471 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,3 +1,3 @@ -binary "https://www.mapbox.com/ios-sdk/Mapbox-iOS-SDK.json" "3.7.6" +binary "https://www.mapbox.com/ios-sdk/Mapbox-iOS-SDK.json" "3.7.8" github "AliSoftware/OHHTTPStubs" "6.1.0" github "raphaelmor/Polyline" "v4.2.0" diff --git a/MapboxDirections/MBDirectionsOptions.swift b/MapboxDirections/MBDirectionsOptions.swift index d16dfb295..c0282af73 100644 --- a/MapboxDirections/MBDirectionsOptions.swift +++ b/MapboxDirections/MBDirectionsOptions.swift @@ -1,4 +1,5 @@ import Foundation +import Polyline /** A `RouteShapeFormat` indicates the format of a route or match shape in the raw HTTP response. @@ -51,6 +52,27 @@ public enum RouteShapeFormat: UInt, CustomStringConvertible { } } +extension RouteShapeFormat { + + func coordinates(from geometry: Any?) -> [CLLocationCoordinate2D]? { + switch self { + case .geoJSON: + if let geometry = geometry as? JSONDictionary { + return CLLocationCoordinate2D.coordinates(geoJSON: geometry) + } + case .polyline: + if let geometry = geometry as? String { + return decodePolyline(geometry, precision: 1e5)! + } + case .polyline6: + if let geometry = geometry as? String { + return decodePolyline(geometry, precision: 1e6)! + } + } + return nil + } +} + /** A `RouteShapeResolution` indicates the level of detail in a route’s shape, or whether the shape is present at all. */ diff --git a/MapboxDirections/MBDirectionsResult.swift b/MapboxDirections/MBDirectionsResult.swift index cdd7cd619..399815e9c 100644 --- a/MapboxDirections/MBDirectionsResult.swift +++ b/MapboxDirections/MBDirectionsResult.swift @@ -8,7 +8,7 @@ import Polyline @objc(MBDirectionsResult) open class DirectionsResult: NSObject, NSSecureCoding { - @objc internal init(options: DirectionsOptions, legs: [RouteLeg], distance: CLLocationDistance, expectedTravelTime: TimeInterval, coordinates: [CLLocationCoordinate2D]?, speechLocale: Locale?) { + @objc internal init(legs: [RouteLeg], distance: CLLocationDistance, expectedTravelTime: TimeInterval, coordinates: [CLLocationCoordinate2D]?, speechLocale: Locale?, options: DirectionsOptions) { self.directionsOptions = options self.legs = legs self.distance = distance diff --git a/MapboxDirections/MBRoute.swift b/MapboxDirections/MBRoute.swift index 90c86efd0..4e8832597 100644 --- a/MapboxDirections/MBRoute.swift +++ b/MapboxDirections/MBRoute.swift @@ -9,8 +9,8 @@ import Polyline open class Route: DirectionsResult { // MARK: Creating a Route - @objc internal init(routeOptions: RouteOptions, legs: [RouteLeg], distance: CLLocationDistance, expectedTravelTime: TimeInterval, coordinates: [CLLocationCoordinate2D]?, speechLocale: Locale?) { - super.init(options: routeOptions, legs: legs, distance: distance, expectedTravelTime: expectedTravelTime, coordinates: coordinates, speechLocale: speechLocale) + @objc internal override init(legs: [RouteLeg], distance: CLLocationDistance, expectedTravelTime: TimeInterval, coordinates: [CLLocationCoordinate2D]?, speechLocale: Locale?, options: DirectionsOptions) { + super.init(legs: legs, distance: distance, expectedTravelTime: expectedTravelTime, coordinates: coordinates, speechLocale: speechLocale, options: options) } /** @@ -23,32 +23,24 @@ open class Route: DirectionsResult { - parameter routeOptions: The `RouteOptions` used to create the request. */ @objc(initWithJSON:waypoints:routeOptions:) - public init(json: [String: Any], waypoints: [Waypoint], routeOptions: RouteOptions) { + public init(json: [String: Any], waypoints: [Waypoint], options: RouteOptions) { // Associate each leg JSON with a source and destination. The sequence of destinations is offset by one from the sequence of sources. let legInfo = zip(zip(waypoints.prefix(upTo: waypoints.endIndex - 1), waypoints.suffix(from: 1)), json["legs"] as? [JSONDictionary] ?? []) let legs = legInfo.map { (endpoints, json) -> RouteLeg in - RouteLeg(json: json, source: endpoints.0, destination: endpoints.1, profileIdentifier: routeOptions.profileIdentifier) + RouteLeg(json: json, source: endpoints.0, destination: endpoints.1, options: options) } let distance = json["distance"] as! Double let expectedTravelTime = json["duration"] as! Double - var coordinates: [CLLocationCoordinate2D]? - switch json["geometry"] { - case let geometry as JSONDictionary: - coordinates = CLLocationCoordinate2D.coordinates(geoJSON: geometry) - case let geometry as String: - coordinates = decodePolyline(geometry, precision: 1e5)! - default: - coordinates = nil - } + let coordinates = options.shapeFormat.coordinates(from: json["geometry"]) var speechLocale: Locale? if let locale = json["voiceLocale"] as? String { speechLocale = Locale(identifier: locale) } - super.init(options: routeOptions, legs: legs, distance: distance, expectedTravelTime: expectedTravelTime, coordinates: coordinates, speechLocale: speechLocale) + super.init(legs: legs, distance: distance, expectedTravelTime: expectedTravelTime, coordinates: coordinates, speechLocale: speechLocale, options: options) } public var routeOptions: RouteOptions { @@ -63,8 +55,8 @@ open class Route: DirectionsResult { // MARK: Support for Directions API v4 internal class RouteV4: Route { - convenience override init(json: JSONDictionary, waypoints: [Waypoint], routeOptions: RouteOptions) { - let leg = RouteLegV4(json: json, source: waypoints.first!, destination: waypoints.last!, profileIdentifier: routeOptions.profileIdentifier) + convenience override init(json: JSONDictionary, waypoints: [Waypoint], options: RouteOptions) { + let leg = RouteLegV4(json: json, source: waypoints.first!, destination: waypoints.last!, options: options) let distance = json["distance"] as! Double let expectedTravelTime = json["duration"] as! Double @@ -78,6 +70,6 @@ internal class RouteV4: Route { coordinates = nil } - self.init(routeOptions: routeOptions, legs: [leg], distance: distance, expectedTravelTime: expectedTravelTime, coordinates: coordinates, speechLocale: nil) + self.init(legs: [leg], distance: distance, expectedTravelTime: expectedTravelTime, coordinates: coordinates, speechLocale: nil, options: options) } } diff --git a/MapboxDirections/MBRouteLeg.swift b/MapboxDirections/MBRouteLeg.swift index 026c7188d..14001c4ef 100644 --- a/MapboxDirections/MBRouteLeg.swift +++ b/MapboxDirections/MBRouteLeg.swift @@ -10,10 +10,10 @@ open class RouteLeg: NSObject, NSSecureCoding { // MARK: Creating a Leg - @objc internal init(steps: [RouteStep], json: JSONDictionary, source: Waypoint, destination: Waypoint, profileIdentifier: MBDirectionsProfileIdentifier) { + @objc internal init(steps: [RouteStep], json: JSONDictionary, source: Waypoint, destination: Waypoint, options: RouteOptions) { self.source = source self.destination = destination - self.profileIdentifier = profileIdentifier + self.profileIdentifier = options.profileIdentifier self.steps = steps distance = json["distance"] as! Double expectedTravelTime = json["duration"] as! Double @@ -57,11 +57,10 @@ open class RouteLeg: NSObject, NSSecureCoding { - parameter destination: The waypoint at the end of the leg. - parameter profileIdentifier: The profile identifier used to request the routes. */ - @objc(initWithJSON:source:destination:profileIdentifier:) - public convenience init(json: [String: Any], source: Waypoint, destination: Waypoint, profileIdentifier: MBDirectionsProfileIdentifier) { - let steps = (json["steps"] as? [JSONDictionary] ?? []).map { RouteStep(json: $0) } - - self.init(steps: steps, json: json, source: source, destination: destination, profileIdentifier: profileIdentifier) + @objc(initWithJSON:source:destination:options:) + public convenience init(json: [String: Any], source: Waypoint, destination: Waypoint, options: RouteOptions) { + let steps = (json["steps"] as? [JSONDictionary] ?? []).map { RouteStep(json: $0, options: options) } + self.init(steps: steps, json: json, source: source, destination: destination, options: options) } public required init?(coder decoder: NSCoder) { @@ -224,8 +223,8 @@ open class RouteLeg: NSObject, NSSecureCoding { // MARK: Support for Directions API v4 internal class RouteLegV4: RouteLeg { - internal convenience init(json: JSONDictionary, source: Waypoint, destination: Waypoint, profileIdentifier: MBDirectionsProfileIdentifier) { + internal convenience init(json: JSONDictionary, source: Waypoint, destination: Waypoint, options: RouteOptions) { let steps = (json["steps"] as? [JSONDictionary] ?? []).map { RouteStepV4(json: $0) } - self.init(steps: steps, json: json, source: source, destination: destination, profileIdentifier: profileIdentifier) + self.init(steps: steps, json: json, source: source, destination: destination, options: options) } } diff --git a/MapboxDirections/MBRouteOptions.swift b/MapboxDirections/MBRouteOptions.swift index 777ea3cda..0d3a65b9d 100644 --- a/MapboxDirections/MBRouteOptions.swift +++ b/MapboxDirections/MBRouteOptions.swift @@ -167,7 +167,7 @@ open class RouteOptions: DirectionsOptions { let waypoints = namedWaypoints ?? self.waypoints let routes = (json["routes"] as? [JSONDictionary])?.map { - Route(json: $0, waypoints: waypoints, routeOptions: self) + Route(json: $0, waypoints: waypoints, options: self) } return (waypoints, routes) } @@ -253,7 +253,7 @@ open class RouteOptionsV4: RouteOptions { let intermediateWaypoints = (json["waypoints"] as! [JSONDictionary]).compactMap { Waypoint(geoJSON: $0) } let waypoints = [sourceWaypoint] + intermediateWaypoints + [destinationWaypoint] let routes = (json["routes"] as? [JSONDictionary])?.map { - RouteV4(json: $0, waypoints: waypoints, routeOptions: self) + RouteV4(json: $0, waypoints: waypoints, options: self) } return (waypoints, routes) } diff --git a/MapboxDirections/MBRouteStep.swift b/MapboxDirections/MBRouteStep.swift index b55c27d73..b0ce36155 100644 --- a/MapboxDirections/MBRouteStep.swift +++ b/MapboxDirections/MBRouteStep.swift @@ -620,8 +620,8 @@ open class RouteStep: NSObject, NSSecureCoding { - parameter json: A JSON object that conforms to the [route step](https://www.mapbox.com/api-documentation/#routestep-object) format described in the Directions API documentation. */ - @objc(initWithJSON:) - public convenience init(json: [String: Any]) { + @objc(initWithJSON:options:) + public convenience init(json: [String: Any], options: RouteOptions) { let maneuver = json["maneuver"] as! JSONDictionary let finalHeading = maneuver["bearing_after"] as? Double let maneuverType = ManeuverType(description: maneuver["type"] as? String ?? "") ?? .none @@ -631,15 +631,7 @@ open class RouteStep: NSObject, NSSecureCoding { let name = json["name"] as! String - var coordinates: [CLLocationCoordinate2D]? - switch json["geometry"] { - case let geometry as JSONDictionary: - coordinates = CLLocationCoordinate2D.coordinates(geoJSON: geometry) - case let geometry as String: - coordinates = decodePolyline(geometry, precision: 1e5)! - default: - coordinates = nil - } + let coordinates = options.shapeFormat.coordinates(from: json["geometry"]) self.init(finalHeading: finalHeading, maneuverType: maneuverType, maneuverDirection: maneuverDirection, drivingSide: drivingSide, maneuverLocation: maneuverLocation, name: name, coordinates: coordinates, json: json) } diff --git a/MapboxDirections/Match/MBMatch.swift b/MapboxDirections/Match/MBMatch.swift index 2bd123172..079f2b9ac 100644 --- a/MapboxDirections/Match/MBMatch.swift +++ b/MapboxDirections/Match/MBMatch.swift @@ -12,7 +12,7 @@ open class Match: DirectionsResult { self.confidence = confidence self.tracepoints = tracepoints self.waypointIndices = waypointIndices - super.init(options: matchOptions, legs: legs, distance: distance, expectedTravelTime: expectedTravelTime, coordinates: coordinates, speechLocale: speechLocale) + super.init(legs: legs, distance: distance, expectedTravelTime: expectedTravelTime, coordinates: coordinates, speechLocale: speechLocale, options: matchOptions) } /** @@ -27,21 +27,13 @@ open class Match: DirectionsResult { let legInfo = zip(zip(tracepoints.prefix(upTo: tracepoints.endIndex - 1), tracepoints.suffix(from: 1)), json["legs"] as? [JSONDictionary] ?? []) let legs = legInfo.map { (endpoints, json) -> RouteLeg in - RouteLeg(json: json, source: endpoints.0, destination: endpoints.1, profileIdentifier: matchOptions.profileIdentifier) + return RouteLeg(json: json, source: endpoints.0, destination: endpoints.1, options: RouteOptions(matchOptions: matchOptions)) } let distance = json["distance"] as! Double let expectedTravelTime = json["duration"] as! Double - var coordinates: [CLLocationCoordinate2D]? - switch json["geometry"] { - case let geometry as JSONDictionary: - coordinates = CLLocationCoordinate2D.coordinates(geoJSON: geometry) - case let geometry as String: - coordinates = decodePolyline(geometry, precision: 1e5)! - default: - coordinates = nil - } + let coordinates = matchOptions.shapeFormat.coordinates(from: json["geometry"]) let confidence = (json["confidence"] as! NSNumber).floatValue diff --git a/MapboxDirections/Match/MBMatchOptions.swift b/MapboxDirections/Match/MBMatchOptions.swift index 0ab05ed1b..62b822526 100644 --- a/MapboxDirections/Match/MBMatchOptions.swift +++ b/MapboxDirections/Match/MBMatchOptions.swift @@ -158,7 +158,7 @@ open class MatchOptions: DirectionsOptions { } let routes = (json["matchings"] as? [JSONDictionary])?.map { - Route(json: $0, waypoints: filteredWaypoints ?? waypoints, routeOptions: opts) + Route(json: $0, waypoints: filteredWaypoints ?? waypoints, options: opts) } return (waypoints, routes) diff --git a/MapboxDirectionsTests/V5Tests.swift b/MapboxDirectionsTests/V5Tests.swift index 30001c1c4..9317452db 100644 --- a/MapboxDirectionsTests/V5Tests.swift +++ b/MapboxDirectionsTests/V5Tests.swift @@ -1,5 +1,6 @@ import XCTest import OHHTTPStubs +import Polyline @testable import MapboxDirections class V5Tests: XCTestCase { @@ -8,7 +9,9 @@ class V5Tests: XCTestCase { super.tearDown() } - func test(shapeFormat: RouteShapeFormat) { + typealias JSONTransformer = ((JSONDictionary) -> JSONDictionary) + + func test(shapeFormat: RouteShapeFormat, transformer: JSONTransformer? = nil, filePath: String? = nil) { let expectation = self.expectation(description: "calculating directions should return results") let queryParams: [String: String?] = [ @@ -22,8 +25,12 @@ class V5Tests: XCTestCase { stub(condition: isHost("api.mapbox.com") && isPath("/directions/v5/mapbox/driving/-122.42,37.78;-77.03,38.91.json") && containsQueryParams(queryParams)) { _ in - let path = Bundle(for: type(of: self)).path(forResource: "v5_driving_dc_\(shapeFormat)", ofType: "json") - return OHHTTPStubsResponse(fileAtPath: path!, statusCode: 200, headers: ["Content-Type": "application/json"]) + let path = Bundle(for: type(of: self)).path(forResource: filePath ?? "v5_driving_dc_\(shapeFormat)", ofType: "json") + let filePath = URL(fileURLWithPath: path!) + let data = try! Data(contentsOf: filePath, options: []) + let jsonObject = try! JSONSerialization.jsonObject(with: data, options: []) + let transformedData = transformer?(jsonObject as! JSONDictionary) ?? jsonObject + return OHHTTPStubsResponse(jsonObject: transformedData, statusCode: 200, headers: ["Content-Type": "application/json"]) } let options = RouteOptions(coordinates: [ @@ -148,4 +155,44 @@ class V5Tests: XCTestCase { XCTAssertEqual(String(describing: RouteShapeFormat.polyline), "polyline") test(shapeFormat: .polyline) } + + func testPolyline6() { + XCTAssertEqual(String(describing: RouteShapeFormat.polyline6), "polyline6") + + // Transform polyline5 to polyline6 + let transformer: JSONTransformer = { json in + var transformed = json + var route = (transformed["routes"] as! [JSONDictionary])[0] + let polyline = route["geometry"] as! String + + let decodedCoordinates: [CLLocationCoordinate2D] = decodePolyline(polyline, precision: 1e5)! + route["geometry"] = Polyline(coordinates: decodedCoordinates, levels: nil, precision: 1e6).encodedPolyline + + let legs = route["legs"] as! [JSONDictionary] + var newLegs = [JSONDictionary]() + for var leg in legs { + let steps = leg["steps"] as! [JSONDictionary] + + var newSteps = [JSONDictionary]() + for var step in steps { + let geometry = step["geometry"] as! String + let coords: [CLLocationCoordinate2D] = decodePolyline(geometry, precision: 1e5)! + step["geometry"] = Polyline(coordinates: coords, precision: 1e6).encodedPolyline + newSteps.append(step) + } + + leg["steps"] = newSteps + newLegs.append(leg) + } + + route["legs"] = newLegs + + let secondRoute = (json["routes"] as! [JSONDictionary])[1] + transformed["routes"] = [route, secondRoute] + + return transformed + } + + test(shapeFormat: .polyline6, transformer: transformer, filePath: "v5_driving_dc_polyline") + } }