/*
 * jsphoto.js - photo gallery with index
 * 
 * Copyright (C) 2004  Toni Ronkko
 * 
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * ``Software''), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL TONI RONKKO BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 * 
 * June 2004, Toni Ronkko <http://www.softagalleria.net>
 *
 * $Id: jsphoto.js,v 1.26 2007/03/11 11:34:49 tronkko Exp $
 * 
 * $Log: jsphoto.js,v $
 * Revision 1.26  2007/03/11 11:34:49  tronkko
 * image buttons
 *
 * Revision 1.25  2007/03/10 20:35:37  tronkko
 * name the album specific cookie automatically to simplify installation
 * (the name may be still overridden in custom configuration file),
 * scroll the browser window directly instead of using in-document links
 * to position the top of the image to users browser, fixed a display
 * problem that occured in HTML 4.0 transitional documents (thanks to
 * Jari Tikkanen)
 *
 * Revision 1.24  2006/10/18 16:08:19  tronkko
 * fixed minor typos in the help text
 *
 * Revision 1.23  2006/08/24 20:31:58  tronkko
 * change of e-mail address
 *
 * Revision 1.22  2006/07/16 16:49:42  tronkko
 * allow images upto 935x635 to be used, be sure to clear filler images
 * at the end of page that have no image or text
 *
 * Revision 1.21  2006/03/23 22:10:28  tronkko
 * bug fix: titles of thumbnail images were marked with dummy A tags, and
 * this caused the titles to light up when user moved mouse cursor over
 * the titles.  However, titles were not clickable even though they changed
 * appearance when moving mouse.
 *
 * Revision 1.20  2005/01/25 21:19:15  tronkko
 * using normal.css if user's browser is at least 850 pixels wide
 *
 * Revision 1.19  2005/01/22 15:52:13  tronkko
 * maximum width set to 800 pixels, normal.css no longer used
 *
 * Revision 1.18  2005/01/17 20:45:40  tronkko
 * do not set image width or height to 1 in html code or else Mac 
 * browsers fail to enlarge the image
 *
 * Revision 1.1  2004/06/10 00:06:20  tronkko
 * initial version
 *
 */

/****** customizable parameters **********************************************/

/* set to 1 to enable scaling of large images */
var jsphotoscaling = 0;

/* size into which to scale large images */
var jsphotomaxw = 935;
var jsphotomaxh = 635;

/* number of thumbnails on a index page */
var jsphotothumbsx = 5;
var jsphotothumbsy = 4;

/* scale thumbnails to size */
var jsphotothumbsmaxw = 140;
var jsphotothumbsmaxh = 95;

/* base directory for button images */
var jsbuttondir = ".";

/* set to 0 to hide find, info and exit buttons */
var jsphotofindbtn = 0;
var jsphotoinfobtn = 0;
var jsphotoexitbtn = 0;

/* divide keywords in the search form into n columns */
var jsphotokeycols = 4;

/* set to 1 to enable debug buttons */
var jsphotodebug = 0;

/* set to 0 to disable pre-loading of images */
var jsphotopreloadenable = 1;

/* maximum number of images to cache internally */
var jsphotocachesize = 50;


/****** language specific strings ********************************************/

/* no translation needed for English */
var jsphotostrings = new Array();


/****** global variables *****************************************************/

/* tree of images and galleries */
var jsphototree = new Array();

/* currently showing tab (1=index, 2=enlarged, 3=search) */
var jsphotocurtab = 1;

/* list of indices to reach the selected photo album in jsphototree */
var jsphotocuralbum = new Array();

/* index of current photo in the current album */
var jsphotocurimg = 0;

/* array of loaded images, indexed by image url */
var jsphotoimages = new Array();

/* number of images in cache */
var jsphotocachedimages = 0;

/* array of keywords from user */
var jsphotokeywords;



/****** initialization *******************************************************/

/*
 * Write HTML code needed to display the photo gallery.
 *
 * The generated html-page is divided into two sections of which only one is
 * visible at any given moment.  The first section contains thumbnail
 * images, and, by clicking an image, the second page displaying that very
 * image in larger size is revealed.
 */
