/*
Class: PinupController

Singleton pattern PinupController that will take care of user interaction.
Google-API call lat & lng as acronym, on the other hand, 
Yahoo-API call lat & lon as acronym...

ToDos:
  - error check for no ClientLocation people...

License:
  MIT-style license.

Author:
  Takashi Mizohata <beatak@nydd.org>

Copyright:
  2008 [nydd](http://code.nydd.org/).

Code & Documentation:
  [nydd jslib](http://code.nydd.org/).

Notes:
  - for Member methods named: init*** cannot use PinupController.getInstance(), use "this" keyword instead.
  - for Member methods named: on*** cannot use "this" keyword, use PinupController.getInstance() instead.
*/


var PinupController = function()
{
  throw new Error("This is Singleton pattern. Please use getInstance() instead.");
}


/*
Method: getInstance

static method to get instance of PinupController

Return:
  (PinupController)
*/
PinupController.getInstance = function ()
{
  PinupController.initObject();
  return PinupController.getInstance();
}


/*
Method: initObject

private static method to initialize the object.

Return:
  undefined
*/
PinupController.initObject = function ()
{
  delete PinupController.getInstance;
  var InnerClass = function ()
  {
    this.initInstance();
    delete this.initInstance;
  }
  InnerClass.prototype = PinupController.prototype;
  InnerClass.prototype.constructor = PinupController;
  var instance = new InnerClass();

  PinupController.getInstance = function ()
  {
    return instance;
  }
  delete PinupController.initObject;
}


// Actual implementation begins here
// _______________________________________________________________________

// NOTE FOR IMPLEMENTATION
// _____________________________________________________________________
// timing of animation firing should be after img.onload.
// so that transition will be more smooth.


/*
Method: initInstance

initialization of PinupController

Return:
  undefined
*/
PinupController.prototype.initInstance = function ()
{
  console.log('PinupController::initInstance');
  lap();

  // CONSTANT-ish
  this.MILSEC_INITIAL_WAIT       = 200;
  this.MILSEC_RESIZE_WAIT        = 18;
  this.MILSEC_FADE_IN            = 2400;
  this.MILSEC_FADE_OUT           = 2000;
  this.MILSEC_SWITCHING_WAIT     = 15000;
  this.SEC_FADE_IN               = 2.4;
  this.SEC_FADE_OUT              = 2;
  this.PIX_FRAME_WIDTH           = 10;
  this.PIX_ICON_EACH_SIDE        = 16;
  this.SYNC_INDEX_IMG_LOAD       = 0;
  this.SYNC_INDEX_SWITCH_TIMER   = 1;
  this.SYNC_INDEX_ICON_LOAD      = 2;

  // geocode
  this.cities  = ['New York', 'London', 'Paris', 'Berlin', 'Stockholm', 'Tokyo', 'Hong Kong', 'Beijing', 'Rome', 'Barcelona', 'Moscow', 'Dubai'];
  this.geocode = {};
  this.geocode['Barcelona'] = {'lat': 41.387853, 'lon': 2.169499};
  this.geocode['Beijing']   = {'lat': 39.918624, 'lon': 116.396627};
  this.geocode['Berlin']    = {'lat': 52.526248, 'lon': 13.409157};
  this.geocode['Dubai']     = {'lat': 25.285679, 'lon': 55.307922};
  this.geocode['Hong Kong'] = {'lat': 22.284609, 'lon': 114.157948};
  this.geocode['London']    = {'lat': 51.499981, 'lon': -0.126214};
  this.geocode['Moscow']    = {'lat': 55.754651, 'lon': 37.617702};
  this.geocode['New York']  = {'lat': 40.706718, 'lon': -74.009356};
  this.geocode['Paris']     = {'lat': 48.856583, 'lon': 2.349701};
  this.geocode['Rome']      = {'lat': 41.902341, 'lon': 12.456951};
  this.geocode['Stockholm'] = {'lat': 59.331088, 'lon': 18.060837};
  this.geocode['Tokyo']     = {'lat': 35.681283, 'lon': 139.766006};
  this.cityIndex            = null;
  this.latitude             = null;
  this.longitude            = null;

  // dom elements
  this.elm = {};
  this.elm.container    = $('#container').get(0);
  this.elm.header       = $('#header').get(0);
  this.elm.icons        = $('#icons').get(0);
  this.elm.authorInfo         = $('#authorInfo').get(0);
  this.elm.authorInfo.picture = $('div.picture',  this.elm.authorInfo).get(0);
  this.elm.authorInfo.legend  = $('p.legend',     this.elm.authorInfo).get(0);
  this.elm.authorInfo.author  = $('p.author',     this.elm.authorInfo).get(0);
  this.elm.authorInfo.cache   = $('div.cache',    this.elm.authorInfo).get(0);
  this.elm.nameOfCity     = $('#nameOfCity').get(0);
  this.elm.nameOfCity.swf = $('#swfTitler').get(0);
  this.elm.docktab = $('#docktab').get(0);
  this.elm.postcard             = $('#postcard').get(0);
  this.elm.postcard.description = $('div.description', this.elm.postcard).get(0);
  this.elm.frame        = {};
  this.elm.frame.top    = $('div#frameTop').get(0);
  this.elm.frame.right  = $('div#frameRight').get(0);
  this.elm.frame.bottom = $('div#frameBottom').get(0);
  this.elm.frame.left   = $('div#frameLeft').get(0);
  this.elm.picture            = $('#picture').get(0);
  this.elm.picture.showing    = $('div.showing',    this.elm.picture).get(0);
  this.elm.picture.transition = $('div.transition', this.elm.picture).get(0);
  this.elm.picture.tank       = $('div.tank',       this.elm.picture).get(0);
  this.elm.picture.cache      = $('div.cache',      this.elm.picture).get(0);
  this.elm.icons         = $('#icons').get(0);
  this.elm.icons.google  = $('#icons div.google').get(0);
  this.elm.icons.flickr  = $('#icons div.flickr').get(0);
  this.elm.icons.grf     = $('#icons div.grf').get(0);

  // other instance variables
  this.timerIdOnResize    = null;
  this.timerIdOnSwitching = null;

  this.photosKey      = 'pinupPhotos';
  this.photosArray    = [];
  this.photosHash     = {};
  this.photosPointer  = null;

  this.authorsKey     = 'pinupAuthors';
  this.authorsArray   = [];
  this.authorsHash    = {};

  this.bulkAuthorsKey = 'bulkAuthors';

  this.initializeSychronizer();
  this.nextTranisition = null;
  this.transitionCode  = ['fade']; //, 'starwipe'

  this.current            = {};
  this.current.elmImg     = null;
  this.current.elmIcon    = null;
  this.current.arrAuthors = null;

  this.isOpening = true;
  this.enabledAuthorInfo = false;

  this.icon = [this.elm.icons.google, this.elm.icons.flickr, this.elm.icons.grf];
  this.dictionary = new Dictionary();
  this.titler = null;
  this.notifier = null;

  // function cache
  this.fnCache = {};
  this.fnCache.onResize               = jQuery.scope(this, this.onResize);
  this.fnCache.onResizePrivate        = jQuery.scope(this, this.onResizePrivate);
  this.fnCache.onSwitching            = jQuery.scope(this, this.onSwitching);
  this.fnCache.callbackGetByGeocode   = jQuery.scope(this, this.callbackGetByGeocode);
  this.fnCache.callbackFetchByGeocode = jQuery.scope(this, this.callbackFetchByGeocode);
  this.fnCache.callbackGetAuthorInfo  = jQuery.scope(this, this.callbackGetAuthorInfo);
  this.fnCache.liftUpPicture          = jQuery.scope(this, this.liftUpPicture);
  this.fnCache.accessGetAuthors       = jQuery.scope(this, this.accessGetAuthors);
  this.fnCache.callbackGetAuthors     = jQuery.scope(this, this.callbackGetAuthors);
  this.fnCache.reintroduceDocktab     = jQuery.scope(this, this.reintroduceDocktab);

  // do other initializing process
  $(window).bind('resize', this.fnCache.onResize);
  window.setTimeout(
    this.fnCache.onResizePrivate,
    this.MILSEC_INITIAL_WAIT
  );

  this.initGeocode();
  this.initIcons();
  this.initDocktab();
  this.initNotifier();
  this.alignSwf();
  this.initTitler();

  if (window.location.host.indexOf('.local') !== -1)
  {
    //throw new Error('local environment.');
    this.isLocal = true;
    jQuery.get = function (){
      console.log('Now running on local.');
      for (var i = 0, len = arguments.lenght; i < len; ++i)
      {
        console.log('argument ' + i + ': ');
        console.log(arguments[i]);
      }
    }
    return;
  }

  this.accessGetByGeocode();
}


