/**
 * Giant Scalable Image Viewer (GSIV) 1.0
 *
 * Generates a draggable and zoomable viewer for images that would
 * be otherwise too large for a browser window.  Examples would include
 * maps or high resolution document scans.
 *
 * Images must be precut into tiles, such as by the accompanying tilemaker.py
 * python library.
 *
 * <div class="viewer">
 *   <div class="well"><!-- --></div>
 *   <div class="surface"><!-- --></div>
 *   <div class="controls">
 *     <a href="#" class="zoomIn">+</a>
 *     <a href="#" class="zoomOut">-</a>
 *   </div>
 * </div>
 * 
 * The "well" node is where generated IMG elements are appended. It
 * should have the CSS rule "overflow: hidden", to occlude image tiles
 * that have scrolled out of view.
 * 
 * The "surface" node is the transparent mouse-responsive layer of the
 * image viewer, and should match the well in size.
 *
 * var viewerBean = new GSIV(element, 'tiles', 256, 3, 1);
 *
 * To disable the image toolbar in IE, be sure to add the following:
 * <meta http-equiv="imagetoolbar" content="no" />
 *
 * Copyright (c) 2005 Michal Migurski <mike-gsv@teczno.com>
 *                    Dan Allen <dan.allen@mojavelinux.com>
 * 
 * Redistribution and use in source form, with or without modification,
 * are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author Michal Migurski <mike-gsv@teczno.com>
 * @author Dan Allen <dan.allen@mojavelinux.com>
 *
 * NOTE: if artifacts are appearing, then positions include half-pixels
 * TODO: additional jsdoc and package jsmin
 * TODO: Tile could be an object
 */
function GSIV(viewer, options) {

	// listeners that are notified on a move (pan) event
	this.viewerMovedListeners = [];
	// listeners that are notified on a zoom event
	this.viewerZoomedListeners = [];

	if (typeof viewer == 'string') {
		this.viewer = document.getElementById(viewer);
	}
	else {
		this.viewer = viewer;
	}

	if (typeof options == 'undefined') {
		options = {};
	}

	if (typeof options.tileUrlProvider != 'undefined' &&
		GSIV.isInstance(options.tileUrlProvider, GSIV.TileUrlProvider)) {
		this.tileUrlProvider = options.tileUrlProvider;
	}
	else {
		this.tileUrlProvider = new GSIV.TileUrlProvider(
			options.tileBaseUri ? options.tileBaseUri : GSIV.TILE_BASE_URI,
			options.tilePrefix ? options.tilePrefix : GSIV.TILE_PREFIX,
			options.tileExtension ? options.tileExtension : GSIV.TILE_EXTENSION
		);
	}

	this.tileSize = (options.tileSize ? options.tileSize : GSIV.TILE_SIZE);

	// NN
	// Image width and height + scales
	if (options.imgWidth && options.imgHeight && options.scales) {
		this.imgWidth = options.imgWidth;
		this.imgHeight = options.imgHeight;
		this.scales = options.scales;
		// NN 2008-07-17
		this.levelWidths = null;
		this.levelHeights = null;

	} else {

		// extract from the image
		var infoUrl = this.tileUrlProvider.baseUri + '/info';
		var w = 0;
		var h = 0;
		var s = [];
		// NN 2008-07-17
		var ws = [];
		var hs = [];
//alert(infoUrl) ;
		$.ajax({
			url: infoUrl,
			type: 'GET',
			dataType: 'xml',
			async: false,
			error: function() {
				alert("ERROR on " + infoUrl);
			},
			success: function(xmlr) {
// XPath doesn't work with jQuery 1.2.3
//				$(xmlr).find('/image/width').each(function() {
				$(xmlr).find('image').find('width').each(function() {
					w = $(this).text();
				});
//				$(xmlr).find('/image/height').each(function() {
				$(xmlr).find('image').find('height').each(function() {
					h = $(this).text();
				});
//				$(xmlr).find('/image/levels/level/scale').each(function() {
				$(xmlr).find('image').find('levels').find('level').find('scale').each(function() {
					s[$(this).parent().attr('id')] = $(this).text();
					
				});
				// NN 2008-07-17
				$(xmlr).find('image').find('levels').find('level').find('width').each(function() {
					ws[$(this).parent().attr('id')] = $(this).text();
					
				});
				$(xmlr).find('image').find('levels').find('level').find('height').each(function() {
					hs[$(this).parent().attr('id')] = $(this).text();
					
				});
			}
		});
		this.imgWidth = w;
		this.imgHeight = h;
		this.scales = s;
		// NN 2008-07-17
		this.levelWidths = ws;
		this.levelHeights = hs;
//alert("22 this.scales="+this.scales + "  infoUrl +"+infoUrl + " this.imgWidth="+this.imgWidth + " this.imgHeight="+ this.imgHeight ) ;

	}
	this.maxZoomLevel = this.scales.length - 1;

	// assign and do some validation on the zoom levels to ensure sanity
	this.zoomLevel = (typeof options.initialZoom == 'undefined' ? -1 : parseInt(options.initialZoom));
//	this.maxZoomLevel = (typeof options.maxZoom == 'undefined' ? 0 : Math.abs(parseInt(options.maxZoom)));
	if (this.zoomLevel > this.maxZoomLevel) {
		this.zoomLevel = this.maxZoomLevel;
	}
	this.origZoom = this.zoomLevel;

	// NN
	this.initialCoords = (options.initialCoords ? options.initialCoords : null);
	// highlightAreas: coordinates in the 1:1 (100%) scale
	this.highlightAreas = (options.highlightAreas ? options.highlightAreas : null);

	this.initialPan = (options.initialPan ? options.initialPan : GSIV.INITIAL_PAN);

	this.panFactor = GSIV.PAN_FACTOR;

	this.initialized = false;
	this.surface = null;
	this.well = null;
//	this.width = 0;
//	this.height = 0;
	this.width = this.defaultWidth = (typeof options.width == 'undefined' ? 0 : parseInt(options.width));
	this.height = this.defaultHeight = (typeof options.height == 'undefined' ? 0 : parseInt(options.height));
	this.top = 0;
	this.left = 0;
	this.x = 0;
	this.y = 0;
	this.border = -1;
	this.mark = { 'x' : 0, 'y' : 0 };
	this.pressed = false;
	this.tiles = [];
	this.cache = {};
	var blankTile = options.blankTile ? options.blankTile : GSIV.BLANK_TILE_IMAGE;
	var loadingTile = options.loadingTile ? options.loadingTile : GSIV.LOADING_TILE_IMAGE;
	this.cache['blank'] = new Image();
	this.cache['blank'].src = blankTile;
	if (blankTile != loadingTile) {
		this.cache['loading'] = new Image();
		this.cache['loading'].src = loadingTile;
	}
	else {
		this.cache['loading'] = this.cache['blank'];
	}

	// employed to throttle the number of redraws that
	// happen while the mouse is moving
	this.moveCount = 0;
	this.slideMonitor = 0;
	this.slideAcceleration = 0;

	this.moved = false;

	// NN - thumb
	this.thumb = null;
	this.thumbMark = { 'x' : 0, 'y' : 0 };
	this.thumbPressed = false;

	this.isDoubleClick = false;

	// KF - record time between mouse down and click
	this.mouseDownInterval = 0 ;

	// NN - scrollbars
	this.scrollSteps = 30;    // FOR SCROLL WHEEL
	this.scrollAmount = 200;  // FOR CLICKING A BUTTON ON THE END OF THE SCROLLBARS

	// add to viewer registry
	GSIV.VIEWERS[GSIV.VIEWERS.length] = this;
}

// project specific variables
GSIV.PROJECT_NAME = 'GSIV';
GSIV.PROJECT_VERSION = '1.0.0';
GSIV.REVISION_FLAG = '';

// CSS definition settings
GSIV.SURFACE_STYLE_CLASS = 'surface';
GSIV.WELL_STYLE_CLASS = 'well';
// NN - change class name so it doesn't clash with the new jquery slider controls
//GSIV.CONTROLS_STYLE_CLASS = 'controls'
GSIV.CONTROLS_STYLE_CLASS = 'controlsGSIV'
GSIV.TILE_STYLE_CLASS = 'tile';
// NN - thumb
GSIV.THUMBNAIL_STYLE_CLASS = 'thumbnail';

// language settings
GSIV.MSG_BEYOND_MIN_ZOOM = 'Cannot zoom out past the current level.';
GSIV.MSG_BEYOND_MAX_ZOOM = 'Cannot zoom in beyond the current level.';

// defaults if not provided as constructor options
GSIV.TILE_BASE_URI = 'tiles';
GSIV.TILE_PREFIX = 'tile-';
GSIV.TILE_EXTENSION = 'jpg';
GSIV.TILE_SIZE = 256;
GSIV.BLANK_TILE_IMAGE = 'blank.gif';
GSIV.LOADING_TILE_IMAGE = 'blank.gif';
GSIV.INITIAL_PAN = { 'x' : .5, 'y' : .5 };
GSIV.USE_LOADER_IMAGE = true;
GSIV.USE_SLIDE = true;
GSIV.USE_KEYBOARD = true;

// performance tuning variables
GSIV.MOVE_THROTTLE = 3;
GSIV.SLIDE_DELAY = 40;
GSIV.SLIDE_ACCELERATION_FACTOR = 5;

