﻿// Slideshow: rotates through a series of image objects defined in Pictures.js
// Version 1.3.1 09Sep2008 MikeSoSoft
// 1.2 Added "px" to setting style.top in Fade Resize().
// 1.2.2 Added () to pictureChanged in changePicture(). Not calling function.
// 1.2.3 04Aug2008 Moved call to set target.className to before setting .src to see if it
//                 fixes IE6 not displaying pictures correctly.
// 1.2.4 08Aug2008 Changed the way callbacks are called, for consistancy and brevity.
// 1.2.5 12Aug2008 Rewrote Fade object to get over jittering and popping in FF.
//                 Used idea from http://www.brothercake.com/ that simplifies using
//                 additional image object (at the expense of more DOM manipulation).
//                 Also used the idea of cloning the Slideshow div to get the centred
//                 image position. Still not smooth in FF because my machine is too slow
//                 to do the opacity changes. Testing showed 400ms per transition vz
//                 a target of 50ms. 
// 1.2.6 13Aug2008 Went back to previous idea of two images and fading between them,
//                 ie not the brothercake way of switching the sourceImage.src. Instead
//                 keep the targetImage in play and fade from source to target to source.
//                 Tested online and no blinking or movement.
// 1.3 05Sep2008 1. Added functionality to wait if next picture is not ready. Use the onLoading
//                  callback to notify client to display loading...
//                  In changePicture(), moved call to pictureChanged() inside else.
//                  In loadingDelegate(), removed call to pictureChanged().
//                  Changed next: & previous: to use checkNextPicture() instead of changePicture().
//                  Removed showPicture().
//                  Replaced withFade parameter in changePicture() to use firstLoad variable.
//                      Initially true, reset false in loadingDelegate().
//                  Put all variables relating to aPicture as members of aPicture.
//               2. Changed picture loading routine to run in a timer and check if each
//                  picture is complete before requesting the next.
//               3. Created RepositionAbsoluteImageOnResize() in Fade because source and target
//                  images have been swaped. Also moved deleting cloned image to createCloneImage().
// 1.3.1 09Sep2008 1. In Fade.toggle(), if opacity.type == none, call onFade & on FadeComplete.
//                 2. In setOpacityType(), test for Firefox and if fadeInFirefox option false,
//                    set opacityType.none. Caused soundtrack to stutter.
var Slideshow = function () {

    var _this = null;
    var initialised = false;            //set true when init() called
    var started = false;                //set true when start() called
    var target = null;                  //target image element 
    var slideTitle = null;              //element containing slide title
    var slideDescription = null;        //element containing slide desctiption
    var paused = false;                 //flag to pause SlideShow
    var fade = true;                    //whether to fade one image to the next

    // ----------------------------------------------------------------------------------------
    var errorMessages = 
    {targetId: "Target id is invalid. Call Slideshow.init('slideID') with a valid target id.",
     PicturesUndefined: "Pictures is undefined. Insert reference to Pictures.js in HTML file.",
     alreadyInitialised: "Slideshow has already been initialised.",
     invalidInterval: "Interval not set, must be greater than zero and less than ten.",
     notInitialised: "Slideshow has not been initialised. Call Slideshow.init() first.",
     alreadyStarted: "Slideshow has already been started."
    }
    function displayError(messageId) {
        alert(errorMessages[messageId]);
        return false;
    }
    // ----------------------------------------------------------------------------------------
    
    var aPictures = [];                     // Array of Image objects
    aPictures.index = -1;                   // Index of current Image object
    aPictures.increment = 1;                // Value to increment index by (1 or -1)
    aPictures.nextIndex = -1;               // Index of next Image object (index + imcrement)
    aPictures.getNextIndex = function () {  // Calculate and store next index value
        this.nextIndex = this.index + this.increment;
        if (this.nextIndex == Pictures.length) {this.nextIndex = 0};
        if (this.nextIndex < 0) {this.nextIndex = (Pictures.length -1)};
        return this.nextIndex;
    }
    aPictures.nextImageComplete = function () {
        if (aPictures.getNextIndex() < this.length && aPictures[this.nextIndex].complete) {return true};
    }
    aPictures.loadIndex = -1;               // Index of current picture being loaded
    aPictures.loadTimer = null;
    aPictures.loadInterval = 1000;
    aPictures.loadNextPicture = function() {// Load next image from Pictures array
        aPictures.loadIndex += 1;
        aPictures[aPictures.loadIndex] = new Image;
        aPictures[aPictures.loadIndex].src = Pictures[aPictures.loadIndex].fileName;
        aPictures[aPictures.loadIndex].alt = Pictures[aPictures.loadIndex].title;
        aPictures[aPictures.loadIndex].desc = Pictures[aPictures.loadIndex].description;
    }
    aPictures.loadDelegate = function() {   // Timer function to test if each image is complete
        if (aPictures[aPictures.loadIndex].complete) {
            if (aPictures.loadIndex < Pictures.length - 1) {
                aPictures.loadNextPicture();
            }
            if (aPictures.loadIndex == Pictures.length - 1) {
                clearInterval(aPictures.loadTimer);
                aPictures.loadTimer = null;
            }
        }
    }
    // ----------------------------------------------------------------------------------------
    
    var firstLoad = true;                   // Flag denoting first use of loadingDelegate()
                                            // used in changePicture() to show first image 
                                            // imediately rather than with fade
    var loadingTimer = null;
    var loadingDelegate = function () {     // Called from start() and checkNextPicture()
        if (aPictures.nextImageComplete()) {
            if (firstLoad) {
                if (_this.onLoad) {_this.onLoad()};
            } else {
                if (_this.onLoaded) {_this.onLoaded()};
            }
            changePicture();
            if (!paused) {refreshTimer = setInterval(refreshDelegate, interval)};
            clearInterval(loadingTimer);
            loadingTimer = null;
            if (firstLoad) {firstLoad = false};
        }
    }
    // ----------------------------------------------------------------------------------------
    
    var interval = 1000;
    var refreshTimer = null;
    var refreshDelegate = function () {
        if (paused) {return};
        checkNextPicture(1)
    }
    // ----------------------------------------------------------------------------------------
    
    // Called from refreshDelegate() and next: & previous: members. Checks if next image
    // is complete before changing to it, if not go into wait mode.
    var changeInProgress = false;
    function checkNextPicture(direction) {
        if (changeInProgress) {return};
        changeInProgress = true;
        aPictures.increment = direction;
        if (aPictures.nextImageComplete()) {
            if (paused) {           // If paused just change the image
                changePicture(direction);
            } else {                // If not paused, stop timer, change and restart.
                clearInterval(refreshTimer);
                changePicture(direction);
                refreshTimer = setInterval(refreshDelegate, interval);
            }
        } else {                    // Go into wait mode
            if (_this.onLoading) {_this.onLoading()};
            if (!paused) {          // Only stop timer if not paused.
                clearInterval(refreshTimer);
                refreshTimer = null;
            }
            loadingTimer = setInterval(loadingDelegate, 40);
        }
    }
    
    function changePicture() {
        aPictures.index = aPictures.nextIndex;  //NB depends on previous call to getNextIndex()
        if (fade && !firstLoad) {
            Fade.toggle(aPictures[aPictures.index].src, aPictures[aPictures.index].alt, getOrientationClass());
        } else {
            target.className = getOrientationClass();
            target.src = aPictures[aPictures.index].src;
            pictureChanged()
            changeInProgress = false;
        }
    }
    
    function pictureChanged() {
        if (slideTitle) {slideTitle.innerHTML = aPictures[aPictures.index].alt};
        if (slideDescription) {slideDescription.innerHTML = aPictures[aPictures.index].desc};
        if (_this.onChange) {_this.onChange()};
    }
    
    function getOrientationClass() {
        var imgRatio = aPictures[aPictures.index].width / aPictures[aPictures.index].height;
        var viewPort = target.parentNode;
        var viewportRatio = viewPort.offsetWidth / viewPort.offsetHeight;
        return (imgRatio <= viewportRatio ) ? "portrait" : "landscape";
    }
    // ----------------------------------------------------------------------------------------
    
    function addClick(toElementId, callFunction) {
        if (toElementId) {
            var element = document.getElementById(toElementId);
            element.onclick = function () {callFunction()};
            //NB FF trims space if only one class name
            element.onmouseover = function () {this.className += " hover"};
            element.onmouseout = function() {
                this.className = this.className.replace(/ hover\b/, "");
                this.className = this.className.replace(/^hover$/, "");
            }
        }
    }
    
    function onResize() {
        if (document.addEventListener) { 
            window.addEventListener("resize", Resize, false); 
        } else {
            window.attachEvent('onresize', Resize); // IE 
        } 
    
    }
    
    function Resize() {
        if (fade) {
            Fade.setImageClass(getOrientationClass());
        } else {
            target.className = getOrientationClass();
        }
    }
    // ----------------------------------------------------------------------------------------
    
    return {
        init: function (slideId, options) {
            if (initialised) { return displayError("alreadyInitialised") };
            target = document.getElementById(slideId);
            if (!target) { return displayError("targetId") };

            try {var test = Pictures; if (!test) {return displayError("PicturesUndefined") }}
            catch (e) {return displayError("PicturesUndefined") };

            if (options.titleId) {slideTitle = document.getElementById(options.titleId)};
            if (options.descriptionId) {slideDescription = document.getElementById(options.descriptionId)};

            _this = this;
            addClick(options.pauseId, _this.pause);
            addClick(options.resumeId, _this.resume);
            addClick(options.nextId, _this.next);
            addClick(options.previousId, _this.previous);
            addClick(options.togglePauseId, _this.pauseToggled);

            aPictures.loadNextPicture();    // Load first image and set timer going.
            aPictures.loadTimer = setInterval(aPictures.loadDelegate, aPictures.loadInterval);
            
            if (options.fade != undefined) { 
                fade = options.fade; 
                var fadeOptions = (options.fadeOptions) ? options.fadeOptions : {}
                if (fade) {
                    Fade.apply(slideId, fadeOptions)
                    Fade.onFade = pictureChanged;
                    Fade.onComplete = function () {changeInProgress = false};
                }
            }
            onResize();
            initialised = true;
        },
        setInterval: function (value) {
            if (value < 0 || value > 10) { return displayError("invalidInterval") };
            interval = value * 1000;
        },
        start: function () {
            if (!initialised) { return displayError("notInitialised") };
            if (started) { return displayError("alreadyStarted") };
            loadingTimer = setInterval(loadingDelegate, 40);
            started = true;
        },
        pause: function () {
            paused = true;
            if (_this.onPauseToggled) {_this.onPauseToggled(paused)};
        },
        resume: function () {
            paused = false;
            if (_this.onPauseToggled) {_this.onPauseToggled(paused)};
        },
        pauseToggled: function () {
            paused = !paused;
            if (_this.onPauseToggled) {_this.onPauseToggled(paused)};
        },
        next: function () {
            checkNextPicture(1);
        },
        previous: function () {
            checkNextPicture(-1);
        },
        addControl: function () {
            if (arguments) {
                if (arguments[0].id && arguments[0].action) {
                    addClick(arguments[0].id, arguments[0].action);
                }
            }
        },
        slideCount: function () {return Pictures.length},
        slideIndex: function () {return aPictures.index},
        slideTitle: function () {return Pictures[aPictures.index].title},
        slideDescription: function () {return Pictures[aPictures.index].description},
        onLoad: null,
        onLoading: null,
        onChange: null,
        onPauseToggled: null
    }
}();

