      var MonthAbbr = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
      var Month = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];

      // Cache a bunch of elements we'll use a lot
      var elPhotos = $('#photos'), elPane = $('#pane'), elDate = $(".date"), elInfo = $("#info"), elRefine = $('#date');
      var elAddKeyword = $("#add_keyword"), elKeywords = $("#keywords"), elSuggestions = $("#suggestions");

      // half the height of the pane. Used to position slates and reposition after a resize
      var h2 = elPane.height() / 2;      
      
      // The photo that is currently enlarged, if any
      var showing = null, last_showing = null, need_to_show = null, preparing = null;

      // The last date we showed, so we can update the date only when it has changed      
      var last_date = new Date(0), jump_to_date = null;

      // What does last_value do?!
      var last_keyword = '', last_value = elAddKeyword.attr('value');
      var last_suggestion = null;
      var keyword_cache = { };

      var photos = [ ], details = { }, cache = { }, qid_map = { }, people_map = { };
      
      var requests = 0;
      
      // Query id for tracking in-flight requests and caching
      var qid = 0;

      // We use this to optimize calling bookmark update code and to know when we changed.
      var last_url = null;
      
      function refine_list(clazz) {
        var found = [ ];
        $(clazz).each(function (idx, el) { found.push($(el).data('word')) });
        if (found.length == 0) { return ''; }
        found.sort();
        var ret = escape(found[0]);
        for (var i = 1; i < found.length; i++) { ret += ',' + escape(found[i]); }
        return ret;
      }
      function get_keywords() { return refine_list('.kw_keyword'); }
      function get_people() { return refine_list('.kw_person'); }
      function get_people_names() {
        var people = get_people();
        if (people == '') { return ''; }
        people = people.split(',');
        var lookup = { };
        $('.kw_person').each(function (idx, el) { lookup[escape($(el).data('word'))] = $(el).data('display'); });
        var ret = escape(lookup[people[0]]);
        for (var i = 1; i < people.length; i++) { ret += ',' + escape(lookup[people[i]]); }
        return ret        
      }

      var reCommify  = new RegExp('(-?[0-9]+)([0-9]{3})'); 
      function commify(text) {
        text = '' + text;
        while(reCommify.test(text)) {
          text = text.replace(reCommify, '$1, $2');
        }
        return text;
      }
      
      function zoom_size(photo) {
        var height = elPane.height() - 120;
        if (height > 900) { height = 900; }
        var width = parseInt(height * photo.w / photo.h);
        if (width > elPane.width() - 120 - 260 || width > 900) {
          width = elPane.width() - 120 - 260;
          if (width > 900) { width = 900; }
          height = parseInt(width * photo.h / photo.w);
        }
        return {
          height: height + 'px',
          left: (elPane.scrollLeft() + parseInt((elPane.width() - width - 260) / 2)) + 'px',
          top: parseInt((elPane.height() - height - 20) / 2) + 'px',
          width: width + 'px'
        }
      }
      
      function find_photo(date) {
        var center = elPane.scrollLeft() + elPane.width() / 2;
        var closest = null, delta = null;
        for (var i = 0; i < photos.length; i++) {
          var photo = photos[i];
          var d = Math.abs((date) ? (date - photo.e): (center - photo.x));
          
          if ((delta == null || d < delta) && photo.m) {
            delta = d;
            closest = photo;
          }
        }
        return closest;
      }
      
      function show_info() {
        var info = (showing) ? details[showing.k]: null;

        // Hide the info box
        if (!info) {
          if (!preparing && elInfo.css('right') != '-300px') {
            elInfo.stop().animate({ right: '-300px' }, 700, function() {
              $('.about', elInfo).empty().data('check', '')
            });
          }
          return;
        }

        // Update the date, if necessary
        var date = new Date(showing.e * 1000);
        var check = date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate();
        var eDate = $('.datebox', elInfo);
        eDate = $(eDate.get(eDate.length - 1));
        if (eDate.data('check') != check) {
          var newDate = eDate.clone().css({ opacity: 0 }).animate({
            opacity: 1 
          }, 800, function() {
            eDate.remove();
          }).data('check', check);
          $('h1', newDate).html(date.getFullYear());
          $('h2', newDate).html(MonthAbbr[date.getMonth()]);
          $('h3', newDate).html(date.getDate());
          eDate.after(newDate);
        }
        
        // Update the globe, if necessary
        var eGlobe = $('.globebox', elInfo);
        eGlobe = $(eGlobe.get(eGlobe.length - 1));
        if (eGlobe.data('check') != info.globe) {
          var newGlobe = eGlobe.clone().css({ 
            backgroundImage: 'url(http://static.ricmoo.com/globe/latlng_' + info.globe + '.png)',
            opacity: 0 
          }).animate({ opacity: 1 }, 800, function () {
            eGlobe.remove();
          }).data('check', info.globe);
          eGlobe.after(newGlobe);
        }
        
        var eAbout = $('.about', elInfo);
        eAbout = $(eAbout.get(eAbout.length - 1));
        var new_html = '';
        if (info.keywords && info.keywords.length) { 
          new_html += '<h1 class="header">Keywords</h1>';
          for (var i = 0; i < info.keywords.length; i++) { 
            new_html += ((i == 0) ? '': ', ') + '<span class="add_keyword">' + info.keywords[i] + '</span>'; 
          }
        }
        if (info.people && info.people.length) { 
          new_html += '<h1 class="header">People</h1>';
          for (var i = 0; i < info.people.length; i++) { 
            var person = people_map[info.people[i]];
            if (!person) { continue; }
            new_html += ((i == 0) ? '': ', ') + '<span class="add_person" key="' + info.people[i] + '">' + person + '</span>'; 
          }
        }

        if (new_html != eAbout.data('check')) {
          var newAbout = eAbout.clone().empty().css({
            opacity: 0
          }).append($(new_html)).data('check', new_html);
          eAbout.after(newAbout).animate({ 
            opacity: 0 
          }, 600, function() {
            $(this).remove();
          });
          newAbout.animate({ opacity: 1 }, 600);
          eAbout = newAbout;
        }

        $(".add_keyword").click(function () {
          var display = $(this).html()
          add_keyword(display, display, 'kw_keyword');
          /*
          var from_box = $(this).offset(), to_box = elAddKeyword.offset();
          var floater = $(document.createElement('div')).css({
            color: '#fff',
            position: 'absolute',
            left: from_box.left + 'px',
            top: from_box.top + 'px',
            zIndex: 2000
          }).html(display).animate({
            top: to_box.top + 'px'
          }, 1000, function () { $(this).remove() });
          $('html').append(floater);
          */
          refresh();
        });

        $(".add_person").click(function () {
          add_keyword(this.getAttribute('key'), $(this).html(), 'kw_person');
          refresh();
        });

        // Show the info box
        if (elInfo.css('right') == '-300px') {
          elInfo.stop().css({ height: (10 + eAbout.height()) + 'px'}).animate({ right: '-30px' }, 700);
        } else {
          elInfo.stop().animate({ height: (10 + eAbout.height()) + 'px', right: '-30px' }, 700);
        }
        
        if (true) {
          //console.log('r');
          //showing.div.append($('<div class="description">Hello world</div>'));
        }

      }


      /**
       * Load images sufficient to satisfy the viewing window
       *   - We remove anything far away
       *   - When we add a slate, we kick off loading the src.
       *   - Each slate, when created, has an additional div mask placed 
       *     on top. Once loaded:
       *     - If the image is onscreen, we fade it in
       *     - Otherwise, we bring it inimmediately
       */            
      var last_scrollleft = null;
      function load_window(left, width) {
        var window = 3000;
      
        //if (last_scrollleft != null && Math.abs(elPane.scrollLeft() - last_scrollleft) < window / 3) { return; }
        last_scrollleft = elPane.scrollLeft();
        
        var t0 = (new Date()).getTime();
        
        var min_epoch = null, max_epoch = null, fetch_details = false, fetch_count = 0;

        for (var i = 0; i < photos.length; i++) {
          var photo = photos[i];
          
          // We want to fetch details for any image within a large distance of the screen,
          // whenever something in a near distance doesn't have details.
          if (photo.x > left - 6000 && photo.x < left + width + 6000) {
            if (photo.x > left - 3000 && photo.x < left + width + 3000) {
              if (!photo.f) { 
                fetch_details = true; 
              }
            }
            
            // If we don't have any details and the details aren't currently being fetched...
            if (!photo.f) {
              fetch_count++;
              if (min_epoch == null || photo.e < min_epoch) { min_epoch = photo.e; }
              if (max_epoch == null || photo.e > max_epoch) { max_epoch = photo.e; }
            }
            
          }

          // Already in the dom...
          if (photo.div) {
          
            // ...but far away. Nuke it!
            if (photo.x < left - window || photo.x > left + width + window) {
              photo.div.remove();
              delete photo.div;
            }
            
            continue; 
          }
          
          // Not near screen. Skip it.
          if (photo.x < left - window || photo.x > left + width + window) { continue; }

          // Closure to handle showing/hiding full sized image when clicked
          function clicked(photo) {
            return function() {
            
              // Hide this full size photo
              if ($('img', photo.div).get(0)) {
                photo.div.stop().animate({
                  height: photo.h + 'px',
                  left: parseInt(photo.x - photo.w / 2) + 'px',
                  rotate: photo.a + 'deg',
                  top: parseInt(h2 + photo.y - photo.h / 2 - 10) + 'px',
                  width: photo.w + 'px'
                }, 350, function() {
                  $(this).css({ zIndex: photo.z });
                  $('img', photo.div).remove();
                });
                last_showing = showing;
                showing = null;
              
              // Show the full size photo
              } else {
                // Hide any currently showing photo
                if (showing && showing.div) { showing.div.click(); }
                
                $('div', photo.div).remove();

                // Find the maximum dimensions we can use that is smaller than (900, 900) and fits on the screen.
                var box = zoom_size(photo);
                
                // The image we're loading. We set the background to the low-res                
                var img = $(document.createElement('img')).css({
                  background: 'url(http://nebula-' + (photo.e % 8) + '.ricmoo.com/thumb.' + photo.k + '.jpg) center no-repeat'
                }).attr({
                  src: 'http://nebula-' + (photo.e % 8) + '.ricmoo.com/preview.' + photo.k + '.jpg'
                });

                // Prefetch the previous and next image
                if (photo.p) {
                  (new Image()).src = 'http://nebula-' + (photo.p.e % 8) + '.ricmoo.com/preview.' + photo.p.k + '.jpg';
                }
                if (photo.n) {
                  (new Image()).src = 'http://nebula-' + (photo.n.e % 8) + '.ricmoo.com/preview.' + photo.n.k + '.jpg';
                }
                
                // Zoom in on the full size photo
                photo.div.append(img).css({ zIndex: 1000 }).stop().animate({
                  height: box.height,
                  left: box.left,
                  rotate: '0deg',
                  top: box.top, 
                  width: box.width
                }, 500);

                showing = photo;
              }
              
              setTimeout(show_info, 300);
              
              write_hash();
            }
          }

          // The div to hold the actual image.. We load the image after a delay below.
          photo.div = $(document.createElement('div')).addClass('slate').css({
            height: photo.h + 'px',
            position: 'absolute',
            left: parseInt(photo.x - photo.w / 2) + 'px',
            top: parseInt(h2 + photo.y - photo.h / 2 - 10) + 'px',
            padding: '2px',
            width: photo.w + 'px',
            zIndex: photo.z
          }).rotate(photo.a).click(clicked(photo)); ///.html(photo.z);
          
          // A mask we put over the photo while it loads
          var mask = $(document.createElement('div')).css({
            height: photo.h + 'px',
            width: photo.w + 'px',
          });
          photo.div.append(mask);

          // Closure to fade away the mask we laid on top of the image
          // If not visible we pop it in immediately
          function fadeaway(target, x) {
            return function() {
              var left = elPane.scrollLeft(), width = elPane.width();
              var onscreen = (x > left - 250 && x < left + width + 250);
              if (onscreen) {  
                target.stop().animate({ opacity: 0 }, 1000, function() {
                  $(this).remove();
                });
              } else {
                target.remove();
              }
            };
          };


          // Closure that downloads the image only if it is still in the dom
          function load_image(photo, img) {
            return function() {
              if (!photo.div) { return; }

              img.src = 'http://nebula-' + (photo.e % 8) + '.ricmoo.com/thumb.' + photo.k + '.jpg';
              photo.div.css({
                background: '#000 url(' + img.src + ') no-repeat center'
              });
              
              photo.c = 1;
              
              requests++;
            }
          }
          
          // Track the image download status
          var img = new Image();
          img.onload = fadeaway(mask, photo.x);

          // We now delay kicking off the image download for 100ms in case they are dragging the
          // scrollbar. Otherwise we fetch a lot of images that we won't end up showing and tie
          // up the browser host limit for currently showing images. If we have already seen the
          // image before, however, we assume the browser cached it.
          if (photo.c) {
            load_image(photo, img)();
          } else {
            setTimeout(load_image(photo, img), 100);
          }
          
          elPhotos.append(photo.div);
        }

        // Show the date of the current center photo
        var photo = find_photo();
        if (photo) {
          var date = new Date(photo.e * 1000);
          
          // Set the date in the upper right hand corner if it has changed
          if (last_date.getYear() != date.getYear() || last_date.getMonth() != date.getMonth() || last_date.getDate() != date.getDate()) {
            var suffix = 'th';
            if (parseInt(date.getDate() / 10) != 1) {
              if ((date.getDate() % 10) == 1) { 
                suffix = 'st'; 
              } else if ((date.getDate() % 10) == 2) { 
                suffix = 'nd'; 
              } else if ((date.getDate() % 10) == 3) { 
                suffix = 'rd'; 
              }
            }
            var new_date = $('<span class="date">' + Month[date.getMonth()] + ' ' + date.getDate() + '<span class="sup">' + suffix + '</span>, ' + date.getFullYear() + '</span>')
            elDate.css({ opacity: 0.4 }).after(new_date).animate({ 
              opacity: 0
            }, 500, function () {
              $(this).remove();
            });
            elDate = new_date;
            last_date = date;
          }
        }
        
        if (fetch_details) {
          var foo = 0;
          for (var i = 0; i < photos.length; i++) {
            var photo = photos[i];
            if (photo.e < min_epoch || photo.e > max_epoch) { continue; }
            foo++;
            photo.f = true;
          }
          $.getJSON('/lookup?method=details&keywords=' + get_keywords() + '&people=' + get_people() + '&min_epoch=' + min_epoch + '&max_epoch=' + max_epoch, function(data, status) {
            if (status != 'success') {
              alert('Error: JSON failed - ' + status);
            }
            var deltas = data.photos;
            var last_epoch = 0;
            for (var i = 0; i < deltas.length; i++) {
              var key = deltas[i][0] + last_epoch;
              if (key == parseInt(key)) { key += '.0'; }
              details[key] = deltas[i][1];
              last_epoch = parseInt(key);
            }
            for (var key in data.people) {
              people_map[key] = data.people[key];
            }
            show_info();
          });
        }
        var t1 = (new Date()).getTime();
        //console.log('active', $('.slate').length, t1 - t0 + 'ms');
      }


      /**
       *  Scroll the pane
       */      
      function scroll() {
        last_showing = null;
        if (showing) { showing.div.click(); }
        var left = elPane.scrollLeft();
        var width = elPane.width();
        load_window(left, width);
        write_hash();
      }
      
      
      /**
       * Allows when showing a zoomed image:
       *   - left and right to navigate between images
       *   - space, enter and esc to unzoom
       * Otherwise:
       *   - left and right pan
       *   - space zooms in on the center
       */
      function keydown(event) {
      
        // This isn't for us. Prolly trying to send a command to the browser (switch tabs, go back/forward)
        if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { return true; }
      
        var consumed = false;
        switch (event.keyCode) {
          case 36: // Home
            if (!showing) {
              elPane.stop(false, true).animate({ scrollLeft: '0px' });
            }
            consumed = true;
            break;
          case 35: // End
            if (!showing) {
              elPane.stop(false, true).animate({ scrollLeft: parseInt(elPhotos.width() - elPane.width()) + 'px' });
            }
            consumed = true;
            break;
          case 33: // Page up
            if (!showing) {
              elPane.stop(false, true).animate({ scrollLeft: parseInt(elPane.scrollLeft() - elPane.width() + 125) + 'px' });
            }
            consumed = true;
            break;
          case 34: // Page down
            if (!showing) {
              elPane.stop(false, true).animate({ scrollLeft: parseInt(elPane.scrollLeft() + elPane.width() - 125) + 'px' });
            }
            consumed = true;
            break;
          case 37: case 63234: // Left
            if (showing) {
              if (showing.p) { 
                var p = showing.p;
                if (p.x < elPane.scrollLeft() + 250) {
                  preparing = p;
                  elPane.stop(false, true).animate({ scrollLeft: (p.x - 250) + 'px' }, 300, function(target) {
                    return function() {
                      setTimeout(function() {
                        preparing = null;
                        if (showing != target) { target.div.click(); }
                      }, ($.browser.webkit) ? 50: 300);  // firefox sends scroll events longer... *shrug*
                    }
                  }(p));
                } else {
                  p.div.click();
                }
              }
            } else if (!preparing) {
              elPane.stop(false, true).animate({ scrollLeft: parseInt(elPane.scrollLeft() - elPane.width() / 3) + 'px' });
            }
            consumed = true;
            break;
          case 39: case 63235: // Right
            if (showing) {
              if (showing.n) { 
                var n = showing.n;
                if (n.x > elPane.scrollLeft() + elPane.width() - 250) { 
                  preparing = n;
                  elPane.stop(false, true).animate({ scrollLeft: (n.x - elPane.width() + 250) + 'px' }, 300, function(target) {
                    return function() {
                      setTimeout(function() {
                        preparing = null;
                        if (showing != target) { target.div.click();}
                      }, ($.browser.webkit) ? 50: 300);
                    }
                  }(n));
                } else {
                  n.div.click();
                }
              }
            } else if (!preparing) {
              elPane.stop(false, true).animate({ scrollLeft: parseInt(elPane.scrollLeft() + elPane.width() / 3) + 'px' });
            }
            consumed = true;
            break;
          case 32:
            if (!showing) {
              if (photos.length) {
                if (last_showing && last_showing.div) {
                  last_showing.div.click();
                } else {
                  var photo = find_photo();
                  if (photo && photo.div) { photo.div.click(); }                  
                }
                consumed = true;
              }
            }
          case 13: case 27:
            if (!consumed && showing) {
              showing.div.click();
              consumed = true;
            }
        }
        return !consumed;
      }

      
      /**
       * Resize all the necessary views and reposition the images
       */
      function resize() {
        elPane.css({ height: $(window).height() });
        elPhotos.css({ height: $(window).height() });
        
        // Shift everything vertically to be centred
        var old_h2 = h2;
        h2 = elPane.height() / 2;      
        $('.slate').each(function(i, e) {
          e.style.top = (parseInt(e.style.top) + (h2 - old_h2)) + 'px';
        });
        
        if (showing) { showing.div.click(); }

      }
      
      
      /**
       *  Load a fresh batch of data and prepare the pane
       */
      function load_photos(data, status) {
        var deltas = data.deltas, sizes = data.sizes, sizemap = data.sizemap;
        
        cache[qid_map[data.qid]] = data;
        if ('q' + qid != data.qid) { return; }

        elPhotos.empty();
        
        $('body').append($('<div class="notice">Organizing ' + commify(deltas.length) + ' photos...</div>').delay(2000).animate({
          opacity: 0
        }, 4000, function () { 
          $(this).remove();
        }));
        
        // nextX is in pixels, nextY [ -1, 0, 1 ] depending whether to move up or down
        var nextX = 0, nextY = 0;   
        
        // Find median delta
        var sorted = deltas.slice(0).sort(function (a, b) { return a - b; });
        var cap = sorted[parseInt(sorted.length * 0.80)]
        
        // Build up the photos array. Includes adding (x, y) from triangle grid        
        photos = [ ];
        
        var last_epoch = 0, more_space = 1, direction = 0;
        for (var i = 0; i < deltas.length; i++) {
        
          var key = deltas[i] + last_epoch;
          if (key == parseInt(key)) { key += '.0'; }
          
          var rand = parseInt(997.0 * key);
          
          var sig = (deltas[i] > cap);
          var next_sig = false;
          if (i + 1 < deltas.length) { next_sig = (deltas[i + 1] > cap); }
          
          //var sig = (((i == 0) ? 0: deltas[i]) > 60 * 60 * 24);
          //var next_sig = (i + 1 >= deltas.length) ? false: (deltas[i + 1] > 60 * 60 * 24);

          if (i == 0 || sig) {
            nextX += 250;
            nextY = 0;
            direction = rand % 2;
          } else {
            if (nextY != 0 && next_sig) { nextY = -1; }
            switch (nextY) {
              case -1: nextY = 0; nextX += 250; break
              case 0: nextY = 1; break
              case 1: nextY = -1; break
            }
          }
          // Determine the dimensions for the 250x250 image. Larger then 250 indicates the height is less than the width
          var width = sizemap[sizes[i]], height = 250;
          if (width > 250) {
            height = 500 - width;
            width = 250;
          }
          
          photos.push({ 
            a: (rand % 30) - 15,
            d: ((i == 0) ? '0': deltas[i]) + ' ' + ((sig) ? 'sig': 'insig') + ' ' + ((next_sig) ? 'nextsig': 'nextinsig'),
            e: parseInt(key),
            f: false,            // true if fetching or fetched
            h: height,
            k: '' + key,
            m: (nextY == 0),     // middle - used for find_photo
            w: width,                                        
//            x: nextX + (125 * Math.abs(nextY)) + ((rand % 50) - 25), 
            x: nextX + (125 * Math.abs(nextY)), 
//            y: (nextY * ((direction & 1) ? -1: 1)) * 230 + (rand % 50) - 25,
            y: (nextY * ((direction & 1) ? -1: 1)) * 250,
            z: parseInt(rand % 100) + 10
          });

          // Add the previous and next markers for skipping forward and backward          
          var this_photo = photos[photos.length - 1];
          if (photos.length > 1) {
            var last_photo = photos[photos.length - 2];
            last_photo.n = this_photo;
            this_photo.p = last_photo;
          }
          if (this_photo.k == need_to_show) { need_to_show = this_photo; }

          last_epoch = this_photo.e;
        }

        // Now make the page big enough so our background tiles all the way to the end
        var width = (photos) ? (photos[photos.length - 1].x + 250 + 170): 0;
        if (width < elPane.width()) { width = elPane.width(); }
        elPhotos.css({
          height: elPane.height() + 'px',
          width: width + 'px'
        });
        
        // Scroll to the far right
        var left = elPhotos.width() - elPane.width();
        if (jump_to_date) {
          var photo = find_photo(jump_to_date);
          left = parseInt(photo.x - elPane.width() / 2);
        }
        elPane.scrollLeft(left);
        scroll();
        
        if (need_to_show && need_to_show.div) {
          setTimeout(function(target) {
            return function() { target.div.click(); }
          }(need_to_show), 500);
        }
        need_to_show = null;
      }
      
      elAddKeyword.focus(function() {
        $(document).unbind('keydown');
        $(this).css({
          color: '#fff',
          fontStyle: 'normal'
        }).attr('value', last_keyword);
        last_suggestion = '';
        show_suggestions();

      }).blur(function () {
        $(document).keydown(keydown);
        last_keyword = $(this).attr('value');
        $(this).css({
          color: '#989898',
          fontStyle: 'italic'
        }).attr('value', 'add keywords...');
      });

      function changed() {
        var value = elAddKeyword.attr('value');
        if (value == 'add keywords...' || value == '') {
          elSuggestions.empty();
          return;
        }
        if (last_value == value) { return; }
        last_value = value;
        var prefix = value.substring(0, 3);
        if (keyword_cache[prefix]) {
          show_suggestions();
        } else if (prefix.length > 0) {
          $.getJSON('/lookup/?method=keywords&prefix=' + escape(prefix.toLowerCase()), function(data, status) {
            if (status != 'success') {
              alert('Error: JSON failed - ' + status);
            }
            keyword_cache[data.prefix] = data;
            show_suggestions();
            last_suggestion = null;
          });
        }
      };
      setInterval(changed, 200);

      function add_keyword(word, display, clazz) {
        var keyword = $(document.createElement('div')).addClass(clazz).data('word', word).data('display', display); /*.css({
          fontSize: '1.0em'
        }).animate({
          fontSize: '0.7em'
        });
        */
        elKeywords.append(keyword);
        keyword.append($(document.createTextNode(display)));
        keyword.append($(document.createElement('img')).attr({
          src: '/static/remove.png'
        }).click(function () {
          $(this).parent().remove();
          var photo = find_photo();
          if (photo) { jump_to_date = photo.e; }
          refresh();
        }));
        return keyword;
      }

      function build_suggestion(word, display, clazz, weight, count) {
        var color = '9f';
        switch (parseInt(weight * 20)) {
          case 19: case 20: color = 'ff'; break;
          case 18: color = 'f8'; break;
          case 17: color = 'ef'; break;
          case 16: color = 'e8'; break;
          case 15: color = 'df'; break;
          case 14: color = 'd8'; break;
          case 13: color = 'cf'; break;
          case 12: color = 'c8'; break;
          case 11: color = 'bf'; break;
          case 10: color = 'b8'; break;
          case 9: color = 'af'; break;
          case 8: color = 'a8'; break;
        }
        
        return $(document.createElement('div')).data('word', word).css({
            color: ('#' + color + color + color),
          }).addClass(clazz).append(
            $(document.createElement('span')).html(' (' + count + ')')
          ).append($(document.createTextNode(' ' + display))).append(
            $(document.createElement('img')).attr({ 
              src: '/static/add.png'
            })
          ).click(function () {
            add_keyword(word, display, clazz);
            last_keyword = '';
            elSuggestions.empty();
            refresh();
          })
      }

      function show_suggestions() {
        var suggestion = elAddKeyword.attr('value').toLowerCase();
        if (last_suggestion == suggestion || !keyword_cache[suggestion.substring(0, 3)]) { return; }
        last_suggestion = suggestion;  
        elSuggestions.empty();
            
        var check = keyword_cache[suggestion.substring(0, 3)];
        if (check.keywords) {
          check = check.keywords;
          var maxCount = 0, minCount = 9999999;
          var sorted = new Array(), values = new Array();
          for (var word in check) {
            if (word.toLowerCase().indexOf(suggestion) != 0 && word.toLowerCase().indexOf(' ' + suggestion) <= 0) { continue; }
            if (check[word][0] > maxCount) { maxCount = check[word][0]; }
            if (check[word][0] < minCount) { minCount = check[word][0]; }
            values.push(check[word][0]);
            sorted.push(word);
          }
          sorted = sorted.sort(function(a, b) {
            a = a.toLowerCase();
            b = b.toLowerCase();
            if (a < b) { return -1; }
            if (a > b) { return 1; }
            return 0;
          });
          values = values.sort(function(a, b) { return b - a; }); 
          var value_map = { };
          var value = 0, last_value = null;
          for (var idx = 0; idx < values.length; idx++) {
            value_map[" " + values[idx]] = value;
            if (values[idx] != last_value) {
              last_value = values[idx];
              value++;
            }
          }
          for (var idx = 0; idx < sorted.length; idx++) {
            var word = sorted[idx]; 
            var count = check[word][0];
            var weight = 1 - (value_map[" " + count] / value);
            elSuggestions.append(build_suggestion(check[word][2], word, ((check[word][1] == 'k') ? 'kw_keyword': 'kw_person'), weight, count));
          }
        }
      }
              
      // If they press enter, add the first keyword to the selected keywords
      $('form').submit(function () {
        var children = elSuggestions.children();
        if (children.length) {
          $(children[0]).click();
          last_suggestion = '';
          elAddKeyword.attr('value', '');
          elSuggestions.empty();
        }   
        return false;
      });

      function refresh() {
        last_scrollleft = null;
        if (showing) { showing.div.click(); }
        elPhotos.empty();
        photos = [ ];
        elPhotos.css({ width: elPane.width() + 'px' });
        make_spinner(elPhotos);
        qid++;
        var url = '/lookup/?method=photos&people=' + get_people() + '&keywords=' + get_keywords();
        qid_map['q' + qid] = url;
        if (cache[url]) {
          cache[url]['qid'] = 'q' + qid;
          load_photos(cache[url]);
        } else {
          $.getJSON(url + '&qid=q' + qid, load_photos);
        }
      }

      function write_hash() {
        var loc = location.href;
        
        var hash = loc.indexOf('#');
        if (hash >= 0) { loc = loc.substring(0, hash); }
        var k = get_keywords();
        var p = get_people();
        
        loc += '#/';

        if (showing || k != '' || p != '' || elPane.scrollLeft() < elPhotos.width() - elPane.width() - 150) {
          var photo = find_photo();
          loc += (photo) ? photo.k: 0;
          if (k != '') { loc += '/k' + k; }
          if (p != '') { loc += '/p' + p + '/P' + get_people_names(); }
          if (showing) { loc += '/' + showing.k; }
          
        }
        location.replace(loc);      
        last_url = loc;
      }
      
      function check_hash(first) {
        var loc = location.href;
        if (loc == last_url) { return; }
        elKeywords.empty();
        if (showing) { showing.div.click(); }
        last_url = loc;
        var hash = loc.indexOf('#/');
        if (hash == -1) { location.replace(loc + '#/'); }
        var params = loc.substring(hash + 2).split('/');
        var people = null;
        for (var i = 0; i < params.length; i++) {
          var param = params[i];
          if (param.charAt(0) == 'k') {
            var keywords = param.substring(1).split(',');
            for (var j = 0; j < keywords.length; j++) {
              add_keyword(unescape(keywords[j]), unescape(keywords[j]), 'kw_keyword');
            }
          } else if (param.charAt(0) == 'p') {
            people = param.substring(1).split(',');
          } else if (param.charAt(0) == 'P') {
            var names = param.substring(1).split(',');
            if (people && names.length == people.length) {
              for (var j = 0; j < people.length; j++) {
                add_keyword(unescape(people[j]), unescape(names[j]), 'kw_person');
              }
            }
          } else {
            jump_to_date = param;
            if (i != 0) {
              need_to_show = param;
            }
          }
        }
        
        if (!first) { refresh(); }
      }
      
      check_hash(true);
      setInterval(function() { check_hash(false); }, 500);
      
      $(window).resize(resize);
      resize();
      
      refresh();

      elPane.scroll(scroll);
      
      $(document).keydown(keydown);