// the following are calculated settings
GSIV.DOM_ONLOAD = (navigator.userAgent.indexOf('KHTML') >= 0 ? false : true);
//GSIV.GRAB_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'pointer' : (document.attachEvent ? 'url(grab.cur)' : '-moz-grab'));
//GSIV.GRABBING_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'move' : (document.attachEvent ? 'url(grabbing.cur)' : '-moz-grabbing'));

GSIV.GRAB_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'pointer' : (document.attachEvent ? 'url(/static/ndp/gsiv-1.0.1/assets/gfx/grab.cur)' : '-moz-grab')); //kkf
GSIV.GRABBING_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'move' : (document.attachEvent ? 'url(/static/ndp/gsiv-1.0.1/assets/gfx/grabbing.cur)' : '-moz-grabbing')); //kkf

// pan factor - width and height
GSIV.PAN_FACTOR = .3;

// registry of all known viewers
GSIV.VIEWERS = [];

// utility functions
GSIV.isInstance = function(object, clazz) {
	while (object != null) {
		if (object == clazz.prototype) {
			return true;
		}

		object = object.__proto__;
	}

	return false;
}

function findPos(obj) { //kf
	var curleft = curtop = 0;
	if (obj.offsetParent) {
		curleft = obj.offsetLeft
		curtop = obj.offsetTop
		while (obj = obj.offsetParent) {
			curleft += obj.offsetLeft
			curtop += obj.offsetTop
		}
	}
	return [curleft,curtop];
}

GSIV.prototype = {

	/**
	 * Resize the viewer to fit snug inside the browser window (or frame),
	 * spacing it from the edges by the specified border.
	 *
	 * This method should be called prior to init()
	 * FIXME: option to hide viewer to prevent scrollbar interference
	 */
	fitToWindow : function(border) {
		if (typeof border != 'number' || border < 0) {
			border = 0;
		}

		this.border = border;
		var calcWidth = 0;
		var calcHeight = 0;
		if (window.innerWidth) {
			calcWidth = window.innerWidth;
			calcHeight = window.innerHeight;
		}
		else {
			calcWidth = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientWidth : document.body.clientWidth);
			calcHeight = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientHeight : document.body.clientHeight);
		}
		
//		var k1 = document.getElementById("pageC") ; //KFOCT07
		var k1 = document.getElementById("viewer") ; //NN 2008-06-23
//		var k1pos = findPos(k1) ; //KFOCT07
// NN 2008-06-23
		var k1pos;
		if (this.k1pos != null) {
			k1pos = this.k1pos;
		} else {
			k1pos = findPos(k1);
			this.k1pos = k1pos;
		}

//kf		calcWidth = Math.max(calcWidth - 2 * border - 40, 0); // NN
//kf		calcHeight = Math.max(calcHeight - 2 * border - 30, 0); // NN

//		calcWidth -= (k1pos[0] + 5) ; //KFOCT07
//		calcHeight -= (k1pos[1] + 5) ; //KFOCT07
// NN 2008-06-23
		calcWidth -= (k1pos[0]) ; //KFOCT07
		calcHeight -= (k1pos[1]) ; //KFOCT07

//		calcWidth = Math.max(calcWidth - 2 * border - 40, 0); // NN
//		calcHeight = Math.max(calcHeight - 2 * border - 30, 0); // NN

//		calcWidth = Math.max(calcWidth - 2 * border - 24, 0); // NN
//		calcHeight = Math.max(calcHeight - 2 * border - 30, 0); // NN
// NN 2008-06-23
		calcWidth = Math.max(calcWidth - 2 * border - 2, 0); // NN
		calcHeight = Math.max(calcHeight - 2 * border - 2, 0); // NN

		if (calcWidth % 2) {
			calcWidth--;
		}

		if (calcHeight % 2) {
			calcHeight--;
		}

		var splitterk = document.getElementById("MySplitter") ;  //KFOCT07
		if (splitterk != null) {
			this.width = calcWidth;
		}
		else {
		/*27nov	var rightEdge = document.getElementById("pageR") ; //KFOCT07 
			var rightPos = findPos(rightEdge) ;
			//alert("k1="+k1+" left="+k1pos[0] + "left2=" + rightPos[0]) ;
			this.width = rightPos[0] - k1pos[0] - 20 ;
		*/
		
			if (window.innerWidth) {
//				this.width = window.innerWidth - k1pos[0] - 35 ;
// NN 2008-06-23
				this.width = window.innerWidth - k1pos[0] - 2;
			}
			else {
//				this.width = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientWidth : document.body.clientWidth) - k1pos[0] - 30 ;
// NN 2008-06-23
				this.width = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientWidth : document.body.clientWidth) - k1pos[0] - 2 ;
			}
		}

		this.height = calcHeight;

		this.viewer.style.width = this.width + 'px';
		this.viewer.style.height = this.height + 'px';
		this.viewer.style.top = border + 'px';
		this.viewer.style.left = border + 'px';

		if (splitterk != null) {		//KFOCT07
			splitterHeight = calcHeight + 10 ;	//KFOCT07 set var!
			splitterk.style.height = splitterHeight + 'px' ;
			$("#MySplitter").trigger("resize");
		} 
