-
Notifications
You must be signed in to change notification settings - Fork 319
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
navigation specific route options builder #5427
Conversation
...igation-core/src/main/java/com/mapbox/navigation/core/routeoptions/NavRouteOptionsBuilder.kt
Outdated
Show resolved
Hide resolved
examples/src/main/java/com/mapbox/navigation/examples/core/MapboxNavigationActivity.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The proposal looks good to me in general - have an opinionated, higher-level API that allows us to build routes while taking advantage of all the onboard features (matched location, bearing, z-level, etc) without developers having to dive deep into documentation to build a good route request manually.
The problem that might arise is maintenance, we might get more and more request to expose features through this NavRouteOptionsBuilder
that are already available via RouteOptionsBuilder
from mapbox-java
. We need to have a clear set of defined rules for what goes into this higher level API and what doesn't.
Then is the question of compatibility, we also need to define a clear path for developers to start with higher-level API and transition to RouteOptiosnBuilder
to tweak a parameter that wasn't exposed via NavRouteOptionsBuilder
, maybe additional extension that works directly on RouteOptiosnBuilder
or something along these lines.
And lastly I'd like to discuss platform parity - are there any measures in the iOS SDK that would apply the runtime properties (like current location, bearing, z-level, etc) without developers having to do that themselves when building a route request? cc @1ec5 @Udumft
...igation-core/src/main/java/com/mapbox/navigation/core/routeoptions/NavRouteOptionsBuilder.kt
Outdated
Show resolved
Hide resolved
...ion-core/src/test/java/com/mapbox/navigation/core/routeoptions/NavRouteOptionsBuilderTest.kt
Outdated
Show resolved
Hide resolved
...ation-core/src/main/java/com/mapbox/navigation/core/routeoptions/builder/LocationProvider.kt
Outdated
Show resolved
Hide resolved
...ation-core/src/main/java/com/mapbox/navigation/core/routeoptions/builder/LocationProvider.kt
Outdated
Show resolved
Hide resolved
27eb977
to
368928b
Compare
is the PR ready for review? |
yes. |
d227288
to
239d217
Compare
val zLevel: Int? | ||
) | ||
|
||
internal class LocationFromTripSessionProvider( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class and LocationProvider
interface can be replaced by an extension method on TripSessionLocationProvider
internal suspend fun TripSessionLocationProvider.currentLocation() = suspendCancellableCoroutine<CurrentLocation> { continuation ->
// return immediately if locationMatcherResult has already been set
locationMatcherResult?.also {
continuation.resume(it.toCurrentLocation())
return@suspendCancellableCoroutine
}
// subscribe for the next locationMatcherResult
val observer = object : LocationObserver {
override fun onNewRawLocation(rawLocation: Location) = Unit
override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
unregisterLocationObserver(this)
continuation.resume(locationMatcherResult.toCurrentLocation())
}
}
registerLocationObserver(observer)
continuation.invokeOnCancellation {
unregisterLocationObserver(observer)
}
}
private fun LocationMatcherResult.toCurrentLocation() = CurrentLocation(
point = enhancedLocation.toPoint(),
bearing = enhancedLocation.bearing.toDouble(),
zLevel = zLevel
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you. I agree it can. I'm not sure I understand why an extension function is better than a wrapper class. Please share your thoughts about exception function vs wrapper class .
Not sure if the provided code is expected to be fully functional but it will crash on unregisterLocationObserver(this)
. You can reed and share your opinion about the crash here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please share your thoughts about exception function vs wrapper class .
There is nothing wrong with wrapper classes. They are extremely useful in languages that doesn't provide any other mechanism to add new functionality to an existing class or when we need to store some state.
In this specific case, we can extend TripSessionLocationProvider functionality by utilizing Kotlin extensions.
Not sure if the provided code is expected to be fully functional but it will crash on unregisterLocationObserver(this).
I am assuming the crash is caused by ConcurrentModificationException. We should create new ticket for patching unregisterLocationObserver()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I replace wrapper by an extension function then NavRouteOptionsBuilder
will start to depend on the TripSessionLocationProvider
directly. But I would prefer NavRouteOptionsBuilder
to work with an interface LocationProvider
which can be implemented by other classes.
fun profileCycling( | ||
cyclingSpecificSetup: CyclingSpecificSetup.() -> Unit = { } | ||
): RouteOptionsBuilderWithWaypoints | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mixing builder and configuration blocks results in an inconsistent api usage which IMHO looks a bit messy.
val routeOptions = mapboxNavigation.buildRouteOptions { builder ->
builder
.fromCurrentLocation() // builder
.toDestination(destination) // builder
.profileDriving {
maxWidth(1.42) // configuration block
maxHeight(2.2) // configuration block
}
}
Maybe we should stick to either builders or configuration blocks, not both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean, the API would look less messy if we used one of two approaches. I don't want to get rig of the step builder for now, because it makes the API safe. Anyway let's discuss the step bulder in the different answer, maybe we will decide to get rid of it, who knows 🙂
@tomaszrybakiewicz , would you like the API more if I replaced configuration block by builder?
val routeOptions = mapboxNavigation.buildRouteOptions { builder ->
builder
.fromCurrentLocation()
.toDestination(destination)
.profileDriving { drivingProfileConfig ->
drivingProfileConfig
.maxWidth(1.42)
.maxHeight(2.2)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still looks awkward. But at least it's consistent with buildRouteOptions
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for feedback. I understand what you feel and I tend to agree with you. I will think about other way to structure API. 👍
But I think that current API is okay for the first iteration. As the feature is experimental, I will be able to change API in the future.
): WaypointsInProgressBuilder | ||
|
||
fun fromCurrentLocation(): WaypointsInProgressBuilder | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this API design introduces a lot of rigidity. Builders are meant to be flexible and allow developers to call any setter method in any order before calling build()
.
Building route options like will work:
val routeOptions = mapboxNavigation.buildRouteOptions { builder ->
builder
.fromCurrentLocation()
.toDestination(destination)
.profileDriving {
maxWidth(1.42)
maxHeight(2.2)
}
}
But like this won't work:
val routeOptions = mapboxNavigation.buildRouteOptions { builder ->
builder
.profileDriving {
maxWidth(1.42)
maxHeight(2.2)
}
.toDestination(destination)
.fromCurrentLocation()
}
Also it won't allow developers to re-use of RouteOptions Builder between calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Builders are meant to be flexible and allow developers to call any setter method in any order before calling build()
The builder pattern is just a pattern, i.e. I think it meant to be as the majority of developers think it is 🙂
And I've seen recently a description of a step builder pattern in AutoValue docs, which I think is quite popular. So I can't agree with "Builders are meant to be flexible" for now.
I agree that the "step builder" pattern doesn't allow re-use the builder between calls, because it changes the interface on each call.
I see a tradeoff here. From one side of the tradeoof is flexibility, from the other side is safety. The "step builder" pattern ensures that all the required fields are filled in. With current approach we get safety sacrificing flexibility.
@tomaszrybakiewicz, what do you think about it? Do you think it better to pick flexibility over safety?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both are not mutually exclusive. You can offer a flexible API to developers and enforce its safety on runtime via assertions/preconditions. Lots of libraries does that. Take firebase-android-sdk for example. Even though they use @NonNull
annotations and explicitly mention argument non nullability requirement in documentation, they still use preconditions to validate assumptions and enforce correct api usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This discussion is very similar to the discussion above.
I will try different API formats before making this API not experimental, added note for myself and I will create an issue once PR is merged. But I think results of the first iteration is good enough to be merged.
Anyway feel free to say that the API is too bad to be merged. I will try to change it in the context of this PR if you think so.
suspend fun buildRouteOptions( | ||
optionsBlock: (NoWaypointsOptionsBuilder) -> RouteOptionsBuilderWithWaypoints | ||
): RouteOptions { | ||
tripSession.start(false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should be starting trip session here.
Doings so will introduce a side effect that will lead to unexpected behaviour and very hard to detect bugs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tomaszrybakiewicz , what do you think we should do? Throw an exceptions if trip session hasn't been started yet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @tomaszrybakiewicz. As things stand, the session would never be started by itself, it's always initiated by developers, especially since it has billing consequences, next to functional consequences like starting all the observers.
Not only that but also the fact that we are not stopping the session that's started here, nor are we monitoring if it's been changed by developer which could result in the buildRouteOptions
never returning because it would get stuck in LocationFromTripSessionProvider
.
Let's start small and require the trip session to be started before someone calls buildRouteOptions
or the proposed above MapboxNavigation#requestRoutes(NavRouteOptionsBuilder)
. We'll still need to register a TripSessionStateObserver
in the LocationFromTripSessionProvider
to abort if the session was stopped before we got a location sample.
This will lest us experiment with the ergonomics without being blocked. As a next step, we can consider looking for a way to either bypass or initialize the session more ergonomically/silently when trying to obtain the location sample.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I just throw an exception if trip session isn't started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll still need to register a TripSessionStateObserver in the LocationFromTripSessionProvider to abort if the session was stopped before we got a location sample.
Added timeout for waining for location update. it's easier and handles more cases
return this | ||
} | ||
|
||
internal suspend fun build(): RouteOptions { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Builder a suspend method? I understand it's because of locationProvider.getCurrentLocation()
but I don't think builders should ever be suspend methods.
To eliminate the need to call getCurrentLocation()
, we could set a placeholder object that will tell MapboxNavigation to use current location when requestRoutes()
is called.
eg. placeholder object could look like this
data class Waypoint(
val coordinate: Point,
val name: String?,
val bearing: Double?,
val zLevel: Int?,
val isSilent: Boolean = false,
val targetCoordinate: Point?
) {
companion object {
val CURRENT_LOCATION = Waypoint(
coordinate = Point.fromLngLat(Double.NaN, Double.NaN),
name = "CURRENT_LOCATION",
bearing = null,
zLevel = null,
isSilent = false,
targetCoordinate = null
)
}
}
The current location retrieval could be customized by additionally passing LocationProvider
to RouteOptions and updating either MapboxNavigation.requestRoutes()
or MapboxDirectionsSession.requestRoutes()
to launch coroutine and acquiring waypoint location before forwarding it to the router.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think builders should ever be suspend methods.
Why do you think so? Is there any unexpected consequences I can get violating "build should be synchronous" rule?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a note. I don't like current API in terms of asynchronously. Currently, the API makes a user to asynchronously build a RouteOptions
object first and then request again asynchronously a route. It would be a callback hell without coroutines 🙂
I don't know good solution for this right now, so I decided to leave it for the next iterations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to propose a simple solution for this problem - expose a new MapboxNavigation#requestRoutes(NavRouteOptionsBuilder)
which will build the options and request a route in one asynchronous call.
We should still leave the buildRouteOptions
public for developers that would like to mutate the predefined options but the basic flow would be not to build the options manually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@LukasPaczos , I agree thatMapboxNavigation#requestRoutes(NavRouteOptionsBuilder)
is the best option. The only reason I didn't do that is that because it requires a tricky cancellation implementation. I wanted the first iteration to be shorter and postponed cancellation to the next iterations.
But actually, cancellation won't be complex if MapboxNavigation#requestRoutes(NavRouteOptionsBuilder)
will be a suspend function. Will try it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented MapboxNavigation#requestRoutes(NavRouteOptionsBuilder)
as suspend function
239d217
to
f371271
Compare
5939510
to
b960736
Compare
@tomaszrybakiewicz , @LukasPaczos , thank you for feedback. I fixed or replied to your suggestions. In general I think that PR is good enough to be merged, but I need a few more iterations before new API can become not experimental. If you don't have strong opinion about some of the new changes and approve it, I will create tickets for the next iterations based on list in the PR description, see "Do in the context of the following tickets". |
b960736
to
5db5746
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then is the question of compatibility, we also need to define a clear path for developers to start with higher-level API and transition to RouteOptiosnBuilder to tweak a parameter that wasn't exposed via NavRouteOptionsBuilder, maybe additional extension that works directly on RouteOptiosnBuilder or something along these lines.
Did you have a chance to think about this? This might be an important element for a transition/experimental period for developers who would like to try the API which doesn't yet offer everything.
@ExperimentalPreviewMapboxNavigationAPI | ||
suspend fun requestRoutes( | ||
optionsBlock: (NoWaypointsOptionsBuilder) -> RouteOptionsBuilderWithWaypoints | ||
): RequestRoutesResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
): RequestRoutesResult { | |
): Expected<RequestRoutesFailure, RequestRoutesSuccess> { |
Let's keep it consistent with other APIs that are in the Nav SDK.
import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions | ||
|
||
@ExperimentalPreviewMapboxNavigationAPI | ||
interface NoWaypointsOptionsBuilder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interface NoWaypointsOptionsBuilder { | |
sealed interface NoWaypointsOptionsBuilder { |
Same everywhere below.
optionsBlock: (NoWaypointsOptionsBuilder) -> RouteOptionsBuilderWithWaypoints | ||
): RequestRoutesResult { | ||
if (tripSession.getState() != TripSessionState.STARTED) { | ||
error("trip session should be started") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't throw but rather return an error message.
fun fromStartLocation( | ||
point: Point, | ||
bearing: Double? = null, | ||
zLevel: Int? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
zLevel: Int? | |
zLevel: Int? = null, |
fun fromStartLocation( | ||
point: Point, | ||
bearing: Double? = null, | ||
zLevel: Int? | ||
): WaypointsInProgressBuilder |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot define interfaces with default arguments because we won't be able to extend the parameter set for Java consumers. Adding a parameter to an interface function will always be a breaking change since @JvmOverloads
is not available in interfaces.
Unfortunately it looks like we'll need to split NavRouteOptionsBuilder
into 3 separate classes that pass the data between each other in a chain, unless you have other ideas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we can do that, it might actually make the experience more consistent with the rest of the build methods that do use functional extensions as arguments.
fun fromStartLocation( | ||
point: Point, | ||
bearing: Double? = null, | ||
zLevel: Int? | ||
): WaypointsInProgressBuilder |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fun fromStartLocation( | |
point: Point, | |
bearing: Double? = null, | |
zLevel: Int? | |
): WaypointsInProgressBuilder | |
fun fromStartLocation( | |
navigationLocation: NavigationLocation, | |
): WaypointsInProgressBuilder | |
class NavigationLocation private constrictor( | |
val point: Point, | |
val bearing: Double? = null, | |
val zLevel: Int? | |
){ | |
Builder... | |
} |
This is more flexible and covers java devs needs. Suppose we must use wrappers for all methods
where are we here? @VysotskiVadim |
@RingerJK , I don't work on it anymore because there is always something more important than this. Do you think I should reconsider priority? |
Problem
Developers use
RouteObject
from themapbox-java
directly. They can easily make a mistake requesting a route: just forget to add any required parameter and Android Navigation SDK may crash.Imagine a team that is trying out different Nav SDK to compare and select for their application. They may didn't notice
applyDefaultNavigationOptions
and make a request with coordinates and profile only, which leads to the crash. It's hard to understand that you're getting anIndexOutOfBounds
error because you forgot to includesteps=true
in the request. Would you pick Mapbox Navigation SDK if had such problems during the first use?Solution
Create such an API for requesting routes so that it isn't possible to make an error.
Changelog
TODO:
Do in the context of the following tickets