Thursday 13 November 2008

Displaying USGS Earthquake Data on Android


The United States Geological Survey (USGS) has a helpful RSS feed for earthquakes all around the globe. You can find the various feeds here. Here I illustrate how to load the data and display them as an overlay on Google maps on Android.

The principal way I have chosen is to have a service running in the background that periodically gets the latest earthquakes and stores them in a ContentProvider. The map overlay queries the ContentProvider for the earthquakes for the given map extent.

We start off with the display. The display is an overlay that redefines the draw() method. There are two steps: first to get all the earthquakes from the content provider, secondly drawing them (with a bit of hint on the magnitude).

Finding all earthquakes is easy, bar the pain of accommodating that map views can span the dateline, where the query to the content provider gets a bit more difficult. First I thought that simply changing the where clause according to the left hand side longitude being smaller than the right hand side longitude was sufficient, but then it turned out that one can zoom out so much that the right hand side longitude is larger than the left hand side, but the map still contains the dateline. I handle this case by checking if the longitude span is larger than 360 degrees. That means that earthquakes regardless of longitude need to be returned. (The same does not apply to latitude: Google maps always just cuts off at 80 degrees and does not wrap around. (For engineers of the Google phone the world seems to be a wrap-around band. They seem to smoke weird stuff down in California, but I guess it makes their life a bit easier.)

Once I have all the earthquakes, I iterate through the list and display them according to magnitude: one degree of magnitude gets one circle and using the zoom-level I scale this a little bit, so that the concentric circles get smaller if one zooms out, resembling a bit an impact zone.

This is the code in all its glory:

import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.util.Log;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;

class EarthquakeOverlay extends Overlay {

public final static String TAG = "EarthquakeOverlay";

EarthquakeOverlay(){}

@Override
public void draw(Canvas canvas, MapView mapView,
boolean shadow) {
super.draw(canvas, mapView, shadow);
GeoPoint[] cornerCoords = MapUtils.getCornerCoordinates(mapView.getProjection(), canvas);
String selection;
if (mapView.getLongitudeSpan() >= 360 * 1e6){
selection = EarthquakeContentProvider.EarthquakeData.LAT + " < " + ((Integer)cornerCoords[0].getLatitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LAT + " > " + ((Integer)cornerCoords[1].getLatitudeE6()).toString();
} else if (cornerCoords[0].getLongitudeE6() < cornerCoords[1].getLongitudeE6()){
selection = EarthquakeContentProvider.EarthquakeData.LAT + " < " + ((Integer)cornerCoords[0].getLatitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LAT + " > " + ((Integer)cornerCoords[1].getLatitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LON + " > " + ((Integer)cornerCoords[0].getLongitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LON + " < " + ((Integer)cornerCoords[1].getLongitudeE6()).toString();
} else {
selection = EarthquakeContentProvider.EarthquakeData.LAT + " < " + ((Integer)cornerCoords[0].getLatitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LAT + " > " + ((Integer)cornerCoords[1].getLatitudeE6()).toString() + " and ( " +
EarthquakeContentProvider.EarthquakeData.LON + " > " + ((Integer)cornerCoords[0].getLongitudeE6()).toString() + " or " +
EarthquakeContentProvider.EarthquakeData.LON + " < " + ((Integer)cornerCoords[1].getLongitudeE6()).toString() + ")";
}

Projection p = mapView.getProjection();
Cursor cursor = null;
Paint paint = new Paint();
paint.setStrokeWidth(0);
paint.setStyle(Paint.Style.STROKE);
try {
cursor = mapView.getContext().getContentResolver().query(EarthquakeContentProvider.EarthquakeData.CONTENT_URI, null, selection, null, null);
if (cursor != null) {
cursor.moveToFirst();
while (!cursor.isAfterLast()){
GeoPoint gp = MapUtils.geopointFromLatLongE6(cursor.getInt(EarthquakeContentProvider.EarthquakeData.LAT_CN), cursor.getInt(EarthquakeContentProvider.EarthquakeData.LON_CN));
Point point = p.toPixels(gp, null);
Float magnitude = cursor.getFloat(EarthquakeContentProvider.EarthquakeData.MAGNITUDE_CN);
canvas.drawPoint(point.x, point.y, paint);
int zoomLevel = mapView.getZoomLevel() + 1;
for (int i = 0; i < magnitude; i++){
canvas.drawCircle(point.x, point.y, i * zoomLevel, paint);
}
cursor.moveToNext(); }
}
} finally {
if (cursor != null){
cursor.close();
}
}
}
}

How to get the data from USGS follows in one of the next posts...

6 comments:

jgostylo said...

Hey, I am writing an android app using Google Maps and I am having trouble getting correct data for the corner coordinates. I see that you use MapUtils in several of your examples but as far as I can tell this is a class you wrote yourself. Would you mind sharing it? Or if I am just thick would you mind pointing out where it is?

Ludwig said...

Getting the corner coordinates of a map view is not difficult. All it involves is knowing that the projection object has a translation from pixel to geographic coordinates.
The top left corner of the map view is (0,0), while the bottom right pixels in width -1 /height -1 of the view). So the top left corner is simply:

projection.fromPixels(0, 0);

while to the bottom right one is

projection.fromPixels(getWidth() - 1, getHeight() - 1);

jgostylo said...

Then I must have a different issue because when I do a getHeight and getWidth on my view it always returns 0 even though it fills the screen. Maybe I am calling it on the wrong object or calling it before the view is finalized. I was hoping there was some other way it was done.

Ludwig said...

Hmmm, always bad to do some last minute fixes without checking. In my original code back then I had used the getHeight/getWidth on the canvas object, but when you asked the question I thought using the view itself might be better. No time to verify this right now, though.

jgostylo said...

I was trying to follow your possible mistaken lead and see if getting the height/width of the canvas was a way to do this. I can't seem to find anything that will give me the Canvas object to do this though. What code do you use to retrieve the Canvas object?

Ludwig said...

You get the canvas as part of the draw method, which you can override:

@Override
public void draw(Canvas canvas, MapView mapView,
boolean shadow){
}

This is part of the code originally posted, so have a look there.