function PictureGallery( elem, on_init ) {
    
    /*
    The "PictureGallery" class.
    */
    
    var picture_gallery = this;
    
    this.picture_dict = Object();
    this.selected_picture = null;
    
    // The pictures are loaded in to the slider element using a queued 
    // mechanism. The reason for this is to ensure that only 1 image is being 
    // loaded at any one time. Typically only 2 resources can be simultaneously
    // download by a browser, so this mechanism ensures that one channel is 
    // kept open allowing a user to load/view the larger version of any image 
    // in the gallery.
    this._picture_queue = Array();
    
    // Locking is used to ensure that application waits until it has completed
    // loading a picture before loading another one (see comments above).
    this._queue_lock = false;
    this._image_lock = false;
    
    // Slider values
    this._slider_position = 0;
    this._slider_speed = 0;
    this._slider_update_event = null;  
    
    // Callback functions
    this.on_init = on_init;
    
    // To set the interface up we load some HTML from a file, then call
    // "init_interface" to bind all events.
    
    // Setup a division to load the lightbox elements into
    $( elem ).append( "<div id='picture-gallery'></div>" );

    // Load the HTML
    $( "#picture-gallery" ).load( "html/picture-gallery.html", 
        function() { picture_gallery._init_interface(); } 
        );
}

PictureGallery.prototype._init_interface = function() {
    
    /*
    Initialise the various interface elements.
    */
    
    var picture_gallery = this;
    
    // Bind the events for user interaction with the slider
    
    // Define a function to handle the events when the mouse is over the flow 
    // element. 
    var slide = function( ev ) {
        var flow_size2 = $( "#picture-gallery-flow" ).innerWidth() / 2;
        var mouse_x = ev.pageX - $( "#picture-gallery-flow" ).offset()[ "left" ] - flow_size2;
        picture_gallery._slider_speed = mouse_x / flow_size2 * picture_gallery.max_slider_speed;
    }
    
    $( "#picture-gallery-flow" ).bind( "mousemove", slide );
    $( "#picture-gallery-flow" ).bind( "mouseleave", function( ev ) { picture_gallery._slider_speed = 0; } );
    
    // The slider is updated on a periodic basis by a timed event, now the 
    // interface has been initialised we start the timer.
    this.start_sliding();

    if ( this.on_init ) { 
        this.on_init.call( this );
    }     
}

PictureGallery.prototype._acquire_queue_lock = function() {
    
    /*
    Attempt to acquire a queue lock.
    */
    
    if ( this._queue_lock ) {
        return false;
    } else {
        this._queue_lock = true;
        return true;
    }
}

PictureGallery.prototype._release_queue_lock = function() {
    
    /* 
    Release the queue lock.
    */
    
    this._queue_lock = false;
    this._process_queue();
}

PictureGallery.prototype._acquire_image_lock = function() {
    
    /*
    Attempt to acquire an image lock.
    */
    
    if ( this._image_lock ) {
        return false;
    } else {
        this._image_lock = true;
        return true;
    }
}

PictureGallery.prototype._release_image_lock = function() {
    
    /* 
    Release the image lock.
    */
    
    this._image_lock = false;
}

PictureGallery.prototype._update_slider_position = function() {
    
    /* 
    Update the slider position based on the slider speed and constraints.
    */
    
    var slider_width = $( "#picture-gallery-slider" ).width();
    var flow_width = $( "#picture-gallery-flow" ).width();
    
    // Is the slider bigger than the flow, if not it can't slide
    if ( slider_width > flow_width ) {
        
        // Is the slider moving, if not no reason to update the position
        if ( Math.abs( this._slider_speed ) > this.slider_tolerance ) {
            
            this._slider_position -= this._slider_speed;
            
            // Ensure the slider does not slide outside the constraints of the
            // flow.
            this._slider_position = Math.max( this._slider_position, 0 - ( slider_width - flow_width ) );
            this._slider_position = Math.min( this._slider_position, 0 );
            
            // Update the sliders position
            $( "#picture-gallery-slider" ).css( "margin-left", this._slider_position );
        }
    } else {
        this._slider_position = 0;
        $( "#picture-gallery-slider" ).css( "margin-left", 0 );
    }
}

