// BEHAVIOUR CLASS: PROJECTOR SLIDESHOW (background)

// (c) 2006 Toowoomba Motor Village - all rights reserved

// FEATURES:    provides parameterised auto-advance carousel slideshow:
//              - to aid page readability, carousel pauses when mouse hovers over either of two nominated elements.
//              - carousel ADVANCES to next-slide by mouse-click on 1 nominated element.
//              - carousel ADVANCES to next slide ONLY if 2 or more slides have downloaded.
//              - silent failure for fatal-errors associated with host-environment.
// STATUS:      production release.     v0.98 rc
//              audit:   revise/review all comments to ensure current & accurate. caveat lector  - medium priority.
//              review: consider comprehensive checking of ProjctorB arguments                   - low    priority.
// TECH-NOTES:  ECMAScript v3. Protoclass based - prototype, etc, (not v4 formal classes) - see notes at end-of-script.
//              ProjctorB constructor mandates at least 2 slide URL arguments  - more are allowed (max urls = 248).
//              ProjctorB constructor mandates exactly  2 slide URL parameters (in function declaration parameter-list).
//              Typically, this class must pause when user has mouse over (hovering) of vital text to aid readability;
//              hence 'hover' events in UserInterfaceBg-class are enabled on instantiation to register the 'pause' state.

function ProjctorB( screenElementID,        // target element for slideshow         // CLASS CONSTRUCTOR //
                    slide1Time,             // first-slide  duration in secs
                    slideTime,              // other-slides duration in secs
                    resumeDelay,            // resume-after-pause delay in secs
                    loadDelay,              // start delay within instance, in secs (breathing-space, etc)
                    nextclickElmtID,        // target element  for next-click  - for optional, use ''
                    pauseElmt1ID,           // target element1 for hover-pause - for optional, use ''
                    pauseElmt2ID,           // target element2 for hover-pause - for optional, use ''
                    slidesFolder,           // relative to page  [use '' if images in same folder as page]
                    slideName1,             // mandatory (usually same as bkgd image of elementID if it has one)
                    slideName2 )            // mandatory - at least 2 image URL arguments! [only 2 URL parameters!]
{

  try
  {
    if (!(this instanceof ProjctorB))                      { throw new Error('ProjctorB: constructor CALLed as function!'); }
    if (!(arguments.length >= ProjctorB.length))           { throw new Error('ProjctorB: mandatory arguments missing'); }
    if (!window.document.getElementById(screenElementID))  { throw new Error('ProjctorB: screen-element invalid'); }

    ProjctorB.Encapsulators.call(this);                    // 'inherit' Encapsulators' features - class 'statics'

    var iFirstSlide = ProjctorB.length - 2;                                                             // 0-based index (2 image urls are mandatory)
    var slidesURLs  = this.encaseArgs(arguments, iFirstSlide, slidesFolder);                            // get array of all image-URLs
    var loadWait_ms = (loadDelay > 0.1) ? Math.round(loadDelay * 1000) : 100;                           // activation delay (browser breathing-space)

    this.carousel   = new ProjctorB.Carousel(this.bind(this,'onloadSlides'),
                                             this.bind(this,'onload2Slides'),
                                             slidesURLs);

    this.screen     = new ProjctorB.ScreenBg(screenElementID);

    this.changer    = new ProjctorB.AnimatorBg(this.bind(this,'showSlide'),
                                               slideTime, slide1Time, loadDelay, resumeDelay);

    this.user       = new ProjctorB.UserInterfaceBg(this.bind(this,'showSlide'),
                                                    this.bind(this,'pause'),
                                                    this.bind(this,'resume'),
                                                    nextclickElmtID, pauseElmt1ID, pauseElmt2ID);

    this.unload  = this.bindEvent(window, this.bind(this,'destroy'), 'unload');                         // function ref (evtHdlr) stored for unbinding
    this.timerID = window.setTimeout(this.bind(this,'start'), loadWait_ms);                             // schedule activation
  }
  catch(e)
  {
    if ((typeof this.timerID) == 'number')  { window.clearTimeout(this.timerID); }
  }

}                                                                                   // END: ProjctorB class-CONSTRUCTOR //


// class ProjctorB - M E T H O D S  (prototype)

ProjctorB.prototype.showSlide = function ()                                    // PLAY-LOOP [bg]: schedule next-frame, show slide
          {
              if (!ProjctorB.enabled)  { this.destroy(); return; }                   // master override to kill each instance
              this.changer.scheduleNext();
              if (this.paused)  { return; }
              this.screen.show(this.carousel.getNextSlideURL());
          };

ProjctorB.prototype.start = function ()                                        // ACTIVATION [bg]
          {
              try
              {
                  this.changer.start();
                  this.carousel.load();
                  this.loaded = true;
              }
              catch(e){this.destroy();}
          };

ProjctorB.prototype.onload2Slides = function ()                                // 2 slides LOADED into carousel permit normal operation
          {
              this.user.enableClick();
          };
ProjctorB.prototype.onloadSlides  = function (slideCount)  { };                // all slides loaded into carousel (unused)

ProjctorB.prototype.pause = function ()                                        // SUSPEND [bg-only]: stop slide changes, user reading
          {
              this.paused = true;
              if (!this.loaded)  { return; }
              this.changer.cancelNext();
          };
ProjctorB.prototype.resume = function ()                                       // CONTINUE [bg-only]: play slides as pause finished
          {
              this.paused = false;
              if (!(ProjctorB.enabled && this.loaded))  { return; }
              this.changer.resume();
          };

ProjctorB.prototype.destroy = function ()                                      // DEACTIVATE
          {
              try
              {
                  if ((typeof this.timerID) == 'number')
                  {
                      window.clearTimeout(this.timerID);
                  }
                  this.changer.destroy();
                  this.user.destroy();
                  this.screen.destroy();
                  this.carousel.destroy();
                  this.unbindEvent(window, this.unload, 'unload');
              }
              catch(e){}
          };

//                                                                                  // END: ProjctorB INSTANCE //



// class ProjctorB - C L A S S   F E A T U R E S   ( S T A T I C S )  -  public


// class ProjctorB - class STATICS, COMPONENT CLASSES

ProjctorB.ScreenBg =                                                                // CLASS CONSTRUCTOR //
          function (screenElementID)
          {
              this.elmtStyle   = window.document.getElementById(screenElementID).style;

              this.show        = function (url)
                                 {
                                     if (!url)  { return; }
                                     this.elmtStyle.backgroundImage = 'url(' + url + ')';
                                 };
              this.destroy     = function ()  { this.elmtStyle  = null; };
          };                                                                        // END: ScreenBg CLASS & CONSTRUCTOR //


ProjctorB.AnimatorBg =                                                              // CLASS CONSTRUCTOR //
          function (nextFrameCallback, frameDuration,
                    frame1Duration, frame1DurationElapsed, resumeDelay)
          {
              var rawTF1 = frame1Duration - frame1DurationElapsed;                            // note: frame#1 is first frame
              this.nextFrameNotify = nextFrameCallback;
              this.timeFrame       = frameDuration;
              this.timeFrame1      = (rawTF1 >= 0.001) ? rawTF1 : 0.001;
              this.resumeDelay     = resumeDelay;

              this.start           = function ()  { this.schedule(this.timeFrame1);  };
              this.resume          = function ()  { this.schedule(this.resumeDelay); };
              this.scheduleNext    = function ()  { this.schedule(this.timeFrame);   };
              this.cancelNext      = function ()
                                     {
                                         if ((typeof this.timerID) == 'number')
                                         {
                                             window.clearTimeout(this.timerID);
                                             delete this.timerID;
                                         }
                                     };
              this.schedule        = function (wait_secs)
                                     {
                                         this.cancelNext();
                                         var wait_ms = Math.round(wait_secs * 1000);
                                         this.timerID = window.setTimeout(this.nextFrameNotify, wait_ms);
                                     };
              this.stop            = function ()  { this.cancelNext(); };
              this.destroy         = function ()  { this.cancelNext(); };
          };                                                                        // END: AnimatorBg CLASS & CONSTRUCTOR //


ProjctorB.UserInterfaceBg =                                                         // CLASS CONSTRUCTOR //
          function (clickCallback, enterCallback, leaveCallback,
                    clickElmtID, hoverElmt1ID, hoverElmt2ID)
          {
              this.clickNotify = clickCallback;
              this.enterNotify = enterCallback;
              this.leaveNotify = leaveCallback;
              this.clickElmt   = window.document.getElementById(clickElmtID);
              this.hoverElmt1  = window.document.getElementById(hoverElmt1ID);
              this.hoverElmt2  = window.document.getElementById(hoverElmt2ID);
              this.nextTitle   = '*** click to change \'view\' ***';

              ProjctorB.Encapsulators.call(this);                                             // inherit Encapsulators' features

              if (this.hoverElmt1)                                                            // hover(d) enabled immediately to register actions
              {
                  this.bindEvent(this.hoverElmt1, enterCallback, 'mouseover');
                  this.bindEvent(this.hoverElmt1, leaveCallback, 'mouseout');
              }
              if (this.hoverElmt2 && (this.hoverElmt2 != this.hoverElmt1))                    // note: avoid duplicated binding
              {
                  this.bindEvent(this.hoverElmt2, enterCallback, 'mouseover');
                  this.bindEvent(this.hoverElmt2, leaveCallback, 'mouseout');
              }

              this.enableClick = function ()                                                  // click-element enable (typically deferred)
                                 {
                                     var el = this.clickElmt;
                                     if (!el)  { return; }                                    // non existent element - abandon
                                     this.bindEvent(el, this.clickNotify, 'click');
                                     var tl = el.title;
                                     if (tl)  { tl += ' \n'; }
                                     el.title = tl + this.nextTitle;
                                     el.style.cursor = 'pointer';
                                 };
              this.destroy     = function ()                                                             // failsafe against possible IE memory leaks
                                 {
                                     if (this.hoverElmt1)
                                     {
                                         this.unbindEvent(this.hoverElmt1, this.enterNotify, 'mouseover');
                                         this.unbindEvent(this.hoverElmt1, this.leaveNotify, 'mouseout');
                                     }
                                     if (this.hoverElmt2 && (this.hoverElmt2 != this.hoverElmt1))                    // note: avoid duplicated binding
                                     {
                                         this.unbindEvent(this.hoverElmt2, this.enterNotify, 'mouseover');
                                         this.unbindEvent(this.hoverElmt2, this.leaveNotify, 'mouseout');
                                     }
                                     if (this.clickElmt)
                                     {
                                         this.unbindEvent(this.clickElmt, this.clickNotify, 'click');
                                     }
                                     this.clickElmt  = null;
                                     this.hoverElmt1 = null;
                                     this.hoverElmt2 = null;
                                 };
          };                                                                        // END: UserInterfaceBg CLASS & CONSTRUCTOR //



// COMMON PROJECTOR-LIBRARY - (used in Projector, ProjctorB)

ProjctorB.Carousel =                                                                // CLASS CONSTRUCTOR //   common projector-lib
          function (onloadAllSlidesCallback, onload2SlidesCallback, urls)
          {
              this.onloadAllNotify   = onloadAllSlidesCallback;
              this.onload2SldsNotify = onload2SlidesCallback;
              this.size              = urls.length;             // 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 1-based !!!   (slides[0] unused)

              this.getNextSlideURL = function ()
                                     {
                                         if (this.received < 2)  { return false; }                 // 'false' is type-safe as url can't be only a 0
                                         this.slideNum += 1;
                                         if (this.slideNum > this.received)  { this.slideNum = 1; }
                                         return (this.slides[this.slideNum].url);
                                     };

              this.rewind          = function ()
                                     {
                                         this.slideNum = 1;                                        // slide #2 is to be next-slide
                                     };
              this.destroy         = function ()
                                     {
                                         this.received = 0;
                                         var s = this.size;
                                         for (var i=1; i<=s; i++)  { this.slides[i].destroy(); }
                                         this.slides = null;
                                     };

              this.load            = function ()                                                   // retrieve serially (see also 'onloadSlide')
                                     {
                                         if (this.filled)  { return; }                             // prevent multiple loads
                                         window.status = this.size;
                                         this.slides[1].load();                                    // note: first slide is typically already cached
                                     };
              this.onloadSlide     = function (slideNum)
                                     {
                                         this.received += 1;
                                         if (this.received == 2)  { this.onload2SldsNotify(); }
                                         if (this.received < this.size)
                                         {
                                             window.status = this.size - slideNum;
                                             this.slides[slideNum + 1].load();
                                         }
                                         else
                                         {
                                             this.filled = true;
                                             window.status = '';
                                             this.onloadAllNotify(this.received);
                                         }
                                     };

              var s = this.size;
              for (var i=1; i<=s; i++)
              {
                 this.slides[i] = new ProjctorB.Slide(ProjctorB.bind(this,'onloadSlide'), i, urls[i-1]);         // (urls[] is 0-based)
              }
          };                                                                        // END: Carousel CLASS & CONSTRUCTOR //


ProjctorB.Slide =                                                                   // CLASS CONSTRUCTOR //   common projector-lib
          function (onloadCallback, slideNum, url)
          {
              ProjctorB.Encapsulators.call(this);                                             // inherit Encapsulators' features
              this.onloadNotify = onloadCallback;
              this.slideNum     = slideNum;
              this.loaded       = false;
              this.url          = url;
              this.img          = new Image();                                                // note: image obj is retained to ensure browser caching
              this.img_onload   = this.bind(this,'onload');                                   // the function ref must be stored for unbinding
          };                                                                        // END: Slide class CONSTRUCTOR //
ProjctorB.Slide.prototype.load    = function ()
                                    {
                                        if (window.opera && (this.slideNum == 1))  { this.onload(); return; }          // opera remedial (see tech-notes)
                                        this.bindEvent(this.img, this.img_onload, 'load');
                                        this.img.setAttribute('src', this.url);               // initiates image retrieval
                                    };
ProjctorB.Slide.prototype.onload  = function ()                                               // image-loaded event-handler
                                    {
                                        this.loaded = true;
                                        this.onloadNotify(this.slideNum);
                                    };
ProjctorB.Slide.prototype.destroy = function ()
                                    {
                                        try
                                        {
                                            this.unbindEvent(this.img, this.img_onload, 'load');
                                            this.loaded = false;
                                            this.img = null;
                                        }
                                        catch(e){}
                                    };
//                                                                                  // END: Slide CLASS //



// class - class STATICS, PROPERTIES & METHODS - (the methods & classes are non Projector-specific)

ProjctorB.enabled = true;                                                           // property: master-state for class

ProjctorB.Encapsulators =                                                           // CLASS CONSTRUCTOR // generic abstract - typically inherited by call/apply methods
          function ()
          {
              this.bind        = ProjctorB.bind;
              this.encaseArgs  = ProjctorB.encaseArgs;
              this.bindEvent   = ProjctorB.bindEvent;
              this.unbindEvent = ProjctorB.unbindEvent;
          };                                                                        // END: Encapsulators class //

ProjctorB.bind        = function (obj, funct)                                       // generic: bind-as-method - method bindery (callback-object factory, etc)
                        {
                            var method = (funct instanceof Function) ? funct : obj[funct];    // function ('method') may be passed by name or reference
                            return  function method_proxy ()                                  // note: by returning function-ref, closure persists
                                    {
                                        try
                                        {
                                            return method.apply(obj, arguments);              // invoke with args as method of obj, returning result if any
                                        }
                                        catch(e){return null;}                                // (return is to satisfy JavaScript-Lint)
                                    };
                        };
ProjctorB.bindEvent   = function (obj, evtHandler, evt_w3c, onevt_ie)               // generic: DOM-event bindery (deprecated bindings not supported)
                        {
                          try
                          {
                            if (window.addEventListener)                                      // w3c-DOM method?
                            {
                                var capture = false;
                                obj.addEventListener(evt_w3c, evtHandler, capture);           // w3c standards-compliant binding
                            }
                            else
                            {
                                if (window.attachEvent)                                       // IE method?
                                {
                                    var evtIE = onevt_ie || ('on' + evt_w3c);                 // optional arg; default if not supplied
                                    obj.attachEvent(evtIE, evtHandler);                       // non-standard binding for IE
                                }
                            }
                            return evtHandler;                                                // return function ref for unbinding
                          }
                          catch(e){return null;}
                        };
ProjctorB.unbindEvent = function (obj, evtHandler, evt_w3c, onevt_ie)               // generic: DOM-event unbindery
                        {
                          try
                          {
                            if (window.removeEventListener)
                            {
                                var capture = false;
                                obj.removeEventListener(evt_w3c, evtHandler, capture);
                            }
                            else
                            {
                                if (window.detachEvent)
                                {
                                    var evtIE = onevt_ie || ('on' + evt_w3c);
                                    obj.detachEvent(evtIE, evtHandler);
                                }
                            }
                          }
                          catch(e){}
                        };
ProjctorB.encaseArgs  = function (argsObject, iFirstArg, optionalFolder)            // generic: parse function-arguments & return array
                        {
                            var fldr = (optionalFolder) ? (optionalFolder + '/') : '' ;
                            var len  = argsObject.length - iFirstArg;                              // iFirstArg must be idx 0-based
                            var args = new Array();                                                // indexed 0-based
                            for (var i=0; i<len; i++)
                            {
                                args[i] = fldr + argsObject[i + iFirstArg];
                            }
                            return args;
                        };


//                                                                                  // END: ProjctorB CLASS //



// TECH-NOTES - ProjctorB CLASS & RELATED CLASSES
//
// see end-notes associated with class Projector (Projector.js)


// (c) 2006 Ian Brown

// end script
