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

Add stick to chosen route when re-routing with UI functionality #808

Merged
merged 1 commit into from
Mar 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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:
* <p>
* <ul>
* <li>Character Insertion</li>
* <li>Character Deletion</li>
* <li>Character Replacement</li>
* <li>Adjacent Character Swap</li>
* </ul>
* <p>
* 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.
* <p>
* <p>
* 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.
* <p>
* <p>
* 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<Character, Integer> 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];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,6 +33,8 @@

public class RouteViewModel extends AndroidViewModel implements Callback<DirectionsResponse> {

private static final int FIRST_ROUTE = 0;
private static final int ONE_ROUTE = 1;
public final MutableLiveData<DirectionsRoute> route = new MutableLiveData<>();
public final MutableLiveData<Point> destination = new MutableLiveData<>();
public final MutableLiveData<String> requestErrorMessage = new MutableLiveData<>();
Expand All @@ -58,9 +61,7 @@ public RouteViewModel(@NonNull Application application) {
*/
@Override
public void onResponse(@NonNull Call<DirectionsResponse> call, @NonNull Response<DirectionsResponse> response) {
if (validRouteResponse(response)) {
route.setValue(response.body().routes().get(0));
}
processRoute(response);
}

@Override
Expand Down Expand Up @@ -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}.
* <p>
Expand All @@ -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<DirectionsResponse> response) {
if (isValidRoute(response)) {
List<DirectionsRoute> 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<DirectionsResponse> response) {
return response.body() != null && !response.body().routes().isEmpty();
}

private boolean isNavigationRunning(DirectionsRoute chosenRoute) {
return chosenRoute != null;
}

private DirectionsRoute obtainMostSimilarRoute(List<DirectionsRoute> 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<DirectionsRoute> 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<RouteLeg> 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());
}

/**
Expand All @@ -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<DirectionsResponse> 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);
Expand All @@ -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.
Expand All @@ -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) {
Expand All @@ -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);
}
}