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:

10 comments:

phiil said...

hello,

i'm wondering what is the maputils class ?

where does it come from ?

phiil said...

hello,first,thank you for giving informations to the community.It's very helpfull.I was very interested by the possibility to catch image from a WMS source. I tried your code and i realize that i don't know what is the "MapUtils" class. Could you please help me? ps:sorry for my bad level in english

Ludwig said...

The MapUtils class was just a simple class that extracts a few data values, eg. latitude or longitude from tuples or computes the corner coordinates of a map image. I am away from my desk at the moment, so I cannot give you the code, but it was nothing magic.
Sorry.

Clint Cabanero said...

Hey, awesome work! Can you share your MapUtils class?

Nick Shontz said...

Thanks much for the tutorial, It would be really helpful if you could post the maputils class.

Unknown said...

Here is a full example of his WMS

http://code.google.com/p/android-eksempler/source/browse/trunk/AndroidElementer/src/eks/kort/KortAktivitet.java

Martin said...

Hi,

that works well, but is there an update? because performance is very bad when panning or zooming.

Ahmed said...

using threads to overcome panning and zoom performance :


package google.trafic;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.Log;

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

public class WMSOverlay extends Overlay {
private WMSLoader wmsclient = new WMSLoader();
private long lat = 0;
private long lon = 0;
private int zoomLevel = 0;
private Bitmap image = null;
@Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
super.draw(canvas, mapView, shadow);
if (lat != mapView.getMapCenter().getLatitudeE6()
|| lon != mapView.getMapCenter().getLongitudeE6() || zoomLevel != mapView.getZoomLevel())
{
lat = mapView.getMapCenter().getLatitudeE6();
lon = mapView.getMapCenter().getLongitudeE6();
zoomLevel = mapView.getZoomLevel();

// Log.v("DRAWING", mapView.getMapCenter().getLatitudeE6() + " " +
// mapView.getMapCenter().getLongitudeE6() + " " + mapView.getZoomLevel());

new WMSThread(canvas, mapView);
}
else if(image != null)
{
Paint semitransparent = new Paint();
semitransparent.setAlpha(0x888);
canvas.drawBitmap(image, 0, 0, semitransparent);
}
}

public class WMSThread extends Thread {
Canvas canvas;
MapView mapView;

public WMSThread(Canvas canvas_, MapView mapView_) {
canvas = canvas_;
mapView = mapView_;
this.start();
}

public void run() {

try {
// WMSLoader2 wmsclient = new WMSLoader2("", new Handler());
GeoPoint[] cornerCoords = MapUtils.getCornerCoordinates(
mapView.getProjection(), canvas);
image = wmsclient.loadMap(canvas.getWidth(),
canvas.getHeight(), cornerCoords[0], cornerCoords[1]);
Paint semitransparent = new Paint();
semitransparent.setAlpha(0x888);
canvas.drawBitmap(image, 0, 0, semitransparent);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}

a said...

would you mind if this code found it's way into osmdroid's baseline? with attribution of course ;)

Ludwig said...

Feel free, but this code is so old, I am pretty sure there is something better out there.