Photos Around You

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:

A screenshot of the Photos Around You Web-App

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');

				});

			});
		}

	});

});

View Photos Around You

If you liked this post you can suppot me with a Tip

1 Comment 18 Tweets 4 Other Comments

9 Responses to “Photos Around You”


Comments are currently closed.

Additional comments powered by BackType