Saturday, August 31, 2013

Plotting icons in OpenStreetMap with GPS coordinates

I’ve been working on a project with GPS data, where a map plotting would be handy. At first I thought about using Google Maps, but it’s always better go open source, and since I’m an enthusiast of OpenStreetMap, I started searching a way to do it, and I found the OpenLayers project, which is insanely cool.

My goal: plot some small images on the map (icons), and when the cursor goes over an image, a popup would appear. On my way to make it happen, I had to develop some interesting things, that I share now with everyone, in the hope that it can be useful.

The first difficulty I found was to make OpenLayers understand GPS coordinates without much fuss. To do so, you must have a coordinate transformation, which is not exactly trivial. Below is a function to make this conversion trivial, yes, so that you’ll never need to worry about it again. The map argument is the map object you’re working upon, and lon and lat are floating point numbers.
function Geo(map, lat, lon) {
	return new OpenLayers.LonLat(lon, lat)
		.transform(new OpenLayers.Projection('EPSG:4326'),
			map.getProjectionObject());
}

// Example usage:
var map = new OpenLayers.Map('myMapDivId');
map.addLayer(new OpenLayers.Layer.OSM());
var coor = Geo(map, -5.773274, -35.204948);
map.setCenter(coor, 8);
Second, the image (icon) plotting. When you plot an image at a given point, this image is centered at that point. I wanted to plot an image of a pushpin, which points at the wrong location if centered – the very end of the pushpin will point to a location below the center of the image. It had to be moved above, so that the bottom of the image was exactly over the point.

The function below creates the image and plots it with a bottom offset, if needed. The img argument is a valid image URL, cx and cy are the size of the image in pixels, and bottomOffset is a boolean value. The function is asychronous and waits the image to be loaded; use the onDone callback, which receives the new marker as argument.
function PlotMarker(map, img, lat, lon, bottomOffset, onDone) {
	var imgObj = new Image();
	imgObj.src = img;
	imgObj.onload = function() {
		var sz = new OpenLayers.Size(imgObj.width, imgObj.height);
		var off = new OpenLayers.Pixel(-(sz.w / 2),
			bottomOffset ? -sz.h : -(sz.w / 2));
		var ico = new OpenLayers.Icon(img, sz, off);
		if(onDone !== undefined && onDone !== null)
			onDone(new OpenLayers.Marker(Geo(map, lat, lon), ico));
	};
}

// Example usage:
var map = new OpenLayers.Map('myMapDivId');
map.addLayer(new OpenLayers.Layer.OSM());
var markers = new OpenLayers.Layer.Markers('Markers');
map.addLayer(markers);
PlotMarker(map, 'pushpin.png', -5.773274, -35.204948,
	true, // it's a pushpin, I want it bottom-aligned
	function(mk) {
		markers.addMarker(mk);
	});
Below you have a fully functional web application composed of three files, which displays an OpenStreetMap map at the whole page, loads points from a separated JSON file, and renders it accordingly. There’s no server-side processing involved, but the JSON file could easily be something processed by the server, which would load the data from some sort of database. I used jQuery library to handle the mouseover event which shows the popup over the images, but it’s not really necessary if you want to use something else.

