/*
 * slideshow.js -- interactive slideshow widget with contact sheet
 *
 * Copyright 2006 (c) Randy Saldinger, randy(at)mothersruin.com
 *
 * $Id: slideshow.js,v 1.48 2006/02/25 23:27:20 randy Exp $
 */

/****************************************************************************
 * SlideShowMgr CONSTRUCTOR
 *
 * The SlideShowMgr object manages a list of slideshows, as loaded
 * from an XML file, and handles transitions in and out of the show.
 * Give it the URL to the XML definition (see below), along with the
 * element whose children will be replaced (temporarily) with the
 * slide show. If this element is the body, a full-screen effect will
 * be achieved (including adjusting to window resizes).
 *
 * The XML file should have the following structure:
 *
 * <slideshows>
 *    <slideshow name="one">
 *       <baseurl>http://example.com/images/</baseurl>
 *       <slide>
 *          <url>a.jpg</url>
 *          <width>578</width>
 *          <height>384</height>
 *          <title><![CDATA[
 *          A title, which <i>can</i> include HTML
 *          ]]></title>
 *       </slide>
 *       <slide>
 *          <!-- more -->
 *       </slide>
 *    </slideshow>
 *    <slideshow name="two">
 *       <!-- more -->
 *    </slideshow>
 * </slideshow>
 *
 * In the onload handler, construct the SlideShowMgr object. It will
 * load the XML on construction. To start one of the shows, invoke:
 *
 *    showMgr.startShow( 'one' );
 *
 * using the name attribute given in the XML slideshow element. This will
 * replace the indicated element with the slideshow, and automatically
 * start playing it. All the controls are handled directly. If the user
 * doesn't click any controls, the slideshow will be transitioned out
 * at the end; otherwise, it will be removed when the user clicks close.
 ***************************************************************************/

function SlideShowMgr( xmlURL, toggleElem, iconURL, urlTitles )
{
   this.xmlURL     = xmlURL;
   this.toggleElem = toggleElem;
   this.iconURL    = ( iconURL ? iconURL : "i/" );
   this.urlTitles  = urlTitles;

   // start loading XML
   var _this = this;
   try
   {
      AjaxUtil.loadURLInBackground( this.xmlURL,
         function( url, request )
         {
            _this.gotXML( request );
         },
         "text/xml" );
   }
   catch ( e )
   {
      // detect lack of XMLHttpRequest support
      if ( e.type == "XMLHttpRequest" )
      {
         this.unsupported = true;
         return;
      }
   }

   // precache the control images
   for ( var i = 0 ; i < SlideShow.CONTROL_IMAGES.length ; ++i )
      new Image().src = this.iconURL + SlideShow.CONTROL_IMAGES[ i ] + ".jpg";

   // insert new div between toggleElem and children
   this.container = document.createElement( "div" );
   while ( this.toggleElem.hasChildNodes() )
      this.container.appendChild( this.toggleElem.firstChild );
   this.toggleElem.appendChild( this.container );
}

SlideShowMgr.prototype.startShow = function( name )
{
   if ( this.unsupported )
      return( true );

   // install frame and then install show when transitioned in;
   // if the XML hasn't been loaded yet at transition's end,
   // we don't create the actual show until it's loaded (but we
   // always do the transition to the frame, so that it doesn't
   // look as if nothing has happened)
   this.createFrame(); 
   var _this = this;

   // if we must (IE), toggle the background properties off at
   // the midpoint of the transition; save the original so we can
   // restore it when the show closes (but this works ONLY if the
   // background properties are set via style attribute)
   this.origBackground = undefined;
   var onMid;
   if ( AjaxUtil.UserAgent.bodyVisibleOnTransparent 
        && this.toggleElem == document.body )
   {
      this.origBackground = this.toggleElem.style.background;
      onMid = function()
      {
         _this.toggleElem.style.background = SlideShow.BACKGROUND_COLOR;
      }
   }
   var onDone = function()
   {
      if ( _this.xmlDoc )
         _this.installShow( name );
      else
         _this.waitingOnShow = name;
   }
   AjaxUtil.transitionFX( this.toggleElem, this.container,
                          this.currentFrame,
                          "Opacity", SlideShow.TRANSITION_CYCLE,
                          onMid, onDone );

   return( false ); // event handled
}