// SCRATCH PADS
// _______________________________________________________________________












PinupController.prototype.alignSwf = function ()
{
  var jq = $(this.elm.nameOfCity);
  var max_h = $(document.body).height();
  var h = jq.height();
  jq.css({
    'left' : '20px',
    'top'  : Math.floor((max_h - h) / 2) + 'px'
  });
}


PinupController.prototype.writeNameOfCity = function ()
{
  var photo = this.getPhotoByIndex(this.photosPointer);
  this.titler.doSequence(
    {'time': 1, 'delay': 0},
    photo.location,
    {'time': 1, 'delay': 0}
  );
  //this.titler.setText(photo.location);
}






PinupController.prototype.stopSlideShow = function ()
{
  window.clearTimeout(this.timerIdOnSwitching);
}

PinupController.prototype.restartSlideShow = function ()
{
  this.clearCache();
  this.initializeSychronizer();
  this.preloadNextPicture();
  this.flagSynchronizer(this.SYNC_INDEX_SWITCH_TIMER);
}

PinupController.prototype.clearCache = function ()
{
  PinupController.cleanupChildren(this.elm.authorInfo.cache);
  PinupController.cleanupChildren(this.elm.picture.cache);
}






PinupController.prototype.hideIcons = function ()
{
  this.elm.icons.style.display = 'none';
}

PinupController.prototype.showIcons = function ()
{
  this.elm.icons.style.display = 'block';
}


PinupController.prototype.reintroduceIcons = function (delay, first)
{
  //console.log('reintroduceIcons');
  first = first || false;
  if (!first)
  {
    this.restartSlideShow();
  }
  this.elm.icons.style.display = 'block';
  for (var i = 0, len = this.icon.length; i < len; ++i)
  {
    PinupController.slideUpIcon(this.icon[i], 0, delay);
  }
  window.setTimeout(this.fnCache.reintroduceDocktab, Math.floor(delay * 1000));
}


PinupController.prototype.reintroduceDocktab = function ()
{
  $(this.elm.docktab).hide();
}


PinupController.prototype.alignIcons = function ()
{
  var len         = this.icon.length;
  var offset_left = Math.floor(
    (
      $(this.elm.icons).width() 
      - (
          this.PIX_ICON_EACH_SIDE * len 
          + this.PIX_FRAME_WIDTH  * (len - 1)
        )
    ) / 2
  );
  for (var i = 0; i < len; ++i)
  {
    $(this.icon[i]).css({
      'left': (offset_left + ((this.PIX_ICON_EACH_SIDE + this.PIX_FRAME_WIDTH) * i)) + 'px'
    });
  }
}



PinupController.receiveFaviconClick = function (ev)
{
  PinupController.getInstance().onFaviconClick(ev.target.eventLineage);
}



PinupController.enhanceCompleteEvent = function (dom, isTarget)
{
  isTarget = isTarget || false;
  if (isTarget)
  {
    dom.afterComplete = function (ev)
    {
      window.setTimeout(
        function () 
        {
          var c = PinupController.getInstance();
          c.hideIcons();
          c.dictionary.get(dom).show();
        },
        500
      );
      this.afterComplete = function (ev) {};
    }
  }
  else
  {
    dom.afterComplete = function (ev) {}
  }
}



PinupController.slideUpIcon = function (obj, goal, delay)
{
  obj.style.visibility = 'visible';
  delay   = delay || 0;
  obj._y  = parseInt(obj.style.top);
  JSTweener.addTween(
    obj,
    {
      'delay'       : delay,
      '_y'          : goal,
      'time'        : .6,
      'transition'  : 'easeInQuart',
      'onUpdate'    : function () { obj.style.top  = obj._y + 'px'; }
    }
  );
}

PinupController.slideDownIcon = function (obj, goal, delay)
{
  delay   = delay || 0;
  obj._y  = 0;
  JSTweener.addTween(
    obj,
    {
      'delay'       : delay,
      '_y'          : goal,
      'time'        : .6,
      'transition'  : 'easeInQuart',
      'onUpdate'    : function () { obj.style.top  = obj._y + 'px'; },
      'onComplete'  : function () 
                      { 
                        obj.style.visibility = 'hidden'; 
                        obj.afterComplete();
                      }
    }
  );
}








// INIT METHODS
// _______________________________________________________________________


/*
Method: initDocktab

Initialize dock tabs' instances.

Return:
  undefined
*/
PinupController.prototype.initDocktab = function ()
{
  $(this.elm.docktab).hide();
  var google  = new DocktabGoogle(this.elm.icons.google, this.latitude, this.longitude);
  this.dictionary.set(this.elm.icons.google, google);
  var flickr  = new DocktabFlickr(this.elm.icons.flickr);
  this.dictionary.set(this.elm.icons.flickr, flickr);
  var grf     = new DocktabGrf(this.elm.icons.grf);
  this.dictionary.set(this.elm.icons.grf, grf);
}


/*
Method: initIcons

Initialize Favicons' interaction and position

Return:
  undefined
*/
PinupController.prototype.initIcons = function ()
{
  var height_icons = this.PIX_FRAME_WIDTH * 2 + this.PIX_ICON_EACH_SIDE;
  this.elm.icons.style.height = height_icons + 'px';
  for (var i = 0, len = this.icon.length; i < len; ++i)
  {
    PinupController.prepareEventLineage(this.icon[i], 'click', PinupController.receiveFaviconClick);
    this.icon[i].style.visibility = 'hidden';
    this.icon[i].style.top = (height_icons + 16) + 'px';
  }
}


/*
Method: initGeocode

Initialize client geocode by using Google's API, or preset

Return:
  undefined
*/
PinupController.prototype.initGeocode = function ()
{
  if ((typeof(google) === 'undefined') || (google.loader.ClientLocation === null))
  {
    this.cityIndex = Math.floor(Math.random() * this.cities.length);
    var geo = this.geocode[this.cities[this.cityIndex]];
    this.latitude  = geo.lat;
    this.longitude = geo.lon;
  }
  else
  {
    this.latitude  = google.loader.ClientLocation.latitude;
    this.longitude = google.loader.ClientLocation.longitude;
  }
}


/*
Method: initNotifier

Initialize Notifier

Return:
  undefined
*/
PinupController.prototype.initNotifier = function ()
{
  this.notifier = new Postcard(this.elm.postcard, this.elm.postcard.description);
  var city = (this.cityIndex === null) ? null : this.cities[this.cityIndex];
  var opening   = PinupController.buildPostcardOpening(city);
  var self      = this;
  this.notifier.setContent(opening.dom);
  this.notifier.appendFunctionHide(function(){
    self.flagSynchronizer(self.SYNC_INDEX_SWITCH_TIMER);
  });
  opening.link.onclick = function (){
    self.notifier.hide();
    return false;
  }
}


/*
Method: initTitler

Initialize Titler Swf.  Need to be refactored here!!

Return:
  undefined
*/
PinupController.prototype.initTitler = function ()
{
  this.titler = swfobject.createSWF(
    {
      'width': 500,
      'height': 120,
      'data': 'swf/titler.swf'
    },
    {
      'wmode': 'transparent'
    },
    'swfTitler'
  );
}


// ANIMATION METHODS
// _______________________________________________________________________


/*
Method: fadeOutAuthorInfo

Receives FadeOut signal from authorInfo panel

Arguments:
  ev - (Event Object)
  photo - (Object)

Return:
  undefined
*/
PinupController.prototype.fadeOutAuthorInfo = function (ev, photo)
{
  this.current.elmIcon.parentNode.removeChild(this.current.elmIcon);
  this.current.elmIcon.style.visibility = 'visible';

  PinupController.cleanupChildren(this.elm.authorInfo.picture);
  PinupController.cleanupChildren(this.elm.authorInfo.legend);
  PinupController.cleanupChildren(this.elm.authorInfo.author);

  var author = this.authorsHash[this.current.elmIcon.nsid];
  this.dictionary.get(this.elm.icons.flickr).setContent(photo, author);

  if (this.enabledAuthorInfo)
  {
    var page_url = photo.page_url;
    var a_photo  = document.createElement('a');
    a_photo.href = author.profile_url;
    a_photo.appendChild(this.current.elmIcon);
    this.elm.authorInfo.picture.appendChild(a_photo);

    var a_title         = document.createElement('a');
    a_title.href        = page_url;
    a_title.innerHTML   = photo.title;
    this.elm.authorInfo.legend.appendChild(a_title);

    var a_author        = document.createElement('a');
    a_author.href       = page_url;
    a_author.innerHTML  = author.username;
    this.elm.authorInfo.author.appendChild(a_author);

    $(this.elm.authorInfo).fadeIn(this.MILSEC_FADE_IN);
  }
  this.current.elmIcon = null;
}



/*
Method: switchAuthorInfo

Transition for AuthorInfo panel

Return:
  undefined
*/
PinupController.prototype.switchAuthorInfo = function ()
{
  var self = this;
  var photo = this.getPhotoByIndex(this.photosPointer);
  $(this.elm.authorInfo).fadeOut(
    this.MILSEC_FADE_OUT, 
    function(ev)
    {
      self.fadeOutAuthorInfo(ev, photo);
    }
  );
}


/*
Method: switchFade

Pictures transition: Fade in and out

Return:
  undefined
*/
PinupController.prototype.switchFade = function ()
{
  if (this.current.elmImg && this.current.elmImg.parentNode)
  {
    this.current.elmImg.parentNode.removeChild(this.current.elmImg);
    this.current.elmImg.style.visibility = 'visible';
  }

  PinupController.initializePosition(this.elm.picture.tank);
  this.elm.picture.tank.style.display = 'none';
  this.elm.picture.tank.appendChild(this.current.elmImg);
  PinupController.alignImage(this.current.elmImg);

  $(this.elm.picture.showing).fadeOut(this.MILSEC_FADE_OUT);
  $(this.elm.picture.tank).fadeIn(this.MILSEC_FADE_IN, this.fnCache.liftUpPicture);

/*
  JSTweener for Opacity is too heavy...
  var obj_out    = this.elm.picture.showing;
  obj_out._alpha = 1;
  JSTweener.addTween(
    obj_out,
    {
      'delay'       : 0,
      '_alpha'      : 0,
      'time'        : this.SEC_FADE_OUT,
      'transition'  : 'linear',
      'onUpdate'    : function () {obj_out.style.opacity = obj_out._alpha; },
      'onComplete'  : function(){console.log('JSTweener!');}
    }
  );
  var obj_in    = this.elm.picture.tank;
  obj_in.style.opacity = 0;
  obj_in._alpha = 0;
  JSTweener.addTween(
    obj_in,
    {
      'delay'       : 0,
      '_alpha'      : 1,
      'time'        : this.SEC_FADE_IN,
      'transition'  : 'linear',
      'onUpdate'    : function () { obj_in.style.opacity = obj_in._alpha },
      'onComplete'  : this.fnCache.liftUpPicture
    }
  );
*/
  this.current.elmImg = null;
}


/*
Method: switchOpening

Pictures transition for Opening: Invoke fadeOutOpeningTitle()

Return:
  undefined
*/
PinupController.prototype.switchOpening = function ()
{
  console.log('PinupController::switchOpening');
  this.current.elmImg.parentNode.removeChild(this.current.elmImg);
  this.current.elmImg.style.visibility = 'visible';

  PinupController.initializePosition(this.elm.picture.showing);
  $(this.elm.picture.showing).hide();
  this.elm.picture.showing.appendChild(this.current.elmImg);
  PinupController.alignImage(this.current.elmImg);
  $(this.elm.picture.showing).fadeIn(this.MILSEC_FADE_IN);
  this.current.elmImg = null;

  var photo = this.getPhotoByIndex(this.photosPointer);
  if (this.enabledAuthorInfo)
  {
    this.fadeOutAuthorInfo(null, photo);
  }
  var author = this.authorsHash[this.current.elmIcon.nsid];
  this.dictionary.get(this.elm.icons.flickr).setContent(photo, author);

  this.isOpening = false;
  this.reintroduceIcons(0, true);
  this.accessFetchByGeocode();
}


/*
Method: switchStarWipe

Pictures transition: Star shape Wipe out

Return:
  undefined
*/
PinupController.prototype.switchStarWipe = function ()
{
  console.log('not yet implemented.');
  this.switchFade();
}


/*
Method: liftUpPicture

delete img from front, and pull img from tank to front

Return:
  undefined
*/
PinupController.prototype.liftUpPicture = function ()
{
  var jq_tank = $(this.elm.picture.tank);
  var jq_show = $(this.elm.picture.showing);
  var jq_img_top = $('img', this.elm.picture.showing);
  if (jq_img_top.length > 0)
  {
    var img_top = jq_img_top.get(0);
    this.elm.picture.showing.removeChild(img_top);
  }
  jq_show.show();
  var jq_img_bot = $('img', this.elm.picture.tank);
  if (jq_img_bot.length > 0)
  {
    this.elm.picture.showing.style.opacity = 1;
    this.elm.picture.showing.style.left = jq_tank.css('left');
    this.elm.picture.showing.style.top  = jq_tank.css('top');
    var img_bot = this.elm.picture.tank.removeChild(jq_img_bot.get(0));
    this.elm.picture.tank.opacity = 1;
    this.elm.picture.showing.appendChild(img_bot);
  }
  jq_tank.hide();

  window.setTimeout(
    this.fnCache.onResizePrivate,
    this.MILSEC_INITIAL_WAIT
  );
}


// ACCESS METHODS
// _______________________________________________________________________


/*
Method: accessGetAuthors

Access Server to get multiple author's info

Return:
  undefined
*/
PinupController.prototype.accessGetAuthors = function ()
{
  console.log('PinupController::accessGetAuthors');

  if ((this.current.arrAuthors === null) || !(this.current.arrAuthors instanceof Array))
  {
    return;
  }

  var arr_nsid = [];
  var arr, nsid;
  for (var i = 0, len = this.current.arrAuthors.length; i < len; ++i)
  {
    arr  = this.current.arrAuthors[i].split('/');
    nsid = arr[arr.length - 2];
    if (typeof(this.authorsHash[nsid]) === 'undefined')
    {
      arr_nsid[arr_nsid.length] = nsid;
    }
  }

  if (arr_nsid.length !== 0)
  {
    var url = Config.routes.getAuthors;
    var data = {
      'pad'   : this.bulkAuthorsKey,
      'nsids' : arr_nsid.join(',')
    };
    jQuery.get( url, data, this.fnCache.callbackGetAuthors);
  }
  this.current.arrAuthors = null;
}


/*
Method: accessGetByGeocode

Access Server to get photo objects by Geocode

Return:
  undefined
*/
PinupController.prototype.accessGetByGeocode = function ()
{
  console.log('PinupController::accessGetByGeocode');

  var lat, lon;
  if ((typeof(google) === 'undefined') || (google.loader.ClientLocation === null))
  {
    lat = "";
    lon = "";
  }
  else
  {
    lat = google.loader.ClientLocation.latitude;
    lon = google.loader.ClientLocation.longitude;
  }

  var url = Config.routes.getByGeocode;
  var data = {
    'pad': this.photosKey,
    'lat': lat,
    'lon': lon
  }
  jQuery.get( url, data, this.fnCache.callbackGetByGeocode);
}


/*
Method: accessFetchByGeocode

Access Server to get photo objects by Geocode with asking a new picture

Return:
  undefined
*/
PinupController.prototype.accessFetchByGeocode = function (lat, lon)
{
  console.log('PinupController::accessFetchByGeocode');

  if (typeof(lat) === 'undefined' || typeof(lon) === 'undefined')
  {
    if ((typeof(google) === 'undefined') || (google.loader.ClientLocation === null))
    {
      return;
    }
    else
    {
      lat = google.loader.ClientLocation.latitude;
      lon = google.loader.ClientLocation.longitude;
    }
  }

  var url = Config.routes.fetchByGeocode;
  var data = {
    'pad': this.photosKey,
    'lat': lat,
    'lon': lon
  }
  jQuery.get( url, data, this.fnCache.callbackFetchByGeocode);
}


/*
Method: accessGetPeopleInfo

OBSOLETE!!
Access Server to get author of the given photo object

Arguments:
  object - (Object) Photo object

Return:
  undefined
*/
PinupController.prototype.accessGetPeopleInfo = function (object)
{
  //console.log('PinupController::accessGetPeopleInfo');
  var arr = object.page_url.split('/');
  arr.pop();
  var nsid = arr.pop();

  if (typeof(this.authorsHash[nsid]) === 'undefined')
  {
    var url = Config.routes.getAuthorInfo;
    var data = {
      'pad'   : this.authorsKey,
      'nsid'  : nsid
    };
    jQuery.get( url, data, this.fnCache.callbackGetAuthorInfo);
  }
  else
  {
    this.preloadBuddyIcon(this.authorsHash[nsid]);
  }
}


// CALLBACK METHODS
// _______________________________________________________________________


/*
Method: callbackGetAuthorInfo

Parse server response and do preloading proces

Arguments:
  textContent - (String)
  status - (String)

Return:
  undefined
*/
PinupController.prototype.callbackGetAuthorInfo = function (textContent, status)
{
  //console.log('PinupController::callbackGetAuthorInfo => ' + status);

  eval(textContent);
  var obj = window[this.authorsKey];

  if ( typeof(obj.nsid) === 'undefined' )
  {
    console.log('No Authors returned.');
    window[this.authorsKey] = null;
    return;
  }

  this.authorsArray[this.authorsArray.length] = obj.nsid;
  this.authorsHash[obj.nsid] = obj;
  this.preloadBuddyIcon(obj);
}


/*
Method: callbackGetAuthors

Parse server responses for AuthorsInfo

Arguments:
  textContent - (String)
  status - (String)

Return:
  undefined
*/
PinupController.prototype.callbackGetAuthors = function (textContent, status)
{
  console.log('PinupController::callbackGetAuthors => ' + status);

  eval(textContent);
  var arr = window[this.bulkAuthorsKey];

  if ( typeof(arr) === 'undefined' )
  {
    console.log('No Authors returned.');
    window[this.bulkAuthorsKey] = null;
    return;
  }

  var obj;
  var counter = 0;
  for (var i = 0, len = arr.length; i < len; ++i)
  {
    obj = arr[i];
    if (typeof(this.authorsHash[obj.nsid]) === 'undefined')
    {
      this.authorsArray[this.authorsArray.length] = obj.nsid;
      this.authorsHash[obj.nsid] = obj;
      ++counter;
    }
  }
  console.log('newly author info added: ' + counter);
}


/*
Method: callbackGetByGeocode

Parse server response
Is it ok to just eval entire input?

Arguments:
  textContent - (String)
  status - (String)

Return:
  undefined
*/
PinupController.prototype.callbackGetByGeocode = function (textContent, status)
{
  console.log('PinupController::callbackGetByGeocode => ' + status);

  eval(textContent);
  var arr = window[this.photosKey];

  if ( !(arr instanceof Array) || (arr.length < 1) )
  {
    console.log('No photos returned.');
    window[this.photosKey] = null;
    return;
  }

  var pages = []; // for bulk access to Authors info.
  var counter = 0;
  var key;
  for (var i = 0, len = arr.length; i < len; ++i)
  {
    key = arr[i].photo_url;
    if (typeof(this.photosHash[key]) === 'undefined')
    {
      this.photosArray[this.photosArray.length] = key;
      this.photosHash[key] = arr[i];
      ++counter;
      pages[pages.length] = arr[i].page_url;
    }
  }
  console.log('newly added photo: ' + counter);
  this.current.arrAuthors = pages;

  if (this.isOpening)
  {
    this.photosPointer = 0;
    this.initiateAnimation();
  }

  window.setTimeout(this.fnCache.accessGetAuthors, Math.floor(this.MILSEC_SWITCHING_WAIT / 3));
}


/*
Method: callbackFetchByGeocode

Parse server response from accessFetchByGeocode

Arguments:
  textContent - (String)
  status - (String)

Return:
  undefined
*/
PinupController.prototype.callbackFetchByGeocode = function (textContent, status)
{
  console.log('PinupController::callbackFetchByGeocode => ' + status);

  eval(textContent);
  var arr = window[this.photosKey];

  if ( !(arr instanceof Array) || (arr.length < 1) )
  {
    console.log('No photos returned.');
    window[this.photosKey] = null;
    return;
  }

  var pages = []; // for bulk access to Authors info.
  var counter = 0;
  var orig_length = this.photosArray.length;
  var key;
  for (var i = 0, len = arr.length; i < len; ++i)
  {
    key = arr[i].photo_url;
    if (typeof(this.photosHash[key]) === 'undefined')
    {
      this.photosArray[this.photosArray.length] = key;
      this.photosHash[key] = arr[i];
      ++counter;
      pages[pages.length] = arr[i].page_url;
    }
  }
  console.log('newly added photo: ' + counter);
  this.current.arrAuthors = pages;

  if (this.isOpening)
  {
    this.photosPointer = 0;
    this.initiateAnimation();
  }
  else
  {
    this.photosPointer = orig_length - 1;
  }

  window.setTimeout(this.fnCache.accessGetAuthors, Math.floor(this.MILSEC_SWITCHING_WAIT / 3));
}


/*
Method: callbackIconImgOnload

Receives onload action from Icon

Arguments:
  img - (DOMImageElement)

Return:
  undefined
*/
PinupController.prototype.callbackIconImgOnload = function (img)
{
  this.current.elmIcon = img;
  this.flagSynchronizer(this.SYNC_INDEX_ICON_LOAD);
}


/*
Method: callbackImgOnload

invoked by onloaded image

Arguments:
  img - (HTML Element img) the loaded image

Return:
  undefined
*/
PinupController.prototype.callbackImgOnload = function (img)
{
  this.current.elmImg = img;
  this.flagSynchronizer(this.SYNC_INDEX_IMG_LOAD);
}


// EVENT METHODS
// _______________________________________________________________________


/*
Method: onFaviconClick

First the clicked icon drops, and then others will follow.  Any favicon 
click will stop Slideshow.

Arguments:
  target - (DOMElement) icon received click

Return:
  undefined
*/
PinupController.prototype.onFaviconClick = function (target)
{
  //console.log('onFaviconClick => ' + target.className);
  // stop slideshow
  $(this.elm.docktab).show();
  this.stopSlideShow();

  // drop target
  var goal_y = parseInt(this.elm.icons.style.height) + 16;
  PinupController.enhanceCompleteEvent(target, true);
  PinupController.slideDownIcon(target, goal_y);

  // drop others
  var counter = 1;
  for (var i = 0, len = this.icon.length; i < len; ++i)
  {
    if (this.icon[i] != target)
    {
      var div = this.icon[i];
      //console.log('onFaviconClick => ' + div.className);
      PinupController.enhanceCompleteEvent(div);
      PinupController.slideDownIcon(div, goal_y, (counter * 0.2));
      ++counter
    }
  }
}


/*
Method: onNextPhoto

Invoke transition, pre-loading next picture and triggering a timer for next 
picture. Preload and timer will be synchronized, and will invoke onNextPhoto
again.

Arguments:
  method - (String) transition method

Return:
  undefined
*/
PinupController.prototype.onNextPhoto = function (method)
{
  switch (method)
  {
    case 'opening':
      this.switchOpening();
    break;

    case 'starwipe':
      this.switchStarWipe();
      this.switchAuthorInfo();
    break;

    case 'fade':
    default:
      this.switchFade();
      this.switchAuthorInfo();
    break;
  }
  this.writeNameOfCity();

  this.nextTranisition = null;
  this.increasePhotosPointer();
  this.initializeSychronizer();
  this.preloadNextPicture();
  this.timerIdOnSwitching = window.setTimeout(this.fnCache.onSwitching, this.MILSEC_SWITCHING_WAIT);
}




/*
Method: onResize

Resize event wrapper. Triggering timing of window.onResize varies by brwoser
implementation.  This method tries to wrap it.

Return:
  undefined
*/
PinupController.prototype.onResize = function ()
{
  if (this.timerIdOnResize !== null)
  {
    window.clearTimeout(this.timerIdOnResize);
  }
  this.timerIdOnResize = window.setTimeout(
    this.fnCache.onResizePrivate,
    this.MILSEC_RESIZE_WAIT
  );
}


/*
Method: onResizePrivate

Actual behavior for window.resizing 

Return:
  undefined
*/
PinupController.prototype.onResizePrivate = function ()
{
  //console.log('PinupController::onResizePrivate');
  var max_w = $(document.body).width();
  var max_h = $(document.body).height();
  // the following should be rewritten to make it faster
  $(this.elm.frame.top).css({
    'top'   : 0,
    'left'  : 0,
    'width' : '100%',
    'height': this.PIX_FRAME_WIDTH + 'px'
  });
  $(this.elm.frame.right).css({
    'top'   : 0,
    'right' : 0,
    'width' : this.PIX_FRAME_WIDTH + 'px',
    'height': max_h + 'px'
  });
  $(this.elm.frame.bottom).css({
    'bottom': 0,
    'left'  : 0,
    'width' : '100%',
    'height': this.PIX_FRAME_WIDTH + 'px'
  });
  $(this.elm.frame.left).css({
    'top'   : 0,
    'left'  : 0,
    'width' : this.PIX_FRAME_WIDTH + 'px',
    'height': max_h + 'px'
  });

  var jq_img_top = $('img', this.elm.picture.showing);
  if (jq_img_top.length)
  {
    PinupController.alignImage(jq_img_top.get(0));
  }

  var jq_img_bot = $('img', this.elm.picture.tank);
  if (jq_img_bot.length)
  {
    PinupController.alignImage(jq_img_bot.get(0));
  }

  this.alignIcons();
  this.alignSwf();
  this.timerIdOnResize = null;
}


/*
Method: onSwitching

Invoked when switching timer finishes

Return:
  undefined
*/
PinupController.prototype.onSwitching = function ()
{
  this.flagSynchronizer(this.SYNC_INDEX_SWITCH_TIMER);
}


// INSTANCE UTILITY METHODS
// _______________________________________________________________________


/*
Method: decreasePhotosPointer

decrement PhotosPointer

Return:
  undefined
*/
PinupController.prototype.decreasePhotosPointer = function ()
{
  --this.photosPointer;
  if (this.photosPointer < 0)
  {
    this.photosPointer = (this.photosArray.length - 1);
  }
}


/*
Method: flagSynchronizer

Receives event finishing and if all finishes, trig onNextPhoto()

Arguments:
  source - (integer) defined in initInstance() as SYNC_INDEX_*

Return:
  undefined
*/
PinupController.prototype.flagSynchronizer = function (source)
{
  this.synchronizer[source] = true;
  var result = true;
  for (var i = 0, len = this.synchronizer.length; i < len; ++i)
  {
    if (!this.synchronizer[i])
    {
      result = false;
      return;
    }
  }
  if (result)
  {
    this.onNextPhoto(this.nextTranisition);
  }
}


/*
Method: getPhotoByIndex

returns Photo Object by index.  Maybe used sequential access

Return:
  (Object) photo 
*/
PinupController.prototype.getPhotoByIndex = function (index)
{
  return this.photosHash[this.photosArray[index]];
}


/*
Method: getTransitionCode

FIXME++++ randomly choosing trasition method

Return:
  (Sring)
*/
PinupController.prototype.getTransitionCode = function ()
{
  var max = this.transitionCode.length;
  var i = Math.floor(Math.random() * max);
  return this.transitionCode[i];
}


/*
Method: initiateAnimation

for special method for opening animation

Return:
  undefined
*/
PinupController.prototype.initiateAnimation = function ()
{
  console.log('PinupController::initiateAnimation');
  this.preloadNextPicture();
  this.nextTranisition = 'opening';
}


/*
Method: initializeSychronizer

Initialize Synchronizer

Return:
  undefined
*/
PinupController.prototype.initializeSychronizer = function ()
{
  //console.log('PinupController::initializeSychronizer');
  if (typeof(this._synchronizerKey) === 'undefined')
  {
    this._synchronizerKey = [];
    for (var i in this)
    {
      if (i.indexOf('SYNC_INDEX_') === 0)
      {
        this._synchronizerKey[this[i]] = i;
      }
    }
  }

  this.synchronizer = [];
  for (var i = 0, len = this._synchronizerKey.length; i < len; ++i)
  {
    this.synchronizer[i] = false;
  }
}


/*
Method: increasePhotosPointer

Increment PhotosPointer

Return:
  undefined
*/
PinupController.prototype.increasePhotosPointer = function ()
{
  ++this.photosPointer;
  if (this.photosPointer >= this.photosArray.length)
  {
    this.photosPointer = 0;
  }
}


/*
Method: preloadBuddyIcon

fire preload process by given author object

Arguments:
  author - (Object) author object

Return:
  undefined
*/
PinupController.prototype.preloadBuddyIcon = function (author)
{
  var img = PinupController.buildIcon(author.nsid, author.icon_url, author.username);
  this.elm.authorInfo.cache.appendChild(img);
}


/*
Method: preloadNextPicture

Pre-load next picture

Return:
  undefined
*/
PinupController.prototype.preloadNextPicture = function ()
{
  var photo = this.getPhotoByIndex(this.photosPointer);
  this.accessGetPeopleInfo(photo);
  var img   = PinupController.buildPicture(photo);
  this.nextTranisition = this.getTransitionCode();
  this.elm.picture.cache.appendChild(img);
}


