{"database": "24ways", "table": "articles", "is_view": false, "human_description_en": "where author_slug = \"simonwillison\" and year = 2007", "rows": [[168, "Unobtrusively Mapping Microformats with jQuery", "Microformats are everywhere. You can\u2019t 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?\n\nNevertheless, while it\u2019s good to know that users of tools such as Tails and Operator will derive added value from your shiny semantics, it\u2019s nice to be able to reuse that effort in your own code.\n\nWe\u2019re going to build a map of some of my favourite restaurants in Brighton. Fitting with the principles of unobtrusive JavaScript, we\u2019ll 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.\n\nWe\u2019ll 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.\n\nThe second is Mapstraction, introduced here by Andrew Turner a few days ago. We\u2019ll be using Google Maps in the background, but Mapstraction makes it easy to change to a different provider if we want to later.\n\nGetting Started\n\nWe\u2019ll 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:\n\n<li class=\"vcard\">\n\t<h3><a class=\"fn org url\" href=\"http://www.riddleandfinns.co.uk/\">Riddle & Finns</a></h3>\n\t<div class=\"adr\">\n\t\t<p class=\"street-address\">12b Meeting House Lane</p>\n\t\t<p><span class=\"locality\">Brighton</span>, <abbr class=\"country-name\" title=\"United Kingdom\">UK</abbr></p>\n\t\t<p class=\"postal-code\">BN1 1HB</p>\n\t</div>\n\t<p>Telephone: <span class=\"tel\">+44 (0)1273 323 008</span></p>\n\t<p>E-mail: <a href=\"mailto:info@riddleandfinns.co.uk\" class=\"email\">info@riddleandfinns.co.uk</a></p>\n</li>\n\nSince we\u2019re 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\u2019s 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).\n\nThe 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.\n\nA nice thing about microformats is that they provide us with automatic hooks for our styling. For the moment we\u2019ll just tidy up the whitespace a bit; for more advanced style tips consult John Allsop\u2019s guide from 24 ways 2006.\n\n.vcard p {\n\tmargin: 0;\n}\n.adr {\n\tmargin-bottom: 0.5em;\n}\n\nTo plot the restaurants on a map we\u2019ll 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:\n\n<script type=\"text/javascript\" src=\"http://mapstraction.com/src/mapstraction-geocode.js\"></script>\n\nWhile we\u2019re at it, let\u2019s pull in the other external scripts we\u2019ll be using:\n\n<script type=\"text/javascript\" src=\"jquery-1.2.1.js\"></script>\n<script src=\"http://maps.google.com/maps?file=api&v=2&key=YOUR_KEY\" type=\"text/javascript\"></script>\n<script type=\"text/javascript\" src=\"http://mapstraction.com/src/mapstraction.js\"></script>\n<script type=\"text/javascript\" src=\"http://mapstraction.com/src/mapstraction-geocode.js\"></script>\n\nThat\u2019s everything set up: let\u2019s write some JavaScript!\n\nIn 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\u2019s 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:\n\njQuery(function() {\n\t// This code will be executed when the DOM is ready\n});\n\nInitialising the map\n\nThe 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\u2019t currently include this markup, but we can insert it with a single line of jQuery code:\n\njQuery(function() {\n\t// First create a div to host the map\n\tvar themap = jQuery('<div id=\"themap\"></div>').css({\n\t\t'width': '90%',\n\t\t'height': '400px'\n\t}).insertBefore('ul.restaurants');\n});\n\nWhile this is technically just a single line of JavaScript (with line-breaks added for readability) it\u2019s actually doing quite a lot of work. Let\u2019s break it down in to steps:\n\nvar themap = jQuery('<div id=\"themap\"></div>')\n\nHere\u2019s jQuery\u2019s 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:\n\nvar themap = document.createElement('div');\nthemap.id = 'themap';\n\nNext 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:\n\nvar themap = jQuery('<div id=\"themap\"></div>').css({\n\t'width': '90%',\n\t'height': '400px'\n})\n\nFinally, 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\u2019s 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.\n\nvar themap = jQuery('<div id=\"themap\"></div>').css({\n\t'width': '90%',\n\t'height': '400px'\n}).insertBefore('ul.restaurants');\n\nFinally, 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 ):\n\n// Initialise the map\nvar mapstraction = new Mapstraction('themap','google');\n\nWe want the map to appear centred on Brighton, so we\u2019ll 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.\n\n// Show map centred on Brighton\nmapstraction.setCenterAndZoom(\n\tnew LatLonPoint(50.82423734980143, -0.14007568359375),\n\t15 // Zoom level appropriate for Brighton city centre\n);\n\nWe also want controls on the map to allow the user to zoom in and out and toggle between map and satellite view.\n\nmapstraction.addControls({\n\tzoom: 'large',\n\tmap_type: true\n});\n\nAdding the markers\n\nIt\u2019s finally time to parse some microformats. Since we\u2019re using hCard, the information we want is wrapped in elements with the class vcard. We can use jQuery\u2019s CSS selector support to find them:\n\nvar vcards = jQuery('.vcard');\n\nNow that we\u2019ve 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\u2019s each() method to execute a function against each of the hCards.\n\njQuery('.vcard').each(function() {\n\t// Do something with the hCard\n});\n\nWithin 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\u2019ll need to wrap it in another call to jQuery:\n\njQuery('.vcard').each(function() {\n\tvar hcard = jQuery(this);\n});\n\nThe 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\u2019ll use jQuery\u2019s find() method which searches within the current jQuery selection:\n\nvar streetaddress = hcard.find('.street-address').text();\nvar postcode = hcard.find('.postal-code').text();\n\nThe text() method extracts the text contents of the selected node, minus any HTML markup.\n\nWe\u2019ve got the address; now we need to geocode it. Mapstraction\u2019s geocoding API requires us to first construct a MapstractionGeocoder, then use the geocode() method to pass it an address. Here\u2019s the code outline:\n\nvar geocoder = new MapstractionGeocoder(onComplete, 'google');\ngeocoder.geocode({'address': 'the address goes here');\n\nThe 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:\n\nvar geocoder = new MapstractionGeocoder(function(result) {\n\tvar marker = new Marker(result.point);\n\tmapstraction.addMarker(marker);\n}, 'google');   \n\nFor our purposes, joining the street address and postcode with a comma to create the address should suffice:\n\ngeocoder.geocode({'address': streetaddress + ', ' + postcode});   \n\nThere\u2019s 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\u2019ll construct that HTML using jQuery\u2019s html() method on our hcard object, which extracts the HTML contained within that DOM node as a string.\n\nvar marker = new Marker(result.point);\nmarker.setInfoBubble(\n\t'<div class=\"bubble\">' + hcard.html() + '</div>'\n);\nmapstraction.addMarker(marker);\n\nWe\u2019ve wrapped the bubble in a div with class bubble to make it easier to style. Google Maps can behave strangely if you don\u2019t provide an explicit width for your info bubbles, so we\u2019ll add that to our CSS now:\n\n.bubble {\n\twidth: 300px;\n}\n\nThat\u2019s everything we need: let\u2019s combine our code together:\n\njQuery(function() {\n\t// First create a div to host the map\n\tvar themap = jQuery('<div id=\"themap\"></div>').css({\n\t\t'width': '90%',\n\t\t'height': '400px'\n\t}).insertBefore('ul.restaurants');\n\t// Now initialise the map\n\tvar mapstraction = new Mapstraction('themap','google');\n\tmapstraction.addControls({\n\t\tzoom: 'large',\n\t\tmap_type: true\n\t});\n\t// Show map centred on Brighton\n\tmapstraction.setCenterAndZoom(\n\t\tnew LatLonPoint(50.82423734980143, -0.14007568359375),\n\t\t15 // Zoom level appropriate for Brighton city centre\n\t);\n\t// Geocode each hcard and add a marker\n\tjQuery('.vcard').each(function() {\n\t\tvar hcard = jQuery(this);\n\t\tvar streetaddress = hcard.find('.street-address').text();\n\t\tvar postcode = hcard.find('.postal-code').text();\n\t\tvar geocoder = new MapstractionGeocoder(function(result) {\n\t\t\tvar marker = new Marker(result.point);\n\t\t\tmarker.setInfoBubble(\n\t\t\t\t'<div class=\"bubble\">' + hcard.html() + '</div>'\n\t\t\t);\n\t\t\tmapstraction.addMarker(marker);\n\t\t}, 'google');\t \n\t\tgeocoder.geocode({'address': streetaddress + ', ' + postcode});\n\t});\n});\n\nHere\u2019s the finished code.\n\nThere\u2019s 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:\n\njQuery(function($) {\n\t// Within this function, $ now refers to jQuery\n\t// ...\n});\n\njQuery 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.\n\nLimitations of Geocoding\n\nYou 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.\n\nIn 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\u2019t too accurate (although Google do a pretty good job).\n\nFinally, there\u2019s the performance overhead. We\u2019re making five geocoding requests to Google for every page served, even though the restaurants themselves aren\u2019t likely to change location any time soon. Surely there\u2019s a better way of doing this?\n\nMicroformats 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:\n\n<li class=\"vcard\">\n\t<h3 class=\"fn org\">E-Kagen</h3>\n\t<div class=\"adr\">\n\t\t<p class=\"street-address\">22-23 Sydney Street</p>\n\t\t<p><span class=\"locality\">Brighton</span>, <abbr class=\"country-name\" title=\"United Kingdom\">UK</abbr></p>\n\t\t<p class=\"postal-code\">BN1 4EN</p>\n\t</div>\n\t<p>Telephone: <span class=\"tel\">+44 (0)1273 687 068</span></p>\n\t<p class=\"geo\">Lat/Lon: \n\t\t<span class=\"latitude\">50.827917</span>, \n\t\t<span class=\"longitude\">-0.137764</span>\n\t</p>\n</li>\n\nAs before, I used www.getlatlon.com to find the exact locations \u2013 I find satellite view is particularly useful for locating individual buildings.\n\nLatitudes 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):\n\n.vcard .geo {\n\tmargin-top: 0.5em;\n\tfont-size: 0.85em;\n\tcolor: #ccc;\n}\n\nIt\u2019s probably a good idea to hide them completely when they\u2019re displayed inside an info bubble:\n\n.bubble .geo {\n\tdisplay: none;\n}\n\nWe can extract the co-ordinates in the same way we extracted the address. Since we\u2019re no longer geocoding anything our code becomes a lot simpler:\n\n$('.vcard').each(function() {\n\tvar hcard = $(this);\n\tvar latitude = hcard.find('.geo .latitude').text();\n\tvar longitude = hcard.find('.geo .longitude').text();\n\tvar marker = new Marker(new LatLonPoint(latitude, longitude));\n\tmarker.setInfoBubble(\n\t\t'<div class=\"bubble\">' + hcard.html() + '</div>'\n\t);\n\tmapstraction.addMarker(marker);\n});\n\nAnd here\u2019s the finished geo example.\n\nFurther reading\n\nWe\u2019ve only scratched the surface of what\u2019s 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.\n\n\n\tThe hCard specification\n\tNotes on parsing hCards\n\tjQuery for JavaScript programmers \u2013 my extended tutorial on jQuery.\n\tDann Webb\u2019s Sumo \u2013 a full JavaScript library for parsing microformats, based around some clever metaprogramming techniques.\n\tJeremy Keith\u2019s Adactio Austin \u2013 the first place I saw using microformats to unobtrusively plot locations on a map. Makes clever use of hEvent as well.", "2007", "Simon Willison", "simonwillison", "2007-12-12T00:00:00+00:00", "https://24ways.org/2007/unobtrusively-mapping-microformats-with-jquery/", "code"]], "truncated": false, "table_rows_count": 336, "filtered_table_rows_count": 1, "expanded_columns": [], "expandable_columns": [], "columns": ["rowid", "title", "contents", "year", "author", "author_slug", "published", "url", "topic"], "primary_keys": [], "units": {}, "query": {"sql": "select rowid, * from articles where \"author_slug\" = :p0 and \"year\" = :p1 order by rowid limit 101", "params": {"p0": "simonwillison", "p1": "2007"}}, "facet_results": {"topic": {"name": "topic", "results": [{"value": "code", "label": "code", "count": 1, "toggle_url": "http://search-24ways.herokuapp.com/24ways-f8f455f/articles.json?year=2007&_facet=topic&_facet=author_slug&author_slug=simonwillison&topic=code", "selected": false}], "truncated": false}, "author_slug": {"name": "author_slug", "results": [{"value": "simonwillison", "label": "simonwillison", "count": 1, "toggle_url": "http://search-24ways.herokuapp.com/24ways-f8f455f/articles.json?year=2007&_facet=topic&_facet=author_slug", "selected": true}], "truncated": false}}, "suggested_facets": [], "next": null, "next_url": null, "query_ms": 12.878894805908203}