Developer

Microformats and Mapping

We begin by looking at what a microformat is and how they are useful, then progress to introducing the Google Maps API and finally putting it all together to produce the user group map

A mashup of our user group listings and Google Maps is one of those things that has been on the BuilderAU wishlist for quite some time. So it was quite timely that I attended John Allsopp's presentation on microformats at the Web Directions conference last week. We'll begin by looking at what a microformat is and how they are useful, then progress to introducing the Google Maps API and finally putting it all together to produce the user group map that you can see on our user groups page.

Microformats

A microformat is a way of marking up HTML to give it a semantic structure using class or rel attributes or even particular elements. The benefit of this is that it allows for easier automated parsing of content and sharing between unrelated sites. One can always create one's own microformat but the benefit lies in the using the predefined microformat standards, if available.

One of the standard microformats is hCard - the HTML equivalent of vCard. Below is the first example from the vCard RFC 2426:

BEGIN:VCARD
FN:Joe Friday
TEL:+1-919-555-7878
TITLE:Area Administrator, Assistant 
EMAIL;TYPE=INTERNET:
jfriday@host.com
END:VCARD

This is the hCard equivalent:

<div class="agent vcard">
 <a class="email fn" href="mailto:jfriday@host.com">Joe Friday</a>
 <div class="tel">+1-919-555-7878</div>
 <div class="title">Area Administrator, Assistant</div>
</div>

Most of the names of the fields should be obvious, the only one that needs explanation fn, means full name. One thing to notice is that the names of fields have not changed, they have simply been moved into classes. Therefore if you can can understand vCard then hCard should need minimal fuss to grasp. Where ever possible, microformats should utilise existing standards. Doing so lowers the learning curve and helps in information reuse and exchange.

For a much fuller explanation of microformats and their usage, check out microformats.org

Using with User Groups

The old user group page was very simple, it consisted purely of two definition lists with each entry linking to a group's web page. To be able to map them, we needed to have the address for each user group and a microformat to display them with. Using hCard was the perfect fit with each entry taking the following format:

<li class="vcard"><a href="[url]" class="url fn">[Group Name]</a> 
<span class="org">[Group Organsation - if any]</span>
<address class="adr">
<span class="street-address">[Street Address]</span><br />
<span class="locality">[Suburb]</span> <acronym class="region" title="[State Full Name]">[State Abbreviation]</acronym> <span class="postal-code">[postcode]</span> <span class="country-name">[country]</span>
<address>
</li>

Now we needed the user group's addresses; hours and many interestingly designed pages later, we had all the address entered into the page.
The addresses were not entered into a database at all, as it currently stands the user groups page is a completely static page, so the address were entered straight into that static page.

Enter Google Maps

Now that we have a static page with addresses in it, the next problem is retrieving this data and passing it to the Google Maps API - we would have to build a scraper. Fortunately PHP 5 makes this task very quick and easy to develop for by using the DOMDocument class.
Our listing file is loaded using the loadHTMLFile method then the real fun starts. Because the only elements that we interested in are within list items, as seen in the above format we use, we fetch only the li elements. The information that we are displaying on the map will be the user group name and address, therefore if any of the list item's children have a class value of "url fn", we know we have the name in that element's node value. After that we need to fetch the address, this is easily recognised by the address element, so we only have to check against a nodeName of address to know that we have it.
Once inside the address element, we loop through it storing the various values for the address in an array which we use to send a request to the Google Maps API asking it to return a latitude and longitude for the address in csv format.

The csv response takes the following structure:

[status code], [accuracy], [latitude], [longitude]

Hence we can fetch all this data easily by exploding the csv with a comma.

If we have an OK (200) response code, the address is tidied up for human readability and stored inside a results array with the user group name prefixed to it. The results array is then serialised and encoded for later use within a global.

As might be expected, sending a query to google for each address on a page is rather time intensive, so we separate the file that we fetches the results, in this case the scaper, from the page actually shows the results on a Google Map. The scraper's results are cached using our content management system's caching functions to reduce the overhead and speed up displaying of the map, but you could store this into a file and achieve the same results.

Here is our scraper code:

$doc = new DOMDocument();
$doc->loadHTMLFile("userlist.htm");

$linodes = $doc->getElementsByTagName("li");

$resultsarr = array();

for ($i = 0; $i length; $i++) {
   $details = $linodes->item($i)->childNodes;
	foreach($details as $det){
		if($det->nodeName == "address"){ //need to fetch children
			$addarr = array();
			foreach($det->childNodes as $addrspan){
				foreach($addrspan->attributes as $attrs){
					$addarr[$attrs->value] = $addrspan->nodeValue;
				}

			}
			$exp = explode(",",file_get_contents("http://maps.google.com/maps/geo?output=csv&key=ABCD1234&q=".urlencode($addarr["street-address"].",".$addarr["locality"].",".$addarr["region"].",".$addarr["postal-code"].",".$addarr["country-name"])));

			if($exp[0]=="200"){
				$addy = (strlen($addarr["street-address"])? $addarr["street-address"].", ":"").(strlen($addarr["locality"])?$addarr["locality"].", ":"").(strlen($addarr["region"])?$addarr["region"].", ":"").(strlen($addarr["postal-code"])?$addarr["postal-code"].", ":"").(strlen($addarr["country-name"])?$addarr["country-name"]:"");

			$resultsarr[$exp[2]][$exp[3]] = "".$fn."
".(count($resultsarr[$exp[2]][$exp[3]]) ? "":$addy).$resultsarr[$exp[2]][$exp[3]]; } } foreach($det->attributes as $attr){ if($attr->value == "url fn"){$fn = $det->nodeValue;} } } } $GLOBALS['usergroup_array'] = base64_encode(serialize($resultsarr));

Now we get to the bit where we start to see the results of our hard work - bringing up a map and adding our own markers to it. Google has a great API and examples document that takes you through the process of going from nothing to a functional and good looking mashup, so rather than repeat it, we shall go through what is needed only for our task.

To display a map you must include the Google Maps API script, a div with an id of "map" and then code up the load function to direct the api to show what you wish.
We tried to keep the user interface (UI) as similar to the standard Google Maps UI as we can, this means we add the large map control (the zoom and arrows on the left hand side), the map type control (lets you choose between map, satellite and hybrid views), and the overview map control (that is the funky overview map in the lower right corner). Then we need to tell the map what we want to look at, through trial and error we found a latitude and longitude with a zoom level that showed Australia nicely in the centre of the map.

At this point we need to fetch our cached results and create a javascript array with them, this reason for this array rather than simply adding markers based solely on those results will become apparent soon. Using the javascript array we add our markers to the map using the createMarker function we defined. This function takes three parameters, the point to anchor the marker on, what user groups are there and a parameter called when which we will hopefully use in the future; as it stands currently it puts the text on the marker and anchors it at the point we pass to it.

By now we have a functional map that has the markers we want on it, pretty nice but navigating the map itself is still a real pain, so we need to improve the interface some more. This is where the list of places down the right hand side of the map comes into play. The panhandle function sets the zoom level and pans the map to the point we pass it to. By searching Google Maps we were able to find out the latitude and longitude of the majority of places that have user groups on our map; luckily at this point there was only a handful or we may have to include this places into scraper, definitely something for improvement later.

The final method we define on this page is the findme method, this method is used by little icons next to the user group name in the listings. It searches the javascript array we created earlier and looks for the name of the group, once the group is found, the map is panned and centred on the group's location.

Here is the full listing of our map file:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
    <title>Google Maps JavaScript API Example</title>
    <style type="text/css">
    v\:* {
      behavior:url(#default#VML);
    }
    </style>
    <script src="http://maps.google.com/maps?file=api&v=2&key=ABCD1234"
      type="text/javascript"></script>
    <script type="text/javascript">

    //<![CDATA[

    var map;
    var defcentre = new GLatLng(-30.25,148);
    var pointarr = new Array();

    function load() {
      if (GBrowserIsCompatible()) {
        map = new GMap2(document.getElementById("map"));
        map.addControl(new GLargeMapControl());
        map.addControl(new GMapTypeControl());
        map.addControl(new GOverviewMapControl());
        map.setCenter(defcentre, 3);


        function createMarker(point, who, when) {
            // Our info window content
            var infoTabs = [
                new GInfoWindowTab("Who", who),
                //new GInfoWindowTab("When", when)
            ];

            var marker = new GMarker(point);
            GEvent.addListener(marker, "click", function() {
                marker.openInfoWindowTabsHtml(infoTabs);
            });
            return marker;
        }

    <?
        include("resultsfile.htm");
        foreach(unserialize(base64_decode($GLOBALS['usergroup_array'])) as $latkey => $latarr){
	           foreach($latarr as $longkey => $text){
    ?>
                    pointarr.push(new Array([?= $latkey?], [?= $longkey?], "[?= $text?]"));
    <?
	           }
        }
    ?>

        for(i=0;i<pointarr.length;i++){
	       map.addOverlay(createMarker(new GLatLng(pointarr[i][0], pointarr[i][1]), pointarr[i][2], "Time unknown"));
        }	
      }
    }

	function panhandle(lat, longi, zoom){
		map.setZoom(zoom);
		window.setTimeout(function() {map.panTo(new GLatLng(lat, longi));}, 1000);
	}

    function findme(name){
	   for(i=0;i<pointarr.length;i++){
		  if(pointarr[i][2].indexOf(name)> -1){panhandle(pointarr[i][0], pointarr[i][1], 13);break;}
	   }	
    }
    //]]>
    </script>

  </head>
  <body onload="load()" onunload="GUnload()" style="background:white;margin:0;padding:0;font:11px black arial,verdana,sans-serif;">
    <div id="map" style="width: 450px; height: 300px;float:left"></div>
    <dl class="first" style="float:left">
    <dt><a href="javascript:panhandle(-33.869988,151.209984, 10)">Sydney</a></dt>
    <dt><a href="javascript:panhandle(-37.810055,144.959965, 11)">Melbourne</a></dt>
    <dt><a href="javascript:panhandle(-35.310001,149.130004, 11)">Canberra</a></dt>
    <dt><a href="javascript:panhandle(-27.459999,153.020004, 11)">Brisbane</a></dt>
    <dt><a href="javascript:panhandle(-34.93,138.600006, 11)">Adelaide</a></dt>
    <dt><a href="javascript:panhandle(-31.959999,115.83999, 11)">Perth</a></dt>
    <dt><a href="javascript:panhandle(-42.849998,147.289993, 11)">Hobart</a></dt>
    <dt><a href="javascript:panhandle(-32.919998,151.75, 11)">Newcastle</a></dt>
    <dt><a href="javascript:panhandle(-34.419998,150.869995, 11)">Wollongong</a></dt>
    <dt><a href="javascript:panhandle(-26.517892,153.096542, 9)">Sunshine Coast</a></dt>
    <dt><a href="javascript:panhandle(-23.7,133.869995, 11)">Alice Springs</a></dt>
    </dl>
  </body>
</html>

Final Touches and Future Improvements

Currently our user group list shows the full address underneath the group, while there is nothing wrong with that in principal it does make the page exceedingly long and that information is semi-redundant as it is also shown in hte map itself. Therefore we put a display:none on the address element and org class to keep the list looking as it does now, with only names.

The map file above is included on our user group page using an iframe therefore we need to use some javascript funkery to get inside the iframe to call the findme function:

javascript:top.document.getElementById('mapframe').contentWindow.findme('SydneyDeep .Net User Group')

An added benefit of all the work we have done is that we can use the Tails extension for Firefox to see the hCards that we have created, which can be yet another way to browse the user group listing at zero extra work.

As for what is planned in the future, the createMarker leaves a very good clue. If user group sites would use hCal meeting microformats we could integrate them into the map and show the location and time and date of the next meeting.

I don't know about you, but I think that would be extremely cool and useful.

About Chris Duckett

Some would say that it is a long way from software engineering to journalism, others would correctly argue that it is a mere 10 metres according to the floor plan.During his first five years with CBS Interactive, Chris started his journalistic advent...

Editor's Picks

Free Newsletters, In your Inbox