HTML compass madness!

So I wanted to do a compass from a while just to push it to some apps mostly as a nice way of showing directions (e.g. "this way to the museum"). I thought that the hardest part would be to draw the needle, and translate North azimuth to object azimuth. Turned out I was wrong...

Update: There is now a more stable version of below code with additional library fo mocking compass and PhoneGap support. See: WebCompass project.

Basic compass from a browser

But let's start from the beginning. How is this even possible to have a compass in a browser? Well almost all smartphones and tablets have it. This works like a real compass and - in case you are wondering - you can have fun and make it go nuts with any magnet (like the ones you probably have on your refrigerator).

But from a browser? Yes. You just need to observe an DeviceOrientation event. Like this:

window.ondeviceorientation = function(eventData)
{
	drawCompass(eventData.alpha);
};
 

That easy... Well, no. But for now let's assume it will work and draw the needle on canvas.

Drawing compass on canvas

Preparing canvas

Quick background - canvas in real life is something artists use to paint on. Canvas in HTML is an element which programmers can use to paint on :-). This is how you declare it:

<canvas height="400" id="canvasId" width="400"></canvas>
 

To keep it simple we can initialize our canvas for drawing like this:

// this is what we'll use for accessing drawing functions
var context;
 
// we need to wait on load of the page
window.onload = function()
{
	var canvas = document.getElementById("canvasId");
	context = canvas.getContext("2d");
}
 

The drawCompassArrow function

So we have the angle and something to paint on. We now need to draw a line that rotates when an angle changes. Rotation is a bit tricky but this is not that bad.

An example implementation:

/**
 * alpha is a normalized angle where 0°/360° is North, 90° is East and so on.
 */
function drawCompassArrow(alpha)
{
	// line paramaters
	var cx, cy, radius;
	cx = cy = 70;	// this is a rotation center
	r = 50;			// this is a radius
 
	// clear canvas
	context.clearRect(0, 0, canvas.width, canvas.height);
 
	// save context matrix/settings for later restoration
	context.save();
 
	// transform deg. to radians
	var aRad = alpha * Math.PI / 180;
 
	// rotate on (cx, cy) point
	context.translate(cx,cy);
	context.rotate(-aRad);
	context.translate(-cx,-cy);
 
	// stroke styling
	context.lineWidth = 1;
	context.strokeStyle = 'black';
 
	// draw
	context.beginPath();
	context.moveTo(x, y);
	context.lineTo(x, y + radius);
	context.stroke();
 
	// restore matrix
	context.restore();
 
	// info about current angle
	context.font = "25px sans-serif";
	context.fillText(alpha.toString(), cx - 5, cy + radius + 25);
}
 


It works! ...or does it?

So we test e.g. on HTC Desire HD on Firefox. It works! But wait, let's test on other browsers/devices...

Basic testing...

Well I had 2 device at hand 3 browsers at each.

  • Nokia N9
    1. Native - eventData.alpha is ranged from -180° to 180° where both 180° and -180° means North.
    2. Firefox - doesn't support DeviceOrientation event.
    3. Opera - doesn't support DeviceOrientation event.
  • HTC Desire HD
    1. Native - doesn't support DeviceOrientation event (WTF?!).
    2. Firefox - eventData.alpha is ranged from 0° to 360° where both 0° and 360° means North.
    3. Opera - eventData.alpha is ranged from 0° to 360° where both 0° and 360° means North.

So you might say - it's not that bad. Only two cases, right? We just need to ask the user to rotate the device and if we get a negative value that we just do:

normalizedAlpha = Math.abs(eventData.alpha - 180);
 

The real testing - madness comes!

OK, so real normalizing is not a one-liner (as the one above), but few state variables, if-s and one function later you get a working example. North becomes North. Great!

But wait... Rotating works funny... It turns out we have 3 browsers and 3 cases. It's like the good old times ;-). What happens? Firefox gives you 90° for East, but Opera gives you 270° for East. Yupi!

You now need to:

  1. Tell the user to rotate his device clockwise. If he will do it the other way you're app is screwed.
  2. Figure out if the angle is getting bigger or smaller. Not that easy! After getting 5° you might get 360° but you should wait at least until you are at 330° to avoid user mistake.
  3. Make yet another transformation of the direction angle.

So now this little example code:

window.ondeviceorientation = function(eventData)
{
	drawCompass(eventData.alpha);
};			
 

Becomes a working code:

// function for translating what is read from the browser to 0-360
var directionTranslation = function(browserDirection)
{
	return browserDirection;
};
// if still calibrating
var inCalibration = true;
// initial calibration? (not yet know the range)
var inInitCalibration = true;
// max alpha
var maxAlphaDirection = 0;
 
/**
 * Basic device orientation handling which includes heading relative to magnetic North.
 */
var initCalibrationAngle = 0;
window.ondeviceorientation = function(e)
{
	if (inCalibration)
	{
		if (inInitCalibration)
		{
			// Nokia browser style (N9 tested): -180 to 180 where 180/-180 is North
			if (e.alpha < 0)
			{
				directionTranslation = function(browserDirection)
				{
					return Math.abs(browserDirection - 180);
				};
				inCalibration = inInitCalibration = false;
			}
			// Android HTC HD, Fox+Opera; 0 to 360 where 0/360 is North
			else if (e.alpha > 180)
			{
				// Some rotate to clockwise some oposite - still calibrating...
				inInitCalibration = false;
				initCalibrationAngle = Math.round(directionTranslation(e.alpha));
			}
		}
		else
		{
			// a is normalized to 0-360 integer
			var a = Math.round(directionTranslation(e.alpha));
			// normalize to 0-360 as if initCalibrationAngle where at 0
			a = (360 + a - initCalibrationAngle) % 360;
			// if user is between 60 and 120 deg. in the rotated space
			// this means we already have clock-wise compass (yupi!)
			if (60 < a && a < 120)
			{
				inCalibration = false;
			}
			// if user is between 300 and 240 deg. in the rotated space
			// this means we need make it a clock-wise compass...
			else if (240 < a && a < 300)
			{
				inCalibration = false;
 
				var prevDirectionTranslation = directionTranslation;
				directionTranslation = function(browserDirection)
				{
					return Math.abs(360 - prevDirectionTranslation(browserDirection));
				};
			}
		}
	}
	drawCompass(directionTranslation(e.alpha));
};


I've also made a working, simple compass available here: http://m.enux.pl/_tests/compass/

OK. So that should work everywhere it can work. Still it's hard to calibrate, so users might have problems using it. Some extra calculations might make it easier, but still many browsers (also in modern devices) don't support device motion events. So it's still not a prime time for compass in browsers.

However if you want to add compass to your web-view based app you can use PhoneGap (or other Cordova based framework). See compass API documentation on Apache Cordova page.