Look around you – Fun with Geolocation and Wikipedia

When we were at the Location Business Summit in Amsterdam last week, one of the things that fascinated me when showing off the GeoPlanet Explorer tool was that it found a lot of Points of Interest (museums, churches, sights) around the area of our hotel. I thought it might be fun to have an interface that does this as its main task, so let's have a go at that.

Look around you is a mashup that finds your location and shows Wikipedia entries about the nearby surroundings. You can also navigate further and extend your area by clicking the names of neighbouring areas:

You can download the source code of the tool on Github.

In order to build the tool I used a few helpers:

To make things easier and to "eat my own dogfood" I used the YQLGeo library we talked about earlier.

This not only provided me with the necessary functionality but also gave me a namespace (yqlgeo) to play with to prevent me polluting from the global namespace. Using YUI3 and YUI grids for the layout, I didn't have to worry about browser inconsistencies either.

The HTML is simply a few DIVs (using the YUI grids for display next to another):

<div class="yui-g">
<div class="yui-u first" id="map">
<p class="info">Trying to determine your location...</p>
</div>
<div class="yui-u" id="sights"></div>
</div>
<div id="neighbours"></div>
<div id="children"></div>

For the JavaScript I embedded YUI, the Yahoo Maps API, and yqlgeo at the end of the code:

<script src="http://api.maps.yahoo.com/ajaxymap?v=3.8&appid={maps-id}'">
</script>
<script type="text/javascript" src="https://yui-s.yahooapis.com/3.1.1/build/yui/yui-min.js"></script>
<script src="yqlgeo.js"></script>
<script src="lookaround.js"></script>

This is all we need to make this work. So let's get started, shall we?