PictureGallery.prototype._update_slider_size = function() {

    /* 
    Accurately determine and set the size of the slider "div" element. 
    */

    var thumbnail_elem_list = $( "#picture-gallery-slider" ).find( ".thumbnail" );
    
    var slider_width = 0;
    for ( var i = 0; i < thumbnail_elem_list.length; i++ ) {
        if ( $( thumbnail_elem_list[i] ).css( "display" ) != "none" ) {
            slider_width += $( thumbnail_elem_list[i] ).outerWidth( { margin : true } );
        }
    }        

    $( "#picture-gallery-slider" ).css( "width", slider_width );
}

PictureGallery.prototype._process_queue = function() {

    /*
    Attempt to process a picture in the queue.
    
    NOTE: This is a self calling process, when a picture loads and unlocks the
    queue it calls the "_process_queue" function again to process the next 
    image. This cycle continues until there are no images left to process.
    */
    
    var picture_gallery = this;
    
    if ( this._picture_queue.length && this._acquire_queue_lock() ) {
       
        // Obtain the next picture to process
        var picture_instr = this._picture_queue.shift();
        var picture = picture_instr[0];
        var before = picture_instr[1];
        var callback = picture_instr[2];
       
        // Clone an existing branch of the DOM to create a template for the new
        // picture. The ID is then changed provide a pointer to the new element.
        var thumbnail_elem = $( "#picture-gallery-thumbnail-clone" ).clone();
        thumbnail_elem.attr( "id", picture.uid );
       
        // Adjust slider width as an approximation using the "max_thumbnail_size"
        $( "#picture-gallery-slider" ).css( "width", 
            $( "#picture-gallery-slider" ).width() + this.max_thumbnail_size );
        
        // Look up the element we are inserting the picture element before
        var before_elem = null;
                
        if ( before ) {
            before_elem = $( "#" + before.uid );
        
        } else {
            before_elem = $( "#picture-gallery-thumbnail-clone" );
        }
        
        if ( before_elem.length == 0 ) {
            throw Error( "Unable to insert picture '" + picture.uid + "' before picture '" + before +"'." );
        }
        
        // Insert the picture as a thumbnail in the slider
        thumbnail_elem.insertBefore( before_elem );
        
        // Load the thumbnail
        $( "#" + picture.uid + " img" ).load( function( ev ) {
    
            // Hide the animated loading gif 
            thumbnail_elem.removeClass( "loading" );
    
            // Vertical align the thumbnail bottom
            $( this ).css( "display", "block" );
            var thumbnail_height = $( this ).height();
            $( this ).css( "display", "none" );
            var thumbnail_area_height = thumbnail_elem.height();
            var thumbnail_y = parseInt( Math.floor( ( thumbnail_area_height - thumbnail_height ) ) );   
            $( this ).css( "margin-top", thumbnail_y );
            
            // Show the thumbnail
            $( this ).fadeIn( "normal", 
                function() {
                
                    // Now the image is loaded and visible we adjust the slider
                    // to an accurate size for it's content.
                    picture_gallery._update_slider_size();
                            
                    // Bind the thumbnails callback functions
                    
                    // Click
                    $( this ).bind( "click",
                        function( ev ) {
                            
                            // Select the image
                            picture_gallery.select_picture( picture, picture.on_click_thumbnail );
                            
                        } );                     
                    
                    // Double click
                    if ( picture.on_dblclick_thumbnail  ) {
                        $( this ).bind( "dblclick", 
                            function( ev ) { picture.on_dblclick_thumbnail.call( picture ); } 
                            );
                    }                    
                    
                    // Now the thumbnail can be clicked change the cursor to a 
                    // pointer when the mouse is over.
                    $( this ).css( "cursor", "pointer" );                    
                
                    if ( callback ) {
                        callback.call( picture );
                    }
                
                    // Unlock the queue so the next picture can be processed
                    picture_gallery._release_queue_lock();
                    
                } );
        } ).error( function() {
            
            // Should an error occur while loading the thumbnail we remove the
            // picture and unlock the queue.
            picture_gallery.remove_picture( picture ); 
            picture_gallery._release_queue_lock();
        } );
        
        $( "#" + picture.uid + " img" ).attr( { "alt" : picture.caption, 
            "src" : picture.thumbnail, 
            "title" : picture.caption }
            );
    }
}