SlideShowMgr.prototype.closeCurrentShow = function()
{
   if ( ! this.currentFrame ) 
      return;
   var _this = this;

   // put back the background if we had to hide it on open
   var onMid;
   if ( this.origBackground )
   {
      onMid = function()
      {
         _this.toggleElem.style.background = _this.origBackground;
      }
   }
   AjaxUtil.transitionFX( this.toggleElem, this.currentFrame,
                          this.container,
                          "Opacity", SlideShow.TRANSITION_CYCLE,
                          onMid );
   this.currentFrame = undefined;
   this.currentShow  = undefined;
}

SlideShowMgr.prototype.buildImageInfosFromXML = function( xmlNode )
{
   var infos = new Array();

   var baseURL = textForKey( xmlNode, "baseurl" );
   if ( baseURL.charAt( baseURL.length - 1 ) != "/" )
      baseURL += "/";

   var slides = xmlNode.getElementsByTagName( "slide" );
   for ( var i = 0 ; i < slides.length ; ++i )
   {
      var slide = slides[ i ];
      var url    = baseURL + textForKey( slide, "url" );
      var title  = textForKey( slide, "title" );
      if ( this.urlTitles && ( ! title || ! title.length ) )
         title = "Picture from " + url;
      var width  = parseInt( textForKey( slide, "width" ) );
      var height = parseInt( textForKey( slide, "height" ) );
      infos.push( new ImageInfo( url, width, height, title ) );
   }

   function textForKey( slide, key )
   {
      var elem = slide.getElementsByTagName( key );
      if ( elem.length != 1 )
         return( undefined );
      return( AjaxUtil.XML.getTextContent( elem[ 0 ] ) );
   }

   return( infos );
}

SlideShowMgr.prototype.gotXML = function( request )
{
   this.xmlDoc = request.responseXML;
   if ( ! this.xmlDoc )
      this.xmlDoc = true;

   if ( ! this.waitingOnShow )
      return;

   this.installShow( this.waitingOnShow );
}

SlideShowMgr.prototype.createFrame = function()
{
   // create div to cover page and transition in; as a special
   // case, if the toggleElem is the body element, make it large
   // enough to fill the entire window (not just the element height)
   var _this = this;
   this.currentFrame = document.createElement( "div" );
   this.currentFrame.style.width = "100%";
   this.currentFrame.style.height = this.toggleElem.offsetHeight + "px";
   this.currentFrame.style.backgroundColor = SlideShow.BACKGROUND_COLOR;
   this.currentFrame.style.margin = "0";
   if ( this.toggleElem == document.body )
   {
      // TODO: deal with fact that body margin/padding may not be zero?
      var fullScreenResize = function()
      {
         var winHeight = window.innerHeight;
         if ( ! winHeight )
            winHeight = AjaxUtil.UserAgent.getInnerHeight();
         _this.currentFrame.style.height = winHeight + "px";
      }
      fullScreenResize();
      window.onresize = function()
      {
         if ( _this.currentShow )
         {
            fullScreenResize();
            _this.currentShow.resize();
         }
      }
   }
}

SlideShowMgr.prototype.installShow = function( name )
{
   var showData = getShowByName( name, this.xmlDoc );
   if ( ! showData )
   {
      alert( "The slide show (" + name + ") could not be loaded." );
      this.closeCurrentShow();
      return;
   }

   // build ImageInfos
   var imageInfos = this.buildImageInfosFromXML( showData );

   // install slideshow and set to autoplay
   var _this = this;
   var closeIt = function()
   {
      _this.closeCurrentShow();
   }
   this.currentShow = new SlideShow( this.currentFrame,
                                     imageInfos, true, true, closeIt,
                                     this.iconURL );
   this.currentShow.autoPlay( closeIt );

   function getShowByName( name, xmlDoc )
   {
      if ( xmlDoc === true )
         return( undefined ); // load failure
      var shows = xmlDoc.getElementsByTagName( "slideshow" );
      for ( var i = 0 ; i < shows.length ; ++i )
      {
         if ( shows[ i ].getAttribute( "name" ) == name )
            return( shows[ i ] );
      }
      return( undefined ); // no matching name
   }
}


