Simon Willison’s Weblog

Subscribe

Unobtrusively Mapping Microformats with jQuery

12th December 2007

Microformats are everywhere. You can’t shake an electronic stick these days without accidentally poking a microformat-enabled site, and many developers use microformats as a matter of course. And why not? After all, why invent your own class names when you can re-use pre-defined ones that give your site extra functionality for free?

Nevertheless, while it’s good to know that users of tools such as Tails and Operator will derive added value from your shiny semantics, it’s nice to be able to reuse that effort in your own code.

We’re going to build a map of some of my favourite restaurants in Brighton. Fitting with the principles of unobtrusive JavaScript, we’ll start with a semantically marked up list of restaurants, then use JavaScript to add the map, look up the restaurant locations and plot them as markers.

We’ll be using a couple of powerful tools. The first is jQuery, a JavaScript library that is ideally suited for unobtrusive scripting. jQuery allows us to manipulate elements on the page based on their CSS selector, which makes it easy to extract information from microformats.

The second is Mapstraction, introduced here by Andrew Turner a few days ago. We’ll be using Google Maps in the background, but Mapstraction makes it easy to change to a different provider if we want to later.

Getting Started

We’ll start off with a simple collection of microformatted restaurant details, representing my seven favourite restaurants in Brighton. The full, unstyled list can be seen in restaurants-plain.html. Each restaurant listing looks like this:

<li class="vcard">
	<h3><a class="fn org url" href="http://www.riddleandfinns.co.uk/">Riddle & Finns</a></h3>
	<div class="adr">
		<p class="street-address">12b Meeting House Lane</p>
		<p><span class="locality">Brighton</span>, <abbr class="country-name" title="United Kingdom">UK</abbr></p>
		<p class="postal-code">BN1 1HB</p>
	</div>
	<p>Telephone: <span class="tel">+44 (0)1273 323 008</span></p>
	<p>E-mail: <a href="mailto:info@riddleandfinns.co.uk" class="email">info@riddleandfinns.co.uk</a></p>
</li>

Since we’re dealing with a list of restaurants, each hCard is marked up inside a list item. Each restaurant is an organisation; we signify this by placing the classes fn and org on the element surrounding the restaurant’s name (according to the hCard spec, setting both fn and org to the same value signifies that the hCard represents an organisation rather than a person).

The address information itself is contained within a div of class adr. Note that the HTML <address> element is not suitable here for two reasons: firstly, it is intended to mark up contact details for the current document rather than generic addresses; secondly, address is an inline element and as such cannot contain the paragraphs elements used here for the address information.

A nice thing about microformats is that they provide us with automatic hooks for our styling. For the moment we’ll just tidy up the whitespace a bit; for more advanced style tips consult John Allsop’s guide from 24 ways 2006.

.vcard p {
	margin: 0;
}
.adr {
	margin-bottom: 0.5em;
}

To plot the restaurants on a map we’ll need latitude and longitude for each one. We can find this out from their address using geocoding. Most mapping APIs include support for geocoding, which means we can pass the API an address and get back a latitude/longitude point. Mapstraction provides an abstraction layer around these APIs which can be included using the following script tag:

<script type="text/javascript" src="http://mapstraction.com/src/mapstraction-geocode.js"></script>

While we’re at it, let’s pull in the other external scripts we’ll be using:

<script type="text/javascript" src="jquery-1.2.1.js"></script>
<script src="http://maps.google.com/maps?file=api&v=2&key=YOUR_KEY" type="text/javascript"></script>
<script type="text/javascript" src="http://mapstraction.com/src/mapstraction.js"></script>
<script type="text/javascript" src="http://mapstraction.com/src/mapstraction-geocode.js"></script>

That’s everything set up: let’s write some JavaScript!

In jQuery, almost every operation starts with a call to the jQuery function. The function simulates method overloading to behave in different ways depending on the arguments passed to it. When writing unobtrusive JavaScript it’s important to set up code to execute when the page has loaded to the point that the DOM is available to be manipulated. To do this with jQuery, pass a callback function to the jQuery function itself:

jQuery(function() {
	// This code will be executed when the DOM is ready
});

Initialising the map

The first thing we need to do is initialise our map. Mapstraction needs a div with an explicit width, height and ID to show it where to put the map. Our document doesn’t currently include this markup, but we can insert it with a single line of jQuery code:

jQuery(function() {
	// First create a div to host the map
	var themap = jQuery('<div id="themap"></div>').css({
		'width': '90%',
		'height': '400px'
	}).insertBefore('ul.restaurants');
});