PictureGallery.prototype.start_sliding = function() {
    
    /* 
    Start sliding event.
    */
    
    var picture_gallery = this;
    
    if ( !this._slider_update_event ) {
        this._slider_update_event = $.timer( 1000 / picture_gallery.slider_fps,
            function( tm ) { picture_gallery._update_slider_position() } 
            );
    }
}

PictureGallery.prototype.stop_sliding = function() {
    
    /* 
    Stop sliding event.
    
    NOTE: For performance reasons it is recommended you stop sliding whenever
    the picture gallery is not in use, for example if you keep it in the 
    background whilst some other user task is being performed.
    */
    
    this._slider_update_event.stop();
    this._slider_update_event = null;
}

PictureGallery.prototype.prepare_slider = function() {
    
    /* 
    Prepare the slider for showing. 
    */
    
    var thumbnail_elem_list = $( "#picture-gallery-slider" ).find( ".thumbnail" );
    var slider_width = thumbnail_elem_list.length * this.max_thumbnail_size;
    
    $( "#picture-gallery-slider" ).css( "width", slider_width );
}

PictureGallery.prototype.update_slider = function() {
    
    /* 
    Update the slider bar.
    */
    
    // Update the size
    this._update_slider_size();

    // Update each thumbnails position
    var thumbnail_elem_list = $( "#picture-gallery-slider" ).find( ".thumbnail" );
    
    var slider_width = 0;
    for ( var i = 0; i < thumbnail_elem_list.length; i++ ) {

        var thumbnail_elem = $( thumbnail_elem_list[i] );
        var thumbnail_img = thumbnail_elem.find( "img" );

        if ( thumbnail_elem.css( "display" ) != "none" ) {

            // Vertical align the thumbnail bottom
            var thumbnail_height = $( thumbnail_img ).height();
            var thumbnail_area_height = thumbnail_elem.height();
            var thumbnail_y = parseInt( Math.floor( ( thumbnail_area_height - thumbnail_height ) ) );   
            $( thumbnail_img ).css( "margin-top", thumbnail_y );

        }
    }  
}

PictureGallery.prototype.insert_picture = function( picture, before, callback ) {
    
    /*  
    Insert a picture into the gallery.
    
    NOTE: If "before" picture is not specified then the default behaviour is to
    add the picture to the end of the slider. If a "before" picture is 
    specified that does not exist (or has not yet been inserted into the 
    slider) this will throw an error.
    */
    
    this.picture_dict[ picture.uid ] = picture;

    this._picture_queue.push( [ picture, before, callback ] );
    this._process_queue();
}

PictureGallery.prototype.remove_picture = function( picture, callback ) {

    /*
    Remove a picture from the gallery.
    */
    
    var picture_gallery = this;
    var picture_elem = $( "#" + picture.uid );
    
    // Only remove the picture if we are not in the process of showing a full
    // image (so we require an image lock).
    if ( this._acquire_image_lock() ) {
        
        // Fade the picture out
        picture_elem.fadeOut( "normal", 
            function() {
        
                // Remove the picture element
                picture_elem.remove();
                
                // Update the size of the slider bar due now the picture has been removed
                picture_gallery._update_slider_size();        
            
                delete picture_gallery.picture_dict[ picture.uid ];
    
                // Picture has been removed so release the lock
                picture_gallery._release_image_lock();
    
                if ( callback ) {
                    callback.call( picture );
                }
            } );
    }
}