function jsphoto(images, keywords) {

  /* initialize global variables */
  jsphototree = images; 
  jsphotocurtab = 1;
  jsphotocurimg = 0;
  jsphotocuralbum = new Array();
  jsphotoimages = new Array();
  jsphotocachedimages = 0;

  /* initialize keywords */
  if (keywords) {
    jsphotokeywords = keywords;
    jsphotokeywords.sort();
  } else {
    jsphotokeywords = new Array();
  }

  /*
   * Container for the image album.  The name and id tags are needed for
   * positioning the browser to the top of the gallery.
   */
  document.write(
      "<div class='jsphoto'" +
      " name='jsphotoalbum' id='jsphotoalbum'>\n");

  /* title row */
  document.write(
      "<table class='jsphototitlerow' cellspacing='0' cellpadding='0'>\n" +
      "<tr>\n" +

      /* previous image/page */
      "<td>" +
      "<input type='image'" +
      " id='jsphotoprev'" + 
      " onclick='jsphotoprev();'" +
      " class='jsphotobutton'" +
      " src='" + jsbuttondir + "/left.gif'" +
      " alt='[Prev]'" + 
      " title='" + jsphototranslate("Previous") + "' disabled />" +
      "</td>\n" +

      /* forward button */
      "<td>" +
      "<input type='image' " +
      " id='jsphotonext'" + 
      " onclick='jsphotonext();'" +
      " class='jsphotobutton'" +
      " src='" + jsbuttondir + "/right.gif'" +
      " alt='[Next]'" + 
      " title='" + jsphototranslate("Next") + "' />" +
      "</td>\n" +

      /* top button */
      "<td>" +
      "<input type='image' " +
      " id='jsphototop'" + 
      " onclick='jsphotoascent();'" +
      " class='jsphotobutton'" +
      " src='" + jsbuttondir + "/up.gif'" +
      " alt='[Up]'" + 
      " title='" + jsphototranslate("Return to index") + "' disabled />" +
      "</td>\n" +

      /* title */
      "<td>" + 
      "<input type='text'"+
      " id='jsphototitle'" +
      " value=''" +
      " class='jsphototitle' readonly />" +
      "</td>\n");

  /* find */
  if (jsphotofindbtn) {
    document.write(
      "<td>" +
      "<input type='image' " +
      " onclick='jsphotosearch();'" +
      " class='jsphotobutton'" + 
      " src='" + jsbuttondir + "/search.gif'" + 
      " alt='[Find]'" + 
      " title='" + jsphototranslate("Search Photos") + "' />" +
      "</td>\n");
  }

  /* info/help button */
  if (jsphotoinfobtn) {
    document.write(
      "<td>" +
      "<input type='image'" +
      " onclick='jsphotohelp();'" +
      " class='jsphotobutton'" + 
      " src='" + jsbuttondir + "/info2.gif'" + 
      " alt='[Help]'" + 
      " title='" + jsphototranslate("Information on Photo Gallery") + "' />" +
      "</td>\n");
  }

  /* close */
  if (jsphotoexitbtn) {
    document.write(
      "<td>" +
      "<input type='image'" +
      " onclick='jsphotoexit();'" +
      " class='jsphotobutton'" + 
      " src='" + jsbuttondir + "/door_exit.gif'" + 
      " alt='[Close]'" + 
      " title='" + jsphototranslate("Exit Photo Gallery") + "' />" +
      "</td>\n");
  }

  document.write(
      "</tr>\n"+
      "</table>\n");

  /* index page */
  document.write(
      "<div id='jsphototab1' class='jsphotovisible'>\n" + 
      "<table"+
      " border='0'" +
      " class='jsphotothumbs'" +
      " cellpadding='0'" +
      " cellspacing='0'>\n" +
      "<tbody>\n");

  var img = 0;
  for (var y = 0; y < jsphotothumbsy; y++) {
    document.write("<tr valign='top'>");
    for (var x = 0; x < jsphotothumbsx; x++, img++) {
      document.write("<td class='jsphotothumbbox'>\n");

      /* thumbnail image */
      var base = "jsphotothumb_" + x + "_" + y
      document.write(
          "<a href=''"+
          " id='" + base + "_link' "+
          " class='jsphotolink'" +
          " onclick='return jsphotothumbselect("+x+","+y+");'>\n" +
          "<img class='jsphotothumbnail'" +
          " id='" + base + "_img'" +
          " title='" + jsphototranslate("Enlarge picture") + "'" +
          " alt='Image:' />" +
          "</a><br />\n");

      /* picture title */
      document.write(
          "<span id='" + base + "_title'" +
          " class='jsphotothumbtitle'>" +
          "</span>\n");

      document.write("</td>\n");
    }
    document.write("</tr>\n");
  }
  document.write(
      "</tbody>\n" +
      "</table>\n" +
      "</div>\n");

  /* enlarged image page */
  document.write(
      "<div id='jsphototab2' class='jsphotohidden'>\n" +
      "<div class='jsphotosingle'>\n" +
      "<a id='jsphotolargelink'" +
      " onclick='return jsphotoascent();'" +
      " href='" + location.href + "'" +
      " class='jsphotovisited'>" +
//      "<img src=''" +
//      " id='jsphotolarge'" +
//      " class='jsphotolarge'" +
//      " title='" + jsphototranslate("Return to index") + "'" +
//      " alt='Image:' />" +
      "</a>" +
      "</div>\n" + 
      "</div>\n");

  /* search page */
  document.write(
      "<div id='jsphototab3' class='jsphotohidden'>\n" +
      "<div class='jsphotosearch'>\n" +
      "<form>\n" +
      "<table>\n");

  /* print a list of pre-defined keywords, if provided by the user */
  if (jsphotokeywords.length > 0) {
    document.write(
      "<tr>\n" +
      "<td class='jsphotolabel'>" + jsphototranslate("Keywords") + ":</td>\n" +
      "<td>\n" +
      "<table class='jsphotokeywords'>\n");

    var n = Math.floor((jsphotokeywords.length + jsphotokeycols - 1) / jsphotokeycols);
    var row = 0;
    while (row < n) {
      document.write("<tr>\n");
      for (var i = 0; i < jsphotokeycols; ++i) {
        var j = i*n+row;
        if (j < jsphotokeywords.length) {
          document.write(
              "<td class='jsphotokeyword'>" +
              "<label>" +
              "<input type='checkbox'" +
              " name='check" + j + "'" +
              " value='" + escape(jsphotokeywords[j]) + "' />" +
              jsphotokeywords[j] +
              "</label></td>\n");
        }
      }
      document.write("</tr>\n");
      ++row;
    }

    document.write(
      "</table>\n" +
      "</td>\n" +
      "</tr>\n");
  }

  /* free text search */
  document.write(
      "<tr>\n" +
      "<td class='jsphotolabel'>" + jsphototranslate("Title") + ":</td>\n"+
      "<td>\n" +
      "<input type='text' size='60' name='search' /></td>\n" +
      "</tr>\n");

  /* list of authors */
  document.write(
      "<tr>\n" +
      "<td class='jsphotolabel'>"+jsphototranslate("Photographer")+":</td>\n" +
      "<td>\n" +
      "<select name='author'>\n" +
      "<option value='-'>" + jsphototranslate("Anybody") + "</option>\n");

  var authors = jsphotoauthors();
  for (var i = 0; i < authors.length; ++i) {
    document.write("<option>" + authors[i] + "</option>\n");
  }

  document.write(
      "</select>\n" +
      "</td>\n" +
      "</tr>\n");

  /* date range */
  document.write(
      "<tr>\n" +
      "<td  class='jsphotolabel'>" + jsphototranslate("Date") + ":</td>\n" +
      "<td>" +
      "<input type='text' size='10' name='from' /> - " +
      "<input type='text' size='10' name='to' /></td>\n" +
      "</tr>\n");

  /* end of search elements */
  document.write(
      "</table>\n");

  /* search buttons */
  document.write(
      "<div class='jsphotosearchbtns'>\n" +
      "<input type='submit' value='" + jsphototranslate("Search") + "' />\n" +
      "<input type='reset' value='" + jsphototranslate("Clear") + "' />\n" +
      "</div>\n" +

      "</form>\n" +
      "</div>\n" + 
      "</div>\n");

  /* end of jsphoto container */
  document.write("</div>\n");

  /* debug functions */
  if (jsphotodebug) {
    document.write(
        "Debug: " +
        "<a href='#' onclick='return jsphotosource();'>[Source]</a>\n" +
        "<a href='#' onclick='return jsphotocookies();'>[Cookies]</a>\n");
  }

  /* load gallery to display */
  jsphotoindex();
}



