Skip to content

Commit

Permalink
Revamped waypoint types
Browse files Browse the repository at this point in the history
Replaced the Waypoint and Tracepoint classes with separate classes for requests and responses.
  • Loading branch information
1ec5 committed Nov 13, 2019
1 parent a1e3a74 commit ae081ab
Show file tree
Hide file tree
Showing 24 changed files with 487 additions and 353 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions Sources/MapboxDirections/Directions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
13 changes: 13 additions & 0 deletions Sources/MapboxDirections/DirectionsError.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Foundation

/**
An error that occurs when calculating directions.
*/
public enum DirectionsError: LocalizedError {
/**
The server returned an empty response.
Expand Down Expand Up @@ -169,3 +172,13 @@ extension DirectionsError: Equatable {
}
}
}

/**
An error that occurs when encoding or decoding a type defined by the MapboxDirections framework.
*/
public enum DirectionsCodingError: Error {
/**
Decoding this type requires the `Decoder.userInfo` dictionary to contain the `CodingUserInfoKey.options` key.
*/
case missingOptions
}
2 changes: 2 additions & 0 deletions Sources/MapboxDirections/DirectionsOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public enum RouteShapeFormat: String, Codable {
This format is an order of magnitude more precise than `polyline`.
*/
case polyline6

static let `default` = RouteShapeFormat.polyline
}

/**
Expand Down
15 changes: 8 additions & 7 deletions Sources/MapboxDirections/DirectionsResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ open class DirectionsResult: Codable {
distance = try container.decode(CLLocationDistance.self, forKey: .distance)
expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime)

_directionsOptions = decoder.userInfo[.options] as! DirectionsOptions
guard let directionsOptions = decoder.userInfo[.options] as? DirectionsOptions else {
throw DirectionsCodingError.missingOptions
}
_directionsOptions = directionsOptions

if let polyLineString = try container.decodeIfPresent(PolyLineString.self, forKey: .shape) {
shape = try LineString(polyLineString: polyLineString)
Expand All @@ -38,13 +41,11 @@ 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 = endpoints.0
leg.destination = endpoints.1
(leg.source, leg.destination) = endpoints
}

accessToken = try container.decodeIfPresent(String.self, forKey: .accessToken)
Expand All @@ -53,7 +54,7 @@ open class DirectionsResult: Codable {

do {
speechLocale = try container.decodeIfPresent(Locale.self, forKey: .speechLocale)
} catch let DecodingError.typeMismatch(mismatchedType, context){
} catch let DecodingError.typeMismatch(mismatchedType, context) {
guard mismatchedType == Dictionary<String, Any>.self else {
throw DecodingError.typeMismatch(mismatchedType, context)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/MapboxDirections/Extensions/Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ extension PolyLineString: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let options = decoder.userInfo[.options] as? DirectionsOptions
switch options?.shapeFormat ?? .polyline {
switch options?.shapeFormat ?? RouteShapeFormat.default {
case .geoJSON:
self = .lineString(try container.decode(LineString.self))
case .polyline, .polyline6:
Expand Down
23 changes: 12 additions & 11 deletions Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -12,22 +12,23 @@ class MapMatchingResponse: Decodable {
}

public required init(from decoder: Decoder) throws {
let options = decoder.userInfo[.options] as? MatchOptions
let container = try decoder.container(keyedBy: CodingKeys.self)
code = try container.decode(String.self, forKey: .code)
let waypoints = try container.decode([Waypoint].self, forKey: .tracepoints)
routes = try container.decodeIfPresent([Route].self, forKey: .matches)

if let optionsPoints = options?.waypoints {
let updatedPoints = zip(waypoints, optionsPoints).map { (arg) -> Waypoint in
let (local, api) = arg

return Waypoint(coordinate: api.coordinate, coordinateAccuracy: local.coordinateAccuracy, name: local.name ?? api.name)
// Decode waypoints from the response and update their names according to the waypoints from DirectionsOptions.waypoints.
let waypoints = 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.
self.waypoints = zip(waypoints, options.waypoints).map { (waypoint, waypointFromOptions) in
var waypoint = waypoint
if waypointFromOptions.separatesLegs, let name = waypointFromOptions.name?.nonEmptyString {
waypoint.name = name
}
return waypoint
}
self.waypoints = updatedPoints
} else {
self.waypoints = waypoints
}

routes = try container.decodeIfPresent([Route].self, forKey: .matches)
}
}
14 changes: 13 additions & 1 deletion Sources/MapboxDirections/MapMatching/Match.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,23 @@ open class Match: DirectionsResult {
case tracepoints
case matchOptions
}

/**
Creates a match from a decoder.

- precondition: If the decoder is decoding JSON data from an API response, the `Decoder.userInfo` dictionary must contain a `MatchOptions` object in the `CodingUserInfoKey.options` key. If it does not, a `DirectionsCodingError.missingOptions` error is thrown.
- parameter decoder: The decoder of JSON-formatted API response data or a previously encoded `Match` object.
*/
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
confidence = try container.decode(Float.self, forKey: .confidence)
tracepoints = try container.decodeIfPresent([Tracepoint?].self, forKey: .tracepoints) ?? []
matchOptions = try container.decodeIfPresent(MatchOptions.self, forKey: .matchOptions) ?? decoder.userInfo[.options] as! MatchOptions
if let matchOptions = try container.decodeIfPresent(MatchOptions.self, forKey: .matchOptions)
?? decoder.userInfo[.options] as? MatchOptions {
self.matchOptions = matchOptions
} else {
throw DirectionsCodingError.missingOptions
}
try super.init(from: decoder)
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/MapboxDirections/MapMatching/MatchOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ open class MatchOptions: DirectionsOptions {
self.init(waypoints: waypoints, profileIdentifier: profileIdentifier)
}

public required init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier?) {
public required init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier? = nil) {
super.init(waypoints: waypoints, profileIdentifier: profileIdentifier)
}