//alert("ftw thisthumb="+this.thumb) ;
		if (this.thumb != null) this.repositionThumb() ;

	},

	init : function() {
		if (document.attachEvent) {
			document.body.ondragstart = function() { return false; }
		}
		
		if (this.width == 0 && this.height == 0) {
			this.width = this.viewer.offsetWidth;
			this.height = this.viewer.offsetHeight;
		}

		// NN work around for IE 6 problem on the home page (without fitToWindow)
		if (this.width == 0 && this.height == 0) {
			this.width = parseInt($(this.viewer).css('width'));
//			this.height = parseInt($(this.viewer).css('height')) - 220;
			this.height = parseInt($(this.viewer).css('height')) - 150;
			this.viewer.style.width = this.width + 'px';
			this.viewer.style.height = this.height + 'px';
//			this.viewer.style.top = '120px';
//			this.viewer.style.left = '0px';
		}

		// NN
		this.setupLevels();

		var fullSize = this.tileSize;
		// explicit set of zoom level
//alert("this.zoomLevel="+this.zoomLevel) ;
		if (this.zoomLevel >= 0 && this.zoomLevel <= this.maxZoomLevel) {
			// NN
//			fullSize = this.tileSize * Math.pow(2, this.zoomLevel);
			fullSize = this.levels[this.zoomLevel].fullsize.width;
		}
		// calculate the zoom level based on what fits best in window
		else {
			// NN
			this.zoomLevel = -1;
/*
			fullSize = this.tileSize / 2;
			do {
				this.zoomLevel += 1;
				fullSize *= 2;
			} while (fullSize < Math.max(this.width, this.height));
*/
			do {
				this.zoomLevel += 1;
//alert("this.zoomLevel="+this.zoomLevel+",this.levels.length="+this.levels.length) ;
				fullSize = this.levels[this.zoomLevel].fullsize.width;
			} while (fullSize < Math.max(this.width, this.height));

		}

		// NN
		if (this.initialCoords != null) {
			// move to the initial coordinates
			this.origX = this.x = this.initialCoords.x - this.levels[this.zoomLevel].offset.x;
			this.origY = this.y = this.initialCoords.y - this.levels[this.zoomLevel].offset.y;
		} else {
			// move top level up and to the left so that the image is centered
			this.x = Math.floor((fullSize - this.width) * -this.initialPan.x);
			this.y = Math.floor((fullSize - this.height) * -this.initialPan.y);
		}

		// offset of viewer in the window
//		for (var node = this.viewer; node; node = node.offsetParent) {
//			this.top += node.offsetTop;
//			this.left += node.offsetLeft;
//		}

		this.calculateViewerOffset();

		for (var child = this.viewer.firstChild; child; child = child.nextSibling) {
			if (child.className == GSIV.SURFACE_STYLE_CLASS) {
				this.surface = child;
				child.backingBean = this;
			}
			else if (child.className == GSIV.WELL_STYLE_CLASS) {
				this.well = child;
				child.backingBean = this;
			}
			else if (child.className == GSIV.CONTROLS_STYLE_CLASS) {
				for (var control = child.firstChild; control; control = control.nextSibling) {
					if (control.className) {
						control.onclick = GSIV[control.className + 'Handler'];
					}
				}
			}
			// NN - thumb
			else if (child.className == GSIV.THUMBNAIL_STYLE_CLASS) {
				this.thumb = child;
				child.backingBean = this;
				this.setupThumb();
			}
		}

		this.viewer.backingBean = this;

		// NN - scrollbars
		this.setupScrollBars();
		this.setupZoomSlider();

//alert("b1") ;
		this.surface.style.cursor = GSIV.GRAB_MOUSE_CURSOR;
//alert("b2=" + GSIV.GRAB_MOUSE_CURSOR) ;
		this.prepareTiles();
		this.initialized = true;

		// NN
		this.viewer.style.width = '' + this.width + 'px';
		this.viewer.style.height = '' + this.height + 'px';

		// NN - thumbcontrol
		if (this.thumb != null) {
			this.setupThumbControl();
		}

		// NN
		this.activate(false);
	},

	// NN
	calculateViewerOffset : function() {
		// offset of viewer in the window
		this.left = 0;
		this.top = 0;
		for (var node = this.viewer; node; node = node.offsetParent) {
			this.top += node.offsetTop;
			this.left += node.offsetLeft;
		}
	},

	// NN
	setupLevels : function() {
		// Calculate sizes for each level
		this.levels = [];
		for (var i = 0; i < this.scales.length; ++i) {
			// NN 2008-07-17
//			var w = Math.round(this.imgWidth * this.scales[i]);
//			var h = Math.round(this.imgHeight * this.scales[i]);
			var w = (this.levelWidths == null) ? Math.round(this.imgWidth * this.scales[i]) : this.levelWidths[i];
			var h = (this.levelHeights == null) ? Math.round(this.imgHeight * this.scales[i]) : this.levelHeights[i];
			var fw = 1;
			do {
				fw *= 2;
			} while (fw < Math.max(w, h));
			this.levels.push({ 'scale' : this.scales[i],
					   'size' : { 'width' : w, 'height' : h },
					   'fullsize' : { 'width' : fw, 'height' : fw },
					   'offset' : { 'x' : Math.floor((fw - w) / 2) ,
							'y' : Math.floor((fw - h) / 2) }
					 });
		}
	},

	// NN - scrollbars
	recreateScrollBars : function(sbPos) {
		$('.scroll-x .slider').slider('destroy');
		$('.scroll-y .slider').slider('destroy');
		this.setupScrollBars(sbPos);
	},

	// NN - scrollbars
	setupScrollBars : function(sbPos) {
		// default to initial coordinates
		if (typeof sbPos == 'undefined') {
			sbPos = { 'x' : -this.initialCoords.x, 'y' : -this.initialCoords.y };
		}

		this.panCoords = {'x': -sbPos.x, 'y': -sbPos.y};

		var viewer = this;
		var currentLevel = this.levels[this.zoomLevel];

		// SCROLLBARS
		// Set sliders width + height
		// IMPORTANT: MUST set them before creating the sliders due to the offsetWidth and offsetHeight problem

		$('.scroll-x').width(this.width);
		$('.scroll-x .slider').width($('.scroll-x').width() - $('.scroll-x .right').width() * 2);

		$('.scroll-y').height(this.height - $('.scroll-y .down').height());
		$('.scroll-y .slider').height($('.scroll-y').height() - $('.scroll-y .down').height() * 2);

		// Create sliders
		// Horizontal slider
		this.canSlide = true;
		var sbXStart = 0;
		var sbXStop = 0;
		var sbXLastVal = 0;
		$('.scroll-x .slider').slider({
			handle: '.thumb',
			min: 0,
			max: currentLevel.size.width - this.width,
			startValue: sbPos.x,
			start: function(e, ui) {
				if (viewer.canSlide) {
					// NN - To prevent sliding over the view port
					viewer.useScrollBars = true;
					var val = (isNaN(ui.value) ? 0 : ui.value);
					sbXStart = val;
					viewer.press({'x': val, 'y' : 0});
				}
			},
			slide: function(e, ui) {
				if (viewer.canSlide && sbXStart != ui.value) {
					var val = (isNaN(ui.value) ? 0 : ui.value);
					viewer.mark.x = viewer.mark.y = 0;
					// Work around to make the moving distance absolute
					viewer.thumbMark.x = viewer.thumbMark.y = -1;
//					viewer.positionTiles({'x': -(ui.value - sbXStart), 'y' : 0}, false, false, true);
//					viewer.positionTiles({'x': -(ui.value - sbXStart), 'y' : 0});
					viewer.positionTiles({'x': -(val - sbXStart), 'y' : 0});
				}
			},
			stop: function(e, ui) {
				if (viewer.canSlide) {
					var val = (isNaN(ui.value) ? 0 : ui.value);
					viewer.mark = {'x' : 0, 'y' : 0};
					sbXStop = val;
					if (sbXStart == sbXStop) {
						viewer.release({'x': -(sbXStart -  sbXLastVal), 'y' : 0});
					} else {
						viewer.release({'x': -(sbXStop - sbXStart), 'y' : 0});
					}
					viewer.positionTiles();
//					viewer.positionTiles(viewer.mark, false, false, true);
					sbXLastVal = val;
					// NN - To prevent sliding over the view port
					viewer.useScrollBars = false;
				}

			}
		});
//		}).slider('moveTo', 1); // BUG FIX

		// Vertical slider
		var sbYStart = 0;
		var sbYStop = 0;
		var sbYLastVal = 0;
		$('.scroll-y .slider').slider({
			handle: '.thumb',
			min: 0,
			max: currentLevel.size.height - this.height,
			startValue: sbPos.y,
			start: function(e, ui) {
				if (viewer.canSlide) {
					// NN - To prevent sliding over the view port
					viewer.useScrollBars = true;
					var val = (isNaN(ui.value) ? 0 : ui.value);
					sbYStart = val;
					viewer.press({'x': 0, 'y' : val});
				}
			},
			slide: function(e, ui) {
				if (viewer.canSlide && sbYStart != ui.value) {
					var val = (isNaN(ui.value) ? 0 : ui.value);
					viewer.mark.x = viewer.mark.y = 0;
					// Work around to make the moving distance absolute
					viewer.thumbMark.x = viewer.thumbMark.y = -1;
//					viewer.positionTiles({'x': 0, 'y' : -(ui.value - sbYStart)}, false, false, true);
//					viewer.positionTiles({'x': 0, 'y' : -(ui.value - sbYStart)});
					viewer.positionTiles({'x': 0, 'y' : -(val - sbYStart)});
				}
			},
			stop: function(e, ui) {
				if (viewer.canSlide) {
					viewer.mark = {'x' : 0, 'y' : 0};
					var val = (isNaN(ui.value) ? 0 : ui.value);
					sbYStop = val;
					if (sbYStart == sbYStop) {
						viewer.release({'x': 0, 'y' : -(sbYStart - sbYLastVal)});
					} else {
						viewer.release({'x': 0, 'y' : -(sbYStop - sbYStart)});
					}
					viewer.positionTiles();
//					viewer.positionTiles(viewer.mark, false, false, true);
//					sbYLastVal = ui.value;
					sbYLastVal = val;
					// NN - To prevent sliding over the view port
					viewer.useScrollBars = false;
				}
			}
		});
//		}).slider('moveTo', 1);  // fix bug

		// END BUTTONS
		$('.scroll-y .up').click(function() {
			$('.scroll-y .slider').slider('moveTo', '-=' + viewer.scrollAmount);
		});
		$('.scroll-y .down').click(function() {
			$('.scroll-y .slider').slider('moveTo', '+=' + viewer.scrollAmount);
		});
		$('.scroll-x .right').click(function() {
			$('.scroll-x .slider').slider('moveTo', '+=' + viewer.scrollAmount);
		});
		$('.scroll-x .left').click(function() {
			$('.scroll-x .slider').slider('moveTo', '-=' + viewer.scrollAmount);
		});

		// SCROLL WHEEL
		if (!window.ActiveXObject) {  // not IE - need to check for Opera, Safari, etc... more!!!
			window.addEventListener('DOMMouseScroll', GSIV.scrollWheelHandler, false);
			// TO MAKE SURE YOU'RE USING THE SCROLL WHEEL ON THIS WINDOW
			$(this.viewer).hover(function() {
				$(this).addClass('hover');
			}, function() {
				$(this).removeClass('hover');
			});
		}

		// Size thumbs
		var w = (this.width / currentLevel.size.width) * $('.scroll-x .slider').width();
		var h = (this.height / currentLevel.size.height ) * $('.scroll-y .slider').height();
		// DISABLE THE SCROLLBAR?
		if (w > $('.scroll-x .slider').width()) {
			w = $('.scroll-x .slider').width();
		}
		if (h > $('.scroll-y .slider').height()) {
			h = $('.scroll-y .slider').height();
		}
		$('.scroll-x .thumb').width(w);
		$('.scroll-y .thumb').height(h);

		// Move sliders to their correct positions
		this.canSlide = false;
//		$('.scroll-x .slider').slider('moveTo', sbPos.x);
//		$('.scroll-y .slider').slider('moveTo', sbPos.y);
		// NN - This is to fix a bug with IE when sbPos = (0,0)
		var mx = (sbPos.x == 0) ? -1 : sbPos.x;
		var my = (sbPos.y == 0) ? -1 : sbPos.y;
		$('.scroll-x .slider').slider('moveTo', mx);
		$('.scroll-y .slider').slider('moveTo', my);
		this.canSlide = true;

		sbXLastVal = $('.scroll-x .slider').slider('value');
		sbYLastVal = $('.scroll-y .slider').slider('value');
		if (isNaN(sbXLastVal)) {
			sbXLastVal = 0;
		}
		if (isNaN(sbYLastVal)) {
			sbYLastVal = 0;
		}
	},

	// NN - scrollbars
	adjustScrollBars : function(motion) {
		// default to no motion
		if (typeof motion == 'undefined') {
			motion = { 'x' : 0, 'y' : 0 };
		}

		this.canSlide = false;
		this.panCoords.x += motion.x;
		this.panCoords.y += motion.y;
		$('.scroll-x .slider').slider('moveTo', -this.panCoords.x);
		$('.scroll-y .slider').slider('moveTo', -this.panCoords.y);
		this.canSlide = true;
	},

	// NN - scrollbars
	scrollWheel : function(direction) {
		if (!$(this.viewer).hasClass('hover')) return; // IF YOU AREN'T HOVERING OVER THE WINDOW
		var h = this.levels[this.zoomLevel].size.height;
		var moveTo = ((direction < 0) ? '-=' : '+=') + Math.round((h - this.height) / this.scrollSteps);
		$('.scroll-y .slider').slider('moveTo', moveTo);
	},

	// NN - zoom slider
	setupZoomSlider : function() {
		var viewer = this;
		this.zoomFromSlider = true;
		$('.controls .callout').hide();
		$('.controls .slider').slider({
			handle: '.thumb',
			min: 0,
			max: viewer.maxZoomLevel,
			steps: viewer.maxZoomLevel,
			startValue: viewer.zoomLevel,
			start: function(e, ui) {
				$('.controls .callout').fadeIn('fast');
			},
			slide: function(e, ui) {
				$('.controls .callout').html((Math.round(viewer.scales[ui.value] * 100)) + '%')
					.css('left', $('.controls .thumb').css('left'));
			},
			stop: function(e, ui) {
				$('.controls .callout').fadeOut();
				if (viewer.zoomFromSlider) {
					viewer.zoomToLevel(ui.value);
				}
			}
		});
		$('.controls .plus').click(function() {
			$('.controls .slider').slider('moveTo', '+=1');
		});
		$('.controls .minus').click(function() {
			var moveTo = '-=1';
			if ($('.controls .slider').slider('value') == 1) moveTo = '0';
			$('.controls .slider').slider('moveTo', moveTo);
		});
	},

	// NN
	setupHighlights : function() {
		// Remove all children
		$(this.surface).children(".highlight").each(function(i) {
			$(this).remove();
		});
		// Create a div for every highlight area
		this.highlightOnMouseMove = false;
		if (this.highlightAreas != null) {
			this.highlights = [];
			var level = this.levels[this.zoomLevel];
			var maxLevel = this.levels[this.maxZoomLevel];
			for (var i = 0; i < this.highlightAreas.length; ++i) {
				var area = this.highlightAreas[i];
				if (area == null) {
					continue;
				}

				this.addHighlight('hlarea' + i, area, level, maxLevel) ;
/*** kf
				var hlId = 'hlarea' + i;
				// scale
				var x = Math.floor(area.x * level.scale / maxLevel.scale) + level.offset.x + this.x;
				var y = Math.floor(area.y * level.scale / maxLevel.scale) + level.offset.y + this.y;
				var w = Math.floor(area.width * level.scale);
				var h = Math.floor(area.height * level.scale);
				if (area.content == null) area.content = "" ;

 				$(this.surface).append('<div id="' + hlId + '" class="highlight" style="left:' + x + 'px; top:' + y + 'px; width:' + w + 'px; height:' + h + 'px; background-color:' + area.bgcolour + '">' + area.content + '<!-- --></div>');

				this.highlights.push({
					'posx' : x,
					'posy' : y,
					'width' : area.width,
					'height' : area.height,
					'bgcolour' : area.bgcolour,
					'border' : (area.border ? area.border : ''),
					'element' : this.surface.lastChild
				});
**/
			}	
//	alert("kf") ;
		} else {
			this.highlights = null;
		}

	},

	addHighlight : function(hlId, area, level, maxLevel) {
		var areas = null;
		if (area.areas != null) {
			areas = area.areas;
		} else if (area.x != null) {
			areas = [ area ];
		}
		var bgcolour = area.bgcolour;
		var border = area.border ? area.border : '';
		var heading = area.heading ? area.heading : '';
		var mouseover = area.mouseover ? area.mouseover : false;
		var url = area.url ? area.url : '';
		var patt = new RegExp(/\bhighlight\b/);

		for (var i = 0; i < areas.length; ++i) {
			var a = areas[i];

			// scale
			var x = Math.floor(a.x * level.scale / maxLevel.scale) + level.offset.x + this.x;
			var y = Math.floor(a.y * level.scale / maxLevel.scale) + level.offset.y + this.y;
//			var w = Math.floor(a.width * level.scale);
//			var h = Math.floor(a.height * level.scale);
			var w = Math.round(a.width * level.scale);
			var h = Math.round(a.height * level.scale);
			if (a.content == null) {
				a.content = "";
			}
			if (a.styleClass == null) {
				a.styleClass = "highlight highlightOpacity";
			} else if (!patt.test(a.styleClass)) {
				a.styleClass += " highlight";
			}
// NN a.styleClass = "article";


			var id = hlId + '_' + i;
			$(this.surface).append('<div id="' + id + '" class="' + a.styleClass + '" style="left:' + x + 'px; top:' + y + 'px; width:' + w + 'px; height:' + h + 'px; background-color:' + bgcolour + '">' + a.content + '<!-- --></div>');

			var child = this.surface.lastChild;
			if (mouseover) {
// NN for later
/*
				$(child).append('<div class="background"></div>');
				$(child).hover(function(){
console.log($(this));
					$(this).addClass('hover');
				}, function() {
					$(this).removeClass('hover');
				});
*/
				$(child).hide();
				this.highlightOnMouseMove = true;
			}

			this.highlights.push({
				'id' : id,
				'x' : a.x,
				'y' : a.y,
				'posx' : x,
				'posy' : y,
				'width' : a.width,
				'height' : a.height,
				'bgcolour' : bgcolour,
				'border' : border,
				'heading' : heading,
				'mouseover' : mouseover,
				'url' : url,
				'element' : child
			});

		}

		return hlId ;
	},


	// NN - thumb
	setupThumb : function() {
		var thumbUrl = this.tileUrlProvider.baseUri + '/thumb';

		// Should use javascript/jquery to load the thumbnail and get its width and height
		// Use a dirty way - We know the thumbnail is 150 pixel maximum on each side
		var maxSize = this.levels[this.maxZoomLevel].size;
		var sc = Math.max(maxSize.width / 150, maxSize.height / 150);
		var thumbWidth = Math.round(maxSize.width / sc);
		var thumbHeight = Math.round(maxSize.height / sc);

		// NN 2008-02-27
		$(this.viewer).children('.thumbnail').empty();
		$(this.viewer).children('.thumbnail').append('<img src="' + thumbUrl + '" width="' + thumbWidth + '" height="' + thumbHeight + '"/>');

		var el = $(this.viewer).children('.thumbnail')[0];
		this.thumbImg = el.lastChild;
		this.thumbImgHeight = this.thumbImg.height ;
		this.thumbImgWidth = this.thumbImg.width;
		this.repositionThumb() ;

		// highlight areas
		if (this.highlightAreas != null) {
			var level = this.levels[this.maxZoomLevel];
			var scale = this.thumbImg.height / level.size.height;

			for (var i = 0; i < this.highlightAreas.length; ++i) {
				var area = this.highlightAreas[i];
				if (area == null) {
					continue;
				}
				var areas = null;
				if (area.areas != null) {
					areas = area.areas;
				} else if (area.x != null) {
					areas = [ area ];
				}
				var bgcolour = area.bgcolour;
				var border = area.border ? area.border : '';
				var mouseover = area.mouseover ? area.mouseover : false;
				for (j = 0; j < areas.length; ++j) {
					var a = areas[j];
					var x = Math.floor(a.x * scale);
					var y = Math.floor(a.y * scale);
					var w = Math.floor(a.width * scale);
					var h = Math.floor(a.height * scale);
 					$(this.thumb).append('<div class="highlight" style="left:' + x + 'px; top:' + y + 'px; width:' + w + 'px; height:' + h + 'px; background-color:' + bgcolour + ((border != '') ? ';border: ' + border : '') + '"><!-- --></div>');

					var child = this.thumb.lastChild;
					if (mouseover) {
						$(child).hide();
					}
				}
			}

		}

		// view area
		$(this.thumb).append('<div class="viewarea"><!-- --></div>');
		this.thumbViewArea = this.thumb.lastChild;
		this.thumbViewCoords = { 'posx' : 0, 'posy' : 0, 'width' : 0, 'height' : 0 };

		// NN 2008-02-27
		this.refreshThumb({ 'x' : 0, 'y' : 0 });

	},

	// NN - thumb
	repositionThumb : function() {
		this.thumb.style.top = (this.height - this.thumbImgHeight) + 'px';
		this.thumb.style.left = (this.width - this.thumbImgWidth) + 'px';
		this.thumb.style.width = this.thumbImgWidth + 'px';
		this.thumb.style.height = this.thumbImgHeight + 'px';

	},

	// NN - thumb
	refreshThumb : function(motion) {
		var level = this.levels[this.zoomLevel];
		var scale = this.thumbImg.height / level.size.height;

		this.thumbViewCoords.posy = Math.floor(-(this.y + level.offset.y + motion.y) * scale);
		this.thumbViewCoords.posx = Math.floor(-(this.x + level.offset.x + motion.x) * scale);
		this.thumbViewCoords.width = Math.floor(this.width * scale);
		this.thumbViewCoords.height = Math.floor(this.height * scale);

		this.thumbViewArea.style.top = this.thumbViewCoords.posy + 'px';
		this.thumbViewArea.style.left = this.thumbViewCoords.posx + 'px';
		this.thumbViewArea.style.width = this.thumbViewCoords.width + 'px';
		this.thumbViewArea.style.height = this.thumbViewCoords.height + 'px';
	},

	prepareTiles : function() {
		var rows = Math.ceil(this.height / this.tileSize) + 1;
		var cols = Math.ceil(this.width / this.tileSize) + 1;

		for (var c = 0; c < cols; c++) {
			var tileCol = [];

			for (var r = 0; r < rows; r++) {
				/**
				 * element is the DOM element associated with this tile
				 * posx/posy are the pixel offsets of the tile
				 * xIndex/yIndex are the index numbers of the tile segment
				 * qx/qy represents the quadrant location of the tile
				 */
				var tile = {
					'element' : null,
					'posx' : 0,
					'posy' : 0,
					'xIndex' : c,
					'yIndex' : r,
					'qx' : c,
					'qy' : r
				};

				tileCol.push(tile);
			}
		
			this.tiles.push(tileCol);
		}

		this.surface.onmousedown = GSIV.mousePressedHandler;
		this.surface.onmouseup = this.surface.onmouseout = GSIV.mouseReleasedHandler;
		this.surface.onclick = GSIV.mouseClickHandler;
		this.surface.ondblclick = GSIV.doubleClickHandler;
		if (GSIV.USE_KEYBOARD) {
			window.onkeypress = GSIV.keyboardMoveHandler;
			window.onkeydown = GSIV.keyboardZoomHandler;
		}

		this.positionTiles();

		// NN
		if (this.thumb != null) {
			this.thumb.onmousedown = GSIV.thumbMousePressedHandler;
			this.thumb.onmouseup = GSIV.thumbMouseReleasedHandler;

			this.thumb.style.cursor = GSIV.GRAB_MOUSE_CURSOR;	// KF 11dec07 - make grab cursor appear on IE
		}
	},

	/**
	 * Position the tiles based on the x, y coordinates of the
	 * viewer, taking into account the motion offsets, which
	 * are calculated by a motion event handler.
	 */
	positionTiles : function(motion, reset, refreshThumb) {
		// default to no motion, just setup tiles
		if (typeof motion == 'undefined') {
			motion = { 'x' : 0, 'y' : 0 };
		}

		// NN
		if (motion.x == 0 && motion.y == 0) {
			// Zoom, resize
			this.setupHighlights();
		} else if (this.highlights != null) {
			// Move
			for (var i = 0; i < this.highlights.length; ++i) {
				var area = this.highlights[i];
				var x = area.posx + motion.x;
				var y = area.posy + motion.y;

				area.element.style.left = x + 'px';
				area.element.style.top = y + 'px';

				if (this.mark.x == 0 && this.mark.y == 0 && this.thumbMark.x == 0 && this.thumbMark.y == 0) {
					area.posx = x;
					area.posy = y;
				}
			}
		}

		// NN - thumb
		if ((typeof refreshThumb == 'undefined' || refreshThumb) && this.thumb != null) {
			this.refreshThumb(motion);
		}

		for (var c = 0; c < this.tiles.length; c++) {
			for (var r = 0; r < this.tiles[c].length; r++) {
				var tile = this.tiles[c][r];

				tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
				tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;

				var visible = true;

				if (tile.posx > this.width) {
					// tile moved out of view to the right
					// consider the tile coming into view from the left
					do {
						tile.xIndex -= this.tiles.length;
						tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
					} while (tile.posx > this.width);

					if (tile.posx + this.tileSize < 0) {
						visible = false;
					}

				} else {
					// tile may have moved out of view from the left
					// if so, consider the tile coming into view from the right
					while (tile.posx < -this.tileSize) {
						tile.xIndex += this.tiles.length;
						tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
					}

					if (tile.posx > this.width) {
						visible = false;
					}
				}

				if (tile.posy > this.height) {
					// tile moved out of view to the bottom
					// consider the tile coming into view from the top
					do {
						tile.yIndex -= this.tiles[c].length;
						tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
					} while (tile.posy > this.height);

					if (tile.posy + this.tileSize < 0) {
						visible = false;
					}

				} else {
					// tile may have moved out of view to the top
					// if so, consider the tile coming into view from the bottom
					while (tile.posy < -this.tileSize) {
						tile.yIndex += this.tiles[c].length;
						tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
					}

					if (tile.posy > this.height) {
						visible = false;
					}
				}

				// initialize the image object for this quadrant
				if (!this.initialized) {
					this.assignTileImage(tile, true);
					tile.element.style.top = tile.posy + 'px';
					tile.element.style.left = tile.posx + 'px';
				}

				// display the image if visible
				if (visible) {
					this.assignTileImage(tile);
				}
				// seems to need this no matter what
				tile.element.style.top = tile.posy + 'px';
				tile.element.style.left = tile.posx + 'px';
			}
		}

		// reset the x, y coordinates of the viewer according to motion
		if (reset) {
			this.x += motion.x;
			this.y += motion.y;
		}
	},

	/**
	 * Determine the source image of the specified tile based
	 * on the zoom level and position of the tile.  If forceBlankImage
	 * is specified, the source should be automatically set to the
	 * null tile image.  This method will also setup an onload
	 * routine, delaying the appearance of the tile until it is fully
	 * loaded, if configured to do so.
	 */
	assignTileImage : function(tile, forceBlankImage) {
		var tileImgId, src;
		var useBlankImage = (forceBlankImage ? true : false);

		// check if image has been scrolled too far in any particular direction
		// and if so, use the null tile image
		if (!useBlankImage) {
			var left = tile.xIndex < 0;
			var high = tile.yIndex < 0;
			// NN
//			var right = tile.xIndex >= Math.pow(2, this.zoomLevel);
//			var low = tile.yIndex >= Math.pow(2, this.zoomLevel);
			var right = tile.xIndex * this.tileSize >= this.levels[this.zoomLevel].fullsize.width;
			var low = tile.yIndex * this.tileSize >= this.levels[this.zoomLevel].fullsize.height;
			if (high || left || low || right) {
				useBlankImage = true;
			}
		}

		if (useBlankImage) {
			tileImgId = 'blank:' + tile.qx + ':' + tile.qy;
			src = this.cache['blank'].src;
		}
		else {
			tileImgId = src = this.tileUrlProvider.assembleUrl(tile.xIndex, tile.yIndex, this.zoomLevel);
		}

		// only remove tile if identity is changing
		if (tile.element != null &&
			tile.element.parentNode != null &&
			tile.element.relativeSrc != src) {
			this.well.removeChild(tile.element);
		}

		var tileImg = this.cache[tileImgId];
		// create cache if not exist
		if (tileImg == null) {
			tileImg = this.cache[tileImgId] = this.createPrototype(src);
		}

		if (useBlankImage || !GSIV.USE_LOADER_IMAGE || tileImg.complete || (tileImg.image && tileImg.image.complete)) {
			tileImg.onload = function() {};
			if (tileImg.image) {
				tileImg.image.onload = function() {};
			}

			if (tileImg.parentNode == null) {
				tile.element = this.well.appendChild(tileImg);
			}
		}
		else {
			var loadingImgId = 'loading:' + tile.qx + ':' + tile.qy;
			var loadingImg = this.cache[loadingImgId];
			if (loadingImg == null) {
				loadingImg = this.cache[loadingImgId] = this.createPrototype(this.cache['loading'].src);
			}

			loadingImg.targetSrc = tileImgId;

			var well = this.well;
			tile.element = well.appendChild(loadingImg);
			tileImg.onload = function() {
				// make sure our destination is still present
				if (loadingImg.parentNode && loadingImg.targetSrc == tileImgId) {
					tileImg.style.top = loadingImg.style.top;
					tileImg.style.left = loadingImg.style.left;
					well.replaceChild(tileImg, loadingImg);
					tile.element = tileImg;
				}

				tileImg.onload = function() {};
				return false;
			}

			// konqueror only recognizes the onload event on an Image
			// javascript object, so we must handle that case here
			if (!GSIV.DOM_ONLOAD) {
				tileImg.image = new Image();
				tileImg.image.onload = tileImg.onload;
				tileImg.image.src = tileImg.src;
			}
		}
	},

	createPrototype : function(src) {
		var img = document.createElement('img');

		img.src = src;
		img.relativeSrc = src;
		img.className = GSIV.TILE_STYLE_CLASS;
		img.style.width = this.tileSize + 'px';
		img.style.height = this.tileSize + 'px';
		return img;
	},

	addViewerMovedListener : function(listener) {
		this.viewerMovedListeners.push(listener);
	},

	addViewerZoomedListener : function(listener) {
		this.viewerZoomedListeners.push(listener);
	},

	/**
	 * Notify listeners of a zoom event on the viewer.
	 */
	notifyViewerZoomed : function() {
		var percentage = (100/(this.maxZoomLevel + 1)) * (this.zoomLevel + 1);
		for (var i = 0; i < this.viewerZoomedListeners.length; i++) {
			this.viewerZoomedListeners[i].viewerZoomed(
				new GSIV.ZoomEvent(this.x, this.y, this.zoomLevel, percentage)
			);
		}
	},

	/**
	 * Notify listeners of a move event on the viewer.
	 */
	notifyViewerMoved : function(coords) {
		if (typeof coords == 'undefined') {
			coords = { 'x' : 0, 'y' : 0 };
		}

		for (var i = 0; i < this.viewerMovedListeners.length; i++) {
			this.viewerMovedListeners[i].viewerMoved(
				new GSIV.MoveEvent(
					this.x + (coords.x - this.mark.x),
					this.y + (coords.y - this.mark.y)
				)
			);
		}
	},

	zoom : function(direction, keepTopLeft) {
		if (typeof keepTopLeft == 'undefined') {
			keepTopLeft = false;
		}

		// ensure we are not zooming out of range
		if (this.zoomLevel + direction < 0) {
		//kf dont care	alert(GSIV.MSG_BEYOND_MIN_ZOOM);
			return;
		}
		else if (this.zoomLevel + direction > this.maxZoomLevel) {
		//kf dont care	alert(GSIV.MSG_BEYOND_MAX_ZOOM);
			return;
		}

		this.blank();

		var coords = { 'x' : Math.floor(this.width / 2), 'y' : Math.floor(this.height / 2) };

//		var before = {
//			'x' : (coords.x - this.x),
//			'y' : (coords.y - this.y)
//		};

//		var after = {
//			'x' : Math.floor(before.x * Math.pow(2, direction)),
//			'y' : Math.floor(before.y * Math.pow(2, direction))
//		};

		// NN
		var currentLevel = this.levels[this.zoomLevel];
		var nextLevel = this.levels[this.zoomLevel + direction];
		var scale = this.scales[this.zoomLevel + direction] / this.scales[this.zoomLevel];
//		var fullSizeScale = nextLevel.fullsize.width / currentLevel.fullsize.width;

		if (!keepTopLeft) {
			// Check if the centre point (coords) is inside the image
			if (this.pointExceedsBoundaries(coords)) {
				if (coords.x < this.x + currentLevel.offset.x) {
					this.x = coords.x - currentLevel.offset.x;
				} else if (coords.x > (currentLevel.size.width + this.x + currentLevel.offset.x)) {
					this.x += coords.x - (currentLevel.size.width + this.x + currentLevel.offset.x);
				}
				if (coords.y < this.y + currentLevel.offset.y) {
					this.y = coords.y - currentLevel.offset.y;
				} else if (coords.y > (currentLevel.size.height + this.y + currentLevel.offset.y)) {
					this.y += coords.y - (currentLevel.size.height + this.y + currentLevel.offset.y);
				}
			}
		}

		var imgBefore = { 'x' : this.x + currentLevel.offset.x,
				  'y' : this.y + currentLevel.offset.y};

		var before = {
			'x' : (coords.x - imgBefore.x),
			'y' : (coords.y - imgBefore.y)
		};

		var after = {
			'x' : Math.floor(before.x * scale),
			'y' : Math.floor(before.y * scale)
		};

		var imgAfter = { 'x' : coords.x - after.x,
				 'y' : coords.y - after.y};


//		this.x = coords.x - after.x;
//		this.y = coords.y - after.y;

		this.x = imgAfter.x - nextLevel.offset.x;
		this.y = imgAfter.y - nextLevel.offset.y;

		if (keepTopLeft) {
			this.x += coords.x * scale - coords.x;
			this.y += coords.y * scale - coords.y;
		}

		this.zoomLevel += direction;
		this.positionTiles();

		// NN - scrollbars
		var sbPos = { 'x' : -(this.x + nextLevel.offset.x),
			      'y' : -(this.y + nextLevel.offset.y)};
		this.recreateScrollBars(sbPos);

		this.notifyViewerZoomed();

	},

	zoomTopLeft : function(direction) {
		this.zoom(direction, true);
	},

	// NN - zoom slider
	zoomToLevel : function(level, updateSliderVal, keepTopLeft) {
		if (level < 0 || level > this.maxZoomLevel) {
			return;
		}

		var currentLevel = this.zoomLevel ;
		if (currentLevel != level) {
			if (updateSliderVal) {
				this.zoomFromSlider = false;
				$('.controls .slider').slider('moveTo', level);
				this.zoomFromSlider = true;
			}
			this.zoom(level - currentLevel, keepTopLeft) ;
		}
	},

	/** 
	 * Clear all the tiles from the well for a complete reinitialization of the
	 * viewer. At this point the viewer is not considered to be initialized.
	 */
	clear : function() {
		this.blank();
		this.initialized = false;
		this.tiles = [];
	},

	/**
	 * Remove all tiles from the well, which effectively "hides"
	 * them for a repaint.
	 */
	blank : function() {
		for (imgId in this.cache) {
			var img = this.cache[imgId];
			img.onload = function() {};
			if (img.image) {
				img.image.onload = function() {};
			}

			if (img.parentNode != null) {
				this.well.removeChild(img);
			}
		}
	},

	/**
	 * Method specifically for handling a mouse move event.  A direct
	 * movement of the viewer can be achieved by calling positionTiles() directly.
	 */
	moveViewer : function(coords) {
		this.positionTiles({ 'x' : (coords.x - this.mark.x), 'y' : (coords.y - this.mark.y) });
		this.notifyViewerMoved(coords);
	},

	pan : function(coords) {
		this.positionTiles(coords, true, true);
	},

	panUp : function() {
		this.pan({'x' : 0, 'y' : this.height * this.panFactor});
	},

	panDown : function() {
		this.pan({'x' : 0, 'y' : -this.height * this.panFactor});
	},

	panLeft : function() {
		this.pan({'x' : this.width * this.panFactor, 'y' : 0});
	},

	panRight : function() {
		this.pan({'x' : -this.width * this.panFactor, 'y' : 0});
	},

	panOriginal : function() {
		this.zoomLevel = this.origZoom;
		this.setupHighlights();
//		this.setupScrollBars();
		this.pan({'x' : -(this.x - this.origX), 'y' : -(this.y - this.origY)});
		// Adjust the slider
		if (A_SLIDERS != null) {
			A_SLIDERS[0].f_setValue(this.zoomLevel);
		}
	},

	/**
	 * Method for displaying or hiding an area when the mouse is moved in/out of that area.
	 */
	displayHighlightAreas : function(coords) {
		if (this.highlights != null && this.highlightOnMouseMove) {
			var hlPrefix = this.getHighlightIdPrefix(coords);
			if (hlPrefix == null || hlPrefix != this.highlightPrefix) {
				for (var i = 0; i < this.highlights.length; ++i) {
					var hl = this.highlights[i];
					if (hl.mouseover) {
						if (hlPrefix != null && hl.id.indexOf(hlPrefix) == 0) {
							$(hl.element).show();
						} else {
							$(hl.element).hide();
						}
					}
				}
				this.highlightPrefix = hlPrefix;
			}
		}
	},

	getHighlightId : function(coords) {
		var hlId = null;
		if (this.highlights != null && this.highlightOnMouseMove) {
			var currentLevel = this.levels[this.zoomLevel];
			var maxLevel = this.levels[this.maxZoomLevel];
			var scale = currentLevel.scale / maxLevel.scale;

			var x = Math.floor((coords.x - (this.x + currentLevel.offset.x)) / scale);
			var y = Math.floor((coords.y - (this.y + currentLevel.offset.y)) / scale);

			for (var i = 0; i < this.highlights.length && hlId == null; ++i) {
				var hl = this.highlights[i];
				if (hl.mouseover && x >= hl.x && x <= (hl.x + hl.width) && y >= hl.y && y <= (hl.y + hl.height)) {
					hlId = hl.id;
				}
			}
		}
		return hlId;
	},

	getHighlightIdPrefix : function(coords) {
		var hlPrefix = null;
		var hlId = this.getHighlightId(coords);
		if (hlId != null) {
			hlPrefix = hlId.substring(0, hlId.indexOf('_') + 1);
		}
		return hlPrefix;
	},

	/**
	 * Make the specified coords the new center of the image placement.
	 * This method is typically triggered as the result of a double-click
	 * event.  The calculation considers the distance between the center
	 * of the viewable area and the specified (viewer-relative) coordinates.
	 * If absolute is specified, treat the point as relative to the entire
	 * image, rather than only the viewable portion.
	 */
	recenter : function(coords, absolute) {
		if (absolute) {
			coords.x += this.x;
			coords.y += this.y;
		}

		var motion = {
			'x' : Math.floor((this.width / 2) - coords.x),
			'y' : Math.floor((this.height / 2) - coords.y)
		};

		if (motion.x == 0 && motion.y == 0) {
			return;
		}

		if (GSIV.USE_SLIDE) {
			var target = motion;
			var x, y;
			// handle special case of vertical movement
			if (target.x == 0) {
				x = 0;
				y = this.slideAcceleration;
			}
			else {
				var slope = Math.abs(target.y / target.x);
				x = Math.round(Math.pow(Math.pow(this.slideAcceleration, 2) / (1 + Math.pow(slope, 2)), .5));
				y = Math.round(slope * x);
			}
			
			motion = {
				'x' : Math.min(x, Math.abs(target.x)) * (target.x < 0 ? -1 : 1),
				'y' : Math.min(y, Math.abs(target.y)) * (target.y < 0 ? -1 : 1)
			}
		}

		// Make sure highlights areas are moved correctly
		this.thumbMark.x = this.thumbMark.y = 0;
		this.positionTiles(motion, true);
		this.notifyViewerMoved();

		if (!GSIV.USE_SLIDE) {
			return;
		}

		var newcoords = {
			'x' : coords.x + motion.x,
			'y' : coords.y + motion.y
		};

		var self = this;
		// TODO: use an exponential growth rather than linear (should also depend on how far we are going)
		// FIXME: this could be optimized by calling positionTiles directly perhaps
		this.slideAcceleration += GSIV.SLIDE_ACCELERATION_FACTOR;
		this.slideMonitor = setTimeout(function() { self.recenter(newcoords); }, GSIV.SLIDE_DELAY );

		// NN - scrollbars
		this.adjustScrollBars(motion);
	},

	resize : function() {
		// IE fires a premature resize event
		if (!this.initialized) {
			return;
		}

		this.viewer.style.display = 'none';
		this.clear();

		var before = {
			'x' : Math.floor(this.width / 2),
			'y' : Math.floor(this.height / 2)
		};

		if (this.border >= 0) {
			this.fitToWindow(this.border);
		}

		this.prepareTiles();

		var after = {
			'x' : Math.floor(this.width / 2),
			'y' : Math.floor(this.height / 2)
		};

		this.positionTiles();
		this.viewer.style.display = '';
		this.initialized = true;

		if (this.thumb != null) this.repositionThumb() ;  // kkf 28nov07 was this.thumb.repositionThumb()

		// NN - scrollbars
		var sbPos = { 'x' : -(this.x + this.levels[this.zoomLevel].offset.x),
			      'y' : -(this.y + this.levels[this.zoomLevel].offset.y)};
		this.recreateScrollBars(sbPos);

		this.notifyViewerMoved();

		// NN - 20080904
		this.calculateViewerOffset();

	},

	/**
	 * Resolve the coordinates from this mouse event by subtracting the
	 * offset of the viewer in the browser window (or frame).  This does
	 * take into account the scroll offset of the page.
	 */
	resolveCoordinates : function(e) {
		return {
			'x' : (e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft))) - this.left,
			'y' : (e.pageY || (e.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.top
		}
	},

	press : function(coords) {
		this.activate(true);
		this.mark = coords;
		this.mouseDownInterval = new Date().getTime() ;
	},

	release : function(coords) {
		// NN - To prevent sliding over the view port
		if (this.useScrollBars && coords.x != 0 && coords.y != 0) {
			return;
		}
		this.activate(false);
		var motion = {
			'x' : (coords.x - this.mark.x),
			'y' : (coords.y - this.mark.y)
		};

		this.x += motion.x;
		this.y += motion.y;
		this.mark = { 'x' : 0, 'y' : 0 };

		// NN
		if (this.moved) {
			this.positionTiles();
		}

		// NN - scrollbars
		this.adjustScrollBars(motion);
	},

	click : function(coords, coordsOnPage) {
		if (this.isDoubleClick) {
			return;
		}

		var kf = new Date().getTime() ;// - this.kf ;
		//alert("mc time ms="+this.mouseDownInterval + " to " + kf + "= " + (kf - this.mouseDownInterval)) ;
		if ((kf - this.mouseDownInterval) > 600) return ; // kf - dont treat this as a click if more than 800ms between mouse down and click

		var hlId = this.getHighlightId(coords);
		if (hlId != null) {
			var url = null;
			var heading = '';
			for (var i = 0; i < this.highlights.length && url == null; ++i) {
				var hl = this.highlights[i];
				if (hl.mouseover && hl.id == hlId) {
					url = hl.url;
					heading = hl.heading;
				}
			}
			if (url != null && url != '') {
				$(this.viewer).append('<div class="menu"><h4>'+heading+'</h4><a href="'+url+'">Read this Article</a></div>');
				var top = coordsOnPage.y - $(this.viewer).offset().top;
				var left = coordsOnPage.x - $(this.viewer).offset().left;
				var h = $('#viewer .menu').height();
				$('#viewer .menu').css('top', top-h-10).css('left', left-81);

//				document.location = url;
			}
		}
	},

	/**
	 * Activate the viewer into motion depending on whether the mouse is pressed or
	 * not pressed.  This method localizes the changes that must be made to the
	 * layers.
	 */
	activate : function(pressed) {
//		this.pressed = pressed;
//		this.surface.style.cursor = (pressed ? GSIV.GRABBING_MOUSE_CURSOR : GSIV.GRAB_MOUSE_CURSOR);
//		this.surface.onmousemove = (pressed ? GSIV.mouseMovedHandler : function() {});

		// NN
		var cursor = (pressed ? GSIV.GRABBING_MOUSE_CURSOR : GSIV.GRAB_MOUSE_CURSOR);
		this.pressed = pressed;
		this.surface.style.cursor = cursor;
//		this.surface.onmousemove = (pressed ? GSIV.mouseMovedHandler : function() {});
		this.surface.onmousemove = (pressed ? GSIV.mouseMovedHandler : GSIV.mouseMovedHandler1);
		if (this.highlights != null) {
			for (var i = 0; i < this.highlights.length; ++i) {
				this.highlights[i].element.style.cursor = cursor;
			}
		}
	},

	/**
	 * Check whether the specified point exceeds the boundaries of
	 * the viewer's primary image.
	 */
	pointExceedsBoundaries : function(coords) {
		// NN
//		return (coords.x < this.x ||
//			coords.y < this.y ||
//			coords.x > (this.tileSize * Math.pow(2, this.zoomLevel) + this.x) ||
//			coords.y > (this.tileSize * Math.pow(2, this.zoomLevel) + this.y));

//		return (coords.x < this.x ||
//			coords.y < this.y ||
//			coords.x > (this.levels[this.zoomLevel].fullsize.width + this.x) ||
//			coords.y > (this.levels[this.zoomLevel].fullsize.height + this.y));

		var level = this.levels[this.zoomLevel];
		return (coords.x < this.x + level.offset.x ||
			coords.y < this.y + level.offset.y ||
			coords.x > (level.size.width + this.x + level.offset.x) ||
			coords.y > (level.size.height + this.y + level.offset.y));
	},

	// QUESTION: where is the best place for this method to be invoked?
	resetSlideMotion : function() {
		if (this.slideMonitor != 0) {
			clearTimeout(this.slideMonitor);
			this.slideMonitor = 0;
		}

		this.slideAcceleration = 0;
	},

	// NN - thumb
	pixelToNumber : function(pixel) {
		var num = parseInt(pixel.replace('px', ''));
		if (isNaN(num)) {
			num = 0;
		}
		return num;
	},

	// NN - thumb
	thumbMoveViewArea : function(coords) {
		var scale = this.thumbImg.height / this.levels[this.zoomLevel].size.height;

		this.thumbViewArea.style.top = (this.thumbViewCoords.posy + coords.y - this.thumbMark.y) + 'px';
		this.thumbViewArea.style.left = (this.thumbViewCoords.posx + coords.x - this.thumbMark.x) + 'px';
//		this.thumbViewArea.style.width = this.thumbViewCoords.width + 'px';
//		this.thumbViewArea.style.height = this.thumbViewCoords.height + 'px';

		this.positionTiles({ 'x' : Math.floor(-(coords.x - this.thumbMark.x) / scale), 'y' : Math.floor(-(coords.y - this.thumbMark.y) / scale) }, false, false);
		this.notifyViewerMoved(coords);
	},

	// NN - thumb
	thumbPress : function(coords) {
		this.thumbMark = coords;
		this.thumbActivate(true);
	},

	// NN - thumb
	thumbRelease : function(coords) {
		this.thumbActivate(false);

		this.thumbViewCoords.posy += coords.y - this.thumbMark.y;
		this.thumbViewCoords.posx += coords.x - this.thumbMark.x;

		var scale = this.thumbImg.height / this.levels[this.zoomLevel].size.height;
		var motion = {
			'x' : -(coords.x - this.thumbMark.x) / scale,
			'y' : -(coords.y - this.thumbMark.y) / scale
		};

		this.x += motion.x;
		this.y += motion.y;
		this.thumbMark = { 'x' : 0, 'y' : 0 };

		// NN
		if (this.moved) {
			this.positionTiles();
		}

	},

	// NN - thumb
	thumbActivate : function(pressed) {
		if (this.thumb != null) {
			var cursor = (pressed ? GSIV.GRABBING_MOUSE_CURSOR : GSIV.GRAB_MOUSE_CURSOR);
			this.thumbViewArea.style.cursor = cursor;
			this.thumbPressed = pressed;
			this.thumb.onmousemove = (pressed ? GSIV.thumbMouseMovedHandler : function() {});
		}
	},

	// NN - thumb
	thumbResolveCoordinates : function(e) {
		var topStr, leftStr;
		if (e.target) {
			// Mozilla
			topStr = e.target.style.top;
			leftStr = e.target.style.left;
		} else if (e.srcElement) {
			// IE
			topStr = e.srcElement.style.top;
			leftStr = e.srcElement.style.left;
		} else {
			// Don't know
		}
		return {
			'x' : (e.layerX || e.offsetX) + this.pixelToNumber(leftStr),
			'y' : (e.layerY || e.offsetY) + this.pixelToNumber(topStr)
		}
	},

	// NN - thumb
	thumbPointExceedsBoundaries : function(coords) {
		return (coords.x < 0 ||
			coords.y < 0 ||
			coords.x > this.thumb.width ||
			coords.y > this.thumb.height);
	},

	// NN - thumbcontrol
	setupThumbControl : function() {
		this.thumbDisplayFlag = $.cookie('NDPThumbDisplay') == "ON";
		var imgUrl = this.thumbDisplayFlag ? thumbCloseUrl : thumbOpenUrl;
		$("#thumbcontrolimg").attr("src", imgUrl);
		if (this.thumbDisplayFlag) {
			$(this.thumb).show();
		} else {
			$(this.thumb).hide();
		}
	},

	// NN - thumbcontrol
	toggleThumb : function() {
		this.thumbDisplayFlag = !this.thumbDisplayFlag;
		// Set cookie
		$.cookie('NDPThumbDisplay', (this.thumbDisplayFlag ? 'ON' : 'OFF'), {path: '/', expires: 365});
		// Display
		this.setupThumbControl();
	}

};

GSIV.TileUrlProvider = function(baseUri, prefix, extension) {
	this.baseUri = baseUri;
	this.prefix = prefix;
	this.extension = extension;
}

GSIV.TileUrlProvider.prototype = {
	assembleUrl: function(xIndex, yIndex, zoom) {
//		return this.baseUri + '/' +
//			this.prefix + zoom + '-' + xIndex + '-' + yIndex + '.' + this.extension +
//			(GSIV.REVISION_FLAG ? '?r=' + GSIV.REVISION_FLAG : '');
		return this.baseUri + '/tile' + zoom + '-' + xIndex + '-' + yIndex;
	}
}

GSIV.mousePressedHandler = function(e) {
	e = e ? e : window.event;
	// only grab on left-click
	if (e.button < 2) {
		var self = this.backingBean;
		var coords = self.resolveCoordinates(e);
		if (self.pointExceedsBoundaries(coords)) {
			e.cancelBubble = true;
		}
		self.press(coords);
	}

	// NOTE: MANDATORY! must return false so event does not propagate to well!
	return false;
};

GSIV.mouseReleasedHandler = function(e) {
	e = e ? e : window.event;
	var self = this.backingBean;
	if (self.pressed) {
		// OPTION: could decide to move viewer only on release, right here
		self.release(self.resolveCoordinates(e));
	}

	// NN
	self.moved = false;

	if (self.highlightOnMouseMove) {
		self.highlightPrefix = null;
		self.displayHighlightAreas(self.resolveCoordinates(e));
	}

};