/****** index page ***********************************************************/

/*
 * Display the index page of the currently selected sub-album.
 */
function jsphotoindex() {
  /* display index page */
  jsphotosettab(1);

  /* compute the index of first and last visible image */
  var thumbs = jsphotothumbsx * jsphotothumbsy;
  var i = Math.floor(jsphotocurimg / thumbs) * thumbs;

  /* load the thumbnails to the page */
  for (y = 0; y < jsphotothumbsy; y++) {
    for (x = 0; x < jsphotothumbsx; x++, i++) {
      jsphotosetthumb(x, y, i);
    }
  }

  /* update title bar */
  jsphotoupdatetitle();
}


/*
 * Set thumbnail image at position (x,y).  The functions loads images as
 * needed, sets titles and hides unused images.
 */
function jsphotosetthumb(x, y, i) {
  var branch = jsphotobranch();

  /* compute the number of image/sub-galleries in this branch */
  var n = branch["photos"].length;

  /* find title and link elements */
  var base = "jsphotothumb_" + x + "_" + y;
  var f2 = document.getElementById(base + "_title");
  var f3 = document.getElementById(base + "_link");

  /* does the image exist? */
  if (i < n) {

    /* url of thumbnail image to load */
    var url = jsphotothumburl(i);

    /* load thumbnail to cache & screen */
    jsphotoload(url, function() { jsphotosizethumb(x, y, url); });

    /* image title */
    if (branch["photos"][i]["photos"]) {
      /* title and number of images */
      var count = jsphotoimgcount(branch["photos"][i]);
      f2.innerHTML = jsphototitle(i) + 
          " (" + count + " " + jsphototranslate("images") + ")";
    } else {
      f2.innerHTML = jsphototitle(i);
    }

     f3.href = jsphotoimageurl(i);

    /* display thumbnail */
    f3.className = "jsphotolink";

  } else {

    /* the ith image is not used  */
    f2.innerHTML = "";
    f3.className = "jsphotohidden";

  }
}


