// 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 + "]"; } }