GSIV.mouseClickHandler = function(e) {
	e = e ? e : window.event;
	var x = e.pageX || e.clientX;
	var y = e.pageY || e.clientY;
//	var coordsOnPage = { 'x' : e.pageX, 'y' : e.pageY};
	var coordsOnPage = { 'x' : x, 'y' : y};

	var self = this.backingBean;
//	self.click(self.resolveCoordinates(e));
	var coords = self.resolveCoordinates(e);
	setTimeout(function() {self.click(coords, coordsOnPage);}, 300);
};

GSIV.mouseMovedHandler = function(e) {
	e = e ? e : window.event;
	var self = this.backingBean;
	self.moveCount++;
	if (self.moveCount % GSIV.MOVE_THROTTLE == 0) {
		self.moveViewer(self.resolveCoordinates(e));
	}

	// NN
	self.moved = true;
};

GSIV.mouseMovedHandler1 = function(e) {
	e = e ? e : window.event;
	var self = this.backingBean;
	if (self.highlightOnMouseMove) {
		// NN - Remove menu box
		$('#viewer .menu').remove();

		self.displayHighlightAreas(self.resolveCoordinates(e));
	}
};

GSIV.zoomInHandler = function(e) {
	e = e ? e : window.event;
	var self = this.parentNode.parentNode.backingBean;
	self.zoom(1);
	return false;
};