/*
 * Set size of thumbnail image.  The function is typically invoked after 
 * loading an image.
 */
function jsphotosizethumb(x, y, url) {
  if (jsphotoimages[url] != null) {

    /* get the native width and height of the image */
    var w = jsphotoimages[url]["image"].width;
    var h = jsphotoimages[url]["image"].height;

    /* compute the new width and height of the image */
    if (h  &&  w) {
      var d1 = w / h;
      var d2 = jsphotothumbsmaxw / jsphotothumbsmaxh;
      if (d1 < d2) {
        h = jsphotothumbsmaxh;
        w = Math.floor(h * d1);
      } else {
        w = jsphotothumbsmaxw;
        h = Math.floor(w / d1);
      }

      /* set new image size */
      var f1 = document.getElementById("jsphotothumb_" + x + "_" + y + "_img");
      if (f1) {
        f1.width  = w;
        f1.height = h;
        f1.src = url;
      }
    }
  }
}


/*
 * Select thumbnail at (x,y) and view the corresponding image/gallery.
 * Invoked by the user clicking on a thumbnail.
 */
function jsphotothumbselect(x,y) {
  var branch = jsphotobranch();

  /* compute the number of image/sub-galleries in this branch */
  var n = branch["photos"].length;

  /* compute the number of selected image/gallery */
  var thumbs = jsphotothumbsx * jsphotothumbsy;
  var i = Math.floor(jsphotocurimg / thumbs) * thumbs + x + y * jsphotothumbsx;

  /* is there an image below the cursor? */
  if (i < n) {
    jsphotodescent(i);
  }

  /* to prevent browser from following a link */
  return false;
}



/****** single image page ****************************************************/

/*
 * Display ith image in enlarged view.  If the image is not already in memory,
 * then load the image in background before displaying it.
 */
function jsphotosetimage(i) {
  /* remember the current image */
  jsphotocurimg = i;

  /* switch to single image page */
  jsphotosettab(2);

  /* url of the image to display */
  var url = jsphotoimageurl(i);

  /*
   * Display partially loaded large image by re-writing the img tag inside
   * the link.  If the jpeg image has been prepared for progressive loading,
   * then the image will display first as rough preview version that 
   * continues sharpen as the loading progresses.
   *
   * Note that it is imperative that we re-write the img tag, as browsers
   * seem to display only the first image with progressive loading
   * enabled.  Changing just the src property would be easier, but then the
   * progressive loading would be disabled for all but the very first image.
   */
  var f1 = document.getElementById("jsphotolargelink");
  if (f1) {
    f1.innerHTML = 
      "<img src='" + url + "'" +
      " id='jsphotolarge'" +
      " class='jsphotolarge'" +
      " title='" + jsphototranslate("Return to index") + "'" +
      " alt='Image:' />";
  }

  /* load image to cache & screen */
  jsphotoload(url, function() { jsphotosizeimage(); });

  /* update title bar */
  jsphotoupdatetitle();
}


/*
 * Prepare the current image for display.  If the scaling is enabled, 
 * then resize the image to fill the available space.  Note that the 
 * image size is not known until the image has been completely loaded.  Thus,
 * if progressive loading is used, the image will first display in its
 * original size and it is only after the complete image has been loaded that
 * the image is scaled.
 * 
 * This function is called from the on-load handler when the browser
 * has finished loading an image.  Thus, image size is known at this time.
 */
function jsphotosizeimage() {
  /* url of the currently displaying image */
  var url = jsphotoimageurl(jsphotocurimg);

  if (jsphotoimages[url] != null) {

    /* the image element in page */
    var f1 = document.getElementById("jsphotolarge");

    /* scale the image to fit the view */
    if (jsphotoscaling) {
      /* get the native width and height of the image */
      var w = jsphotoimages[url]["image"].width;
      var h = jsphotoimages[url]["image"].height;

      /* compute the new width and height of the image */
      if (h  &&  w) {
        var d1 = w / h;
        var d2 = jsphotomaxw / jsphotomaxh;
        if (d1 < d2) {
          h = jsphotomaxh;
          w = Math.floor(h * d1);
        } else {
          w = jsphotomaxw;
          h = Math.floor(w / d1);
        }

        /* set new image size */
        if (f1) {
          f1.width  = w;
          f1.height = h;
        }
      }
    }

    /* display the loaded image */
    if (f1) {
      f1.src = url;
    }

    /* 
     * Set the url link.  The link url is not really used for anything, but
     * this is sometimes useful for debugging.  The link will be activated
     * by the javascript onclick handler so the url has no real use.
     */
    f1 = document.getElementById("jsphotolargelink");
    if (f1) {
      f1.href = url;
    }

    /* continue with pre-loading next image */
    jsphotopreload();

  } else {
    alert("internal error: image " + url + " not loaded");
  }
}


