Archive for May, 2011

Share on TwitterDigg This

I’m still playing with Android (loving it, it would be so nice if I could actually do it for living). So I was playing with the MapView and MapActivity. After playing for a while with the google maps API on JS and also with openlayers, the MapView is really missing some basic stuff.

On my simple demo the basics I needed:

  • Receive zoom events
  • Receive pan events
  • Get the map extent (that is the bounds from the rectangle that defines your map view)

For my surprise none of those are actually defined on the MapView, but at least they can be easily added. So here’s my small contribution to those out there trying to get the same

The events

So first, two small listeners that you can use to receive notifications once a zoom or a pan has happened:

package com.furiousbob.slide.events;
 
import com.google.android.maps.GeoPoint;
 
public interface PanChangeListener {
	public void onPan(GeoPoint old, GeoPoint current);
}
package com.furiousbob.slide.events;
 
public interface ZoomChangeListener {
	public void onZoom(int old, int current);
}

The SimpleMapView

That’s basically it, so now for the new map view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package com.furiousbob.slide;
 
import java.util.ArrayList;
import java.util.List;
 
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;
 
import com.furiousbob.slide.events.PanChangeListener;
import com.furiousbob.slide.events.ZoomChangeListener;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
 
public class SimpleMapView extends MapView {
 
	private int currentZoomLevel = -1;
	private GeoPoint currentCenter;
	private List<ZoomChangeListener> zoomEvents = new ArrayList<ZoomChangeListener>();
	private List<PanChangeListener> panEvents = new ArrayList<PanChangeListener>();
 
	public SimpleMapView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
 
	public SimpleMapView(Context context, String apiKey) {
		super(context, apiKey);
	}
 
	public SimpleMapView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
 
	public int[][] getBounds() {
		GeoPoint center = getMapCenter();
		int latitudeSpan = getLatitudeSpan();
		int longtitudeSpan = getLongitudeSpan();
		int[][] bounds = new int[2][2];
 
		bounds[0][0] = center.getLatitudeE6() + (latitudeSpan / 2);
		bounds[0][1] = center.getLongitudeE6() + (longtitudeSpan / 2);
 
		bounds[1][0] = center.getLatitudeE6() - (latitudeSpan / 2);
		bounds[1][1] = center.getLongitudeE6() - (longtitudeSpan / 2);
		return bounds;
	}
 
	public boolean onTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            GeoPoint centerGeoPoint = this.getMapCenter();
            if (currentCenter == null || 
                    (currentCenter.getLatitudeE6() != centerGeoPoint.getLatitudeE6()) ||
                    (currentCenter.getLongitudeE6() != centerGeoPoint.getLongitudeE6()) ) {
            	firePanEvent(currentCenter, this.getMapCenter());
            }
            currentCenter = this.getMapCenter();
        }
        return super.onTouchEvent(ev);
    }
 
	@Override
	protected void dispatchDraw(Canvas canvas) {
		super.dispatchDraw(canvas);
		if(getZoomLevel() != currentZoomLevel){
			fireZoomLevel(currentZoomLevel, getZoomLevel());
			currentZoomLevel = getZoomLevel();
		}
	}
 
	private void fireZoomLevel(int old, int current){
		for(ZoomChangeListener event : zoomEvents){
			event.onZoom(old, current);
		}
	}
 
	private void firePanEvent(GeoPoint old, GeoPoint current){
		for(PanChangeListener event : panEvents){
			event.onPan(old, current);
		}
	}
 
	public void addZoomChangeListener(ZoomChangeListener listener){
		this.zoomEvents.add(listener);
	}
 
	public void addPanChangeListener(PanChangeListener listener){
		this.panEvents.add(listener);
	}
 
 
}

The new stuff here is the line 35

	public int[][] getBounds() {

This is the new method that will give you an array with the bounds of your latitude and longitude. You can then use this to query a spatial database for instance :)

Zoom events are detected at the dispatchDraw method on line 63. We have to store the old value and the new value. If the view gets to be redrawn (either by a pan or a zoom) we just check if it was a zoom and fire the event

And finally them pan. On a “finger up” event we just fire an event informing the new center of the map.

Those small methods really helped me achieving what was required for my use case. It’s a simple addition that I really don’t understand why google left out of it’s API.

Happy coding

Share on TwitterDigg This