GSIV.zoomOutHandler = function(e) {
	e = e ? e : window.event;
	var self = this.parentNode.parentNode.backingBean;
	self.zoom(-1);
	return false;
};

GSIV.doubleClickHandler = function(e) {
	e = e ? e : window.event;
	var self = this.backingBean;
	self.isDoubleClick = true;
	coords = self.resolveCoordinates(e);
	if (!self.pointExceedsBoundaries(coords)) {
		// NN - move to top left instead of centre
//		coords.x += self.width / 2 - 20;
//		coords.y += self.height / 2 - 20;

		self.resetSlideMotion();
		self.recenter(coords);
	}
//	setTimeout(function() { self.zoom(1); }, GSIV.SLIDE_DELAY * 20 ); // kf schedule zoom to occur after slide has done (approx)
//	setTimeout(function() { zoomToLevel(self.zoomLevel + 1, true, true); }, GSIV.SLIDE_DELAY * 20 ); // kf schedule zoom to occur after slide has done (approx)
//	setTimeout(function() { zoomToLevel(self.zoomLevel + 1, true, true); self.isDoubleClick = false; }, GSIV.SLIDE_DELAY * 20 ); // kf schedule zoom to occur after slide has done (approx)
	setTimeout(function() { self.zoomToLevel(self.zoomLevel + 1, true, false); self.isDoubleClick = false; }, GSIV.SLIDE_DELAY * 20 ); // kf schedule zoom to occur after slide has done (approx)
	// NN - change the slider
	//self.zoom(1) ; //kf	
};

