Skip to content

Commit

Permalink
Decode polyline6 (#281)
Browse files Browse the repository at this point in the history
* Decode polyline6

* Transform polyline5 to 6 in v5 test

* Add a blurb in the changelog

* Fix geometry decoding for match
  • Loading branch information
frederoni committed Jun 25, 2018
1 parent 3faf9f0 commit a8c9300
Show file tree
Hide file tree
Showing 11 changed files with 101 additions and 56 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -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"
22 changes: 22 additions & 0 deletions MapboxDirections/MBDirectionsOptions.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import Polyline

/**
A `RouteShapeFormat` indicates the format of a route or match shape in the raw HTTP response.
Expand Down Expand Up @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion MapboxDirections/MBDirectionsResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 9 additions & 17 deletions MapboxDirections/MBRoute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand All @@ -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 {
Expand All @@ -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

Expand All @@ -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)
}
}
17 changes: 8 additions & 9 deletions MapboxDirections/MBRouteLeg.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
}
4 changes: 2 additions & 2 deletions MapboxDirections/MBRouteOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
14 changes: 3 additions & 11 deletions MapboxDirections/MBRouteStep.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
14 changes: 3 additions & 11 deletions MapboxDirections/Match/MBMatch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion MapboxDirections/Match/MBMatchOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
53 changes: 50 additions & 3 deletions MapboxDirectionsTests/V5Tests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import XCTest
import OHHTTPStubs
import Polyline
@testable import MapboxDirections

class V5Tests: XCTestCase {
Expand All @@ -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?] = [
Expand All @@ -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: [
Expand Down Expand Up @@ -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")
}
}

0 comments on commit a8c9300

Please sign in to comment.