-
Notifications
You must be signed in to change notification settings - Fork 318
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
NavigationMapRoute 2.0 #1387
NavigationMapRoute 2.0 #1387
Conversation
8455195
to
a528c88
Compare
Processing the traffic data has seemed to help to UI thread blockage by a decent amount, there is still some blockage but most likely by Part 2: If either of you have a minute to check this code out, after my changes, I'm wondering if there's anything glaring causing this behavior I'm seeing: It seems the previous route geometry is being drawn before the new updated geometry is drawn. I'm clearing all of the data for the feature collections, so I've been having a tough time tracking this one down. The |
a528c88
to
808119e
Compare
3d24d10
to
29ca858
Compare
Update here - had an awesome pairing session with @LukasPaczos and we were able to identify some great areas of optimization.
Also were able to track this down ^ thanks for the eyes on that @LukasPaczos! |
29ca858
to
5459faa
Compare
@LukasPaczos I made a lot of the updates here, but struggled with the I removed the The last piece to this puzzle is the bug fix we discussed. The fix is we need to notify the |
5459faa
to
1dfe6e1
Compare
In order to achieve that using only one source, you'll need to add a property to each "CongestionFeature" that indicates, that this feature is a part of a selected, or an alternative route. Then, you need to verify that in your layer setup.
Haven't tested it, but I believe setting the layer up this way, would let you add it to the map just once instead of re-adding it with each route selection, because you are only using data-driven values for the segments' colors. All you need to do, is reset the
If coupled with the above setup, there would be no need for removing the layer! But besides that, have you tried reseting the source with an empty collection once |
1dfe6e1
to
90f28c5
Compare
Yeah that seems to have fixed it, thanks @tobrun |
This is ready for review! |
eed90a3
to
44db396
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.
Thanks for patience @danesfeder!
I was able to take this one for another spin - generally, setup looks good, but I found some issues. The major one being that the NavigationMapRouteActivity
crashes on the device rotation.
Above is caused by the MapRouteLine
leak - when the activity is recreated it draws an initial line and immediately schedules a redraw
of all of the features because style loaded callback is delivered (the nature of this double invocation is a separate issue). This needs to go through the FeatureProcessingTask
that keeps a reference to the MapRouteLine
via the anonymous inner class callback. When this callback is fired, it's going to try and update sources that are invalid since a new instance of the MapRouteLine
is created in the redraw method.
To avoid that, we need to store a weak reference to the listener instead. This shows, however, that a reference to the MapRouteLine
is still kept in the MapRouteClickListener
and MapRouteProgressChangeListener
. Those 2 instances will have to be recreated and reassigned when redrawing after a new style has been loaded.
The same logic will have to be applied to the PrimaryRouteUpdateTask
as well.
Another pain point I can see is an unlikely scenario, where the route is tried to be drawn after the style change has been started and before it's finished. We'll need to listen for the style load started callback and abort any source updates until it has finished loading.
Here's a diff of the changes needed to be done to prevent leaks of the FeatureProcessingTask
.
This is just an example - initialization of the listeners in the NavigationMapRoute
should probably be cleaned up.
@@ -421,6 +426,14 @@ public void onStop() { | |||
mapWayName.onStop(); | |||
} | |||
|
|||
/** | |||
* Should be used in {@link FragmentActivity#onDestroy()} to ensure proper |
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.
Javadoc should also mention that in case of Fragment
s, this should be called from the #onDestroyView
.
private List<Layer> arrowLayers; | ||
private GeoJsonSource arrowShaftGeoJsonSource; | ||
private GeoJsonSource arrowHeadGeoJsonSource; | ||
private Feature arrowShaftGeoJsonFeature = Feature.fromGeometry(Point.fromLngLat(0, 0)); |
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.
arrowShaftGeoJsonFeature
and arrowHeadGeoJsonFeature
can be local variables and sources can be initialized with an empty feature instead of holding the reference to those features.
); | ||
} | ||
|
||
private void initializeArrowLayers(LineLayer shaftLayer, LineLayer shaftCasingLayer, SymbolLayer headLayer, |
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.
NIT: this method name is a bit misleading
return routeLineStrings == null || routeLineStrings.isEmpty() || !alternativesVisible; | ||
} | ||
|
||
private void findClickedRoute(@NonNull LatLng point, HashMap<LineString, DirectionsRoute> routeLineStrings, |
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'm not confident that this is the best way to determine which route was selected. With the "closest point to route" approach the route is going to be selected even if the users clicks far off the route, just because it is the closest one. This method also scales poorly when the routes get longer and longer because we are adding more points to the calculation.
I'd rather leverage map query and try using something like MapboxMap#queryRenderedFeatures
with a similar filtering that we are using for drawing the routes. That said, I don't think this is a blocker for this PR because it's copied from the previous implementation, but definitely worth tackling in the future.
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 sounds great - let's ticket this out, while keeping in mind using this implementation with a bounding box setup to increase the click region.
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.
ticketed in #1634
updateAllLayersVisibilityTo(isVisible); | ||
} | ||
|
||
boolean retrieveVisibilty() { |
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.
NIT typo
callback.onRouteFeaturesProcessed(routeFeatureCollections, routeLineStrings); | ||
} | ||
|
||
private FeatureCollection createRouteFeatureCollection(DirectionsRoute route, boolean isPrimary) { |
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.
Following the code path, it seems like this method can just return a Feature
. MapRouteLine
is generating a FeatureCollection
regardless and it would allow us to drop this one.
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 maybe I'm not following completely, but I think this needs to be a collection? This task is generating a list of Feature
s based on traffic congestion and then pushing them to MapRouteLine
. I couldn't find the get(0)
you spoke of during paring - sorry if I'm just not seeing 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.
I think you are right, not sure what I've seen here before... 🤔
for (int i = 0; i < styleLayers.size(); i++) { | ||
if (!(styleLayers.get(i) instanceof SymbolLayer) | ||
// Avoid placing the route on top of the user location layer | ||
&& !styleLayers.get(i).getId().contains(RouteConstants.MAPBOX_LOCATION_ID)) { |
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 should expose the LocationComponent
layer constant!
44db396
to
318953c
Compare
@LukasPaczos this is up-to-date now with your changes, thanks again! My only remaining question is #1387 (comment) |
318953c
to
2848504
Compare
if (leg.annotation() != null && leg.annotation().congestion() != null) { | ||
for (int i = 0; i < leg.annotation().congestion().size(); i++) { | ||
// See https://github.com/mapbox/mapbox-navigation-android/issues/353 | ||
if (leg.annotation().congestion().size() + 1 <= lineString.coordinates().size()) { |
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.
What's wrong with if (leg.annotation().congestion().size() < lineString.coordinates().size()) {
?
* | ||
* @param isVisible true to show routes, false to hide | ||
*/ | ||
public void updateRouteVisibilityTo(boolean isVisible) { |
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.
wouldn't it be more consistent with other visibility modifiers for these to be named updateRouteVisibility
or showRoute
?
* | ||
* @param isVisible true to show routes, false to hide | ||
*/ | ||
public void updateRouteArrowVisibilityTo(boolean isVisible) { |
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.
same here
import java.util.HashMap; | ||
import java.util.List; | ||
|
||
class MapRouteClickListener implements MapboxMap.OnMapClickListener { |
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 know this implements OnMapClickListener
but I'm not sure Listener
is a good way to describe this class, maybe MapRouteClickHandler
or something?
this.routeArrow = new MapRouteArrow(mapView, mapboxMap, styleRes); | ||
this.mapRouteClickListener = new MapRouteClickListener(routeLine); | ||
this.mapRouteProgressChangeListener = new MapRouteProgressChangeListener(routeLine, routeArrow); | ||
initializeDidFinishLoadingStyleListener(); |
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.
maybe combine the three lines above and just have a method initializeListeners
@@ -10,11 +10,13 @@ | |||
|
|||
class MapRouteProgressChangeListener implements ProgressChangeListener { |
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.
Same comment for this class, I don't really think this qualifies as a Listener
since it's not just listening, it's taking action
@Override | ||
protected void onPostExecute(Void result) { | ||
super.onPostExecute(result); | ||
Runtime.getRuntime().gc(); |
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.
Why is it manually garbage collecting?
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.
@devotaaabel this is needed to avoid leaks 👀 #1387 (review)
@danesfeder let's add a TODO
comment here stating that would be 💯 to find another approach that doesn't need to call the garbage collector explicitly.
2848504
to
2540c5a
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.
Overall this looks awesome 💯 Can't wait to merge this in! Good job @danesfeder 👏
Great refactoring extracting MapRouteArrow
and MapRouteLine
out from NavigationMapRoute
❤️ ❤️
I left some minor comments / questions that we can discuss.
@Override | ||
protected void onPostExecute(Void result) { | ||
super.onPostExecute(result); | ||
Runtime.getRuntime().gc(); |
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.
@devotaaabel this is needed to avoid leaks 👀 #1387 (review)
@danesfeder let's add a TODO
comment here stating that would be 💯 to find another approach that doesn't need to call the garbage collector explicitly.
this.routeLayers = new ArrayList<>(); | ||
|
||
TypedArray typedArray = context.obtainStyledAttributes(styleRes, R.styleable.NavigationMapRoute); | ||
// Primary Route attributes |
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.
What about extracting this block of code into a private
method and give it a name based on the comment? This way the constructor will be easier to read and understand and comment will become superfluous so it won't be necessary.
ContextCompat.getColor(context, R.color.mapbox_navigation_route_shield_layer_color)); | ||
routeScale = typedArray.getFloat(R.styleable.NavigationMapRoute_routeScale, 1.0f); | ||
|
||
// Secondary Routes attributes |
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.
Same here re: extract method based on comment
alternativeRouteScale = typedArray.getFloat( | ||
R.styleable.NavigationMapRoute_alternativeRouteScale, 1.0f); | ||
|
||
// Waypoint attributes |
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.
Same here re: extract method based on comment
@Override | ||
protected List<FeatureCollection> doInBackground(Void... voids) { | ||
List<FeatureCollection> updatedRouteCollections = new ArrayList<>(routeFeatureCollections); | ||
// Update the primary new collection |
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.
What about extracting this block of code into a private
method and give it a name based on the comment? This way doInBackground
will be easier to read and understand and comment will become superfluous so it won't be necessary.
|
||
@Override | ||
protected void onPostExecute(List<FeatureCollection> updatedRouteCollections) { | ||
Runtime.getRuntime().gc(); |
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.
What about dropping a comment here (TODO
?) explaining why we're calling the garbage collector explicitly?
for (Feature feature : primaryFeatures) { | ||
feature.addBooleanProperty(PRIMARY_ROUTE_PROPERTY_KEY, true); | ||
} | ||
// Update non-primary collections (not including the primary) |
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.
Same here re: extract method based on comment
@@ -0,0 +1,49 @@ | |||
package com.mapbox.services.android.navigation.ui.v5.route; | |||
|
|||
class RouteConstants { |
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.
Comment - IMO constants, objects and methods should live / be where their context is. From my experience constants classes as well as utility classes become a drawer easily and we should treat them carefully.
import static org.mockito.Mockito.when; | ||
|
||
@RunWith(RobolectricTestRunner.class) | ||
public class MapRouteLineTest extends BaseTest { |
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.
Could you extract (if possible) some of the shared arrange code across these tests into a private
factory method? It seems there's some duplicated code used across almost all the tests 👇
MapUtils.addLayerToMap(mapboxMap, waypointLayer, belowLayer); | ||
} | ||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) | ||
public void onDestroy() { |
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 believe this was added to workaround 👇 commented in #1387 (comment)
I updated the
MapRouteLine
andMapRouteArrow
to each have anonDestroy
that aims to clean up the layers. I also updated the arrow code to remove the layers if we find they exist.
Question (especially for @LukasPaczos) - Shouldn't this be handled gracefully from the Maps SDK side when calling MapboxMap#onDestroy
? Have we explored this option? I can cut a ticket in mapbox-gl-native
to discuss further if you think this should be addressed upstream (if there isn't one already 😅). Does that make sense? Let me know what you think.
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.
Correct, there is no need for clearing the layers on the actual MapView
destroy. You can even see logs like
E/Mbgl-NativeMapView: You're calling `removeLayer` after the `MapView` was destroyed, were you invoking it after `onDestroy()`?
because it's called after the call to MapView#onDestroy
and it doesn't have any real impact.
I believe that the initial idea here was to clear layers when the NavigationMapRoute
is detached from the map so that the navigation SDK doesn't leave unused layers on the map that can still be in use otherwise. If a solution for this minor use-case seems not so relevant in the grand scheme of things, it can definitely be tackled by a separate PR.
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.
Sorry if I didn't make myself clear. I was wondering if the Maps SDK should be in charge of the "layers cleanup" somehow - ensuring that any layers added are removed properly when in onDestroy
. Probably it has more implications / considerations that I'm not taking into account / missing but IMO this should be maps responsibility - obviously calling removeLayer
afterwards is a dev implementation mistake (even in this case which doesn't seem to affect - other than the logs printed out). Is that what you're saying? What am I missing? I'm confused 😅
I believe that the initial idea here was to clear layers when the NavigationMapRoute is detached from the map so that the navigation SDK doesn't leave unused layers on the map that can still be in use otherwise.
Not sure if I follow, I think ☝️ was the part that make me 😕 Could you clarify?
If a solution for this minor use-case seems not so relevant in the grand scheme of things, it can definitely be tackled by a separate PR.
Sure thing! This was a question 🤔 that I wanted to fully understand. Not a blocker at all, I agree.
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 was wondering if the Maps SDK should be in charge of the "layers cleanup" somehow - ensuring that any layers added are removed properly when in onDestroy.
This is already the case. There is no need for a manual layer cleanup when the map is destroyed, that's what I meant above.
Not sure if I follow, I think ☝️ was the part that make me 😕 Could you clarify?
I was going with an impression that the NavigationMapRoute
is a standalone object that can be added and removed from the map. If that's not the case, there's nothing to worry about :)
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 is already the case. There is no need for a manual layer cleanup when the map is destroyed, that's what I meant above.
Nice! Thanks for confirming. In any case, I don't see where the Maps SDK does this cleanup. That's why I asked.
I was going with an impression that the NavigationMapRoute is a standalone object that can be added and removed from the map.
Nah, not really. What you can do is show / hide both the MapRouteLine
and MapRouteArrow
which translates internally to set the visibility of the layer accordingly.
If that's not the case, there's nothing to worry about :)
Do you mean that if that's the case we shouldn't worry about removing the layers and the sources explicitly when in onDestroy
? This is what we're currently doing in
Lines 315 to 323 in 2540c5a
/** | |
* This method should be added in your {@link Activity#onDestroy()} or | |
* {@link android.support.v4.app.Fragment#onDestroyView()} to handle removing resources that were added to the map. | |
*/ | |
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) | |
public void onDestroy() { | |
routeLine.onDestroy(); | |
routeArrow.onDestroy(); | |
} |
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.
Yes, if the map will never outlive MapRouteLine
, we shouldn't worry about removing those layers.
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.
Got it! Thanks @LukasPaczos 🙏
cc'ing @danesfeder to double check / confirm and remove.
2540c5a
to
b4d4db7
Compare
@LukasPaczos great catches! I believe I was able to address both of them with this latest round of updates. What was happening is we were trying to recreate these options (visibility, primary route, etc.) before the feature processing task was finished. Previous route not being displayed / primary route selection maintained |
LGTM, thanks for the fantastic work @danesfeder! Leaving to final review to the nav folks. |
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.
Overall this looks good to me. I've retested and it's working 💯
Would you mind @LukasPaczos to do a quick final check 👀 before we merge here? Thanks in advance 🙏
Although some feedback isn't tackled, let's revisit later / create a follow-up PR to address it. They're minor comments mainly around naming so shouldn't block the PR.
Thanks again @danesfeder for the great work 👏
You beat me @LukasPaczos 😅 Thanks for your help here, much appreciated! As mentioned above, going ahead and merging 🚀 |
Closes #704 Fixes #1512
This PR aims to refactor the monstrous
NavigationMapRoute
and solve the issues of the route freezing the UI thread when in the process of drawing new routes.Part 1 is processing traffic data on a background thread. Part 2 is going to be optimizing how we update the map source after we have the data we need.
TODO:
cc @tobrun @LukasPaczos for Maps 👀