Friday, 26 September 2008

A Simple WMS Client For Android

Google's maps are not the only ones of interest: for those interested in science there are many more maps out there that show things like population density, cloud images, etc.

Some of these maps are available via Web Map Service WMS interfaces as specified by the OGC. WMS is essentially a standardised interface to maps on the Web that allows a client to specify the format (e.g. PNG), the size (e.g. 320x460) and the exact geographical coordinates: and what the user gets in return in an image.

That makes it very easy to overlay Google Maps with all sorts of imagery, particularly satellite imagery.

For this we start out defining a new Overlay class, WMSClient:


private class WMSOverlay extends Overlay {
@Override
public void draw(Canvas canvas, MapView mapView,
boolean shadow) {
super.draw(canvas, mapView, shadow);
WMSLoader wmsclient = new WMSLoader();
GeoPoint[] cornerCoords = MapUtils.getCornerCoordinates(mapView.getProjection(), canvas);
Bitmap image = wmsclient.loadMap(canvas.getWidth(), canvas.getHeight(), cornerCoords[0], cornerCoords[1]);

Paint semitransparent = new Paint();
semitransparent.setAlpha(0x888);
canvas.drawBitmap(image, 0, 0, semitransparent);
}

}


It is very simple: it somehow gets a Bitmap image from a class WMSLoader, to which I come in a second. Then it simply draws that image on the canvas, making it here semitransparent, so we see the underlying Google image.

To get the WMS image we first need to get the corner coordinates of the screen, which is not very difficult as the projection object has a translation function for this (0,0) is the left top corner, and the right bottom is (width(), height()).

Now to our WMSLoader:

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

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;

import com.google.android.maps.GeoPoint;

