package edu.vanderbilt.vm.guide.util;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import edu.vanderbilt.vm.guide.annotations.NeedsTesting;
import edu.vanderbilt.vm.guide.db.GuideDBConstants;
import edu.vanderbilt.vm.guide.db.GuideDBConstants.NodeTable;
import edu.vanderbilt.vm.guide.ui.listener.GeomancerListener;
/**
* Provide methods related to Geolocation and positioning. - at the start of the
* application, call activateGeolocation() which returns void and accepts no
* argument. - when the device's location is needed, call getDeviceLocation()
* which accepts no argument. It returns a Location object which can then be fed
* into findClosestPlace() which returns a Place object. These are
* array-transversal procedures, so there may be conflicts with the SQL approach
*
* @author abdulra1
*/
@NeedsTesting(lastModifiedDate = "12/22/12")
public class Geomancer {
private static final Logger logger = LoggerFactory.getLogger("util.Geomancer");
private static final double DEFAULT_LONGITUDE = -86.803889;
private static final double DEFAULT_LATITUDE = 36.147381;
public static final double FEET_PER_METER = 3.28083989501312;
public static final int FEET_PER_MILE = 5280;
private static Location sCurrLocation;
private static LocationManager sLocationManager;
private static LocationListener mLocListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
// Called when a new location is found
// by the network location provider.
sCurrLocation = location;
logger.info("Receiving location at lat/lon {},{}", location.getLatitude(),
location.getLongitude());
notifyObservers(location);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
};
private final static int DEFAULT_RADIUS = 5; // 5 meters, for you americans
// out there.
private final static int DEFAULT_TIMEOUT = 5000;
private static Location sDefaultLocation;
private static ArrayList<GeomancerListener> mListeners = new ArrayList<GeomancerListener>();
/**
* Find the closest place in the placeCursor to the given location
*
* @param location The location to find the closest place to
* @param placeCursor The cursor containing the places to search for
* @return The position in the cursor of the closest place
*/
public static int findClosestPlace(Location location, Cursor placeCursor) {
if (!placeCursor.moveToFirst()) {
// Cursor was empty
return -1;
}
int latIx = placeCursor.getColumnIndex(GuideDBConstants.PlaceTable.LATITUDE_COL);
int lonIx = placeCursor.getColumnIndex(GuideDBConstants.PlaceTable.LONGITUDE_COL);
if (latIx == -1 || lonIx == -1) {
throw new SQLException("Place cursor must have a lat and lon column");
}
double lat = placeCursor.getDouble(latIx);
double lon = placeCursor.getDouble(lonIx);
double shortestDist = findDistance(location.getLatitude(), location.getLongitude(), lat,
lon);
int closestIx = 0;
while (placeCursor.moveToNext()) {
lat = placeCursor.getDouble(latIx);
lon = placeCursor.getDouble(lonIx);
double dist = findDistance(location.getLatitude(), location.getLongitude(), lat, lon);
if (dist < shortestDist) {
shortestDist = dist;
closestIx = placeCursor.getPosition();
}
}
return closestIx;
}
public static int findClosestNodeId(Location location, Context c) {
SQLiteDatabase db = GlobalState.getReadableDatabase(c);
Cursor nodeCursor = db.query(NodeTable.NODE_TABLE_NAME, new String[] {
NodeTable.ID_COL, NodeTable.LAT_COL, NodeTable.LON_COL
}, null, null, null, null, null);
nodeCursor.moveToPosition(findClosestNode(location, nodeCursor));
return nodeCursor.getInt(nodeCursor.getColumnIndex(NodeTable.ID_COL));
}
/**
* Find the closest node in the nodeCursor to the given location
*
* @param location The location to find the closest node to
* @param nodeCursor The cursor containing the nodes to search for
* @return The position in the cursor of the closest node
*/
public static int findClosestNode(Location location, Cursor nodeCursor) {
if (!nodeCursor.moveToFirst()) {
// Cursor was empty
return -1;
}
int latIx = nodeCursor.getColumnIndex(GuideDBConstants.NodeTable.LAT_COL);
int lonIx = nodeCursor.getColumnIndex(GuideDBConstants.NodeTable.LON_COL);
if (latIx == -1 || lonIx == -1) {
throw new SQLException("Cursor must have a lat and lon column");
}
double lat = nodeCursor.getDouble(latIx);
double lon = nodeCursor.getDouble(lonIx);
double shortestDist = findDistance(location.getLatitude(), location.getLongitude(), lat,
lon);
int closestIx = 0;
while (nodeCursor.moveToNext()) {
lat = nodeCursor.getDouble(latIx);
lon = nodeCursor.getDouble(lonIx);
double dist = findDistance(location.getLatitude(), location.getLongitude(), lat, lon);
if (dist < shortestDist) {
shortestDist = dist;
closestIx = nodeCursor.getPosition();
}
}
return closestIx;
}
public static String getDistanceString(Location location) {
double distInFeet = location.distanceTo(Geomancer.getDeviceLocation()) * FEET_PER_METER;
String distStr;
if (distInFeet < 1000) {
// Use feet measurements
distStr = Integer.toString((int)distInFeet) + " ft";
} else {
// Use mile measurements
DecimalFormat df = new DecimalFormat("#.##");
distStr = df.format(distInFeet / FEET_PER_MILE) + " mi";
}
return distStr;
}
/**
* Setup the mechanism for determining device location. this method is
* called by GuideMain on application loading. Any activity that needs the
* device's location simply need to call getDeviceLocation()
*/
public static void activateGeolocation(Context ctx) {
if (sLocationManager == null) {
// Acquire a reference to the system Location Manager
sLocationManager = (LocationManager)ctx.getSystemService(Context.LOCATION_SERVICE);
}
String provider = sLocationManager.getBestProvider(getCriteriaA(), true);
if (provider != null) {
sLocationManager.requestLocationUpdates(provider, DEFAULT_TIMEOUT, DEFAULT_RADIUS,
mLocListener);
}
/*
* List<String> matchingProviders = sLocationManager.getProviders(
* getCriteriaA(), false); logger.trace("Found {} providers.",
* matchingProviders.size()); if (!matchingProviders.isEmpty()) { String
* provider = matchingProviders.get(0);
* sLocationManager.requestLocationUpdates(provider, DEFAULT_TIMEOUT,
* DEFAULT_RADIUS, mLocListener); } else {
* sLocationManager.requestLocationUpdates(
* LocationManager.NETWORK_PROVIDER, DEFAULT_TIMEOUT, DEFAULT_RADIUS,
* mLocListener); }
*/
logger.trace("Geolocation init done.");
}
public static double findDistance(double x1, double y1, double x2, double y2) {
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}
/**
* Gives the device location. The method is guaranteed to never return null.
* It always delivers d=(^o^d=)
*
* @return device's location
*/
public static Location getDeviceLocation() {
if (sCurrLocation == null) {
sCurrLocation = sLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
}
if (sCurrLocation == null) {
sCurrLocation = sLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
}
if (sCurrLocation == null) {
sCurrLocation = sLocationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);
}
if (sCurrLocation == null) {
sDefaultLocation.setTime((new Date()).getTime());
sCurrLocation = sDefaultLocation;
}
return sCurrLocation;
}
/**
* Provide Geomancer with fresh information on device location. This is
* intended for later on when we may have alternative methods to get that
* information other than GPS and radio.
*
* @param loc Current device Location
*/
public static void setDeviceLocation(Location loc) {
sCurrLocation = loc;
notifyObservers(loc);
}
static {
// Set up the default location
sDefaultLocation = new Location("Dummy");
sDefaultLocation.setLatitude(DEFAULT_LATITUDE);
sDefaultLocation.setLongitude(DEFAULT_LONGITUDE);
}
/**
* In order to get update on device location, make the Activity implement
* GeomancerListener and call: Geomancer.registerGeomancerListener(this)
* Make sure to call removeGeomancerListener() inside the onPause() callback
* in order to avoid memory leak.
*
* @param listener The Activity that implements GeomancerListener
*/
public static void registerGeomancerListener(GeomancerListener listener) {
mListeners.add(listener);
if (mListeners.size() == 1) {
String provider = sLocationManager.getBestProvider(getCriteriaA(), true);
if (provider != null) {
sLocationManager.requestLocationUpdates(provider, DEFAULT_TIMEOUT, DEFAULT_RADIUS,
mLocListener);
} else {
logger.error("Failed to get a Location Provider");
}
}
}
public static void removeGeomancerListener(GeomancerListener listener) {
mListeners.remove(listener);
if (mListeners.isEmpty()) {
sLocationManager.removeUpdates(mLocListener);
}
}
private static void notifyObservers(Location loc) {
for (GeomancerListener observer : mListeners) {
observer.updateLocation(loc);
}
}
private static Criteria getCriteriaA() {
Criteria crit = new Criteria();
crit.setAccuracy(Criteria.ACCURACY_FINE);
crit.setAltitudeRequired(false);
crit.setBearingRequired(false);
crit.setSpeedRequired(false);
crit.setCostAllowed(true);
return crit;
}
}