diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/DamerauLevenshteinAlgorithm.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/DamerauLevenshteinAlgorithm.java
new file mode 100644
index 00000000000..41f759741e1
--- /dev/null
+++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/DamerauLevenshteinAlgorithm.java
@@ -0,0 +1,127 @@
+package com.mapbox.services.android.navigation.ui.v5.route;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/* Copyright (c) 2012 Kevin L. Stern
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/**
+ * The Damerau-Levenshtein Algorithm is an extension to the Levenshtein
+ * Algorithm which solves the edit distance problem between a source string and
+ * a target string with the following operations:
+ *
+ *
+ * - Character Insertion
+ * - Character Deletion
+ * - Character Replacement
+ * - Adjacent Character Swap
+ *
+ *
+ * Note that the adjacent character swap operation is an edit that may be
+ * applied when two adjacent characters in the source string match two adjacent
+ * characters in the target string, but in reverse order, rather than a general
+ * allowance for adjacent character swaps.
+ *
+ *
+ * This implementation allows the client to specify the costs of the various
+ * edit operations with the restriction that the cost of two swap operations
+ * must not be less than the cost of a delete operation followed by an insert
+ * operation. This restriction is required to preclude two swaps involving the
+ * same character being required for optimality which, in turn, enables a fast
+ * dynamic programming solution.
+ *
+ *
+ * The running time of the Damerau-Levenshtein algorithm is O(n*m) where n is
+ * the length of the source string and m is the length of the target string.
+ * This implementation consumes O(n*m) space.
+ *
+ * @author Kevin L. Stern
+ */
+class DamerauLevenshteinAlgorithm {
+ private static final int DELETE_COST = 1;
+ private static final int INSERT_COST = 1;
+ private static final int REPLACE_COST = 1;
+ private static final int SWAP_COST = 1;
+
+ /**
+ * Compute the Damerau-Levenshtein distance between the specified source
+ * string and the specified target string.
+ */
+ static int execute(String source, String target) {
+ if (source.length() == 0) {
+ return target.length() * INSERT_COST;
+ }
+ if (target.length() == 0) {
+ return source.length() * DELETE_COST;
+ }
+ int[][] table = new int[source.length()][target.length()];
+ Map sourceIndexByCharacter = new HashMap<>();
+ if (source.charAt(0) != target.charAt(0)) {
+ table[0][0] = Math.min(REPLACE_COST, DELETE_COST + INSERT_COST);
+ }
+ sourceIndexByCharacter.put(source.charAt(0), 0);
+ for (int i = 1; i < source.length(); i++) {
+ int deleteDistance = table[i - 1][0] + DELETE_COST;
+ int insertDistance = (i + 1) * DELETE_COST + INSERT_COST;
+ int matchDistance = i * DELETE_COST + (source.charAt(i) == target.charAt(0) ? 0 : REPLACE_COST);
+ table[i][0] = Math.min(Math.min(deleteDistance, insertDistance), matchDistance);
+ }
+ for (int j = 1; j < target.length(); j++) {
+ int deleteDistance = (j + 1) * INSERT_COST + DELETE_COST;
+ int insertDistance = table[0][j - 1] + INSERT_COST;
+ int matchDistance = j * INSERT_COST + (source.charAt(0) == target.charAt(j) ? 0 : REPLACE_COST);
+ table[0][j] = Math.min(Math.min(deleteDistance, insertDistance), matchDistance);
+ }
+ for (int i = 1; i < source.length(); i++) {
+ int maxSourceLetterMatchIndex = source.charAt(i) == target.charAt(0) ? 0 : -1;
+ for (int j = 1; j < target.length(); j++) {
+ Integer candidateSwapIndex = sourceIndexByCharacter.get(target.charAt(j));
+ int indexJSwap = maxSourceLetterMatchIndex;
+ int deleteDistance = table[i - 1][j] + DELETE_COST;
+ int insertDistance = table[i][j - 1] + INSERT_COST;
+ int matchDistance = table[i - 1][j - 1];
+ if (source.charAt(i) != target.charAt(j)) {
+ matchDistance += REPLACE_COST;
+ } else {
+ maxSourceLetterMatchIndex = j;
+ }
+ int swapDistance;
+ if (candidateSwapIndex != null && indexJSwap != -1) {
+ int indexISwap = candidateSwapIndex;
+ int preSwapCost;
+ if (indexISwap == 0 && indexJSwap == 0) {
+ preSwapCost = 0;
+ } else {
+ preSwapCost = table[Math.max(0, indexISwap - 1)][Math.max(0, indexJSwap - 1)];
+ }
+ swapDistance = preSwapCost + (i - indexISwap - 1) * DELETE_COST + (j - indexJSwap - 1) * INSERT_COST
+ + SWAP_COST;
+ } else {
+ swapDistance = Integer.MAX_VALUE;
+ }
+ table[i][j] = Math.min(Math.min(Math.min(deleteDistance, insertDistance), matchDistance), swapDistance);
+ }
+ sourceIndexByCharacter.put(source.charAt(i), i);
+ }
+ return table[source.length() - 1][target.length() - 1];
+ }
+}
\ No newline at end of file
diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/RouteViewModel.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/RouteViewModel.java
index 48d6cf39725..dc702dc5e6b 100644
--- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/RouteViewModel.java
+++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/RouteViewModel.java
@@ -11,6 +11,7 @@
import com.mapbox.api.directions.v5.models.DirectionsResponse;
import com.mapbox.api.directions.v5.models.DirectionsRoute;
import com.mapbox.api.directions.v5.models.LegStep;
+import com.mapbox.api.directions.v5.models.RouteLeg;
import com.mapbox.api.directions.v5.models.RouteOptions;
import com.mapbox.geojson.Point;
import com.mapbox.mapboxsdk.Mapbox;
@@ -32,6 +33,8 @@
public class RouteViewModel extends AndroidViewModel implements Callback {
+ private static final int FIRST_ROUTE = 0;
+ private static final int ONE_ROUTE = 1;
public final MutableLiveData route = new MutableLiveData<>();
public final MutableLiveData destination = new MutableLiveData<>();
public final MutableLiveData requestErrorMessage = new MutableLiveData<>();
@@ -58,9 +61,7 @@ public RouteViewModel(@NonNull Application application) {
*/
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
- if (validRouteResponse(response)) {
- route.setValue(response.body().routes().get(0));
- }
+ processRoute(response);
}
@Override
@@ -90,16 +91,6 @@ public void extractRouteOptions(NavigationViewOptions options) {
}
}
- /**
- * Updates the request unit type based on what was set in
- * {@link NavigationViewOptions}.
- *
- * @param options possibly containing unitType
- */
- private void extractUnitType(NavigationViewOptions options) {
- unitType = NavigationUnitType.getDirectionsCriteriaUnitType(options.navigationOptions().unitType());
- }
-
/**
* Requests a new {@link DirectionsRoute}.
*
@@ -121,18 +112,90 @@ public void fetchRouteFromOffRouteEvent(OffRouteEvent event) {
NavigationRoute.Builder builder = buildRouteRequestFromCurrentLocation(origin, bearing, progress);
if (builder != null) {
addNavigationViewOptions(builder);
+ builder.alternatives(true);
builder.build().getRoute(this);
}
}
}
- private void fetchRouteFromCoordinates(Point origin, Point destination) {
- NavigationRoute.Builder builder = NavigationRoute.builder()
- .accessToken(Mapbox.getAccessToken())
- .origin(origin)
- .destination(destination);
- addNavigationViewOptions(builder);
- builder.build().getRoute(this);
+ private void processRoute(@NonNull Response response) {
+ if (isValidRoute(response)) {
+ List routes = response.body().routes();
+ DirectionsRoute bestRoute = routes.get(FIRST_ROUTE);
+ DirectionsRoute chosenRoute = route.getValue();
+ if (isNavigationRunning(chosenRoute)) {
+ bestRoute = obtainMostSimilarRoute(routes, bestRoute, chosenRoute);
+ }
+ route.setValue(bestRoute);
+ }
+ }
+
+ /**
+ * Checks if we have at least one {@link DirectionsRoute} in the given
+ * {@link DirectionsResponse}.
+ *
+ * @param response to be checked
+ * @return true if valid, false if not
+ */
+ private boolean isValidRoute(Response response) {
+ return response.body() != null && !response.body().routes().isEmpty();
+ }
+
+ private boolean isNavigationRunning(DirectionsRoute chosenRoute) {
+ return chosenRoute != null;
+ }
+
+ private DirectionsRoute obtainMostSimilarRoute(List routes, DirectionsRoute currentBestRoute,
+ DirectionsRoute chosenRoute) {
+ DirectionsRoute mostSimilarRoute = currentBestRoute;
+ if (routes.size() > ONE_ROUTE) {
+ mostSimilarRoute = findMostSimilarRoute(chosenRoute, routes);
+ }
+ return mostSimilarRoute;
+ }
+
+ private DirectionsRoute findMostSimilarRoute(DirectionsRoute chosenRoute, List routes) {
+ int routeIndex = 0;
+ String chosenRouteLegDescription = obtainRouteLegDescriptionFrom(chosenRoute);
+ int minSimilarity = Integer.MAX_VALUE;
+ for (int index = 0; index < routes.size(); index++) {
+ String routeLegDescription = obtainRouteLegDescriptionFrom(routes.get(index));
+ int currentSimilarity = DamerauLevenshteinAlgorithm.execute(chosenRouteLegDescription, routeLegDescription);
+ if (currentSimilarity < minSimilarity) {
+ minSimilarity = currentSimilarity;
+ routeIndex = index;
+ }
+ }
+ return routes.get(routeIndex);
+ }
+
+ private String obtainRouteLegDescriptionFrom(DirectionsRoute route) {
+ List routeLegs = route.legs();
+ StringBuilder routeLegDescription = new StringBuilder();
+ for (RouteLeg leg : routeLegs) {
+ routeLegDescription.append(leg.summary());
+ }
+ return routeLegDescription.toString();
+ }
+
+ /**
+ * Looks for a route locale provided by {@link NavigationViewOptions} to be
+ * stored for reroute requests.
+ *
+ * @param options to look for set locale
+ */
+ private void extractLocale(NavigationViewOptions options) {
+ locale = LocaleUtils.getNonNullLocale(this.getApplication(), options.navigationOptions().locale());
+ }
+
+ /**
+ * Updates the request unit type based on what was set in
+ * {@link NavigationViewOptions}.
+ *
+ * @param options possibly containing unitType
+ */
+ private void extractUnitType(NavigationViewOptions options) {
+ unitType = NavigationUnitType.getDirectionsCriteriaUnitType(options.navigationOptions().unitType());
}
/**
@@ -159,43 +222,6 @@ private void extractRouteFromOptions(NavigationViewOptions options) {
}
}
- /**
- * Extracts the {@link Point} coordinates, adds a destination marker,
- * and fetches a route with the coordinates.
- *
- * @param options containing origin and destination
- */
- private void extractCoordinatesFromOptions(NavigationViewOptions options) {
- if (options.origin() != null && options.destination() != null) {
- cacheRouteProfile(options);
- cacheRouteLanguage(options, null);
- Point origin = options.origin();
- destination.setValue(options.destination());
- fetchRouteFromCoordinates(origin, destination.getValue());
- }
- }
-
- /**
- * Checks if we have at least one {@link DirectionsRoute} in the given
- * {@link DirectionsResponse}.
- *
- * @param response to be checked
- * @return true if valid, false if not
- */
- private static boolean validRouteResponse(Response response) {
- return response.body() != null
- && !response.body().routes().isEmpty();
- }
-
- private void addNavigationViewOptions(NavigationRoute.Builder builder) {
- if (routeProfile != null) {
- builder.profile(routeProfile);
- }
- builder
- .language(locale)
- .voiceUnits(unitType);
- }
-
private void cacheRouteInformation(NavigationViewOptions options, DirectionsRoute route) {
cacheRouteOptions(route.routeOptions());
cacheRouteProfile(options);
@@ -207,16 +233,6 @@ private void cacheRouteOptions(RouteOptions routeOptions) {
cacheRouteDestination();
}
- /**
- * Looks for a route profile provided by {@link NavigationViewOptions} to be
- * stored for reroute requests.
- *
- * @param options to look for set profile
- */
- private void cacheRouteProfile(NavigationViewOptions options) {
- routeProfile = options.directionsProfile();
- }
-
/**
* Looks at the given {@link DirectionsRoute} and extracts the destination based on
* the last {@link LegStep} maneuver.
@@ -231,13 +247,13 @@ private void cacheRouteDestination() {
}
/**
- * Looks for a route locale provided by {@link NavigationViewOptions} to be
+ * Looks for a route profile provided by {@link NavigationViewOptions} to be
* stored for reroute requests.
*
- * @param options to look for set locale
+ * @param options to look for set profile
*/
- private void extractLocale(NavigationViewOptions options) {
- locale = LocaleUtils.getNonNullLocale(this.getApplication(), options.navigationOptions().locale());
+ private void cacheRouteProfile(NavigationViewOptions options) {
+ routeProfile = options.directionsProfile();
}
private void cacheRouteLanguage(NavigationViewOptions options, @Nullable DirectionsRoute route) {
@@ -249,4 +265,36 @@ private void cacheRouteLanguage(NavigationViewOptions options, @Nullable Directi
locale = Locale.getDefault();
}
}
+
+ /**
+ * Extracts the {@link Point} coordinates, adds a destination marker,
+ * and fetches a route with the coordinates.
+ *
+ * @param options containing origin and destination
+ */
+ private void extractCoordinatesFromOptions(NavigationViewOptions options) {
+ if (options.origin() != null && options.destination() != null) {
+ cacheRouteProfile(options);
+ cacheRouteLanguage(options, null);
+ Point origin = options.origin();
+ destination.setValue(options.destination());
+ fetchRouteFromCoordinates(origin, destination.getValue());
+ }
+ }
+
+ private void fetchRouteFromCoordinates(Point origin, Point destination) {
+ NavigationRoute.Builder builder = NavigationRoute.builder()
+ .accessToken(Mapbox.getAccessToken())
+ .origin(origin)
+ .destination(destination);
+ addNavigationViewOptions(builder);
+ builder.build().getRoute(this);
+ }
+
+ private void addNavigationViewOptions(NavigationRoute.Builder builder) {
+ if (routeProfile != null) {
+ builder.profile(routeProfile);
+ }
+ builder.language(locale).voiceUnits(unitType);
+ }
}