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.

1 comment:

telugu muni said...

The way you presented the code is more systematic and attractive.
Same menu with different approach is given here: DROP-DOWN MENU .

Also you may find vrious menu bars and banner source code at: www.freemenu.info