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

Revamp waypoint types #388

wants to merge 8 commits into from

Conversation

1ec5
Copy link
Contributor

@1ec5 1ec5 commented Nov 14, 2019

Replaced the Waypoint and Tracepoint classes with separate classes for requests and responses to better match the design of the Directions API and Map Matching API and avoid the messy situation of classes with multiple mutually exclusive properties.

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.

Also renamed the Tracepoint.alternateCount property to Tracepoint.countOfAlternatives and added the snapped distance to the DirectionsResult.Waypoint type.

Fixes #333 and fixes #564. Depends on #382.

/cc @mapbox/navigation-ios

@1ec5 1ec5 added backwards incompatible changes that break backwards compatibility of public API op-ex Refactoring, Tech Debt or any other operational excellence work. labels Nov 14, 2019
@1ec5 1ec5 added this to the v1.0 milestone Nov 14, 2019
@1ec5 1ec5 self-assigned this Nov 14, 2019
@1ec5
Copy link
Contributor Author

1ec5 commented Nov 14, 2019

Some tests are failing because the test fixtures need to updated. They were generated back before the Directions and Map Matching APIs started returning distance as part of each snapped waypoint.

@@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This PR continues to assume that options are passed in as a precondition for decoding, but it throws errors instead of force-unwrapping dictionary entries. Ideally, #382 would incorporate these changes to fix #228.

@1ec5
Copy link
Contributor Author

1ec5 commented Nov 23, 2019

A major problem with converting Waypoint to a struct is that a given waypoint appears in multiple places in the response. The developer would potentially expect that changing the name of one leg’s destination would affect the name of the next leg’s source. The solution would be to move RouteLeg.source to Route and, once #391 is implemented, make Response.waypoints a computed property based on Route.source and RouteLeg.destination.

Replaced the Waypoint and Tracepoint classes with separate classes for requests and responses.
@1ec5 1ec5 force-pushed the 1ec5-waypoint-struct branch from be7816b to b04d6a5 Compare December 16, 2019 22:11
@1ec5 1ec5 changed the base branch from jerrad/objc-delenda-est to master December 16, 2019 23:28
CHANGELOG.md Outdated
* 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.

Comment on lines 25 to 26
// 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".

@Udumft Udumft mentioned this pull request Apr 4, 2020
9 tasks
Victor Kononov added 3 commits April 4, 2020 16:12
# Conflicts:
#	CHANGELOG.md
#	Sources/MapboxDirections/Directions.swift
#	Sources/MapboxDirections/DirectionsResult.swift
#	Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift
#	Sources/MapboxDirections/MapMatching/MatchResponse.swift
#	Sources/MapboxDirections/RouteResponse.swift
#	Sources/MapboxDirections/Waypoint.swift
#	Tests/MapboxDirectionsTests/DirectionsTests.swift
#	Tests/MapboxDirectionsTests/MatchTests.swift
#	Tests/MapboxDirectionsTests/RoutableMatchTests.swift
#	Tests/MapboxDirectionsTests/WaypointTests.swift
*/
public var countOfAlternatives: Int

public var name: String? // ??? matching response otherwise won't be able to decode waypoints names.
Copy link
Contributor

@Udumft Udumft Apr 7, 2020

Choose a reason for hiding this comment

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

If we don't supply a name in a Tracepoint, Map Matching response decoding won't pass this data to RouteResponse and in it's turn resulting Waypoints won't have name 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.

A waypoint (or tracepoint) name coming from the API isn’t actually that meaningful. Normally it’s just the name of the street that the waypoint got snapped to. If the application supplied a waypoint name as part of the request, that name is worth preserving. In the past, we’d override the API response’s waypoint names with the request’s waypoint names, where available. Now it’s up to the application to hang onto the request and associate the request’s waypoints with the response’s waypoints in some cases.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, it's a bit misleading: "that name is worth preserving" and "it’s up to the application to hang onto the request and associate the request’s waypoints with the response’s waypoints" seems to be the opposite statements.
If we are to preserve waypoint name returned by the API (and apparently Route.Waypoint is), Tracepoint should have a name. Otherwise, Waypoints won't be decoded correctly as they depend on Tracepoint data: https://github.com/mapbox/mapbox-directions-swift/pull/388/files#diff-a2198eb412da79c9d8351c4e811b7dc6R63-R66

Comment on lines 30 to 32
case code // << do we need these?
case message // <<
case error // <<
Copy link
Contributor

Choose a reason for hiding this comment

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

These values are now decoded using ResponseDisposition, but It is only used internally while parsing the response and is not passed to the user. I assume this is what we want, and these keys inside RouteResponse are not needed?