Few weeks ago, I got my self a motorola xoom. From an android developer perspective that is just heaven :) . I really believe that iPad is just a giant size iphone, and having the opportunity to have the first real tablet OS is just amazing.

So, last week I’ve started playing around with it for real. I do have some ideas around for an app, that may never took off, but at least I’m having fun creating loads of mini apps and experimenting the new APIs.

Today I’m working with information visualization at Ericsson. And we’ve been trying all types of nice visual paradigms to display information. It’s been a really, really nice ride. One of my favorites is heatmaps. If you want to know how to implement a heatmap this link is your BEST bet. It’s really amazing what the author had achieved. It’s simple, it’s beautiful and it works perfectly. And since “imitation is the sincerest form of flattery” (I’ve just copied this from the best book on REST out there: REST in Practice ). I decided to port the original Javascript and canvas algorithm to the android platform. And on Honeycomb, thanks to the GPU hardware acceleration, it looks quite amazing. So let’s get our hand’s dirty

The alpha map

You should take a time and go to the original article and understand a bit of the magic behind the heatmap. To sum up, first we create a alpha map with a shade of black/white and using alpha to define the heat (intensity) of the future color. That’s really simple with canvas API on android:

RadialGradient g = new RadialGradient(x, y, radius, Color.argb(10, 0, 0, 0), Color.TRANSPARENT, TileMode.CLAMP);
Paint gp = new Paint();
gp.setShader(g);
myCanvas.drawCircle(x, y, radius, gp);

Here we are just creating a circle with a paint configured as a RadialGradient shader. The colors varies from black with a low alpha (almost transparent) to a complete transparent color. The values x,y are just the coordinates we want to add the circles.

Adding some color

Now let’s add some color to the recently created circle. Here what we do is iterate over every pixel of the square that circle is contained within, and change it’s pixel color (keeping the alpha, that’s important). The variable backbuffer is just our bitmap that the canvas we used previously draw at.

private void colorize(float x, float y, float d) {
		if (x + d > myCanvas.getWidth()) {
			x = myCanvas.getWidth() - d;
		}
		if (x < 0) {
			x = 0;
		}
		if (y < 0) {
			y = 0;
		}
		if (y + d > myCanvas.getHeight()) {
			y = myCanvas.getHeight() - d;
		}
 
		int[] pixels = new int[(int) (d * d)];
		backbuffer.getPixels(pixels, 0, (int) d, (int) x, (int) y, (int) d,
				(int) d);
		for (int i = 0; i < pixels.length; i++) {
			int r = 0, g = 0, b = 0, tmp = 0;
			int alpha = pixels[i] >>> 24;
			if (alpha <= 255 && alpha >= 240) {
				tmp = 255 - alpha;
				r = 255 - tmp;
				g = tmp * 12;
			} else if (alpha <= 239 && alpha >= 200) {
				tmp = 234 - alpha;
				r = 255 - (tmp * 8);
				g = 255;
			} else if (alpha <= 199 && alpha >= 150) {
				tmp = 199 - alpha;
				g = 255;
				b = tmp * 5;
			} else if (alpha <= 149 && alpha >= 100) {
				tmp = 149 - alpha;
				g = 255 - (tmp * 5);
				b = 255;
			} else
				b = 255;
			pixels[i] = Color.argb(alpha, r, g, b);
		}
		backbuffer.setPixels(pixels, 0, (int) d, (int) x, (int) y, (int) d,
				(int) d);
	}

What’s worth mentioning on the above code is the extraction of the alpha value from a ARGB integer:

int alpha = pixels[i] >>> 24;

Tracking movement, refreshing the screen

Now the final touch (literally). In android you can track user movement by overriding the onTouchEvent method. So in our View we just override this method, and inside it, we call the method addPoint (which in turn draws the circle, colorize it and refreshes the screen by calling invalidate()) :

public boolean onTouchEvent(MotionEvent event) {
addPoint(event.getX(),event.getY());
return true;
}
public void addPoint(float x, float y){
RadialGradient g = new RadialGradient(x, y, radius, Color.argb(10, 0, 0, 0), Color.TRANSPARENT, TileMode.CLAMP);
Paint gp = new Paint();
gp.setShader(g);
myCanvas.drawCircle(x, y, radius, gp);
colorize(x - radius, y - radius, radius * 2);
invalidate();
}
protected void onDraw(Canvas canvas) {
if (backbuffer == null) {
	init();
}
canvas.drawBitmap(backbuffer, 0, 0, new Paint(Paint.ANTI_ALIAS_FLAG));
}

