On the tails of the exciting release of YUI 3.1, I wanted to build a little mini web-app to show some of the powerful new features. I built Photos Around You, an app which determines your Geolocation and finds photos geo-tagged around this location. Building this app took ~200 lines of YUI 3.1 JavaScript that I had to write; and this is how its put together:
YUI 3.1 Modules Used
- node: The Node Utility provides an expressive way to collect, create, and manipulate DOM nodes.
- overlay: Overlay is a positionable and stackable widget, which also provides support for the standard module format layout, with a header, body and footer section.
- substitute: Does variable substitution on a string. It scans through the string looking for expressions enclosed in { } braces. If an expression is found, it is used a key on the object.
- gallery-jsonp: Provides a JSONPRequest class for repeated JSONP calls, and a convenience method Y.jsonp(url, callback) to instantiate and send a JSONP request.
- gallery-yql: This module adds a little sugar to YUI3 to make simple easy YQL queries.
- gallery-markout: Markout is an API for creating DOM nodes in JavaScript. Much easier to use than the built-in DOM API, and allows for easy delegation for code modularity.
YUI 3.1’s loading and dependency management features make this extremely easy even for YUI 3 Gallery Modules which are contributed from the YUI community.
<script src="http://yui.yahooapis.com/3.1.0/build/yui/yui-min.js"></script>
<script>
YUI().use('node', 'overlay', 'substitute', 'gallery-jsonp', 'gallery-yql', 'gallery-markout', function(Y){
// Everything is ready!
});
</script>
Determining the User’s Location
Christian Heilmann (codepo8) published [on Github] an ingenious way to determine the user’s location using YQL and the W3C Geolocation API with IP-based location identification as a fallback. Basing my code on Heilmann’s work and using YUI 3.1 I wrote a set of functions to get the user’s Geolocation.
Getting The User’s IP
Using the JSONP YUI 3 Gallery Module written by Luke Smith (YUI Core Team) and the JSONIP Google App Engine app getting the user’s IP is simple:
getIP = function (callback) {
Y.jsonp('http://jsonip.appspot.com/', function(data){
callback(data.ip);
});
};
Getting Location Data From An IP
We have the user’s IP, but we need info about the physical location of that IP address; we can get this info using YQL tables: ip.location, flickr.places, and geo.places.
locFromIP = function (ip, callback) {
var query = 'select * from geo.places where woeid in ' +
'(select place.woeid from flickr.places where (lat, lon) in ' +
'(select Latitude, Longitude from ip.location where ip="{ip}"));',
yql;
yql = new Y.yql(Y.substitute(query, { ip: ip }), function(r){
callback(r.query && r.query.results ? r.query.results.place : null);
});
};
Getting Location Data From Geolocation Position
Almost identical to the YQL query to get the location data from an IP address, but where we already have the latitude/longitude coordinates from the W3C Geolocation API.
locFromPos = function (pos, callback) {
var position = { lat: pos.coords.latitude, lon: pos.coords.longitude },
query = 'select * from geo.places where woeid in ' +
'(select place.woeid from flickr.places where lat={lat} and lon={lon});',
yql;
yql = new Y.yql(Y.substitute(query, position), function(r){
callback(r.query && r.query.results ? r.query.results.place : null);
});
};
Feature Detect W3C Geolocation API, Fallback to IP
A simple check for the presence of navigator.geolocation allows the use of the Geolocation API to fetch the user’s current position if granted (the user is prompted by the browser to authorizing giving the application the location.); otherwise we fallback to using the IP-based location.
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
Y.rbind(locFromPos, this, callback),
Y.bind(getIP, this, Y.rbind(locFromIP, this, callback))
);
} else {
getIP(Y.rbind(locFromIP, this, callback));
}
Finding Geo-tagged Photos
Flickr, with its extensive API, intrinsic YQL support, and mass of awesome photos seemed the logical place to fetch the photos for this app. Equipped with a WOEID (Where On Earth IDentifier) corresponding to the user’s location, we can initiate a Flickr Photos Search API request using the flickr.photos.search YQL table.
getPhotos = (function(){
var query = 'select * from flickr.photos.search({start},{num}) ' +
'where woe_id="{woeid}" and radius_units="mi" and sort="interestingness-desc" and extras="path_alias";';
return (function (loc, start, num, callback) {
var yql = new Y.yql(Y.substitute(query, { woeid: loc.woeid, start: start, num: num }), function(r){
callback(r.query && r.query.results? r.query.results.photo : null);
});
});
}());
Photo Utility Functions
I wrote a few utility functions to encapsulate the common tasks this app is doing with photo data.
getPhotoURL(photo, size)
Using the Flickr URL patterns we can generate a URL to an image file on Flickr of a particular size.
getPhotoURL = (function(){
var template = 'http://farm{farm}.static.flickr.com/{server}/{id}_{secret}{size}.jpg';
return (function (photo, size) {
return Y.substitute(template, Y.merge(photo, { size: size ? '_'+size : '' }));
});
}());
getPhotoPageURL(photo)
I wanted to create links back to the Flickr Photo Pages when viewing the medium-sized photo in the overlay; this function generates that URL.
getPhotoPageURL = (function(){
var template = 'http://www.flickr.com/photos/{user}/{id}';
return (function (photo) {
return Y.substitute(template, { id: photo.id, user: photo.pathalias || photo.owner });
});
}());
loadImg(src, callback)
A utility function derived from Luke Smith’s work to determine when an image has fully loaded. Using this approach and waiting until all the image’s bytes have been loaded became crucial to aide the sizing and positioning of the Photo Overlay.
loadImg = function (src, callback) {
// insired by: Lucas Smith (http://lucassmith.name/2008/11/is-my-image-loaded.html)
var img = new Image(),
prop = img.naturalWidth ? 'naturalWidth' : 'width';
img.src = src;
if (img.complete) {
callback(img[prop] ? img : null);
} else {
img.onload = Y.bind(callback, this, img);
img.onerror = Y.bind(callback, this, null);
}
};
renderThumbnail(photo)
A simple function using MarkoutJS to render a photo thumbnail into a list-item node.
renderThumbnail = function (photo) {
var li = Y.Markout().li({ 'class': 'photo' });
li.img({ src: getPhotoURL(photo, 's'), title: photo.title });
return li.getNode();
};
renderPhoto(photo, size)
Another utility function using MarkoutJS to render a larger version of the photo with it’s title into a DocumentFragment. This is used to set the contents of the Overlay which appears when you click on a photo thumbnail.
renderPhoto = function (photo, size) {
var df = Y.Markout();
df.div().a({ title: 'View on Flickr', href: getPhotoPageURL(photo) }).img({ src: getPhotoURL(photo, size), alt: photo.title });
df.p().text(photo.title);
return df.getNode();
};
Putting It All Together
At this point I have a way to get the user’s location, fetch photos from Flickr based on a location, and photo utility functions; this is how I put it all together:
Constructing The Photo Overlay
YUI 3.1 comes with a feature-rich Overlay Widget; for this app I’m creating a single Overlay instance which a larger version of the photo will be rendered into when the user clicks on a photo thumbnail.
photoOverlay = new Y.Overlay({
visible : false,
centered : true,
width : '550px',
zIndex : 100,
constrain : true,
headerContent : '<span class="close">×</span>',
render : true
});
I’m keeping the Overlay hidden as first, and rendering a close “button” in the header area.
Clicking a Thumbnail Shows Larger Version in the Overlay
Using YUI 3.1’s Event Delegation, I attached a click handler to the list of thumbnails which is only triggered if a thumbnail is clicked. Using the photo’s data (which is stored with the thumbnail Y.Node instance) I’m setting the content of the Overlay to the larger version of the photo and making sure the Overlay is visible.
Y.delegate('click', function(e){
var thumbnail = e.currentTarget.addClass('loading'),
photoData = thumbnail.getData();
loadImg(getPhotoURL(photoData), function(){
photoOverlay.setStdModContent(Y.WidgetStdMod.BODY, renderPhoto(photoData));
photoOverlay.show().centered();
thumbnail.removeClass('loading');
});
}, '#photos', '.photo');
The Overlay’s Close “button”
The only fancy part here is the CSS used to style the close “button”; the JavaScript is pretty straight forward:
Y.one('.close').on('click', Y.bind(photoOverlay.hide, photoOverlay));
.yui3-overlay-content .close {
position: absolute;
top: -10px;
left: -10px;
-webkit-box-shadow: 0 0 4px rgba(0, 0, 0, 0.50);
-moz-box-shadow: 0 0 4px rgba(0, 0, 0, 0.50);
box-shadow: 0 0 4px rgba(0, 0, 0, 0.50);
border: #f9f9f9 2px solid;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
background: rgb(34, 34, 34);
width: 18px;
height: 18px;
line-height: 16px;
text-align: center;
font-size: 1.143em;
font-weight: 700;
cursor: pointer;
}
Wiring Everything Together
Now for the main “run-time” of the app. This is the code that makes the calls to get the user’s location, fetch the initial set of 100 photos based on the location, renders them as thumbnails, and builds and wires-up a “more photos” button.
getLocation(function(loc){
var locality = loc ? loc.locality1.content : null,
num = 100,
start = 1,
heading = Y.one('h1'),
loading = Y.one('#loading');
if ( ! loc) {
loading.remove();
heading.set('text', 'No Photos Around You :-(');
return;
}
loading.set('text', 'Fetching ' + locality + ' Photos…');
heading.set('text', 'Photos Around ' + locality);
getPhotos(loc, start, num, function(photos){
var photosNode = Y.one('#photos'),
df = Y.Markout().getNode(),
more;
loading.remove();
if ( ! photos) {
heading.set('text', 'No Photos Around ' + locality + ' :-(');
return;
}
photos = Y.Lang.isArray(photos) ? photos : photos ? [photos] : [];
Y.Array.each(photos, function(photo){
df.append(renderThumbnail(photo).setData(photo));
});
photosNode.append(df);
if (photos.length === num) {
more = Y.Markout('#content').p({ id: 'more' }).button().getNode().set('text', 'More ' + locality + ' Photos');
more.on('click', function(){
more.set('disabled', true).set('text', 'Loading More ' + locality + ' Photos…');
getPhotos(loc, start+=num, num, function(photos){
var df = Y.Markout().getNode();
photos = Y.Lang.isArray(photos) ? photos : photos ? [photos] : [];
Y.Array.each(photos, function(photo){
df.append(renderThumbnail(photo).setData(photo));
});
photosNode.append(df);
more.set('disabled', photos.length < num).set('text', 'More ' + locality + ' Photos');
});
});
}
});
});

