diff --git a/.classpath b/.classpath index b3366ba..1e34f60 100644 --- a/.classpath +++ b/.classpath @@ -5,6 +5,7 @@ - + + diff --git a/libs/android-support-v4.jar b/libs/android-support-v4.jar new file mode 100644 index 0000000..99e063b Binary files /dev/null and b/libs/android-support-v4.jar differ diff --git a/res/layout/departure_listing.xml b/res/layout/departure_listing.xml index 9727b8a..c3ed530 100644 --- a/res/layout/departure_listing.xml +++ b/res/layout/departure_listing.xml @@ -43,9 +43,10 @@ android:layout_below="@id/topRow" android:src="@drawable/xfer" /> - diff --git a/res/layout/train_length_arrival_textview.xml b/res/layout/train_length_arrival_textview.xml new file mode 100644 index 0000000..a580980 --- /dev/null +++ b/res/layout/train_length_arrival_textview.xml @@ -0,0 +1,3 @@ + + diff --git a/src/com/dougkeen/bart/AddRouteActivity.java b/src/com/dougkeen/bart/AddRouteActivity.java index 5af8cfd..a02d7ea 100644 --- a/src/com/dougkeen/bart/AddRouteActivity.java +++ b/src/com/dougkeen/bart/AddRouteActivity.java @@ -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; diff --git a/src/com/dougkeen/bart/DepartureArrayAdapter.java b/src/com/dougkeen/bart/DepartureArrayAdapter.java index df9c8cb..a152f0e 100644 --- a/src/com/dougkeen/bart/DepartureArrayAdapter.java +++ b/src/com/dougkeen/bart/DepartureArrayAdapter.java @@ -2,21 +2,28 @@ package com.dougkeen.bart; import java.util.List; +import org.apache.commons.lang3.StringUtils; + import android.content.Context; import android.graphics.Color; 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 { + public static int refreshCounter = 0; + public DepartureArrayAdapter(Context context, int textViewResourceId, Departure[] objects) { super(context, textViewResourceId, objects); @@ -59,8 +66,30 @@ public class DepartureArrayAdapter extends ArrayAdapter { 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); + + final String estimatedArrivalTimeText = departure + .getEstimatedArrivalTimeText(getContext()); + String arrivalText = "Est. arrival " + estimatedArrivalTimeText; + if (StringUtils.isBlank(estimatedArrivalTimeText)) { + textSwitcher.setCurrentText(departure.getTrainLengthText()); + } else if (refreshCounter % 6 < 3) { + String trainLengthText = departure.getTrainLengthText(); + if (refreshCounter % 6 == 0) { + textSwitcher.setText(trainLengthText); + } else { + textSwitcher.setCurrentText(trainLengthText); + } + } else { + if (refreshCounter % 6 == 3) { + textSwitcher.setText(arrivalText); + } else { + textSwitcher.setCurrentText(arrivalText); + } + } ImageView colorBar = (ImageView) view .findViewById(R.id.destinationColorBar); ((GradientDrawable) colorBar.getDrawable()).setColor(Color @@ -87,4 +116,17 @@ public class DepartureArrayAdapter extends ArrayAdapter { 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.fade_in)); + } + } } diff --git a/src/com/dougkeen/bart/RoutesListActivity.java b/src/com/dougkeen/bart/RoutesListActivity.java index fe6347f..d599736 100644 --- a/src/com/dougkeen/bart/RoutesListActivity.java +++ b/src/com/dougkeen/bart/RoutesListActivity.java @@ -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(); diff --git a/src/com/dougkeen/bart/ViewDeparturesActivity.java b/src/com/dougkeen/bart/ViewDeparturesActivity.java index 986af16..6ad519c 100644 --- a/src/com/dougkeen/bart/ViewDeparturesActivity.java +++ b/src/com/dougkeen/bart/ViewDeparturesActivity.java @@ -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 mDeparturesAdapter; + private ScheduleInformation mLatestScheduleInfo; + private TextView mListTitleView; - private AsyncTask mGetDeparturesTask; + private AsyncTask mGetDeparturesTask; + private AsyncTask 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,21 +331,106 @@ 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"); } } private void runAutoUpdate() { if (mIsAutoUpdating && mDeparturesAdapter != null) { + DepartureArrayAdapter.refreshCounter++; mDeparturesAdapter.notifyDataSetChanged(); } if (hasWindowFocus()) { diff --git a/src/com/dougkeen/bart/data/BartContentProvider.java b/src/com/dougkeen/bart/data/BartContentProvider.java index 2344eed..9894a24 100644 --- a/src/com/dougkeen/bart/data/BartContentProvider.java +++ b/src/com/dougkeen/bart/data/BartContentProvider.java @@ -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; diff --git a/src/com/dougkeen/bart/data/DatabaseHelper.java b/src/com/dougkeen/bart/data/DatabaseHelper.java index 5e633a9..d22383b 100644 --- a/src/com/dougkeen/bart/data/DatabaseHelper.java +++ b/src/com/dougkeen/bart/data/DatabaseHelper.java @@ -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 diff --git a/src/com/dougkeen/bart/data/RoutesColumns.java b/src/com/dougkeen/bart/data/RoutesColumns.java index acf646e..e4ad2ee 100644 --- a/src/com/dougkeen/bart/data/RoutesColumns.java +++ b/src/com/dougkeen/bart/data/RoutesColumns.java @@ -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 diff --git a/src/com/dougkeen/bart/Constants.java b/src/com/dougkeen/bart/model/Constants.java similarity index 86% rename from src/com/dougkeen/bart/Constants.java rename to src/com/dougkeen/bart/model/Constants.java index 62ded3a..6134a31 100644 --- a/src/com/dougkeen/bart/Constants.java +++ b/src/com/dougkeen/bart/model/Constants.java @@ -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"; } diff --git a/src/com/dougkeen/bart/data/Departure.java b/src/com/dougkeen/bart/model/Departure.java similarity index 77% rename from src/com/dougkeen/bart/data/Departure.java rename to src/com/dougkeen/bart/model/Departure.java index fd1a4eb..a1af043 100644 --- a/src/com/dougkeen/bart/data/Departure.java +++ b/src/com/dougkeen/bart/model/Departure.java @@ -1,13 +1,15 @@ -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 { private static final int ESTIMATE_EQUALS_TOLERANCE_MILLIS = 59999; + private static final int ESTIMATE_EQUALS_TOLERANCE_LONG_LINGER_MILLIS = 719999; private static final int MINIMUM_MERGE_OVERLAP_MILLIS = 10000; public Departure() { @@ -31,6 +33,7 @@ public class Departure implements Parcelable, Comparable { readFromParcel(in); } + private Station origin; private Station destination; private Line line; private String destinationColor; @@ -45,6 +48,20 @@ public class Departure implements Parcelable, Comparable { private long minEstimate; private long maxEstimate; + private int estimatedTripTime; + + private boolean beganAsDeparted; + + private long arrivalTimeOverride; + + public Station getOrigin() { + return origin; + } + + public void setOrigin(Station origin) { + this.origin = origin; + } + public Station getDestination() { return destination; } @@ -131,6 +148,9 @@ public class Departure implements Parcelable, Comparable { public void setMinutes(int minutes) { this.minutes = minutes; + if (minutes == 0) { + beganAsDeparted = true; + } } public long getMinEstimate() { @@ -149,6 +169,18 @@ public class Departure implements Parcelable, Comparable { 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 +194,27 @@ public class Departure implements Parcelable, Comparable { } 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() { + if (arrivalTimeOverride > 0) { + return arrivalTimeOverride; + } + return getMeanEstimate() + getEstimatedTripTime(); + } + + public String getEstimatedArrivalTimeText(Context context) { + if (getEstimatedTripTime() > 0) { + return DateFormat.getTimeFormat(context).format( + new Date(getEstimatedArrivalTime())); + } else { + return ""; + } } public boolean hasDeparted() { @@ -177,6 +228,13 @@ public class Departure implements Parcelable, Comparable { } public void mergeEstimate(Departure departure) { + if (departure.hasDeparted() && origin.longStationLinger + && getMinEstimate() > 0 && !beganAsDeparted) { + // This is probably not a true departure, but an indication that + // the train is in the station. Don't update the estimates. + return; + } + if ((getMaxEstimate() - departure.getMinEstimate()) < MINIMUM_MERGE_OVERLAP_MILLIS || departure.getMaxEstimate() - getMinEstimate() < MINIMUM_MERGE_OVERLAP_MILLIS) { // The estimate must have changed... just use the latest incoming @@ -250,7 +308,7 @@ public class Departure implements Parcelable, Comparable { return false; if (line != other.line) return false; - if (Math.abs(maxEstimate - other.maxEstimate) > ESTIMATE_EQUALS_TOLERANCE_MILLIS) + if (Math.abs(maxEstimate - other.maxEstimate) > getEqualsTolerance()) return false; if (platform == null) { if (other.platform != null) @@ -267,11 +325,23 @@ public class Departure implements Parcelable, Comparable { return true; } + private int getEqualsTolerance() { + if (origin.longStationLinger && hasDeparted()) { + return ESTIMATE_EQUALS_TOLERANCE_LONG_LINGER_MILLIS; + } else { + return ESTIMATE_EQUALS_TOLERANCE_MILLIS; + } + } + public String getCountdownText() { StringBuilder builder = new StringBuilder(); int secondsLeft = getMeanSecondsLeft(); if (hasDeparted()) { - builder.append("Departed"); + if (origin.longStationLinger && beganAsDeparted) { + builder.append("At station"); + } else { + builder.append("Departed"); + } } else { builder.append(secondsLeft / 60); builder.append("m, "); diff --git a/src/com/dougkeen/bart/Line.java b/src/com/dougkeen/bart/model/Line.java similarity index 87% rename from src/com/dougkeen/bart/Line.java rename to src/com/dougkeen/bart/model/Line.java index 5fd79bb..a37f58e 100644 --- a/src/com/dougkeen/bart/Line.java +++ b/src/com/dougkeen/bart/model/Line.java @@ -1,4 +1,4 @@ -package com.dougkeen.bart; +package com.dougkeen.bart.model; import java.util.ArrayList; import java.util.Arrays; @@ -37,6 +37,12 @@ public enum Line { Station.COLS, Station.SANL, Station.BAYF, Station.HAYW, Station.SHAY, Station.UCTY, Station.FRMT), YELLOW_ORANGE_SCHEDULED_TRANSFER(YELLOW, ORANGE, Station.MLBR, + Station.SFIA, Station.SBRN, Station.SSAN, Station.COLM, + Station.DALY, Station.BALB, Station.GLEN, Station._24TH, + Station._16TH, Station.CIVC, Station.POWL, Station.MONT, + Station.EMBR, Station.WOAK, Station.ASHB, Station.DBRK, + Station.NBRK, Station.PLZA, Station.DELN, Station.RICH), + YELLOW_RED_SCHEDULED_TRANSFER(YELLOW, RED, Station.MLBR, Station.SFIA, Station.SBRN, Station.SSAN, Station.COLM, Station.DALY, Station.BALB, Station.GLEN, Station._24TH, Station._16TH, Station.CIVC, Station.POWL, Station.MONT, diff --git a/src/com/dougkeen/bart/data/RealTimeDepartures.java b/src/com/dougkeen/bart/model/RealTimeDepartures.java similarity index 91% rename from src/com/dougkeen/bart/data/RealTimeDepartures.java rename to src/com/dougkeen/bart/model/RealTimeDepartures.java index a7ec600..0d62bd4 100644 --- a/src/com/dougkeen/bart/data/RealTimeDepartures.java +++ b/src/com/dougkeen/bart/model/RealTimeDepartures.java @@ -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, diff --git a/src/com/dougkeen/bart/Route.java b/src/com/dougkeen/bart/model/Route.java similarity index 94% rename from src/com/dougkeen/bart/Route.java rename to src/com/dougkeen/bart/model/Route.java index f063411..7ce4d4c 100644 --- a/src/com/dougkeen/bart/Route.java +++ b/src/com/dougkeen/bart/model/Route.java @@ -1,4 +1,4 @@ -package com.dougkeen.bart; +package com.dougkeen.bart.model; public class Route { private Station origin; diff --git a/src/com/dougkeen/bart/model/ScheduleInformation.java b/src/com/dougkeen/bart/model/ScheduleInformation.java new file mode 100644 index 0000000..25e635a --- /dev/null +++ b/src/com/dougkeen/bart/model/ScheduleInformation.java @@ -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 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 getTrips() { + if (trips == null) { + trips = new ArrayList(); + } + return trips; + } + + public void setTrips(List 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; + } +} \ No newline at end of file diff --git a/src/com/dougkeen/bart/model/ScheduleItem.java b/src/com/dougkeen/bart/model/ScheduleItem.java new file mode 100644 index 0000000..ac18840 --- /dev/null +++ b/src/com/dougkeen/bart/model/ScheduleItem.java @@ -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; + } +} diff --git a/src/com/dougkeen/bart/Station.java b/src/com/dougkeen/bart/model/Station.java similarity index 77% rename from src/com/dougkeen/bart/Station.java rename to src/com/dougkeen/bart/model/Station.java index 55a65d0..76ca947 100644 --- a/src/com/dougkeen/bart/Station.java +++ b/src/com/dougkeen/bart/model/Station.java @@ -1,8 +1,10 @@ -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), @@ -41,7 +43,7 @@ public enum Station { 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"), + SFIA("sfia", "SFO Airport", false, false, "sbrn", "balb", true), SHAY("shay", "South Hayward", true, false, "bayf"), SSAN("ssan", "South San Francisco", false, false, "balb", "balb"), UCTY("ucty", "Union City", true, false, "bayf"), @@ -56,43 +58,50 @@ public enum Station { protected final String inboundTransferStation; protected final String outboundTransferStation; public final boolean endOfLine; + public final boolean longStationLinger; - private Station(String abbreviation, String name, boolean invertDirection, boolean endOfLine) { - this.abbreviation = abbreviation; - this.name = name; - this.invertDirection = invertDirection; - this.inboundTransferStation = null; - this.outboundTransferStation = null; - this.endOfLine = endOfLine; + private Station(String abbreviation, String name, boolean invertDirection, + boolean endOfLine) { + this(abbreviation, name, invertDirection, endOfLine, null, null, false); } - private Station(String abbreviation, String name, boolean invertDirection, boolean endOfLine, - String transferStation) { - this.abbreviation = abbreviation; - this.name = name; - this.invertDirection = invertDirection; - this.inboundTransferStation = transferStation; - this.outboundTransferStation = null; - this.endOfLine = endOfLine; + private Station(String abbreviation, String name, boolean invertDirection, + boolean endOfLine, String transferStation) { + this(abbreviation, name, invertDirection, endOfLine, transferStation, + null, false); } - 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, name, invertDirection, endOfLine, + inboundTransferStation, outboundTransferStation, false); + } + + private Station(String abbreviation, String name, boolean invertDirection, + boolean endOfLine, String inboundTransferStation, + String outboundTransferStation, boolean longStationLinger) { this.abbreviation = abbreviation; this.name = name; this.invertDirection = invertDirection; this.inboundTransferStation = inboundTransferStation; this.outboundTransferStation = outboundTransferStation; this.endOfLine = endOfLine; + this.longStationLinger = longStationLinger; } public static Station getByAbbreviation(String abbr) { - if (abbr == null) { + try { + if (abbr == null) { + return null; + } else if (Character.isDigit(abbr.charAt(0))) { + return Station.valueOf("_" + abbr.toUpperCase()); + } else { + return Station.valueOf(abbr.toUpperCase()); + } + } catch (IllegalArgumentException e) { + Log.e(Constants.TAG, "Could not find station for '" + abbr + "'", e); return null; - } else if (Character.isDigit(abbr.charAt(0))) { - return Station.valueOf("_" + abbr.toUpperCase()); - } else { - return Station.valueOf(abbr.toUpperCase()); } } @@ -155,10 +164,9 @@ public enum Station { } if (isNorth == null) { if (outboundTransferStation != null) { - returnList - .addAll(getOutboundTransferStation() - .getRoutesForDestination(dest, - getOutboundTransferStation())); + returnList.addAll(getOutboundTransferStation() + .getRoutesForDestination(dest, + getOutboundTransferStation())); } else if (dest.getInboundTransferStation() != null) { final List routesForDestination = getRoutesForDestination( dest.getInboundTransferStation(), diff --git a/src/com/dougkeen/bart/model/StationPair.java b/src/com/dougkeen/bart/model/StationPair.java new file mode 100644 index 0000000..23f7a3e --- /dev/null +++ b/src/com/dougkeen/bart/model/StationPair.java @@ -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; + } +} \ No newline at end of file diff --git a/src/com/dougkeen/bart/EtdContentHandler.java b/src/com/dougkeen/bart/networktasks/EtdContentHandler.java similarity index 88% rename from src/com/dougkeen/bart/EtdContentHandler.java rename to src/com/dougkeen/bart/networktasks/EtdContentHandler.java index 87dadba..5345733 100644 --- a/src/com/dougkeen/bart/EtdContentHandler.java +++ b/src/com/dougkeen/bart/networktasks/EtdContentHandler.java @@ -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, @@ -55,6 +59,7 @@ public class EtdContentHandler extends DefaultHandler { currentDeparture = new Departure(); currentDeparture.setDestination(Station .getByAbbreviation(currentDestination)); + currentDeparture.setOrigin(realTimeDepartures.getOrigin()); } } diff --git a/src/com/dougkeen/bart/FareContentHandler.java b/src/com/dougkeen/bart/networktasks/FareContentHandler.java similarity index 91% rename from src/com/dougkeen/bart/FareContentHandler.java rename to src/com/dougkeen/bart/networktasks/FareContentHandler.java index 19a0e78..fa1c0fb 100644 --- a/src/com/dougkeen/bart/FareContentHandler.java +++ b/src/com/dougkeen/bart/networktasks/FareContentHandler.java @@ -1,4 +1,4 @@ -package com.dougkeen.bart; +package com.dougkeen.bart.networktasks; import org.xml.sax.Attributes; import org.xml.sax.SAXException; diff --git a/src/com/dougkeen/bart/GetRealTimeDeparturesTask.java b/src/com/dougkeen/bart/networktasks/GetRealTimeDeparturesTask.java similarity index 71% rename from src/com/dougkeen/bart/GetRealTimeDeparturesTask.java rename to src/com/dougkeen/bart/networktasks/GetRealTimeDeparturesTask.java index 64a4df0..2eb5e1e 100644 --- a/src/com/dougkeen/bart/GetRealTimeDeparturesTask.java +++ b/src/com/dougkeen/bart/networktasks/GetRealTimeDeparturesTask.java @@ -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 { +public abstract class GetRealTimeDeparturesTask extends + AsyncTask { 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 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) { diff --git a/src/com/dougkeen/bart/GetRouteFareTask.java b/src/com/dougkeen/bart/networktasks/GetRouteFareTask.java similarity index 92% rename from src/com/dougkeen/bart/GetRouteFareTask.java rename to src/com/dougkeen/bart/networktasks/GetRouteFareTask.java index 4499eb2..892dfd6 100644 --- a/src/com/dougkeen/bart/GetRouteFareTask.java +++ b/src/com/dougkeen/bart/networktasks/GetRouteFareTask.java @@ -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; diff --git a/src/com/dougkeen/bart/networktasks/GetScheduleInformationTask.java b/src/com/dougkeen/bart/networktasks/GetScheduleInformationTask.java new file mode 100644 index 0000000..f8eb974 --- /dev/null +++ b/src/com/dougkeen/bart/networktasks/GetScheduleInformationTask.java @@ -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 { + + 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); +} diff --git a/src/com/dougkeen/bart/NetworkUtils.java b/src/com/dougkeen/bart/networktasks/NetworkUtils.java similarity index 94% rename from src/com/dougkeen/bart/NetworkUtils.java rename to src/com/dougkeen/bart/networktasks/NetworkUtils.java index 417d937..6d06981 100644 --- a/src/com/dougkeen/bart/NetworkUtils.java +++ b/src/com/dougkeen/bart/networktasks/NetworkUtils.java @@ -1,4 +1,4 @@ -package com.dougkeen.bart; +package com.dougkeen.bart.networktasks; import java.io.IOException; diff --git a/src/com/dougkeen/bart/networktasks/ScheduleContentHandler.java b/src/com/dougkeen/bart/networktasks/ScheduleContentHandler.java new file mode 100644 index 0000000..b18b4a8 --- /dev/null +++ b/src/com/dougkeen/bart/networktasks/ScheduleContentHandler.java @@ -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 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); + } + } +}