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):

No comments: