// BEHAVIOUR CLASS: PROJECTOR SLIDESHOW (foreground)

// (c) 2006 Toowoomba Motor Village - all rights reserved

// DESCRIPTION: carousel-projector slide-show - projection-screen is an image-element.
// FEATURES:    provides parameterised auto-advance carousel slideshow:
//              - carousel ADVANCES to next-slide immediately as mouse HOVERS over image-element.
//              - carousel ADVANCES to next-slide when image-element is CLICKED.
//              - carousel ADVANCES to next slide ONLY once 2 slides downloaded.
//              - TWO PLAY-MODES (mutually exclusive): DEMAND (on-demand by user) & AUTO.
//              - DEMAND slideshow: starts when mouse hovers, playing normal-rate; stops when mouse leaves.
//              - AUTO   slideshow: starts on instantiation, playing normal-rate.
//              - auto   slideshow: uses fast-advance mode WHILST mouse HOVERS over image-element.
//              - auto   slideshow: uses slow-advance mode AFTER mouse has ONCE HOVERED over & then left.
//              - silent abort & shutdown when fatal-errors. Note: at least 2 slides is mandatory.
// STATUS:      prototype operation.     v0.96 rc 
//              review:  'auto/demand' play-mode implementation (Timer class, etc)               - high   priority.
//              audit:   revise/review all comments to ensure current & accurate. caveat lector  - medium priority.
//              upgrade: consider comprehensive checking of Projector class arguments            - low    priority.
// TECH-NOTES:  EcmaScript3 encapsulated non-prototype classes (multi-instantiation).  [see notes at end-of-script]



function Projector (                                                               // CLASS CONSTRUCTOR //
                      screenElementID,     // target element for slideshow
                      slideTimePlay,       // play slide-duration in secs (normal play rate)
                      retainOnPause,       // on-pause: true == RETAIN current-slide, false == REWIND to start
                      autoPlay,            // playmode: true == AUTO-PLAY of slides,  false == DEMAND-PLAY (hover)
                      pauseTime,           // auto-play: pause-duration [use zero (0) if on-demand-play]
                      loadDelay,           // start delay within instance, in secs (breathing-space, etc)
                      slidesFolder,        // relative to page  [use '' if images in same folder as page]
                      slideName1,          // mandatory (usually same as bkgd of elementID if it has one)
                      slideName2           // mandatory - at least 2 image URLs are mandatory, more are allowed
                   )
{

  try
  {
  
    var ME = this;                                                   // persistent instance-reference

    var ARGUMENTS_MANDATORY = 9;                                     // fixed args (including mandatory first 2 image urls)
    
    if (!(this instanceof Projector))                      { throw new Error('Projector: constructor CALLed as function!'); }
    if (!(arguments.length >= ARGUMENTS_MANDATORY))        { throw new Error('Projector: mandatory arguments missing'); }
    if (!window.document.getElementById(screenElementID))  { throw new Error('Projector: screen-element invalid'); }
    


    if (autoPlay)   
    {   
      this.manualPlay    = function ()
                           {
                               try
                               {
                                   ME.changer.useFastRate();                   // fast-forward
                                   ME.showNextSlide();
                               }
                               catch(e){ME.unload();}
                           };
      this.manualPause   = function () 
                           {
                               try
                               {
                                   ME.changer.useSlowRate();                   // adjust frame-rate given user 'touch'
                                   ME.suspend();
                               }
                               catch(e){ME.unload();}
                           };
      this.manualAdvance = function ()  { ME.showNextSlide(); };
    }
    else
    {   
      this.manualPlay    = function ()  { ME.showNextSlide(); };
      this.manualPause   = function ()  { ME.suspend(); };
      this.manualAdvance = function ()
                           {
                               try
                               {
                                   ME.changer.useSlowRate();                   // adjust frame-rate given user click
                                   ME.showNextSlide();
                               }
                               catch(e){ME.unload();}
                           };
    }
    
    this.suspend         = function ()                     
                           {
                               try
                               {
                                   ME.changer.pause();
                                   if (ME.autoRewind)  
                                   { 
                                       ME.carousel.rewind();
                                       ME.screen.restore(); 
                                   }
                               }
                               catch(e){ME.unload();}
                           };
                         

    this.showNextSlide   = function ()
                           {
                               try
                               {
                                   if (!Projector.enabled)  { ME.unload(); return; }     // master override to kill each instance
                                   ME.changer.scheduleNext();
                                   ME.screen.show(ME.carousel.nextSlideURL());
                               }
                               catch(e){ME.unload();}
                           };

    this.onloadSlides    = function (slideCount)  { };                         // unused
    this.onload2Slides   = function ()  { ME.user.enableClick(); };

    this.unload          = function ()
                           {
                               try
                               {
                                   ME.changer.cancelNext();
                                   ME.carousel.unload();
                               }
                               catch(e){}
                           };
    this.load            = function ()
                           {
                               try
                               {
                                   ME.changer.start();
                                   ME.carousel.load();
                                   ME.user.removeTitle();
                                   ME.user.enableHover();
                               }
                               catch(e){ME.unload();}
                           };
     
    var slidesURLs  = Projector.encaseArgs(arguments, (ARGUMENTS_MANDATORY - 2), slidesFolder);    // get array of all image-URLs  (class-static)
    var loadWait_ms = (loadDelay > 0.1) ? Math.round(loadDelay * 1000) : 100;                      // activation delay (browser breathing-space)

    this.autoRewind = !(retainOnPause);                                                            // true == after a pause, play show from start
    this.carousel   = new Carousel(this.onloadSlides, this.onload2Slides, slidesURLs);
    this.screen     = new Screen(screenElementID);
    this.changer    = new Animator(this.showNextSlide, slideTimePlay, autoPlay, pauseTime, loadDelay);
    this.user       = new UserInteraction(this.manualPlay, this.manualPause, this.manualAdvance, screenElementID);
    Projector.bindEvent(window, this.unload, 'unload', 'onunload');                                // class static-method
    
    var timerLoad = window.setTimeout(this.load, loadWait_ms);	                                   // schedule activation (induces a closure)
  }
  catch(e)
  {
    if (timerLoad)  { window.clearTimeout(timerLoad); }
  }



    // class Projector - private classes (components)

    function Screen (screenElementID)                                               // CLASS CONSTRUCTOR //
    {
        var Me = this;                                                         // persistent instance-reference

        this.screenElmt  = window.document.getElementById(screenElementID);
        this.originalURL = this.screenElmt.getAttribute('src');
        
        this.show        = function (url)                                      
                           {
                               if (!url)  { return; }
                               Me.screenElmt.setAttribute('src', url);
                           };
                           
        this.restore     = function ()                                         
                           {
                               Me.screenElmt.setAttribute('src', Me.originalURL);
                           };
    }                                                                               // END: Screen CLASS & CONSTRUCTOR //


    function Animator (nextFrameCallback, frameDuration, autoPlay,                  // CLASS CONSTRUCTOR // 
                       pauseDuration, firstFrameDurationThusFar)                                
    {
        var Me = this;                                                         // persistent instance-reference

        var ratioFast = 3.0;                                                   // fastforward    ratio relative to play-rate     
        var ratioSlow = 0.5;                                                   // unhurried-play ratio relative to play-rate      
        this.nextFrameNotify = nextFrameCallback;
        this.timeFast        = frameDuration / ratioFast;                                                  
        this.timePlay        = frameDuration;                                  // normal play frame-duration
        this.timeSlow        = frameDuration / ratioSlow;                                                   
        this.timeNext        = 0.0;                                            // actual duration of next-frame
        this.timerRef        = null;

        this.useFastRate     = function ()  { Me.timeNext = Me.timeFast; };
        this.usePlayRate     = function ()  { Me.timeNext = Me.timePlay; };
        this.useSlowRate     = function ()  { Me.timeNext = Me.timeSlow; };
 
        this.scheduleNext    = function ()  { Me.schedule(Me.timeNext); };
        this.cancelNext      = function ()  { if (Me.timerRef)  {window.clearTimeout(Me.timerRef);} };
        this.schedule        = function (secs)
                               {
                                   Me.cancelNext();
                                   var wait_ms = Math.round(secs * 1000);
                                   Me.timerRef = window.setTimeout(Me.nextFrameNotify, wait_ms);
                               };
        if (autoPlay)   
        {   
            var ts = this.timePlay - firstFrameDurationThusFar;
            this.timeStart   = (ts > 0) ? ts : 0.001;
            this.timePause   = pauseDuration;
            this.start       = function ()  { Me.schedule(Me.timeStart); };
            this.pause       = function ()  { Me.schedule(Me.timePause); };
        }
        else
        {   
            this.start       = function ()  { Me.pause(); }; 
            this.pause       = function ()  { Me.cancelNext(); };
        }
        this.usePlayRate();
    }                                                                               // END: Timer CLASS & CONSTRUCTOR //


    function UserInteraction (enterCallback, leaveCallback, clickCallback,          // CLASS CONSTRUCTOR //   user-interface module
                              elementID) 
    {
        var Me = this;                                                                             // persistent instance-reference
        
        this.enterNotify = enterCallback;
        this.leaveNotify = leaveCallback;
        this.clickNotify = clickCallback;
        this.elmnt = window.document.getElementById(elementID);

        this.enableInterface = function ()
                               {
                                   Me.enableHover(); 
                                   Me.enableClick(); 
                               };
        this.enableHover     = function ()
                               {
                                   Projector.bindEvent(Me.elmnt, Me.enterNotify, 'mouseover', 'onmouseover');
                                   Projector.bindEvent(Me.elmnt, Me.leaveNotify, 'mouseout',  'onmouseout');
                               };
        this.enableClick     = function ()
                               {
                                   Projector.bindEvent(Me.elmnt, Me.clickNotify, 'click', 'onclick');
                                   Me.elmnt.style.cursor = 'pointer';
                               };
        this.removeTitle       = function ()  { Me.elmnt.title = ''; };                            // avoids disruption to hover-state (where necessary)
    }                                                                               // END: UserInteraction CLASS & CONSTRUCTOR //



    // class Carousel - common projector-lib - (used in Projector, ProjectorBg)

    function Carousel (onloadAllSlidesCallback, onload2SlidesCallback, urls)        // CLASS CONSTRUCTOR // 
    {
        var Me = this;                                                         // persistent instance-reference

        this.onloadAllNotify   = onloadAllSlidesCallback;
        this.onload2SldsNotify = onload2SlidesCallback;
        this.size              = urls.length;             // carousel capacity in slides
        this.filled            = false;                   // carousel filled? (all slides fully downloaded?)
        this.received          = 0;                       // count of slides fully downloaded
        this.slideNum          = 1;                       // current slide (at start, assumes 1st slide shown as static)
        this.slides            = new Array();             // INDEXED AS base-1 !!!

        this.nextSlideURL = function ()
                            {
                                if (Me.received < 2)  { return false; }                  // 'false' is type-safe as url can't be only a 0
                                Me.slideNum += 1;
                                if (Me.slideNum > Me.received)  { Me.slideNum = 1; }
                                return (Me.slides[Me.slideNum].url);
                            };

        this.rewind       = function ()
                            {
                                Me.slideNum = 1;                               // slide #2 is to be next-slide
                            };
        this.unload       = function ()
                            {
                                if (Me.filled)  { return; }                    // if no image is being downloaded, leave
                                Me.slides[Me.received+1].unload();
                            };
        this.unloadAll    = function ()                                        // not used
                            {
                                Me.received = 0;
                                var s = Me.size;
                                for (var i=1; i<=s; i++)  { Me.slides[i].unload(); }
                            };

        this.load         = function ()                                        // retrieve serially (see also 'onloadSlide')
                            {
                                if (Me.filled)  { return; }                    // prevent multiple loads
                                window.status = Me.size;
                                Me.slides[2].load();                           // 2nd slide
                            };
        this.onloadSlide  = function (slideNum)
                            {
                                Me.received += 1;
                                if (Me.received == 2)  { Me.onload2SldsNotify(); }
                                if (Me.received < Me.size)
                                {
                                    window.status = Me.size - (slideNum - 1);
                                    var nextSld = slideNum + 1;
                                    if (nextSld > Me.size)  
                                    { 
                                       nextSld = 1;
                                       if (window.opera)  { Me.received = Me.size; }        // opera remedial (see tech-notes)
                                    }
                                    Me.slides[nextSld].load();           
                                }
                                else
                                {
                                    Me.filled = true;
                                    window.status = '';
                                    Me.onloadAllNotify(Me.received);
                                }
                            };

        var s = this.size;
        for (var i=1; i<=s; i++)
        {
           this.slides[i] = new Slide(this.onloadSlide, i, urls[i-1]);
        }


        // class Carousel - private classes (components)

        function Slide (onloadCallback, slideNum, url)                              // CLASS CONSTRUCTOR //
        {
            var me = this;                                                     // persistent instance-reference

            this.onloadNotify = onloadCallback;
            this.slideNum     = slideNum;
            this.loaded       = false;
            this.url          = url;
            this.img          = new Image();
            this.img.onload   = null;
            this.img.setAttribute('src', null);

            this.load   = function ()
                          {
                              me.img.onload = function ()                      // image-loaded event-handler
                                              {
                                                  try
                                                  {
                                                       me.loaded = true;
                                                       me.onloadNotify(me.slideNum);
                                                  }
                                                  catch(e){}
                                              };
                              me.img.setAttribute('src', me.url);              // initiates image retrieval
                          };
            this.unload = function ()
                          {
                              me.loaded = false;
                              me.img.onload = null;
                              me.img.setAttribute('src', null);
                              me.img = null;
                          };
        }                                                                           // END: Slide     CLASS & CONSTRUCTOR //

    }                                                                               // END: Carousel  CLASS & CONSTRUCTOR //

}                                                                                   // END: Projector CLASS-CONSTRUCTOR //



// class Projector - class features (statics), public (can't be private) - generic methods are non class-specific

Projector.enabled    = true;                                                   // property: master-state for class
Projector.encaseArgs = function (argsObject, iFirstArg, optionalFolder)        // generic: parse function-arguements & return array
                       {
                           var folder = (optionalFolder) ? (optionalFolder + '/') : '' ;
                           var nURLs  = argsObject.length - iFirstArg;                              // iFirstArg is idx base-0
                           var args   = new Array();                                                // idx base-0
                           for (var i=0; i<nURLs; i++)  
                           {
                               args[i] = folder + argsObject[i + iFirstArg];
                           }
                           return args;                      
                       };
Projector.bindEvent  = function (obj, evtHandler, onEvt_W3C, onEvt_MSIE)        // generic: DOM-event bindery
                       {
                           var CAPTURE  = false;
                           var W3C      = !!(window.addEventListener);
                           var MSIE     = !!(window.attachEvent);
                           if (W3C)  { obj.addEventListener(onEvt_W3C, evtHandler, CAPTURE); }
                           else 
                           if (MSIE) { obj.attachEvent(onEvt_MSIE, evtHandler); }
                       };

//                                                                                  // END: Projector CLASS //




// TECH-NOTES - PROJECTOR CLASS & RELATED CLASSES                    
//
// NOTES: [1]   The Projector class enhances a complete web-page by adding non-essential functionality.
//              The page needs to be complete and not dependant on this class as the script-engine might be
//              disabled in some browsers. For this reason, the image that is the first slide should
//              be used in the web-page markup/styling as normal (as would be done without the class).
//              Hence, the Projector class assumes that first slide is visible to the user at pageload,
//              and displays the second slide at the appropriate time; the initial slide displayed by the
//              class is actually the second slide in the carousel.
//        [2]   When the Projector class is instantiated, the instance-reference is typically not stored
//              explicitly. Such a reference is unnecessary as [a] the class does not need
//              public instance 'features', and, [b] instance remains in existence due to various
//              external references - those assigned to the DOM (unload, etc) & global-object (timer-callback).
//        [3]   These classes are time-based: practically all methods are invoked either via
//              a system-timer callback, ondownload callback, or occasionally, a inter-class callback].
//        [4]   Opera 9 fails to trigger the 'onload' event for an image that has been loaded by the host-page 
//              (typically the case with slide1). Opera-specific remedial-code in Carousel.onloadSlide method  
//              partially compensates (so all slides are displayed) - this remedial-code shall be removed 
//              once defective Operas are no longer in the public domain of the market segment 
//              (possibly as late as 2008 or 2010? given the bug exists in dec2006).
//              Note: this bug manifests with most methods of loading a page into Opera, but not all -
//                    in one case, it works correctly & has identical performance to IE & FireFox!
//
// CONVENTIONS: Instance references are formalised as an custom object-reference (me) to attain persistency:
//              - 'me' is an persistent reference to the instance (used when 'this' not applicable).
//              - 'me' (not 'this') MUST be used within methods invoked directly of indirectly via call-backs.
//              - 'me' is expressed in differing case in nested classes purely for reader clarity.
//              - In these classes (and similar classes which use of system-methods & other callbacks)
//                both 'this' & 'me' instance object-references are used in the following manner:
//                  - 'this' refers to instance AT 'constructor-time'. Thus, 'this' tags class-constructor
//                     stage, a side-effect of which is improved readability.
//                  - 'me' MUST be used to refer to the instance AFTER 'constructor-time'
//                     (but might be used if methods are called at constructor-time).
//                  - 'this' must NOT be used after constructor stage, as it refers elsewhere (to the calling-object,
//                     which is often the global-object given use of system-methods in classes).
//              - To 'tag' closures, 'me' is used explicitly, even if other identifiers are in lexical scope (parameters, etc);
//                this convention may result in additional class properties than otherwise (trivial given clarity).
//                The single exception (timerLoad) of the Projector constructor is noted inline.
//              - Public static class-features (properties & members) require a suitable host-object - the
//                class constructor-function itself is ideal.
//              - For frequent reference to public static class-members (not the case in these classes),
//                an additional reference 'we' could be used, to access & tag such public static class-members
//                for convenience (and reference speed).
//
// PATTERN:     MeClosures - full-callback to instance - multi-instantiation.
//              - encapsulated classes using constructor-persistence via closures.
//              - the class is encapsulated in external terms, but is within scope of internal nested-classes.
//              - encapsulation/privacy is intended to avoid typical unintentional misuse (non maleficent).
//              - stronger external instance-privacy, if warranted, requires addition of getters/setters, etc.
//              - class reserves only *one* global-identifier (Projector) - no global-variables used.
//
//              Callbacks into instances are attained by use of higher-order functions with their closures (as in LISP)
//              and do have a cost, using more memory per instance than ecmaScript prototype-classes.
//              In classes such as these, the memory overhead is trivial & resulting encapsulation worth the cost.
//              Browser memory-leaks with patterns using closures are known bugs (esp older browsers)
//              and need to be at least considered. Memory-leaks, if any, are not apparent with these classes,
//              would probably be miniscule, and, probably unimportant given the transient page-life of a browser.


// (c) 2006 Ian Brown

// end script