While this is technically just a single line of JavaScript (with line-breaks added for readability) it’s actually doing quite a lot of work. Let’s break it down in to steps:

var themap = jQuery('<div id="themap"></div>')

Here’s jQuery’s method overloading in action: if you pass it a string that starts with a < it assumes that you wish to create a new HTML element. This provides us with a handy shortcut for the more verbose DOM equivalent:

var themap = document.createElement('div');
themap.id = 'themap';

Next we want to apply some CSS rules to the element. jQuery supports chaining, which means we can continue to call methods on the object returned by jQuery or any of its methods:

var themap = jQuery('<div id="themap"></div>').css({
	'width': '90%',
	'height': '400px'
})

Finally, we need to insert our new HTML element in to the page. jQuery provides a number of methods for element insertion, but in this case we want to position it directly before the <ul> we are using to contain our restaurants. jQuery’s insertBefore() method takes a CSS selector indicating an element already on the page and places the current jQuery selection directly before that element in the DOM.

var themap = jQuery('<div id="themap"></div>').css({
	'width': '90%',
	'height': '400px'
}).insertBefore('ul.restaurants');

Finally, we need to initialise the map itself using Mapstraction. The Mapstraction constructor takes two arguments: the first is the ID of the element used to position the map; the second is the mapping provider to use (in this case google ):

// Initialise the map
var mapstraction = new Mapstraction('themap','google');

We want the map to appear centred on Brighton, so we’ll need to know the correct co-ordinates. We can use www.getlatlon.com to find both the co-ordinates and the initial map zoom level.

// Show map centred on Brighton
mapstraction.setCenterAndZoom(
	new LatLonPoint(50.82423734980143, -0.14007568359375),
	15 // Zoom level appropriate for Brighton city centre
);

We also want controls on the map to allow the user to zoom in and out and toggle between map and satellite view.

mapstraction.addControls({
	zoom: 'large',
	map_type: true
});

Adding the markers

It’s finally time to parse some microformats. Since we’re using hCard, the information we want is wrapped in elements with the class vcard. We can use jQuery’s CSS selector support to find them:

var vcards = jQuery('.vcard');

Now that we’ve found them, we need to create a marker for each one in turn. Rather than using a regular JavaScript for loop, we can instead use jQuery’s each() method to execute a function against each of the hCards.

jQuery('.vcard').each(function() {
	// Do something with the hCard
});

Within the callback function, this is set to the current DOM element (in our case, the list item). If we want to call the magic jQuery methods on it we’ll need to wrap it in another call to jQuery:

jQuery('.vcard').each(function() {
	var hcard = jQuery(this);
});

The Google maps geocoder seems to work best if you pass it the street address and a postcode. We can extract these using CSS selectors: this time, we’ll use jQuery’s find() method which searches within the current jQuery selection:

var streetaddress = hcard.find('.street-address').text();
var postcode = hcard.find('.postal-code').text();

The text() method extracts the text contents of the selected node, minus any HTML markup.

We’ve got the address; now we need to geocode it. Mapstraction’s geocoding API requires us to first construct a MapstractionGeocoder, then use the geocode() method to pass it an address. Here’s the code outline:

var geocoder = new MapstractionGeocoder(onComplete, 'google');
geocoder.geocode({'address': 'the address goes here');

The onComplete function is executed when the geocoding operation has been completed, and will be passed an object with the resulting point on the map. We just want to create a marker for the point:

var geocoder = new MapstractionGeocoder(function(result) {
	var marker = new Marker(result.point);
	mapstraction.addMarker(marker);
}, 'google');   

For our purposes, joining the street address and postcode with a comma to create the address should suffice:

geocoder.geocode({'address': streetaddress + ', ' + postcode});   

There’s one last step: when the marker is clicked, we want to display details of the restaurant. We can do this with an info bubble, which can be configured by passing in a string of HTML. We’ll construct that HTML using jQuery’s html() method on our hcard object, which extracts the HTML contained within that DOM node as a string.

var marker = new Marker(result.point);
marker.setInfoBubble(
	'<div class="bubble">' + hcard.html() + '</div>'
);
mapstraction.addMarker(marker);

We’ve wrapped the bubble in a div with class bubble to make it easier to style. Google Maps can behave strangely if you don’t provide an explicit width for your info bubbles, so we’ll add that to our CSS now:

.bubble {
	width: 300px;
}

That’s everything we need: let’s combine our code together:

jQuery(function() {
	// First create a div to host the map
	var themap = jQuery('<div id="themap"></div>').css({
		'width': '90%',
		'height': '400px'
	}).insertBefore('ul.restaurants');
	// Now initialise the map
	var mapstraction = new Mapstraction('themap','google');
	mapstraction.addControls({
		zoom: 'large',
		map_type: true
	});
	// Show map centred on Brighton
	mapstraction.setCenterAndZoom(
		new LatLonPoint(50.82423734980143, -0.14007568359375),
		15 // Zoom level appropriate for Brighton city centre
	);
	// Geocode each hcard and add a marker
	jQuery('.vcard').each(function() {
		var hcard = jQuery(this);
		var streetaddress = hcard.find('.street-address').text();
		var postcode = hcard.find('.postal-code').text();
		var geocoder = new MapstractionGeocoder(function(result) {
			var marker = new Marker(result.point);
			marker.setInfoBubble(
				'<div class="bubble">' + hcard.html() + '</div>'
			);
			mapstraction.addMarker(marker);
		}, 'google');	 
		geocoder.geocode({'address': streetaddress + ', ' + postcode});
	});
});

Here’s the finished code.

There’s one last shortcut we can add: jQuery provides the $ symbol as an alias for jQuery. We could just go through our code and replace every call to jQuery() with a call to $(), but this would cause incompatibilities if we ever attempted to use our script on a page that also includes the Prototype library. A more robust approach is to start our code with the following:

jQuery(function($) {
	// Within this function, $ now refers to jQuery
	// ...
});

jQuery cleverly passes itself as the first argument to any function registered to the DOM ready event, which means we can assign a local $ variable shortcut without affecting the $ symbol in the global scope. This makes it easy to use jQuery with other libraries.

Limitations of Geocoding

You may have noticed a discrepancy creep in to the last example: whereas my original list included seven restaurants, the geocoding example only shows five. This is because the Google Maps geocoder incorporates a rate limit: more than five lookups in a second and it starts returning error messages instead of regular results.

In addition to this problem, geocoding itself is an inexact science: while UK postcodes generally get you down to the correct street, figuring out the exact point on the street from the provided address usually isn’t too accurate (although Google do a pretty good job).

Finally, there’s the performance overhead. We’re making five geocoding requests to Google for every page served, even though the restaurants themselves aren’t likely to change location any time soon. Surely there’s a better way of doing this?

Microformats to the rescue (again)! The geo microformat suggests simple classes for including latitude and longitude information in a page. We can add specific points for each restaurant using the following markup:

<li class="vcard">
	<h3 class="fn org">E-Kagen</h3>
	<div class="adr">
		<p class="street-address">22-23 Sydney Street</p>
		<p><span class="locality">Brighton</span>, <abbr class="country-name" title="United Kingdom">UK</abbr></p>
		<p class="postal-code">BN1 4EN</p>
	</div>
	<p>Telephone: <span class="tel">+44 (0)1273 687 068</span></p>
	<p class="geo">Lat/Lon: 
		<span class="latitude">50.827917</span>, 
		<span class="longitude">-0.137764</span>
	</p>
</li>

As before, I used www.getlatlon.com to find the exact locations – I find satellite view is particularly useful for locating individual buildings.

Latitudes and longitudes are great for machines but not so useful for human beings. We could hide them entirely with display: none, but I prefer to merely de-emphasise them (someone might want them for their GPS unit):

.vcard .geo {
	margin-top: 0.5em;
	font-size: 0.85em;
	color: #ccc;
}

It’s probably a good idea to hide them completely when they’re displayed inside an info bubble:

.bubble .geo {
	display: none;
}

We can extract the co-ordinates in the same way we extracted the address. Since we’re no longer geocoding anything our code becomes a lot simpler:

$('.vcard').each(function() {
	var hcard = $(this);
	var latitude = hcard.find('.geo .latitude').text();
	var longitude = hcard.find('.geo .longitude').text();
	var marker = new Marker(new LatLonPoint(latitude, longitude));
	marker.setInfoBubble(
		'<div class="bubble">' + hcard.html() + '</div>'
	);
	mapstraction.addMarker(marker);
});

And here’s the finished geo example.

Further reading

We’ve only scratched the surface of what’s possible with microformats, jQuery (or just regular JavaScript) and a bit of imagination. If this example has piqued your interest, the following links should give you some more food for thought.

This article originally appeared on 24ways.