/****************************************************************************
 * ImageInfo CONSTRUCTOR
 *
 * Each image to be included in the slideshow is represented by an
 * ImageInfo object. An array of this objects is used to construct
 * the SlideShow object. Each ImageInfo contains the URL of the image,
 * along with the image width and height (these are specified rather
 * than calculated, so that they are known before the image is loaded).
 ***************************************************************************/

// TODO: allow optional thumbURL, to be loaded for thumbnail if
// provided (if not, load the main one)

function ImageInfo( url, width, height, title )
{
   this.url    = url;
   this.width  = width;
   this.height = height;
   this.title  = ( title && title.length ? title : "" );
}

/****************************************************************************
 * ImageInfo PRIVATE METHODS
 ***************************************************************************/

ImageInfo.prototype.getImageForFrame = function( frameWidth,
                                                 frameHeight,
                                                 onLoad )
{
   return( this.getImageOfType( "image", frameWidth, frameHeight, onLoad ) );
}

ImageInfo.prototype.getImageForThumbnail = function( frameWidth,
                                                     frameHeight,
                                                     onLoad )
{
   return( this.getImageOfType( "thumb", frameWidth, frameHeight, onLoad ) );
}

ImageInfo.prototype.getImageOfType = function( type,
                                               frameWidth,
                                               frameHeight,
                                               onLoad )
{
   // check for previous load that has been completed
   if ( this[ type + "DidLoad" ] )
   {
      // if already loaded, but frame change, resize existing image
      if ( frameWidth && frameHeight
           && ( this[ type + "FrameWidth" ] != frameWidth
                || this[ type + "FrameHeight" ] != frameHeight ) )
      {
         this.adjustImageToFrame( this[ type ], frameWidth, frameHeight );
         this[ type + "FrameWidth" ]  = frameWidth;
         this[ type + "FrameHeight" ] = frameHeight;
      }
      // dispatch directly to handler if any
      if ( onLoad )
         onLoad( this[ type ] );
      return( this[ type ] );
   }

   // check for a previous load that is still in progress
   if ( this[ type ] )
   {
      // add the handler to the list to be invoked when load done
      if ( onLoad )
         this[ type + "OnLoad" ].push( onLoad );
      return( this[ type ] );
   }

   // otherwise, create IMG element and start it loading
   this[ type ] = document.createElement( "img" );

   // use the given frame and the image size to calculate the
   // proportion-appropriate element size that will fit inside
   // the frame (shrinking if needed, but never growing the image),
   // along with the necessary padding on each side to center the
   // image and make overall dimensions match the desired frame
   this.adjustImageToFrame( this[ type ], frameWidth, frameHeight );

   // store frame size so we can detect changes
   this[ type + "FrameWidth" ]  = frameWidth;
   this[ type + "FrameHeight" ] = frameHeight;

   // store the onLoad handler in a list: we do this so that
   // subsequent requests before the load is done can also have
   // their onLoad handlers invoked 
   this[ type + "OnLoad" ] = new Array();
   if ( onLoad )
      this[ type + "OnLoad" ].push( onLoad );

   // use an Image object to trigger a load, because it has an onload
   var img = new Image();
   var _this = this;
   img.onload = function()
   {
      // now that it's loaded, link the real IMG element to the URL
      // (we wait to do this so we don't get broken-looking images)
      _this[ type ].src = _this.url;
      // mark as loaded and dispatch to handler(s)
      _this[ type + "DidLoad" ] = true;
      for ( var i = 0 ; i < _this[ type + "OnLoad" ].length ; ++i )
         _this[ type + "OnLoad" ][ i ]( _this[ type ] );
   };
   img.src = this.url; // don't do this until onload is installed!

   return( this[ type ] );
}