public class WMSLoader {
public static String TAG = "WMSLoader";

public Bitmap loadMap(int width, int height, GeoPoint ul, GeoPoint lr) {
URL url = null;

try {
url = new URL(String.format("http://iceds.ge.ucl.ac.uk/cgi-bin/icedswms?" +
"LAYERS=lights&TRANSPARENT=true&FORMAT=image/png&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&STYLES=&EXCEPTIONS=application/vnd.ogc.se_inimage&SRS=EPSG:4326" + "" +
"&BBOX=%f,%f,%f,%f&WIDTH=%d&HEIGHT=%d",
MapUtils.longitude(ul), MapUtils.latitude(lr),
MapUtils.longitude(lr), MapUtils.latitude(ul), width, height));
} catch (MalformedURLException e) {
Log.e(TAG, e.getMessage());
}
InputStream input = null;
try {
input = url.openStream();
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
return BitmapFactory.decodeStream(input);
}

This class here has most of the URL hard-coded for proof of concept. Here we get an image from the ICEDS server, an academic WMS Server at University College London. In this case the 'lights' layer, which shows night-time light pollution - an indication of populatio density (or industrial activity). We just compose the complete URL, then open the input stream, where we get the image and pass it back to our Overlay. Bingo.

(There is a thing about projections: Google Maps uses a form of Mercator, the one supported by the ICEDS server is WGS84. That means pixels will not lie exactly on top of each other, but it matters most when the image is zoomed out. We would need a server supporting the Google Maps projection to be exact....)

Anyway, this is how it looks on the screen:

Thursday, 25 September 2008

Integrating Preferences

Every application needs to be configured somehow and fortunately in Android the preferences framework takes away most of the pain.

Here I want to have a single simple preference whether the tracklogging service of the GPS application is turned on.

First of all, a user dialog should be derived from PreferenceActivity. Then the onCreate() method can simply load a declaration of preferences from an XML file.

This is how the simplest onCreate() looks like:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
prefs = getSharedPreferences("com.ucont_preferences", Context.MODE_PRIVATE);
prefs.registerOnSharedPreferenceChangeListener(this);

}


That references a the file xml/preferences.xml in our resources. For our single preference this file can look like this:

<?xml version="1.0" encoding="utf-8"?>

<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">

<PreferenceCategory
android:title="@string/preferences_title">

<CheckBoxPreference
android:key="@string/preferences_key_tracklogging_enabled"
android:title="@string/preferences_tracklogging_enabled"
android:summary="@string/preferences_tracklogging_enabled_summary" />

</PreferenceCategory>
</PreferenceScreen>


And after we added the new PreferenceActivity to the manifest and put some wiring it to make it accessible, it shows up, quite nicely, in the dialog:


All changes are automagically persisted to an XML file in /data/data/myapp/shared_prefs/ from where it is easy to download the file and see that the changes actually happened.

That we store the preferences does of course not mean that any action is taken. This will be covered soon.

Wednesday, 24 September 2008

Starting an Android Service at Boot time

A service that has to be started manually is an oxymoron, so starting a service at boot time is for many applications a must.

My tracklogging service is such an example, which should run whenever the phone is turned on, so that one can refer to the route travelled later. A further example is the much touted CarbonFootprint, which again relies on accurate measurements all the time, and not just when the user has turned it on.

The last post detailed how the TrackloggingService worked, but started the service only when the main activity was launched. Now comes the time to hook it into the Android boot sequence. Here is how:

After boot completes the Android system broadcasts an intent with the action android.intent.action.BOOT_COMPLETED. And now all we need is an IntentReceiver, now called a BroadcastReceiver, to listen and act on it. This is how this class looks:


public class LocationLoggerServiceManager extends BroadcastReceiver {

public static final String TAG = "LocationLoggerServiceManager";
@Override
public void onReceive(Context context, Intent intent) {
// just make sure we are getting the right intent (better safe than sorry)
if( "android.intent.action.BOOT_COMPLETED".equals(intent.getAction())) {
ComponentName comp = new ComponentName(context.getPackageName(), LocationLoggerService.class.getName());
ComponentName service = context.startService(new Intent().setComponent(comp));
if (null == service){
// something really wrong here
Log.e(TAG, "Could not start service " + comp.toString());
}
} else {
Log.e(TAG, "Received unexpected intent " + intent.toString());
}
}
}

The key is of course the onReceive() method. I have decided to check that it is actually the Intent I am expecting, but otherwise it is straightforward starting the service and return.

The receiver needs to be declared in the manifest, e.g. with the following entry:

<receiver android:name=".LocationLoggerServiceManager"
android:enabled="true"
android:exported="false"
android:label="LocationLoggerServiceManager">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>


Furthermore this class listen to this specific event needs to be declared in the security settings:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />


That's it. Now the service will be started as soon a Android has finished booting.
Now we will need something to make this user-configurable...

A simple Android Tracklogging Service

So far, I had wired my tracklogging to the main Activity just to get going, but in real life this is supposed to happen behind the scenes, so a Service is the right thing to implement. This turned out to be easier that I thought.

First of all the class needs to be derived from Service, which is a bit like an Acitivty but without any (ever) visible action. As a service can potentially communicate with other applications it has the onBind() method, but we return always null here: there is nothing this service has to communicate about.

A second thing is that my tracklogging service needs to get the location updates, so I made it implement the LocationListener interface. I do not care that much about providers at the moment (with the Mock providers gone in 1.0 there is not so very much one can do anyway), I only implement onLocationChanged().

In this I simply stuff the data into my ContentProvider (which has been in some aspects covered earlier, but awaits a bigger write-up here soon) and I am done.
Fortunately, this operation should be very short, so there is no need to start another thread for this service.

This is how the service looks like:


import android.app.Service;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

public class LocationLoggerService extends Service implements LocationListener {

private final static String TAG = "LocationLoggerService";
LocationManager lm;
GPXWriter writer;

public LocationLoggerService() {
}

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
subscribeToLocationUpdates();
}

public void onLocationChanged(Location loc) {
Log.d(TAG, loc.toString());
ContentValues values = new ContentValues();
values.put(GPSData.GPSPoint.LONGITUDE, loc.getLongitude());
values.put(GPSData.GPSPoint.LATITUDE, loc.getLatitude());
values.put(GPSData.GPSPoint.TIME, loc.getTime());
getContentResolver().insert(GPSDataContentProvider.CONTENT_URI, values);
}
public void onProviderEnabled(String s){
}
public void onProviderDisabled(String s){
}
public void onStatusChanged(String s, int i, Bundle b){
}

public void subscribeToLocationUpdates() {
this.lm = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
this.lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
}
}