/*
 * Start pre-loading next image in background.  Called from on-load handler
 * once an image has been loaded.
 */
function jsphotopreload() {
  if (jsphotocurtab == 2  &&  jsphotopreloadenable) {
    /* viewing enlarged image */
    var branch = jsphotobranch();

    /* get url of image to preload */
    var preloadurl;
    if (jsphotocurimg < branch["photos"].length-1) {
      var url = jsphotoimageurl(jsphotocurimg+1);
      if (!jsphotoimages[url]) {
        preloadurl = url;
      }
    }
    if (!preloadurl  &&  jsphotocurimg < branch["photos"].length-2) {
      var url = jsphotoimageurl(jsphotocurimg+2);
      if (!jsphotoimages[url]) {
        preloadurl = url;
      }
    }

    if (preloadurl) {
      jsphotoload(preloadurl, jsphotopreload);
    }
  }
}



/****** search page **********************************************************/

/*
 * Activate search page.
 */ 
function jsphotosearch() {
  jsphotosettab(3);
  jsphotoupdatetitle();
}


/*
 * Returns a list of authors in alphabetical order.  The list of authors
 * is collected from the photo album.
 *
 * Note that it is possible that the function returns authors for which no
 * photographs exist.  This happens at least when the author of a sub-album 
 * has no photographs in that album.  That is, each photograph in the 
 * sub-album has another author.
 */
function jsphotoauthors() {
  /* collect authors starting from the root of the tree */
  var authors = new Array();
  jsphotoauthors2(authors, jsphototree[0]);
  return authors;
}

/*
 * Extracts names of all authors from given branch to array res.
 */
function jsphotoauthors2(res,branch) {

  /* author of sub-album */
 jsphotoaddauthor(res, branch["author"]);

  /* check all sub-albums and photos */
  var n = branch["photos"].length;
  for (var i = 0; i < n; i++) {
    if (branch["photos"][i]) {
      if (branch["photos"][i]["photos"]) {
        /* descent to sub-album */
        jsphotoauthors2(res, branch["photos"][i]);
      } else {
        /* get author of an image */
        jsphotoaddauthor(res, branch["photos"][i][3]);
        jsphotoaddauthor(res, branch["photos"][i]["author"]);
      }
    } else {
      /* 
       * Cannot access ith photo in this branch.  This is usually due to
       * missing commas in the array definition.
       */
      alert("internal error: invalid photo " + (i+1) + " in album " + branch["title"]);
    }
  }
}


/*
 * Appends author to array res while maintaining the array in alphabetical
 * order.
 */
function jsphotoaddauthor(res, author) {
  if (author) {
    /* search author from array res which is sorted alphabetically */
    var i = 0;
    while (i < res.length) {
      if (res[i].toLowerCase() == author.toLowerCase()) {
        /* author already added */
        return;
      }
      if (res[i].toLowerCase() > author.toLowerCase()) {
        /* author does not exist, add here */
        res.splice(i, 0, author);
        return;
      }
     ++i;
     }
  
    /* add as last */
    res.splice(i, 0, author);
  }
}


/****** functions that affect all pages **************************************/

/*
 * Make ith tab visible.
 */
function jsphotosettab(i) {
  var j, f;
  var pages = 3;

  if (jsphotocurtab != i) {
    /* hide/show tabs */
    for (j = 1; j <= pages; j++) {
      f = document.getElementById("jsphototab" + j);
      if (i != j) {
        f.className = "jsphotohidden";
      } else {
        f.className = "jsphotovisible";
      }
    }

    /* scroll the page to view */
    jsphotoscroll();

    /* remember the current page */
    jsphotocurtab = i;
  }
}



/*
 * Update title bar to reflect the current image and tab.  That is, load the
 * page title and enable/disable buttons.
 */