YUI().use('node',function(Y){
Y.one('body').addClass('js');
yqlgeo.get('visitor',function(o){
yqlgeo.getinfo(o);
});

We tell YUI to use the node module to ease event handling and DOM manipulation. We add a class called js to the body of the document, which - together with the right CSS - effectively hides the point of interest information. Then we use YQLGeo to get the visitor's location for us.

  yqlgeo.getinfo = function(o){
var cur = o.place ? o.place : o;
yqlgeo.rendermap('*', cur.centroid.latitude,cur.centroid.longitude,
cur.boundingBox.northEast.latitude,
cur.boundingBox.northEast.longitude,
cur.boundingBox.southWest.latitude,
cur.boundingBox.southWest.longitude;
Y.one('#sights').set('innerHTML','Loading landmarks…');

Once we have the location, we call the rendermap() method with the coordinates and boundingBox measurements. To tell the end user what is going on, we show a loading message in the container that will later on show the sights around you.

    var url = 'http://ws.geonames.org/findNearbyWikipediaJSON?formatted=true'+
'&lat=' + cur.centroid.latitude + '&lng='+
cur.centroid.longitude+'&style=full&callback=yqlgeo.wiki';
yqlgeo.get(url);

We assemble the REST URL pointing to the geonames API and give it the right latitude and longitude. As the callback method for the JSON-P call, we define yqlgeo.wiki() and call the get method to inject a script node into the document.

   Y.one('#neighbours').set('innerHTML','Loading neighbouring areas...');
url = 'http://query.yahooapis.com/v1/public/yql?q=select%20*%20from'+
'%20geo.places.neighbors%20where%20neighbor_woeid%3D'+
cur.woeid+'&diagnostics=false&format=json&'+
'callback=yqlgeo.neighbours';
yqlgeo.get(url);
};

We get the container for the neighbouring areas and give it a loading message (again so as to not keep our users in the dark as to what is happening). We call YQL with the correct query, give it the Where On Earth ID (WOEID) and define yqlgeo.neighbours() as the callback method before calling get().

  yqlgeo.get = function(url){
Y.one('head').append('<script src="'+url+'"></script>');
};

The get() method simply adds a new script node to the document with a given url as the src attribute. In effect, this means the REST APIs get called and return JSON to the defined callback methods.

  yqlgeo.rendermap = function(){
var x = arguments;
if(x[1]){
yqlgeo.map = new YMap(Y.one('#map')._node);
yqlgeo.map.addTypeControl();
yqlgeo.map.addZoomLong();
yqlgeo.map.addPanControl();
yqlgeo.map.disableKeyControls();
yqlgeo.map.setMapType(YAHOO_MAP_REG);
var points = [];
var point = new YGeoPoint(x[1],x[2]);
points.push(point);
var img = new YImage();
img.src = '16x16.png';
img.size = new YSize(16,16);
var newMarker = new YMarker(point,img);
yqlgeo.map.addOverlay(newMarker);
}
if(x[3] && x[4]){
point = new YGeoPoint(x[3],x[4]);
points.push(point);
}
if(x[5] && x[6]){
point = new YGeoPoint(x[5],x[6]);
points.push(point);
}
var zac = yqlgeo.map.getBestZoomAndCenter(points);
var level = points.length > 1 ? zac.zoomLevel : zac.zoomLevel + 1;
yqlgeo.map.drawZoomAndCenter(zac.YGeoPoint,level);
yqlgeo.map.drawZoomAndCenter(points[0],level);
};

The rendermap() method is a reusable way of showing a Yahoo map with a bunch of locations at the right zoom level. This version has been modified a bit, as I wanted a user icon at the centre of the map rather than another marker.

  yqlgeo.wiki = function(o){
if(o.geonames){
var out = '<ol>';
for(var i=0;i<o.geonames.length;i++){
var sight = o.geonames[i];
out += '<li><h2>'+
'<a href="http://' + sight.wikipediaUrl + '">'+
sight.title+'</a></h2><p>';
if(sight.thumbnailImg){
out += '<img src="https://media.zenfs.com/en-US/blogs/ept_beta1/christian_heilman.jpeg"  src="'+sight.thumbnailImg+'" alt="">';
}
out += sight.summary + '</p>'+
'<p class="distance">'+sight.distance+' miles away</p>'+
'<p class="url"><a href="http://' + sight.wikipediaUrl + '">'+
'http://' + sight.wikipediaUrl + '</a></p>'+
'</li>';
var point = new YGeoPoint(sight.lat,sight.lng);
var marker = new YMarker(point);
marker.addLabel(i+1);
marker.addAutoExpand(sight.title);
yqlgeo.map.addOverlay(marker);
}
out += '</ol>';
}
Y.one('#sights').set('innerHTML',out);
};

In the wiki() method we check if geonames returned us some points of interest and if it did we loop over them assembling the HTML string for displaying them as we go along. We also put new markers on the map for each sight that was found.

  yqlgeo.neighbours = function(o){
if(!o.error && o.query.results && o.query.results.place){
var out = '<ul><li>Around this area:<ul>';
yqlgeo.neighbourdata = o.query.results.place;
var all = o.query.results.place.length;
for(var i=0;i<all;i++){
var cur = o.query.results.place[i];
out+='<li><a href="#n'+i+'">'+cur.name+'</a></li>';
}
out += '</ul></li></ul>';
Y.one('#neighbours').set('innerHTML',out);
} else {
Y.one('#neighbours').remove();
}
};

The neighbours() method does the same, except we don't plot on the map and instead we render out a list of locations with links with numbered anchors. We also cache the neighbourhood data in its own object for later use. If there are no neighbourhoods found, we remove their container from the document.

  Y.delegate('click', function(e) {
e.preventDefault();
var current = Y.one(e.target).get('href').replace(/.*#n/,'');
if(yqlgeo.neighbourdata[current]){
yqlgeo.getinfo(yqlgeo.neighbourdata[current]);
}
}, '#neighbours', 'a');

We use the event delegation functionality of YUI to add a click event to each of the links in the neighbourhood list. We then get the number of the link from the href attribute and call getinfo() with the data of the new neighbourhood.

  Y.delegate('click', function(e) {
e.preventDefault();
var dad = Y.one(e.target).ancestor('li');
if(dad.hasClass('show')){
dad.removeClass('show');
} else {
dad.addClass('show');
}
}, '#sights', 'h2 a');
});

For the sights list, we apply click handlers to each of the links inside the headings (not the Wikipedia ones in the description) and add or remove a class called show to or from the LI that contains the link that was clicked. This, together with the right CSS definition shows and hides the descriptions of the different places.

And that's all there is to it! By using YUI for the functionality and YQL to get the data it is pretty easy to put together a neat little app like this.

Christian Heilmann
Christian Heilmann (@codepo8)
Yahoo! Senior Developer Evangelist