1) index.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8"/>
	<title>Rodrigo's map</title>
	<style>
	* { -moz-box-sizing:border-box; -webkit-box-sizing:border-box; box-sizing:border-box; }
	body { margin:0; font:10pt Arial; color:#242424; }
	#mapArea { position:absolute; width:100%; height:100%; }
	</style>
	<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.3.min.js"></script>
	<script src="http://openlayers.org/api/2.13.1/OpenLayers.js"></script>
	<script src="index.js"></script>
</head>
<body>
	<div id="mapArea"></div>
</body>
</html>
2) index.js
function PlotData(divId, data) {
	function Geo(map, lat, lon) {
		return new OpenLayers.LonLat(lon, lat)
			.transform(new OpenLayers.Projection('EPSG:4326'), map.getProjectionObject());
	}
	function PlotMarker(map, img, lat, lon, bottomOffset, onDone) {
		var imgObj = new Image();
		imgObj.src = img;
		imgObj.onload = function() {
			var sz = new OpenLayers.Size(imgObj.width, imgObj.height);
			var off = new OpenLayers.Pixel(-(sz.w / 2),
				bottomOffset ? -sz.h : -(sz.w / 2));
			var ico = new OpenLayers.Icon(img, sz, off);
			if(onDone !== undefined && onDone !== null)
				onDone(new OpenLayers.Marker(Geo(map, lat, lon), ico));
		};
	}

	var map = new OpenLayers.Map(divId);
	map.addLayer(new OpenLayers.Layer.OSM());
	map.setCenter(Geo(map, data.center[0], data.center[1]), data.zoom);

	var markers = new OpenLayers.Layer.Markers('Markers');
	map.addLayer(markers);

	var $map = $('#'+divId),
	$det = $('<div></div>').css({
			'position':'absolute',
			'padding':'0 4px',
			'display':'none',
			'box-shadow':'2px 2px 3px #999',
			'background-color':'rgba(255,255,255,.85)',
			'border':'1px solid #AAA'
		}).appendTo('body');
	$.each(data.points, function(i, pt) {
		PlotMarker(map, pt.icon, pt.coor[0], pt.coor[1], true, function(mk) {
			markers.addMarker(mk);
			mk.events.on({
				mouseover: function(ev) {
					$det.html(pt.text).css({
						left: (ev.pageX < $(document).width() / 2) ?
							ev.pageX+'px' : (ev.pageX - $det.outerWidth())+'px',
						top: (ev.pageY < $(document).height() / 2) ?
							ev.pageY+'px' : (ev.pageY - $det.outerHeight())+'px',
						display: 'block'
					});
					$map.css('cursor', 'pointer');
				},
				mouseout: function(ev) {
					$det.empty().css('display', 'none');
					$map.css('cursor', 'auto');
				}
			});
		});
	});
}

$(document).ready(function() {
	$.getJSON('data.json', function(data) {
		PlotData('mapArea', data);
	});
});
3) data.json – assumes image “pushpin.png” exists
{
	"center": [ -5.797942,-35.211782 ],
	"zoom": 14,
	"points": [
		{
			"coor": [ -5.799500,-35.21951 ],
			"icon": "pushpin.png",
			"text": "First point<br/>Anything here"
		},
		{
			"coor": [ -5.790982,-35.19409 ],
			"icon": "pushpin.png",
			"text": "Other <b>point</b> here"
		},
		{
			"coor": [ -5.802083,-35.20877 ],
			"icon": "pushpin.png",
			"text": "Something else<br/>placed here"
		}
	]
}
Enjoy.

Thursday, August 8, 2013

Firefox 23 is slower than Firefox 22

In last June, I’ve been amazed with the Firefox 22 launching. It featured a new JavaScript engine called OdinMonkey, and particularly on Linux, the browser became really fast and smooth. The upgrade was instantly noticeable with Firebug, which became a lot more responsive.

Yesterday, however, Mozilla rolled out Firefox 23 – with a horrible new logo, without contrast and which looks blurry at small size. But mainly, to my dismay, right after upgrade, everything became slower. It felt like last year’s versions, with a sluggish performance, and essentially a pain to use. On Firebug, this is felt very strongly.

I forgot to backup my profile, but luckily the profile structure was not modified, and I was able to downgrade, download Firefox 22 again and removing this horrible Firefox 23, in the hope that they can fix it on the next version.

Update, Sep. 19:
I’ve just tested Firefox 24, and it seems to be even slower than 23. So, I’m still keeping Firefox 22.

Update, Nov. 7:
Apparently the slowness is fixed on Firefox 25, which I’m testing right now. Finally.