Now I only need to start the service. In the next post I will cover how to start the service at boot time, but here, I still link it to my main Activity, where in its onCreate() method I add a call to start the service, like so:

componentName comp = new ComponentName(getPackageName(), LocationLoggerService.class.getName());
ComponentName service = startService(new Intent().setComponent(comp));


The last step is to add the service to the AndroidManifest.xml:

<service android:name="LocationLoggerService"
android:enabled="true"
android:exported="false"
android:label="LocationLoggerService"
/>

Android SDK 1.0 released

Finally, Google has taken Android (almost) out of the beta camp with the release of the 1.0 version of the SDK, the little _r1 appendix indicating that this is in fact probably only the release candidate 1. But at least this means that the API should be stable for now, where 'now' means for the time that the HTC phone is the only with Android to be formally announced.

The Android Developers Mailing List has seen a marked increase in traffic in the last week, it looks like everybody is getting excited that Android is finally starting to leave the drawing board.

The good news is that there have been few changes in the API since 0.9 and my code seemed to be working almost without a change. The only two lines I had to comment out, had to do with the test provider. For now I left them uncommented, they had been copied from some example and I am now not so sure what they were for. The development code runs without them, so maybe it is good the lines are gone:

//this.lm.setTestProviderStatus(LocationManager.GPS_PROVIDER,LocationProvider.AVAILABLE, null, System.currentTimeMillis());
//this.lm.setTestProviderEnabled(LocationManager.GPS_PROVIDER,true);

Tuesday, 23 September 2008

Displaying the Copyright Information for Google Maps

A condition of using the Google Maps API in Android is the display of the Copyright notice as it is defined in the terms and conditions.
I have decided just to add another simple Activity that will just display the mandated string. This is a very simple Activity, which just uses a TextView and looks like this:

import android.app.Activity;
import android.os.Bundle;

public class CopyrightInfo extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.copyrightinfo);
}
}
The view is defined via XML in my layout/copyrightinfo.xml declaration:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="20dip"
android:layout_width="fill_parent"
android:layout_height="fill_parent">

<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/copyrightinfo_fulltext"/>

</LinearLayout>


I have decided to bypass the Activity matching mechanisms for Intents here and when it comes to launching the CopyrightInfo Activity, I have just hard-coded the class into my main Activity:


ComponentName comp = new ComponentName(this.getPackageName(),CopyrightInfo.class.getName());
startActivity(new Intent().setComponent(comp));


The whole shebang just looks like this now:


(By the way, does the ScreenCapture button on DDMS work for anyone? It seems a bit of functionality that is simply broken)

That was an easy lesson, but it is preparing the way for more displays from the GPS.

Monday, 22 September 2008

Displaying Tracklogs on Google Maps (Part 2)

Now that we have a simple MapView centered on the start of the track it comes to displaying the tracklog.

This is done via an Overlay. I first tried to display the tracklog as points using the ItemizedOverlay, but I ran into the issue that more than a few points seemed to degrade performance very markedly, as it is possible to interact with each of these points, that is not very surprising. (The big question was how much the emulator reflects the speed we will get from a real phone: it might be even slower -- or, since it runs on hardware, not software, much faster.)

The second approach is just to subclass Overlay and override the draw method, painting onto the canvas.
There were several issues to deal with:
  • tracklogs can be very long and it might for performance reason not be advisable to display all of them.
  • tracklogs can contain points off the map, which need not (and should not) be displayed.
  • if we select only those points on the map, we want to draw lines between adjacent points, but not between segments.
  • we will want to colour trackpoints, name them etc, but we do not tackle this issue at this point.