function jsphotoupdatetitle() {
  var branch = jsphotobranch();

  /* 
   * Compute the index of first and last visible image.  Moreover, compute
   * the number of images in branch (n).
   */
  var first, last, n
  switch (jsphotocurtab) {
  case 1:
    var thumbs = jsphotothumbsx * jsphotothumbsy;
    first = Math.floor(jsphotocurimg / thumbs) * thumbs;
    last = first + thumbs;
    n = branch["photos"].length;
    break;

  case 2:
    first = jsphotocurimg;
    last = jsphotocurimg + 1;
    n = branch["photos"].length;
    break;

  case 3:
    /* no images in page 3 (the search page) */
    first = 0;
    last = 0;
    n = 0;
    break;
  }

  /* generate page title */
  var title = "";
  switch (jsphotocurtab) {
  case 1:
    /* 
     * Determine if the current branch contains sub-albums or images, and
     * provide title "Series" or "Pictures" accordingly.
     */
    if (n > 0) {
      if (branch["photos"][0]) {
        /* first image is valid */
        if (branch["photos"][0]["photos"]) {
          /* album containing sub-albums */
          title = jsphototranslate("Series");
        } else {
          /* an album containing individual images */
          title = jsphototranslate("Pictures");
        }
      }
    }

    /* append page range such as "1-10/25" */
    if (n > 0) {
      title = title + " " + (first+1) + "-" + 
          (first+thumbs>n ? n : first+thumbs) 
          + "/" + n;
    }

    /* append album title provided by the album creator */
    if (branch["title"]) {
      if (title != "") {
        title = title + ": ";
      }
      title = title + branch["title"];
    }
    break;

  case 2:
    /* prepare the page label such as "Picture 5/25" */
    title = jsphototranslate("Picture") + " " + (jsphotocurimg+1) + "/" + n;

    /* append image title provided by the album creator */
    var imgtitle = jsphototitle(jsphotocurimg);
    if (imgtitle != "") {
      /* has a custom title */
      title = title + ": " + imgtitle;
    } else {
      /* no custom title */
      if (branch["title"]) {
        /* use the name of the image album */
        title = title + ": " + branch["title"];
      } else {
        /* image gallery has no name either */
        /*NOP*/;
      }
    }
    break;

  case 3:
    title = jsphototranslate("Search Photos");
    break;
  }

  /* set the title */
  var f = document.getElementById("jsphototitle");
  if (f) {
    f.value = title;
  }

  /* enable/disable the up button */
  var enable;
  switch (jsphotocurtab) {
  case 1:
    if (jsphotocuralbum.length > 0) {
      enable = true;
    } else {
      /* at root branch */
      enable = false;
    }
    break;

  case 2:
    /* return to index */
    enable = true;
    break;

  case 3:
    /* return to root */
    enable = true;
    break;
  }
  f = document.getElementById("jsphototop");
  if (f) {
    if (enable) {
      f.disabled = false;
      f.src = jsbuttondir + "/up.gif";
    } else {
      f.disabled = true;
      f.src = jsbuttondir + "/up_inact.gif";
    }
  }

  /* enable/disable forward button */
  f = document.getElementById("jsphotonext");
  if (f) {
    if (last >= n) {
      f.disabled = true;
      f.src = jsbuttondir + "/right_inact.gif";
    } else {
      f.disabled = false;
      f.src = jsbuttondir + "/right.gif";
    }
  }

  /* enable/disable back button */
  f = document.getElementById("jsphotoprev");
  if (f) {
    if (first == 0) {
      f.disabled = true;
      f.src = jsbuttondir + "/left_inact.gif";
    } else {
      f.disabled = false;
      f.src = jsbuttondir + "/left.gif";
    }
  }


}



/****** scrolling ************************************************************/

/*
 * Scroll the browser view to show the full image/index page.
 */
function jsphotoscroll() {
  window.scrollTo(0,jsphotopos());
}


/*
 * Computes the y-coordinate of the jsphoto title element.
 *
 * The function is based on the findPos() function by Peter-Paul Koch.  The
 * original code can be found at http://www.quirksmode.org/js/findpos.html
 */
function jsphotopos() {
  var curtop = 0;

  /* get element those position is to be computed */
  var f = document.getElementById("jsphotoalbum");
  if (f  &&  f.offsetParent) {

    /* traverse parent elements up until the document body */
    curtop = f.offsetTop
    while (f = f.offsetParent) {
      curtop += f.offsetTop
    }

  }
  return curtop;
}



/****** branches and image tree **********************************************/

/*
 * Returns the currently selected photo album as an array.
 */
function jsphotobranch() {
  var branch = jsphototree[0];
  var n = jsphotocuralbum.length;
  for (var i = 0; i < n; i++) {
    if (branch["photos"][jsphotocuralbum[i]]["photos"]) {
      /* descent to sub-album */
      branch = branch["photos"][jsphotocuralbum[i]];
    } else {
      /* an image found */
      alert("internal error: invalid album index");
      return "";
    }
  }
  return branch;
}


/*
 * Returns the complete URL address of ith thumbnail image in the current
 * branch.
 */
function jsphotothumburl(i) {
  var branch = jsphotobranch();
  var base = jsphotoimagebase(i);

  if (branch["photos"][i]["thumbnail"]) {
    /* photos is an associative array  */
    return jsphotoappend(base, branch["photos"][i]["thumbnail"]);
  } else if (branch["photos"][i][0]) {
    /* photos is a plain two-dimensional array */
    return jsphotoappend(base, branch["photos"][i][0]);
  } else {
    alert("internal error: missing thumbnail url");
    return "";
  }
}


/*
 * Returns the complete URL address of ith image in the current branch.
 */
function jsphotoimageurl(i) {
  var branch = jsphotobranch();
  var base = jsphotoimagebase(i);

  if (branch["photos"][i]["image"]) {
    /* photos is an associative array  */
    return jsphotoappend(base, branch["photos"][i]["image"]);
  } else if (branch["photos"][i][1]) {
    /* photos is a plain two-dimensional array */
    return jsphotoappend(base, branch["photos"][i][1]);
  } else {
    /* no URL specified.  This is perfectly normal for sub-albums. */
    return "#";
  }
}