ImageInfo.prototype.adjustImageToFrame = function( imgElem,
                                                   frameWidth,
                                                   frameHeight )
{
   var finalWidth  = this.width;
   var finalHeight = this.height;

   // decide if we have to resize the image (only downwards)
   if ( finalWidth > frameWidth || finalHeight > frameHeight )
   {
     // always scale the image proportionally
     var ratio = this.width / this.height;

     // if width is too large, shrink that
     if ( finalWidth > frameWidth )
     {
        finalWidth = frameWidth;
        finalHeight = finalWidth / ratio;
     }
     // and then if height is (still) too large, shrink that
     if ( finalHeight > frameHeight )
     {
        finalHeight = frameHeight;
        finalWidth = finalHeight * ratio;
     }

     // make sure dimensions are whole numbers (truncated)
     finalWidth  = Math.round( finalWidth - 0.5 );
     finalHeight = Math.round( finalHeight - 0.5 );
   }

   // determine padding needed on each side to bring total element
   // to the exact dimensions of the frame (to the border edge, exclusive)
   var padTop    = 0;
   var padRight  = 0;
   var padBottom = 0;
   var padLeft   = 0;
   if ( finalWidth < frameWidth )
   {
      padLeft = Math.round( ( frameWidth - finalWidth ) / 2 - 0.5 )
      padRight = frameWidth - finalWidth - padLeft;
   }
   if ( finalHeight < frameHeight )
   {
      padTop    = Math.round( ( frameHeight - finalHeight ) / 2 - 0.5 )
      padBottom = frameHeight - finalHeight - padTop;
   }

   // finally set what we computed
   imgElem.width  = finalWidth;
   imgElem.height = finalHeight;
   imgElem.style.padding = padTop + "px " + padRight + "px "
                           + padBottom + "px " + padLeft + "px";
   if ( AjaxUtil.UserAgent.imgResizeRequiresCSSProps )
   {
      imgElem.style.width  = finalWidth + "px";
      imgElem.style.height = finalHeight + "px";
   }
}

/****************************************************************************
 * SlideShow CONSTRUCTOR
 *
 * Given an array of ImageInfo objects and a container element, creates
 * a slideshow inside the container with the desired features. Note that
 * the slideshow sizes its panes according to the offsetWidth and
 * offsetWidth of the parentElem, so the element should already be 
 * installed in the DOM when the constructor is called. If the size of
 * parentElem changes, call resize() to adjust the slideshow.
 *
 * If controls are enabled, they will be displayed inside the slideshow
 * element, and managed by the SlideShow object directly. If an onClose
 * handler is given, a close button will be shown and the handler invoked
 * when the button is clicked. By default, the button icons are found
 * in "i/" (a relative URL), but a different directory URL can be specified
 * via iconURL (it should end with a slash).
 ***************************************************************************/

function SlideShow( parentElem,
                    imageList,
                    showTitles,
                    showControls,
                    onClose,
                    iconURL )
{
   this.parentElem = parentElem;
   this.imageList  = imageList;

   // create new div to act as the frame
   this.slideFrame = document.createElement( "div" );
   this.parentElem.appendChild( this.slideFrame );

   // if enabled, create an area for the titles
   if ( showTitles )
   {
      this.titleFrame = document.createElement( "div" );
      this.parentElem.appendChild( this.titleFrame );
   }

   // if enabled create an area for the controls
   if ( showControls )
   {
      this.controlFrame = document.createElement( "div" );
      this.parentElem.appendChild( this.controlFrame );
      this.iconURL = ( iconURL ? iconURL : "i/" );
      this.onClose = onClose;
   }

   // adjust sizes and formatting per parent element size
   this.resizeToParent();

   // create controls (but only after resizing)
   if ( showControls )
      this.createControls();

   // start out showing no image
   this.currentImage = -1;
}

/****************************************************************************
 * SlideShow CONSTANTS
 ***************************************************************************/

SlideShow.FRAME_PADDING    = 25;
SlideShow.SHOW_INTERVAL    = 5000;
SlideShow.CONTROL_HEIGHT   = 85;
SlideShow.TITLE_HEIGHT     = 75;
SlideShow.TRANSITION_CYCLE = 750;

SlideShow.BACKGROUND_COLOR            = "black";
SlideShow.CONTACT_BORDER_COLOR        = "black";
SlideShow.CONTACT_PADDING_COLOR       = "black";
SlideShow.CONTACT_HOVER_BORDER_COLOR  = "#777";
SlideShow.CONTACT_HOVER_PADDING_COLOR = "#666";
SlideShow.TITLE_TEXT_COLOR            = "white";

SlideShow.CONTROL_IMAGES =
   [ "prev", "play", "pause", "next", "contact", "close" ];

/****************************************************************************
 * SlideShow PUBLIC METHODS
 ***************************************************************************/

SlideShow.prototype.resize = function()
{
   this.resizeToParent();
   // redisplay the image (which really means just adjusting the
   // size and padding so that it fills the frame); but don't do
   // this if we were displaying the contacts; don't try to
   // resize the contacts dynamically, because Safari fires
   // onresize events continuously, so we wind up throwing it 
   // away and rebuilding it over and over and over again; also
   // don't redisplay if we are playing the show, since the next
   // image will be right and we don't want transitions getting stomped
   if ( ! this.showingContacts && ! this.playing )
      this.displayImageAt( this.currentImage );
}

SlideShow.prototype.displayNextImage = function( onDisplayed )
{
   if ( this.commandWasQueued( arguments ) )
      return;

   this.currentImage = this.nextImageIndex();
   this.displayImageAt( this.currentImage, onDisplayed );
}

SlideShow.prototype.displayPreviousImage = function( onDisplayed )
{
   if ( this.commandWasQueued( arguments ) )
      return;

   this.currentImage = this.previousImageIndex();
   this.displayImageAt( this.currentImage, onDisplayed );
}

SlideShow.prototype.isPlaying = function()
{
   return( this.playing );
}

SlideShow.prototype.autoPlay = function( onDone )
{
   this.startPlaying( true, onDone );
   this.updateControls();
}

SlideShow.prototype.playShow = function()
{
   if ( this.commandWasQueued( arguments ) )
      return;

   this.startPlaying( false, undefined );
   this.updateControls();
}

SlideShow.prototype.stopShow = function()
{
   if ( this.commandWasQueued( arguments ) )
      return;

   if ( this.timeoutID )
      clearTimeout( this.timeoutID );
   this.timeoutID     = undefined;

   this.playing       = false;
   this.playOnce      = false;
   this.onDonePlaying = undefined;
   this.updateControls();
}

SlideShow.prototype.toggleShow = function()
{
   if ( this.playing )
      this.stopShow();
   else
      this.playShow();
}

SlideShow.prototype.clearImage = function()
{
   this.displayImageAt( -1 );
}

SlideShow.prototype.displayContactSheet = function()
{
   if ( this.commandWasQueued( arguments ) )
      return;

   // make sure show is stopped and clear the title area
   this.stopShow();
   this.updateTitle(); // clear it

   // decide how big to make each proof
   var tile = this.calculateContactDimensions();
   var thumbWidth  = tile.width - 16; // less 6px border + 10px margins
   var thumbHeight = tile.height - 16; // no margin collapsing on floats?

   // create container of the right size
   var container = document.createElement( "div" );
   container.style.width  = this.width + "px";
   container.style.height = this.height + "px";
   container.style.backgroundColor = SlideShow.CONTACT_PADDING_COLOR;

   // insert each thumbnail as a float
   var _this = this;
   for ( var i = 0 ; i < this.imageList.length ; ++i )
   {
      // get each image with the right size and padding
      var img = this.imageList[ i ];
      var thumb = img.getImageForThumbnail( thumbWidth, thumbHeight );

      // set up the decorations for the thumbnail
      thumb.style.margin = "5px";
      thumb.style.cssFloat = "left";
      thumb.style.border = "3px solid " + SlideShow.CONTACT_BORDER_COLOR;
      thumb.style.backgroundColor = SlideShow.CONTACT_PADDING_COLOR;

      // define the event handlers; because we need to access the
      // various image and thumbnail details here, we need to go
      // through an extra layer of closures to capture the data
      thumb.onclick = handlerForImage( img, i, thumb,
      function( imageInfo, imageIndex, thumb )
      {
         _this.showingContacts = false;
         _this.displayImageAt( imageIndex );
         // don't let mouse moving out after click stomp the title!
         // we cannot just check for container.parentNode in the
         // existing handler, because the transition effect means
         // it will still have one immediately after the click; 
         // note that an empty function is required to keep IE6/Win
         // from crashing (even though undefined works elsewhere)
         thumb.onmouseout = function() { };
      } );
      thumb.onmouseover = handlerForImage( img, i, thumb,
      function( imageInfo, imageIndex, thumb )
      {
         thumb.style.borderColor     = SlideShow.CONTACT_HOVER_BORDER_COLOR;
         thumb.style.backgroundColor = SlideShow.CONTACT_HOVER_PADDING_COLOR;
         _this.updateTitle( imageInfo, imageIndex );
      } );
      thumb.onmouseout = handlerForImage( img, i, thumb,
      function( imageInfo, imageIndex, thumb )
      {
         thumb.style.borderColor     = SlideShow.CONTACT_BORDER_COLOR;
         thumb.style.backgroundColor = SlideShow.CONTACT_PADDING_COLOR;
         _this.updateTitle();
      } );
      container.appendChild( thumb );
   }

   function handlerForImage( imageInfo, imageIndex, thumb, f )
   {
      return( function() { f( imageInfo, imageIndex, thumb ); } );
   }

   // transition in the contact sheet
   AjaxUtil.transitionFX( this.slideFrame,
                          this.slideFrame.firstChild,
                          container,
                          "Opacity", SlideShow.TRANSITION_CYCLE );
   this.showingContacts = true;
}

