blob: 545d7dc8927413e5637cd4aefede6c008ed73c2a [file] [log] [blame]
// Copyright 2007 The Android Open Source Project
package com.android.internal.location;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationProviderImpl;
import android.os.Bundle;
import android.util.Config;
import android.util.Log;
/**
* A dummy provider that returns positions interpolated from a sequence
* of caller-supplied waypoints. The waypoints are supplied as a
* String containing one or more numeric quadruples of the form:
* <br>
* <code>
* <time in millis> <latitude> <longitude> <altitude>
* </code>
*
* <p> The waypoints must be supplied in increasing timestamp order.
*
* <p> The time at which the provider is constructed is considered to
* be time 0, and further requests for positions will return a
* position that is linearly interpolated between the waypoints whose
* timestamps are closest to the amount of wall clock time that has
* elapsed since time 0.
*
* <p> Following the time of the last waypoint, the position of that
* waypoint will continue to be returned indefinitely.
*
* {@hide}
*/
public class TrackProvider extends LocationProviderImpl {
static final String LOG_TAG = "TrackProvider";
private static final long INTERVAL = 1000L;
private boolean mEnabled = true;
private double mLatitude;
private double mLongitude;
private boolean mHasAltitude;
private boolean mHasBearing;
private boolean mHasSpeed;
private double mAltitude;
private float mBearing;
private float mSpeed;
private Bundle mExtras;
private long mBaseTime;
private long mLastTime = -1L;
private long mTime;
private long mMinTime;
private long mMaxTime;
private List<Waypoint> mWaypoints = new ArrayList<Waypoint>();
private int mWaypointIndex = 0;
private boolean mRequiresNetwork = false;
private boolean mRequiresSatellite = false;
private boolean mRequiresCell = false;
private boolean mHasMonetaryCost = false;
private boolean mSupportsAltitude = true;
private boolean mSupportsSpeed = true;
private boolean mSupportsBearing = true;
private boolean mRepeat = false;
private int mPowerRequirement = Criteria.POWER_LOW;
private int mAccuracy = Criteria.ACCURACY_COARSE;
private float mTrackSpeed = 100.0f; // km/hr - default for kml tracks
private Location mInitialLocation;
private void close(Reader rdr) {
try {
if (rdr != null) {
rdr.close();
}
} catch (IOException e) {
Log.w(LOG_TAG, "Exception closing reader", e);
}
}
public void readTrack(File trackFile) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(trackFile), 8192);
String s;
long lastTime = -Long.MAX_VALUE;
while ((s = br.readLine()) != null) {
String[] tokens = s.split("\\s+");
if (tokens.length != 4 && tokens.length != 6) {
Log.e(LOG_TAG, "Got track \"" + s +
"\", wanted <time> <long> <lat> <alt> [<bearing> <speed>]");
continue;
}
long time;
double longitude, latitude, altitude;
try {
time = Long.parseLong(tokens[0]);
longitude = Double.parseDouble(tokens[1]);
latitude = Double.parseDouble(tokens[2]);
altitude = Double.parseDouble(tokens[3]);
} catch (NumberFormatException e) {
Log.e(LOG_TAG, "Got track \"" + s +
"\", wanted <time> <long> <lat> <alt> " +
"[<bearing> <speed>]", e);
continue;
}
Waypoint w = new Waypoint(getName(), time, latitude, longitude, altitude);
if (tokens.length >= 6) {
float bearing, speed;
try {
bearing = Float.parseFloat(tokens[4]);
speed = Float.parseFloat(tokens[5]);
w.setBearing(bearing);
w.setSpeed(speed);
} catch (NumberFormatException e) {
Log.e(LOG_TAG, "Ignoring bearing and speed \"" +
tokens[4] + "\", \"" + tokens[5] + "\"", e);
}
}
if (mInitialLocation == null) {
mInitialLocation = w.getLocation();
}
// Ignore waypoints whose time is less than or equal to 0 or
// the time of the previous waypoint
if (time < 0) {
Log.e(LOG_TAG, "Ignoring waypoint at negative time=" + time);
continue;
}
if (time <= lastTime) {
Log.e(LOG_TAG, "Ignoring waypoint at time=" + time +
" (< " + lastTime + ")");
continue;
}
mWaypoints.add(w);
lastTime = time;
}
setTimes();
return;
} catch (IOException e) {
Log.e(LOG_TAG, "Exception reading track file", e);
mWaypoints.clear();
} finally {
close(br);
}
}
public void readKml(File kmlFile) {
FileReader kmlReader = null;
try {
kmlReader = new FileReader(kmlFile);
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser xpp = factory.newPullParser();
xpp.setInput(kmlReader);
// Concatenate the text of each <coordinates> tag
boolean inCoordinates = false;
StringBuilder sb = new StringBuilder();
int eventType = xpp.getEventType();
do {
if (eventType == XmlPullParser.START_DOCUMENT) {
// do nothing
} else if (eventType == XmlPullParser.END_DOCUMENT) {
// do nothing
} else if (eventType == XmlPullParser.START_TAG) {
String startTagName = xpp.getName();
if (startTagName.equals("coordinates")) {
inCoordinates = true;
}
} else if (eventType == XmlPullParser.END_TAG) {
String endTagName = xpp.getName();
if (endTagName.equals("coordinates")) {
inCoordinates = false;
}
} else if (eventType == XmlPullParser.TEXT) {
if (inCoordinates) {
sb.append(xpp.getText());
sb.append(' ');
}
}
eventType = xpp.next();
} while (eventType != XmlPullParser.END_DOCUMENT);
String coordinates = sb.toString();
// Parse the "lon,lat,alt" triples and supply times
// for each waypoint based on a constant speed
Location loc = null;
double KM_PER_HOUR = mTrackSpeed;
double KM_PER_METER = 1.0 / 1000.0;
double MILLIS_PER_HOUR = 60.0 * 60.0 * 1000.0;
double MILLIS_PER_METER =
(1.0 / KM_PER_HOUR) * (KM_PER_METER) * (MILLIS_PER_HOUR);
long time = 0L;
StringTokenizer st = new StringTokenizer(coordinates, ", ");
while (st.hasMoreTokens()) {
try {
String lon = st.nextToken();
String lat = st.nextToken();
String alt = st.nextToken();
if (Config.LOGD) {
Log.d(LOG_TAG,
"lon=" + lon + ", lat=" + lat + ", alt=" + alt);
}
double nLongitude = Double.parseDouble(lon);
double nLatitude = Double.parseDouble(lat);
double nAltitude = Double.parseDouble(alt);
Location nLoc = new Location(getName());
nLoc.setLatitude(nLatitude);
nLoc.setLongitude(nLongitude);
if (loc != null) {
double distance = loc.distanceTo(nLoc);
if (Config.LOGD) {
Log.d(LOG_TAG, "distance = " + distance);
}
time += (long) (distance * MILLIS_PER_METER);
}
Waypoint w = new Waypoint(getName(), time,
nLatitude, nLongitude, nAltitude);
if (supportsSpeed()) {
w.setSpeed(mTrackSpeed);
}
if (supportsBearing()) {
w.setBearing(0.0f);
}
mWaypoints.add(w);
if (mInitialLocation == null) {
mInitialLocation = w.getLocation();
}
loc = nLoc;
} catch (NumberFormatException nfe) {
Log.e(LOG_TAG, "Got NumberFormatException reading KML data: " +
nfe, nfe);
}
}
setTimes();
return;
} catch (IOException ioe) {
mWaypoints.clear();
Log.e(LOG_TAG, "Exception reading KML data: " + ioe, ioe);
// fall through
} catch (XmlPullParserException xppe) {
mWaypoints.clear();
Log.e(LOG_TAG, "Exception reading KML data: " + xppe, xppe);
// fall through
} finally {
close(kmlReader);
}
}
public void readNmea(String name, File file) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(file), 8192);
String s;
String provider = getName();
NmeaParser parser = new NmeaParser(name);
while ((s = br.readLine()) != null) {
boolean newWaypoint = parser.parseSentence(s);
if (newWaypoint) {
Location loc = parser.getLocation();
Waypoint w = new Waypoint(loc);
mWaypoints.add(w);
// Log.i(TAG, "Got waypoint " + w);
}
}
setTimes();
return;
} catch (IOException ioe) {
Log.e(LOG_TAG, "Exception reading NMEA data: " + ioe);
mWaypoints.clear();
} finally {
close(br);
}
}
private static boolean booleanVal(String tf) {
return (tf == null) || (tf.equalsIgnoreCase("true"));
}
private static int intVal(String val) {
try {
return (val == null) ? 0 : Integer.parseInt(val);
} catch (NumberFormatException nfe) {
return 0;
}
}
private static float floatVal(String val) {
try {
return (val == null) ? 0 : Float.parseFloat(val);
} catch (NumberFormatException nfe) {
return 0.0f;
}
}
public void readProperties(File propertiesFile) {
BufferedReader br = null;
if (!propertiesFile.exists()) {
return;
}
try {
if (Config.LOGD) {
Log.d(LOG_TAG, "Loading properties file " +
propertiesFile.getPath());
}
br = new BufferedReader(new FileReader(propertiesFile), 8192);
String s;
while ((s = br.readLine()) != null) {
StringTokenizer st = new StringTokenizer(s);
String command = null;
String value = null;
if (!st.hasMoreTokens()) {
continue;
}
command = st.nextToken();
if (st.hasMoreTokens()) {
value = st.nextToken();
}
if (command.equalsIgnoreCase("requiresNetwork")) {
setRequiresNetwork(booleanVal(value));
} else if (command.equalsIgnoreCase("requiresSatellite")) {
setRequiresSatellite(booleanVal(value));
} else if (command.equalsIgnoreCase("requiresCell")) {
setRequiresCell(booleanVal(value));
} else if (command.equalsIgnoreCase("hasMonetaryCost")) {
setHasMonetaryCost(booleanVal(value));
} else if (command.equalsIgnoreCase("supportsAltitude")) {
setSupportsAltitude(booleanVal(value));
} else if (command.equalsIgnoreCase("supportsBearing")) {
setSupportsBearing(booleanVal(value));
} else if (command.equalsIgnoreCase("repeat")) {
setRepeat(booleanVal(value));
} else if (command.equalsIgnoreCase("supportsSpeed")) {
setSupportsSpeed(booleanVal(value));
} else if (command.equalsIgnoreCase("powerRequirement")) {
setPowerRequirement(intVal(value));
} else if (command.equalsIgnoreCase("accuracy")) {
setAccuracy(intVal(value));
} else if (command.equalsIgnoreCase("trackspeed")) {
setTrackSpeed(floatVal(value));
} else {
Log.e(LOG_TAG, "Unknown command \"" + command + "\"");
}
}
} catch (IOException ioe) {
Log.e(LOG_TAG, "IOException reading properties file " +
propertiesFile.getPath(), ioe);
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
Log.w(LOG_TAG, "IOException closing properties file " +
propertiesFile.getPath(), e);
}
}
}
public TrackProvider(String name) {
super(name);
setTimes();
}
public TrackProvider(String name, File file) {
this(name);
String filename = file.getName();
if (filename.endsWith("kml")) {
readKml(file);
} else if (filename.endsWith("nmea")) {
readNmea(getName(), file);
} else if (filename.endsWith("track")) {
readTrack(file);
} else {
Log.e(LOG_TAG, "Can't initialize TrackProvider from file " +
filename + " (not *kml, *nmea, or *track)");
}
setTimes();
}
private void setTimes() {
mBaseTime = System.currentTimeMillis();
if (mWaypoints.size() >= 2) {
mMinTime = mWaypoints.get(0).getTime();
mMaxTime = mWaypoints.get(mWaypoints.size() - 1).getTime();
} else {
mMinTime = mMaxTime = 0;
}
}
private double interp(double d0, double d1, float frac) {
return d0 + frac * (d1 - d0);
}
private void update() {
// Don't update the position at all unless INTERVAL milliseconds
// have passed since the last request
long time = System.currentTimeMillis() - mBaseTime;
if (time - mLastTime < INTERVAL) {
return;
}
List<Waypoint> waypoints = mWaypoints;
if (waypoints == null) {
return;
}
int size = waypoints.size();
if (size < 2) {
return;
}
long t = time;
if (t < mMinTime) {
t = mMinTime;
}
if (mRepeat) {
t -= mMinTime;
long deltaT = mMaxTime - mMinTime;
t %= 2 * deltaT;
if (t > deltaT) {
t = 2 * deltaT - t;
}
t += mMinTime;
} else if (t > mMaxTime) {
t = mMaxTime;
}
// Locate the time interval for the current time
// We slide the window since we don't expect to move
// much between calls
Waypoint w0 = waypoints.get(mWaypointIndex);
Waypoint w1 = waypoints.get(mWaypointIndex + 1);
// If the right end of the current interval is too early,
// move forward to the next waypoint
while (t > w1.getTime()) {
w0 = w1;
w1 = waypoints.get(++mWaypointIndex + 1);
}
// If the left end of the current interval is too late,
// move back to the previous waypoint
while (t < w0.getTime()) {
w1 = w0;
w0 = waypoints.get(--mWaypointIndex);
}
// Now we know that w0.mTime <= t <= w1.mTime
long w0Time = w0.getTime();
long w1Time = w1.getTime();
long dt = w1Time - w0Time;
float frac = (dt == 0) ? 0 : ((float) (t - w0Time) / dt);
mLatitude = interp(w0.getLatitude(), w1.getLatitude(), frac);
mLongitude = interp(w0.getLongitude(), w1.getLongitude(), frac);
mHasAltitude = w0.hasAltitude() && w1.hasAltitude();
if (mSupportsAltitude && mHasAltitude) {
mAltitude = interp(w0.getAltitude(), w1.getAltitude(), frac);
}
if (mSupportsBearing) {
mHasBearing = frac <= 0.5f ? w0.hasBearing() : w1.hasBearing();
if (mHasBearing) {
mBearing = frac <= 0.5f ? w0.getBearing() : w1.getBearing();
}
}
if (mSupportsSpeed) {
mHasSpeed = frac <= 0.5f ? w0.hasSpeed() : w1.hasSpeed();
if (mHasSpeed) {
mSpeed = frac <= 0.5f ? w0.getSpeed() : w1.getSpeed();
}
}
mLastTime = time;
mTime = time;
}
public void setRequiresNetwork(boolean requiresNetwork) {
mRequiresNetwork = requiresNetwork;
}
@Override public boolean requiresNetwork() {
return mRequiresNetwork;
}
public void setRequiresSatellite(boolean requiresSatellite) {
mRequiresSatellite = requiresSatellite;
}
@Override public boolean requiresSatellite() {
return mRequiresSatellite;
}
public void setRequiresCell(boolean requiresCell) {
mRequiresCell = requiresCell;
}
@Override public boolean requiresCell() {
return mRequiresCell;
}
public void setHasMonetaryCost(boolean hasMonetaryCost) {
mHasMonetaryCost = hasMonetaryCost;
}
@Override public boolean hasMonetaryCost() {
return mHasMonetaryCost;
}
public void setSupportsAltitude(boolean supportsAltitude) {
mSupportsAltitude = supportsAltitude;
}
@Override public boolean supportsAltitude() {
return mSupportsAltitude;
}
public void setSupportsSpeed(boolean supportsSpeed) {
mSupportsSpeed = supportsSpeed;
}
@Override public boolean supportsSpeed() {
return mSupportsSpeed;
}
public void setSupportsBearing(boolean supportsBearing) {
mSupportsBearing = supportsBearing;
}
@Override public boolean supportsBearing() {
return mSupportsBearing;
}
public void setRepeat(boolean repeat) {
mRepeat = repeat;
}
public void setPowerRequirement(int powerRequirement) {
if (powerRequirement < Criteria.POWER_LOW ||
powerRequirement > Criteria.POWER_HIGH) {
throw new IllegalArgumentException("powerRequirement = " +
powerRequirement);
}
mPowerRequirement = powerRequirement;
}
@Override public int getPowerRequirement() {
return mPowerRequirement;
}
public void setAccuracy(int accuracy) {
mAccuracy = accuracy;
}
@Override public int getAccuracy() {
return mAccuracy;
}
public void setTrackSpeed(float trackSpeed) {
mTrackSpeed = trackSpeed;
}
@Override public void enable() {
mEnabled = true;
}
@Override public void disable() {
mEnabled = false;
}
@Override public boolean isEnabled() {
return mEnabled;
}
@Override public int getStatus(Bundle extras) {
return AVAILABLE;
}
@Override public boolean getLocation(Location l) {
if (mEnabled) {
update();
l.setProvider(getName());
l.setTime(mTime + mBaseTime);
l.setLatitude(mLatitude);
l.setLongitude(mLongitude);
if (mSupportsAltitude && mHasAltitude) {
l.setAltitude(mAltitude);
}
if (mSupportsBearing && mHasBearing) {
l.setBearing(mBearing);
}
if (mSupportsSpeed && mHasSpeed) {
l.setSpeed(mSpeed);
}
if (mExtras != null) {
l.setExtras(mExtras);
}
return true;
} else {
return false;
}
}
public Location getInitialLocation() {
return mInitialLocation;
}
}
/**
* A simple tuple of (time stamp, latitude, longitude, altitude), plus optional
* extras.
*
* {@hide}
*/
class Waypoint {
public Location mLocation;
public Waypoint(Location location) {
mLocation = location;
}
public Waypoint(String providerName, long time, double latitude, double longitude,
double altitude) {
mLocation = new Location(providerName);
mLocation.setTime(time);
mLocation.setLatitude(latitude);
mLocation.setLongitude(longitude);
mLocation.setAltitude(altitude);
}
public long getTime() {
return mLocation.getTime();
}
public double getLatitude() {
return mLocation.getLatitude();
}
public double getLongitude() {
return mLocation.getLongitude();
}
public boolean hasAltitude() {
return mLocation.hasAltitude();
}
public double getAltitude() {
return mLocation.getAltitude();
}
public boolean hasBearing() {
return mLocation.hasBearing();
}
public void setBearing(float bearing) {
mLocation.setBearing(bearing);
}
public float getBearing() {
return mLocation.getBearing();
}
public boolean hasSpeed() {
return mLocation.hasSpeed();
}
public void setSpeed(float speed) {
mLocation.setSpeed(speed);
}
public float getSpeed() {
return mLocation.getSpeed();
}
public Bundle getExtras() {
return mLocation.getExtras();
}
public Location getLocation() {
return new Location(mLocation);
}
@Override public String toString() {
return "Waypoint[mLocation=" + mLocation + "]";
}
}