/*
 * Get title of ith image or sub-album in the current branch.
 */
function jsphototitle(i) {
  var branch = jsphotobranch();

  if (branch["photos"][i]["title"]) {
    /* title attribute specified */
    return branch["photos"][i]["title"];
  } else if (branch["photos"][i][2]) {
    /* photos is a plain two-dimensional array */
    return branch["photos"][i][2];
  } else {
    /* no title specified */
    return "";
  }
}


/*
 * Returns the base directory of ith image.  If the image/album has no
 * explicit base directory, then the base directory of the parent album
 * will be returned.
 */
function jsphotoimagebase(i) {
  var branch = jsphototree[0];

  /* base directory of the root node */
  if (branch["base"]) {
      base = jsphotoappend(".", branch["base"]);
  } else {
    base = ".";
  }

  /* accumulate the base directory from the root to the current album */
  var n = jsphotocuralbum.length;
  for (var j = 0; j < n; j++) {
    if (branch["photos"][jsphotocuralbum[j]]["photos"]) {
      /* descent to sub-album */
      branch = branch["photos"][jsphotocuralbum[j]];

      /* append base directory of the new branch */
      if (branch["base"]) {
        base = jsphotoappend(base, branch["base"]);
      }
    } else {
      /* an image found instead of sub-album */
      alert("internal error: invalid album index");
      return "";
    }
  }

  /* 
   * Inspect the base directory of the sub-album.  This is of primary
   * importance when displaying thumbnails that refer to sub-albums, but
   * this also means that individual images could possibly have base 
   * attributes.
   */
  if (branch["photos"][i]["base"]) {
    base = jsphotoappend(base, branch["photos"][i]["base"]);
  }

  return base;
}


/*
 * Returns the total number of images in the sub-album.  Computes the
 * number of images in descendant albums recursively.
 */
function jsphotoimgcount(branch) {
  var sum = 0;

  var n = branch["photos"].length;
  for (var j = 0; j < n; ++j) {
    if (branch["photos"][j]) {
      if (branch["photos"][j]["photos"]) {
        /* recursively count images in sub-album */
        sum += jsphotoimgcount(branch["photos"][j]);
      } else {
        /* regular image */
        sum++;
      }
    } else {
      /* 
       * Cannot access ith image in this branch.  This is usually due to
       * missing commas in the array definition.  The situation is really 
       * an error, but the situation should already been reported when
       * initializing the album.  Thus, the error report may be omitted.
       */
      /*NOP*/;
    }
  }

  return sum;
}



/****** traversal ***********************************************************/

/*
 * Descent to ith image or sub-album.  Invoked when the user clicks on a 
 * thumbnail image.
 */
function jsphotodescent(i) {
  var branch = jsphotobranch();

  if (branch["photos"][i]["photos"]) {

    /* descent to sub-gallery */
    jsphotocuralbum[jsphotocuralbum.length] = i;
    jsphotoindex();

  } else {

    /* display ith image */
    jsphotosetimage(i);

  }
}


/*
 * Ascent to parent sub-album.  Invoked when the user clicks on the up 
 * button or the large image.
 */
function jsphotoascent() {
  if (jsphotocurtab == 1  &&  jsphotocuralbum.length > 0) {
    /* ascent to parent album */
    jsphotocurimg = 0;
    jsphotocuralbum.pop();
  } else if (jsphotocurtab == 3) {
    /* return to root */
    jsphotocuralbum = new Array();
  }

  /* switch to the index page */
  jsphotoindex();

  /* return false to prevent browser from following a link */
  return false;
}


/* 
 * Show the very next image/album.
 */
function jsphotonext() {
  var branch = jsphotobranch();

  switch (jsphotocurtab) {
  case 1:
    /* compute the index of the first image in page */
    var thumbs = jsphotothumbsx * jsphotothumbsy;
    var i = Math.floor(jsphotocurimg / thumbs) * thumbs;

    if (i + thumbs < branch["photos"].length) {
      jsphotocurimg = i + thumbs;
      jsphotoindex();
    }
    break;

  case 2:
    if (jsphotocurimg < branch["photos"].length-1) {
      jsphotosetimage(jsphotocurimg + 1);
    }
    break;

  case 3:
    break;
  }
}


/* 
 * Show the previous image or the index page.
 */
function jsphotoprev() {
  switch (jsphotocurtab) {
  case 1:
    /* compute the number of first image in page */
    var thumbs = jsphotothumbsx * jsphotothumbsy;
    var i = Math.floor(jsphotocurimg / thumbs) * thumbs;

    /* previous page */
    if (i >= thumbs) {
      jsphotocurimg = i - thumbs;
      jsphotoindex();
    }
    break;

  case 2:
    /* previous image */
    if (jsphotocurimg > 0) {
      jsphotosetimage(jsphotocurimg - 1);
    }
    break;

  case 3:
    /* return to index */
    jsphotoascent();
    break;
  }
}