GSIV.keyboardMoveHandler = function(e) {
	e = e ? e : window.event;
	if (t == "INPUT" || t == "TEXTAREA") return ;	// kkf dont react to chars typed in input boxes...
	for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
		if (e.keyCode == 38)
				viewer.positionTiles({'x': 0,'y': -GSIV.MOVE_THROTTLE}, true);
		if (e.keyCode == 39)
				viewer.positionTiles({'x': -GSIV.MOVE_THROTTLE,'y': 0}, true);
		if (e.keyCode == 40)
				viewer.positionTiles({'x': 0,'y': GSIV.MOVE_THROTTLE}, true);
		if (e.keyCode == 37)
				viewer.positionTiles({'x': GSIV.MOVE_THROTTLE,'y': 0}, true);
	}
}

GSIV.keyboardZoomHandler = function(e) {
	e = e ? e : window.event;
	t = e.target ? e.target.tagName : e.srcElement.tagName ;
	if (t == "INPUT" || t == "TEXTAREA") return ;	// kkf dont react to chars typed in input boxes...
	for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
		if (e.keyCode == 109) {
				// NN
//				viewer.zoom(-1);
//				zoomToLevel(viewer.zoomLevel - 1, true, true);
				viewer.zoomToLevel(viewer.zoomLevel - 1, true, false);
		}
		if (e.keyCode == 107) {
				// NN
//				viewer.zoom(1);
//				zoomToLevel(viewer.zoomLevel + 1, true, true);
				viewer.zoomToLevel(viewer.zoomLevel + 1, true, false);
		}
	}
}