/****************************************************************************
 * SlideShow PRIVATE METHODS
 ***************************************************************************/

SlideShow.prototype.displayImageAt = function( index, onDisplayed )
{
   // make sure the contact sheet state is cleared
   this.showingContacts = false;

   // make sure the image index is correct (even if we are clearing)
   this.currentImage = index;

   // special index indicates we clear image
   if ( index == -1 )
   {
      AjaxUtil.transitionFX( this.slideFrame,
                             this.slideFrame.firstChild,
                             undefined,
                             "Opacity", SlideShow.TRANSITION_CYCLE );
      this.updateTitle();
      return;
   }

   // set up the onload handler and grab the image
   var info = this.imageList[ index ];
   var _this = this;
   var onLoad = function( imageElem )
   {
      // don't transition if image is same (e.g. resize event)
      if ( _this.slideFrame.firstChild != imageElem )
      {
         _this.inTransition = true;
         imageElem.style.backgroundColor = SlideShow.BACKGROUND_COLOR;
         AjaxUtil.transitionFX( _this.slideFrame,
                                _this.slideFrame.firstChild,
                                imageElem,
                                "Opacity", SlideShow.TRANSITION_CYCLE,
                                function() 
                                {
                                   // change titles at midpoint
                                   _this.updateTitle( info, index );
                                },
                                function()
                                {
                                   _this.inTransition = false;
                                   _this.runQueuedCommands();
                                   if ( onDisplayed )
                                      onDisplayed();
                                } );
      }
      else if ( onDisplayed )
         onDisplayed(); // but always call onDisplayed!
   };
   info.getImageForFrame( this.width, this.height, onLoad );
}

SlideShow.prototype.commandWasQueued = function( cmd )
{
   // if we are in the middle of a transition, "queue" the command
   // by saving the given arguments object; only save the *first*
   // queued command (not the last, which gets us into trouble)
   if ( this.inTransition && ! this.queuedCommand )
      this.queuedCommand = cmd;
   return( this.inTransition );
}

SlideShow.prototype.runQueuedCommands = function()
{
   // to run the queued command, we get the actual function object
   // through the callee property and apply() it to this, passing
   // the arguments object itself back again
   if ( this.queuedCommand )
      this.queuedCommand.callee.apply( this, this.queuedCommand );
   this.queuedCommand = undefined;
}

SlideShow.prototype.updateTitle = function( imageInfo, atIndex )
{
   if ( ! this.titleFrame )
      return;
   // Gecko handles a transition here okay, but Safari messes up
   // the styling of the title frame really badly, so don't try it
   while ( this.titleFrame.hasChildNodes() )
      this.titleFrame.removeChild( this.titleFrame.firstChild );
   if ( imageInfo )
      this.titleFrame.appendChild( this.getTitleElement( imageInfo, atIndex ) );
}

