teaching machines

Homework 3 – Uncens.us

November 7, 2011 by . Filed under cs491 mobile, fall 2011, postmortems.

Uncens.us is a map-based demographics browsing tool. Using an API from infochimps.com, the application connects a user to data from the US Census Bureau’s 2009 American Community Survey, grouped at a census tract level. The name comes from an impulse-buy domain I registered sometime last year (a hobby I can’t seem to get enough of…).

A lot of information is available, but I limited what I display to a few basics like median household income, median housing value, percentage of housing which is owned (vs. rented), and average household size. I included a total population figure as well, but this doesn’t mean much. Census tracts aren’t uniform in size, but rather are based on population areas with around 4,000 inhabitants. In urban areas, a tract may be a small neighborhood, while rural census tracts may spread over many square miles.

Tract-level data is selected by panning and zooming around a map. If you find an area you’re interested in, tap “Search Here” and the longitude and latitude of the center of the MapView is sent to the aforementioned API. One wonky thing you end up dealing with in Android is converting between standard decimal versions of latitude and longitude used by most APIs and Android’s own Location objects (i.e., 44.77, -91.48), and Android’s GeoPoint object which MapView uses, normalizing coordinates as integers of a higher magnitude (i.e., 44.77 X 10^6 = 44770000, -91480000).

private void searchForTractHere() {
  GeoPoint point = mapView.getMapCenter();
  Intent intent = new Intent(K.ACTION_GET_TRACT, null, this, UncensusService.class);
  intent.putExtra("latitude", latitude);
  intent.putExtra("longitude", point.getLongitudeE6() / (double) 1E6);
  intent.putExtra("receiver", point.getLatitudeE6() / (double) 1E6);
  startService(intent);
}

Once data for a census tract is downloaded, it is cached in a local SQLite database and a map marker is added. Tapping the map marker brings up a dialog with summary information for that area with an option to view more details.
 

Notes on GPS

I included options for asking the API for information about the device’s current location (with support limited to only GPS satellite service). My results with GPS were spotty, and I don’t know if that was because of my device’s GPS hardware or my first-shot attempt at implementing LocationManager/LocationListener. The manual map-based search works great, and turns out to be more fun to use, anyhow.

I learned a little about locations, but getting a robust location-aware application built can be complicated. There are a variety of levels of location service providers (i.e., GPS satellites, WiFi networks, cellular towers, IP address) and a good application would use intelligently defined combinations of these to to balance reasonable accuracy with power consumption, reliability, and compatibility with the large family of Android devices. I won’t go into the details here, but peruse the Android Developer guide to Obtaining User Location to get a high-level overview (and let me know if you come across a good tutorial yourself).

Detail View and String Formatting

 

The detail view for each tract includes more data in a tabular format. Getting those numbers to line up in a way that made sense required some trickery including DecimalFormat objects and the String.format() static method (which performs like printf() in many other C-like languages).

// Grab the TextView.
TextView medianIncome = (TextView) findViewById(R.id.tract_detail_median_income);

// Create a DecimalFormat object, and...
DecimalFormat dollarFormat = new DecimalFormat("$0,000");

// ...use it to format the dollar amount into a string a la "$107,003".
String d = dollarFormat.format(tract.getMedianHouseholdIncome());

// Finally, use String.format to pad the dollar string with spaces for alignment with other fields
medianIncome.setText(String.format("%10s", d));

ListView and Comparator vs Comparable

The ListView for this application displays the census tracts that have been “visited” by the user either via the map interface or using GPS, sorted in order by how far away they are from a “base location,” which can mean one of the following

The logic for this looks a little convoluted.

sortLocation = locManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
Bundle bundle = getIntent().getExtras();
long id = bundle.getLong("id");
if (bundle != null) {   // If there was a bundle,
  if (id > 0) {         // ...and it has a legitimate database ID,
    TractDatabase db = ((UncensusApplication)getApplication()).getTractDatabase();
    // ...then that means we came here from a detail view Activity.
    sortLocation = db.getTractByID(id).toLocation();
  } else {
    // Otherwise, this must have come from the MapView Activity, which means there's no actual
    // database entry for the location, and we use the latitude and logitude extras that were sent.
    sortLocation = new Location(K.UNCENSUS_LOCATION_PROVIDER);
    sortLocation.setLatitude(bundle.getDouble("latitude"));
    sortLocation.setLongitude(bundle.getDouble("longitude"));
  }
}

Finally, to actually do the sorting took some tricky thinking. The sort order couldn’t be based on a simple compareTo() call because the sort order is a moving target. If the list is based on a map centered in Wyoming, the distances will be very different than if they are based on the last detail view of some census information from West Virginia. A custom Comparator object with the “base location” as an instance variable was one option, but I still wanted the ListAdapter to be able to set the “miles to” information in the display or each list item. In the end, I decided to add a “miles to” instance variable to each census tract, using it only for objects that are instantiated in memory and need to be displayed in a list. This extra piece of info is not stored in the local cache. Then, I used the regular old Comparable.compareTo() method to compare those milesTo distances in a call to Collections.sort(). This introduces some inconsistencies with my equals() method, but it was a cleaner compromise for the situation.