Copy link
Contributor

@Udumft Udumft Apr 8, 2020

Choose a reason for hiding this comment

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

Comment still actual

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 you’re right, because #418 turns the three fields into an Error in the Result.

Comment on lines +132 to +142
let (decodedWaypoint, waypointInOptions) = pair
if decodedWaypoint == waypoints.last! ||
decodedWaypoint == waypoints.first! {
return decodedWaypoint
}

guard let index = optionsLegSeparators.firstIndex(of: waypointInOptions) else {
return nil
}
optionsLegSeparators.remove(at: index)
return decodedWaypoint
Copy link
Contributor

Choose a reason for hiding this comment

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

I hope I understood correctly how filtering should be applied. I decided to stick to options.legSeparators and filter out decoded waypoints that correspond it.

@@ -0,0 +1,31 @@
import Foundation

class MatchResponse: Codable {
Copy link
Contributor

Choose a reason for hiding this comment

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

MatchResponse seems removed in some other PR. I assume it means that that tracepoint filtering should occur in MapMatchingResponse instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the time this PR was originally written, MatchResponse interpreted Map Matching API responses as Map Matching API responses (MatchOptions→Match), whereas MapMatchingResponse interpreted Map Matching API responses as Directions API responses (MatchOptions→Route). It was confusing, so #406 eliminated MapMatchingResponse and renamed MatchResponse to MapMatchingResponse.

… matching response decoding to respect matchings index
Comment on lines 29 to 39
public var name: String?

/***
The index of the match object in matchings that the sub-trace was matched to.
*/
public var matchingIndex: Int = 0 // TODO: not sure about default value

/***
The index of the waypoint inside the matched route.
*/
public var waypointIndex: Int = 0 // TODO: not sure about default value
Copy link
Contributor

Choose a reason for hiding this comment

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

There were not originally in the doc, but looks like it is required to have.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These indices are parallel state that can get out of sync with the array of matches or waypoints. This starts to get into #386 territory. I think the filtering suggested in that issue could make these properties unnecessary.

Comment on lines 69 to 82
if let routes = routes {
let sorted = zip(filtered, waypoints!).sorted {
$0.0.waypointIndex < $1.0.waypointIndex
}
for (index, route) in routes.enumerated() {
let legSeparators = sorted.filter {
return $0.0.matchingIndex == index
}.map { $0.1 }

assert(legSeparators.count == route.legs.count + 1)

route.legSeparators = legSeparators
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This part is how I understand Map Matching Doc. The idea is to set leg separators explicitly via matchings_index and waypoint_index. Not sure if it is required though or will have any effect at all since routes and waypoints are passed to the user as public properties and waypoints are not modified in this code.
A help/review would be required here.

return $0.0.matchingIndex == index
}.map { $0.1 }

assert(legSeparators.count == route.legs.count + 1)
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume this is something we can expect to be guaranteed?

Copy link
Contributor Author

@1ec5 1ec5 Apr 9, 2020

Choose a reason for hiding this comment

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

There will be fewer leg separators if any of the waypoints in the request had separatesLegs equal to false. A waypoint that doesn’t separate legs is also known as a “silent waypoint” or “via point”: it influences the path of the route but doesn’t get called out specifically as a destination.

@Udumft
Copy link
Contributor

Udumft commented Apr 9, 2020

distance property added to Waypoint, Waypoint and Tracepoint classes replaced with structures. Did I miss anything else should be done here?

@Udumft Udumft marked this pull request as ready for review April 10, 2020 08:24
@Udumft Udumft assigned 1ec5 and unassigned 1ec5 Apr 10, 2020
@1ec5 1ec5 modified the milestones: v1.0.0, v0.31.0 (v0.40) Apr 14, 2020
@1ec5 1ec5 modified the milestones: v0.31.0, v1.0.0 Apr 24, 2020
@1ec5 1ec5 modified the milestones: v1.0.0, v2.0.0 Sep 2, 2020
@1ec5 1ec5 changed the base branch from master to main October 6, 2020 17:20
@truburt truburt modified the milestones: v2.0.0, v2.0.0 (GA) Mar 5, 2021
@1ec5 1ec5 modified the milestones: v2.0, v3.0 Oct 19, 2021
@S2Ler S2Ler marked this pull request as draft January 17, 2023 06:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backwards incompatible changes that break backwards compatibility of public API op-ex Refactoring, Tech Debt or any other operational excellence work.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

RouteOptions and Waypoint should be structs Add snapped distance property to Waypoint
3 participants