package net.osmand.plus.activities;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import net.osmand.LogUtil;
import net.osmand.OsmAndFormatter;
import net.osmand.osm.LatLon;
import net.osmand.osm.MapUtils;
import net.osmand.plus.OsmandSettings;
import net.osmand.plus.R;
import net.osmand.plus.OsmandSettings.ApplicationMode;
import net.osmand.plus.activities.RouteProvider.RouteCalculationResult;
import net.osmand.plus.activities.RouteProvider.RouteService;
import net.osmand.plus.voice.CommandPlayer;
import android.app.Activity;
import android.content.Context;
import android.location.Location;
import android.util.FloatMath;
import android.widget.Toast;
public class RoutingHelper {
private static final org.apache.commons.logging.Log log = LogUtil.getLog(RoutingHelper.class);
public static interface IRouteInformationListener {
public void newRouteIsCalculated(boolean updateRoute);
public void routeWasCancelled();
}
private final double DISTANCE_TO_USE_OSMAND_ROUTER = 20000;
private List<IRouteInformationListener> listeners = new ArrayList<IRouteInformationListener>();
private Context context;
// activity to show messages & refresh map when route is calculated
private Activity uiActivity;
private boolean isFollowingMode = false;
private List<Location> currentGPXRoute = null;
// instead of this properties RouteCalculationResult could be used
private List<Location> routeNodes = new ArrayList<Location>();
private List<RouteDirectionInfo> directionInfo = null;
private int[] listDistance = null;
// Note always currentRoute > get(currentDirectionInfo).routeOffset,
// but currentRoute <= get(currentDirectionInfo+1).routeOffset
protected int currentDirectionInfo = 0;
protected int currentRoute = 0;
private LatLon finalLocation;
private Location lastFixedLocation;
private Thread currentRunningJob;
private long lastTimeEvaluatedRoute = 0;
private int evalWaitInterval = 3000;
private ApplicationMode mode;
private RouteProvider provider = new RouteProvider();
private VoiceRouter voiceRouter;
public RoutingHelper(ApplicationMode mode, Context context, CommandPlayer player){
this.mode = mode;
this.context = context;
voiceRouter = new VoiceRouter(this, player);
}
public boolean isFollowingMode() {
return isFollowingMode;
}
public void setFollowingMode(boolean isFollowingMode) {
this.isFollowingMode = isFollowingMode;
}
public void setFinalAndCurrentLocation(LatLon finalLocation, Location currentLocation){
setFinalAndCurrentLocation(finalLocation, currentLocation, null);
}
public synchronized void setFinalAndCurrentLocation(LatLon finalLocation, Location currentLocation, List<Location> gpxRoute){
this.finalLocation = finalLocation;
this.routeNodes.clear();
listDistance = null;
directionInfo = null;
evalWaitInterval = 3000;
currentGPXRoute = gpxRoute;
for(IRouteInformationListener l : listeners){
l.routeWasCancelled();
}
// to update route
setCurrentLocation(currentLocation);
}
public List<Location> getCurrentGPXRoute() {
return currentGPXRoute;
}
public void setFinalLocation(LatLon finalLocation){
setFinalAndCurrentLocation(finalLocation, getCurrentLocation());
}
public void setAppMode(ApplicationMode mode){
this.mode = mode;
voiceRouter.updateAppMode();
}
public ApplicationMode getAppMode() {
return mode;
}
public LatLon getFinalLocation() {
return finalLocation;
}
public boolean isRouterEnabled(){
return finalLocation != null && lastFixedLocation != null;
}
public boolean isRouteCalculated(){
return !routeNodes.isEmpty();
}
public VoiceRouter getVoiceRouter() {
return voiceRouter;
}
public boolean finishAtLocation(Location currentLocation) {
Location lastPoint = routeNodes.get(routeNodes.size() - 1);
if(currentRoute > routeNodes.size() - 3 && currentLocation.distanceTo(lastPoint) < 60){
if(lastFixedLocation != null && lastFixedLocation.distanceTo(lastPoint) < 60){
showMessage(context.getString(R.string.arrived_at_destination));
OsmandSettings.setFollowingByRoute(context, false);
setFollowingMode(false);
voiceRouter.arrivedDestinationPoint();
updateCurrentRoute(routeNodes.size() - 1);
// clear final location to prevent all time showing message
finalLocation = null;
}
lastFixedLocation = currentLocation;
return true;
}
return false;
}
public void setUiActivity(Activity uiActivity) {
this.uiActivity = uiActivity;
}
public Location getCurrentLocation() {
return lastFixedLocation;
}
private void updateCurrentRoute(int currentRoute){
this.currentRoute = currentRoute;
if(directionInfo != null){
while(currentDirectionInfo < directionInfo.size() - 1 &&
directionInfo.get(currentDirectionInfo + 1).routePointOffset < currentRoute){
currentDirectionInfo ++;
}
}
}
public void addListener(IRouteInformationListener l){
listeners.add(l);
}
public boolean removeListener(IRouteInformationListener l){
return listeners.remove(l);
}
public void setCurrentLocation(Location currentLocation) {
if(finalLocation == null || currentLocation == null){
return;
}
boolean calculateRoute = false;
synchronized (this) {
if(routeNodes.isEmpty() || routeNodes.size() <= currentRoute){
calculateRoute = true;
} else {
// Check whether user follow by route in correct direction
// 1. try to mark passed route (move forward)
float dist = currentLocation.distanceTo(routeNodes.get(currentRoute));
while(currentRoute + 1 < routeNodes.size()){
float newDist = currentLocation.distanceTo(routeNodes.get(currentRoute + 1));
boolean proccesed = false;
if (newDist < dist){
if(newDist > 150){
// may be that check is really not needed ? only for start position
if(currentRoute > 0 ){
// check that we are not far from the route (if we are on the route distance doesn't matter)
float bearing = routeNodes.get(currentRoute - 1).bearingTo(routeNodes.get(currentRoute));
float bearingMovement = currentLocation.bearingTo(routeNodes.get(currentRoute));
float d = Math.abs(currentLocation.distanceTo(routeNodes.get(currentRoute)) * FloatMath.sin((bearingMovement - bearing)*3.14f/180f));
if(d > 50){
proccesed = true;
}
} else {
proccesed = true;
}
if(proccesed && log.isDebugEnabled()){
log.debug("Processed distance : " + newDist + " " + dist); //$NON-NLS-1$//$NON-NLS-2$
}
} else {
// case if you are getting close to the next point after turn
// but you haven't turned before (could be checked bearing)
if(currentLocation.hasBearing() || lastFixedLocation != null){
float bearingToPoint = currentLocation.bearingTo(routeNodes.get(currentRoute));
float bearingBetweenPoints = routeNodes.get(currentRoute).bearingTo(routeNodes.get(currentRoute+1));
float bearing = currentLocation.hasBearing() ? currentLocation.getBearing() : lastFixedLocation.bearingTo(currentLocation);
if(Math.abs(bearing - bearingToPoint) >
Math.abs(bearing - bearingBetweenPoints)){
if(log.isDebugEnabled()){
log.debug("Processed point bearing : " + Math.abs(currentLocation.getBearing() - bearingToPoint) + " " //$NON-NLS-1$ //$NON-NLS-2$
+ Math.abs(currentLocation.getBearing() - bearingBetweenPoints));
}
proccesed = true;
}
}
}
}
if(proccesed){
// that node already passed
updateCurrentRoute(currentRoute + 1);
dist = newDist;
} else {
break;
}
}
// 2. check if destination found
if(finishAtLocation(currentLocation)){
return;
}
// 3. check if closest location already passed
if(currentRoute + 1 < routeNodes.size()){
float bearing = routeNodes.get(currentRoute).bearingTo(routeNodes.get(currentRoute + 1));
float bearingMovement = currentLocation.bearingTo(routeNodes.get(currentRoute));
// only 35 degrees for that case because it wrong catches sharp turns
if(Math.abs(bearing - bearingMovement) > 140 && Math.abs(bearing - bearingMovement) < 220){
if(log.isDebugEnabled()){
log.debug("Processed point movement bearing : "+bearingMovement +" bearing " + bearing); //$NON-NLS-1$ //$NON-NLS-2$
}
updateCurrentRoute(currentRoute + 1);
}
}
// 3.5 check that we already pass very sharp turn by missing one point (so our turn is sharper than expected)
// instead of that rule possible could be introduced another if the dist < 5m mark the location as already passed
if(currentRoute + 2 < routeNodes.size()){
float bearing = routeNodes.get(currentRoute + 1).bearingTo(routeNodes.get(currentRoute + 2));
float bearingMovement = currentLocation.bearingTo(routeNodes.get(currentRoute + 1));
// only 15 degrees for that case because it wrong catches sharp turns
if(Math.abs(bearing - bearingMovement) > 165 && Math.abs(bearing - bearingMovement) < 195){
if(log.isDebugEnabled()){
log.debug("Processed point movement bearing 2 : "+bearingMovement +" bearing " + bearing); //$NON-NLS-1$ //$NON-NLS-2$
}
updateCurrentRoute(currentRoute + 2);
}
}
// 4. evaluate distance to the route and reevaluate if needed
if(currentRoute > 0){
float bearing = routeNodes.get(currentRoute - 1).bearingTo(routeNodes.get(currentRoute));
float bearingMovement = currentLocation.bearingTo(routeNodes.get(currentRoute));
float d = Math.abs(currentLocation.distanceTo(routeNodes.get(currentRoute)) * FloatMath.sin((bearingMovement - bearing)*3.14f/180f));
if(d > 50) {
log.info("Recalculate route, because correlation : " + d); //$NON-NLS-1$
calculateRoute = true;
}
}
// 5. also check bearing by summing distance
if(!calculateRoute){
float d = currentLocation.distanceTo(routeNodes.get(currentRoute));
if (d > 80) {
if (currentRoute > 0) {
// possibly that case is not needed (often it is covered by 4.)
float f1 = currentLocation.distanceTo(routeNodes.get(currentRoute - 1)) + d;
float c = routeNodes.get(currentRoute - 1).distanceTo(routeNodes.get(currentRoute));
if (c * 2 < d + f1) {
log.info("Recalculate route, because too far from points : " + d + " " + f1 + " >> " + c); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
calculateRoute = true;
}
} else {
// that case is needed
log.info("Recalculate route, because too far from start : " + d); //$NON-NLS-1$
calculateRoute = true;
}
}
}
// 5. Also bearing could be checked (is it same direction)
// float bearing;
// if(currentLocation.hasBearing()){
// bearing = currentLocation.getBearing();
// } else if(lastFixedLocation != null){
// bearing = lastFixedLocation.bearingTo(currentLocation);
// }
// bearingRoute = currentLocation.bearingTo(routeNodes.get(currentRoute));
// if (Math.abs(bearing - bearingRoute) > 60f && 360 - Math.abs(bearing - bearingRoute) > 60f) {
// something wrong however it could be starting movement
// }
}
}
voiceRouter.updateStatus();
lastFixedLocation = currentLocation;
if(calculateRoute){
calculateRoute(lastFixedLocation, finalLocation, currentGPXRoute);
}
}
private synchronized void setNewRoute(RouteCalculationResult res){
boolean updateRoute = !routeNodes.isEmpty();
routeNodes = res.getLocations();
directionInfo = res.getDirections();
listDistance = res.getListDistance();
currentDirectionInfo = 0;
currentRoute = 0;
if(isFollowingMode){
voiceRouter.newRouteIsCalculated(updateRoute);
}
for(IRouteInformationListener l : listeners){
l.newRouteIsCalculated(updateRoute);
}
}
public synchronized int getLeftDistance(){
if(listDistance != null && currentRoute < listDistance.length){
int dist = listDistance[currentRoute];
Location l = routeNodes.get(currentRoute);
if(lastFixedLocation != null){
dist += lastFixedLocation.distanceTo(l);
}
return dist;
}
return 0;
}
public Location getLocationFromRouteDirection(RouteDirectionInfo i){
if(i.routePointOffset < routeNodes.size()){
return routeNodes.get(i.routePointOffset);
}
return null;
}
public RouteDirectionInfo getNextRouteDirectionInfo(){
if(directionInfo != null && currentDirectionInfo < directionInfo.size() - 1){
return directionInfo.get(currentDirectionInfo + 1);
}
return null;
}
public RouteDirectionInfo getNextNextRouteDirectionInfo(){
if(directionInfo != null && currentDirectionInfo < directionInfo.size() - 2){
return directionInfo.get(currentDirectionInfo + 2);
}
return null;
}
public List<RouteDirectionInfo> getRouteDirections(){
if(directionInfo != null && currentDirectionInfo < directionInfo.size()){
if(currentDirectionInfo == 0){
return directionInfo;
}
if(currentDirectionInfo < directionInfo.size() - 1){
return directionInfo.subList(currentDirectionInfo + 1, directionInfo.size());
}
}
return Collections.emptyList();
}
public int getDistanceToNextRouteDirection() {
if (directionInfo != null && currentDirectionInfo < directionInfo.size()) {
int dist = listDistance[currentRoute];
if (currentDirectionInfo < directionInfo.size() - 1) {
dist -= listDistance[directionInfo.get(currentDirectionInfo + 1).routePointOffset];
}
if(lastFixedLocation != null){
dist += lastFixedLocation.distanceTo(routeNodes.get(currentRoute));
}
return dist;
}
return 0;
}
public synchronized int getLeftTime(){
if(directionInfo != null && currentDirectionInfo < directionInfo.size()){
int t = directionInfo.get(currentDirectionInfo).afterLeftTime;
int e = directionInfo.get(currentDirectionInfo).expectedTime;
if (e > 0) {
int passedDist = listDistance[directionInfo.get(currentDirectionInfo).routePointOffset] - listDistance[currentRoute];
int wholeDist = listDistance[directionInfo.get(currentDirectionInfo).routePointOffset];
if (currentDirectionInfo < directionInfo.size() - 1) {
wholeDist -= listDistance[directionInfo.get(currentDirectionInfo + 1).routePointOffset];
}
if (wholeDist > 0) {
t = (int) (t + ((float)e) * (1 - (float) passedDist / (float) wholeDist));
}
}
return t;
}
return 0;
}
public void calculateRoute(final Location start, final LatLon end, final List<Location> currentGPXRoute){
if(start == null || end == null){
return;
}
// temporary check while osmand offline router is not stable
RouteService serviceToUse= OsmandSettings.getRouterService(OsmandSettings.getPrefs(context));
if (serviceToUse == RouteService.OSMAND && !OsmandSettings.isOsmandRoutingServiceUsed(context)) {
double distance = MapUtils.getDistance(end, start.getLatitude(), start.getLongitude());
if (distance > DISTANCE_TO_USE_OSMAND_ROUTER) {
showMessage(context.getString(R.string.osmand_routing_experimental));
serviceToUse = RouteService.CLOUDMADE;
}
}
final RouteService service = serviceToUse;
if(currentRunningJob == null){
// do not evaluate very often
if (System.currentTimeMillis() - lastTimeEvaluatedRoute > evalWaitInterval) {
final boolean fastRouteMode = OsmandSettings.isFastRouteMode(context);
synchronized (this) {
currentRunningJob = new Thread(new Runnable() {
@Override
public void run() {
RouteCalculationResult res = provider.calculateRouteImpl(start, end, mode, service, context, currentGPXRoute, fastRouteMode);
synchronized (RoutingHelper.this) {
if (res.isCalculated()) {
setNewRoute(res);
// reset error wait interval
evalWaitInterval = 3000;
} else {
evalWaitInterval = evalWaitInterval * 4 / 3;
if (evalWaitInterval > 120000) {
evalWaitInterval = 120000;
}
}
currentRunningJob = null;
}
if (res.isCalculated()) {
int[] dist = res.getListDistance();
int l = dist != null && dist.length > 0 ? dist[0] : 0;
showMessage(context.getString(R.string.new_route_calculated_dist)
+ " : " + OsmAndFormatter.getFormattedDistance(l, context)); //$NON-NLS-1$
if (uiActivity instanceof MapActivity) {
// be aware that is non ui thread
((MapActivity) uiActivity).getMapView().refreshMap();
}
} else {
if (res.getErrorMessage() != null) {
showMessage(context.getString(R.string.error_calculating_route) + " : " + res.getErrorMessage()); //$NON-NLS-1$
} else if (res.getLocations() == null) {
showMessage(context.getString(R.string.error_calculating_route_occured));
} else {
showMessage(context.getString(R.string.empty_route_calculated));
}
}
lastTimeEvaluatedRoute = System.currentTimeMillis();
}
}, "Calculating route"); //$NON-NLS-1$
currentRunningJob.start();
}
}
}
}
public boolean isRouteBeingCalculated(){
return currentRunningJob != null;
}
private void showMessage(final String msg){
if (uiActivity != null) {
uiActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
if(uiActivity != null){
Toast.makeText(uiActivity, msg, Toast.LENGTH_SHORT).show();
}
}
});
}
}
public boolean hasPointsToShow(){
return finalLocation != null && !routeNodes.isEmpty();
}
protected Context getContext() {
return context;
}
public synchronized void fillLocationsToShow(double topLatitude, double leftLongitude, double bottomLatitude,double rightLongitude, List<Location> l){
l.clear();
boolean previousVisible = false;
if(lastFixedLocation != null){
if(leftLongitude <= lastFixedLocation.getLongitude() && lastFixedLocation.getLongitude() <= rightLongitude &&
bottomLatitude <= lastFixedLocation.getLatitude() && lastFixedLocation.getLatitude() <= topLatitude){
l.add(lastFixedLocation);
previousVisible = true;
}
}
for (int i = currentRoute; i < routeNodes.size(); i++) {
Location ls = routeNodes.get(i);
if(leftLongitude <= ls.getLongitude() && ls.getLongitude() <= rightLongitude &&
bottomLatitude <= ls.getLatitude() && ls.getLatitude() <= topLatitude){
l.add(ls);
if (!previousVisible) {
if (i > currentRoute) {
l.add(0, routeNodes.get(i - 1));
} else if (lastFixedLocation != null) {
l.add(0, lastFixedLocation);
}
}
previousVisible = true;
} else if(previousVisible){
l.add(ls);
previousVisible = false;
// do not continue make method more efficient (because it calls in UI thread)
// this break also has logical sense !
break;
}
}
}
public static class TurnType {
public static final String C = "C"; // continue (go straight) //$NON-NLS-1$
public static final String TL = "TL"; // turn left //$NON-NLS-1$
public static final String TSLL = "TSLL"; // turn slight left //$NON-NLS-1$
public static final String TSHL = "TSHL"; // turn sharp left //$NON-NLS-1$
public static final String TR = "TR"; // turn right //$NON-NLS-1$
public static final String TSLR = "TSLR"; // turn slight right //$NON-NLS-1$
public static final String TSHR = "TSHR"; // turn sharp right //$NON-NLS-1$
public static final String TU = "TU"; // U-turn //$NON-NLS-1$
public static String[] predefinedTypes = new String[] {C, TL, TSLL, TSHL, TR, TSLR, TSHR, TU};
public static TurnType valueOf(String s){
for(String v : predefinedTypes){
if(v.equals(s)){
return new TurnType(v);
}
}
if(s!= null && s.startsWith("EXIT")){ //$NON-NLS-1$
return getExitTurn(Integer.parseInt(s.substring(4)), 0);
}
return null;
}
private final String value;
private int exitOut;
// calculated CW head rotation if previous direction to NORTH
private float turnAngle;
public static TurnType getExitTurn(int out, float angle){
TurnType r = new TurnType("EXIT", out); //$NON-NLS-1$
r.setTurnAngle(angle);
return r;
}
private TurnType(String value, int exitOut){
this.value = value;
this.exitOut = exitOut;
}
// calculated CW head rotation if previous direction to NORTH
public float getTurnAngle() {
return turnAngle;
}
public void setTurnAngle(float turnAngle) {
this.turnAngle = turnAngle;
}
private TurnType(String value){
this.value = value;
}
public String getValue() {
return value;
}
public int getExitOut() {
return exitOut;
}
public boolean isRoundAbout(){
return value.equals("EXIT"); //$NON-NLS-1$
}
}
public static class RouteDirectionInfo {
public String descriptionRoute = ""; //$NON-NLS-1$
// expected time after route point
public int expectedTime;
public TurnType turnType;
// location when you should action (turn or go ahead)
public int routePointOffset;
// calculated vars
// after action (excluding expectedTime)
public int afterLeftTime;
// distance after action (for i.e. after turn to next turn)
public int distance;
}
}