Wednesday 1 October 2008

An OpenStreetMap ContentProvider for Android

The Android ContentProvider is a wrapper for data access that allows applications to hide the specifics behind a small number of calls that are modelled on a database interface.
However, there is nothing that limits a ContentProvider to being implemented on top of a DB: any data source will do.

The OpenStreetMap Project is a collaborative mapping project, collecting map data all around the world for access under a liberal license (attribution/shared-alike). There are several ways of getting hold of OSM data, but one is the OSMXAPI interface. It allows getting data based on a bounding box and selection details. Returned from a query is (if successful) an XML document, which we parse with SAX.

Here, we implement a simple lookup only interface to OSMXAPI, which looks up public toilets.

Fundamentally, the query method, which is really the only we implement, goes like this:
  1. First of all we expect in the selection parameter something like "amenity=toilets". OSMXAPI is not a database, but this is as close as a DB selection parameter as we get in this context.
  2. Secondly in the selectionArgs we expect the four corner coordinates for which to check. This is a bit a rough-handling of this parameter, but the ContentProvider interface is a bit too tightly coupled to DBs to do this differently.
  3. Then we construct the URL for the OSMXAPI call and retrieve the XML document. If anything goes wrong here, we will just return an empty cursor.
  4. Now we parse the document with an XML parser, scanning for the nodes which contain the lat/long positions of our points. Every node is inserted into the data of a MatrixCursor, which is a generalised cursor essentially containing a table of data over which the user can iterate. 
  5. We return this cursor to the user.

This is the full code for the ContentProvider:

package com.ucont;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.util.Log;

public class OSMPointsContentProvider extends ContentProvider {

private final String TAG = "OSMPointsContentProvider";

public static final String AUTHORITY = "org.osm.data";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/osmdata");

private final String osmxapiserver = "http://www.informationfreeway.org/api/0.5/";

private class OSMPointCursor extends MatrixCursor {
public OSMPointCursor(String[] columnNames) {
super(columnNames);
}
}

protected String constructQueryUrl(String selection, Double left, Double bottom, Double right, Double top) {
String query = String.format("node[%][bbox=%f,%f,%f,%f]", selection, left, bottom, right, top);
String url = osmxapiserver + query;
return url;
}

protected String constructQueryUrl(String selection, String top, String left, String bottom, String right) {
String query = String.format("node[%s][bbox=%s,%s,%s,%s]", selection, left, bottom, right, top);
String url = osmxapiserver + query;
return url;
}


@Override
public int delete(Uri arg0, String arg1, String[] arg2) {
// TODO Auto-generated method stub
return 0;
}

@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub
return null;
}

@Override
public Uri insert(Uri uri, ContentValues values) {
// TODO Auto-generated method stub
return null;
}

@Override
public boolean onCreate() {
// TODO Auto-generated method stub
return false;
}

private class OSMHandler extends DefaultHandler {
private OSMPointCursor cursor;
OSMHandler(OSMPointCursor cursor){
super();
this.cursor = cursor;
}
public void startElement (String uri, String name,
String qName, Attributes atts){
if (name.equals("node")){
Object[] columnValues = {atts.getValue("lat"), atts.getValue("lon")};
cursor.addRow(columnValues);
}
}
}

@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
String[] columnNames = {"lat", "lon" };
OSMPointCursor result = new OSMPointCursor(columnNames);
URL url = null;
if (null == selection){
selection = "*=*";
}
try {
url = new URL (constructQueryUrl(selection, selectionArgs[0], selectionArgs[1], selectionArgs[2], selectionArgs[3]));
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser();
XMLReader xmlReader = sp.getXMLReader();
OSMHandler handler = new OSMHandler(result);
xmlReader.setContentHandler(handler);
xmlReader.setErrorHandler(handler);
xmlReader.parse(new InputSource(url.openStream()));
} catch (MalformedURLException e){
Log.e(TAG, e.getMessage());
} catch (IOException e){
Log.e(TAG, "IOException " + e.getMessage());
} catch (SAXException e){
Log.e(TAG, "SAX " + e.getMessage());
} catch (ParserConfigurationException e) {
Log.e(TAG, "PC Exception " + e.getMessage());
}

return result;
}

@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO Auto-generated method stub
return 0;
}

}
What are the lessons learned from this?
  1. The ContentProvider interface is a bit too closely aligned with databases. A more general concept, based on key/values to match would have been much better. Android developers have somehow recognised this and provided the MatrixCursor for a generalised return value.
  2. It would be nice if a ContentProvider could be queried which operations it supported. That way there would be a general way of implementing a read-only or write-only ContentProvider. The interface for insert does not allow a ContentProvider to throw an exception to indicate that inserts are not supported.
  3. It would be good to have things like cost functions on ContentProviders (e.g. the cheapest way of providing a certain content or the most up-to-date). In that sense ContentProvider is some two decades behind what has been done in terms of service provision negotiation in distributed systems. 
Next will be to display the retrieved data on a map.




1 comment:

schaze said...

Thanks a million for this!

I never thought I would come around an example of a content provider that does NOT use sqlite! I have been searching for ages. Now I finally have a starting point.