SlideShow.prototype.getTitleElement = function( imageInfo, atIndex )
{
   var container = document.createElement( "div" );
   container.id = "titleBlock";
   container.style.margin = "0 25px";
   container.style.borderTop = "1px solid " + SlideShow.TITLE_TEXT_COLOR;
   container.style.padding = "0";

   var p = document.createElement( "div" );
   p.className = "counter";
   p.appendChild( document.createTextNode(
      ( atIndex + 1 ) + " of " + this.imageList.length ) );
   p.style.cssFloat = "right";
   if ( AjaxUtil.UserAgent.nonStandardCSSFloat )
      p.style[ AjaxUtil.UserAgent.nonStandardCSSFloat ] = "right";
   p.style.color = SlideShow.BACKGROUND_COLOR;
   p.style.backgroundColor = SlideShow.TITLE_TEXT_COLOR;
   p.style.padding = "5px";
   p.style.margin = "0";
   p.style.fontSize = "70%";
   if ( AjaxUtil.UserAgent.floatInFixedHeightRequiresPosition )
      p.style.position = "relative";
   container.appendChild( p );

   p = document.createElement( "div" );
   p.className = "title";
   p.style.cssFloat = "left";
   if ( AjaxUtil.UserAgent.nonStandardCSSFloat )
      p.style[ AjaxUtil.UserAgent.nonStandardCSSFloat ] = "left";
   p.style.color = SlideShow.TITLE_TEXT_COLOR;
   p.style.marginTop = "0.25em";
   p.style.width = "90%"; // stay away from counter if text is too long
   p.style.fontSize = "110%";
   if ( AjaxUtil.UserAgent.floatInFixedHeightRequiresPosition )
      p.style.position = "relative";
   p.innerHTML = imageInfo.title;
   container.appendChild( p );

   return( container );
}

SlideShow.prototype.nextImageIndex = function()
{
   var next = this.currentImage + 1;
   if ( next >= this.imageList.length )
      next = 0;
   return( next );
}

SlideShow.prototype.previousImageIndex = function()
{
   var previous = this.currentImage - 1;
   if ( previous < 0 )
      previous = this.imageList.length - 1;
   return( previous );
}

SlideShow.prototype.startPlaying = function( once, onDone )
{
   if ( this.playing )
      return;

   // move to a playing state and record parameters
   this.playing       = true;
   this.playOnce      = once;
   this.onDonePlaying = onDone;

   // on each interval, check for end-of-autoplay; otherwise,
   // display the next image (but don't schedule the next interval
   // until that image is actually *loaded*, or we'll advance 
   // through the entire show without showing it all!)
   var _this = this;
   var startLoadingImage = function()
   {
      _this.timeoutID = undefined;
      if ( _this.playing )
      {
         // check for repeat disabled and showing last image
         if ( _this.playOnce
              && _this.currentImage == ( _this.imageList.length - 1 ) )
         {
            var onDone = _this.onDonePlaying;
            _this.stopShow();
            if ( onDone )
               onDone();
            return;
         }
         // show is still running, so start loading the next
         // image (but we don't schedule the next timeout until
         // we get notified that the image has been loaded)
         _this.displayNextImage( scheduleNextImage );
      }
   }

   // once we've actually loaded the image, schedule the next round;
   // note that this makes sense even for autoplay, because we want
   // to display the last image for the interval before exiting
   var scheduleNextImage = function()
   {
      if ( _this.playing )
      {
         // current image has been loaded and displayed, so schedule next
         _this.timeoutID =
            setTimeout( startLoadingImage, SlideShow.SHOW_INTERVAL );
         // also start preloading the next image now
         _this.imageList[ _this.nextImageIndex() ].getImageForFrame(
                                                 _this.width, _this.height );
      }
   }

   // start loading the next image (if we are just starting, this
   // will load image 0, since curentImage starts at -1)
   startLoadingImage(); 
}

