A List Apart Survey 2009

The annual A List Apart Survey 2009 is live. If you work on the web, fill it in.

No Comments

Gumtree beta launched

gumtree-rouk.jpg

The culmination of a summer's work, today we set live a beta test to start testing changes we're making at Gumtree. A small proportion of people will be automatically included in the test, with the ability to opt out, or you can opt in to the test with the link below:

http://www.gumtree.com/r/opt_in

No Comments

Passenger / mod_rails and symlinks

Setting up a Rails app (the rather spiffy Retrospectiva) to run using Phusion Passenger today, I came across a fun problem where the app kept throwing 500 errors. The Rails production.log was empty but in Apache's error_log I found this error:

Request exceeded the limit of 10 internal redirects due to probable configuration error. Use 'LimitInternalRecursion' to increase the limit if necessary. Use 'LogLevel debug' to get a backtrace.

It turns out that Passenger doesn't deal well with the DocumentRoot being a symbolic link, which is exactly what I had set up. Luckily that makes the solution nice and easy: just point the DocumentRoot at the Rails app's public folder, something like:

DocumentRoot    /home/user/webappname/public

... and you should be good to go after restarting Apache.

No Comments

Progressively-enhanced Google Maps for your website

At Gumtree we've been doing some cool stuff recently (unfortunately not live to site just yet) and part of it has involved re-implementing the Google Maps that show an item's location.

The problem

With all our new work we're trying to make everything degrade much more gracefully if you don't have Javascript enabled. Normally using the Google Maps API if a user doesn't have Javascript enabled, they just won't see a map. Working with Natalie from Clearleft I think we came up with a pretty good solution that uses progressive enhancement to ensure everyone gets a map, even without JS.

Google Static Maps API

The Google Static Maps API is the little brother of the main Google Maps API, producing simple, static, map images on demand. The best bit is that it doesn't involve Javascript; you just build up a query string in the src attribute of an image tag and it gets loaded like any other image on the page.

The solution

The idea is to use the Google Static Maps API to display a simple flat map image to everyone and then switch it for an interactive one once the DOM is ready if Javascript is available. The example code below uses jQuery which makes handing the events easier, among other things. It's all pretty simple, the main clever bit is in pulling out the data we need to load the interactive map from the static image using a bit of regular expressions magic.

An example

First, we'll create a static Google map image centred on the glorious St. James' Park football ground in Newcastle.

<img src="http://maps.google.com/maps/api/staticmap?center=St+James+Park,Newcastle+upon+Tyne,UK&zoom=14&size=512x512&maptype=roadmap&markers=color:blue|label:S|54.9755361,-1.6216139&sensor=false&key=MAPS_API_KEY" alt="" width="512" height="512" />

The code above tells the Google Static Maps API that we want a map image centred on the address 'St James Park,Newcastle upon Tyne,UK', 512x512px and at zoom level 14, which should look like this:

If you don't already have one, then you'll need a Google Maps API key for the image to work.

Now we want to turn this boring static map into a nice interactive one for everyone who has Javascript enabled. The basic JS code for activating a Google map is as below (you also need to include the general Maps JS, as shown in the docs):

google.load('maps', '2', {'callback':googleMapSetup});

In the above code, we're using Google's load() function to load the Maps API, version 2. The final bit passes the name of a callback function we're going to write which handles setting up the customisation we want for our map (things like adding the zoom controls). The callback function will look something like this:

function googleMapSetup() {
if (GBrowserIsCompatible()) {
var thismap = new GMap2(document.getElementById('map_canvas'));
var point = new GLatLng(thislat, thislon);
thismap.setCenter(point, parseInt(zoomLevel,10));
thismap.addControl(new google.maps.SmallMapControl());
thismap.addOverlay(new google.maps.Marker(thismap.getCenter()));
}
}

You'll notice a few variables being used like thislat, thislon and zoomLevel that aren't defined anywhere yet. As we're doing this with progressive enhancement, we want to pull this data out of the existing static image. All the data we need is in the image tag's src attribute, we just need to get at it.