That’s pretty much it. After adding a point we make a call to invalidate, which will force your view to redraw itself, calling the onDraw method, where you should copy your backBuffer bitmap (which contains your heatmap to your view’s canvas). Besides some initialization code, that’s all that’s needed to create a heatmap on your android. Pretty simple for something so fancy right?

Wait a second, what about multi touch?

Yeah, you are right. There’s no fun doing all this on a device that can track 255 simultaneous fingers right? So the final code below you can see the version with multi touch support.

I really hope you enjoyed this, please let me know if you decide to use this somewhere, it would be quite nice to see that we have smart people and companies out there that would like to benefit from this.

package com.furiousbob.trackme;
 
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Shader.TileMode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
 
public class HeatView extends View {
	private Canvas myCanvas;
	private Bitmap backbuffer;
	private float radius;
 
	public HeatView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
 
	public HeatView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
 
	public HeatView(Context context) {
		super(context);
	}
 
	private void init() {
		this.radius = 20f;
		backbuffer = Bitmap.createBitmap(getWidth(), getHeight(),
				Bitmap.Config.ARGB_8888);
		myCanvas = new Canvas(backbuffer);
		Paint p = new Paint();
		p.setStyle(Paint.Style.FILL);
		p.setColor(Color.TRANSPARENT);
		myCanvas.drawRect(0, 0, getWidth(), getHeight(), p);
 
	}
 
	@Override
	protected void onDraw(Canvas canvas) {
 
		if (backbuffer == null) {
			init();
		}
		canvas.drawBitmap(backbuffer, 0, 0, new Paint(Paint.ANTI_ALIAS_FLAG));
 
	}
 
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		int fingers = event.getPointerCount();
		float points[][] = new float[fingers][2];
		for (int i = 0; i < fingers; i++) {
			points[i][0] = event.getX(event.getPointerId(i));
			points[i][1] = event.getY(event.getPointerId(i));
		}
		addPoint(points);
		return true;
	}
 
	public void addPoint(float[][] points) {
		for (int i = 0; i < points.length; i++) {
			float x = points[i][0];
			float y = points[i][1];
			RadialGradient g = new RadialGradient(x, y, radius, Color.argb(10,
					0, 0, 0), Color.TRANSPARENT, TileMode.CLAMP);
			Paint gp = new Paint();
			gp.setShader(g);
			myCanvas.drawCircle(x, y, radius, gp);
			colorize(x - radius, y - radius, radius * 2);
		}
		invalidate();
	}
 
	private void colorize(float x, float y, float d) {
		if (x + d > myCanvas.getWidth()) {
			x = myCanvas.getWidth() - d;
		}
		if (x < 0) {
			x = 0;
		}
		if (y < 0) {
			y = 0;
		}
		if (y + d > myCanvas.getHeight()) {
			y = myCanvas.getHeight() - d;
		}
 
		int[] pixels = new int[(int) (d * d)];
		backbuffer.getPixels(pixels, 0, (int) d, (int) x, (int) y, (int) d,
				(int) d);
		for (int i = 0; i < pixels.length; i++) {
			int r = 0, g = 0, b = 0, tmp = 0;
			int alpha = pixels[i] >>> 24;
			if (alpha <= 255 && alpha >= 240) {
				tmp = 255 - alpha;
				r = 255 - tmp;
				g = tmp * 12;
			} else if (alpha <= 239 && alpha >= 200) {
				tmp = 234 - alpha;
				r = 255 - (tmp * 8);
				g = 255;
			} else if (alpha <= 199 && alpha >= 150) {
				tmp = 199 - alpha;
				g = 255;
				b = tmp * 5;
			} else if (alpha <= 149 && alpha >= 100) {
				tmp = 149 - alpha;
				g = 255 - (tmp * 5);
				b = 255;
			} else
				b = 255;
			pixels[i] = Color.argb(alpha, r, g, b);
		}
		backbuffer.setPixels(pixels, 0, (int) d, (int) x, (int) y, (int) d,
				(int) d);
	}
 
}

To give you an idea of this app running, I’ve decided to post a movie :)