Estimated arrival display in progress

--HG--
branch : estarrival
This commit is contained in:
dkeen@dkeen-laptop 2012-04-12 18:07:55 -07:00
parent 45293dc694
commit 58f623b1cf
27 changed files with 840 additions and 142 deletions

View File

@ -5,6 +5,7 @@
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry kind="lib" path="libs/commons-io-2.0.1.jar"/>
<classpathentry kind="lib" path="libs/commons-lang3-3.1.jar"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="lib" path="libs/android-support-v4.jar"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

BIN
libs/android-support-v4.jar Normal file

Binary file not shown.

View File

@ -43,9 +43,10 @@
android:layout_below="@id/topRow"
android:src="@drawable/xfer" />
<TextView
<TextSwitcher
android:id="@+id/trainLengthText"
style="@style/DepartureTrainLengthText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/topRow"
android:layout_toRightOf="@id/destinationColorBar" />

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/DepartureTrainLengthText" />

View File

@ -1,6 +1,8 @@
package com.dougkeen.bart;
import com.dougkeen.bart.data.RoutesColumns;
import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Station;
import android.app.Activity;
import android.content.ContentValues;

View File

@ -8,12 +8,15 @@ import android.graphics.drawable.GradientDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextSwitcher;
import android.widget.TextView;
import android.widget.ViewSwitcher.ViewFactory;
import com.dougkeen.bart.data.Departure;
import com.dougkeen.bart.model.Departure;
public class DepartureArrayAdapter extends ArrayAdapter<Departure> {
@ -46,6 +49,8 @@ public class DepartureArrayAdapter extends ArrayAdapter<Departure> {
super(context, textViewResourceId);
}
private String currentViewSwitcherText;
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
@ -59,8 +64,27 @@ public class DepartureArrayAdapter extends ArrayAdapter<Departure> {
Departure departure = getItem(position);
((TextView) view.findViewById(R.id.destinationText)).setText(departure
.getDestination().toString());
((TextView) view.findViewById(R.id.trainLengthText)).setText(departure
.getTrainLengthText());
TextSwitcher textSwitcher = (TextSwitcher) view
.findViewById(R.id.trainLengthText);
initTextSwitcher(textSwitcher);
if (System.currentTimeMillis() % 6000 > 3000) {
String trainLengthText = departure.getTrainLengthText();
if (currentViewSwitcherText == null
|| !currentViewSwitcherText.equals(trainLengthText)) {
textSwitcher.setText(trainLengthText);
currentViewSwitcherText = trainLengthText;
}
} else {
String arrivalText = "Est. arrival "
+ departure.getEstimatedArrivalTimeText(getContext());
if (currentViewSwitcherText == null
|| !currentViewSwitcherText.equals(arrivalText)) {
textSwitcher.setText(arrivalText);
currentViewSwitcherText = arrivalText;
}
}
ImageView colorBar = (ImageView) view
.findViewById(R.id.destinationColorBar);
((GradientDrawable) colorBar.getDrawable()).setColor(Color
@ -87,4 +111,19 @@ public class DepartureArrayAdapter extends ArrayAdapter<Departure> {
return view;
}
private void initTextSwitcher(TextSwitcher textSwitcher) {
if (textSwitcher.getInAnimation() == null) {
textSwitcher.setFactory(new ViewFactory() {
public View makeView() {
return LayoutInflater.from(getContext()).inflate(
R.layout.train_length_arrival_textview, null);
}
});
textSwitcher.setInAnimation(AnimationUtils.loadAnimation(
getContext(), android.R.anim.slide_in_left));
textSwitcher.setOutAnimation(AnimationUtils.loadAnimation(
getContext(), android.R.anim.slide_out_right));
}
}
}

View File

@ -29,6 +29,9 @@ import android.widget.TextView;
import com.dougkeen.bart.data.CursorUtils;
import com.dougkeen.bart.data.RoutesColumns;
import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Station;
import com.dougkeen.bart.networktasks.GetRouteFareTask;
public class RoutesListActivity extends ListActivity {
private static final TimeZone PACIFIC_TIME = TimeZone
@ -51,7 +54,9 @@ public class RoutesListActivity extends ListActivity {
mQuery = managedQuery(Constants.FAVORITE_CONTENT_URI, new String[] {
RoutesColumns._ID.string, RoutesColumns.FROM_STATION.string,
RoutesColumns.TO_STATION.string, RoutesColumns.FARE.string,
RoutesColumns.FARE_LAST_UPDATED.string }, null, null,
RoutesColumns.FARE_LAST_UPDATED.string,
RoutesColumns.AVERAGE_TRIP_SAMPLE_COUNT.string,
RoutesColumns.AVERAGE_TRIP_LENGTH.string }, null, null,
RoutesColumns._ID.string);
refreshFares();

View File

@ -3,6 +3,7 @@ package com.dougkeen.bart;
import java.util.List;
import android.app.ListActivity;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
@ -22,10 +23,16 @@ import android.widget.ArrayAdapter;
import android.widget.TextView;
import android.widget.Toast;
import com.dougkeen.bart.GetRealTimeDeparturesTask.Params;
import com.dougkeen.bart.data.Departure;
import com.dougkeen.bart.data.RealTimeDepartures;
import com.dougkeen.bart.data.RoutesColumns;
import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Departure;
import com.dougkeen.bart.model.ScheduleInformation;
import com.dougkeen.bart.model.ScheduleItem;
import com.dougkeen.bart.model.StationPair;
import com.dougkeen.bart.model.RealTimeDepartures;
import com.dougkeen.bart.model.Station;
import com.dougkeen.bart.networktasks.GetRealTimeDeparturesTask;
import com.dougkeen.bart.networktasks.GetScheduleInformationTask;
public class ViewDeparturesActivity extends ListActivity {
@ -35,12 +42,17 @@ public class ViewDeparturesActivity extends ListActivity {
private Station mOrigin;
private Station mDestination;
private int mAverageTripLength;
private int mAverageTripSampleCount;
private ArrayAdapter<Departure> mDeparturesAdapter;
private ScheduleInformation mLatestScheduleInfo;
private TextView mListTitleView;
private AsyncTask<Params, Integer, RealTimeDepartures> mGetDeparturesTask;
private AsyncTask<StationPair, Integer, RealTimeDepartures> mGetDeparturesTask;
private AsyncTask<StationPair, Integer, ScheduleInformation> mGetScheduleInformationTask;
private boolean mIsAutoUpdating = false;
@ -52,7 +64,8 @@ public class ViewDeparturesActivity extends ListActivity {
private PowerManager.WakeLock mWakeLock;
private boolean mDataFetchIsPending;
private boolean mDepartureFetchIsPending;
private boolean mScheduleFetchIsPending;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -69,13 +82,18 @@ public class ViewDeparturesActivity extends ListActivity {
Cursor cursor = managedQuery(mUri, new String[] {
RoutesColumns.FROM_STATION.string,
RoutesColumns.TO_STATION.string }, null, null, null);
RoutesColumns.TO_STATION.string,
RoutesColumns.AVERAGE_TRIP_LENGTH.string,
RoutesColumns.AVERAGE_TRIP_SAMPLE_COUNT.string }, null, null,
null);
if (!cursor.moveToFirst()) {
throw new IllegalStateException("URI not found: " + mUri.toString());
}
mOrigin = Station.getByAbbreviation(cursor.getString(0));
mDestination = Station.getByAbbreviation(cursor.getString(1));
mAverageTripLength = cursor.getInt(2);
mAverageTripSampleCount = cursor.getInt(3);
String header = "Departures:\n" + mOrigin.name + " to "
+ mDestination.name;
@ -114,7 +132,11 @@ public class ViewDeparturesActivity extends ListActivity {
private void cancelDataFetch() {
if (mGetDeparturesTask != null) {
mGetDeparturesTask.cancel(true);
mDataFetchIsPending = false;
mDepartureFetchIsPending = false;
}
if (mGetScheduleInformationTask != null) {
mGetScheduleInformationTask.cancel(true);
mScheduleFetchIsPending = false;
}
}
@ -132,7 +154,7 @@ public class ViewDeparturesActivity extends ListActivity {
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
if (!mDataFetchIsPending) {
if (!mDepartureFetchIsPending) {
fetchLatestDepartures();
}
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
@ -162,7 +184,7 @@ public class ViewDeparturesActivity extends ListActivity {
mGetDeparturesTask = new GetRealTimeDeparturesTask() {
@Override
public void onResult(RealTimeDepartures result) {
mDataFetchIsPending = false;
mDepartureFetchIsPending = false;
Log.i(Constants.TAG, "Processing data from server");
processLatestDepartures(result);
Log.i(Constants.TAG, "Done processing data from server");
@ -170,19 +192,55 @@ public class ViewDeparturesActivity extends ListActivity {
@Override
public void onError(Exception e) {
mDataFetchIsPending = false;
mDepartureFetchIsPending = false;
Log.w(Constants.TAG, e.getMessage(), e);
Toast.makeText(ViewDeparturesActivity.this,
R.string.could_not_connect, Toast.LENGTH_LONG).show();
((TextView) findViewById(android.R.id.empty))
.setText(R.string.could_not_connect);
// Try again in 60s
scheduleDataFetch(60000);
scheduleDepartureFetch(60000);
}
};
Log.i(Constants.TAG, "Fetching data from server");
mGetDeparturesTask.execute(new GetRealTimeDeparturesTask.Params(
mOrigin, mDestination));
mGetDeparturesTask.execute(new StationPair(mOrigin, mDestination));
}
private void fetchLatestSchedule() {
if (!hasWindowFocus())
return;
if (mGetScheduleInformationTask != null
&& mGetScheduleInformationTask.getStatus().equals(
AsyncTask.Status.RUNNING)) {
// Don't overlap fetches
return;
}
mGetScheduleInformationTask = new GetScheduleInformationTask() {
@Override
public void onResult(ScheduleInformation result) {
mScheduleFetchIsPending = false;
Log.i(Constants.TAG, "Processing data from server");
mLatestScheduleInfo = result;
applyScheduleInformation();
Log.i(Constants.TAG, "Done processing data from server");
}
@Override
public void onError(Exception e) {
mScheduleFetchIsPending = false;
Log.w(Constants.TAG, e.getMessage(), e);
Toast.makeText(ViewDeparturesActivity.this,
R.string.could_not_connect, Toast.LENGTH_LONG).show();
((TextView) findViewById(android.R.id.empty))
.setText(R.string.could_not_connect);
// Try again in 60s
scheduleScheduleInfoFetch(60000);
}
};
Log.i(Constants.TAG, "Fetching data from server");
mGetScheduleInformationTask.execute(new StationPair(mOrigin,
mDestination));
}
protected void processLatestDepartures(RealTimeDepartures result) {
@ -238,11 +296,12 @@ public class ViewDeparturesActivity extends ListActivity {
needsBetterAccuracy = true;
}
mDeparturesAdapter.notifyDataSetChanged();
requestScheduleIfNecessary();
if (hasWindowFocus() && firstDeparture != null) {
if (needsBetterAccuracy || firstDeparture.hasDeparted()) {
// Get more data in 20s
scheduleDataFetch(20000);
scheduleDepartureFetch(20000);
} else {
// Get more 90 seconds before next train arrives, right when
// next train arrives, or 3 minutes, whichever is sooner
@ -262,7 +321,7 @@ public class ViewDeparturesActivity extends ListActivity {
interval = 20000;
}
scheduleDataFetch(interval);
scheduleDepartureFetch(interval);
}
if (!mIsAutoUpdating) {
mIsAutoUpdating = true;
@ -272,15 +331,99 @@ public class ViewDeparturesActivity extends ListActivity {
}
}
private void scheduleDataFetch(int millisUntilExecute) {
if (!mDataFetchIsPending) {
private void requestScheduleIfNecessary() {
if (mDeparturesAdapter.getCount() == 0) {
return;
}
if (mLatestScheduleInfo == null) {
fetchLatestSchedule();
return;
}
Departure lastDeparture = mDeparturesAdapter.getItem(mDeparturesAdapter
.getCount() - 1);
if (mLatestScheduleInfo.getLatestDepartureTime() < lastDeparture
.getMeanEstimate()) {
fetchLatestSchedule();
return;
}
}
private void applyScheduleInformation() {
int localAverageLength = mLatestScheduleInfo.getAverageTripLength();
int departuresCount = mDeparturesAdapter.getCount();
int lastSearchIndex = 0;
int tripCount = mLatestScheduleInfo.getTrips().size();
boolean departureUpdated = false;
for (int departureIndex = 0; departureIndex < departuresCount; departureIndex++) {
Departure departure = mDeparturesAdapter.getItem(departureIndex);
for (int i = lastSearchIndex; i < tripCount; i++) {
ScheduleItem trip = mLatestScheduleInfo.getTrips().get(i);
long departTimeDiff = Math.abs(trip.getDepartureTime()
- departure.getMeanEstimate());
if (departTimeDiff <= (60000 + departure
.getUncertaintySeconds() * 1000)
&& departure.getEstimatedTripTime() != trip
.getTripLength()) {
departure.setEstimatedTripTime(trip.getTripLength());
lastSearchIndex = i;
departureUpdated = true;
break;
}
}
if (!departure.hasEstimatedTripTime() && localAverageLength > 0) {
departure.setEstimatedTripTime(localAverageLength);
} else if (!departure.hasEstimatedTripTime()) {
departure.setEstimatedTripTime(mAverageTripLength);
}
}
if (departureUpdated) {
mDeparturesAdapter.notifyDataSetChanged();
}
// Update global average
if (mLatestScheduleInfo.getTripCountForAverage() > 0) {
int newAverageSampleCount = mAverageTripSampleCount
+ mLatestScheduleInfo.getTripCountForAverage();
int newAverage = (mAverageTripLength * mAverageTripSampleCount + localAverageLength
* mLatestScheduleInfo.getTripCountForAverage())
/ newAverageSampleCount;
ContentValues contentValues = new ContentValues();
contentValues.put(RoutesColumns.AVERAGE_TRIP_LENGTH.string,
newAverage);
contentValues.put(RoutesColumns.AVERAGE_TRIP_SAMPLE_COUNT.string,
newAverageSampleCount);
getContentResolver().update(mUri, contentValues, null, null);
}
}
private void scheduleDepartureFetch(int millisUntilExecute) {
if (!mDepartureFetchIsPending) {
mListTitleView.postDelayed(new Runnable() {
public void run() {
fetchLatestDepartures();
}
}, millisUntilExecute);
mDataFetchIsPending = true;
Log.i(Constants.TAG, "Scheduled another data fetch in "
mDepartureFetchIsPending = true;
Log.i(Constants.TAG, "Scheduled another departure fetch in "
+ millisUntilExecute / 1000 + "s");
}
}
private void scheduleScheduleInfoFetch(int millisUntilExecute) {
if (!mScheduleFetchIsPending) {
mListTitleView.postDelayed(new Runnable() {
public void run() {
fetchLatestSchedule();
}
}, millisUntilExecute);
mScheduleFetchIsPending = true;
Log.i(Constants.TAG, "Scheduled another schedule fetch in "
+ millisUntilExecute / 1000 + "s");
}
}

View File

@ -2,7 +2,7 @@ package com.dougkeen.bart.data;
import java.util.HashMap;
import com.dougkeen.bart.Constants;
import com.dougkeen.bart.model.Constants;
import android.content.ContentProvider;
import android.content.ContentUris;
@ -45,6 +45,10 @@ public class BartContentProvider extends ContentProvider {
RoutesColumns.FARE.string);
sFavoritesProjectionMap.put(RoutesColumns.FARE_LAST_UPDATED.string,
RoutesColumns.FARE_LAST_UPDATED.string);
sFavoritesProjectionMap.put(RoutesColumns.AVERAGE_TRIP_SAMPLE_COUNT.string,
RoutesColumns.AVERAGE_TRIP_SAMPLE_COUNT.string);
sFavoritesProjectionMap.put(RoutesColumns.AVERAGE_TRIP_LENGTH.string,
RoutesColumns.AVERAGE_TRIP_LENGTH.string);
}
private DatabaseHelper mDatabaseHelper;

View File

@ -15,7 +15,7 @@ import android.util.Log;
public class DatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "bart.dougkeen.db";
private static final int DATABASE_VERSION = 2;
private static final int DATABASE_VERSION = 4;
public static final String FAVORITES_TABLE_NAME = "Favorites";
@ -34,7 +34,9 @@ public class DatabaseHelper extends SQLiteOpenHelper {
+ RoutesColumns.FROM_STATION.getColumnDef() + ", "
+ RoutesColumns.TO_STATION.getColumnDef() + ", "
+ RoutesColumns.FARE.getColumnDef() + ", "
+ RoutesColumns.FARE_LAST_UPDATED.getColumnDef() + ");");
+ RoutesColumns.FARE_LAST_UPDATED.getColumnDef() + ", "
+ RoutesColumns.AVERAGE_TRIP_SAMPLE_COUNT.getColumnDef() + ", "
+ RoutesColumns.AVERAGE_TRIP_LENGTH.getColumnDef() + ");");
}
@Override

View File

@ -5,7 +5,9 @@ public enum RoutesColumns {
FROM_STATION("FROM_STATION", "TEXT", false),
TO_STATION("TO_STATION", "TEXT", false),
FARE("FARE", "TEXT", true),
FARE_LAST_UPDATED("FARE_LAST_UPDATED", "INTEGER", true);
FARE_LAST_UPDATED("FARE_LAST_UPDATED", "INTEGER", true),
AVERAGE_TRIP_SAMPLE_COUNT("AVE_TRIP_SAMPLE_COUNT", "INTEGER", true),
AVERAGE_TRIP_LENGTH("AVE_TRIP_LENGTH", "INTEGER", true);
// This class cannot be instantiated

View File

@ -1,4 +1,4 @@
package com.dougkeen.bart;
package com.dougkeen.bart.model;
import android.net.Uri;
@ -10,6 +10,6 @@ public class Constants {
+ AUTHORITY + "/favorites");
public static final String MAP_URL = "http://m.bart.gov/images/global/system-map29.gif";
public static final String TAG = "BartCatcher";
public static final String TAG = "BartRunner";
public static final String API_KEY = "5LD9-IAYI-TRAT-MHHW";
}

View File

@ -1,10 +1,11 @@
package com.dougkeen.bart.data;
package com.dougkeen.bart.model;
import java.util.Date;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import com.dougkeen.bart.Line;
import com.dougkeen.bart.Station;
import android.text.format.DateFormat;
public class Departure implements Parcelable, Comparable<Departure> {
private static final int ESTIMATE_EQUALS_TOLERANCE_MILLIS = 59999;
@ -45,6 +46,8 @@ public class Departure implements Parcelable, Comparable<Departure> {
private long minEstimate;
private long maxEstimate;
private int estimatedTripTime;
public Station getDestination() {
return destination;
}
@ -149,6 +152,18 @@ public class Departure implements Parcelable, Comparable<Departure> {
this.maxEstimate = maxEstimate;
}
public int getEstimatedTripTime() {
return estimatedTripTime;
}
public void setEstimatedTripTime(int estimatedTripTime) {
this.estimatedTripTime = estimatedTripTime;
}
public boolean hasEstimatedTripTime() {
return this.estimatedTripTime > 0;
}
public int getUncertaintySeconds() {
return (int) (maxEstimate - minEstimate + 1000) / 2000;
}
@ -162,8 +177,24 @@ public class Departure implements Parcelable, Comparable<Departure> {
}
public int getMeanSecondsLeft() {
return (int) (((getMinEstimate() + getMaxEstimate()) / 2 - System
.currentTimeMillis()) / 1000);
return (int) ((getMeanEstimate() - System.currentTimeMillis()) / 1000);
}
public long getMeanEstimate() {
return (getMinEstimate() + getMaxEstimate()) / 2;
}
public long getEstimatedArrivalTime() {
return getMeanEstimate() + getEstimatedTripTime();
}
public String getEstimatedArrivalTimeText(Context context) {
if (getEstimatedTripTime() > 0) {
return DateFormat.getTimeFormat(context).format(
new Date(getEstimatedArrivalTime()));
} else {
return "";
}
}
public boolean hasDeparted() {

View File

@ -1,4 +1,4 @@
package com.dougkeen.bart;
package com.dougkeen.bart.model;
import java.util.ArrayList;
import java.util.Arrays;

View File

@ -1,11 +1,9 @@
package com.dougkeen.bart.data;
package com.dougkeen.bart.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.dougkeen.bart.Route;
import com.dougkeen.bart.Station;
public class RealTimeDepartures {
public RealTimeDepartures(Station origin, Station destination,

View File

@ -1,4 +1,4 @@
package com.dougkeen.bart;
package com.dougkeen.bart.model;
public class Route {
private Station origin;

View File

@ -0,0 +1,91 @@
package com.dougkeen.bart.model;
import java.util.ArrayList;
import java.util.List;
import android.R.integer;
public class ScheduleInformation {
public ScheduleInformation(Station origin, Station destination) {
super();
this.origin = origin;
this.destination = destination;
}
private Station origin;
private Station destination;
private long date;
private List<ScheduleItem> trips;
public Station getOrigin() {
return origin;
}
public void setOrigin(Station origin) {
this.origin = origin;
}
public Station getDestination() {
return destination;
}
public void setDestination(Station destination) {
this.destination = destination;
}
public long getDate() {
return date;
}
public void setDate(long date) {
this.date = date;
}
public List<ScheduleItem> getTrips() {
if (trips == null) {
trips = new ArrayList<ScheduleItem>();
}
return trips;
}
public void setTrips(List<ScheduleItem> trips) {
this.trips = trips;
}
public void addTrip(ScheduleItem trip) {
getTrips().add(trip);
}
public long getLatestDepartureTime() {
if (getTrips().isEmpty())
return -1;
else
return getTrips().get(getTrips().size() - 1).getDepartureTime();
}
private int aveTripLength = -1;
private int tripCount = 0;
public int getAverageTripLength() {
if (aveTripLength < 0) {
int sum = 0;
for (ScheduleItem trip : getTrips()) {
int tripLength = trip.getTripLength();
if (tripLength > 0) {
sum += tripLength;
tripCount++;
}
}
if (tripCount > 0) {
aveTripLength = sum / tripCount;
}
}
return aveTripLength;
}
public int getTripCountForAverage() {
getAverageTripLength();
return tripCount;
}
}

View File

@ -0,0 +1,86 @@
package com.dougkeen.bart.model;
public class ScheduleItem {
public ScheduleItem() {
super();
}
public ScheduleItem(Station origin, Station destination) {
super();
this.origin = origin;
this.destination = destination;
}
private Station origin;
private Station destination;
private String fare;
private long departureTime;
private long arrivalTime;
private boolean bikesAllowed;
private String trainHeadStation;
public Station getOrigin() {
return origin;
}
public void setOrigin(Station origin) {
this.origin = origin;
}
public Station getDestination() {
return destination;
}
public void setDestination(Station destination) {
this.destination = destination;
}
public String getFare() {
return fare;
}
public void setFare(String fare) {
this.fare = fare;
}
public long getDepartureTime() {
return departureTime;
}
public void setDepartureTime(long departureTime) {
this.departureTime = departureTime;
}
public long getArrivalTime() {
return arrivalTime;
}
public void setArrivalTime(long arrivalTime) {
this.arrivalTime = arrivalTime;
}
public int getTripLength() {
if (departureTime <= 0 || arrivalTime <= 0) {
return 0;
} else {
return (int) (arrivalTime - departureTime);
}
}
public boolean isBikesAllowed() {
return bikesAllowed;
}
public void setBikesAllowed(boolean bikesAllowed) {
this.bikesAllowed = bikesAllowed;
}
public String getTrainHeadStation() {
return trainHeadStation;
}
public void setTrainHeadStation(String trainHeadStation) {
this.trainHeadStation = trainHeadStation;
}
}

View File

@ -1,54 +1,48 @@
package com.dougkeen.bart;
package com.dougkeen.bart.model;
import java.util.ArrayList;
import java.util.List;
import android.util.Log;
public enum Station {
_12TH("12th", "12th St./Oakland City Center", false, false, "bayf"),
_16TH("16th", "16th St. Mission", false, false),
_19TH("19th", "19th St./Oakland", false, false, "bayf"),
_24TH("24th", "24th St. Mission", false, false),
ASHB("ashb", "Ashby", false, false, "mcar"),
BALB("balb", "Balboa Park", false, false),
BAYF("bayf", "Bay Fair", true, false, "mcar"),
CAST("cast", "Castro Valley", false, false, "bayf"),
CIVC("civc", "Civic Center", false, false),
COLS("cols", "Coliseum/Oakland Airport", true, false, "mcar"),
COLM("colm", "Colma", false, false, "balb", "balb"),
CONC("conc", "Concord", false, false, "mcar"),
DALY("daly", "Daly City", false, false),
DBRK("dbrk", "Downtown Berkeley", false, false, "mcar"),
DUBL("dubl", "Dublin/Pleasanton", false, true, "bayf"),
DELN("deln", "El Cerrito del Norte", false, false, "mcar"),
PLZA("plza", "El Cerrito Plaza", false, false, "mcar"),
EMBR("embr", "Embarcadero", false, false),
FRMT("frmt", "Fremont", true, false, "bayf"),
FTVL("ftvl", "Fruitvale", true, false, "mcar"),
GLEN("glen", "Glen Park", false, false),
HAYW("hayw", "Hayward", true, false, "bayf"),
LAFY("lafy", "Lafayette", false, false, "mcar"),
LAKE("lake", "Lake Merritt", true, false, "mcar"),
MCAR("mcar", "MacArthur", false, false, "bayf"),
MLBR("mlbr", "Millbrae", false, true, "balb", "balb"),
MONT("mont", "Montgomery St.", false, false),
NBRK("nbrk", "North Berkeley", false, false, "mcar"),
NCON("ncon", "North Concord/Martinez", false, false, "mcar"),
ORIN("orin", "Orinda", false, false, "mcar"),
PITT("pitt", "Pittsburg/Bay Point", false, true, "mcar"),
PHIL("phil", "Pleasant Hill", false, false, "mcar"),
POWL("powl", "Powell St.", false, false),
RICH("rich", "Richmond", false, true, "mcar"),
ROCK("rock", "Rockridge", false, false, "mcar"),
SBRN("sbrn", "San Bruno", false, false, "balb", "balb"),
SANL("sanl", "San Leandro", true, false, "mcar"),
SFIA("sfia", "SFO Airport", false, false, "sbrn", "balb"),
SHAY("shay", "South Hayward", true, false, "bayf"),
SSAN("ssan", "South San Francisco", false, false, "balb", "balb"),
UCTY("ucty", "Union City", true, false, "bayf"),
WCRK("wcrk", "Walnut Creek", false, false, "mcar"),
WDUB("wdub", "West Dublin/Pleasanton", false, false, "bayf"),
WOAK("woak", "West Oakland", false, false),
SPCL("spcl", "Special", false, false);
_12TH("12th", "12th St./Oakland City Center", false, false, "bayf"), _16TH(
"16th", "16th St. Mission", false, false), _19TH("19th",
"19th St./Oakland", false, false, "bayf"), _24TH("24th",
"24th St. Mission", false, false), ASHB("ashb", "Ashby", false,
false, "mcar"), BALB("balb", "Balboa Park", false, false), BAYF(
"bayf", "Bay Fair", true, false, "mcar"), CAST("cast",
"Castro Valley", false, false, "bayf"), CIVC("civc",
"Civic Center", false, false), COLS("cols",
"Coliseum/Oakland Airport", true, false, "mcar"), COLM("colm",
"Colma", false, false, "balb", "balb"), CONC("conc", "Concord",
false, false, "mcar"), DALY("daly", "Daly City", false, false), DBRK(
"dbrk", "Downtown Berkeley", false, false, "mcar"), DUBL("dubl",
"Dublin/Pleasanton", false, true, "bayf"), DELN("deln",
"El Cerrito del Norte", false, false, "mcar"), PLZA("plza",
"El Cerrito Plaza", false, false, "mcar"), EMBR("embr",
"Embarcadero", false, false), FRMT("frmt", "Fremont", true, false,
"bayf"), FTVL("ftvl", "Fruitvale", true, false, "mcar"), GLEN(
"glen", "Glen Park", false, false), HAYW("hayw", "Hayward", true,
false, "bayf"), LAFY("lafy", "Lafayette", false, false, "mcar"), LAKE(
"lake", "Lake Merritt", true, false, "mcar"), MCAR("mcar",
"MacArthur", false, false, "bayf"), MLBR("mlbr", "Millbrae", false,
true, "balb", "balb"), MONT("mont", "Montgomery St.", false, false), NBRK(
"nbrk", "North Berkeley", false, false, "mcar"), NCON("ncon",
"North Concord/Martinez", false, false, "mcar"), ORIN("orin",
"Orinda", false, false, "mcar"), PITT("pitt",
"Pittsburg/Bay Point", false, true, "mcar"), PHIL("phil",
"Pleasant Hill", false, false, "mcar"), POWL("powl", "Powell St.",
false, false), RICH("rich", "Richmond", false, true, "mcar"), ROCK(
"rock", "Rockridge", false, false, "mcar"), SBRN("sbrn",
"San Bruno", false, false, "balb", "balb"), SANL("sanl",
"San Leandro", true, false, "mcar"), SFIA("sfia", "SFO Airport",
false, false, "sbrn", "balb"), SHAY("shay", "South Hayward", true,
false, "bayf"), SSAN("ssan", "South San Francisco", false, false,
"balb", "balb"), UCTY("ucty", "Union City", true, false, "bayf"), WCRK(
"wcrk", "Walnut Creek", false, false, "mcar"), WDUB("wdub",
"West Dublin/Pleasanton", false, false, "bayf"), WOAK("woak",
"West Oakland", false, false), SPCL("spcl", "Special", false, false);
public final String abbreviation;
public final String name;
@ -57,7 +51,8 @@ public enum Station {
protected final String outboundTransferStation;
public final boolean endOfLine;
private Station(String abbreviation, String name, boolean invertDirection, boolean endOfLine) {
private Station(String abbreviation, String name, boolean invertDirection,
boolean endOfLine) {
this.abbreviation = abbreviation;
this.name = name;
this.invertDirection = invertDirection;
@ -66,8 +61,8 @@ public enum Station {
this.endOfLine = endOfLine;
}
private Station(String abbreviation, String name, boolean invertDirection, boolean endOfLine,
String transferStation) {
private Station(String abbreviation, String name, boolean invertDirection,
boolean endOfLine, String transferStation) {
this.abbreviation = abbreviation;
this.name = name;
this.invertDirection = invertDirection;
@ -76,8 +71,9 @@ public enum Station {
this.endOfLine = endOfLine;
}
private Station(String abbreviation, String name, boolean invertDirection, boolean endOfLine,
String inboundTransferStation, String outboundTransferStation) {
private Station(String abbreviation, String name, boolean invertDirection,
boolean endOfLine, String inboundTransferStation,
String outboundTransferStation) {
this.abbreviation = abbreviation;
this.name = name;
this.invertDirection = invertDirection;
@ -87,6 +83,7 @@ public enum Station {
}
public static Station getByAbbreviation(String abbr) {
try {
if (abbr == null) {
return null;
} else if (Character.isDigit(abbr.charAt(0))) {
@ -94,6 +91,10 @@ public enum Station {
} else {
return Station.valueOf(abbr.toUpperCase());
}
} catch (IllegalArgumentException e) {
Log.e(Constants.TAG, "Could not find station for '" + abbr + "'", e);
return null;
}
}
public Station getInboundTransferStation() {
@ -155,8 +156,7 @@ public enum Station {
}
if (isNorth == null) {
if (outboundTransferStation != null) {
returnList
.addAll(getOutboundTransferStation()
returnList.addAll(getOutboundTransferStation()
.getRoutesForDestination(dest,
getOutboundTransferStation()));
} else if (dest.getInboundTransferStation() != null) {

View File

@ -0,0 +1,21 @@
package com.dougkeen.bart.model;
public class StationPair {
public StationPair(Station origin, Station destination) {
super();
this.origin = origin;
this.destination = destination;
}
private Station origin;
private Station destination;
public Station getOrigin() {
return origin;
}
public Station getDestination() {
return destination;
}
}

View File

@ -1,4 +1,4 @@
package com.dougkeen.bart;
package com.dougkeen.bart.networktasks;
import java.util.Arrays;
import java.util.Date;
@ -11,8 +11,12 @@ import org.xml.sax.helpers.DefaultHandler;
import android.util.Log;
import com.dougkeen.bart.data.Departure;
import com.dougkeen.bart.data.RealTimeDepartures;
import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Departure;
import com.dougkeen.bart.model.Line;
import com.dougkeen.bart.model.RealTimeDepartures;
import com.dougkeen.bart.model.Route;
import com.dougkeen.bart.model.Station;
public class EtdContentHandler extends DefaultHandler {
public EtdContentHandler(Station origin, Station destination,

View File

@ -1,4 +1,4 @@
package com.dougkeen.bart;
package com.dougkeen.bart.networktasks;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

View File

@ -1,4 +1,4 @@
package com.dougkeen.bart;
package com.dougkeen.bart.networktasks;
import java.io.IOException;
import java.io.StringWriter;
@ -17,11 +17,13 @@ import android.os.AsyncTask;
import android.util.Log;
import android.util.Xml;
import com.dougkeen.bart.data.RealTimeDepartures;
import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.StationPair;
import com.dougkeen.bart.model.RealTimeDepartures;
import com.dougkeen.bart.model.Route;
public abstract class GetRealTimeDeparturesTask
extends
AsyncTask<GetRealTimeDeparturesTask.Params, Integer, RealTimeDepartures> {
public abstract class GetRealTimeDeparturesTask extends
AsyncTask<StationPair, Integer, RealTimeDepartures> {
private final static String ETD_URL = "http://api.bart.gov/api/etd.aspx?cmd=etd&key="
+ Constants.API_KEY + "&orig=%1$s&dir=%2$s";
@ -34,11 +36,12 @@ public abstract class GetRealTimeDeparturesTask
private List<Route> mRoutes;
@Override
protected RealTimeDepartures doInBackground(Params... paramsArray) {
protected RealTimeDepartures doInBackground(StationPair... paramsArray) {
// Always expect one param
Params params = paramsArray[0];
StationPair params = paramsArray[0];
mRoutes = params.origin.getRoutesForDestination(params.destination);
mRoutes = params.getOrigin().getRoutesForDestination(
params.getDestination());
if (!isCancelled()) {
return getDeparturesFromNetwork(params, 0);
@ -47,23 +50,23 @@ public abstract class GetRealTimeDeparturesTask
}
}
private RealTimeDepartures getDeparturesFromNetwork(Params params,
private RealTimeDepartures getDeparturesFromNetwork(StationPair params,
int attemptNumber) {
String xml = null;
try {
String url;
if (params.origin.endOfLine) {
if (params.getOrigin().endOfLine) {
url = String.format(ETD_URL_NO_DIRECTION,
params.origin.abbreviation);
params.getOrigin().abbreviation);
} else {
url = String.format(ETD_URL, params.origin.abbreviation,
url = String.format(ETD_URL, params.getOrigin().abbreviation,
mRoutes.get(0).getDirection());
}
HttpUriRequest request = new HttpGet(url);
EtdContentHandler handler = new EtdContentHandler(params.origin,
params.destination, mRoutes);
EtdContentHandler handler = new EtdContentHandler(
params.getOrigin(), params.getDestination(), mRoutes);
if (isCancelled()) {
return null;
}
@ -113,25 +116,6 @@ public abstract class GetRealTimeDeparturesTask
}
}
public static class Params {
public Params(Station origin, Station destination) {
super();
this.origin = origin;
this.destination = destination;
}
private Station origin;
private Station destination;
public Station getOrigin() {
return origin;
}
public Station getDestination() {
return destination;
}
}
@Override
protected void onPostExecute(RealTimeDepartures result) {
if (result != null) {

View File

@ -1,4 +1,4 @@
package com.dougkeen.bart;
package com.dougkeen.bart.networktasks;
import java.io.IOException;
import java.io.StringWriter;
@ -12,6 +12,9 @@ import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.xml.sax.SAXException;
import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Station;
import android.os.AsyncTask;
import android.util.Log;
import android.util.Xml;

View File

@ -0,0 +1,118 @@
package com.dougkeen.bart.networktasks;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.xml.sax.SAXException;
import android.os.AsyncTask;
import android.util.Log;
import android.util.Xml;
import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.ScheduleInformation;
import com.dougkeen.bart.model.StationPair;
public abstract class GetScheduleInformationTask extends
AsyncTask<StationPair, Integer, ScheduleInformation> {
private final static String SCHED_URL = "http://api.bart.gov/api/sched.aspx?cmd=depart&key="
+ Constants.API_KEY + "&orig=%1$s&dest=%2$s&b=1&a=5";
private final static int MAX_ATTEMPTS = 5;
private Exception mException;
@Override
protected ScheduleInformation doInBackground(StationPair... paramsArray) {
// Always expect one param
StationPair params = paramsArray[0];
if (!isCancelled()) {
return getScheduleFromNetwork(params, 0);
} else {
return null;
}
}
private ScheduleInformation getScheduleFromNetwork(StationPair params,
int attemptNumber) {
String xml = null;
try {
String url = String.format(SCHED_URL,
params.getOrigin().abbreviation,
params.getDestination().abbreviation);
HttpUriRequest request = new HttpGet(url);
if (isCancelled()) {
return null;
}
ScheduleContentHandler handler = new ScheduleContentHandler(
params.getOrigin(), params.getDestination());
HttpResponse response = NetworkUtils.executeWithRecovery(request);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
throw new IOException("Server returned "
+ response.getStatusLine().toString());
}
StringWriter writer = new StringWriter();
IOUtils.copy(response.getEntity().getContent(), writer, "UTF-8");
xml = writer.toString();
if (xml.length() == 0) {
throw new IOException("Server returned blank xml document");
}
Xml.parse(xml, handler);
final ScheduleInformation schedule = handler.getSchedule();
return schedule;
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
if (attemptNumber < MAX_ATTEMPTS - 1) {
try {
Log.w(Constants.TAG,
"Attempt to contact server failed... retrying in 3s",
e);
Thread.sleep(3000);
} catch (InterruptedException interrupt) {
// Ignore... just go on to next attempt
}
return getScheduleFromNetwork(params, attemptNumber + 1);
} else {
mException = new Exception("Could not contact BART system", e);
return null;
}
} catch (SAXException e) {
mException = new Exception(
"Could not understand response from BART system: " + xml, e);
return null;
}
}
@Override
protected void onPostExecute(ScheduleInformation result) {
if (result != null) {
onResult(result);
} else {
onError(mException);
}
}
public abstract void onResult(ScheduleInformation result);
public abstract void onError(Exception exception);
}

View File

@ -1,4 +1,4 @@
package com.dougkeen.bart;
package com.dougkeen.bart.networktasks;
import java.io.IOException;

View File

@ -0,0 +1,160 @@
package com.dougkeen.bart.networktasks;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import org.apache.commons.lang3.StringUtils;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import android.util.Log;
import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Departure;
import com.dougkeen.bart.model.Line;
import com.dougkeen.bart.model.RealTimeDepartures;
import com.dougkeen.bart.model.Route;
import com.dougkeen.bart.model.ScheduleInformation;
import com.dougkeen.bart.model.ScheduleItem;
import com.dougkeen.bart.model.Station;
public class ScheduleContentHandler extends DefaultHandler {
public ScheduleContentHandler(Station origin, Station destination) {
super();
schedule = new ScheduleInformation(origin, destination);
}
private final static List<String> TAGS = Arrays.asList("date", "time",
"trip");
private final static DateFormat TRIP_DATE_FORMAT;
private final static DateFormat REQUEST_DATE_FORMAT;
private final static TimeZone PACIFIC_TIME = TimeZone
.getTimeZone("America/Los_Angeles");
static {
TRIP_DATE_FORMAT = new SimpleDateFormat("MM/dd/yyyy h:mm a");
REQUEST_DATE_FORMAT = new SimpleDateFormat("MMM d, yyyy h:mm a");
TRIP_DATE_FORMAT.setTimeZone(PACIFIC_TIME);
REQUEST_DATE_FORMAT.setTimeZone(PACIFIC_TIME);
}
private ScheduleInformation schedule;
public ScheduleInformation getSchedule() {
return schedule;
}
private String currentValue;
private boolean isParsingTag;
private String requestDate;
private String requestTime;
@Override
public void characters(char[] ch, int start, int length)
throws SAXException {
if (isParsingTag) {
currentValue = new String(ch, start, length);
}
}
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
if (TAGS.contains(localName)) {
isParsingTag = true;
}
if (localName.equals("trip")) {
ScheduleItem trip = new ScheduleItem();
String originDate = null;
String originTime = null;
String destinationDate = null;
String destinationTime = null;
for (int i = attributes.getLength() - 1; i >= 0; i--) {
if (attributes.getLocalName(i).equalsIgnoreCase("origin")) {
trip.setOrigin(Station.getByAbbreviation(attributes
.getValue(i)));
} else if (attributes.getLocalName(i).equalsIgnoreCase(
"destination")) {
trip.setDestination(Station.getByAbbreviation(attributes
.getValue(i)));
} else if (attributes.getLocalName(i).equalsIgnoreCase("fare")) {
trip.setFare(attributes.getValue(i));
} else if (attributes.getLocalName(i).equalsIgnoreCase(
"origTimeMin")) {
originTime = attributes.getValue(i);
} else if (attributes.getLocalName(i).equalsIgnoreCase(
"origTimeDate")) {
originDate = attributes.getValue(i);
} else if (attributes.getLocalName(i).equalsIgnoreCase(
"destTimeMin")) {
destinationTime = attributes.getValue(i);
} else if (attributes.getLocalName(i).equalsIgnoreCase(
"destTimeDate")) {
destinationDate = attributes.getValue(i);
} else if (attributes.getLocalName(i).equalsIgnoreCase(
"bikeFlag")) {
trip.setBikesAllowed(attributes.getValue(i).equals("1"));
} else if (attributes.getLocalName(i).equalsIgnoreCase(
"trainHeadStation")) {
trip.setTrainHeadStation(attributes.getValue(i));
}
}
long departTime = parseDate(TRIP_DATE_FORMAT, originDate,
originTime);
if (departTime > 0)
trip.setDepartureTime(departTime);
long arriveTime = parseDate(TRIP_DATE_FORMAT, destinationDate,
destinationTime);
if (arriveTime > 0)
trip.setArrivalTime(arriveTime);
schedule.addTrip(trip);
}
}
private long parseDate(DateFormat format, String dateString,
String timeString) {
if (dateString == null || timeString == null) {
return -1;
}
try {
return format.parse(dateString + " " + timeString).getTime();
} catch (ParseException e) {
Log.e(Constants.TAG, "Unable to parse datetime '" + dateString
+ " " + timeString + "'", e);
return -1;
}
}
@Override
public void endElement(String uri, String localName, String qName)
throws SAXException {
if (localName.equals("date")) {
requestDate = currentValue;
} else if (localName.equals("time")) {
requestTime = currentValue;
}
isParsingTag = false;
currentValue = null;
}
@Override
public void endDocument() {
long date = parseDate(REQUEST_DATE_FORMAT, requestDate, requestTime);
if (date > 0) {
schedule.setDate(date);
}
}
}