The way I chose is with a few regular expressions. Regular expressions are one of those things that are hard to learn but really powerful once you have. Using regex here was a bit simpler than splitting the URL parameters and then figuring out what's what and also safeguards a bit more against any change in the order of params. To the code:

var staticMapURL = $('.map img').attr('src');
var latlon = staticMapURL.match(/markers=.+?\|(-?[0-9]+\.[0-9]+,-?[0-9]+\.[0-9]+)/)[1].split(',');
var thislat = latlon[0];
var thislon = latlon[1];
var mapskey = staticMapURL.match(/key=(.+)$/)[1];
var zoomLevel = staticMapURL.match(/zoom=([0-9]+)/)[1];

var mapImg = $('.map img');
var mapHeight = mapImg.attr('height');
var mapWidth = mapImg.attr('width');

First, we're assigning the map image src attribute value to the variable staticMapURL. We then start pulling out the bits of information we need into the relevant variables. The first value we're after is the latlon (latitude and longitude) to place the marker on the map. The coordinates were passed as part of the markers parameter in the static image URL, so we need to match the coordinates and then split them out into latitude and longitude.

Next come the Google Maps API key and zoom level, which should be reasonably self-explanatory. We look for the key= and zoom= parameters in the image path.

Finally we look at the height and width of the image. Here I'm using jQuery to look at the height and width attributes of the <img> tag, though we could also have used regular expressions to pull it out of the query string. It just seemed easier than splitting the 512x512 notation.

Note: The code here is targetting the static map image with the selectore '.map img'. This assumes the map image is within a containing element with a class of 'map'. The reason for this will hopefully become obvious in the next part: creating the interactive map.

Now that we've got all the data we need, we can finally create the dynamic map in place of the static one. First, we need to create the div for the map to go in:

$('.map').prepend('<div id="map_canvas" style="height:'+mapHeight+'px;width:'+mapWidth+'px;"></div>');

Here we use jQuery's prepend method to insert some HTML inside, and at the start of, the containing element with class of 'map' (probably a div).

Then we remove the old static map:

$('.map img').remove();

Now we're finally ready to activate the interactive Google map with the code from earlier on. The Javascipt should now look something like this:

$(document).ready(function(){
google.load('maps', '2', {'callback':googleMapSetup});
});

function googleMapSetup() {
if (GBrowserIsCompatible()) {
var staticMapURL = $('.map img').attr('src');
var latlon = staticMapURL.match(/markers=.+?\|(-?[0-9]+\.[0-9]+,-?[0-9]+\.[0-9]+)/)[1].split(',');
var thislat = latlon[0];
var thislon = latlon[1];
var mapskey = staticMapURL.match(/key=(.+)$/)[1];
var zoomLevel = staticMapURL.match(/zoom=([0-9]+)/)[1];

var mapImg = $('.map img');
var mapHeight = mapImg.attr('height');
var mapWidth = mapImg.attr('width');

$('.map').prepend('<div id="map_canvas" style="height:'+mapHeight+'px;width:'+mapWidth+'px;"></div>');
$('.map img').remove();

var thismap = new GMap2(document.getElementById('map_canvas'));
var point = new GLatLng(thislat, thislon);
thismap.setCenter(point, parseInt(zoomLevel,10));
thismap.addControl(new google.maps.SmallMapControl());
thismap.addOverlay(new google.maps.Marker(thismap.getCenter()));
}
}

Time for a demo:

This second map should start as a static image and quickly switch to an interactive map, but you probably won't catch it on this page (especially if you bothered to read all the stuff before it), so I've created a separate demo page. The marker will change colour as the switch happens which should make it more obvious.

I hope this was useful to someone. Any questions, just post a comment or email me at adam at made-with-chopsticks.com

2 Comments

Jenny Lake, Grand Teton National Park

I spent a couple of days in the Grand Teton National Park on my recent holiday and we got to visit the stunning Jenny Lake.

No Comments

Older posts