/****** image cache **********************************************************/

/*
 * Load image from internet.  Calls the given onload handler when the image
 * is is ready.
 *
 * BEWARE: an image file may have only one active onload handler at a time.
 * Thus, if a thumbnail image is duplicated within a page, the onload
 * handler may not be invoked for all images.
 */
function jsphotoload(url, onload) {
  if (!jsphotoimages[url]) {

    /* purge least frequently image from cache */
    if (jsphotocachedimages >= jsphotocachesize) {
      /* find least used image */
      var t = new Date();
      var j = -1;
      for (var i in jsphotoimages) {
        if (jsphotoimages[i]["time"] <= t) {
          t = jsphotoimages[i]["time"];
          j = i;
        }
      }
    
      /* 
       * Erase jth image.  Note that jsphotoimages is an associative array, 
       * and, as such, function splice() cannot be used to erase the
       * element.  Moreover, the array length operator cannot be used to
       * get the number of elements in the array, but the jsphotocachedimages
       * variable has to be used.
       */
      delete jsphotoimages[j];
      jsphotocachedimages--;
    }

    /* load new image */
    var img = new Image;
    img.onload = onload;
    img.src = url;

    /* add image to memory array */
    jsphotoimages[url] = new Array();
    jsphotoimages[url]["image"] = img;
    jsphotocachedimages++;
  } else {
    /* image already in memory */
    onload();
  }

  /* record the time when the image was last used */
  jsphotoimages[url]["time"] = new Date();
}



/****** file name and string processig ***************************************/

/*
 * Append file name fn to directory name dir and return the newly formed
 * path.  If the file name is an absolute directory name, then the file
 * name replaces the directory.
 * 
 * Examples:
 * jsphotoappend("/keikat", "19900503") => "/keikat/19900503"
 * jsphotoappend("/keikat", "/19900503") => "/19900503"
 * jsphotoappend("http://en.net/keikat", "/test") => "http://en.net/test"
 */
function jsphotoappend(dir, fn) {
  if (fn.substr(0,1) == "/") {

    /* fn represents an absolute path name, replace the existing path */
    var protocol;
    var domain;

    /* extract protocol name from dir */
    if (dir.toLowerCase().substr(0,7) == "http://") {
      protocol = "http://";
    } else if (dir.toLowerCase().substr(0,8) == "https://") {
      protocol = "https://";
    } else {
      protocol = "";
    }

    /* extract domain name from dir */
    if (protocol != "") {
      var i = dir.indexOf("/", protocol.length);
      if (i > 0) {
        domain = dir.substr(protocol.length, i-protocol.length);
      } else {
        domain = dir.substr(protocol.length);
      }
    } else {
      domain = "";
    }

    /* repace path leaving the protocol and domain name intact */
    return protocol + domain + fn;

  } else if (fn.toLowerCase().substr(0,7) == "http://"  ||
             fn.toLowerCase().substr(0,8) == "https://") {

    /* fn represents complete url address, replace existing path completely */
    return fn;

  } else {

    /* fn is a relative file name, append fn to dir */
    if (dir != "") {
      /* file name relative to directory dir */
      return dir + "/" + fn;
    } else {
      /* file name in current directory */
      return fn;
    }
  }
}



/****** internationalization *************************************************/

/*
 * Translate string i to current language using jsphotostrings array.
 */
function jsphototranslate(i) {
  if (jsphotostrings[i]) {
    /* translate to current language */
    return jsphotostrings[i];
  } else {
    /* no translation exists, use the english text */
    return i;
  }
}


/****** debug ****************************************************************/

/*
 * Debug: show the generated HTML code along with the original source.  Useful
 * for debugging and validating the javascript generated HTML code.  Taken
 * from:
 *
 *   http://www.squarefree.com/bookmarklets/webdevel.html
 *
 * The code works at least in Mozilla.
 */
function jsphotosource() {
  popup = window.open(); 
  popup.document.write('<pre>' + 
      jsphotoescape('<html>\n' + 
      document.documentElement.innerHTML + '\n</html>') + "</pre>");
  popup.document.close();

  /* prevents the browser from following links */
  return false;
}


/*
 * Debug: replace <, > and & characters in s with their HTML counterparts.
 */
function jsphotoescape(s) {
  s = s.replace(/&/g,'&amp;');
  s = s.replace(/>/g,'&gt;');
  s = s.replace(/</g,'&lt;');
  return s;
}


/*
 * Debug: displays the cookies of this page in a popup window.
 */
function jsphotocookies() {
  alert("Cookies stored by this host or domain:\n\n" + 
      document.cookie.replace(/; /g, "\n"));

  /* prevents the browser from following links */
  return false;
}
