/*
  IIPImage Javascript Viewer <http://iipimage.sourceforge.net>
                      Version 1.0

  Copyright (c) 2007 Ruven Pillay <ruven@users.sourceforge.net>

  Built using the Mootools javascript framework <http://www.mootools.net>


   ---------------------------------------------------------------------------

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

   ---------------------------------------------------------------------------


  Example:

   iip = new IIP( 'div_id', 'http://www.server.com/fcgi-bin/iipsrv.fcgi', '/images/test.tif', 'copyright me 2007' );

   where the arguments are:
 	i) The id of the main div element in which to create the viewer window
	ii) The iipsrv server full URL
	iii) The full image path on the server
	iv) An optional image copyright or information

   Note: The new class instance must be assigned to the global variable "iip" in this version.
       : Requires mootools version 1.1 <http://www.mootools.net>
       : The page must have a standard-compliant XHTML declaration at the beginning

*/




// Global instance of our IIP object for use by the TargetDrag class
var iip;




/* Create our own class inherited from Drag.Move for constrained dragging of
   the main target window
 */
var TargetDrag = Drag.Move.extend({

   initialize: function(el, options){
       this.setOptions(options);
       this.element = $(el);
       this.handle = $(this.options.handle) || this.element;
       this.mouse = {'now': {}, 'pos': {}};
       this.value = {'start': {}, 'now': {}};
       this.bound = {
	   'start': this.start.bindWithEvent(this),
	   'check': this.check.bindWithEvent(this),
	   'drag': this.drag.bindWithEvent(this),
	   'stop': this.stop.bind(this)
       };
       this.attach();
       if (this.options.initialize) this.options.initialize.call(this);
   },

   drag: function(event){
       this.out = false;
       this.mouse.now = event.page;
       for (var z in this.options.modifiers){
	   if (!this.options.modifiers[z]) continue;
	   this.value.now[z] = this.mouse.now[z] - this.mouse.pos[z];

	   if( z == 'x' ){
	       if( iip.rgn_x - this.value.now[z] < 0 ){
		   this.value.now[z] = iip.rgn_x;
		   this.out = true;
	       }
	       if( iip.wid > iip.rgn_w ){
		   if( iip.rgn_x - this.value.now[z] > iip.wid - iip.rgn_w ){
		       this.value.now[z] = -(iip.wid - iip.rgn_w - iip.rgn_x);
		       this.out = true;
		   }
	       }
	       else{
		   this.value.now[z] = 0;
		   this.out = true;
	       }
	   }
	   if( z == 'y' ){
	       if( iip.rgn_y - this.value.now[z] < 0 ){
		   this.value.now[z] = iip.rgn_y;
		   this.out = true;
	       }
	       if( iip.hei > iip.rgn_h ){
		   if( iip.rgn_y - this.value.now[z] > iip.hei - iip.rgn_h ){
		       this.value.now[z] = -(iip.hei - iip.rgn_h - iip.rgn_y);
		       this.out = true;
		   }
	       }
	       else{
		   this.value.now[z] = 0;
		   this.out = true;
	       } 
	   }

	   this.element.setStyle(this.options.modifiers[z], this.value.now[z] + this.options.unit);
       }
       this.fireEvent('onDrag', this.element);
       event.stop();
   }

});





/* IIP Javascript Class
 */
