package cgeo.geocaching.maps.routing;
import cgeo.geocaching.CgeoApplication;
import cgeo.geocaching.location.Geopoint;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.utils.Log;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Xml;
import java.util.LinkedList;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
public final class Routing {
private static final double UPDATE_MIN_DISTANCE_KILOMETERS = 0.005;
private static final double MAX_ROUTING_DISTANCE_KILOMETERS = 10.0;
private static final double MIN_ROUTING_DISTANCE_KILOMETERS = 0.04;
private static final int UPDATE_MIN_DELAY_SECONDS = 3;
private static BRouterServiceConnection brouter;
private static Geopoint lastDirectionUpdatePoint;
@Nullable private static Geopoint[] lastRoutingPoints = null;
private static Geopoint lastDestination;
private static long timeLastUpdate;
private Routing() {
// utility class
}
public static void connect() {
if (brouter != null && brouter.isConnected()) {
//already connected
return;
}
brouter = new BRouterServiceConnection();
final Intent intent = new Intent();
intent.setClassName("btools.routingapp", "btools.routingapp.BRouterService");
if (!getContext().bindService(intent, brouter, Context.BIND_AUTO_CREATE)) {
brouter = null;
}
}
private static ContextWrapper getContext() {
return CgeoApplication.getInstance();
}
public static void disconnect() {
if (brouter != null && brouter.isConnected()) {
getContext().unbindService(brouter);
brouter = null;
}
}
/**
* Return a valid track (with at least two points, including the start and destination).
* In some cases (e.g., destination is too close or too far, path could not be found),
* a straight line will be returned.
*
* @param start the starting point
* @param destination the destination point
* @return a track with at least two points including the start and destination points
*/
@NonNull
public static Geopoint[] getTrack(final Geopoint start, final Geopoint destination) {
if (brouter == null || Settings.getRoutingMode() == RoutingMode.STRAIGHT) {
return defaultTrack(start, destination);
}
// avoid updating to frequently
final long timeNow = System.currentTimeMillis();
if ((timeNow - timeLastUpdate) < 1000 * UPDATE_MIN_DELAY_SECONDS) {
return ensureTrack(lastRoutingPoints, start, destination);
}
// Disable routing for huge distances
final float targetDistance = start.distanceTo(destination);
if (targetDistance > MAX_ROUTING_DISTANCE_KILOMETERS) {
return defaultTrack(start, destination);
}
// disable routing when near the target
if (targetDistance < MIN_ROUTING_DISTANCE_KILOMETERS) {
return defaultTrack(start, destination);
}
// Use cached route if current position has not changed more than 5m and we had a route
// TODO: Maybe adjust this to current zoomlevel
if (lastDirectionUpdatePoint != null && destination == lastDestination && start.distanceTo(lastDirectionUpdatePoint) < UPDATE_MIN_DISTANCE_KILOMETERS && lastRoutingPoints != null) {
return lastRoutingPoints;
}
// now really calculate a new route
lastDestination = destination;
lastRoutingPoints = calculateRouting(start, destination);
lastDirectionUpdatePoint = start;
timeLastUpdate = timeNow;
return ensureTrack(lastRoutingPoints, start, destination);
}
@NonNull
private static Geopoint[] ensureTrack(@Nullable final Geopoint[] routingPoints, final Geopoint start, final Geopoint destination) {
return routingPoints != null ? routingPoints : defaultTrack(start, destination);
}
@NonNull
private static Geopoint[] defaultTrack(final Geopoint start, final Geopoint destination) {
return new Geopoint[] { start, destination };
}
@Nullable
private static Geopoint[] calculateRouting(final Geopoint start, final Geopoint dest) {
final Bundle params = new Bundle();
params.putString("trackFormat", "gpx");
params.putDoubleArray("lats", new double[]{start.getLatitude(), dest.getLatitude()});
params.putDoubleArray("lons", new double[]{start.getLongitude(), dest.getLongitude()});
params.putString("v", Settings.getRoutingMode().parameterValue);
final String gpx = brouter.getTrackFromParams(params);
if (gpx == null) {
Log.i("brouter returned no data");
return null;
}
return parseGpxTrack(gpx, dest);
}
@Nullable
private static Geopoint[] parseGpxTrack(@NonNull final String gpx, final Geopoint destination) {
try {
final LinkedList<Geopoint> result = new LinkedList<>();
Xml.parse(gpx, new DefaultHandler() {
@Override
public void startElement(final String uri, final String localName, final String qName, final Attributes atts) throws SAXException {
if (qName.equalsIgnoreCase("trkpt")) {
final String lat = atts.getValue("lat");
if (lat != null) {
final String lon = atts.getValue("lon");
if (lon != null) {
result.add(new Geopoint(lat, lon));
}
}
}
}
});
// artificial straight line from track to target
result.add(destination);
return result.toArray(new Geopoint[result.size()]);
} catch (final SAXException e) {
Log.w("cannot parse brouter output of length " + gpx.length(), e);
}
return null;
}
public static void invalidateRouting() {
lastDirectionUpdatePoint = null;
timeLastUpdate = 0;
}
public static boolean isAvailable() {
return brouter != null;
}
}