A look at the YUI 3.1-based “Photos Around You” project by @ericf: http://bit.ly/9F51FE
This comment was originally posted on Twitter
@ericf the photos around you app is great. Shows how easy YUI 3.1 + YQL makes it to build quick, awesome apps. http://is.gd/bfDcv
This comment was originally posted on Twitter
Great post, Rey. I was thinking of discussing the W3C Geolocation API in my new book but the idea of having a solid failover in this demo (Christian’s fanastic YQL Library) sounds quite practical. Have you checked how well browsers other than FF perform with this in terms of accuracy?
This comment was originally posted on Ajaxian
Yep. I checked out Chrome & IE8 and using the YQL failover both found my location correctly. I was pretty excited about that! :)
I know geo solutions are really maturing so seeing something this simple work is awesome.
This comment was originally posted on Ajaxian
http://925html.com/code/photos-around-you/ is full of win. Location based photo mashup using my yql geo library.
This comment was originally posted on Twitter
Great find Rey! Thanks for sharing this. It’ll be fun to go through his code and dissect it all. Being ~180 lines will definitely go good with my short attention span. How do you test the failover?
This comment was originally posted on Ajaxian
Man, this is just a awesome tutorial about the geolocation apis!
Thanks so much for sharing.
I love the fact that you can accomplish so much with so little code. This is what re-use and services is all about. I think it’s going to massively change the kinds of people developing apps though – there’s less and less need for bit-twiddling C-style programmers. I think that’s kind of sad.
This comment was originally posted on Ajaxian
I’m presenting on Photos Around You http://j.mp/9vGfeu at the Boston JavaScript Meetup next Weds. (the 28th) http://j.mp/c92FpW
This comment was originally posted on Twitter