var IIP = new Class({


  /* Initialize some variables. The constructor takes 4 arguments:
	i) The id of the main div element in which to create the viewer window
	ii) The iipsrv server full URL
	iii) The full image path on the server
	iv) An optional image copyright or information
   */
  initialize: function( main_id, server, image, credit ) {

    this.source = main_id;
    this.server = server;
    this.fif = image;
    this.max_width = 0;
    this.max_height = 0;
    this.min_x = 0;
    this.min_y = 0;
    this.sds = "0,90";
    this.contrast = 1.0;
    this.wid = 0;
    this.hei = 0;
    this.rgn_x = 0;
    this.rgn_y = 0;
    this.rgn_w = this.wid;
    this.rgn_h = this.wid;
    this.xfit = 0;
    this.yfit = 0;
    this.navpos = [0,0];
    this.tileSize = [0,0];
    this.resolution;
    this.refresher = null;
    this.credit = credit;

    /* Load us up when the DOM is fully loaded! 
     */
    window.addEvent( 'domready', function(){ this.load() }.bind(this) );

  },



  /* Create the appropriate CGI strings and change the image sources
   */
  requestImages: function() {

    // Clear our tile refresher
    if( this.refresher ){
      $clear( this.refresher );
      this.refresher = null;
    }

    // Set our cursor and activate our loading animation
    $('target').style.cursor = 'wait';
    $('loading').setOpacity(1);
    $('loading').style.zIndex = '2';

    // Load our image mosaic
    this.loadGrid();

    // Create a tile refresher to check for unloaded tiles
    this.refresher = this.refresh.periodical( 500, this );

  },



  /* Create a grid of tiles with the appropriate JTL request and positioning
   */
  loadGrid: function(){

    // Delete our old image mosaic
    $('target').getChildren().each( function(el){ el.remove(); } );
    $('target').setStyles({
        left: '0px',
        top: '0px'
    });

    // Get the start points for our tiles
    var startx = Math.floor( this.rgn_x / this.tileSize[0] );
    var starty = Math.floor( this.rgn_y / this.tileSize[1] );

    // If our size is smaller than the display window, only get these tiles!
    var len = this.rgn_w;
    if( this.wid < this.rgn_w ) len = this.wid;
    var endx =  Math.floor( (len + this.rgn_x) / this.tileSize[0] );

    len = this.rgn_h;
    if( this.hei < this.rgn_h ) len = this.hei;
    var endy = Math.floor( (len + this.rgn_y) / this.tileSize[1] );

    // Number of tiles is dependent on view width and height
    var xtiles = Math.ceil(this.wid / this.tileSize[0]);
    var ytiles = Math.ceil(this.hei / this.tileSize[1]);

    /* Calculate the offset from the tile top left that we want to display.
       Also Center the image if our viewable image is smaller than the window
    */
    var xoffset = Math.floor(this.rgn_x % this.tileSize[0]);
    if( this.wid < this.rgn_w ) xoffset -=  (this.rgn_w - this.wid)/2;

    var yoffset = Math.floor(this.rgn_y % this.tileSize[1]);
    if( this.hei < this.rgn_h ) yoffset -= (this.rgn_h - this.hei)/2;

    var tile;
    var i, j, k;
    var left, top;
    k = 0;

    // Create our image tile mosaic
    for( j=starty; j<=endy; j++ ){
      for( i=startx; i<=endx; i++ ){

	k = i + (j*xtiles);
	var src = this.server+"?"+this.fif+"&jtl="+this.resolution+"," + k;

	tile = new Element('img', {
	  'src': src,
	  'styles': {
		'left': (i-startx)*this.tileSize[0] - xoffset,
		'top': (j-starty)*this.tileSize[1] - yoffset
          }
	});
 	tile.injectInside('target');
      }
    }

  },



  /* Refresh function to avoid the problem of tiles not loading
     properly in Firefox/Mozilla
   */
  refresh: function(){

    var unloaded = 0;

    $('target').getChildren().each( function(el){
      // If our tile has not yet been loaded, give it a prod ;-)
      if( el.width == 0 || el.height == 0 ){
	el.src = el.src;
	unloaded = 1;
      }
    });

    /* If no tiles are missing, destroy our refresher timer, fade out our loading
       animation and and reset our cursor
     */
    if( unloaded == 0 ){

      $clear( this.refresher );
      this.refresher = null;

      // Fade out our loading animation
      var f = $('loading').effect('opacity', {
	duration: 750,
	frames: 10,
	transition: Fx.Transitions.quadOut,
	onComplete: function(){
	  $('loading').style.zIndex = '-1';
	}
      });
      f.start(1,0);

      $('target').style.cursor = 'move';
    }
  },



  /* Allow us to navigate within the image via the keyboard arrow buttons
   */
  key: function(e){
    var d = 100;
    switch( e.keyCode ){
    case 37: // left
      this.scrollTo(-d,0);
      break;
    case 38: // up
      this.scrollTo(0,-d);
      break;
    case 39: // right
      this.scrollTo(d,0);
      break;
    case 40: // down
      this.scrollTo(0,d);
      break;
    case 107: // plus
      if(!e.ctrlKey) this.zoomIn();
      break;
    case 109: // minus
      if(!e.ctrlKey) this.zoomOut();
      break;
    }
  },



  /* Scroll resulting from a drag of the navigation window
   */
  scrollNavigation: function( e ) {

    var xmove = 0;
    var ymove = 0;

    var zone_size = $("zone").getSize();
    var zone_w = zone_size.size.x;
    var zone_h = zone_size.size.y;

    if( e.event ){
      // From a mouse click
      var pos = $("navwin").getPosition();
      xmove = e.event.clientX - pos.x - zone_w/2;
      ymove = e.event.clientY - pos.y - zone_h/2;
    }
    else{
      // From a drag
      xmove = e.offsetLeft;
      ymove = e.offsetTop;
      if( (Math.abs(xmove-this.navpos[0]) < 3) && (Math.abs(ymove-this.navpos[1]) < 3) ) return;
    }

    if( xmove > (this.min_x - zone_w) ) xmove = this.min_x - zone_w;
    if( ymove > (this.min_y - zone_h) ) ymove = this.min_y - zone_h;
    if( xmove < 0 ) xmove = 0;
    if( ymove < 0 ) ymove = 0;

    this.rgn_x = xmove * this.wid / this.min_x;
    this.rgn_y = ymove * this.hei / this.min_y;

    this.requestImages();
    if( e.event ) this.positionZone();
  },



  /* Scroll from a target drag event
   */
  scroll: function() {
    var xmove =  - $('target').offsetLeft;
    var ymove =  - $('target').offsetTop;
    this.scrollTo( xmove, ymove );
  },



  /* Scroll to a particular position
   */
  scrollTo: function( x, y ) {

    if( x || y ){

      // To avoid unnecessary redrawing ...
      if( (Math.abs(x) < 3) && (Math.abs(y) < 3) ) return;

      this.rgn_x += x;
      this.rgn_y += y;

      if( this.rgn_x > this.wid - this.rgn_w ) this.rgn_x = this.wid - this.rgn_w;
      if( this.rgn_y > this.hei - this.rgn_h ) this.rgn_y = this.hei - this.rgn_h;
      if( this.rgn_x < 0 ) this.rgn_x = 0;
      if( this.rgn_y < 0 ) this.rgn_y = 0;

      this.requestImages();
      this.positionZone();

    }
  },



  /* Generic zoom function
   */
  zoom: function( e ) {

    var event = new Event(e);

    // For mouse scrolls
    if( event.wheel ){
      if( event.wheel > 0 ) this.zoomIn();
      else if( event.wheel < 0 ) this.zoomOut();
    }

    // For double clicks
    else if(event.shift) {
      this.zoomOut();
    }
    else this.zoomIn();

  },



  /* Zoom in by a factor of 2
   */
  zoomIn: function (){

    if( (this.wid <= (this.max_width/2)) && (this.hei <= (this.max_height/2)) ){

      this.wid = this.wid * 2;
      this.hei = this.hei * 2;

      if( this.xfit == 1 ){
	this.rgn_x = this.wid/2 - (this.rgn_w/2);
      }
      else if( this.wid > this.rgn_w ) this.rgn_x = 2*this.rgn_x + this.rgn_w/2;

      if( this.rgn_x > this.wid ) this.rgn_x = this.wid - this.rgn_w;
      if( this.rgn_x < 0 ) this.rgn_x = 0;

      if( this.yfit == 1 ){
	this.rgn_y = this.hei/2 - (this.rgn_h/2);
      }
      else if( this.hei > this.rgn_h ) this.rgn_y = this.rgn_y*2 + this.rgn_h/2;

      if( this.rgn_y > this.hei ) this.rgn_y = this.hei - this.rgn_h;
      if( this.rgn_y < 0 ) this.rgn_y = 0;

      this.resolution++;
      this.requestImages();
      this.positionZone();

    }
  },



  /* Zoom out by a factor of 2
   */
  zoomOut: function(){

    if( (this.wid > this.rgn_w) || (this.hei > this.rgn_h) ){
      this.wid = this.wid / 2;
      this.hei = this.hei / 2;

      this.rgn_x = this.rgn_x/2 - (this.rgn_w/4);
      if( this.rgn_x + this.rgn_w > this.wid ) this.rgn_x = this.wid - this.rgn_w;
      if( this.rgn_x < 0 ){
	this.xfit=1;
	this.rgn_x = 0;
      }
      else this.xfit = 0;

      this.rgn_y = this.rgn_y/2 - (this.rgn_h/4);
      if( this.rgn_y + this.rgn_h > this.hei ) this.rgn_y = this.hei - this.rgn_h;
      if( this.rgn_y < 0 ){
	this.yfit=1;
	this.rgn_y = 0;
      }
      else this.yfit = 0;

      this.resolution--;
      this.requestImages();
      this.positionZone();
    }
  },



  /* Calculate some dimensions
   */
  calculateMinSizes: function(){

    var tx = this.max_width;
    var ty = this.max_height;
    var thumb = 100;

    var winWidth = Window.getWidth();
    var winHeight = Window.getHeight();

    if( winWidth>winHeight ){
	// For panoramic images, use a large navigation window
	if( tx > 2*ty ) thumb = winWidth / 2;
	else thumb = winWidth / 4;
    }
    else thumb = winHeight / 4;

    var r = this.resolution;
    while( tx > thumb ){
      tx = parseInt(tx / 2);
      ty = parseInt(ty / 2);
      // Make sure we don't set our navigation image too small!
      if( --r == 1 ) break;
    }
    this.min_x = tx;
    this.min_y = ty;

    // Determine the resolution for this image view
    tx = this.max_width;
    ty = this.max_height;
    while( tx > winWidth && ty > winHeight ){
      tx = parseInt(tx / 2);
      ty = parseInt(ty / 2);
      this.resolution--;
    }
    this.wid = tx;
    this.hei = ty;
    this.resolution--;

  },



  /* Create our main and navigation windows
   */
  createWindows: function(){

    // Get our window size - subtract some pixels to make sure the browser never
    // adds scrollbars
    var winWidth = Window.getWidth() - 5;
    var winHeight = Window.getHeight() - 5;

    // Create our loading animated icon
    var loading = new Element('img',
    {
        'styles': {
       	  'left': (winWidth/2) - 16 + 'px',
       	  'top':  (winHeight/2) - 16 + 'px'
    	},
    	'id': 'loading',
    	'src': 'images/loading.gif'
    });
    loading.injectInside( this.source );

    // Calculate some sizes and create the navigation window
    this.calculateMinSizes();
    this.createNavigationWindow();

    // Create our main window target div, add our events and inject inside the frame
    var el = new Element('div', {'id': 'target'} );
    new TargetDrag( el, {onComplete: this.scroll.bindAsEventListener(this)} );
    el.injectInside( this.source );
    el.addEvent('mousewheel', this.zoom.bindAsEventListener(this) );
    el.addEvent('dblclick', this.zoom.bindAsEventListener(this) );     

    $(this.source).style.width = winWidth + "px";
    $(this.source).style.height = winHeight + "px";
    this.rgn_w = winWidth;
    this.rgn_h = winHeight;

    this.reCenter();
    this.zoomIn();
    this.requestImages();
    this.positionZone();

    window.addEvent( 'resize', function(){ window.location=window.location; } );
    document.addEvent( 'keydown', this.key.bindAsEventListener(this) );

    // Add our logo and a tooltip explaining how to use the viewer
    new Element( 'a', {href: 'http://iipimage.sourceforge.net', id:'logo'} ).injectInside(this.source);
    new Element('img', {src: 'images/iip.32x32.gif', id: 'info', title: '<h2>IIPMooViewer</h2>IIPImage High Resolution Ajax Image Viewer<ul><li>To navigate within image:<ul><li>drag image within main window or</li><li>drag the zone within the navigation window</li></ul><li>To zoom in:<ul><li>doubleclick with the mouse or</li><li>use the mouse scrollwheel or</li><li>or simply press the "+" key</li></ul><li>To zoom out:<ul><li>shift doubleclick with the mouse or</li><li>use the mousewheel or</li><li>press the "-" key</li></ul></li><li>For more information visit iipimage.sf.net</li></ul>', styles: { opacity: 0.66 } } ).injectInside('logo');
    new Tips( $('info') );

    // Add some info
    new Element( 'div', {id: 'credit'} ).injectInside(this.source);
    if( this.credit ) $('credit').setHTML( this.credit );

  },



  /* Create our navigation window
   */
  createNavigationWindow: function() {

    // Create our navigation div and inject it inside our frame
    var navwin = new Element( 'div', {
      id: 'navwin',
      styles: {
	width: this.min_x + 'px',
	height: this.min_y + 'px'
      }
    });
    navwin.injectInside( this.source );

    // Create our navigation image and inject inside the div we just created
    var navimage = new Element( 'img', { id: 'navigation' } );
    navimage.injectInside( navwin );

    // Create our navigation zone and inject inside the navigation div
    var zone = new Element( 'div', {
      id: 'zone',
      styles: {
	width: this.min_x/2 + 'px',
	height: this.min_y/2 + 'px',
	opacity: 0.4
      }
    });
    zone.injectInside( navwin );

    $("zone").makeDraggable({
    	                container: 'navwin',
			// Take a note of the starting coords of our drag zone
			onStart: function(){
			    this.navpos = [$('zone').offsetLeft, $('zone').offsetTop];
			}.bind(this),
			onComplete: this.scrollNavigation.bindAsEventListener(this)
    });

    var cnt=1.0;
    $("navigation").src = this.server + '?' + this.fif + '&SDS=' + this.sds + '&CNT=' +
	                                cnt + '&WID=' + this.min_x + '&CVT=jpeg';

    // Add our events
    $("navigation").addEvent('click', this.scrollNavigation.bindWithEvent(this) );
    $('navigation').addEvent('mousewheel', this.zoom.bindAsEventListener(this) );
    $('zone').addEvent('mousewheel', this.zoom.bindAsEventListener(this) );
    $("zone").addEvent( 'dblclick', this.zoom.bindWithEvent(this) );

  },



  /* Use a AJAX request to get the image size, tile size and number of resolutions from the server
   */
  load: function(){

    new Ajax( this.server + "?" + this.fif + "&obj=IIP,1.0&obj=Max-size&obj=Tile-size&obj=Resolution-number",
    {
    method: 'get',
	      onComplete: function(transport){
		var response = transport || "No response from server " + this.server;
		var tmp = response.split( "Max-size" );
		if(!tmp[1]) alert( "Unexpected response from server " + this.server );
		var size = tmp[1].split(" ");
		this.max_width = parseInt( size[0].substring(1,size[0].length) );
		this.max_height = parseInt( size[1] );
		tmp = response.split( "Tile-size" );
		size = tmp[1].split(" ");
		this.tileSize[0] = parseInt( size[0].substring(1,size[0].length) );
		this.tileSize[1] = parseInt( size[1] );
		tmp = response.split( "Resolution-number" );
		this.resolution = parseInt( tmp[1].substring(1,tmp[1].length) );
		this.createWindows();
	      }.bind(this),
	      onFailure: function(){ alert('Unable to get image and tile sizes from server!'); }
	      } ).request();

  },



  /* Recenter the image view
   */
  reCenter: function(){
    this.rgn_x = (this.wid-this.rgn_w)/2;
    this.rgn_y = (this.hei-this.rgn_h)/2;
  },



  /* Reposition the navigation rectangle on the overview image
   */
  positionZone: function(){

    var pleft = (this.rgn_x/this.wid) * (this.min_x);
    if( pleft > this.min_x ) pleft = this.min_x;
    if( pleft < 0 ) pleft = 0;

    var ptop = (this.rgn_y/this.hei) * (this.min_y);
    if( ptop > this.min_y ) ptop = this.min_y;
    if( ptop < 0 ) ptop = 0;

    var width = (this.rgn_w/this.wid) * (this.min_x);
    if( pleft+width > this.min_x ) width = this.min_x - pleft;

    var height = (this.rgn_h/this.hei) * (this.min_y);
    if( height+ptop > this.min_y ) height = this.min_y - ptop;

    if( width < this.min_x ) this.xfit = 0;
    else this.xfit = 1;
    if( height < this.min_y ) this.yfit = 0;
    else this.yfit = 1;

    var border = $("zone").offsetHeight - $("zone").clientHeight;

    // Create a smooth special effect to move the zone to the new size and position
    var myEffects = new Fx.Styles('zone', {duration: 250, transition: Fx.Transitions.quadInOut});
    myEffects.start({
	'left': pleft - border/2,
        'top': ptop - border/2,
	'width': width,
	'height': height
    });

  }


});