Expand Down
12 changes: 7 additions & 5 deletions Sources/MapboxDirections/MapMatching/MatchResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
}
Expand Down
62 changes: 33 additions & 29 deletions Sources/MapboxDirections/MapMatching/Tracepoint.swift
Original file line number Diff line number Diff line change
@@ -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 "<latitude: \(coordinate.latitude); longitude: \(coordinate.longitude)>"
}
}
20 changes: 13 additions & 7 deletions Sources/MapboxDirections/Route.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import Polyline

/**
A `Route` object defines a single route that the user can follow to visit a series of waypoints in order. The route object includes information about the route, such as its distance and expected travel time. Depending on the criteria used to calculate the route, the route object may also include detailed turn-by-turn instructions.

Typically, you do not create instances of this class directly. Instead, you receive route objects when you request directions using the `Directions.calculate(_:completionHandler:)` method. However, if you use the `Directions.url(forCalculating:)` method instead, you can pass the results of the HTTP request into this class’s initializer.
*/
private enum CodingKeys: String, CodingKey {
case routeOptions
}

open class Route: DirectionsResult {
private enum CodingKeys: String, CodingKey {
case routeOptions
}

/**
Creates a route from a decoder.

- precondition: If the decoder is decoding JSON data from an API response, the `Decoder.userInfo` dictionary must contain a `RouteOptions` or `MatchOptions` object in the `CodingUserInfoKey.options` key. If it does not, a `DirectionsCodingError.missingOptions` error is thrown.
- parameter decoder: The decoder of JSON-formatted API response data or a previously encoded `Route` object.
*/
public required init(from decoder: Decoder) throws {
if let matchOptions = decoder.userInfo[.options] as? MatchOptions {
routeOptions = RouteOptions(matchOptions: matchOptions)
} else if let routeOptions = decoder.userInfo[.options] as? RouteOptions {
self.routeOptions = routeOptions
} else {
routeOptions = decoder.userInfo[.options] as! RouteOptions
throw DirectionsCodingError.missingOptions
}

try super.init(from: decoder)
Expand Down
Loading

0 comments on commit ae081ab

Please sign in to comment.