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: + *

+ *

+ *

+ * 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); + } }