Skip to content

Commit

Permalink
add stick to chosen route when re-routing with ui functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Guardiola31337 committed Mar 30, 2018
1 parent efd4543 commit 3f417da
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 71 deletions.
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);
}
}

0 comments on commit 3f417da

Please sign in to comment.