Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp waypoint types #388

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
* 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))
* `RouteLeg.source` and `RouteLeg.destination` are now optional. They can be `nil` when the `RouteLeg` object is decoded individually from JSON. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382))
* The `
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finish this sentence.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some context would be helpful to do it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I had been going through the PR looking for breaking changes to call out but got distracted. If you see anything else that has changed in the public API besides the first item, feel free to add it here; otherwise, we can remove this item.

* 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,11 +31,8 @@

* 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))
* `RouteLeg.source` and `RouteLeg.destination` are now optional. They can be `nil` when the `RouteLeg` object is decoded individually from JSON. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382))
* Removed `TransportType.none`, `ManeuverType.none`, and `ManeuverDirection.none`. Unrecognized `TransportType` and `ManeuverDirection` values now raise decoding errors. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382))
* `RouteStep.maneuverType` is now optional. ([#382](https://github.com/mapbox/MapboxDirections.swift/pull/382))
* Renamed the `Tracepoint.alternateCount` property to `Tracepoint.countOfAlternatives`. ([#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
5 changes: 2 additions & 3 deletions Sources/MapboxDirections/DirectionsResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = endpoints.0
leg.destination = endpoints.1
Expand Down
17 changes: 9 additions & 8 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 @@ -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
}
Expand All @@ -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.
Udumft marked this conversation as resolved.
Show resolved Hide resolved
route.legSeparators = waypoints
}
self.routes = routes
} else {
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can someone point me to some kind of a doc or explanation about tracepoints in the response and matchings_index, waypoint_index too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here’s the Map Matching API documentation, which is more relevant than before, now that this PR hews more closely to the Map Matching API response format.

It isn’t necessary to fix #386 as part of this PR, but I think understanding that issue helped me understand these properties better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to that doc, our Match and Tracepoint objects do not reflect the structure described. Is there any special reason why? Or that's what I can safely take care of?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Map Matching API response format avoids repetition to minimize data transfer, and it’s optimized for a file format (JSON) that has no concept of pointer references. As a result, the response is rife with parallel structures and indices. At the same time, the Directions API response is influenced by the OSRM Router service’s heavily nested response format (maneuvers inside steps inside legs inside routes). As part of developing the “bring your own route” workflow, the Map Matching API adopted most of the Directions API’s format so that the client could duck-type matches as routes. So Map Matching API responses end up with a mix of indexable parallel structures in some places and repetitive, nested structures in other places.

Originally, this library only supported the Directions API, so it adopted that API’s highly nested structure. We also avoided index-based structures, even where the Directions API had them, because of potential out-of-bounds issues and other misuse. Pointer references are more convenient to work with than indices and don’t result in excessive memory consumption. It’s also much more convenient for objects to be self-contained: if the application only cares about the match, it can pass around or archive an individual Match object instead of the entire MatchResponse plus a match index.

In the long term, I think we will want to go all-in on indexed parallel structures, once a future version of the Directions API adopts Valhalla’s more consistently indexed-based structure. When we do that, things like the RouteLeg.segmentRangesByStep property added in #250 will be important for avoiding out-of-bounds issues.

But in the short term, we should keep pushing things like tracepoints down into the individual matches so that the matches are more self-contained.

This is a good example of how I think we want to be deliberate about where this library’s types resemble or differ from the API response. The API response format has a lot of warts, having bent over backwards to preserve backwards compatibility, so we only want to copy what make sense. We prioritized refactoring the waypoint/tracepoint types because they were too different from the corresponding API response objects, leading to some questionable patterns like treating RouteOptions.waypoints like an inout parameter. But I tried to avoid churning other classes were we differed in more minor or more deliberate ways.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so due to some reasonable matters like API structure and JSON format we may not follow the doc strictly but instead "adjust" info we provide to users to have some adequate form.
At the same time, to correctly "keep pushing things like tracepoints down into the individual matches" we need to follow the doc and decode Tracepoint's matchings_index and waypoint_index... I see that previous implementation just copied all tracepoints to all matches, which seem to work only while there is a single Match provided (unless it was removed here).
We may just restore this behavior, but why not improve it to make it "more safe".

match.tracepoints = tracepoints
}
}
}
Expand Down
60 changes: 32 additions & 28 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 countOfAlternatives: Int

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
}
}

extension Match.Tracepoint: Codable {
private enum CodingKeys: String, CodingKey {
case coordinate = "location"
case correction = "distance"
case countOfAlternatives = "alternatives_count"
}

init(coordinate: CLLocationCoordinate2D, countOfAlternatives: Int, name: String?) {
self.countOfAlternatives = countOfAlternatives
super.init(coordinate: coordinate, name: name)
}

required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
countOfAlternatives = try container.decode(Int.self, forKey: .countOfAlternatives)
try super.init(from: decoder)
}

public override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(countOfAlternatives, forKey: .countOfAlternatives)
try super.encode(to: encoder)
}
}

extension Tracepoint { //Equatable
public static func ==(lhs: Tracepoint, rhs: Tracepoint) -> Bool {
let superEquals = (lhs as Waypoint == rhs as Waypoint)
return superEquals && lhs.countOfAlternatives == rhs.countOfAlternatives
extension Match.Tracepoint: CustomStringConvertible {
public var description: String {
return "<latitude: \(coordinate.latitude); longitude: \(coordinate.longitude)>"
}
}
9 changes: 5 additions & 4 deletions Sources/MapboxDirections/RouteLeg.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,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)
Expand Down Expand Up @@ -85,7 +86,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.
Expand All @@ -94,7 +95,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

Expand Down
17 changes: 9 additions & 8 deletions Sources/MapboxDirections/RouteResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ struct RouteResponse {
var error: String?
let uuid: String?
let routes: [Route]?
let waypoints: [Waypoint]?
let waypoints: [Route.Waypoint]?
}

extension RouteResponse: Codable {
Expand All @@ -28,17 +28,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
}
Expand All @@ -48,8 +48,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
Expand Down
Loading