// Fade: apply fade transition between two image elements.
var Fade = function () {
    var _this = this;
    var sourceImage = null;         //the initial image element passed in
    var targetImage = null;         //the target image to transition to
    var opacity = 0.999;
    var lastTime = null;
    var thisTimer = null;
    var onFadeCalled = false;
    var firstInterval = true;
    var absoluteImageId = "";
    var cloneDiv = null;
    var cloneImage = null;
    var defaults = {duration: 800, interval: 40, fadeInFirefox: true};
    var opacityTypes = {none: 0, w3c: 1, moz: 2, khtml: 3, ie: 4};
    var opacityType = opacityTypes.none;
    var opacityProperties = ["none", "opacity", "-moz-opacity", "KhtmlOpacity", "filter"];
    var opacityProperty = opacityProperties[0];
    
    function setOptions(options) {
	    options = options || {};
	    var result = {};
	    for (var prop in defaults) {
	        result[prop] = (options[prop] !== undefined) ? options[prop] : defaults[prop];
	    }
	    return result;
    }
    
    function setOpacityType() {
        // If fadeInFirefox not wanted and this browser is Firefox, force opacityType.none.
        // Using Fade in Firefox caused the soundtrack to stutter when opacity changed.
        var userAgent = navigator.userAgent.toLowerCase();
        var is_saf    = ((userAgent.indexOf('applewebkit') != -1) || (navigator.vendor == "Apple Computer, Inc."));
        var is_moz    = ((navigator.product == 'Gecko') && (!is_saf));
        if (!defaults.fadeInFirefox && is_moz) {opacityType = opacityTypes.none}
        else if (typeof sourceImage.style.opacity != 'undefined') {opacityType = opacityTypes.w3c}
        else if (typeof sourceImage.style.MozOpacity != 'undefined') {opacityType = opacityTypes.moz}
        else if (typeof sourceImage.style.KhtmlOpacity != 'undefined') {opacityType = opacityTypes.khtml}
        else if (typeof sourceImage.filters == 'object') {
            opacityType = (sourceImage.filters.length > 0 && 
                           typeof sourceImage.filters.alpha == 'object' &&
                           typeof sourceImage.filters.alpha.opacity == 'number') ? 
                           opacityTypes.ie : opacityTypes.w3c}
        else {opacityType = opacityTypes.none};
        opacityProperty = opacityProperties[opacityType];
    }

    function fade() {
        if (cloneImage && !cloneImage.complete) {return};
        repositionAbsoluteImage();
        
        var now = new Date().getTime();
        var timePassed = now - lastTime;
        lastTime = now;
        opacity -= (timePassed / defaults.duration);
        if (opacity < 0) {opacity = 0};
                
        if (opacity < (defaults.interval / defaults.duration)) {
            clearInterval(thisTimer);
            thisTimer = null;
            opacity = 0;
        }

        sourceImage.style[opacityProperty] = (opacityType == opacityTypes.ie) ? "alpha(opacity=" + Math.round(opacity * 100) + ")" : opacity;
        targetImage.style[opacityProperty] = (opacityType == opacityTypes.ie) ? "alpha(opacity=" + Math.round((1 - opacity) * 100) + ")" : 0.999 - opacity;
                
        if (_this.onFade && !onFadeCalled) {
            if (opacity < 0.5) {
                _this.onFade();
                onFadeCalled = true;
            }
        }

        if (opacity == 0) {
            opacity = 0.999;
            var tmpImage = sourceImage;
            sourceImage = targetImage;
            targetImage = tmpImage;
            if (_this.onComplete) {_this.onComplete()};
        }
    }
        
    function onResize() {
        if (document.addEventListener) { 
            window.addEventListener("resize", repositionAbsoluteImageOnResize, false); 
        } else {
            window.attachEvent('onresize', repositionAbsoluteImageOnResize); // IE 
        } 
    }
    
    function repositionAbsoluteImage() {
        // Must set targetImage left/top in a timer because otherwise offsets
        // reports the wrong values.
        if (targetImage.id == absoluteImageId && firstInterval) {
            targetImage.style.top = cloneImage.offsetTop + "px"; //cloneDiv in a different state
            targetImage.style.left = cloneImage.offsetLeft + "px"; //Need to repeat for FF benefit
            targetImage.style.left = cloneImage.offsetLeft + "px"; //gave wrong answer otherwise
            firstInterval = false;
        }
    }

    function repositionAbsoluteImageOnResize() {
        if (sourceImage.id == absoluteImageId) {
            sourceImage.style.top = cloneImage.offsetTop + "px"; //cloneDiv in a different state
            sourceImage.style.left = cloneImage.offsetLeft + "px"; //Need to repeat for FF benefit
        }
    }

    function createCloneImage(imageSrc, imageClass) {
        if (cloneDiv) {
            document.body.removeChild(cloneDiv);
            cloneImage = null;
            cloneDiv = null;
        }
        cloneDiv = sourceImage.parentNode.cloneNode(true);
        cloneDiv.style["position"] = "absolute";
        cloneDiv.style["top"] = "-10000px";
        cloneDiv.style["left"] = "0px";
        cloneImage = cloneDiv.getElementsByTagName("IMG")[0];
        cloneImage.className = imageClass;
        cloneImage.src = imageSrc;
        document.body.appendChild(cloneDiv);
    }
    
    function createTargetImage() {
        targetImage = sourceImage.cloneNode(false);
        targetImage.id += "_1";
        absoluteImageId = targetImage.id;
        targetImage.style["z-index"] = "30000";
        targetImage.style[opacityProperty] = (opacityType == opacityTypes.ie) ? "alpha(opacity=0)" : 0;
        // Must be absolute positioning, because that takes it out of the flow.
        // With relative, it's in the flow and that pushes the sourceImage to the left
        // (if there's room in the containing div's width)
        targetImage.style["position"] = "absolute";
        targetImage.style["top"] = "0px";
        targetImage.style["left"] = "0px";
        sourceImage.parentNode.insertBefore(targetImage, sourceImage.nextSibling);
    }
    
    return {
        apply: function(sourceImageId, options) {
            _this = this;
            sourceImage = document.getElementById(sourceImageId);
            sourceImage.style["filter"] = "alpha(opacity = 100)";
            sourceImage.parentNode.style.position = "relative";
            defaults = setOptions(options);
            onResize();
            setOpacityType();
            if (opacityType != opacityTypes.none) {createTargetImage()};
        },
        toggle: function (imageSrc, imageAlt, imageClass) {
            if (thisTimer) {return};
            if (opacityType == opacityTypes.none) {
                sourceImage.className = imageClass;
                sourceImage.alt = imageAlt;
                sourceImage.src = imageSrc;
                if (_this.onFade) {_this.onFade()};
                if (_this.onComplete) {_this.onComplete()};
            } else {
                if (targetImage.id == absoluteImageId) {createCloneImage(imageSrc, imageClass)};
                targetImage.className = imageClass;
                targetImage.alt = imageAlt;
                targetImage.src = imageSrc;
                onFadeCalled = false;
                firstInterval = true;
                lastTime = new Date().getTime();
                thisTimer = setInterval(fade, defaults.interval);
            }
        },
        setImageClass: function (className) {
            if (targetImage) {targetImage.className = className};
        },
        onFade: null,
        onComplete: null
    }
}();