Flickr cities tutorial

September 16, 2010 by Oni Labs

This is a tutorial explaining how the Flickr Cities demo makes use of StratifiedJS and Oni Apollo.
The whole code is included in the article, mixed with comments and links to the Apollo documentation and modules description.

UPDATE: This article has been amended slightly to take into account changes made to Apollo's module system in version 0.10.0 (there are no default builtin modules anymore - everything has been externalized into the Apollo Standard Module Library).

Inside the StratifiedJS script tag

First we're loading some modules and external javascript.
// Load jquery from a CDN 
// and install $click (stratified events) and friends:
require("apollo:jquery-binding").install();
// Define a shortcut to the mini-template function:
var r = require("apollo:common").supplant;
// We'll use YQL to get our crossdomain data:
var yql = require("apollo:yql");
var more = $("#more");

In the code above, note how we don't have to provide a callback to functions that go and fetch something asynchronously over the network: require("apollo:jquery-binding").install will block until jquery has loaded. The browser, however, will stay fully responsive during that time.

Next we're retrieving a list of captial cities from wikipedia using the stratified Yahoo Query Language API. At the same time we want to give the user the opportunity to cancel the request. We can do both of these things simulaneously by wrapping them in a waitfor/or construct. In the first clause of the waitfor/or we'll perform the request, and in the second one we'll wait for a click from the user. As soon as one of the clauses finishes (by either the request completing, or the user clicking), the other still pending clause will be automatically cancelled.

waitfor {
  var cities = yql.query(
    "select title from html " +
    "where url='http://en.wikipedia.org/wiki/" + 
    "List_of_cities_proper_by_population' " +
    "and xpath='//table/tr/td[2]/a'").results.a;
}
or {
  $("#more h2").html("Just show Chicago");
  more.$click();
  throw "user can't wait!";
} 
catch(e) {
  // The user cancelled (throwing a "user can't wait" exception)
  // or there was an error with the request.
  // We'll just populate the list of cities with one city and continue:
  var cities = [{
    title: "Chicago"
  }];
}

Now that we've got the list of cities, the real interaction flow starts. First we define a couple of helper functions. loadPhotos loads and displays a bunch of photos of the given city:

var per_page = 13;

function loadPhotos(city, page) {
  var q = "select * from flickr.photos.search(@begin, @amount) " +
    "where text=@city and sort='relevance' and extras='url_m'";
  var rv = yql.query(q, {
    begin: page * per_page, amount: per_page, city:city
  });
  var photos = rv.results ? rv.results.photo: [];
  for (var i = 0, photo; photo = photos[i]; ++i) {
    more.before(r("
    <div class='blob'>
      <a target='_blank' 
        href='http://www.flickr.com/photos/{owner}/{id}' 
        class='img' 
        style='background-image:url({url_m})'/>
        <h3>{title}</h3>
      </a>
    </div>
    ", photo));
  }
}

showCity handles the incremental loading of photos as the user scrolls down. Its main body contains of an endless loop that first loads in a few photos. Simultaneously to loading the photos we're presenting the user with a cancel button, again using a waitfor/or, just like for the wikipedia cities request above.

function showCity(city) {
  var page = 0;
  $("#city h2").text(city);
  $("#results .blob:not(#more):not(#city)").remove();
  
  while (true) {
    waitfor {
      loadPhotos(city, page);
    }
    or {
      $("#more h2").text("Cancel lookup").addClass("working");
      more.$click();
      --page;
    }
    $("#more h2").html("Load more …").removeClass("working");
    ...

The next part of the endless loop waits simultaneously for the user to click on the "Show more" button, or for the user to scroll to the bottom of the page. If either event occurs, we're incrementing the photo offset and go round the loop again, displaying the next bunch of photos.

    ...
    waitfor {
      more.$click();
    }
    or {
      while (true) {
        $(window).$scroll();
        var frombottom = $(document).height() - 
          $(window).height() - $(document).scrollTop();
        if (frombottom < 100) break;
      }
    }
    ++page;    
  }
}

Function animateImages consists of an endless loop that picks a random image on the page, applies a CSS animation to it, waits for 3 second and goes round the loop again:

function animateImages() {
  while (true) {
    var imgs = $(".img");
    var img = imgs.get(Math.floor(Math.random() * imgs.length));
    var p = [20, 20, 50, 80, 80][Math.floor(Math.random() * 5)];
    $(img).css({
      backgroundPosition: p + "% " + 50 + "%"
    });
    hold(3000);
  }
}

Finally, here's the main loop that ties everything together. We have an endless loop that first picks a random city off the list and then runs the showCity loop for this city while simultaneously animating images and waiting for the user to click the "Next city" button. If the latter happens, both showCity and animateImages will be cancelled, and we go round the loop again, picking the next city.

// main program loop
while (true) {
  var cityindex = Math.floor(Math.random() * cities.length);
  waitfor {
    showCity(cities[cityindex].title);
  }
  or {
    animateImages();
  }
  or {
    $("#nextcity").$click();
  }
}