// STATIC UTILITY METHODS
// _______________________________________________________________________


/*
Method: alignImage

Static method for aligning given image into dead-center.  The given img
is needed to be appended in advance.

Arguments:
  img - (DOMImageElement)

Return:
  undefined
*/
PinupController.alignImage = function (img)
{
  var win_w = $(document.body).width();
  var win_h = $(document.body).height();
  var win_r = win_w / win_h;
   //console.log(win_w, win_h, win_r);
  var pic_w = img.naturalWidth  || img.originalWidth;
  var pic_h = img.naturalHeight || img.originalHeight;
  var pic_r = pic_w / pic_h;
   //console.log(pic_w, pic_h, pic_r);

  var w, h, t, l;
  switch (true)
  {
    case (win_r > pic_r):
      //base width
      w = win_w;
      h = Math.round(pic_h * (win_w / pic_w));
      t = Math.round((win_h - h) / 2);
      l = 0;
    break;

    case (win_r < pic_r):
      //base height
      h = win_h;
      w = Math.round(pic_w * (win_h / pic_h));
      l = Math.round((win_w - w) / 2);
      t = 0;
    break;

    default:
      // sq to sq
      w = win_w;
      h = win_h;
      t = 0;
      l = 0;
    break;
  }
  img.width  = w;
  img.height = h;
  img.parentNode.style.width  = w + 'px';
  img.parentNode.style.height = h + 'px';
  img.parentNode.style.left   = l + 'px';
  img.parentNode.style.top    = t + 'px';
}


/*
Method: buildIcon

Build DOM Element by arguments with onload event

Arguments:
  nsid - (String)
  url - (String)
  title - (String)

Return:
  (DOMImageElement)
*/
PinupController.buildIcon = function (nsid, url, title)
{
  var img = document.createElement('img');
  img.onload = function (ev)
  {
    //console.log('PinupController::buildIcon::closure => img.onload');
    img.originalWidth  = img.width;
    img.originalHeight = img.height;
    PinupController.getInstance().callbackIconImgOnload(img);
  }
  img.style.visibility = 'hidden';
  img.src              = url;
  img.title            = title;
  img.nsid             = nsid;
  return img;
}


/*
Method: buildPicture

Build DOM Element by Photo Object with onload event

Arguments:
  photo - (Object)

Return:
  (DOMImageElement)
*/
PinupController.buildPicture = function (photo)
{
  var img = document.createElement('img');
  img.onload = function (ev)
  {
    //console.log('PinupController::closure => img.onload');
    img.originalWidth  = img.width;
    img.originalHeight = img.height;
    PinupController.getInstance().callbackImgOnload(img);
  }
  img.style.visibility = 'hidden';
  img.src              = photo.photo_url;
  img.title            = photo.title;
  img.location         = photo.location;
  return img;
}


/*
Method: buildPostcardOpening

Build DOM Element for Postcard - opening 

Arguments:
  city - (String) or Null

Return:
  (Object) .dom property holds DOMElement, .link holds DOMElementAnchor 
  to attach onclick event later.
*/
PinupController.buildPostcardOpening = function (city)
{
  var result  = {};
  var cont    = document.createElement('div');
  var link    = document.createElement('a');
  link.href   = '#';
  link.innerHTML = 'start slideshow &#8594';

  var p = [];
  p[0]  = document.createElement('p');
  p[0].appendChild(document.createTextNode("Picture Post Card is the site where you can see pictures on the earth. "));

  if (city)
  {
    p[0].appendChild(document.createTextNode("So let's start around " + city + ". You can always change it by clicking Google's g icon, which shows up below later."));
  }
  else
  {
    p[0].appendChild(document.createTextNode("I got where you are, so I can show you picture around you. You can always change it by clicking Google's g icon, which shows up below later."));
  }
  p[1]　= document.createElement('p');
  p[1].appendChild(link);
  p[1].className = 'right';
  for (var i = 0, len = p.length; i < len; ++i)
  {
    cont.appendChild(p[i]);
  }
  result.dom = cont;
  result.link = link;
  return result;
}


/*
Method: cleanupChildren

get rid of child elements by DOM Manupilation

Arguments:
  obj - (DOMElement) paren

Return:
  undefined
*/
PinupController.cleanupChildren = function (obj)
{
  // this should be included by DOMUtility
  for (var i = 0, len = obj.childNodes.length; i < len; ++i)
  {
    obj.removeChild(obj.childNodes.item(0));
  }
}


/*
Method: initializePosition

Set showing or tank as the (0, 0) position

Arguments:
  obj - (DOMDivElement)

Return:
  undefined
*/
PinupController.initializePosition = function (obj)
{
  //console.log('PinupController::initializePosition');
  obj.style.top  = '0px';
  obj.style.left = '0px';
}



/*
Method: prepareEventLineage

add event lineage style recursively

Arguments:
  dom - (DOMNode)
  event - (String) event handler name
  func - (Function)

Return:
  undefined
*/
PinupController.prepareEventLineage = function (dom, event, func)
{
  $(dom).bind(event, func);
  PinupController.setEventLineage(dom, dom);
}



/*
Method: setEventLineage

add eventLineage property to node recursively

Arguments:
  parent - (DOMNode)
  target - (DOMNode)

Return:
  undefined
*/
PinupController.setEventLineage = function (parent, target)
{
  parent.eventLineage = target;
  for (var i = 0, len = parent.childNodes.length; i < len; ++i)
  {
    PinupController.setEventLineage(parent.childNodes.item(i), target);
  }
}