SlideShow.prototype.createControls = function()
{
   var controls = document.createElement( "div" );
   this.controlFrame.appendChild( controls );
   controls.style.padding = "4px 0";
   controls.style.border = "1px solid #777";
   controls.style.width = ( ( 4 + ( this.onClose ? 1 : 0 ) ) * 75 ) + "px";
   controls.style.margin = "0 auto";

   // deal with browsers that don't support auto-margin centering
   // but do (illogically) center non-text with text-align
   if ( AjaxUtil.UserAgent.textCenterForNonText )
      this.controlFrame.style.textAlign = "center";

   var _this = this;
   createControl( SlideShow.CONTROL_IMAGES[ 0 ],
                  function()
                  {
                    if ( _this.playing )
                    {
                       _this.stopShow();
                       _this.updateControls();
                    }
                     _this.displayPreviousImage();
                  },
                  "Go to previous image",
                  true );
   createControl( SlideShow.CONTROL_IMAGES[ 1 ],
                  function( link )
                  {
                     _this.toggleShow();
                     _this.updateControls();
                  },
                  "Start slideshow" );
   createControl( SlideShow.CONTROL_IMAGES[ 3 ],
                  function()
                  {
                    if ( _this.playing )
                    {
                       _this.stopShow();
                       _this.updateControls();
                    }
                     _this.displayNextImage();
                  },
                  "Go to next image" );
   createControl( SlideShow.CONTROL_IMAGES[ 4 ],
                  function()
                 {
                    if ( _this.playing )
                    {
                       _this.stopShow();
                       _this.updateControls();
                    }
                    _this.displayContactSheet();
                 },
                 "Display contact sheet with all images" );
   if ( this.onClose )
      createControl( SlideShow.CONTROL_IMAGES[ 5 ],
                     this.onClose,
                     "Close the slideshow" );

   function createControl( imageName, handler, title, first )
   {
      var link = document.createElement( "a" );
      link.id = imageName;
      link.className = "control"
      link.href = "#";
      link.style.border = "none";
      link.style.outline = "none"; // don't show focus rings on buttons
      link.onclick = function()
      {
         handler( link );
         return( false );
      };
      controls.appendChild( link );

      var img = document.createElement( "img" );
      img.id = imageName + "-image";
      img.src = _this.iconURL + imageName + ".jpg";
      img.title = title;
      img.style.border = "none"
      if ( ! first )
         img.style.borderLeft = "1px solid #777";
      img.style.width   = "64px";
      img.style.height  = "64px";
      img.style.padding = "0 5px";
      link.appendChild( img );
   }
}

SlideShow.prototype.updateControls = function()
{
   if ( ! this.controlFrame )
      return;

   var img = document.getElementById( "play-image" );
   if ( ! img )
      return;

   if ( this.playing )
   {
      img.src = this.iconURL + SlideShow.CONTROL_IMAGES[ 2 ] + ".jpg";
      img.title = "Stop slideshow";
   }
   else 
   {
      img.src = this.iconURL + SlideShow.CONTROL_IMAGES[ 1 ] + ".jpg";
      img.title = "Start slideshow";
   }
}

SlideShow.prototype.resizeToParent = function()
{
   // set dimensions of frame based on rendered size of parent
   this.width  = this.parentElem.offsetWidth
                 - ( SlideShow.FRAME_PADDING * 2 );
   this.height = this.parentElem.offsetHeight
                 - ( SlideShow.FRAME_PADDING * 2 )
                 - ( this.titleFrame ? SlideShow.TITLE_HEIGHT : 0 )
                 - ( this.controlFrame ? SlideShow.CONTROL_HEIGHT : 0 );

   // adjust size and formatting of frame area
   this.slideFrame.style.width = this.width + "px";
   this.slideFrame.style.height = this.height + "px";
   this.slideFrame.style.padding = SlideShow.FRAME_PADDING + "px";
   this.slideFrame.style.margin = "0";
   this.slideFrame.style.backgroundColor = SlideShow.BACKGROUND_COLOR;

   // adjust size and formatting of title area (if any)
   if ( this.titleFrame )
   {
      this.titleFrame.style.height = SlideShow.TITLE_HEIGHT + "px";
      this.titleFrame.style.margin = "0";
      this.titleFrame.style.backgroundColor = SlideShow.BACKGROUND_COLOR;
   }

   // adjust size and formatting of control area (if any)
   if ( this.controlFrame )
   {
      this.controlFrame.style.height = SlideShow.CONTROL_HEIGHT + "px";
      this.controlFrame.style.margin = "0";
      this.controlFrame.style.backgroundColor = SlideShow.BACKGROUND_COLOR;
   }
}

SlideShow.prototype.calculateContactDimensions = function()
{
   var imgCount = this.imageList.length;

   var numWide = 1;
   var numHigh = 1;

   // step through each image to calculate an appropriate layout
   for ( var i = 0 ; i < imgCount ; ++i )
   {
      // whenever we have too few slots, add columns if we have
      // more rows; otherwise favor adding rows (portrait orientation)
      if ( numWide * numHigh < ( i + 1 ) )
      {
         if ( numWide < numHigh )
            ++numWide;
         else
            ++numHigh;
      }
   }

   // calculate width and height of each thumbnail, truncating to integers
   var width  = Math.round( this.width / numWide - 0.5 );
   var height = Math.round( this.height / numHigh - 0.5 );

   return( { width: width, height: height } );
}