A first rough cut of the code looks like this:
private class TrackOverlay extends Overlay {

public TrackOverlay() {
}

private Point pointFromCursor(Projection p, Cursor c){
Double lat = c.getDouble(1);
Double lon = c.getDouble(2);
Point point = p.toPixels(MapUtils.geopointFromLatLong(lat, lon), null);
return point;
}

private Boolean moreThanMinimumDistance(Point a, Point b){
double distance = java.lang.Math.sqrt((double)(a.x - b.x) * (a.x - b.x)) + ((a.y - b.y) * (a.y -b.y));
if (distance > 3){
return true;
}
return false;
}

@Override
public void draw(Canvas canvas, MapView mapView,
boolean shadow) {
super.draw(canvas, mapView, shadow);
Log.e(TAG, "drawing tracks");
Projection proj = mapView.getProjection();

GeoPoint ul = proj.fromPixels(0,0);
GeoPoint lr = proj.fromPixels(canvas.getWidth(), canvas.getHeight());

// select only points in currently displayed window.
// TODO: this has the problem that we cut off the trail at the edges of the image.
// TODO: Ideally we would like to select one point further
String selection = GPSData.GPSPoint.LONGITUDE + " > " + ((Double)(ul.getLongitudeE6() / 1e6)).toString() + " and " +
GPSData.GPSPoint.LONGITUDE + " < " + ((Double)(lr.getLongitudeE6() / 1e6)).toString() + " and " + GPSData.GPSPoint.LATITUDE + " < " + ((Double)(ul.getLatitudeE6() / 1e6)).toString() + " and " + GPSData.GPSPoint.LATITUDE + " > " + ((Double)(lr.getLatitudeE6() / 1e6)).toString();

long points = 0;
Point previousPoint = null;
Integer previousId = 0;

Vector vector = new Vector();

Cursor cursor = getContentResolver().query(GPSDataContentProvider.CONTENT_URI, null, selection, null, null);

cursor.moveToFirst();
do {
// we do not draw isolated points here
if (null == previousPoint){
previousId = cursor.getInt(0);
previousPoint = pointFromCursor(proj, cursor);
continue;
}
Integer currentId = cursor.getInt(0);
if (previousId + 1 == currentId){
// points follow each other, so draw a line segment, if distance is
// large enough
Point currentPoint = pointFromCursor(proj, cursor);
if (moreThanMinimumDistance(previousPoint, currentPoint)){
vector.add(previousPoint.x);
vector.add(previousPoint.y);
vector.add(currentPoint.x);
vector.add(currentPoint.y);
points +=1;
previousPoint = currentPoint;
} else {
// distance is not large enough, just move on
}

} else {
// we are at a break in the track, no line to be painted
previousPoint = pointFromCursor(proj, cursor);
}
previousId = currentId;
} while (cursor.moveToNext() && points < i =" 0;" paint =" new">
First of all, from the ContentManager we only get those points that fall onto the currently displayed screen. There are some pitfalls to this:
  1. actually what we want is all points on the screen plus those that precede and follow them so the tracklog will be drawn to the edge of the screen. This could almost be done by selecting a slightly larger window for the selection, but there is no guarantee that the first outlying points will be in. This is yet to be resolved.
  2. what we get are potentially track-segments where the track meanders on and off the screen. To test for this the code relies on the autonumbering of IDs in the DB: where there is a gap between painted points, there is a gap in the track and we do not draw a line.
  3. depending on the zoomLevel, a lot of different geographical points (GeoPoints) will fall onto the same point on the screen and drawing a line from a to a is pointless and expensive. With the moreThanMinimumDistance I compute a simple pixel Pythagoras and exclude those points that are below a certain distance. This will be a bit inaccurate, but difficult to see on the real unit.
  4. I arbitrarily limit the number of points drawn to some X. This will at one point become a configuration issue, settable for preference and processing power of the unit.

Lastly, I add my new overlay to the MapView with the following line:

view.getOverlays().add(new TrackOverlay());
This is now how it all looks (with a tracklog over Cuba, where GPS are illegal):