PictureGallery.prototype.select_picture = function( picture, callback ) {

    /*
    Select a picture in the gallery.
    */
    
    var picture_gallery = this;
    
    // Only select the picture if it is not the current selection
    if ( this.selected_picture != picture && this._acquire_image_lock() ) {
        
        // Remember the selected picture
        this.selected_picture = picture;
        
        // Mark the thumbnail as selected using a class (first remove any 
        // previous selection).
        $( "#picture-gallery-slider" ).find( ".selected" ).removeClass( "selected" );   
        $( "#" + picture.uid ).addClass( "selected" );
        
        // Unbind any events associated with the existing image
        $( "#picture-gallery-image img" ).unbind();     
        
        $( "#picture-gallery-image img" ).fadeOut( "normal",
            function() {
                
                // Show an animated loading gif while we load the image 
                $( "#picture-gallery-image" ).addClass( "loading" );
                
                // Reset the image src (required by Safari and Opera to unload
                // the image, and allow the load event to be re-triggered.)
                
                // Further more, in opera if the two images have the same 
                // source URL then no load event will be triggered.
                $( "#picture-gallery-image img" ).attr( "src", "" );                
                
                $( "#picture-gallery-image img" ).load( function() {
                    
                    // Now the image has loaded remove the animated loading gif
                    $( "#picture-gallery-image" ).removeClass( "loading" );
                    
                    // Vertical align the image center
                    $( "#picture-gallery-image img" ).css( "display", "block" );
                    var image_height = $( "#picture-gallery-image img" ).height();
                    $( "#picture-gallery-image img" ).css( "display", "none" );
                    var image_area_height = $( "#picture-gallery-image" ).height();
                    var image_y = parseInt( Math.floor( ( image_area_height - image_height ) / 2 ) );
                    
                    $( "#picture-gallery-image img" ).css( "padding-top", image_y );
                    
                    $( "#picture-gallery-image img" ).fadeIn( "normal",
                        function() {
                    
                            // Bind any callback events for the image
                            if ( picture.on_click_image ) {
                                $( "#picture-gallery-image img" ).bind( "click",
                                    function( ev ) { picture.on_click_image.call( picture ); }
                                    );
                            }
                            
                            if ( picture.on_dblclick_image ) {
                                $( "#picture-gallery-image img" ).bind( "dblclick",
                                    function( ev ) { picture.on_dblclick_image.call( picture ); }
                                    );
                            }
                            
                            // Only show pointer when mousing over if events where 
                            // bound to the image.
                            if ( picture.on_click_image || picture.on_dblclick_image ) {
                                $( "#picture-gallery-image img" ).css( "cursor", "pointer" );
                            } else {
                                $( "#picture-gallery-image img" ).css( "cursor", "default" );
                            }
                            
                            if ( callback ) {
                                callback.call( picture );
                            }
                            
                            picture_gallery._release_image_lock();
                    } );
                            
                } ).error( function() {
                    
                    // Should an error occur while loading the thumbnail we remove the
                    // picture and unlock the queue.
                    picture_gallery.remove_picture( picture );
                    picture_gallery._release_image_lock();
                    
                } );
                
        $( "#picture-gallery-image img" ).attr( { "alt" : picture.caption, 
            "src" : picture.image, 
            "title" : picture.caption }
            );                
        } );
    }
}

PictureGallery.prototype.get_picture_by_position = function( position ) {
    
    /* 
    Return a picture by it's position in the gallery.
    */
    
    // Look up a list of the picture elements
    var thumbnail_elem_list = $( "#picture-gallery-slider" ).find( ".thumbnail" );
    
    // Return (if it exists the image at the specified position)
    if ( thumbnail_elem_list.length > position ) {
        return this.picture_dict[ $( thumbnail_elem_list[0] ).attr( "id" ) ];
    } else {
        return null;
    }     
};

// Picture gallery contents
PictureGallery.prototype.max_thumbnail_size = 280; // (in pixels - this should include borders, padding and margin)
PictureGallery.prototype.max_slider_speed = 13; // (in pixels)
PictureGallery.prototype.slider_fps = 28; // (in frames per second)
PictureGallery.prototype.slider_tolerance = 1; // (speed over which motion is applied)

function Picture( thumbnail,
    image,
    caption,
    on_click_thumbnail,
    on_click_image,
    on_dblclick_thumbnail,
    on_dblclick_image,
    data  
    ) {
    
    /*
    The "Picture" class.
    
    A seperate class is used to store information about pictures displayed in
    the gallery as this allows the interaction with pictures to be customized.
    */
    
    // Assign each picture a unique ID
    this.uid = "picture-" + uid();
    
    this.thumbnail = thumbnail;
    this.image = image;
    this.caption = caption || "";
    
    // Callback functions
    this.on_click_thumbnail = on_click_thumbnail;
    this.on_click_image = on_click_image;
    this.on_dblclick_thumbnail = on_dblclick_thumbnail;
    this.on_dblclick_image = on_dblclick_image;

    this.data = data;
}