GSIV.MoveEvent = function(x, y) {
	this.x = x;
	this.y = y;
};

GSIV.ZoomEvent = function(x, y, level, percentage) {
	this.x = x;
	this.y = y;
	this.percentage = percentage;
	this.level = level;
};

// NN - thumb
GSIV.thumbMousePressedHandler = function(e) {
	e = e ? e : window.event;
	// only grab on left-click
	if (e.button < 2) {
		var layerClass = "";
		if (e.target) {
			// Mozilla
			layerClass = e.target.className;
		} else if (e.srcElement) {
			// IE
			layerClass = e.srcElement.className;
		} else {
			// Don't know!
		}
		var self = this.backingBean;
		var coords = self.thumbResolveCoordinates(e);
		if (self.thumbPointExceedsBoundaries(coords) || layerClass != 'viewarea') {
			e.cancelBubble = true;
		}
		else {
			self.thumbPress(coords);
		}
	}

	// NOTE: MANDATORY! must return false so event does not propagate to well!
	return false;
};

// NN - thumb
GSIV.thumbMouseReleasedHandler = function(e) {
	e = e ? e : window.event;
	var self = this.backingBean;
	if (self.thumbPressed) {
		// OPTION: could decide to move viewer only on release, right here
		self.thumbRelease(self.thumbResolveCoordinates(e));
	}

	// NN
	self.moved = false;
};

// NN - thumb
GSIV.thumbMouseMovedHandler = function(e) {
	e = e ? e : window.event;
	var self = this.backingBean;
//	self.moveCount++;
//	if (self.moveCount % GSIV.MOVE_THROTTLE == 0) {
		self.thumbMoveViewArea(self.thumbResolveCoordinates(e));
//	}

	// NN
	self.moved = true;
};

// NN - scrollbars
GSIV.scrollWheelHandler = function(e) {
	e = e ? e : window.event;
	var self = this.viewerBean;
	self.scrollWheel(e.detail);
};

// uses the callback format GSV.{className}Handler
GSIV.maximizeHandler = function(e) {
	if (maximized) {
		// HACK: remove auto-fit to window (this needs to be a function)
		viewerBean.border = -1;
		document.body.style.padding = '10px';
		document.getElementById('header').style.display = 'block';
		document.getElementById('footer').style.display = 'block';
//		document.getElementById('viewer').style.width = '100%';
//		document.getElementById('viewer').style.height = '100%';
		viewer.style.width = '100%';
		viewer.style.height = '100%';
	}
	else {
		document.body.style.padding = '0';
		document.getElementById('header').style.display = 'none';
		document.getElementById('footer').style.display = 'none';
		// HACK allow auto-fit to window (this needs to be a function)
		viewerBean.border = 0;
		viewerBean.resize();
	}
	maximized = !maximized;

}
