A Maidenhead locator is an encoding of latitude and longitude commonly used in amateur radio, for which I recently passed my Foundation and Intermediate licenses, thanks to the teaching and kind assistance of Martin Atherton (G3ZAY) of the Cambridge University Wireless Society. The encoding itself is trivial, but still tedious to do by hand. So I built Where HAM I?, a simple web app using geolocation, Google Maps, and geocoding, to perform the necessary calculations and determine a user’s locator.
First, some background into coordinate systems. Latitude is the North / South positional coordinate, ranging from +90° at the North pole, and -90° at the South pole, with the centre being the Equator. Longitude is the East / West positional coordinate, ranging from +/- 180° centred at the Greenwich Meridian. The Maidenhead system offsets these to avoid negative numbers, then divides the globe into ‘fields’ of 10° latitude and 20° longitude. It is then just a matter of some simple arithmetic to construct the character pairs - the entire function is included below:
function maidenhead(_latitude, _longitude) { var longitude = _longitude; longitude += 180; longitude /= 2; var latitude = _latitude; latitude += 90; function char_shift(start, offset) { return String.fromCharCode(start.charCodeAt() + offset); } var long1 = char_shift('A', Math.floor(longitude / 10)); var lat1 = char_shift('A', Math.floor(latitude / 10)); var long2 = char_shift('0', Math.floor(longitude % 10)); var lat2 = char_shift('0', Math.floor(latitude % 10)); var long3 = char_shift('a', Math.floor((longitude % 1) * 24)); var lat3 = char_shift('a', Math.floor((latitude % 1) * 24)); return { maidenhead: long1 + lat1 + long2 + lat2 + long3 + lat3, maidenhead_coordinates: {latitude: latitude, longitude: longitude}, coordinates: {latitude: _latitude, longitude: _longitude}, }; }
With the encoder function written, it was time to integrate HTML geolocation. It turns out this is beautifully simple:
if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(successCallback, failureCallback); } else { alert("Your browser does not support Geolocation"); }
In the above, successCallback
is a function taking a Position
object, and errorCallback
a function taking a PositionError
object. That’s it; the browser will then present a permission request to the user, and if all is well, we get a Position object containing latitude and longitude.
Now, the geolocation isn’t perfect. I haven’t looked into implementations, but I assume it’s very much like the iOS CoreLocation API; a nice interface behind which lies an implementation of “use every method we can and return the best data we get”, from GPS to mapping nearby wireless APs to the classic fallback of geoip. During development, my laptop (on the WLAN) located the house, but the desktop (on the wired network) gave a location of the town centre. Making this clear to the user is vital, hence the natural next step seemed to be using Google Maps to display the given location, so at least if it’s not correct, it should be obvious. The last time I looked at the Google Maps API was about four years ago - I remembered little more than “write some JavaScript” - it turned out to be very simple:
var latlng = new google.maps.LatLng(latitude, longitude); var myOptions = { zoom: MAP_ZOOM, center: latlng, mapTypeId: google.maps.MapTypeId.ROADMAP, }; map = new google.maps.Map(document.getElementById("map_canvas"), myOptions); var marker = new google.maps.Marker({ map: map, position: latlng, title: location.maidenhead, });
Now it’s vital to show the user the location data provided in the event of it being inaccurate, but better would be to let the user manually provide more accurate data. Fortunately, Google provide the very nice Places Autocomplete service, which just attaches to a text input tag, and provides suggested locations in a drop-down. It’s also beautifully simple to use (in the code shown, show_location is my function to update the UI):
var geocoder_search_input = document.getElementById("geocoder_search_input"); var options = {types: ["geocode"]}; autocomplete = new google.maps.places.Autocomplete(geocoder_search_input, options); google.maps.event.addListener(autocomplete, 'place_changed', function() { var place = autocomplete.getPlace(); show_location(place.geometry.location.lat(), place.geometry.location.lng()); });
With that, I’ve covered Where HAM I?’s functionality and core components. Of course, nothing goes perfectly - two problems in particular had me somewhat flummoxed. First, I couldn’t seem to get Places Autocomplete working - all I got was the somewhat unhelpful message:
Uncaught TypeError: Cannot read property 'Autocomplete' of undefined"
Apparently this stemmed from the line
autocomplete = new google.maps.places.Autocomplete(geocoder_search_input, options);
Now why would google.maps.places be undefined? Well the Places service is a library that must be explicitly loaded via the libraries
URL parameter when sourcing the Maps API. Spot the bug:
src="//maps.googleapis.com/maps/api/js?key=elided&sensor=false&v=3.7&libraries=places'"
If you noticed that extra ‘ before the “, well done. It took me painfully long.
The second major issue was a much weirder and more concerning bug - occasionally and inconsistently, the Google map would appear with some bizarre internal offset, making it impractical to use. A few searches led me to an answered StackOverflow question by someone who’d experienced the same issue - turns out that initialising the map with its container div hidden makes for general unhappiness. A quick tweak later, and success, a consistently working map.
Hosting-wise, it all lives nicely in three static files, one HTML, one CSS, one JavaScript (plus externally-hosted Twitter Bootstrap and jQuery, the CSS and JS frameworks I find myself using for absolutely everything nowadays) so deployment is a cinch. Remarkably simple and pain-free.
So, I hope this rough sketch of Where HAM I? proves a useful insight for some. As projects go, I feel it turned out rather nicely - a kind bunch over at Reddit provided some very useful feedback, and seemed to generally find it useful, and I hope others do too. All feedback welcome, and the source is available on GitHub for the interested.